Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2015 The LUCI Authors. All rights reserved. | 1 // Copyright 2015 The LUCI Authors. All rights reserved. |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | 2 // Use of this source code is governed under the Apache License, Version 2.0 |
| 3 // that can be found in the LICENSE file. | 3 // that can be found in the LICENSE file. |
| 4 | 4 |
| 5 package auth | 5 package auth |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "fmt" | 8 "fmt" |
| 9 "net/http" | 9 "net/http" |
| 10 | 10 |
| 11 "golang.org/x/net/context" | 11 "golang.org/x/net/context" |
| 12 | 12 |
| 13 "github.com/luci/luci-go/common/errors" | 13 "github.com/luci/luci-go/common/errors" |
| 14 "github.com/luci/luci-go/common/logging" | 14 "github.com/luci/luci-go/common/logging" |
| 15 | 15 |
| 16 "github.com/luci/luci-go/server/auth/delegation" | 16 "github.com/luci/luci-go/server/auth/delegation" |
| 17 "github.com/luci/luci-go/server/auth/identity" | 17 "github.com/luci/luci-go/server/auth/identity" |
| 18 "github.com/luci/luci-go/server/auth/signing" | 18 "github.com/luci/luci-go/server/auth/signing" |
| 19 "github.com/luci/luci-go/server/router" | |
| 19 ) | 20 ) |
| 20 | 21 |
| 21 var ( | 22 var ( |
| 22 // ErrNotConfigured is returned by Authenticate if auth library wasn't | 23 // ErrNotConfigured is returned by Authenticate if auth library wasn't |
| 23 // properly initialized (see SetConfig). | 24 // properly initialized (see SetConfig). |
| 24 ErrNotConfigured = errors.New("auth: the library is not properly configu red") | 25 ErrNotConfigured = errors.New("auth: the library is not properly configu red") |
| 25 | 26 |
| 26 // ErrNoUsersAPI is returned by LoginURL and LogoutURL if none of | 27 // ErrNoUsersAPI is returned by LoginURL and LogoutURL if none of |
| 27 // the authentication methods support UsersAPI. | 28 // the authentication methods support UsersAPI. |
| 28 ErrNoUsersAPI = errors.New("auth: methods do not support login or logout URL") | 29 ErrNoUsersAPI = errors.New("auth: methods do not support login or logout URL") |
| 29 | 30 |
| 30 // ErrBadClientID is returned by Authenticate if caller is using | 31 // ErrBadClientID is returned by Authenticate if caller is using |
| 31 // non-whitelisted OAuth2 client. More info is in the log. | 32 // non-whitelisted OAuth2 client. More info is in the log. |
| 32 ErrBadClientID = errors.New("auth: OAuth client_id is not whitelisted") | 33 ErrBadClientID = errors.New("auth: OAuth client_id is not whitelisted") |
| 33 | 34 |
| 34 // ErrIPNotWhitelisted is returned when an account is restricted by an I P | 35 // ErrIPNotWhitelisted is returned when an account is restricted by an I P |
| 35 // whitelist and request's remote_addr is not in it. | 36 // whitelist and request's remote_addr is not in it. |
| 36 ErrIPNotWhitelisted = errors.New("auth: IP is not whitelisted") | 37 ErrIPNotWhitelisted = errors.New("auth: IP is not whitelisted") |
| 37 ) | 38 ) |
| 38 | 39 |
| 39 // Method implements particular kind of low level authentication mechanism for | 40 // Method implements a particular kind of low-level authentication mechanism. |
| 40 // incoming requests. It may also optionally implement UsersAPI (if the method | 41 // |
| 41 // support login and logout URLs). Use type sniffing to figure out. | 42 // It may also optionally implement UsersAPI (if the method support login and |
| 43 // logout URLs). | |
| 44 // | |
| 45 // Methods are not usually used directly, but passed to Authenticator{...} that | |
| 46 // knows how to apply them. | |
| 42 type Method interface { | 47 type Method interface { |
| 43 // Authenticate extracts user information from the incoming request. | 48 // Authenticate extracts user information from the incoming request. |
| 49 // | |
| 44 // It returns: | 50 // It returns: |
| 45 // * (*User, nil) on success. | 51 // * (*User, nil) on success. |
| 46 // * (nil, nil) if the method is not applicable. | 52 // * (nil, nil) if the method is not applicable. |
| 47 // * (nil, error) if the method is applicable, but credentials are inv alid. | 53 // * (nil, error) if the method is applicable, but credentials are inv alid. |
| 48 Authenticate(context.Context, *http.Request) (*User, error) | 54 Authenticate(context.Context, *http.Request) (*User, error) |
| 49 } | 55 } |
| 50 | 56 |
| 51 // UsersAPI may be additionally implemented by Method if it supports login and | 57 // UsersAPI may be additionally implemented by Method if it supports login and |
| 52 // logout URLs. | 58 // logout URLs. |
| 53 type UsersAPI interface { | 59 type UsersAPI interface { |
| (...skipping 27 matching lines...) Expand all Loading... | |
| 81 | 87 |
| 82 // Picture is URL of the user avatar. Optional, default "". | 88 // Picture is URL of the user avatar. Optional, default "". |
| 83 Picture string | 89 Picture string |
| 84 | 90 |
| 85 // ClientID is the ID of the pre-registered OAuth2 client so its identit y can | 91 // ClientID is the ID of the pre-registered OAuth2 client so its identit y can |
| 86 // be verified. Used only by authentication methods based on OAuth2. | 92 // be verified. Used only by authentication methods based on OAuth2. |
| 87 // See https://developers.google.com/console/help/#generatingoauth2 for more. | 93 // See https://developers.google.com/console/help/#generatingoauth2 for more. |
| 88 ClientID string | 94 ClientID string |
| 89 } | 95 } |
| 90 | 96 |
| 91 // Authenticator perform authentication of incoming requests. It is stateless | 97 // Authenticator perform authentication of incoming requests. |
|
iannucci
2017/04/19 21:58:34
nit: performs
Vadim Sh.
2017/04/19 22:32:47
Done.
| |
| 92 // object that just describes what methods to try when authenticating current | 98 // |
| 93 // request. It is fine to create it on per-request basis. | 99 // It is stateless object configured with a list of methods to try when |
|
iannucci
2017/04/19 21:58:34
nit: It is a
Vadim Sh.
2017/04/19 22:32:46
Done.
| |
| 94 type Authenticator []Method | 100 // authenticating incoming requests. It implements Authenticate method that |
| 101 // performs high-level authentication logic using the provided list of low-level | |
| 102 // auth methods. | |
|
iannucci
2017/04/19 21:58:34
How often do we actually have more than one Method
Vadim Sh.
2017/04/19 22:32:46
Not often, that's why the recommended way to use t
| |
| 103 type Authenticator struct { | |
| 104 » Methods []Method // a list of authentication methods to try | |
| 105 } | |
| 95 | 106 |
| 96 // Authenticate authenticates incoming requests and returns new context.Context | 107 // GetMiddleware returns a middleware that uses this Authenticator for |
| 97 // with State stored into it. Returns error if credentials are provided, but | 108 // authentication. |
| 98 // invalid. If no credentials are provided (i.e. the request is anonymous), | 109 // |
| 99 // finishes successfully, but in that case State.Identity() will return | 110 // It uses a.Authenticate internally and handles errors appropriately. |
| 100 // AnonymousIdentity. | 111 func (a *Authenticator) GetMiddleware() router.Middleware { |
| 101 func (a Authenticator) Authenticate(c context.Context, r *http.Request) (context .Context, error) { | 112 » return func(c *router.Context, next router.Handler) { |
| 113 » » ctx, err := a.Authenticate(c.Context, c.Request) | |
| 114 » » switch { | |
| 115 » » case errors.IsTransient(err): | |
| 116 » » » replyError(c.Context, c.Writer, 500, fmt.Sprintf("Transi ent error during authentication - %s", err)) | |
| 117 » » case err != nil: | |
| 118 » » » replyError(c.Context, c.Writer, 401, fmt.Sprintf("Authen tication error - %s", err)) | |
| 119 » » default: | |
| 120 » » » c.Context = ctx | |
| 121 » » » next(c) | |
| 122 » » } | |
| 123 » } | |
| 124 } | |
| 125 | |
| 126 // Authenticate authenticates the requests and adds State into the context. | |
| 127 // | |
| 128 // Returns an error if credentials are provided, but invalid. If no credentials | |
| 129 // are provided (i.e. the request is anonymous), finishes successfully, but in | |
| 130 // that case State.Identity() returns AnonymousIdentity. | |
| 131 func (a *Authenticator) Authenticate(c context.Context, r *http.Request) (contex t.Context, error) { | |
| 102 report := durationReporter(c, authenticateDuration) | 132 report := durationReporter(c, authenticateDuration) |
| 103 | 133 |
| 104 // Make it known to handlers what authentication methods are allowed. | |
| 105 // This is important for LoginURL and LogoutURL functions. | |
| 106 c = SetAuthenticator(c, a) | |
| 107 | |
| 108 // We will need working DB factory below to check IP whitelist. | 134 // We will need working DB factory below to check IP whitelist. |
| 109 cfg := GetConfig(c) | 135 cfg := GetConfig(c) |
| 110 » if cfg == nil || cfg.DBProvider == nil { | 136 » if cfg == nil || cfg.DBProvider == nil || len(a.Methods) == 0 { |
| 111 report(ErrNotConfigured, "ERROR_NOT_CONFIGURED") | 137 report(ErrNotConfigured, "ERROR_NOT_CONFIGURED") |
| 112 return nil, ErrNotConfigured | 138 return nil, ErrNotConfigured |
| 113 } | 139 } |
| 114 | 140 |
| 115 // Pick first authentication method that applies. | 141 // Pick first authentication method that applies. |
| 116 » var s state | 142 » s := state{authenticator: a} |
| 117 » for _, m := range a { | 143 » for _, m := range a.Methods { |
| 118 var err error | 144 var err error |
| 119 s.user, err = m.Authenticate(c, r) | 145 s.user, err = m.Authenticate(c, r) |
| 120 if err != nil { | 146 if err != nil { |
| 121 report(err, "ERROR_BROKEN_CREDS") // e.g. malformed OAut h token | 147 report(err, "ERROR_BROKEN_CREDS") // e.g. malformed OAut h token |
| 122 return nil, err | 148 return nil, err |
| 123 } | 149 } |
| 124 if s.user != nil { | 150 if s.user != nil { |
| 125 if err = s.user.Identity.Validate(); err != nil { | 151 if err = s.user.Identity.Validate(); err != nil { |
| 126 report(err, "ERROR_BROKEN_IDENTITY") // a weird looking email address | 152 report(err, "ERROR_BROKEN_IDENTITY") // a weird looking email address |
| 127 return nil, err | 153 return nil, err |
| (...skipping 102 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 230 "peerID": s.peerIdent, | 256 "peerID": s.peerIdent, |
| 231 "delegatedID": delegatedIdentity, | 257 "delegatedID": delegatedIdentity, |
| 232 }.Debugf(c, "auth: Using delegation") | 258 }.Debugf(c, "auth: Using delegation") |
| 233 } | 259 } |
| 234 | 260 |
| 235 // Inject auth state. | 261 // Inject auth state. |
| 236 report(nil, "SUCCESS") | 262 report(nil, "SUCCESS") |
| 237 return WithState(c, &s), nil | 263 return WithState(c, &s), nil |
| 238 } | 264 } |
| 239 | 265 |
| 240 // usersAPI returns implementation of UsersAPI by examining Methods. Returns nil | 266 // usersAPI returns implementation of UsersAPI by examining Methods. |
| 241 // if none of Methods implement UsersAPI. | 267 // |
| 242 func (a Authenticator) usersAPI() UsersAPI { | 268 // Returns nil if none of Methods implement UsersAPI. |
| 243 » for _, m := range a { | 269 func (a *Authenticator) usersAPI() UsersAPI { |
| 270 » for _, m := range a.Methods { | |
| 244 if api, ok := m.(UsersAPI); ok { | 271 if api, ok := m.(UsersAPI); ok { |
| 245 return api | 272 return api |
| 246 } | 273 } |
| 247 } | 274 } |
| 248 return nil | 275 return nil |
| 249 } | 276 } |
| 250 | 277 |
| 251 // LoginURL returns a URL that, when visited, prompts the user to sign in, | 278 // LoginURL returns a URL that, when visited, prompts the user to sign in, |
| 252 // then redirects the user to the URL specified by dest. | 279 // then redirects the user to the URL specified by dest. |
| 253 func (a Authenticator) LoginURL(c context.Context, dest string) (string, error) { | 280 // |
| 281 // Returns ErrNoUsersAPI if none of the authentication methods support login | |
| 282 // URLs. | |
| 283 func (a *Authenticator) LoginURL(c context.Context, dest string) (string, error) { | |
| 254 if api := a.usersAPI(); api != nil { | 284 if api := a.usersAPI(); api != nil { |
| 255 return api.LoginURL(c, dest) | 285 return api.LoginURL(c, dest) |
| 256 } | 286 } |
| 257 return "", ErrNoUsersAPI | 287 return "", ErrNoUsersAPI |
| 258 } | 288 } |
| 259 | 289 |
| 260 // LogoutURL returns a URL that, when visited, signs the user out, | 290 // LogoutURL returns a URL that, when visited, signs the user out, then |
| 261 // then redirects the user to the URL specified by dest. | 291 // redirects the user to the URL specified by dest. |
| 262 func (a Authenticator) LogoutURL(c context.Context, dest string) (string, error) { | 292 // |
| 293 // Returns ErrNoUsersAPI if none of the authentication methods support login | |
| 294 // URLs. | |
| 295 func (a *Authenticator) LogoutURL(c context.Context, dest string) (string, error ) { | |
| 263 if api := a.usersAPI(); api != nil { | 296 if api := a.usersAPI(); api != nil { |
| 264 return api.LogoutURL(c, dest) | 297 return api.LogoutURL(c, dest) |
| 265 } | 298 } |
| 266 return "", ErrNoUsersAPI | 299 return "", ErrNoUsersAPI |
| 267 } | 300 } |
| 268 | 301 |
| 269 //// | 302 //// |
| 270 | 303 |
| 304 // replyError logs the error and writes it to ResponseWriter. | |
| 305 func replyError(c context.Context, rw http.ResponseWriter, code int, msg string) { | |
| 306 logging.Errorf(c, "HTTP %d: %s", code, msg) | |
| 307 http.Error(rw, msg, code) | |
|
iannucci
2017/04/19 21:58:34
Can this leak some secret info from msg -> user?
Vadim Sh.
2017/04/19 22:32:47
I don't think it can. But just to be sure, removed
| |
| 308 } | |
| 309 | |
| 271 // getOwnServiceIdentity returns 'service:<appID>' identity of the current | 310 // getOwnServiceIdentity returns 'service:<appID>' identity of the current |
| 272 // service. | 311 // service. |
| 273 func getOwnServiceIdentity(c context.Context, signer signing.Signer) (identity.I dentity, error) { | 312 func getOwnServiceIdentity(c context.Context, signer signing.Signer) (identity.I dentity, error) { |
| 274 if signer == nil { | 313 if signer == nil { |
| 275 return "", ErrNotConfigured | 314 return "", ErrNotConfigured |
| 276 } | 315 } |
| 277 serviceInfo, err := signer.ServiceInfo(c) | 316 serviceInfo, err := signer.ServiceInfo(c) |
| 278 if err != nil { | 317 if err != nil { |
| 279 return "", err | 318 return "", err |
| 280 } | 319 } |
| 281 return identity.MakeIdentity("service:" + serviceInfo.AppID) | 320 return identity.MakeIdentity("service:" + serviceInfo.AppID) |
| 282 } | 321 } |
| OLD | NEW |