Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(182)

Side by Side Diff: server/auth/openid/method.go

Issue 2043423004: Make HTTP middleware easier to use (Closed) Base URL: https://github.com/luci/luci-go@master
Patch Set: Update tests Created 4 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698