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 }