/
handler.go
466 lines (412 loc) · 15 KB
/
handler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
package login
import (
"net/http"
"time"
"github.com/ory/kratos/text"
"github.com/ory/nosurf"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/schema"
"github.com/ory/kratos/ui/node"
"github.com/ory/x/decoderx"
"github.com/julienschmidt/httprouter"
"github.com/pkg/errors"
"github.com/ory/x/urlx"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/selfservice/errorx"
"github.com/ory/kratos/selfservice/flow"
"github.com/ory/kratos/session"
"github.com/ory/kratos/x"
)
const (
RouteInitBrowserFlow = "/self-service/login/browser"
RouteInitAPIFlow = "/self-service/login/api"
RouteGetFlow = "/self-service/login/flows"
RouteSubmitFlow = "/self-service/login"
)
type (
handlerDependencies interface {
HookExecutorProvider
FlowPersistenceProvider
errorx.ManagementProvider
StrategyProvider
session.HandlerProvider
session.ManagementProvider
x.WriterProvider
x.CSRFTokenGeneratorProvider
x.CSRFProvider
config.Provider
ErrorHandlerProvider
}
HandlerProvider interface {
LoginHandler() *Handler
}
Handler struct {
d handlerDependencies
hd *decoderx.HTTP
}
)
func NewHandler(d handlerDependencies) *Handler {
return &Handler{d: d, hd: decoderx.NewHTTP()}
}
func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) {
h.d.CSRFHandler().IgnorePath(RouteInitAPIFlow)
h.d.CSRFHandler().IgnorePath(RouteSubmitFlow)
public.GET(RouteInitBrowserFlow, h.initBrowserFlow)
public.GET(RouteInitAPIFlow, h.initAPIFlow)
public.GET(RouteGetFlow, h.fetchFlow)
public.POST(RouteSubmitFlow, h.submitFlow)
public.GET(RouteSubmitFlow, h.submitFlow)
}
func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) {
admin.GET(RouteInitBrowserFlow, x.RedirectToPublicRoute(h.d))
admin.GET(RouteInitAPIFlow, x.RedirectToPublicRoute(h.d))
admin.GET(RouteGetFlow, x.RedirectToPublicRoute(h.d))
admin.POST(RouteSubmitFlow, x.RedirectToPublicRoute(h.d))
admin.GET(RouteSubmitFlow, x.RedirectToPublicRoute(h.d))
}
func (h *Handler) NewLoginFlow(w http.ResponseWriter, r *http.Request, flow flow.Type) (*Flow, error) {
conf := h.d.Config(r.Context())
f := NewFlow(conf, conf.SelfServiceFlowLoginRequestLifespan(), h.d.GenerateCSRFToken(r), r, flow)
for _, s := range h.d.LoginStrategies(r.Context()) {
if err := s.PopulateLoginMethod(r, f); err != nil {
return nil, err
}
}
if err := sortNodes(f.UI.Nodes); err != nil {
return nil, err
}
if f.Forced {
f.UI.Messages.Set(text.NewInfoLoginReAuth())
}
if err := h.d.LoginHookExecutor().PreLoginHook(w, r, f); err != nil {
return nil, err
}
if err := h.d.LoginFlowPersister().CreateLoginFlow(r.Context(), f); err != nil {
return nil, err
}
return f, nil
}
func (h *Handler) FromOldFlow(w http.ResponseWriter, r *http.Request, of Flow) (*Flow, error) {
nf, err := h.NewLoginFlow(w, r, of.Type)
if err != nil {
return nil, err
}
nf.RequestURL = of.RequestURL
return nf, nil
}
// nolint:deadcode,unused
// swagger:parameters initializeSelfServiceLoginFlowForBrowsers initializeSelfServiceLoginFlowWithoutBrowser
type initializeSelfServiceLoginFlowWithoutBrowser struct {
// Refresh a login session
//
// If set to true, this will refresh an existing login session by
// asking the user to sign in again. This will reset the
// authenticated_at time of the session.
//
// in: query
Refresh bool `json:"refresh"`
}
// swagger:route GET /self-service/login/api v0alpha1 initializeSelfServiceLoginFlowWithoutBrowser
//
// Initialize Login Flow for APIs, Services, Apps, ...
//
// This endpoint initiates a login flow for API clients that do not use a browser, such as mobile devices, smart TVs, and so on.
//
// If a valid provided session cookie or session token is provided, a 400 Bad Request error
// will be returned unless the URL query parameter `?refresh=true` is set.
//
// To fetch an existing login flow call `/self-service/login/flows?flow=<flow_id>`.
//
// You MUST NOT use this endpoint in client-side (Single Page Apps, ReactJS, AngularJS) nor server-side (Java Server
// Pages, NodeJS, PHP, Golang, ...) browser applications. Using this endpoint in these applications will make
// you vulnerable to a variety of CSRF attacks, including CSRF login attacks.
//
// This endpoint MUST ONLY be used in scenarios such as native mobile apps (React Native, Objective C, Swift, Java, ...).
//
// More information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: selfServiceLoginFlow
// 400: jsonError
// 500: jsonError
func (h *Handler) initAPIFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
a, err := h.NewLoginFlow(w, r, flow.TypeAPI)
if err != nil {
h.d.Writer().WriteError(w, r, err)
return
}
// we assume an error means the user has no session
if _, err := h.d.SessionManager().FetchFromRequest(r.Context(), r); err != nil {
h.d.Writer().Write(w, r, a)
return
}
if a.Forced {
if err := h.d.LoginFlowPersister().ForceLoginFlow(r.Context(), a.ID); err != nil {
h.d.Writer().WriteError(w, r, err)
return
}
h.d.Writer().Write(w, r, a)
return
}
h.d.Writer().WriteError(w, r, errors.WithStack(ErrAlreadyLoggedIn))
}
// swagger:route GET /self-service/login/browser v0alpha1 initializeSelfServiceLoginFlowForBrowsers
//
// Initialize Login Flow for Browsers
//
// This endpoint initializes a browser-based user login flow. This endpoint will set the appropriate
// cookies and anti-CSRF measures required for browser-based flows.
//
// If this endpoint is opened as a link in the browser, it will be redirected to
// `selfservice.flows.login.ui_url` with the flow ID set as the query parameter `?flow=`. If a valid user session
// exists already, the browser will be redirected to `urls.default_redirect_url` unless the query parameter
// `?refresh=true` was set.
//
// If this endpoint is called via an AJAX request, the response contains the login flow without a redirect.
//
// This endpoint is NOT INTENDED for clients that do not have a browser (Chrome, Firefox, ...) as cookies are needed.
//
// More information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: selfServiceLoginFlow
// 302: emptyResponse
// 500: jsonError
func (h *Handler) initBrowserFlow(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
a, err := h.NewLoginFlow(w, r, flow.TypeBrowser)
if err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
// we assume an error means the user has no session
if _, err := h.d.SessionManager().FetchFromRequest(r.Context(), r); err != nil {
x.AcceptToRedirectOrJson(w, r, h.d.Writer(), a, a.AppendTo(h.d.Config(r.Context()).SelfServiceFlowLoginUI()).String())
return
}
if a.Forced {
if err := h.d.LoginFlowPersister().ForceLoginFlow(r.Context(), a.ID); err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
x.AcceptToRedirectOrJson(w, r, h.d.Writer(), a, a.AppendTo(h.d.Config(r.Context()).SelfServiceFlowLoginUI()).String())
return
}
if x.IsJSONRequest(r) {
h.d.Writer().WriteError(w, r, errors.WithStack(ErrAlreadyLoggedIn))
return
}
returnTo, err := x.SecureRedirectTo(r, h.d.Config(r.Context()).SelfServiceBrowserDefaultReturnTo(),
x.SecureRedirectAllowSelfServiceURLs(h.d.Config(r.Context()).SelfPublicURL(r)),
x.SecureRedirectAllowURLs(h.d.Config(r.Context()).SelfServiceBrowserWhitelistedReturnToDomains()),
)
if err != nil {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
return
}
http.Redirect(w, r, returnTo.String(), http.StatusSeeOther)
}
// nolint:deadcode,unused
// swagger:parameters getSelfServiceLoginFlow
type getSelfServiceLoginFlow struct {
// The Login Flow ID
//
// The value for this parameter comes from `flow` URL Query parameter sent to your
// application (e.g. `/login?flow=abcde`).
//
// required: true
// in: query
ID string `json:"id"`
// HTTP Cookies
//
// When using the SDK on the server side you must include the HTTP Cookie Header
// originally sent to your HTTP handler here.
//
// in: header
// name: Cookie
Cookies string `json:"cookie"`
}
// swagger:route GET /self-service/login/flows v0alpha1 getSelfServiceLoginFlow
//
// Get Login Flow
//
// This endpoint returns a login flow's context with, for example, error details and other information.
//
// Browser flows expect the anti-CSRF cookie to be included in the request's HTTP Cookie Header.
// For AJAX requests you must ensure that cookies are included in the request or requests will fail.
//
// If you use the browser-flow for server-side apps, the services need to run on a common top-level-domain
// and you need to forward the incoming HTTP Cookie header to this endpoint:
//
// ```js
// // pseudo-code example
// router.get('/login', async function (req, res) {
// const flow = await client.getSelfServiceLoginFlow(req.header('cookie'), req.query['flow'])
//
// res.render('login', flow)
// })
// ```
//
// More information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: selfServiceLoginFlow
// 403: jsonError
// 404: jsonError
// 410: jsonError
// 500: jsonError
func (h *Handler) fetchFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
ar, err := h.d.LoginFlowPersister().GetLoginFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("id")))
if err != nil {
h.d.Writer().WriteError(w, r, err)
return
}
// Browser flows must include the CSRF token
//
// Resolves: https://github.com/ory/kratos/issues/1282
if ar.Type == flow.TypeBrowser && !nosurf.VerifyToken(h.d.GenerateCSRFToken(r), ar.CSRFToken) {
h.d.Writer().WriteError(w, r, x.CSRFErrorReason(r, h.d))
return
}
if ar.ExpiresAt.Before(time.Now()) {
if ar.Type == flow.TypeBrowser {
h.d.Writer().WriteError(w, r, errors.WithStack(x.ErrGone.
WithReason("The login flow has expired. Redirect the user to the login flow init endpoint to initialize a new login flow.").
WithDetail("redirect_to", urlx.AppendPaths(h.d.Config(r.Context()).SelfPublicURL(r), RouteInitBrowserFlow).String())))
return
}
h.d.Writer().WriteError(w, r, errors.WithStack(x.ErrGone.
WithReason("The login flow has expired. Call the login flow init API endpoint to initialize a new login flow.").
WithDetail("api", urlx.AppendPaths(h.d.Config(r.Context()).SelfPublicURL(r), RouteInitAPIFlow).String())))
return
}
h.d.Writer().Write(w, r, ar)
}
// nolint:deadcode,unused
// swagger:parameters submitSelfServiceLoginFlow
type submitSelfServiceLoginFlow struct {
// The Login Flow ID
//
// The value for this parameter comes from `flow` URL Query parameter sent to your
// application (e.g. `/login?flow=abcde`).
//
// required: true
// in: query
Flow string `json:"flow"`
// in: body
Body submitSelfServiceLoginFlowBody
}
// swagger:model submitSelfServiceLoginFlowBody
// nolint:deadcode,unused
type submitSelfServiceLoginFlowBody struct{}
// swagger:route POST /self-service/login v0alpha1 submitSelfServiceLoginFlow
//
// Submit a Login Flow
//
// :::info
//
// This endpoint is EXPERIMENTAL and subject to potential breaking changes in the future.
//
// :::
//
// Use this endpoint to complete a login flow. This endpoint
// behaves differently for API and browser flows.
//
// API flows expect `application/json` to be sent in the body and responds with
// - HTTP 200 and a application/json body with the session token on success;
// - HTTP 302 redirect to a fresh login flow if the original flow expired with the appropriate error messages set;
// - HTTP 400 on form validation errors.
//
// Browser flows expect a Content-Type of `application/x-www-form-urlencoded` or `application/json` to be sent in the body and respond with
// - a HTTP 302 redirect to the post/after login URL or the `return_to` value if it was set and if the login succeeded;
// - a HTTP 302 redirect to the login UI URL with the flow ID containing the validation errors otherwise.
//
// Browser flows with an accept header of `application/json` will not redirect but instead respond with
// - HTTP 200 and a application/json body with the signed in identity and a `Set-Cookie` header on success;
// - HTTP 302 redirect to a fresh login flow if the original flow expired with the appropriate error messages set;
// - HTTP 400 on form validation errors.
//
// More information can be found at [Ory Kratos User Login and User Registration Documentation](https://www.ory.sh/docs/next/kratos/self-service/flows/user-login-user-registration).
//
// Schemes: http, https
//
// Consumes:
// - application/json
// - application/x-www-form-urlencoded
//
// Produces:
// - application/json
//
// Header:
// - Set-Cookie
//
// Responses:
// 200: successfulSelfServiceLoginWithoutBrowser
// 302: emptyResponse
// 400: selfServiceLoginFlow
// 500: jsonError
func (h *Handler) submitFlow(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
rid, err := flow.GetFlowID(r)
if err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, nil, node.DefaultGroup, err)
return
}
f, err := h.d.LoginFlowPersister().GetLoginFlow(r.Context(), rid)
if err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err)
return
}
if _, err := h.d.SessionManager().FetchFromRequest(r.Context(), r); err == nil && !f.Forced {
if f.Type == flow.TypeBrowser {
http.Redirect(w, r, h.d.Config(r.Context()).SelfServiceBrowserDefaultReturnTo().String(), http.StatusFound)
return
}
h.d.Writer().WriteError(w, r, errors.WithStack(ErrAlreadyLoggedIn))
return
}
if err := f.Valid(); err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, err)
return
}
var i *identity.Identity
for _, ss := range h.d.AllLoginStrategies() {
interim, err := ss.Login(w, r, f)
if errors.Is(err, flow.ErrStrategyNotResponsible) {
continue
} else if errors.Is(err, flow.ErrCompletedByStrategy) {
return
} else if err != nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, ss.NodeGroup(), err)
return
}
i = interim
break
}
if i == nil {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, errors.WithStack(schema.NewNoLoginStrategyResponsible()))
return
}
// TODO Handle n+1 authentication factor
if err := h.d.LoginHookExecutor().PostLoginHook(w, r, f, i); err != nil {
if err == ErrAddressNotVerified {
h.d.LoginFlowErrorHandler().WriteFlowError(w, r, f, node.DefaultGroup, errors.WithStack(schema.NewAddressNotVerifiedError()))
} else {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, err)
}
return
}
}