pkg/auth/user_login.go (130 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package auth
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-openapi/runtime"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/elastic/cloud-sdk-go/pkg/api/apierror"
"github.com/elastic/cloud-sdk-go/pkg/client"
"github.com/elastic/cloud-sdk-go/pkg/client/authentication"
"github.com/elastic/cloud-sdk-go/pkg/models"
"github.com/elastic/cloud-sdk-go/pkg/multierror"
"github.com/elastic/cloud-sdk-go/pkg/util/ec"
)
var (
defaultRefreshTickerTime = time.Minute * 1
errLoginClientEmpty = errors.New("auth: login client cannot be empty")
)
// UserLogin uses a user's username and password to login against the Login
// API Endpoint. Doing so obtains a JWT token which is then persisted in the
// token field, guarded by a mutex.
// This is a form of user authentication, but API Keys are still the preferred
// authentication mechanism.
type UserLogin struct {
Username, Password string
Holder TokenHandler
}
// NewUserLogin creates a UserLogin from a username and password. It does not
// automatically login against the API until Auth() is called.
func NewUserLogin(username, password string) (*UserLogin, error) {
userLogin := UserLogin{Username: username, Password: password}
if err := userLogin.Validate(); err != nil {
return nil, err
}
if userLogin.Holder == nil {
userLogin.Holder = new(GenericHolder)
}
return &userLogin, nil
}
// Validate ensures the validity of the data container.
func (t *UserLogin) Validate() error {
var merr = multierror.NewPrefixed("auth")
if t.Username == "" {
merr = merr.Append(errors.New("username must not be empty"))
}
if t.Password == "" {
merr = merr.Append(errors.New("password must not be empty"))
}
return merr.ErrorOrNil()
}
// RefreshTokenParams is used to refresh a bearer token, which is necessary
// before its validity expires.
type RefreshTokenParams struct {
Client *client.Rest
Frequency time.Duration
ErrorDevice io.Writer
InterruptChannel chan os.Signal
}
// Validate ensures that the parameters are valid.
func (params *RefreshTokenParams) Validate() error {
params.fillValues()
var merr = multierror.NewPrefixed("auth")
if params.ErrorDevice == nil {
merr = merr.Append(errors.New("errorDevice cannot be nil"))
}
if params.Client == nil {
merr = merr.Append(errors.New("rest client cannot be nil"))
}
return merr.ErrorOrNil()
}
// fillValues sets the default values for the structure.
func (params *RefreshTokenParams) fillValues() {
if params.Frequency.Nanoseconds() == 0 {
params.Frequency = defaultRefreshTickerTime
}
}
// Login calls the authentication/login endpoint with a username and password
// persisting the returned token.
func (t *UserLogin) Login(c *client.Rest) error {
if c == nil {
return errLoginClientEmpty
}
res, err := c.Authentication.Login(authentication.NewLoginParams().
WithBody(&models.LoginRequest{
Username: ec.String(t.Username),
Password: ec.String(t.Password),
}),
nil,
)
if err != nil {
return multierror.NewPrefixed("failed to login with user/password", apierror.Wrap(err))
}
return t.Holder.Update(*res.Payload.Token)
}
// AuthenticateRequest authenticates a runtime.ClientRequest. Implements the
// runtime.ClientAuthInfoWriter interface using the JWT Bearer token.
func (t *UserLogin) AuthenticateRequest(c runtime.ClientRequest, r strfmt.Registry) error {
return httptransport.BearerToken(t.Holder.Token()).AuthenticateRequest(c, r)
}
// AuthRequest adds the Authorization header to an http.Request
func (t *UserLogin) AuthRequest(req *http.Request) *http.Request {
req.Header.Add("Authorization", "Bearer "+t.Holder.Token())
return req
}
// RefreshToken creates a goroutine which will run in the background refreshing
// the token every Frequency. It does not refresh the token until the first
// period has passed.
func (t *UserLogin) RefreshToken(params RefreshTokenParams) error {
if err := params.Validate(); err != nil {
return err
}
if params.InterruptChannel == nil {
params.InterruptChannel = make(chan os.Signal, 1)
}
signal.Notify(params.InterruptChannel, os.Interrupt, syscall.SIGTERM)
go func() {
ticker := time.NewTicker(params.Frequency)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := t.RefreshTokenOnce(params.Client); err != nil {
fmt.Fprintln(params.ErrorDevice, err)
continue
}
case <-params.InterruptChannel:
return
}
}
}()
return nil
}
// RefreshTokenOnce refreshRefreshTokenOncees the current JWT token once.
func (t *UserLogin) RefreshTokenOnce(c *client.Rest) error {
if c == nil {
return errLoginClientEmpty
}
res, err := c.Authentication.RefreshToken(
authentication.NewRefreshTokenParams(), t,
)
if err != nil {
return multierror.NewPrefixed("failed to refresh the loaded token", apierror.Wrap(err))
}
return t.Holder.Update(*res.Payload.Token)
}