From c64424568e9126d98e5a132c040880aace8afd3f Mon Sep 17 00:00:00 2001 From: abador Date: Wed, 29 Dec 2021 08:17:47 +0100 Subject: [PATCH 01/17] Refresh session param for whoami endpoint, PLATFORM-6607 --- driver/config/config.go | 5 +++++ embedx/config.schema.json | 6 ++++++ session/handler.go | 9 +++++++++ session/session.go | 5 +++++ 4 files changed, 25 insertions(+) diff --git a/driver/config/config.go b/driver/config/config.go index b0debaa9b48a..2a23ceaacc48 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -95,6 +95,7 @@ const ( ViperKeySessionPath = "session.cookie.path" ViperKeySessionPersistentCookie = "session.cookie.persistent" ViperKeySessionWhoAmIAAL = "session.whoami.required_aal" + ViperKeySessionWhoAmIRefresh = "session.whoami.refresh" ViperKeyCookieSameSite = "cookies.same_site" ViperKeyCookieDomain = "cookies.domain" ViperKeyCookiePath = "cookies.path" @@ -1029,6 +1030,10 @@ func (p *Config) SessionWhoAmIAAL() string { return p.p.String(ViperKeySessionWhoAmIAAL) } +func (p *Config) SessionWhoAmIRefresh() bool { + return p.p.Bool(ViperKeySessionWhoAmIRefresh) +} + func (p *Config) SelfServiceSettingsRequiredAAL() string { return p.p.String(ViperKeySelfServiceSettingsRequiredAAL) } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 8347853bb213..24f0e3d5033d 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2115,6 +2115,12 @@ "properties": { "required_aal": { "$ref": "#/definitions/featureRequiredAal" + }, + "refresh": { + "title": "Allow session refresh from whoami endpoint", + "description": "If set to true will will allow to refresh session lifespan if ?refresh=true is present", + "type": "boolean", + "default": false } }, "additionalProperties": false diff --git a/session/handler.go b/session/handler.go index b2fadeffa590..9aa49779d7be 100644 --- a/session/handler.go +++ b/session/handler.go @@ -178,6 +178,15 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.P return } + // Refresh session if param was true + refresh := r.URL.Query().Get("refresh") + if h.r.Config(r.Context()).SessionWhoAmIRefresh() && refresh == "true" { + if err := h.r.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, s.Refresh(h.r.Config(r.Context()))); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + } + // s.Devices = nil s.Identity = s.Identity.CopyWithoutCredentials() diff --git a/session/session.go b/session/session.go index a1713b19b99f..96d9be4909c3 100644 --- a/session/session.go +++ b/session/session.go @@ -156,6 +156,11 @@ func (s *Session) Declassify() *Session { return s } +func (s *Session) Refresh(c lifespanProvider) *Session { + s.ExpiresAt = time.Now().Add(c.SessionLifespan()) + return s +} + func (s *Session) IsActive() bool { return s.Active && s.ExpiresAt.After(time.Now()) && (s.Identity == nil || s.Identity.IsActive()) } From 56da87deb6c204f26d40e597fcf85c33d130e30c Mon Sep 17 00:00:00 2001 From: abador Date: Wed, 29 Dec 2021 08:37:12 +0100 Subject: [PATCH 02/17] Refresh session admin endpoint, PLATFORM-6607 --- session/handler.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/session/handler.go b/session/handler.go index 9aa49779d7be..465f1349ce13 100644 --- a/session/handler.go +++ b/session/handler.go @@ -48,6 +48,7 @@ func NewHandler( const ( RouteCollection = "/sessions" RouteWhoami = RouteCollection + "/whoami" + RouteSession = RouteCollection + "/:id" RouteIdentity = "/identities" RouteIdentityManagement = RouteIdentity + "/:id/sessions" RouteIdentitySession = RouteIdentity + "/:id/session" @@ -60,6 +61,7 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { admin.Handle(m, RouteWhoami, x.RedirectToPublicRoute(h.r)) } admin.DELETE(RouteIdentityManagement, h.deleteIdentitySessions) + admin.PATCH(RouteSession, h.adminSessionRefresh) admin.GET(RouteIdentitySession, h.session) } @@ -323,6 +325,37 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: i}) } +// swagger:route PATCH /sessions/{id} v0alpha2 adminIdentitySession +// +// Calling this endpoint refreshes a given session. +// +// This endpoint is useful for: +// +// - Session refresh +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: successfulAdminIdentitySession +// 404: jsonError +// 500: jsonError +func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + s, err := h.r.SessionPersister().GetSession(r.Context(), x.ParseUUID(ps.ByName("id"))) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + if err := h.r.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, s.Refresh(h.r.Config(r.Context()))); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: s.Identity}) +} + // fandom-end func (h *Handler) IsAuthenticated(wrap httprouter.Handle, onUnauthenticated httprouter.Handle) httprouter.Handle { From 7b31be08eb5eee2b76860314a502b6f76be89149 Mon Sep 17 00:00:00 2001 From: abador Date: Wed, 29 Dec 2021 08:55:50 +0100 Subject: [PATCH 03/17] Refresh session admin endpoint based on session cookie/token, PLATFORM-6607 --- session/handler.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/session/handler.go b/session/handler.go index 465f1349ce13..2b8014b1b377 100644 --- a/session/handler.go +++ b/session/handler.go @@ -48,6 +48,7 @@ func NewHandler( const ( RouteCollection = "/sessions" RouteWhoami = RouteCollection + "/whoami" + RouteRefresh = RouteCollection + "/refresh" RouteSession = RouteCollection + "/:id" RouteIdentity = "/identities" RouteIdentityManagement = RouteIdentity + "/:id/sessions" @@ -62,6 +63,7 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { } admin.DELETE(RouteIdentityManagement, h.deleteIdentitySessions) admin.PATCH(RouteSession, h.adminSessionRefresh) + admin.GET(RouteRefresh, h.adminCurrentSessionRefresh) admin.GET(RouteIdentitySession, h.session) } @@ -356,6 +358,38 @@ func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: s.Identity}) } +// swagger:route GET /sessions/refresh v0alpha2 adminIdentitySession +// +// Calling this endpoint refreshes a given session. +// +// This endpoint is useful for: +// +// - Session refresh +// +// Schemes: http, https +// +// Security: +// oryAccessToken: +// +// Responses: +// 200: successfulAdminIdentitySession +// 404: jsonError +// 500: jsonError +func (h *Handler) adminCurrentSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r) + if err != nil { + h.r.Audit().WithRequest(r).WithError(err).Info("No valid session cookie found.") + h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized.WithWrap(err).WithReasonf("No valid session cookie found.")) + return + } + if err := h.r.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, s.Refresh(h.r.Config(r.Context()))); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: s.Identity}) +} + // fandom-end func (h *Handler) IsAuthenticated(wrap httprouter.Handle, onUnauthenticated httprouter.Handle) httprouter.Handle { From e47990409333bc78f617707a2ab65d7c7a97c4fc Mon Sep 17 00:00:00 2001 From: abador Date: Wed, 29 Dec 2021 09:37:44 +0100 Subject: [PATCH 04/17] Remove route conflict, PLATFORM-6607 --- session/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session/handler.go b/session/handler.go index 2b8014b1b377..ca7a2850d1ee 100644 --- a/session/handler.go +++ b/session/handler.go @@ -49,7 +49,7 @@ const ( RouteCollection = "/sessions" RouteWhoami = RouteCollection + "/whoami" RouteRefresh = RouteCollection + "/refresh" - RouteSession = RouteCollection + "/:id" + RouteSession = RouteCollection + "/admin/:id" RouteIdentity = "/identities" RouteIdentityManagement = RouteIdentity + "/:id/sessions" RouteIdentitySession = RouteIdentity + "/:id/session" From f7f808f629178f5141bc31c79438c9f58973f2e1 Mon Sep 17 00:00:00 2001 From: abador Date: Wed, 29 Dec 2021 15:26:02 +0100 Subject: [PATCH 05/17] Routes compatible with upstream, PLATFORM-6607 --- session/handler.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/session/handler.go b/session/handler.go index ca7a2850d1ee..7f317ab52114 100644 --- a/session/handler.go +++ b/session/handler.go @@ -48,22 +48,20 @@ func NewHandler( const ( RouteCollection = "/sessions" RouteWhoami = RouteCollection + "/whoami" - RouteRefresh = RouteCollection + "/refresh" - RouteSession = RouteCollection + "/admin/:id" + RouteSession = RouteCollection + "/:id" RouteIdentity = "/identities" RouteIdentityManagement = RouteIdentity + "/:id/sessions" RouteIdentitySession = RouteIdentity + "/:id/session" ) func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { - for _, m := range []string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, - http.MethodDelete} { + for _, m := range []string{http.MethodHead, http.MethodPost, http.MethodPut, http.MethodDelete} { // Redirect to public endpoint admin.Handle(m, RouteWhoami, x.RedirectToPublicRoute(h.r)) } admin.DELETE(RouteIdentityManagement, h.deleteIdentitySessions) admin.PATCH(RouteSession, h.adminSessionRefresh) - admin.GET(RouteRefresh, h.adminCurrentSessionRefresh) + admin.GET(RouteSession, h.adminSessionRefresh) admin.GET(RouteIdentitySession, h.session) } @@ -185,7 +183,7 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.P // Refresh session if param was true refresh := r.URL.Query().Get("refresh") if h.r.Config(r.Context()).SessionWhoAmIRefresh() && refresh == "true" { - if err := h.r.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, s.Refresh(h.r.Config(r.Context()))); err != nil { + if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(h.r.Config(r.Context()))); err != nil { h.r.Writer().WriteError(w, r, err) return } @@ -345,12 +343,23 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. // 404: jsonError // 500: jsonError func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - s, err := h.r.SessionPersister().GetSession(r.Context(), x.ParseUUID(ps.ByName("id"))) + sid := ps.ByName("id") + if sid == "whoami" { + // Special case where we actually want to handle the whomai endpoint. + x.RedirectToPublicRoute(h.r)(w, r, ps) + return + } + if sid == "refresh" { + // Special case where we actually want to handle the refresh endpoint. + h.adminCurrentSessionRefresh(w, r, ps) + return + } + s, err := h.r.SessionPersister().GetSession(r.Context(), x.ParseUUID(sid)) if err != nil { h.r.Writer().WriteError(w, r, err) return } - if err := h.r.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, s.Refresh(h.r.Config(r.Context()))); err != nil { + if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(h.r.Config(r.Context()))); err != nil { h.r.Writer().WriteError(w, r, err) return } @@ -382,7 +391,7 @@ func (h *Handler) adminCurrentSessionRefresh(w http.ResponseWriter, r *http.Requ h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized.WithWrap(err).WithReasonf("No valid session cookie found.")) return } - if err := h.r.SessionManager().UpsertAndIssueCookie(r.Context(), w, r, s.Refresh(h.r.Config(r.Context()))); err != nil { + if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(h.r.Config(r.Context()))); err != nil { h.r.Writer().WriteError(w, r, err) return } From 9479410e2549f48448446a408db0612dc53da3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Harasimowicz?= Date: Tue, 11 Jan 2022 18:59:26 +0100 Subject: [PATCH 06/17] PLATFORM-6607| add configurable session refresh time window --- driver/config/config.go | 5 +++++ embedx/config.schema.json | 12 ++++++++++++ session/handler.go | 25 ++++++++++++++++--------- session/session.go | 8 ++++++++ session/session_test.go | 11 +++++++++++ 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index 2a23ceaacc48..b9c40dbeac63 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -96,6 +96,7 @@ const ( ViperKeySessionPersistentCookie = "session.cookie.persistent" ViperKeySessionWhoAmIAAL = "session.whoami.required_aal" ViperKeySessionWhoAmIRefresh = "session.whoami.refresh" + ViperKeySessionRefreshTimeWindow = "session.refresh_time_window" ViperKeyCookieSameSite = "cookies.same_site" ViperKeyCookieDomain = "cookies.domain" ViperKeyCookiePath = "cookies.path" @@ -1034,6 +1035,10 @@ func (p *Config) SessionWhoAmIRefresh() bool { return p.p.Bool(ViperKeySessionWhoAmIRefresh) } +func (p *Config) SessionRefreshTimeWindow() time.Duration { + return p.p.DurationF(ViperKeySessionRefreshTimeWindow, p.SessionLifespan()/2) +} + func (p *Config) SelfServiceSettingsRequiredAAL() string { return p.p.String(ViperKeySelfServiceSettingsRequiredAAL) } diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 24f0e3d5033d..dd9626c54087 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2174,6 +2174,18 @@ } }, "additionalProperties": false + }, + "refresh_time_window": { + "title": "Session refresh time window", + "description": "Time window when session can be refreshed to avoid excess refreshes. It is calculated as duration from the session expiration time.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "12h", + "examples": [ + "1h", + "1m", + "1s" + ] } } }, diff --git a/session/handler.go b/session/handler.go index 7f317ab52114..fabef2a3bf35 100644 --- a/session/handler.go +++ b/session/handler.go @@ -170,7 +170,8 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.P } var aalErr *ErrAALNotSatisfied - if err := h.r.SessionManager().DoesSessionSatisfy(r, s, h.r.Config(r.Context()).SessionWhoAmIAAL()); errors.As(err, &aalErr) { + c := h.r.Config(r.Context()) + if err := h.r.SessionManager().DoesSessionSatisfy(r, s, c.SessionWhoAmIAAL()); errors.As(err, &aalErr) { h.r.Audit().WithRequest(r).WithError(err).Info("Session was found but AAL is not satisfied for calling this endpoint.") h.r.Writer().WriteError(w, r, err) return @@ -182,8 +183,8 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.P // Refresh session if param was true refresh := r.URL.Query().Get("refresh") - if h.r.Config(r.Context()).SessionWhoAmIRefresh() && refresh == "true" { - if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(h.r.Config(r.Context()))); err != nil { + if c.SessionWhoAmIRefresh() && refresh == "true" && s.CanBeRefreshed(c) { + if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(c)); err != nil { h.r.Writer().WriteError(w, r, err) return } @@ -359,9 +360,12 @@ func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps h.r.Writer().WriteError(w, r, err) return } - if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(h.r.Config(r.Context()))); err != nil { - h.r.Writer().WriteError(w, r, err) - return + c := h.r.Config(r.Context()) + if s.CanBeRefreshed(c) { + if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(c)); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } } h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: s.Identity}) @@ -391,9 +395,12 @@ func (h *Handler) adminCurrentSessionRefresh(w http.ResponseWriter, r *http.Requ h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized.WithWrap(err).WithReasonf("No valid session cookie found.")) return } - if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(h.r.Config(r.Context()))); err != nil { - h.r.Writer().WriteError(w, r, err) - return + c := h.r.Config(r.Context()) + if s.CanBeRefreshed(c) { + if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(c)); err != nil { + h.r.Writer().WriteError(w, r, err) + return + } } h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: s.Identity}) diff --git a/session/session.go b/session/session.go index 96d9be4909c3..d842a3c7d6af 100644 --- a/session/session.go +++ b/session/session.go @@ -24,6 +24,10 @@ type lifespanProvider interface { SessionLifespan() time.Duration } +type refreshWindowProvider interface { + SessionRefreshTimeWindow() time.Duration +} + // A Session // // swagger:model session @@ -161,6 +165,10 @@ func (s *Session) Refresh(c lifespanProvider) *Session { return s } +func (s *Session) CanBeRefreshed(c refreshWindowProvider) bool { + return time.Now().Add(c.SessionRefreshTimeWindow()).After(s.ExpiresAt) +} + func (s *Session) IsActive() bool { return s.Active && s.ExpiresAt.After(time.Now()) && (s.Identity == nil || s.Identity.IsActive()) } diff --git a/session/session_test.go b/session/session_test.go index 63bed7b20324..5742e11fd8e2 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -60,6 +60,17 @@ func TestSession(t *testing.T) { assert.Empty(t, s.AuthenticatedAt) }) + t.Run("case=session refresh", func(t *testing.T) { + i := new(identity.Identity) + i.State = identity.StateActive + s, _ := session.NewActiveSession(i, conf, authAt, identity.CredentialsTypePassword) + assert.False(t, s.CanBeRefreshed(conf), "fresh session is not refreshable") + + s.ExpiresAt = s.ExpiresAt.Add(-12 * time.Hour) + assert.True(t, s.CanBeRefreshed(conf), "session is refreshable after 12hrs") + + }) + t.Run("case=aal", func(t *testing.T) { for _, tc := range []struct { d string From 3ae1e081f57a28c20c3b6ae700e2f1aac7ff7237 Mon Sep 17 00:00:00 2001 From: abador Date: Wed, 12 Jan 2022 09:07:42 +0100 Subject: [PATCH 07/17] CR fixes, PLATFORM-6607 --- session/handler.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/session/handler.go b/session/handler.go index fabef2a3bf35..c1fe265afda6 100644 --- a/session/handler.go +++ b/session/handler.go @@ -49,19 +49,22 @@ const ( RouteCollection = "/sessions" RouteWhoami = RouteCollection + "/whoami" RouteSession = RouteCollection + "/:id" + RouteSessionRefresh = RouteCollection + "/refresh" + RouteSessionRefreshId = RouteSessionRefresh + "/:id" RouteIdentity = "/identities" RouteIdentityManagement = RouteIdentity + "/:id/sessions" RouteIdentitySession = RouteIdentity + "/:id/session" ) func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { - for _, m := range []string{http.MethodHead, http.MethodPost, http.MethodPut, http.MethodDelete} { + for _, m := range []string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, + http.MethodDelete} { // Redirect to public endpoint admin.Handle(m, RouteWhoami, x.RedirectToPublicRoute(h.r)) } admin.DELETE(RouteIdentityManagement, h.deleteIdentitySessions) admin.PATCH(RouteSession, h.adminSessionRefresh) - admin.GET(RouteSession, h.adminSessionRefresh) + admin.PATCH(RouteSessionRefreshId, h.adminSessionRefresh) admin.GET(RouteIdentitySession, h.session) } @@ -326,7 +329,7 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: i}) } -// swagger:route PATCH /sessions/{id} v0alpha2 adminIdentitySession +// swagger:route PATCH /sessions/refresh/{id} v0alpha2 adminIdentitySession // // Calling this endpoint refreshes a given session. // @@ -345,16 +348,6 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. // 500: jsonError func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { sid := ps.ByName("id") - if sid == "whoami" { - // Special case where we actually want to handle the whomai endpoint. - x.RedirectToPublicRoute(h.r)(w, r, ps) - return - } - if sid == "refresh" { - // Special case where we actually want to handle the refresh endpoint. - h.adminCurrentSessionRefresh(w, r, ps) - return - } s, err := h.r.SessionPersister().GetSession(r.Context(), x.ParseUUID(sid)) if err != nil { h.r.Writer().WriteError(w, r, err) From 08199e6a40553aa7a3f8208657e1104189c8f54e Mon Sep 17 00:00:00 2001 From: abador Date: Wed, 12 Jan 2022 09:08:56 +0100 Subject: [PATCH 08/17] CR fixes, PLATFORM-6607 --- session/handler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/session/handler.go b/session/handler.go index c1fe265afda6..90e0929d5830 100644 --- a/session/handler.go +++ b/session/handler.go @@ -48,7 +48,6 @@ func NewHandler( const ( RouteCollection = "/sessions" RouteWhoami = RouteCollection + "/whoami" - RouteSession = RouteCollection + "/:id" RouteSessionRefresh = RouteCollection + "/refresh" RouteSessionRefreshId = RouteSessionRefresh + "/:id" RouteIdentity = "/identities" @@ -63,7 +62,7 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { admin.Handle(m, RouteWhoami, x.RedirectToPublicRoute(h.r)) } admin.DELETE(RouteIdentityManagement, h.deleteIdentitySessions) - admin.PATCH(RouteSession, h.adminSessionRefresh) + admin.PATCH(RouteSessionRefresh, h.adminSessionRefresh) admin.PATCH(RouteSessionRefreshId, h.adminSessionRefresh) admin.GET(RouteIdentitySession, h.session) } From 1f6613ee15bcb53a6a2b47dd0d1d4a364060bca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Harasimowicz?= Date: Wed, 12 Jan 2022 18:25:56 +0100 Subject: [PATCH 09/17] PLATFORM-6607| fix current session refresh endpoint --- session/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session/handler.go b/session/handler.go index 90e0929d5830..cbde4c97ed2d 100644 --- a/session/handler.go +++ b/session/handler.go @@ -62,7 +62,7 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { admin.Handle(m, RouteWhoami, x.RedirectToPublicRoute(h.r)) } admin.DELETE(RouteIdentityManagement, h.deleteIdentitySessions) - admin.PATCH(RouteSessionRefresh, h.adminSessionRefresh) + admin.PATCH(RouteSessionRefresh, h.adminCurrentSessionRefresh) admin.PATCH(RouteSessionRefreshId, h.adminSessionRefresh) admin.GET(RouteIdentitySession, h.session) } From 8913ee7f57c52c242a7f6062a8ff815618421136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Harasimowicz?= Date: Thu, 13 Jan 2022 13:18:19 +0100 Subject: [PATCH 10/17] PLATFORM-6607| fix session refresh time window --- session/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session/session.go b/session/session.go index d842a3c7d6af..e7c30675d801 100644 --- a/session/session.go +++ b/session/session.go @@ -166,7 +166,7 @@ func (s *Session) Refresh(c lifespanProvider) *Session { } func (s *Session) CanBeRefreshed(c refreshWindowProvider) bool { - return time.Now().Add(c.SessionRefreshTimeWindow()).After(s.ExpiresAt) + return s.ExpiresAt.Add(-c.SessionRefreshTimeWindow()).Before(time.Now()) } func (s *Session) IsActive() bool { From b797293ca73fb0ec5efecace6d68632fc0f987dd Mon Sep 17 00:00:00 2001 From: abador Date: Fri, 14 Jan 2022 10:57:39 +0100 Subject: [PATCH 11/17] CR fixesTests docs and all, PLATFORM-6607 --- driver/config/config.go | 2 +- embedx/config.schema.json | 2 +- internal/testhelpers/handler_mock.go | 5 +- session/handler.go | 25 ++++- session/handler_test.go | 158 ++++++++++++++++++++++++++- session/session.go | 3 +- session/session_test.go | 8 ++ 7 files changed, 193 insertions(+), 10 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index b9c40dbeac63..31f7ac436631 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -1036,7 +1036,7 @@ func (p *Config) SessionWhoAmIRefresh() bool { } func (p *Config) SessionRefreshTimeWindow() time.Duration { - return p.p.DurationF(ViperKeySessionRefreshTimeWindow, p.SessionLifespan()/2) + return p.p.DurationF(ViperKeySessionRefreshTimeWindow, p.SessionLifespan()) } func (p *Config) SelfServiceSettingsRequiredAAL() string { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index dd9626c54087..63122646759b 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2180,7 +2180,7 @@ "description": "Time window when session can be refreshed to avoid excess refreshes. It is calculated as duration from the session expiration time.", "type": "string", "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", - "default": "12h", + "default": "24h", "examples": [ "1h", "1m", diff --git a/internal/testhelpers/handler_mock.go b/internal/testhelpers/handler_mock.go index 570e8b93eb1b..47a2d441a1b7 100644 --- a/internal/testhelpers/handler_mock.go +++ b/internal/testhelpers/handler_mock.go @@ -92,7 +92,8 @@ func NewNoRedirectClientWithCookies(t *testing.T) *http.Client { } } -func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) { +func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) *http.Cookie { + var sessionCookie *http.Cookie res, err := c.Get(u) require.NoError(t, err) defer res.Body.Close() @@ -102,9 +103,11 @@ func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) { for _, c := range res.Cookies() { if c.Name == config.DefaultSessionCookieName { found = true + sessionCookie = c } } require.True(t, found) + return sessionCookie } func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identity.Identity) (httprouter.Handle, *session.Session) { diff --git a/session/handler.go b/session/handler.go index cbde4c97ed2d..571fae7468ee 100644 --- a/session/handler.go +++ b/session/handler.go @@ -1,6 +1,7 @@ package session import ( + "fmt" "net/http" "time" @@ -108,6 +109,12 @@ type toSession struct { // Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. // Additionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response. // +// It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. +// By default session refresh on this endpoint is disabled. +// Session refresh can be enabled only after setting `session.whoami.refresh` to true in the config. +// After enabling this option any refresh request will set the session life equal to `session.lifespan`. +// If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window` +// // If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: // // ```js @@ -139,6 +146,7 @@ type toSession struct { // - AJAX calls. Remember to send credentials and set up CORS correctly! // - Reverse proxies and API Gateways // - Server-side calls - use the `X-Session-Token` header! +// - Session refresh // // This endpoint authenticates users by checking // @@ -163,7 +171,7 @@ type toSession struct { // 401: jsonError // 403: jsonError // 500: jsonError -func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { +func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r) if err != nil { h.r.Audit().WithRequest(r).WithError(err).Info("No valid session cookie found.") @@ -186,10 +194,12 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, ps httprouter.P // Refresh session if param was true refresh := r.URL.Query().Get("refresh") if c.SessionWhoAmIRefresh() && refresh == "true" && s.CanBeRefreshed(c) { - if err := h.r.SessionPersister().UpsertSession(r.Context(), s.Refresh(c)); err != nil { + s = s.Refresh(c) + if err := h.r.SessionPersister().UpsertSession(r.Context(), s); err != nil { h.r.Writer().WriteError(w, r, err) return } + h.r.SessionManager().IssueCookie(r.Context(), w, r, s) } // s.Devices = nil @@ -331,6 +341,7 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. // swagger:route PATCH /sessions/refresh/{id} v0alpha2 adminIdentitySession // // Calling this endpoint refreshes a given session. +// If `session.refresh_time_window` is set it will only refresh the session after this time has passed. // // This endpoint is useful for: // @@ -346,8 +357,12 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. // 404: jsonError // 500: jsonError func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - sid := ps.ByName("id") - s, err := h.r.SessionPersister().GetSession(r.Context(), x.ParseUUID(sid)) + iID, err := uuid.FromString(ps.ByName("id")) + if err != nil { + h.r.Writer().WriteError(w, r, herodot.ErrBadRequest.WithError(err.Error()).WithDebug("could not parse UUID")) + return + } + s, err := h.r.SessionPersister().GetSession(r.Context(), iID) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -366,6 +381,7 @@ func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps // swagger:route GET /sessions/refresh v0alpha2 adminIdentitySession // // Calling this endpoint refreshes a given session. +// If `session.refresh_time_window` is set it will only refresh the session after this time has passed. // // This endpoint is useful for: // @@ -382,6 +398,7 @@ func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps // 500: jsonError func (h *Handler) adminCurrentSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r) + fmt.Printf("\n %+v \n", r.Cookies()) if err != nil { h.r.Audit().WithRequest(r).WithError(err).Info("No valid session cookie found.") h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized.WithWrap(err).WithReasonf("No valid session cookie found.")) diff --git a/session/handler_test.go b/session/handler_test.go index b4abb5a593a5..bde480e1e73c 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -5,7 +5,9 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/cookiejar" "net/http/httptest" + "strings" "testing" "time" @@ -41,8 +43,27 @@ func send(code int) httprouter.Handle { } } -func assertNoCSRFCookieInResponse(t *testing.T, ts *httptest.Server, c *http.Client, res *http.Response) { - assert.Len(t, res.Cookies(), 0, res.Cookies()) +func getSessionCookie(t *testing.T, r *http.Response) *http.Cookie { + var sessionCookie *http.Cookie + var found bool + for _, c := range r.Cookies() { + if c.Name == config.DefaultSessionCookieName { + found = true + sessionCookie = c + } + } + require.True(t, found) + return sessionCookie +} + +func assertNoCSRFCookieInResponse(t *testing.T, _ *httptest.Server, _ *http.Client, r *http.Response) { + found := false + for _, c := range r.Cookies() { + if strings.HasPrefix(c.Name, "csrf_token") { + found = true + } + } + require.False(t, found) } func TestSessionWhoAmI(t *testing.T) { @@ -136,6 +157,37 @@ func TestSessionWhoAmI(t *testing.T) { } }) + t.Run("case=whoami refresh", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + conf.MustSet(config.ViperKeySessionWhoAmIRefresh, "true") + + // No cookie yet -> 401 + res, err := client.Get(ts.URL + RouteWhoami) + require.NoError(t, err) + assertNoCSRFCookieInResponse(t, ts, client, res) // Test that no CSRF cookie is ever set here. + + // Set cookie + reg.CSRFHandler().IgnorePath("/set") + originalCookie := testhelpers.MockHydrateCookieClient(t, client, ts.URL+"/set") + originalCookie.Expires = originalCookie.Expires.Add(-time.Second) + + // Cookie set -> 200 (GET) + req, err := http.NewRequest("GET", ts.URL+RouteWhoami+"?refresh=true", nil) + require.NoError(t, err) + + res, err = client.Do(req) + require.NoError(t, err) + assertNoCSRFCookieInResponse(t, ts, client, res) // Test that no CSRF cookie is ever set here. + + assert.EqualValues(t, http.StatusOK, res.StatusCode) + assert.NotEmpty(t, res.Header.Get("X-Kratos-Authenticated-Identity-Id")) + updatedCookie := getSessionCookie(t, res) + + require.NotEmpty(t, updatedCookie) + require.NotEqual(t, originalCookie.Expires, updatedCookie.Expires) + assert.True(t, originalCookie.Expires.Before(updatedCookie.Expires)) + }) + /* @@ -401,6 +453,108 @@ func TestHandlerDeleteSessionByIdentityID(t *testing.T) { }) } +func TestHandlerRefreshSessionByIdentityID(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + _, ts, _, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + + // set this intermediate because kratos needs some valid url for CRUDE operations + conf.MustSet(config.ViperKeyPublicBaseURL, "http://example.com") + testhelpers.SetDefaultIdentitySchema(t, conf, "file://./stub/identity.schema.json") + conf.MustSet(config.ViperKeyPublicBaseURL, ts.URL) + + t.Run("case=should return 200 after refreshing one session", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + i := identity.NewIdentity("") + require.NoError(t, reg.IdentityManager().Create(context.Background(), i)) + s := &Session{Identity: i, ExpiresAt: time.Now().Add(5 * time.Minute)} + require.NoError(t, reg.SessionPersister().UpsertSession(context.Background(), s)) + + req, _ := http.NewRequest("PATCH", ts.URL+"/sessions/refresh/"+s.ID.String(), nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusOK, res.StatusCode) + + s, err = reg.SessionPersister().GetSession(context.Background(), s.ID) + require.Nil(t, err) + }) + + t.Run("case=should return 400 when bad UUID is sent", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + req, _ := http.NewRequest("PATCH", ts.URL+"/sessions/refresh/BADUUID", nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("case=should return 404 when calling with missing UUID", func(t *testing.T) { + client := testhelpers.NewClientWithCookies(t) + someID, _ := uuid.NewV4() + req, _ := http.NewRequest("PATCH", ts.URL+"/sessions/refresh/"+someID.String(), nil) + res, err := client.Do(req) + require.NoError(t, err) + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) +} + +func TestHandlerRefreshCurrentSession(t *testing.T) { + conf, reg := internal.NewFastRegistryWithMocks(t) + + // Start kratos server + publicTS, adminTS, r, _ := testhelpers.NewKratosServerWithCSRFAndRouters(t, reg) + h, _ := testhelpers.MockSessionCreateHandler(t, reg) + r.GET("/set", h) + + mockServerURL := urlx.ParseOrPanic(publicTS.URL) + + adminTS.URL = strings.Replace(adminTS.URL, "127.0.0.1", "localhost", -1) + reg.Config(context.Background()).MustSet(config.ViperKeyAdminBaseURL, adminTS.URL) + testhelpers.SetDefaultIdentitySchema(t, conf, "file://./stub/identity.schema.json") + testhelpers.SetIdentitySchemas(t, conf, map[string]string{ + "customer": "file://./stub/handler/customer.schema.json", + "employee": "file://./stub/handler/employee.schema.json", + }) + //conf.MustSet(config.ViperKeyPublicBaseURL, mockServerURL.String()) + + client := testhelpers.NewClientWithCookies(t) + // Set cookie + reg.CSRFHandler().IgnorePath("/set") + originalCookie := testhelpers.MockHydrateCookieClient(t, client, publicTS.URL+"/set") + originalCookie.Expires = originalCookie.Expires.Add(-time.Second) + + session := func(t *testing.T, base *httptest.Server, href string, expectCode int) AdminIdentitySessionResponse { + req, err := http.NewRequest("PATCH", base.URL+href, nil) + require.NoError(t, err) + cookies := client.Jar.Cookies(mockServerURL) + adminServerURL := urlx.ParseOrPanic(adminTS.URL) + cj, err := cookiejar.New(&cookiejar.Options{}) + require.NoError(t, err) + cj.SetCookies(adminServerURL, cookies) + base.Client().Jar = cj + + res, err := base.Client().Do(req) + require.NoError(t, err) + + require.EqualValues(t, expectCode, res.StatusCode) + defer res.Body.Close() + + var apiRes AdminIdentitySessionResponse + err = json.NewDecoder(res.Body).Decode(&apiRes) + require.NoError(t, err) + fmt.Print(apiRes) + + return apiRes + } + + t.Run("case=should return 200 after successful session refresh and return valid session and token", func(t *testing.T) { + res := session(t, adminTS, "/sessions/refresh", http.StatusOK) + s, err := reg.SessionPersister().GetSession(context.Background(), res.Session.ID) + require.Empty(t, err) + require.Equal(t, s.Token, res.Token) + require.True(t, res.Session.ExpiresAt.After(originalCookie.Expires)) + require.True(t, s.Active) + }) +} + func TestSessionRequest(t *testing.T) { conf, reg := internal.NewFastRegistryWithMocks(t) diff --git a/session/session.go b/session/session.go index d842a3c7d6af..cf2839214f05 100644 --- a/session/session.go +++ b/session/session.go @@ -162,11 +162,12 @@ func (s *Session) Declassify() *Session { func (s *Session) Refresh(c lifespanProvider) *Session { s.ExpiresAt = time.Now().Add(c.SessionLifespan()) + s.UpdatedAt = time.Now() return s } func (s *Session) CanBeRefreshed(c refreshWindowProvider) bool { - return time.Now().Add(c.SessionRefreshTimeWindow()).After(s.ExpiresAt) + return s.ExpiresAt.Add(-c.SessionRefreshTimeWindow()).Before(time.Now()) } func (s *Session) IsActive() bool { diff --git a/session/session_test.go b/session/session_test.go index 5742e11fd8e2..d517c5e552fc 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -4,6 +4,8 @@ import ( "testing" "time" + "github.com/ory/kratos/driver/config" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/assert" @@ -61,6 +63,12 @@ func TestSession(t *testing.T) { }) t.Run("case=session refresh", func(t *testing.T) { + conf.MustSet(config.ViperKeySessionLifespan, "24h") + conf.MustSet(config.ViperKeySessionRefreshTimeWindow, "12h") + t.Cleanup(func() { + conf.MustSet(config.ViperKeySessionLifespan, "1m") + conf.MustSet(config.ViperKeySessionRefreshTimeWindow, "1m") + }) i := new(identity.Identity) i.State = identity.StateActive s, _ := session.NewActiveSession(i, conf, authAt, identity.CredentialsTypePassword) From 10177d3c2d1b2ba1696df23a8382363c21db7c9c Mon Sep 17 00:00:00 2001 From: abador Date: Fri, 14 Jan 2022 15:31:04 +0100 Subject: [PATCH 12/17] upstream compatibility patch --- session/handler.go | 18 ++++++++++++++---- session/handler_test.go | 9 ++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/session/handler.go b/session/handler.go index 571fae7468ee..7fbff7d7dfad 100644 --- a/session/handler.go +++ b/session/handler.go @@ -338,7 +338,17 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: i}) } -// swagger:route PATCH /sessions/refresh/{id} v0alpha2 adminIdentitySession +// swagger:parameters adminSessionRefresh +// nolint:deadcode,unused +type adminSessionRefresh struct { + // ID is the session's ID. + // + // required: true + // in: path + ID string `json:"id"` +} + +// swagger:route PATCH /sessions/refresh/{id} v0alpha2 adminSessionRefresh // // Calling this endpoint refreshes a given session. // If `session.refresh_time_window` is set it will only refresh the session after this time has passed. @@ -353,7 +363,7 @@ func (h *Handler) session(w http.ResponseWriter, r *http.Request, ps httprouter. // oryAccessToken: // // Responses: -// 200: successfulAdminIdentitySession +// 200: session // 404: jsonError // 500: jsonError func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { @@ -375,7 +385,7 @@ func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps } } - h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: s.Identity}) + h.r.Writer().Write(w, r, s) } // swagger:route GET /sessions/refresh v0alpha2 adminIdentitySession @@ -412,7 +422,7 @@ func (h *Handler) adminCurrentSessionRefresh(w http.ResponseWriter, r *http.Requ } } - h.r.Writer().Write(w, r, &AdminIdentitySessionResponse{Session: s, Token: s.Token, Identity: s.Identity}) + h.r.Writer().Write(w, r, s) } // fandom-end diff --git a/session/handler_test.go b/session/handler_test.go index bde480e1e73c..b86e56130a11 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -521,7 +521,7 @@ func TestHandlerRefreshCurrentSession(t *testing.T) { originalCookie := testhelpers.MockHydrateCookieClient(t, client, publicTS.URL+"/set") originalCookie.Expires = originalCookie.Expires.Add(-time.Second) - session := func(t *testing.T, base *httptest.Server, href string, expectCode int) AdminIdentitySessionResponse { + session := func(t *testing.T, base *httptest.Server, href string, expectCode int) Session { req, err := http.NewRequest("PATCH", base.URL+href, nil) require.NoError(t, err) cookies := client.Jar.Cookies(mockServerURL) @@ -537,7 +537,7 @@ func TestHandlerRefreshCurrentSession(t *testing.T) { require.EqualValues(t, expectCode, res.StatusCode) defer res.Body.Close() - var apiRes AdminIdentitySessionResponse + var apiRes Session err = json.NewDecoder(res.Body).Decode(&apiRes) require.NoError(t, err) fmt.Print(apiRes) @@ -547,10 +547,9 @@ func TestHandlerRefreshCurrentSession(t *testing.T) { t.Run("case=should return 200 after successful session refresh and return valid session and token", func(t *testing.T) { res := session(t, adminTS, "/sessions/refresh", http.StatusOK) - s, err := reg.SessionPersister().GetSession(context.Background(), res.Session.ID) + s, err := reg.SessionPersister().GetSession(context.Background(), res.ID) require.Empty(t, err) - require.Equal(t, s.Token, res.Token) - require.True(t, res.Session.ExpiresAt.After(originalCookie.Expires)) + require.True(t, res.ExpiresAt.After(originalCookie.Expires)) require.True(t, s.Active) }) } From 504d4292498e96af1bab4bf4543fe0362a39951f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Harasimowicz?= Date: Mon, 17 Jan 2022 19:10:58 +0100 Subject: [PATCH 13/17] Update session/session.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Meller <52668809+mmeller-wikia@users.noreply.github.com> --- session/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session/session.go b/session/session.go index cf2839214f05..3079697d7016 100644 --- a/session/session.go +++ b/session/session.go @@ -167,7 +167,7 @@ func (s *Session) Refresh(c lifespanProvider) *Session { } func (s *Session) CanBeRefreshed(c refreshWindowProvider) bool { - return s.ExpiresAt.Add(-c.SessionRefreshTimeWindow()).Before(time.Now()) + return s.ExpiresAt.Sub(c.SessionRefreshTimeWindow()).Before(time.Now()) } func (s *Session) IsActive() bool { From 15fdac0a2ccfccbec4912a3f516a58e9498c6bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Harasimowicz?= Date: Mon, 17 Jan 2022 19:11:13 +0100 Subject: [PATCH 14/17] Update embedx/config.schema.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mikołaj Meller <52668809+mmeller-wikia@users.noreply.github.com> --- embedx/config.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 63122646759b..700849f5a02a 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2118,7 +2118,7 @@ }, "refresh": { "title": "Allow session refresh from whoami endpoint", - "description": "If set to true will will allow to refresh session lifespan if ?refresh=true is present", + "description": "If set to true will allow to refresh session lifespan if ?refresh=true is present", "type": "boolean", "default": false } From 24208c67ffdcbe2e6988c6356a9365643bbeb029 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Harasimowicz?= Date: Mon, 17 Jan 2022 20:38:36 +0100 Subject: [PATCH 15/17] PLATFORM-6607| fix Swagger + remove debug statements --- session/handler.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/session/handler.go b/session/handler.go index 7fbff7d7dfad..147cc5f1e27a 100644 --- a/session/handler.go +++ b/session/handler.go @@ -1,7 +1,6 @@ package session import ( - "fmt" "net/http" "time" @@ -388,9 +387,9 @@ func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps h.r.Writer().Write(w, r, s) } -// swagger:route GET /sessions/refresh v0alpha2 adminIdentitySession +// swagger:route GET /sessions/refresh v0alpha2 // -// Calling this endpoint refreshes a given session. +// Calling this endpoint refreshes a current user session. // If `session.refresh_time_window` is set it will only refresh the session after this time has passed. // // This endpoint is useful for: @@ -408,7 +407,6 @@ func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps // 500: jsonError func (h *Handler) adminCurrentSessionRefresh(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r) - fmt.Printf("\n %+v \n", r.Cookies()) if err != nil { h.r.Audit().WithRequest(r).WithError(err).Info("No valid session cookie found.") h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized.WithWrap(err).WithReasonf("No valid session cookie found.")) From b69eb970764595e559edbbe654e07479cfec111e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Harasimowicz?= Date: Mon, 17 Jan 2022 20:41:24 +0100 Subject: [PATCH 16/17] PLATFORM-6607| regenerate SDKs --- internal/httpclient/.openapi-generator/FILES | 2 + internal/httpclient/README.md | 2 + internal/httpclient/api/openapi.yaml | 80 ++++++++ internal/httpclient/api_default.go | 183 +++++++++++++++++++ internal/httpclient/api_v0alpha2.go | 171 +++++++++++++++++ internal/httpclient/client.go | 3 + internal/httpclient/docs/DefaultApi.md | 70 +++++++ internal/httpclient/docs/V0alpha2Api.md | 71 +++++++ spec/api.json | 104 ++++++++++- spec/swagger.json | 86 ++++++++- 10 files changed, 770 insertions(+), 2 deletions(-) create mode 100644 internal/httpclient/api_default.go create mode 100644 internal/httpclient/docs/DefaultApi.md diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES index 73915f3caef4..93abe7af405b 100644 --- a/internal/httpclient/.openapi-generator/FILES +++ b/internal/httpclient/.openapi-generator/FILES @@ -3,6 +3,7 @@ .travis.yml README.md api/openapi.yaml +api_default.go api_metadata.go api_v0alpha2.go client.go @@ -11,6 +12,7 @@ docs/AdminCreateIdentityBody.md docs/AdminCreateSelfServiceRecoveryLinkBody.md docs/AdminUpdateIdentityBody.md docs/AuthenticatorAssuranceLevel.md +docs/DefaultApi.md docs/ErrorAuthenticatorAssuranceLevelNotSatisfied.md docs/GenericError.md docs/HealthNotReadyStatus.md diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index dddbc40fead8..34b653c8dea1 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -83,6 +83,7 @@ All URIs are relative to *http://localhost* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- +*DefaultApi* | [**V0alpha2**](docs/DefaultApi.md#v0alpha2) | **Get** /sessions/refresh | Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. *MetadataApi* | [**GetVersion**](docs/MetadataApi.md#getversion) | **Get** /version | Return Running Software Version. *MetadataApi* | [**IsAlive**](docs/MetadataApi.md#isalive) | **Get** /health/alive | Check HTTP Server Status *MetadataApi* | [**IsReady**](docs/MetadataApi.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status @@ -93,6 +94,7 @@ Class | Method | HTTP request | Description *V0alpha2Api* | [**AdminGetIdentity**](docs/V0alpha2Api.md#admingetidentity) | **Get** /identities/{id} | Get an Identity *V0alpha2Api* | [**AdminIdentitySession**](docs/V0alpha2Api.md#adminidentitysession) | **Get** /identities/{id}/session | Calling this endpoint issues a session for a given identity. *V0alpha2Api* | [**AdminListIdentities**](docs/V0alpha2Api.md#adminlistidentities) | **Get** /identities | List Identities +*V0alpha2Api* | [**AdminSessionRefresh**](docs/V0alpha2Api.md#adminsessionrefresh) | **Patch** /sessions/refresh/{id} | Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. *V0alpha2Api* | [**AdminUpdateCredentials**](docs/V0alpha2Api.md#adminupdatecredentials) | **Put** /identities/{id}/credentials | Update Identity Credentials *V0alpha2Api* | [**AdminUpdateIdentity**](docs/V0alpha2Api.md#adminupdateidentity) | **Put** /identities/{id} | Update an Identity *V0alpha2Api* | [**CreateSelfServiceLogoutFlowUrlForBrowsers**](docs/V0alpha2Api.md#createselfservicelogoutflowurlforbrowsers) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml index e06d1e36601d..4d4149d4fb3e 100644 --- a/internal/httpclient/api/openapi.yaml +++ b/internal/httpclient/api/openapi.yaml @@ -2227,6 +2227,79 @@ paths: summary: Get Verification Flow tags: - v0alpha2 + /sessions/refresh: + get: + description: |- + This endpoint is useful for: + + Session refresh + operationId: v0alpha2 + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/successfulAdminIdentitySession' + description: successfulAdminIdentitySession + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/jsonError' + description: jsonError + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/jsonError' + description: jsonError + security: + - oryAccessToken: [] + summary: |- + Calling this endpoint refreshes a current user session. + If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + /sessions/refresh/{id}: + patch: + description: |- + This endpoint is useful for: + + Session refresh + operationId: adminSessionRefresh + parameters: + - description: ID is the session's ID. + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/session' + description: session + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/jsonError' + description: jsonError + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/jsonError' + description: jsonError + security: + - oryAccessToken: [] + summary: |- + Calling this endpoint refreshes a given session. + If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + tags: + - v0alpha2 /sessions/whoami: get: description: |- @@ -2234,6 +2307,12 @@ paths: Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. Additionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response. + It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. + By default session refresh on this endpoint is disabled. + Session refresh can be enabled only after setting `session.whoami.refresh` to true in the config. + After enabling this option any refresh request will set the session life equal to `session.lifespan`. + If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window` + If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: ```js @@ -2265,6 +2344,7 @@ paths: AJAX calls. Remember to send credentials and set up CORS correctly! Reverse proxies and API Gateways Server-side calls - use the `X-Session-Token` header! + Session refresh This endpoint authenticates users by checking diff --git a/internal/httpclient/api_default.go b/internal/httpclient/api_default.go new file mode 100644 index 000000000000..53af803048a9 --- /dev/null +++ b/internal/httpclient/api_default.go @@ -0,0 +1,183 @@ +/* + * Ory Kratos API + * + * Documentation for all public and administrative Ory Kratos APIs. Public and administrative APIs are exposed on different ports. Public APIs can face the public internet without any protection while administrative APIs should never be exposed without prior authorization. To protect the administative API port you should use something like Nginx, Ory Oathkeeper, or any other technology capable of authorizing incoming requests. + * + * API version: 1.0.0 + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "io/ioutil" + "net/http" + "net/url" +) + +// Linger please +var ( + _ context.Context +) + +type DefaultApi interface { + + /* + * V0alpha2 Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + * This endpoint is useful for: + + Session refresh + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return DefaultApiApiV0alpha2Request + */ + V0alpha2(ctx context.Context) DefaultApiApiV0alpha2Request + + /* + * V0alpha2Execute executes the request + * @return SuccessfulAdminIdentitySession + */ + V0alpha2Execute(r DefaultApiApiV0alpha2Request) (*SuccessfulAdminIdentitySession, *http.Response, error) +} + +// DefaultApiService DefaultApi service +type DefaultApiService service + +type DefaultApiApiV0alpha2Request struct { + ctx context.Context + ApiService DefaultApi +} + +func (r DefaultApiApiV0alpha2Request) Execute() (*SuccessfulAdminIdentitySession, *http.Response, error) { + return r.ApiService.V0alpha2Execute(r) +} + +/* + * V0alpha2 Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + * This endpoint is useful for: + +Session refresh + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return DefaultApiApiV0alpha2Request +*/ +func (a *DefaultApiService) V0alpha2(ctx context.Context) DefaultApiApiV0alpha2Request { + return DefaultApiApiV0alpha2Request{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return SuccessfulAdminIdentitySession + */ +func (a *DefaultApiService) V0alpha2Execute(r DefaultApiApiV0alpha2Request) (*SuccessfulAdminIdentitySession, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *SuccessfulAdminIdentitySession + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultApiService.V0alpha2") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/sessions/refresh" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["oryAccessToken"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["Authorization"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 404 { + var v JsonError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v JsonError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/internal/httpclient/api_v0alpha2.go b/internal/httpclient/api_v0alpha2.go index 48a1a2fb6331..ee9da5546368 100644 --- a/internal/httpclient/api_v0alpha2.go +++ b/internal/httpclient/api_v0alpha2.go @@ -142,6 +142,23 @@ type V0alpha2Api interface { */ AdminListIdentitiesExecute(r V0alpha2ApiApiAdminListIdentitiesRequest) ([]Identity, *http.Response, error) + /* + * AdminSessionRefresh Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + * This endpoint is useful for: + + Session refresh + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param id ID is the session's ID. + * @return V0alpha2ApiApiAdminSessionRefreshRequest + */ + AdminSessionRefresh(ctx context.Context, id string) V0alpha2ApiApiAdminSessionRefreshRequest + + /* + * AdminSessionRefreshExecute executes the request + * @return Session + */ + AdminSessionRefreshExecute(r V0alpha2ApiApiAdminSessionRefreshRequest) (*Session, *http.Response, error) + /* * AdminUpdateCredentials Update Identity Credentials * Calling this endpoint updates the credentials according to the specification provided. @@ -993,6 +1010,12 @@ type V0alpha2Api interface { Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. Additionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response. + It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. + By default session refresh on this endpoint is disabled. + Session refresh can be enabled only after setting `session.whoami.refresh` to true in the config. + After enabling this option any refresh request will set the session life equal to `session.lifespan`. + If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window` + If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: ```js @@ -1024,6 +1047,7 @@ type V0alpha2Api interface { AJAX calls. Remember to send credentials and set up CORS correctly! Reverse proxies and API Gateways Server-side calls - use the `X-Session-Token` header! + Session refresh This endpoint authenticates users by checking @@ -2064,6 +2088,146 @@ func (a *V0alpha2ApiService) AdminListIdentitiesExecute(r V0alpha2ApiApiAdminLis return localVarReturnValue, localVarHTTPResponse, nil } +type V0alpha2ApiApiAdminSessionRefreshRequest struct { + ctx context.Context + ApiService V0alpha2Api + id string +} + +func (r V0alpha2ApiApiAdminSessionRefreshRequest) Execute() (*Session, *http.Response, error) { + return r.ApiService.AdminSessionRefreshExecute(r) +} + +/* + * AdminSessionRefresh Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + * This endpoint is useful for: + +Session refresh + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @param id ID is the session's ID. + * @return V0alpha2ApiApiAdminSessionRefreshRequest +*/ +func (a *V0alpha2ApiService) AdminSessionRefresh(ctx context.Context, id string) V0alpha2ApiApiAdminSessionRefreshRequest { + return V0alpha2ApiApiAdminSessionRefreshRequest{ + ApiService: a, + ctx: ctx, + id: id, + } +} + +/* + * Execute executes the request + * @return Session + */ +func (a *V0alpha2ApiService) AdminSessionRefreshExecute(r V0alpha2ApiApiAdminSessionRefreshRequest) (*Session, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPatch + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *Session + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "V0alpha2ApiService.AdminSessionRefresh") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/sessions/refresh/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterToString(r.id, "")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + if apiKey, ok := auth["oryAccessToken"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + localVarHeaderParams["Authorization"] = key + } + } + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := ioutil.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = ioutil.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 404 { + var v JsonError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v JsonError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type V0alpha2ApiApiAdminUpdateCredentialsRequest struct { ctx context.Context ApiService V0alpha2Api @@ -6515,6 +6679,12 @@ func (r V0alpha2ApiApiToSessionRequest) Execute() (*Session, *http.Response, err Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent. Additionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response. +It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. +By default session refresh on this endpoint is disabled. +Session refresh can be enabled only after setting `session.whoami.refresh` to true in the config. +After enabling this option any refresh request will set the session life equal to `session.lifespan`. +If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window` + If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: ```js @@ -6546,6 +6716,7 @@ This endpoint is useful for: AJAX calls. Remember to send credentials and set up CORS correctly! Reverse proxies and API Gateways Server-side calls - use the `X-Session-Token` header! +Session refresh This endpoint authenticates users by checking diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index 0469d3c1468c..cb1e4c59c9ac 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -49,6 +49,8 @@ type APIClient struct { // API Services + DefaultApi DefaultApi + MetadataApi MetadataApi V0alpha2Api V0alpha2Api @@ -70,6 +72,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.common.client = c // API Services + c.DefaultApi = (*DefaultApiService)(&c.common) c.MetadataApi = (*MetadataApiService)(&c.common) c.V0alpha2Api = (*V0alpha2ApiService)(&c.common) diff --git a/internal/httpclient/docs/DefaultApi.md b/internal/httpclient/docs/DefaultApi.md new file mode 100644 index 000000000000..8c255417dc93 --- /dev/null +++ b/internal/httpclient/docs/DefaultApi.md @@ -0,0 +1,70 @@ +# \DefaultApi + +All URIs are relative to *http://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**V0alpha2**](DefaultApi.md#V0alpha2) | **Get** /sessions/refresh | Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + + + +## V0alpha2 + +> SuccessfulAdminIdentitySession V0alpha2(ctx).Execute() + +Calling this endpoint refreshes a current user session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.DefaultApi.V0alpha2(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DefaultApi.V0alpha2``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `V0alpha2`: SuccessfulAdminIdentitySession + fmt.Fprintf(os.Stdout, "Response from `DefaultApi.V0alpha2`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiV0alpha2Request struct via the builder pattern + + +### Return type + +[**SuccessfulAdminIdentitySession**](SuccessfulAdminIdentitySession.md) + +### Authorization + +[oryAccessToken](../README.md#oryAccessToken) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + diff --git a/internal/httpclient/docs/V0alpha2Api.md b/internal/httpclient/docs/V0alpha2Api.md index 5ac25231d2b7..cd5abf7ba5b1 100644 --- a/internal/httpclient/docs/V0alpha2Api.md +++ b/internal/httpclient/docs/V0alpha2Api.md @@ -11,6 +11,7 @@ Method | HTTP request | Description [**AdminGetIdentity**](V0alpha2Api.md#AdminGetIdentity) | **Get** /identities/{id} | Get an Identity [**AdminIdentitySession**](V0alpha2Api.md#AdminIdentitySession) | **Get** /identities/{id}/session | Calling this endpoint issues a session for a given identity. [**AdminListIdentities**](V0alpha2Api.md#AdminListIdentities) | **Get** /identities | List Identities +[**AdminSessionRefresh**](V0alpha2Api.md#AdminSessionRefresh) | **Patch** /sessions/refresh/{id} | Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. [**AdminUpdateCredentials**](V0alpha2Api.md#AdminUpdateCredentials) | **Put** /identities/{id}/credentials | Update Identity Credentials [**AdminUpdateIdentity**](V0alpha2Api.md#AdminUpdateIdentity) | **Put** /identities/{id} | Update an Identity [**CreateSelfServiceLogoutFlowUrlForBrowsers**](V0alpha2Api.md#CreateSelfServiceLogoutFlowUrlForBrowsers) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers @@ -522,6 +523,76 @@ Name | Type | Description | Notes [[Back to README]](../README.md) +## AdminSessionRefresh + +> Session AdminSessionRefresh(ctx, id).Execute() + +Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + id := "id_example" // string | ID is the session's ID. + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.V0alpha2Api.AdminSessionRefresh(context.Background(), id).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `V0alpha2Api.AdminSessionRefresh``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `AdminSessionRefresh`: Session + fmt.Fprintf(os.Stdout, "Response from `V0alpha2Api.AdminSessionRefresh`: %v\n", resp) +} +``` + +### Path Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- +**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. +**id** | **string** | ID is the session's ID. | + +### Other Parameters + +Other parameters are passed through a pointer to a apiAdminSessionRefreshRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + + +### Return type + +[**Session**](Session.md) + +### Authorization + +[oryAccessToken](../README.md#oryAccessToken) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## AdminUpdateCredentials > AdminUpdateCredentials(ctx, id).Execute() diff --git a/spec/api.json b/spec/api.json index b1c4c35999f1..6a3d17f233ba 100755 --- a/spec/api.json +++ b/spec/api.json @@ -4332,9 +4332,111 @@ ] } }, + "/sessions/refresh": { + "get": { + "description": "This endpoint is useful for:\n\nSession refresh", + "operationId": "v0alpha2", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/successfulAdminIdentitySession" + } + } + }, + "description": "successfulAdminIdentitySession" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + } + }, + "security": [ + { + "oryAccessToken": [] + } + ], + "summary": "Calling this endpoint refreshes a current user session.\nIf `session.refresh_time_window` is set it will only refresh the session after this time has passed." + } + }, + "/sessions/refresh/{id}": { + "patch": { + "description": "This endpoint is useful for:\n\nSession refresh", + "operationId": "adminSessionRefresh", + "parameters": [ + { + "description": "ID is the session's ID.", + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/session" + } + } + }, + "description": "session" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/jsonError" + } + } + }, + "description": "jsonError" + } + }, + "security": [ + { + "oryAccessToken": [] + } + ], + "summary": "Calling this endpoint refreshes a given session.\nIf `session.refresh_time_window` is set it will only refresh the session after this time has passed.", + "tags": [ + "v0alpha2" + ] + } + }, "/sessions/whoami": { "get": { - "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\n\nThis endpoint authenticates users by checking\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cooke or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", + "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nIt is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url.\nBy default session refresh on this endpoint is disabled.\nSession refresh can be enabled only after setting `session.whoami.refresh` to true in the config.\nAfter enabling this option any refresh request will set the session life equal to `session.lifespan`.\nIf you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window`\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\nSession refresh\n\nThis endpoint authenticates users by checking\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cooke or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", "operationId": "toSession", "parameters": [ { diff --git a/spec/swagger.json b/spec/swagger.json index 33b58cb2c569..5b96a8187b71 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -2005,9 +2005,93 @@ } } }, + "/sessions/refresh": { + "get": { + "security": [ + { + "oryAccessToken": [] + } + ], + "description": "This endpoint is useful for:\n\nSession refresh", + "schemes": [ + "http", + "https" + ], + "summary": "Calling this endpoint refreshes a current user session.\nIf `session.refresh_time_window` is set it will only refresh the session after this time has passed.", + "operationId": "v0alpha2", + "responses": { + "200": { + "description": "successfulAdminIdentitySession", + "schema": { + "$ref": "#/definitions/successfulAdminIdentitySession" + } + }, + "404": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + }, + "500": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + } + } + } + }, + "/sessions/refresh/{id}": { + "patch": { + "security": [ + { + "oryAccessToken": [] + } + ], + "description": "This endpoint is useful for:\n\nSession refresh", + "schemes": [ + "http", + "https" + ], + "tags": [ + "v0alpha2" + ], + "summary": "Calling this endpoint refreshes a given session.\nIf `session.refresh_time_window` is set it will only refresh the session after this time has passed.", + "operationId": "adminSessionRefresh", + "parameters": [ + { + "type": "string", + "description": "ID is the session's ID.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "session", + "schema": { + "$ref": "#/definitions/session" + } + }, + "404": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + }, + "500": { + "description": "jsonError", + "schema": { + "$ref": "#/definitions/jsonError" + } + } + } + } + }, "/sessions/whoami": { "get": { - "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\n\nThis endpoint authenticates users by checking\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cooke or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", + "description": "Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.\nReturns a session object in the body or 401 if the credentials are invalid or no credentials were sent.\nAdditionally when the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header in the response.\n\nIt is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url.\nBy default session refresh on this endpoint is disabled.\nSession refresh can be enabled only after setting `session.whoami.refresh` to true in the config.\nAfter enabling this option any refresh request will set the session life equal to `session.lifespan`.\nIf you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window`\n\nIf you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:\n\n```js\npseudo-code example\nrouter.get('/protected-endpoint', async function (req, res) {\nconst session = await client.toSession(undefined, req.header('cookie'))\n\nconsole.log(session)\n})\n```\n\nWhen calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:\n\n```js\npseudo-code example\n...\nconst session = await client.toSession(\"the-session-token\")\n\nconsole.log(session)\n```\n\nDepending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator\nAssurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn\ncredentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user\nto sign in with the second factor or change the configuration.\n\nThis endpoint is useful for:\n\nAJAX calls. Remember to send credentials and set up CORS correctly!\nReverse proxies and API Gateways\nServer-side calls - use the `X-Session-Token` header!\nSession refresh\n\nThis endpoint authenticates users by checking\n\nif the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;\nif the `Authorization: bearer \u003cory-session-token\u003e` HTTP header was set with a valid Ory Kratos Session Token;\nif the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.\n\nIf none of these headers are set or the cooke or token are invalid, the endpoint returns a HTTP 401 status code.\n\nAs explained above, this request may fail due to several reasons. The `error.id` can be one of:\n\n`session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).\n`session_aal2_required`: An active session was found but it does not fulfil the Authenticator Assurance Level, implying that the session must (e.g.) authenticate the second factor.", "produces": [ "application/json" ], From 0caadacf4187863a9384f96a6a04e69a277a05bf Mon Sep 17 00:00:00 2001 From: abador Date: Tue, 18 Jan 2022 20:46:57 +0100 Subject: [PATCH 17/17] Upstream changes, PLATFORM-6607 --- driver/config/config.go | 12 ++++++------ driver/config/config_test.go | 8 ++++++++ embedx/config.schema.json | 6 +++--- internal/httpclient/README.md | 2 +- internal/httpclient/api/openapi.yaml | 6 +++--- internal/testhelpers/handler_mock.go | 6 +++--- session/handler.go | 10 +++++----- session/handler_test.go | 2 +- session/session.go | 5 ++--- session/session_test.go | 4 ++-- 10 files changed, 34 insertions(+), 27 deletions(-) diff --git a/driver/config/config.go b/driver/config/config.go index 31f7ac436631..4ac01cf46c18 100644 --- a/driver/config/config.go +++ b/driver/config/config.go @@ -95,8 +95,8 @@ const ( ViperKeySessionPath = "session.cookie.path" ViperKeySessionPersistentCookie = "session.cookie.persistent" ViperKeySessionWhoAmIAAL = "session.whoami.required_aal" - ViperKeySessionWhoAmIRefresh = "session.whoami.refresh" - ViperKeySessionRefreshTimeWindow = "session.refresh_time_window" + ViperKeySessionWhoAmIRefreshAllowed = "session.whoami.refresh_allowed" + ViperKeySessionRefreshMinTimeLeft = "session.refresh_min_time_left" ViperKeyCookieSameSite = "cookies.same_site" ViperKeyCookieDomain = "cookies.domain" ViperKeyCookiePath = "cookies.path" @@ -1031,12 +1031,12 @@ func (p *Config) SessionWhoAmIAAL() string { return p.p.String(ViperKeySessionWhoAmIAAL) } -func (p *Config) SessionWhoAmIRefresh() bool { - return p.p.Bool(ViperKeySessionWhoAmIRefresh) +func (p *Config) SessionWhoAmIRefreshAllowed() bool { + return p.p.Bool(ViperKeySessionWhoAmIRefreshAllowed) } -func (p *Config) SessionRefreshTimeWindow() time.Duration { - return p.p.DurationF(ViperKeySessionRefreshTimeWindow, p.SessionLifespan()) +func (p *Config) SessionRefreshMinTimeLeft() time.Duration { + return p.p.DurationF(ViperKeySessionRefreshMinTimeLeft, p.SessionLifespan()) } func (p *Config) SelfServiceSettingsRequiredAAL() string { diff --git a/driver/config/config_test.go b/driver/config/config_test.go index 332a2a26534e..094f1490e6e4 100644 --- a/driver/config/config_test.go +++ b/driver/config/config_test.go @@ -598,6 +598,10 @@ func TestSession(t *testing.T) { p.MustSet(config.ViperKeySessionName, "ory_session") assert.Equal(t, "ory_session", p.SessionName()) + assert.Equal(t, time.Hour*24, p.SessionRefreshMinTimeLeft()) + p.MustSet(config.ViperKeySessionRefreshMinTimeLeft, "1m") + assert.Equal(t, time.Minute, p.SessionRefreshMinTimeLeft()) + assert.Equal(t, time.Hour*24, p.SessionLifespan()) p.MustSet(config.ViperKeySessionLifespan, "1m") assert.Equal(t, time.Minute, p.SessionLifespan()) @@ -605,6 +609,10 @@ func TestSession(t *testing.T) { assert.Equal(t, true, p.SessionPersistentCookie()) p.MustSet(config.ViperKeySessionPersistentCookie, false) assert.Equal(t, false, p.SessionPersistentCookie()) + + assert.Equal(t, false, p.SessionWhoAmIRefreshAllowed()) + p.MustSet(config.ViperKeySessionWhoAmIRefreshAllowed, true) + assert.Equal(t, true, p.SessionWhoAmIRefreshAllowed()) } func TestCookies(t *testing.T) { diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 700849f5a02a..a45c289e6cb5 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -2116,7 +2116,7 @@ "required_aal": { "$ref": "#/definitions/featureRequiredAal" }, - "refresh": { + "refresh_allowed": { "title": "Allow session refresh from whoami endpoint", "description": "If set to true will allow to refresh session lifespan if ?refresh=true is present", "type": "boolean", @@ -2175,8 +2175,8 @@ }, "additionalProperties": false }, - "refresh_time_window": { - "title": "Session refresh time window", + "refresh_min_time_left": { + "title": "Session refresh minimal time left", "description": "Time window when session can be refreshed to avoid excess refreshes. It is calculated as duration from the session expiration time.", "type": "string", "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md index 34b653c8dea1..fc898fd0ba0e 100644 --- a/internal/httpclient/README.md +++ b/internal/httpclient/README.md @@ -94,7 +94,7 @@ Class | Method | HTTP request | Description *V0alpha2Api* | [**AdminGetIdentity**](docs/V0alpha2Api.md#admingetidentity) | **Get** /identities/{id} | Get an Identity *V0alpha2Api* | [**AdminIdentitySession**](docs/V0alpha2Api.md#adminidentitysession) | **Get** /identities/{id}/session | Calling this endpoint issues a session for a given identity. *V0alpha2Api* | [**AdminListIdentities**](docs/V0alpha2Api.md#adminlistidentities) | **Get** /identities | List Identities -*V0alpha2Api* | [**AdminSessionRefresh**](docs/V0alpha2Api.md#adminsessionrefresh) | **Patch** /sessions/refresh/{id} | Calling this endpoint refreshes a given session. If `session.refresh_time_window` is set it will only refresh the session after this time has passed. +*V0alpha2Api* | [**AdminSessionRefresh**](docs/V0alpha2Api.md#adminsessionrefresh) | **Patch** /sessions/refresh/{id} | Calling this endpoint refreshes a given session. If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. *V0alpha2Api* | [**AdminUpdateCredentials**](docs/V0alpha2Api.md#adminupdatecredentials) | **Put** /identities/{id}/credentials | Update Identity Credentials *V0alpha2Api* | [**AdminUpdateIdentity**](docs/V0alpha2Api.md#adminupdateidentity) | **Put** /identities/{id} | Update an Identity *V0alpha2Api* | [**CreateSelfServiceLogoutFlowUrlForBrowsers**](docs/V0alpha2Api.md#createselfservicelogoutflowurlforbrowsers) | **Get** /self-service/logout/browser | Create a Logout URL for Browsers diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml index 4d4149d4fb3e..ea0f908aac3d 100644 --- a/internal/httpclient/api/openapi.yaml +++ b/internal/httpclient/api/openapi.yaml @@ -2297,7 +2297,7 @@ paths: - oryAccessToken: [] summary: |- Calling this endpoint refreshes a given session. - If `session.refresh_time_window` is set it will only refresh the session after this time has passed. + If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. tags: - v0alpha2 /sessions/whoami: @@ -2309,9 +2309,9 @@ paths: It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. By default session refresh on this endpoint is disabled. - Session refresh can be enabled only after setting `session.whoami.refresh` to true in the config. + Session refresh can be enabled only after setting `session.whoami.refresh_allowed` to true in the config. After enabling this option any refresh request will set the session life equal to `session.lifespan`. - If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window` + If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_min_time_left` If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: diff --git a/internal/testhelpers/handler_mock.go b/internal/testhelpers/handler_mock.go index 47a2d441a1b7..b52ffc0c88ee 100644 --- a/internal/testhelpers/handler_mock.go +++ b/internal/testhelpers/handler_mock.go @@ -100,10 +100,10 @@ func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) *http.Cooki assert.EqualValues(t, http.StatusOK, res.StatusCode) var found bool - for _, c := range res.Cookies() { - if c.Name == config.DefaultSessionCookieName { + for _, rc := range res.Cookies() { + if rc.Name == config.DefaultSessionCookieName { found = true - sessionCookie = c + sessionCookie = rc } } require.True(t, found) diff --git a/session/handler.go b/session/handler.go index 147cc5f1e27a..621b0b7a69ac 100644 --- a/session/handler.go +++ b/session/handler.go @@ -110,9 +110,9 @@ type toSession struct { // // It is also possible to refresh the session lifespan of a current session by adding a `refresh=true` param to the request url. // By default session refresh on this endpoint is disabled. -// Session refresh can be enabled only after setting `session.whoami.refresh` to true in the config. +// Session refresh can be enabled only after setting `session.whoami.refresh_allowed` to true in the config. // After enabling this option any refresh request will set the session life equal to `session.lifespan`. -// If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_time_window` +// If you want to refresh the session only some time before session expiration you can set a proper value for `session.refresh_min_time_left` // // If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint: // @@ -192,7 +192,7 @@ func (h *Handler) whoami(w http.ResponseWriter, r *http.Request, _ httprouter.Pa // Refresh session if param was true refresh := r.URL.Query().Get("refresh") - if c.SessionWhoAmIRefresh() && refresh == "true" && s.CanBeRefreshed(c) { + if c.SessionWhoAmIRefreshAllowed() && refresh == "true" && s.CanBeRefreshed(c) { s = s.Refresh(c) if err := h.r.SessionPersister().UpsertSession(r.Context(), s); err != nil { h.r.Writer().WriteError(w, r, err) @@ -350,7 +350,7 @@ type adminSessionRefresh struct { // swagger:route PATCH /sessions/refresh/{id} v0alpha2 adminSessionRefresh // // Calling this endpoint refreshes a given session. -// If `session.refresh_time_window` is set it will only refresh the session after this time has passed. +// If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. // // This endpoint is useful for: // @@ -390,7 +390,7 @@ func (h *Handler) adminSessionRefresh(w http.ResponseWriter, r *http.Request, ps // swagger:route GET /sessions/refresh v0alpha2 // // Calling this endpoint refreshes a current user session. -// If `session.refresh_time_window` is set it will only refresh the session after this time has passed. +// If `session.refresh_min_time_left` is set it will only refresh the session after this time has passed. // // This endpoint is useful for: // diff --git a/session/handler_test.go b/session/handler_test.go index b86e56130a11..2b4eb2335221 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -159,7 +159,7 @@ func TestSessionWhoAmI(t *testing.T) { t.Run("case=whoami refresh", func(t *testing.T) { client := testhelpers.NewClientWithCookies(t) - conf.MustSet(config.ViperKeySessionWhoAmIRefresh, "true") + conf.MustSet(config.ViperKeySessionWhoAmIRefreshAllowed, "true") // No cookie yet -> 401 res, err := client.Get(ts.URL + RouteWhoami) diff --git a/session/session.go b/session/session.go index 3079697d7016..a40e66d0913b 100644 --- a/session/session.go +++ b/session/session.go @@ -25,7 +25,7 @@ type lifespanProvider interface { } type refreshWindowProvider interface { - SessionRefreshTimeWindow() time.Duration + SessionRefreshMinTimeLeft() time.Duration } // A Session @@ -162,12 +162,11 @@ func (s *Session) Declassify() *Session { func (s *Session) Refresh(c lifespanProvider) *Session { s.ExpiresAt = time.Now().Add(c.SessionLifespan()) - s.UpdatedAt = time.Now() return s } func (s *Session) CanBeRefreshed(c refreshWindowProvider) bool { - return s.ExpiresAt.Sub(c.SessionRefreshTimeWindow()).Before(time.Now()) + return s.ExpiresAt.Add(-c.SessionRefreshMinTimeLeft()).Before(time.Now()) } func (s *Session) IsActive() bool { diff --git a/session/session_test.go b/session/session_test.go index d517c5e552fc..50cfd404a9e0 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -64,10 +64,10 @@ func TestSession(t *testing.T) { t.Run("case=session refresh", func(t *testing.T) { conf.MustSet(config.ViperKeySessionLifespan, "24h") - conf.MustSet(config.ViperKeySessionRefreshTimeWindow, "12h") + conf.MustSet(config.ViperKeySessionRefreshMinTimeLeft, "12h") t.Cleanup(func() { conf.MustSet(config.ViperKeySessionLifespan, "1m") - conf.MustSet(config.ViperKeySessionRefreshTimeWindow, "1m") + conf.MustSet(config.ViperKeySessionRefreshMinTimeLeft, "1m") }) i := new(identity.Identity) i.State = identity.StateActive