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 }