github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/authentication/user.go (about)

     1  // Copyright 2014 Canonical Ltd. All rights reserved.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package authentication
     5  
     6  import (
     7  	"net/http"
     8  	"time"
     9  
    10  	"github.com/juju/clock"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	"gopkg.in/juju/names.v2"
    14  	"gopkg.in/macaroon-bakery.v2-unstable/bakery"
    15  	"gopkg.in/macaroon-bakery.v2-unstable/bakery/checkers"
    16  	"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
    17  	"gopkg.in/macaroon.v2-unstable"
    18  
    19  	"github.com/juju/juju/apiserver/common"
    20  	"github.com/juju/juju/apiserver/params"
    21  	"github.com/juju/juju/state"
    22  )
    23  
    24  var logger = loggo.GetLogger("juju.apiserver.authentication")
    25  
    26  // UserAuthenticator performs authentication for local users. If a password
    27  type UserAuthenticator struct {
    28  	AgentAuthenticator
    29  
    30  	// Service holds the service that is used to mint and verify macaroons.
    31  	Service ExpirableStorageBakeryService
    32  
    33  	// Clock is used to calculate the expiry time for macaroons.
    34  	Clock clock.Clock
    35  
    36  	// LocalUserIdentityLocation holds the URL of the trusted third party
    37  	// that is used to address the is-authenticated-user third party caveat
    38  	// to for local users. This always points at the same controller
    39  	// agent that is servicing the authorisation request.
    40  	LocalUserIdentityLocation string
    41  }
    42  
    43  const (
    44  	usernameKey = "username"
    45  
    46  	// LocalLoginInteractionTimeout is how long a user has to complete
    47  	// an interactive login before it is expired.
    48  	LocalLoginInteractionTimeout = 2 * time.Minute
    49  
    50  	// TODO(axw) make this configurable via model config.
    51  	localLoginExpiryTime = 24 * time.Hour
    52  
    53  	// TODO(axw) check with cmars about this time limit. Seems a bit
    54  	// too low. Are we prompting the user every hour, or just refreshing
    55  	// the token every hour until the external IdM requires prompting
    56  	// the user?
    57  	externalLoginExpiryTime = 1 * time.Hour
    58  )
    59  
    60  var _ EntityAuthenticator = (*UserAuthenticator)(nil)
    61  
    62  // Authenticate authenticates the entity with the specified tag, and returns an
    63  // error on authentication failure.
    64  //
    65  // If and only if no password is supplied, then Authenticate will check for any
    66  // valid macaroons. Otherwise, password authentication will be performed.
    67  func (u *UserAuthenticator) Authenticate(
    68  	entityFinder EntityFinder, tag names.Tag, req params.LoginRequest,
    69  ) (state.Entity, error) {
    70  	userTag, ok := tag.(names.UserTag)
    71  	if !ok {
    72  		return nil, errors.Errorf("invalid request")
    73  	}
    74  	if req.Credentials == "" && userTag.IsLocal() {
    75  		return u.authenticateMacaroons(entityFinder, userTag, req)
    76  	}
    77  	return u.AgentAuthenticator.Authenticate(entityFinder, tag, req)
    78  }
    79  
    80  // CreateLocalLoginMacaroon creates a macaroon that may be provided to a
    81  // user as proof that they have logged in with a valid username and password.
    82  // This macaroon may then be used to obtain a discharge macaroon so that
    83  // the user can log in without presenting their password for a set amount
    84  // of time.
    85  func CreateLocalLoginMacaroon(
    86  	tag names.UserTag,
    87  	service BakeryService,
    88  	clock clock.Clock,
    89  ) (*macaroon.Macaroon, error) {
    90  	// We create the macaroon with a random ID and random root key, which
    91  	// enables multiple clients to login as the same user and obtain separate
    92  	// macaroons without having them use the same root key.
    93  	return service.NewMacaroon([]checkers.Caveat{
    94  		{Condition: "is-authenticated-user " + tag.Id()},
    95  		checkers.TimeBeforeCaveat(clock.Now().Add(LocalLoginInteractionTimeout)),
    96  	})
    97  
    98  }
    99  
   100  // CheckLocalLoginCaveat parses and checks that the given caveat string is
   101  // valid for a local login request, and returns the tag of the local user
   102  // that the caveat asserts is logged in. checkers.ErrCaveatNotRecognized will
   103  // be returned if the caveat is not recognised.
   104  func CheckLocalLoginCaveat(caveat string) (names.UserTag, error) {
   105  	var tag names.UserTag
   106  	op, rest, err := checkers.ParseCaveat(caveat)
   107  	if err != nil {
   108  		return tag, errors.Annotatef(err, "cannot parse caveat %q", caveat)
   109  	}
   110  	if op != "is-authenticated-user" {
   111  		return tag, checkers.ErrCaveatNotRecognized
   112  	}
   113  	if !names.IsValidUser(rest) {
   114  		return tag, errors.NotValidf("username %q", rest)
   115  	}
   116  	tag = names.NewUserTag(rest)
   117  	if !tag.IsLocal() {
   118  		tag = names.UserTag{}
   119  		return tag, errors.NotValidf("non-local username %q", rest)
   120  	}
   121  	return tag, nil
   122  }
   123  
   124  // CheckLocalLoginRequest checks that the given HTTP request contains at least
   125  // one valid local login macaroon minted by the given service using
   126  // CreateLocalLoginMacaroon. It returns an error with a
   127  // *bakery.VerificationError cause if the macaroon verification failed. If the
   128  // macaroon is valid, CheckLocalLoginRequest returns a list of caveats to add
   129  // to the discharge macaroon.
   130  func CheckLocalLoginRequest(
   131  	service *bakery.Service,
   132  	req *http.Request,
   133  	tag names.UserTag,
   134  	clock clock.Clock,
   135  ) ([]checkers.Caveat, error) {
   136  	_, err := httpbakery.CheckRequest(service, req, nil, checkers.CheckerFunc{
   137  		// Having a macaroon with an is-authenticated-user
   138  		// caveat is proof that the user is "logged in".
   139  		"is-authenticated-user",
   140  		func(cond, arg string) error { return nil },
   141  	})
   142  	if err != nil {
   143  		return nil, errors.Trace(err)
   144  	}
   145  	firstPartyCaveats := []checkers.Caveat{
   146  		checkers.DeclaredCaveat("username", tag.Id()),
   147  		checkers.TimeBeforeCaveat(clock.Now().Add(localLoginExpiryTime)),
   148  	}
   149  	return firstPartyCaveats, nil
   150  }
   151  
   152  func (u *UserAuthenticator) authenticateMacaroons(
   153  	entityFinder EntityFinder, tag names.UserTag, req params.LoginRequest,
   154  ) (state.Entity, error) {
   155  	// Check for a valid request macaroon.
   156  	assert := map[string]string{usernameKey: tag.Id()}
   157  	_, err := u.Service.CheckAny(req.Macaroons, assert, checkers.New(checkers.TimeBefore))
   158  	if err != nil {
   159  		cause := err
   160  		logger.Debugf("local-login macaroon authentication failed: %v", cause)
   161  		if _, ok := errors.Cause(err).(*bakery.VerificationError); !ok {
   162  			return nil, errors.Trace(err)
   163  		}
   164  
   165  		// The root keys for these macaroons are stored in MongoDB.
   166  		// Expire the documents after after a set amount of time.
   167  		expiryTime := u.Clock.Now().Add(localLoginExpiryTime)
   168  		service, err := u.Service.ExpireStorageAfter(localLoginExpiryTime)
   169  		if err != nil {
   170  			return nil, errors.Trace(err)
   171  		}
   172  
   173  		m, err := service.NewMacaroon([]checkers.Caveat{
   174  			checkers.NeedDeclaredCaveat(
   175  				checkers.Caveat{
   176  					Location:  u.LocalUserIdentityLocation,
   177  					Condition: "is-authenticated-user " + tag.Id(),
   178  				},
   179  				usernameKey,
   180  			),
   181  			checkers.TimeBeforeCaveat(expiryTime),
   182  		})
   183  
   184  		if err != nil {
   185  			return nil, errors.Annotate(err, "cannot create macaroon")
   186  		}
   187  		return nil, &common.DischargeRequiredError{
   188  			Cause:    cause,
   189  			Macaroon: m,
   190  		}
   191  	}
   192  	entity, err := entityFinder.FindEntity(tag)
   193  	if errors.IsNotFound(err) {
   194  		logger.Debugf("entity %s not found", tag.String())
   195  		return nil, errors.Trace(common.ErrBadCreds)
   196  	} else if err != nil {
   197  		return nil, errors.Trace(err)
   198  	}
   199  	return entity, nil
   200  }
   201  
   202  // ExternalMacaroonAuthenticator performs authentication for external users using
   203  // macaroons. If the authentication fails because provided macaroons are invalid,
   204  // and macaroon authentiction is enabled, it will return a *common.DischargeRequiredError
   205  // holding a macaroon to be discharged.
   206  type ExternalMacaroonAuthenticator struct {
   207  	// Service holds the service that is
   208  	// used to verify macaroon authorization.
   209  	Service BakeryService
   210  
   211  	// Macaroon guards macaroon-authentication-based access
   212  	// to the APIs. Appropriate caveats will be added before
   213  	// sending it to a client.
   214  	Macaroon *macaroon.Macaroon
   215  
   216  	// IdentityLocation holds the URL of the trusted third party
   217  	// that is used to address the is-authenticated-user
   218  	// third party caveat to.
   219  	IdentityLocation string
   220  }
   221  
   222  var _ EntityAuthenticator = (*ExternalMacaroonAuthenticator)(nil)
   223  
   224  func (m *ExternalMacaroonAuthenticator) newDischargeRequiredError(cause error) error {
   225  	if m.Service == nil || m.Macaroon == nil {
   226  		return errors.Trace(cause)
   227  	}
   228  	mac := m.Macaroon.Clone()
   229  	// TODO(fwereade): 2016-03-17 lp:1558657
   230  	expiryTime := time.Now().Add(externalLoginExpiryTime)
   231  	if err := addMacaroonTimeBeforeCaveat(m.Service, mac, expiryTime); err != nil {
   232  		return errors.Annotatef(err, "cannot create macaroon")
   233  	}
   234  	err := m.Service.AddCaveat(mac, checkers.NeedDeclaredCaveat(
   235  		checkers.Caveat{
   236  			Location:  m.IdentityLocation,
   237  			Condition: "is-authenticated-user",
   238  		},
   239  		usernameKey,
   240  	))
   241  	if err != nil {
   242  		return errors.Annotatef(err, "cannot create macaroon")
   243  	}
   244  	return &common.DischargeRequiredError{
   245  		Cause:    cause,
   246  		Macaroon: mac,
   247  	}
   248  }
   249  
   250  // Authenticate authenticates the provided entity. If there is no macaroon provided, it will
   251  // return a *DischargeRequiredError containing a macaroon that can be used to grant access.
   252  func (m *ExternalMacaroonAuthenticator) Authenticate(entityFinder EntityFinder, _ names.Tag, req params.LoginRequest) (state.Entity, error) {
   253  	declared, err := m.Service.CheckAny(req.Macaroons, nil, checkers.New(checkers.TimeBefore))
   254  	if _, ok := errors.Cause(err).(*bakery.VerificationError); ok {
   255  		return nil, m.newDischargeRequiredError(err)
   256  	}
   257  	if err != nil {
   258  		return nil, errors.Trace(err)
   259  	}
   260  	username := declared[usernameKey]
   261  	var tag names.UserTag
   262  	if names.IsValidUserName(username) {
   263  		// The name is a local name without an explicit @local suffix.
   264  		// In this case, for compatibility with 3rd parties that don't
   265  		// care to add their own domain, we add an @external domain
   266  		// to ensure there is no confusion between local and external
   267  		// users.
   268  		// TODO(rog) remove this logic when deployed dischargers
   269  		// always add an @ domain.
   270  		tag = names.NewLocalUserTag(username).WithDomain("external")
   271  	} else {
   272  		// We have a name with an explicit domain (or an invalid user name).
   273  		if !names.IsValidUser(username) {
   274  			return nil, errors.Errorf("%q is an invalid user name", username)
   275  		}
   276  		tag = names.NewUserTag(username)
   277  		if tag.IsLocal() {
   278  			return nil, errors.Errorf("external identity provider has provided ostensibly local name %q", username)
   279  		}
   280  	}
   281  	entity, err := entityFinder.FindEntity(tag)
   282  	if errors.IsNotFound(err) {
   283  		return nil, errors.Trace(common.ErrBadCreds)
   284  	} else if err != nil {
   285  		return nil, errors.Trace(err)
   286  	}
   287  	return entity, nil
   288  }
   289  
   290  func addMacaroonTimeBeforeCaveat(svc BakeryService, m *macaroon.Macaroon, t time.Time) error {
   291  	return svc.AddCaveat(m, checkers.TimeBeforeCaveat(t))
   292  }
   293  
   294  // BakeryService defines the subset of bakery.Service
   295  // that we require for authentication.
   296  type BakeryService interface {
   297  	AddCaveat(*macaroon.Macaroon, checkers.Caveat) error
   298  	CheckAny([]macaroon.Slice, map[string]string, checkers.Checker) (map[string]string, error)
   299  	NewMacaroon([]checkers.Caveat) (*macaroon.Macaroon, error)
   300  }
   301  
   302  // ExpirableStorageBakeryService extends BakeryService
   303  // with the ExpireStorageAfter method so that root keys are
   304  // removed from storage at that time.
   305  type ExpirableStorageBakeryService interface {
   306  	BakeryService
   307  
   308  	// ExpireStorageAfter returns a new ExpirableStorageBakeryService with
   309  	// a store that will expire items added to it at the specified time.
   310  	ExpireStorageAfter(time.Duration) (ExpirableStorageBakeryService, error)
   311  }