| 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 openid | 5 package openid |
| 6 | 6 |
| 7 import ( | 7 import ( |
| 8 "fmt" | 8 "fmt" |
| 9 "net/http" | 9 "net/http" |
| 10 "net/url" | 10 "net/url" |
| 11 "time" | 11 "time" |
| 12 | 12 |
| 13 "github.com/julienschmidt/httprouter" | |
| 14 "golang.org/x/net/context" | 13 "golang.org/x/net/context" |
| 15 | 14 |
| 16 "github.com/luci/luci-go/common/clock" | 15 "github.com/luci/luci-go/common/clock" |
| 17 "github.com/luci/luci-go/common/errors" | 16 "github.com/luci/luci-go/common/errors" |
| 18 "github.com/luci/luci-go/common/logging" | 17 "github.com/luci/luci-go/common/logging" |
| 19 "github.com/luci/luci-go/server/auth" | 18 "github.com/luci/luci-go/server/auth" |
| 20 » "github.com/luci/luci-go/server/middleware" | 19 » "github.com/luci/luci-go/server/router" |
| 21 ) | 20 ) |
| 22 | 21 |
| 23 // These are installed into a HTTP router by AuthMethod.InstallHandlers(...). | 22 // These are installed into a HTTP router by AuthMethod.InstallHandlers(...). |
| 24 const ( | 23 const ( |
| 25 loginURL = "/auth/openid/login" | 24 loginURL = "/auth/openid/login" |
| 26 logoutURL = "/auth/openid/logout" | 25 logoutURL = "/auth/openid/logout" |
| 27 callbackURL = "/auth/openid/callback" | 26 callbackURL = "/auth/openid/callback" |
| 28 ) | 27 ) |
| 29 | 28 |
| 30 // AuthMethod implements auth.Method and auth.UsersAPI and can be used as | 29 // AuthMethod implements auth.Method and auth.UsersAPI and can be used as |
| (...skipping 14 matching lines...) Expand all Loading... |
| 45 Insecure bool | 44 Insecure bool |
| 46 | 45 |
| 47 // IncompatibleCookies is a list of cookies to remove when setting or cl
earing | 46 // IncompatibleCookies is a list of cookies to remove when setting or cl
earing |
| 48 // session cookie. It is useful to get rid of GAE cookies when OpenID co
okies | 47 // session cookie. It is useful to get rid of GAE cookies when OpenID co
okies |
| 49 // are being used. Having both is very confusing. | 48 // are being used. Having both is very confusing. |
| 50 IncompatibleCookies []string | 49 IncompatibleCookies []string |
| 51 } | 50 } |
| 52 | 51 |
| 53 // InstallHandlers installs HTTP handlers used in OpenID protocol. Must be | 52 // InstallHandlers installs HTTP handlers used in OpenID protocol. Must be |
| 54 // installed in server HTTP router for OpenID authentication flow to work. | 53 // installed in server HTTP router for OpenID authentication flow to work. |
| 55 func (m *AuthMethod) InstallHandlers(r *httprouter.Router, base middleware.Base)
{ | 54 func (m *AuthMethod) InstallHandlers(r *router.Router, handlers []router.Handler
) { |
| 56 » r.GET(loginURL, base(m.loginHandler)) | 55 » r.GET(loginURL, append(handlers, m.loginHandler)...) |
| 57 » r.GET(logoutURL, base(m.logoutHandler)) | 56 » r.GET(logoutURL, append(handlers, m.logoutHandler)...) |
| 58 » r.GET(callbackURL, base(m.callbackHandler)) | 57 » r.GET(callbackURL, append(handlers, m.callbackHandler)...) |
| 59 } | 58 } |
| 60 | 59 |
| 61 // Warmup prepares local caches. It's optional. | 60 // Warmup prepares local caches. It's optional. |
| 62 func (m *AuthMethod) Warmup(c context.Context) error { | 61 func (m *AuthMethod) Warmup(c context.Context) error { |
| 63 cfg, err := fetchCachedSettings(c) | 62 cfg, err := fetchCachedSettings(c) |
| 64 if err != nil { | 63 if err != nil { |
| 65 return err | 64 return err |
| 66 } | 65 } |
| 67 _, err = fetchDiscoveryDoc(c, cfg.DiscoveryURL) | 66 _, err = fetchDiscoveryDoc(c, cfg.DiscoveryURL) |
| 68 return err | 67 return err |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 111 func (m *AuthMethod) LogoutURL(c context.Context, dest string) (string, error) { | 110 func (m *AuthMethod) LogoutURL(c context.Context, dest string) (string, error) { |
| 112 if m.SessionStore == nil { | 111 if m.SessionStore == nil { |
| 113 return "", ErrNotConfigured | 112 return "", ErrNotConfigured |
| 114 } | 113 } |
| 115 return makeRedirectURL(logoutURL, dest) | 114 return makeRedirectURL(logoutURL, dest) |
| 116 } | 115 } |
| 117 | 116 |
| 118 //// | 117 //// |
| 119 | 118 |
| 120 // loginHandler initiates login flow by redirecting user to OpenID login page. | 119 // loginHandler initiates login flow by redirecting user to OpenID login page. |
| 121 func (m *AuthMethod) loginHandler(c context.Context, rw http.ResponseWriter, r *
http.Request, p httprouter.Params) { | 120 func (m *AuthMethod) loginHandler(c *router.Context) { |
| 122 » dest, err := normalizeURL(r.URL.Query().Get("r")) | 121 » dest, err := normalizeURL(c.Request.URL.Query().Get("r")) |
| 123 if err != nil { | 122 if err != nil { |
| 124 » » replyError(c, rw, err, "Bad redirect URI (%q) - %s", dest, err) | 123 » » replyError(c.Context, c.Writer, err, "Bad redirect URI (%q) - %s
", dest, err) |
| 124 » » c.Abort() |
| 125 return | 125 return |
| 126 } | 126 } |
| 127 | 127 |
| 128 » cfg, err := fetchCachedSettings(c) | 128 » cfg, err := fetchCachedSettings(c.Context) |
| 129 if err != nil { | 129 if err != nil { |
| 130 » » replyError(c, rw, err, "Can't load OpenID settings - %s", err) | 130 » » replyError(c.Context, c.Writer, err, "Can't load OpenID settings
- %s", err) |
| 131 » » c.Abort() |
| 131 return | 132 return |
| 132 } | 133 } |
| 133 | 134 |
| 134 // `state` will be propagated by OpenID backend and will eventually show
up | 135 // `state` will be propagated by OpenID backend and will eventually show
up |
| 135 // in callback URI handler. See callbackHandler. | 136 // in callback URI handler. See callbackHandler. |
| 136 state := map[string]string{ | 137 state := map[string]string{ |
| 137 "dest_url": dest, | 138 "dest_url": dest, |
| 138 » » "host_url": r.Host, | 139 » » "host_url": c.Request.Host, |
| 139 } | 140 } |
| 140 » authURI, err := authenticationURI(c, cfg, state) | 141 » authURI, err := authenticationURI(c.Context, cfg, state) |
| 141 if err != nil { | 142 if err != nil { |
| 142 » » replyError(c, rw, err, "Can't generate authentication URI - %s",
err) | 143 » » replyError(c.Context, c.Writer, err, "Can't generate authenticat
ion URI - %s", err) |
| 144 » » c.Abort() |
| 143 return | 145 return |
| 144 } | 146 } |
| 145 » http.Redirect(rw, r, authURI, http.StatusFound) | 147 » http.Redirect(c.Writer, c.Request, authURI, http.StatusFound) |
| 146 } | 148 } |
| 147 | 149 |
| 148 // logoutHandler nukes active session and redirect back to destination URL. | 150 // logoutHandler nukes active session and redirect back to destination URL. |
| 149 func (m *AuthMethod) logoutHandler(c context.Context, rw http.ResponseWriter, r
*http.Request, p httprouter.Params) { | 151 func (m *AuthMethod) logoutHandler(c *router.Context) { |
| 150 » dest, err := normalizeURL(r.URL.Query().Get("r")) | 152 » dest, err := normalizeURL(c.Request.URL.Query().Get("r")) |
| 151 if err != nil { | 153 if err != nil { |
| 152 » » replyError(c, rw, err, "Bad redirect URI (%q) - %s", dest, err) | 154 » » replyError(c.Context, c.Writer, err, "Bad redirect URI (%q) - %s
", dest, err) |
| 153 return | 155 return |
| 154 } | 156 } |
| 155 | 157 |
| 156 // Close a session if there's one. | 158 // Close a session if there's one. |
| 157 » sid, err := decodeSessionCookie(c, r) | 159 » sid, err := decodeSessionCookie(c.Context, c.Request) |
| 158 if err != nil { | 160 if err != nil { |
| 159 » » replyError(c, rw, err, "Error when decoding session cookie - %s"
, err) | 161 » » replyError(c.Context, c.Writer, err, "Error when decoding sessio
n cookie - %s", err) |
| 162 » » c.Abort() |
| 160 return | 163 return |
| 161 } | 164 } |
| 162 if sid != "" { | 165 if sid != "" { |
| 163 » » if err = m.SessionStore.CloseSession(c, sid); err != nil { | 166 » » if err = m.SessionStore.CloseSession(c.Context, sid); err != nil
{ |
| 164 » » » replyError(c, rw, err, "Error when closing the session -
%s", err) | 167 » » » replyError(c.Context, c.Writer, err, "Error when closing
the session - %s", err) |
| 168 » » » c.Abort() |
| 165 return | 169 return |
| 166 } | 170 } |
| 167 } | 171 } |
| 168 | 172 |
| 169 // Nuke all session cookies to get to a completely clean state. | 173 // Nuke all session cookies to get to a completely clean state. |
| 170 » removeCookie(rw, r, sessionCookieName) | 174 » removeCookie(c.Writer, c.Request, sessionCookieName) |
| 171 » m.removeIncompatibleCookies(rw, r) | 175 » m.removeIncompatibleCookies(c.Writer, c.Request) |
| 172 | 176 |
| 173 // Redirect to the final destination. | 177 // Redirect to the final destination. |
| 174 » http.Redirect(rw, r, dest, http.StatusFound) | 178 » http.Redirect(c.Writer, c.Request, dest, http.StatusFound) |
| 175 } | 179 } |
| 176 | 180 |
| 177 // callbackHandler handles redirect from OpenID backend. Parameters contain | 181 // callbackHandler handles redirect from OpenID backend. Parameters contain |
| 178 // authorization code that can be exchanged for user profile. | 182 // authorization code that can be exchanged for user profile. |
| 179 func (m *AuthMethod) callbackHandler(c context.Context, rw http.ResponseWriter,
r *http.Request, p httprouter.Params) { | 183 func (m *AuthMethod) callbackHandler(ctx *router.Context) { |
| 184 » c, rw, r := ctx.Context, ctx.Writer, ctx.Request |
| 185 |
| 180 // This code path is hit when user clicks "Deny" on consent page. | 186 // This code path is hit when user clicks "Deny" on consent page. |
| 181 q := r.URL.Query() | 187 q := r.URL.Query() |
| 182 errorMsg := q.Get("error") | 188 errorMsg := q.Get("error") |
| 183 if errorMsg != "" { | 189 if errorMsg != "" { |
| 184 replyError(c, rw, errors.New("login error"), "OpenID login error
: %s", errorMsg) | 190 replyError(c, rw, errors.New("login error"), "OpenID login error
: %s", errorMsg) |
| 191 ctx.Abort() |
| 185 return | 192 return |
| 186 } | 193 } |
| 187 | 194 |
| 188 // Validate inputs. | 195 // Validate inputs. |
| 189 code := q.Get("code") | 196 code := q.Get("code") |
| 190 if code == "" { | 197 if code == "" { |
| 191 replyError(c, rw, errors.New("login error"), "Missing 'code' par
ameter") | 198 replyError(c, rw, errors.New("login error"), "Missing 'code' par
ameter") |
| 199 ctx.Abort() |
| 192 return | 200 return |
| 193 } | 201 } |
| 194 stateTok := q.Get("state") | 202 stateTok := q.Get("state") |
| 195 if stateTok == "" { | 203 if stateTok == "" { |
| 196 replyError(c, rw, errors.New("login error"), "Missing 'state' pa
rameter") | 204 replyError(c, rw, errors.New("login error"), "Missing 'state' pa
rameter") |
| 205 ctx.Abort() |
| 197 return | 206 return |
| 198 } | 207 } |
| 199 state, err := validateStateToken(c, stateTok) | 208 state, err := validateStateToken(c, stateTok) |
| 200 if err != nil { | 209 if err != nil { |
| 201 replyError(c, rw, err, "Failed to validate 'state' token") | 210 replyError(c, rw, err, "Failed to validate 'state' token") |
| 211 ctx.Abort() |
| 202 return | 212 return |
| 203 } | 213 } |
| 204 | 214 |
| 205 // Revalidate "dest_url". It was already validate in loginHandler when | 215 // Revalidate "dest_url". It was already validate in loginHandler when |
| 206 // generating state token, but just in case. | 216 // generating state token, but just in case. |
| 207 dest, err := normalizeURL(state["dest_url"]) | 217 dest, err := normalizeURL(state["dest_url"]) |
| 208 if err != nil { | 218 if err != nil { |
| 209 replyError(c, rw, err, "Bad redirect URI (%q) - %s", dest, err) | 219 replyError(c, rw, err, "Bad redirect URI (%q) - %s", dest, err) |
| 220 ctx.Abort() |
| 210 return | 221 return |
| 211 } | 222 } |
| 212 | 223 |
| 213 // Callback URI is hardcoded in OAuth2 client config and must always poi
nt | 224 // Callback URI is hardcoded in OAuth2 client config and must always poi
nt |
| 214 // to default version on GAE. Yet we want to support logging to non-defa
ult | 225 // to default version on GAE. Yet we want to support logging to non-defa
ult |
| 215 // versions that have different hostnames. Do some redirect dance here t
o pass | 226 // versions that have different hostnames. Do some redirect dance here t
o pass |
| 216 // control to required version if necessary (so that it can set cookie o
n | 227 // control to required version if necessary (so that it can set cookie o
n |
| 217 // non-default version domain). Same handler with same params, just with | 228 // non-default version domain). Same handler with same params, just with |
| 218 // different hostname. For most common case of signing in into default v
ersion | 229 // different hostname. For most common case of signing in into default v
ersion |
| 219 // this code path is not triggered. | 230 // this code path is not triggered. |
| 220 if state["host_url"] != r.Host { | 231 if state["host_url"] != r.Host { |
| 221 // There's no Scheme in r.URL. Append one, otherwise url.String(
) returns | 232 // There's no Scheme in r.URL. Append one, otherwise url.String(
) returns |
| 222 // relative (broken) URL. And replace the hostname with desired
one. | 233 // relative (broken) URL. And replace the hostname with desired
one. |
| 223 url := *r.URL | 234 url := *r.URL |
| 224 if m.Insecure { | 235 if m.Insecure { |
| 225 url.Scheme = "http" | 236 url.Scheme = "http" |
| 226 } else { | 237 } else { |
| 227 url.Scheme = "https" | 238 url.Scheme = "https" |
| 228 } | 239 } |
| 229 url.Host = state["host_url"] | 240 url.Host = state["host_url"] |
| 230 logging.Warningf(c, "Redirecting to callback URI on another host
%q", url.Host) | 241 logging.Warningf(c, "Redirecting to callback URI on another host
%q", url.Host) |
| 231 http.Redirect(rw, r, url.String(), http.StatusFound) | 242 http.Redirect(rw, r, url.String(), http.StatusFound) |
| 243 ctx.Abort() |
| 232 return | 244 return |
| 233 } | 245 } |
| 234 | 246 |
| 235 // Use authorization code to grab user profile. | 247 // Use authorization code to grab user profile. |
| 236 cfg, err := fetchCachedSettings(c) | 248 cfg, err := fetchCachedSettings(c) |
| 237 if err != nil { | 249 if err != nil { |
| 238 replyError(c, rw, err, "Can't load OpenID settings - %s", err) | 250 replyError(c, rw, err, "Can't load OpenID settings - %s", err) |
| 251 ctx.Abort() |
| 239 return | 252 return |
| 240 } | 253 } |
| 241 uid, user, err := handleAuthorizationCode(c, cfg, code) | 254 uid, user, err := handleAuthorizationCode(c, cfg, code) |
| 242 if err != nil { | 255 if err != nil { |
| 243 replyError(c, rw, err, "Error when fetching user profile - %s",
err) | 256 replyError(c, rw, err, "Error when fetching user profile - %s",
err) |
| 257 ctx.Abort() |
| 244 return | 258 return |
| 245 } | 259 } |
| 246 | 260 |
| 247 // Grab previous session from the cookie to close it once new one is cre
ated. | 261 // Grab previous session from the cookie to close it once new one is cre
ated. |
| 248 prevSid, err := decodeSessionCookie(c, r) | 262 prevSid, err := decodeSessionCookie(c, r) |
| 249 if err != nil { | 263 if err != nil { |
| 250 replyError(c, rw, err, "Error when decoding session cookie - %s"
, err) | 264 replyError(c, rw, err, "Error when decoding session cookie - %s"
, err) |
| 265 ctx.Abort() |
| 251 return | 266 return |
| 252 } | 267 } |
| 253 | 268 |
| 254 // Create session in the session store. | 269 // Create session in the session store. |
| 255 expTime := clock.Now(c).Add(sessionCookieToken.Expiration) | 270 expTime := clock.Now(c).Add(sessionCookieToken.Expiration) |
| 256 sid, err := m.SessionStore.OpenSession(c, uid, user, expTime) | 271 sid, err := m.SessionStore.OpenSession(c, uid, user, expTime) |
| 257 if err != nil { | 272 if err != nil { |
| 258 replyError(c, rw, err, "Error when creating the session - %s", e
rr) | 273 replyError(c, rw, err, "Error when creating the session - %s", e
rr) |
| 274 ctx.Abort() |
| 259 return | 275 return |
| 260 } | 276 } |
| 261 | 277 |
| 262 // Kill previous session now that new one is successfully created. | 278 // Kill previous session now that new one is successfully created. |
| 263 if prevSid != "" { | 279 if prevSid != "" { |
| 264 if err = m.SessionStore.CloseSession(c, sid); err != nil { | 280 if err = m.SessionStore.CloseSession(c, sid); err != nil { |
| 265 replyError(c, rw, err, "Error when closing the session -
%s", err) | 281 replyError(c, rw, err, "Error when closing the session -
%s", err) |
| 282 ctx.Abort() |
| 266 return | 283 return |
| 267 } | 284 } |
| 268 } | 285 } |
| 269 | 286 |
| 270 // Set the cookies. | 287 // Set the cookies. |
| 271 cookie, err := makeSessionCookie(c, sid, !m.Insecure) | 288 cookie, err := makeSessionCookie(c, sid, !m.Insecure) |
| 272 if err != nil { | 289 if err != nil { |
| 273 replyError(c, rw, err, "Can't make session cookie - %s", err) | 290 replyError(c, rw, err, "Can't make session cookie - %s", err) |
| 291 ctx.Abort() |
| 274 return | 292 return |
| 275 } | 293 } |
| 276 http.SetCookie(rw, cookie) | 294 http.SetCookie(rw, cookie) |
| 277 m.removeIncompatibleCookies(rw, r) | 295 m.removeIncompatibleCookies(rw, r) |
| 278 | 296 |
| 279 // Redirect to the final destination page. | 297 // Redirect to the final destination page. |
| 280 http.Redirect(rw, r, dest, http.StatusFound) | 298 http.Redirect(rw, r, dest, http.StatusFound) |
| 281 } | 299 } |
| 282 | 300 |
| 283 // removeIncompatibleCookies removes cookies specified by m.IncompatibleCookies. | 301 // removeIncompatibleCookies removes cookies specified by m.IncompatibleCookies. |
| (...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 330 // HTTP 400 on fatal errors (that can happen only on bad requests). | 348 // HTTP 400 on fatal errors (that can happen only on bad requests). |
| 331 func replyError(c context.Context, rw http.ResponseWriter, err error, msg string
, args ...interface{}) { | 349 func replyError(c context.Context, rw http.ResponseWriter, err error, msg string
, args ...interface{}) { |
| 332 code := http.StatusBadRequest | 350 code := http.StatusBadRequest |
| 333 if errors.IsTransient(err) { | 351 if errors.IsTransient(err) { |
| 334 code = http.StatusInternalServerError | 352 code = http.StatusInternalServerError |
| 335 } | 353 } |
| 336 msg = fmt.Sprintf(msg, args...) | 354 msg = fmt.Sprintf(msg, args...) |
| 337 logging.Errorf(c, "HTTP %d: %s", code, msg) | 355 logging.Errorf(c, "HTTP %d: %s", code, msg) |
| 338 http.Error(rw, msg, code) | 356 http.Error(rw, msg, code) |
| 339 } | 357 } |
| OLD | NEW |