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 }