github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	"time"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/loggo"
    11  	"github.com/juju/names"
    12  	"github.com/juju/utils/clock"
    13  	"gopkg.in/macaroon-bakery.v1/bakery"
    14  	"gopkg.in/macaroon-bakery.v1/bakery/checkers"
    15  	"gopkg.in/macaroon.v1"
    16  
    17  	"github.com/juju/juju/apiserver/common"
    18  	"github.com/juju/juju/apiserver/params"
    19  	"github.com/juju/juju/state"
    20  )
    21  
    22  var logger = loggo.GetLogger("juju.apiserver.authentication")
    23  
    24  // UserAuthenticator performs authentication for local users. If a password
    25  type UserAuthenticator struct {
    26  	AgentAuthenticator
    27  
    28  	// Service holds the service that is used to mint and verify macaroons.
    29  	Service ExpirableStorageBakeryService
    30  
    31  	// Clock is used to calculate the expiry time for macaroons.
    32  	Clock clock.Clock
    33  }
    34  
    35  const (
    36  	usernameKey = "username"
    37  
    38  	// TODO(axw) make this configurable via model config.
    39  	localLoginExpiryTime = 24 * time.Hour
    40  
    41  	// TODO(axw) check with cmars about this time limit. Seems a bit
    42  	// too low. Are we prompting the user every hour, or just refreshing
    43  	// the token every hour until the external IdM requires prompting
    44  	// the user?
    45  	externalLoginExpiryTime = 1 * time.Hour
    46  )
    47  
    48  var _ EntityAuthenticator = (*UserAuthenticator)(nil)
    49  
    50  // Authenticate authenticates the entity with the specified tag, and returns an
    51  // error on authentication failure.
    52  //
    53  // If and only if no password is supplied, then Authenticate will check for any
    54  // valid macaroons. Otherwise, password authentication will be performed.
    55  func (u *UserAuthenticator) Authenticate(
    56  	entityFinder EntityFinder, tag names.Tag, req params.LoginRequest,
    57  ) (state.Entity, error) {
    58  	userTag, ok := tag.(names.UserTag)
    59  	if !ok {
    60  		return nil, errors.Errorf("invalid request")
    61  	}
    62  	if req.Credentials == "" && userTag.IsLocal() {
    63  		return u.authenticateMacaroons(entityFinder, userTag, req)
    64  	}
    65  	return u.AgentAuthenticator.Authenticate(entityFinder, tag, req)
    66  }
    67  
    68  // CreateLocalLoginMacaroon creates a time-limited macaroon for a local user
    69  // to log into the controller with. The macaroon will be valid for use with
    70  // UserAuthenticator.Authenticate until the time limit expires, or the Juju
    71  // controller agent restarts.
    72  //
    73  // NOTE(axw) this method will generate a key for a previously unseen user,
    74  // and store it in the bakery.Service's storage. Callers should first ensure
    75  // the user is valid before calling this, to avoid filling storage with keys
    76  // for invalid users.
    77  func (u *UserAuthenticator) CreateLocalLoginMacaroon(tag names.UserTag) (*macaroon.Macaroon, error) {
    78  
    79  	expiryTime := u.Clock.Now().Add(localLoginExpiryTime)
    80  
    81  	// Ensure that the private key that we generate and store will be
    82  	// removed from storage once the expiry time has elapsed.
    83  	bakeryService, err := u.Service.ExpireStorageAt(expiryTime)
    84  	if err != nil {
    85  		return nil, errors.Trace(err)
    86  	}
    87  
    88  	// We create the macaroon with a random ID and random root key, which
    89  	// enables multiple clients to login as the same user and obtain separate
    90  	// macaroons without having them use the same root key.
    91  	m, err := bakeryService.NewMacaroon("", nil, []checkers.Caveat{
    92  		// The macaroon may only be used to log in as the user
    93  		// specified by the tag passed to CreateLocalUserMacaroon.
    94  		checkers.DeclaredCaveat(usernameKey, tag.Canonical()),
    95  	})
    96  	if err != nil {
    97  		return nil, errors.Annotate(err, "cannot create macaroon")
    98  	}
    99  	if err := addMacaroonTimeBeforeCaveat(bakeryService, m, expiryTime); err != nil {
   100  		return nil, errors.Trace(err)
   101  	}
   102  	return m, nil
   103  }
   104  
   105  func (u *UserAuthenticator) authenticateMacaroons(
   106  	entityFinder EntityFinder, tag names.UserTag, req params.LoginRequest,
   107  ) (state.Entity, error) {
   108  	// Check for a valid request macaroon.
   109  	assert := map[string]string{usernameKey: tag.Canonical()}
   110  	_, err := u.Service.CheckAny(req.Macaroons, assert, checkers.New(checkers.TimeBefore))
   111  	if err != nil {
   112  		logger.Debugf("local-login macaroon authentication failed: %v", err)
   113  		return nil, errors.Trace(common.ErrBadCreds)
   114  	}
   115  	entity, err := entityFinder.FindEntity(tag)
   116  	if errors.IsNotFound(err) {
   117  		return nil, errors.Trace(common.ErrBadCreds)
   118  	} else if err != nil {
   119  		return nil, errors.Trace(err)
   120  	}
   121  	return entity, nil
   122  }
   123  
   124  // ExternalMacaroonAuthenticator performs authentication for external users using
   125  // macaroons. If the authentication fails because provided macaroons are invalid,
   126  // and macaroon authentiction is enabled, it will return a *common.DischargeRequiredError
   127  // holding a macaroon to be discharged.
   128  type ExternalMacaroonAuthenticator struct {
   129  	// Service holds the service that is
   130  	// used to verify macaroon authorization.
   131  	Service BakeryService
   132  
   133  	// Macaroon guards macaroon-authentication-based access
   134  	// to the APIs. Appropriate caveats will be added before
   135  	// sending it to a client.
   136  	Macaroon *macaroon.Macaroon
   137  
   138  	// IdentityLocation holds the URL of the trusted third party
   139  	// that is used to address the is-authenticated-user
   140  	// third party caveat to.
   141  	IdentityLocation string
   142  }
   143  
   144  var _ EntityAuthenticator = (*ExternalMacaroonAuthenticator)(nil)
   145  
   146  func (m *ExternalMacaroonAuthenticator) newDischargeRequiredError(cause error) error {
   147  	if m.Service == nil || m.Macaroon == nil {
   148  		return errors.Trace(cause)
   149  	}
   150  	mac := m.Macaroon.Clone()
   151  	// TODO(fwereade): 2016-03-17 lp:1558657
   152  	expiryTime := time.Now().Add(externalLoginExpiryTime)
   153  	if err := addMacaroonTimeBeforeCaveat(m.Service, mac, expiryTime); err != nil {
   154  		return errors.Annotatef(err, "cannot create macaroon")
   155  	}
   156  	err := m.Service.AddCaveat(mac, checkers.NeedDeclaredCaveat(
   157  		checkers.Caveat{
   158  			Location:  m.IdentityLocation,
   159  			Condition: "is-authenticated-user",
   160  		},
   161  		usernameKey,
   162  	))
   163  	if err != nil {
   164  		return errors.Annotatef(err, "cannot create macaroon")
   165  	}
   166  	return &common.DischargeRequiredError{
   167  		Cause:    cause,
   168  		Macaroon: mac,
   169  	}
   170  }
   171  
   172  // Authenticate authenticates the provided entity. If there is no macaroon provided, it will
   173  // return a *DischargeRequiredError containing a macaroon that can be used to grant access.
   174  func (m *ExternalMacaroonAuthenticator) Authenticate(entityFinder EntityFinder, _ names.Tag, req params.LoginRequest) (state.Entity, error) {
   175  	declared, err := m.Service.CheckAny(req.Macaroons, nil, checkers.New(checkers.TimeBefore))
   176  	if _, ok := errors.Cause(err).(*bakery.VerificationError); ok {
   177  		return nil, m.newDischargeRequiredError(err)
   178  	}
   179  	if err != nil {
   180  		return nil, errors.Trace(err)
   181  	}
   182  	username := declared[usernameKey]
   183  	var tag names.UserTag
   184  	if names.IsValidUserName(username) {
   185  		// The name is a local name without an explicit @local suffix.
   186  		// In this case, for compatibility with 3rd parties that don't
   187  		// care to add their own domain, we add an @external domain
   188  		// to ensure there is no confusion between local and external
   189  		// users.
   190  		// TODO(rog) remove this logic when deployed dischargers
   191  		// always add an @ domain.
   192  		tag = names.NewLocalUserTag(username).WithDomain("external")
   193  	} else {
   194  		// We have a name with an explicit domain (or an invalid user name).
   195  		if !names.IsValidUser(username) {
   196  			return nil, errors.Errorf("%q is an invalid user name", username)
   197  		}
   198  		tag = names.NewUserTag(username)
   199  		if tag.IsLocal() {
   200  			return nil, errors.Errorf("external identity provider has provided ostensibly local name %q", username)
   201  		}
   202  	}
   203  	entity, err := entityFinder.FindEntity(tag)
   204  	if errors.IsNotFound(err) {
   205  		return nil, errors.Trace(common.ErrBadCreds)
   206  	} else if err != nil {
   207  		return nil, errors.Trace(err)
   208  	}
   209  	return entity, nil
   210  }
   211  
   212  func addMacaroonTimeBeforeCaveat(svc BakeryService, m *macaroon.Macaroon, t time.Time) error {
   213  	return svc.AddCaveat(m, checkers.TimeBeforeCaveat(t))
   214  }
   215  
   216  // BakeryService defines the subset of bakery.Service
   217  // that we require for authentication.
   218  type BakeryService interface {
   219  	AddCaveat(*macaroon.Macaroon, checkers.Caveat) error
   220  	CheckAny([]macaroon.Slice, map[string]string, checkers.Checker) (map[string]string, error)
   221  	NewMacaroon(string, []byte, []checkers.Caveat) (*macaroon.Macaroon, error)
   222  }
   223  
   224  // ExpirableStorageBakeryService extends BakeryService
   225  // with the ExpireStorageAt method so that root keys are
   226  // removed from storage at that time.
   227  type ExpirableStorageBakeryService interface {
   228  	BakeryService
   229  
   230  	// ExpireStorageAt returns a new ExpirableStorageBakeryService with
   231  	// a store that will expire items added to it at the specified time.
   232  	ExpireStorageAt(time.Time) (ExpirableStorageBakeryService, error)
   233  }