github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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/errors"
    11  	"github.com/juju/loggo"
    12  	"github.com/juju/utils/clock"
    13  	"gopkg.in/juju/names.v2"
    14  	"gopkg.in/macaroon-bakery.v1/bakery"
    15  	"gopkg.in/macaroon-bakery.v1/bakery/checkers"
    16  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    17  	"gopkg.in/macaroon.v1"
    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("", nil, []checkers.Caveat{
    94  		{Condition: "is-authenticated-user " + tag.Id()},
    95  		checkers.TimeBeforeCaveat(clock.Now().Add(LocalLoginInteractionTimeout)),
    96  	})
    97  }
    98  
    99  // CheckLocalLoginCaveat parses and checks that the given caveat string is
   100  // valid for a local login request, and returns the tag of the local user
   101  // that the caveat asserts is logged in. checkers.ErrCaveatNotRecognized will
   102  // be returned if the caveat is not recognised.
   103  func CheckLocalLoginCaveat(caveat string) (names.UserTag, error) {
   104  	var tag names.UserTag
   105  	op, rest, err := checkers.ParseCaveat(caveat)
   106  	if err != nil {
   107  		return tag, errors.Annotatef(err, "cannot parse caveat %q", caveat)
   108  	}
   109  	if op != "is-authenticated-user" {
   110  		return tag, checkers.ErrCaveatNotRecognized
   111  	}
   112  	if !names.IsValidUser(rest) {
   113  		return tag, errors.NotValidf("username %q", rest)
   114  	}
   115  	tag = names.NewUserTag(rest)
   116  	if !tag.IsLocal() {
   117  		tag = names.UserTag{}
   118  		return tag, errors.NotValidf("non-local username %q", rest)
   119  	}
   120  	return tag, nil
   121  }
   122  
   123  // CheckLocalLoginRequest checks that the given HTTP request contains at least
   124  // one valid local login macaroon minted by the given service using
   125  // CreateLocalLoginMacaroon. It returns an error with a
   126  // *bakery.VerificationError cause if the macaroon verification failed. If the
   127  // macaroon is valid, CheckLocalLoginRequest returns a list of caveats to add
   128  // to the discharge macaroon.
   129  func CheckLocalLoginRequest(
   130  	service *bakery.Service,
   131  	req *http.Request,
   132  	tag names.UserTag,
   133  	clock clock.Clock,
   134  ) ([]checkers.Caveat, error) {
   135  	_, err := httpbakery.CheckRequest(service, req, nil, checkers.CheckerFunc{
   136  		// Having a macaroon with an is-authenticated-user
   137  		// caveat is proof that the user is "logged in".
   138  		"is-authenticated-user",
   139  		func(cond, arg string) error { return nil },
   140  	})
   141  	if err != nil {
   142  		return nil, errors.Trace(err)
   143  	}
   144  	firstPartyCaveats := []checkers.Caveat{
   145  		checkers.DeclaredCaveat("username", tag.Id()),
   146  		checkers.TimeBeforeCaveat(clock.Now().Add(localLoginExpiryTime)),
   147  	}
   148  	return firstPartyCaveats, nil
   149  }
   150  
   151  func (u *UserAuthenticator) authenticateMacaroons(
   152  	entityFinder EntityFinder, tag names.UserTag, req params.LoginRequest,
   153  ) (state.Entity, error) {
   154  	// Check for a valid request macaroon.
   155  	assert := map[string]string{usernameKey: tag.Id()}
   156  	_, err := u.Service.CheckAny(req.Macaroons, assert, checkers.New(checkers.TimeBefore))
   157  	if err != nil {
   158  		cause := err
   159  		logger.Debugf("local-login macaroon authentication failed: %v", cause)
   160  		if _, ok := errors.Cause(err).(*bakery.VerificationError); !ok {
   161  			return nil, errors.Trace(err)
   162  		}
   163  
   164  		// The root keys for these macaroons are stored in MongoDB.
   165  		// Expire the documents after after a set amount of time.
   166  		expiryTime := u.Clock.Now().Add(localLoginExpiryTime)
   167  		service, err := u.Service.ExpireStorageAt(expiryTime)
   168  		if err != nil {
   169  			return nil, errors.Trace(err)
   170  		}
   171  
   172  		m, err := service.NewMacaroon("", nil, []checkers.Caveat{
   173  			checkers.NeedDeclaredCaveat(
   174  				checkers.Caveat{
   175  					Location:  u.LocalUserIdentityLocation,
   176  					Condition: "is-authenticated-user " + tag.Id(),
   177  				},
   178  				usernameKey,
   179  			),
   180  			checkers.TimeBeforeCaveat(expiryTime),
   181  		})
   182  		if err != nil {
   183  			return nil, errors.Annotate(err, "cannot create macaroon")
   184  		}
   185  		return nil, &common.DischargeRequiredError{
   186  			Cause:    cause,
   187  			Macaroon: m,
   188  		}
   189  	}
   190  	entity, err := entityFinder.FindEntity(tag)
   191  	if errors.IsNotFound(err) {
   192  		logger.Debugf("entity %s not found", tag.String())
   193  		return nil, errors.Trace(common.ErrBadCreds)
   194  	} else if err != nil {
   195  		return nil, errors.Trace(err)
   196  	}
   197  	return entity, nil
   198  }
   199  
   200  // ExternalMacaroonAuthenticator performs authentication for external users using
   201  // macaroons. If the authentication fails because provided macaroons are invalid,
   202  // and macaroon authentiction is enabled, it will return a *common.DischargeRequiredError
   203  // holding a macaroon to be discharged.
   204  type ExternalMacaroonAuthenticator struct {
   205  	// Service holds the service that is
   206  	// used to verify macaroon authorization.
   207  	Service BakeryService
   208  
   209  	// Macaroon guards macaroon-authentication-based access
   210  	// to the APIs. Appropriate caveats will be added before
   211  	// sending it to a client.
   212  	Macaroon *macaroon.Macaroon
   213  
   214  	// IdentityLocation holds the URL of the trusted third party
   215  	// that is used to address the is-authenticated-user
   216  	// third party caveat to.
   217  	IdentityLocation string
   218  }
   219  
   220  var _ EntityAuthenticator = (*ExternalMacaroonAuthenticator)(nil)
   221  
   222  func (m *ExternalMacaroonAuthenticator) newDischargeRequiredError(cause error) error {
   223  	if m.Service == nil || m.Macaroon == nil {
   224  		return errors.Trace(cause)
   225  	}
   226  	mac := m.Macaroon.Clone()
   227  	// TODO(fwereade): 2016-03-17 lp:1558657
   228  	expiryTime := time.Now().Add(externalLoginExpiryTime)
   229  	if err := addMacaroonTimeBeforeCaveat(m.Service, mac, expiryTime); err != nil {
   230  		return errors.Annotatef(err, "cannot create macaroon")
   231  	}
   232  	err := m.Service.AddCaveat(mac, checkers.NeedDeclaredCaveat(
   233  		checkers.Caveat{
   234  			Location:  m.IdentityLocation,
   235  			Condition: "is-authenticated-user",
   236  		},
   237  		usernameKey,
   238  	))
   239  	if err != nil {
   240  		return errors.Annotatef(err, "cannot create macaroon")
   241  	}
   242  	return &common.DischargeRequiredError{
   243  		Cause:    cause,
   244  		Macaroon: mac,
   245  	}
   246  }
   247  
   248  // Authenticate authenticates the provided entity. If there is no macaroon provided, it will
   249  // return a *DischargeRequiredError containing a macaroon that can be used to grant access.
   250  func (m *ExternalMacaroonAuthenticator) Authenticate(entityFinder EntityFinder, _ names.Tag, req params.LoginRequest) (state.Entity, error) {
   251  	declared, err := m.Service.CheckAny(req.Macaroons, nil, checkers.New(checkers.TimeBefore))
   252  	if _, ok := errors.Cause(err).(*bakery.VerificationError); ok {
   253  		return nil, m.newDischargeRequiredError(err)
   254  	}
   255  	if err != nil {
   256  		return nil, errors.Trace(err)
   257  	}
   258  	username := declared[usernameKey]
   259  	var tag names.UserTag
   260  	if names.IsValidUserName(username) {
   261  		// The name is a local name without an explicit @local suffix.
   262  		// In this case, for compatibility with 3rd parties that don't
   263  		// care to add their own domain, we add an @external domain
   264  		// to ensure there is no confusion between local and external
   265  		// users.
   266  		// TODO(rog) remove this logic when deployed dischargers
   267  		// always add an @ domain.
   268  		tag = names.NewLocalUserTag(username).WithDomain("external")
   269  	} else {
   270  		// We have a name with an explicit domain (or an invalid user name).
   271  		if !names.IsValidUser(username) {
   272  			return nil, errors.Errorf("%q is an invalid user name", username)
   273  		}
   274  		tag = names.NewUserTag(username)
   275  		if tag.IsLocal() {
   276  			return nil, errors.Errorf("external identity provider has provided ostensibly local name %q", username)
   277  		}
   278  	}
   279  	entity, err := entityFinder.FindEntity(tag)
   280  	if errors.IsNotFound(err) {
   281  		return nil, errors.Trace(common.ErrBadCreds)
   282  	} else if err != nil {
   283  		return nil, errors.Trace(err)
   284  	}
   285  	return entity, nil
   286  }
   287  
   288  func addMacaroonTimeBeforeCaveat(svc BakeryService, m *macaroon.Macaroon, t time.Time) error {
   289  	return svc.AddCaveat(m, checkers.TimeBeforeCaveat(t))
   290  }
   291  
   292  // BakeryService defines the subset of bakery.Service
   293  // that we require for authentication.
   294  type BakeryService interface {
   295  	AddCaveat(*macaroon.Macaroon, checkers.Caveat) error
   296  	CheckAny([]macaroon.Slice, map[string]string, checkers.Checker) (map[string]string, error)
   297  	NewMacaroon(string, []byte, []checkers.Caveat) (*macaroon.Macaroon, error)
   298  }
   299  
   300  // ExpirableStorageBakeryService extends BakeryService
   301  // with the ExpireStorageAt method so that root keys are
   302  // removed from storage at that time.
   303  type ExpirableStorageBakeryService interface {
   304  	BakeryService
   305  
   306  	// ExpireStorageAt returns a new ExpirableStorageBakeryService with
   307  	// a store that will expire items added to it at the specified time.
   308  	ExpireStorageAt(time.Time) (ExpirableStorageBakeryService, error)
   309  }