Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
549 lines
17 KiB
Go
549 lines
17 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package session
|
|
|
|
import (
|
|
"context"
|
|
stderrors "errors"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/ory/kratos/x/nosurfx"
|
|
"github.com/ory/kratos/x/redir"
|
|
"github.com/ory/x/logrusx"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/ory/kratos/x/events"
|
|
|
|
"github.com/ory/kratos/selfservice/flow"
|
|
"github.com/ory/kratos/selfservice/sessiontokenexchange"
|
|
"github.com/ory/kratos/ui/node"
|
|
"github.com/ory/x/otelx"
|
|
|
|
"github.com/ory/x/randx"
|
|
|
|
"github.com/gorilla/sessions"
|
|
|
|
"github.com/ory/x/urlx"
|
|
|
|
"github.com/gofrs/uuid"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/ory/kratos/driver/config"
|
|
|
|
"github.com/ory/x/sqlcon"
|
|
|
|
"github.com/ory/herodot"
|
|
|
|
"github.com/ory/kratos/identity"
|
|
"github.com/ory/kratos/x"
|
|
"github.com/ory/kratos/x/transaction"
|
|
)
|
|
|
|
func ErrNoAALAvailable() *herodot.DefaultError {
|
|
return herodot.ErrForbidden().WithReasonf("Unable to detect available authentication methods. Perform account recovery or contact support.")
|
|
}
|
|
|
|
type (
|
|
managerHTTPDependencies interface {
|
|
config.Provider
|
|
identity.PoolProvider
|
|
identity.PrivilegedPoolProvider
|
|
identity.ManagementProvider
|
|
x.CookieProvider
|
|
logrusx.Provider
|
|
nosurfx.CSRFProvider
|
|
otelx.Provider
|
|
transaction.PersistenceProvider
|
|
PersistenceProvider
|
|
sessiontokenexchange.PersistenceProvider
|
|
}
|
|
ManagerHTTP struct {
|
|
cookieName func(ctx context.Context) string
|
|
r managerHTTPDependencies
|
|
}
|
|
)
|
|
|
|
func NewManagerHTTP(r managerHTTPDependencies) *ManagerHTTP {
|
|
return &ManagerHTTP{
|
|
r: r,
|
|
cookieName: func(ctx context.Context) string {
|
|
return r.Config().SessionName(ctx)
|
|
},
|
|
}
|
|
}
|
|
|
|
type options struct {
|
|
requestURL string
|
|
upsertAAL bool
|
|
}
|
|
|
|
type ManagerOptions func(*options)
|
|
|
|
// WithRequestURL passes along query parameters from the requestURL to the new URL (if any exist)
|
|
func WithRequestURL(requestURL string) ManagerOptions {
|
|
return func(opts *options) {
|
|
opts.requestURL = requestURL
|
|
}
|
|
}
|
|
|
|
// UpsertAAL will update the available AAL of the identity if it was previoulsy unset. This is used to migrate
|
|
// identities from older versions of Ory Kratos.
|
|
func UpsertAAL(opts *options) {
|
|
opts.upsertAAL = true
|
|
}
|
|
|
|
func (s *ManagerHTTP) UpsertAndIssueCookie(ctx context.Context, w http.ResponseWriter, r *http.Request, ss *Session) (err error) {
|
|
ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.UpsertAndIssueCookie")
|
|
defer otelx.End(span, &err)
|
|
|
|
if err := s.r.SessionPersister().UpsertSession(ctx, ss); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.IssueCookie(ctx, w, r, ss); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ManagerHTTP) RefreshCookie(ctx context.Context, w http.ResponseWriter, r *http.Request, session *Session) (err error) {
|
|
ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.RefreshCookie")
|
|
defer otelx.End(span, &err)
|
|
|
|
// If it is a session token there is nothing to do.
|
|
_, cookieErr := r.Cookie(s.cookieName(r.Context()))
|
|
if errors.Is(cookieErr, http.ErrNoCookie) {
|
|
return nil
|
|
}
|
|
|
|
cookie, err := s.getCookie(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
expiresAt := getCookieExpiry(cookie)
|
|
if expiresAt == nil || expiresAt.Before(session.ExpiresAt) {
|
|
if err := s.IssueCookie(ctx, w, r, session); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ManagerHTTP) IssueCookie(ctx context.Context, w http.ResponseWriter, r *http.Request, session *Session) (err error) {
|
|
ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.IssueCookie")
|
|
defer otelx.End(span, &err)
|
|
|
|
cookie, err := s.r.CookieManager(ctx).Get(r, s.cookieName(ctx))
|
|
// Fix for https://github.com/ory/kratos/issues/1695
|
|
if err != nil && cookie == nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
if s.r.Config().SessionPath(ctx) != "" {
|
|
cookie.Options.Path = s.r.Config().SessionPath(ctx)
|
|
}
|
|
|
|
if domain := s.r.Config().SessionDomain(ctx); domain != "" {
|
|
cookie.Options.Domain = domain
|
|
}
|
|
|
|
if alias := s.r.Config().SelfPublicURL(ctx); s.r.Config().SelfPublicURL(ctx).String() != alias.String() {
|
|
// If a domain alias is detected use that instead.
|
|
cookie.Options.Domain = alias.Hostname()
|
|
cookie.Options.Path = alias.Path
|
|
}
|
|
|
|
// Rotate the anti-CSRF token unless the request was already authenticated
|
|
// as the same identity: regenerate when there is no prior active session or
|
|
// when it belongs to a different identity. This decision only needs the
|
|
// prior session's identity ID, so we deliberately do not expand the
|
|
// identity. Expanding it here loads the full identity graph, which on a
|
|
// multi-region deployment can trigger a slow cross-region read (observed at
|
|
// ~2.4s on the login path) purely to read a single UUID.
|
|
var priorIdentityID uuid.UUID
|
|
hasPriorSession := false
|
|
if token := s.extractToken(r.WithContext(ctx)); token != "" {
|
|
if prior, ferr := s.r.SessionPersister().GetSessionByToken(ctx, token, ExpandNothing, identity.ExpandNothing); ferr == nil && prior.IsActive() {
|
|
priorIdentityID = prior.IdentityID
|
|
hasPriorSession = true
|
|
}
|
|
}
|
|
if !hasPriorSession || priorIdentityID != session.IdentityID {
|
|
_ = s.r.CSRFHandler().RegenerateToken(w, r)
|
|
}
|
|
|
|
if s.r.Config().SessionSameSiteMode(ctx) != 0 {
|
|
cookie.Options.SameSite = s.r.Config().SessionSameSiteMode(ctx)
|
|
}
|
|
|
|
cookie.Options.MaxAge = 0
|
|
if s.r.Config().SessionPersistentCookie(ctx) {
|
|
if session.ExpiresAt.IsZero() {
|
|
cookie.Options.MaxAge = int(s.r.Config().OrganizationSessionLifespan(ctx, session.OrganizationID()).Seconds())
|
|
} else {
|
|
cookie.Options.MaxAge = int(time.Until(session.ExpiresAt).Seconds())
|
|
}
|
|
}
|
|
|
|
cookie.Values["session_token"] = session.Token
|
|
cookie.Values["expires_at"] = session.ExpiresAt.UTC().Format(time.RFC3339Nano)
|
|
cookie.Values["nonce"] = randx.MustString(8, randx.Alpha) // Guarantee new kratos session identifier
|
|
|
|
if err := cookie.Save(r, w); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getCookieExpiry(s *sessions.Session) *time.Time {
|
|
expiresAt, ok := s.Values["expires_at"].(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
n, err := time.Parse(time.RFC3339Nano, expiresAt)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return &n
|
|
}
|
|
|
|
func (s *ManagerHTTP) getCookie(r *http.Request) (*sessions.Session, error) {
|
|
return s.r.CookieManager(r.Context()).Get(r, s.cookieName(r.Context()))
|
|
}
|
|
|
|
func (s *ManagerHTTP) extractToken(r *http.Request) string {
|
|
ctx, span := s.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.ManagerHTTP.extractToken")
|
|
defer span.End()
|
|
|
|
if token := r.Header.Get("X-Session-Token"); len(token) > 0 {
|
|
return token
|
|
}
|
|
|
|
cookie, err := s.getCookie(r.WithContext(ctx))
|
|
if err != nil {
|
|
token, _ := bearerTokenFromRequest(r.WithContext(ctx))
|
|
return token
|
|
}
|
|
|
|
token, ok := cookie.Values["session_token"].(string)
|
|
if ok {
|
|
return token
|
|
}
|
|
|
|
token, _ = bearerTokenFromRequest(r.WithContext(ctx))
|
|
return token
|
|
}
|
|
|
|
func (s *ManagerHTTP) FetchFromRequestContext(ctx context.Context, r *http.Request) (_ *Session, err error) {
|
|
ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.FetchFromRequestContext")
|
|
defer otelx.End(span, &err)
|
|
|
|
if sess, ok := ctx.Value(sessionInContextKey).(*Session); ok {
|
|
return sess, nil
|
|
}
|
|
|
|
// Don't reduce the expansion here unless you want bad performance down the line (because we
|
|
// constantly are unsure if we have the full data fetched or not).
|
|
return s.FetchFromRequest(ctx, r, ExpandEverything, identity.ExpandEverything)
|
|
}
|
|
|
|
func (s *ManagerHTTP) FetchFromRequest(ctx context.Context, r *http.Request, sessionExpand Expandables, identityExpand identity.Expandables) (_ *Session, err error) {
|
|
ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.FetchFromRequest",
|
|
trace.WithAttributes(
|
|
attribute.StringSlice("session.expand", sessionExpand.ToEager()),
|
|
attribute.StringSlice("identity.expand", identityExpand.ToEager()),
|
|
))
|
|
defer endSpanIgnoreNoActiveSession(span, &err)
|
|
|
|
token := s.extractToken(r.WithContext(ctx))
|
|
if token == "" {
|
|
return nil, errors.WithStack(NewErrNoCredentialsForSession())
|
|
}
|
|
|
|
se, err := s.r.SessionPersister().GetSessionByToken(ctx, token, sessionExpand, identityExpand)
|
|
if err != nil {
|
|
if errors.Is(err, herodot.ErrNotFound()) || errors.Is(err, sqlcon.ErrNoRows()) {
|
|
return nil, errors.WithStack(NewErrNoActiveSessionFound())
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
trace.SpanFromContext(ctx).AddEvent(events.NewSessionChecked(ctx, se.ID, se.IdentityID))
|
|
|
|
if !se.IsActive() {
|
|
return nil, errors.WithStack(NewErrNoActiveSessionFound())
|
|
}
|
|
|
|
return se, nil
|
|
}
|
|
|
|
// endSpanIgnoreNoActiveSession ends the span without recording ErrNoActiveSessionFound as a span
|
|
// error, because a request without an active session is an expected condition on these paths.
|
|
func endSpanIgnoreNoActiveSession(span trace.Span, err *error) {
|
|
if _, ok := stderrors.AsType[*ErrNoActiveSessionFound](*err); ok {
|
|
span.End()
|
|
} else {
|
|
otelx.End(span, err)
|
|
}
|
|
}
|
|
|
|
// SessionActiveForRequest reports whether the request carries a token for an
|
|
// active session, without loading the identity. It returns nil when an active
|
|
// session exists and the same errors as FetchFromRequest otherwise, so callers
|
|
// that only branch on the error keep identical behavior while skipping the
|
|
// cross-region identity load.
|
|
func (s *ManagerHTTP) SessionActiveForRequest(ctx context.Context, r *http.Request) error {
|
|
_, err := s.FetchFromRequest(ctx, r, ExpandNothing, identity.ExpandNothing)
|
|
return err
|
|
}
|
|
|
|
func (s *ManagerHTTP) PurgeFromRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (err error) {
|
|
ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.PurgeFromRequest")
|
|
defer otelx.End(span, &err)
|
|
|
|
if token, ok := bearerTokenFromRequest(r); ok {
|
|
_, err := s.r.SessionPersister().RevokeSessionByToken(ctx, token)
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
cookie, _ := s.r.CookieManager(r.Context()).Get(r, s.cookieName(ctx))
|
|
token, ok := cookie.Values["session_token"].(string)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
if _, err := s.r.SessionPersister().RevokeSessionByToken(ctx, token); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
|
|
cookie.Options.MaxAge = -1
|
|
if err := cookie.Save(r, w); err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ManagerHTTP) DoesSessionSatisfy(ctx context.Context, sess *Session, requestedAAL string, opts ...ManagerOptions) (err error) {
|
|
ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.DoesSessionSatisfy")
|
|
defer otelx.End(span, &err)
|
|
|
|
sess.SetAuthenticatorAssuranceLevel()
|
|
|
|
// If we already have AAL2 there is no need to check further because it is the highest AAL.
|
|
if sess.AuthenticatorAssuranceLevel == identity.AuthenticatorAssuranceLevel2 {
|
|
return nil
|
|
}
|
|
|
|
managerOpts := &options{}
|
|
for _, o := range opts {
|
|
o(managerOpts)
|
|
}
|
|
|
|
loginURL := urlx.AppendPaths(s.r.Config().SelfPublicURL(ctx), "/self-service/login/browser")
|
|
query := url.Values{
|
|
"aal": {"aal2"},
|
|
}
|
|
|
|
// return to the requestURL if it was set
|
|
if managerOpts.requestURL != "" {
|
|
query.Set("return_to", managerOpts.requestURL)
|
|
}
|
|
|
|
loginURL.RawQuery = query.Encode()
|
|
|
|
switch requestedAAL {
|
|
case string(identity.AuthenticatorAssuranceLevel1):
|
|
if sess.AuthenticatorAssuranceLevel >= identity.AuthenticatorAssuranceLevel1 {
|
|
return nil
|
|
}
|
|
return NewErrAALNotSatisfied(loginURL.String())
|
|
case config.HighestAvailableAAL:
|
|
if sess.AuthenticatorAssuranceLevel == identity.AuthenticatorAssuranceLevel2 {
|
|
// The session has AAL2, nothing to check.
|
|
return nil
|
|
}
|
|
|
|
// The session is AAL1, we asked for `highest_available` AAL, so the only thing we can do
|
|
// is actually check what authentication methods the identity has.
|
|
if sess.Identity == nil {
|
|
// This is nil if the session did not expand the identity field.
|
|
sess.Identity, err = s.r.IdentityPool().GetIdentity(ctx, sess.IdentityID, identity.ExpandNothing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if aal, ok := sess.Identity.InternalAvailableAAL.ToAAL(); ok && aal == identity.AuthenticatorAssuranceLevel2 {
|
|
// Identity gives us AAL2, but the session is still AAL1. We need to upgrade the session.
|
|
return NewErrAALNotSatisfied(loginURL.String())
|
|
}
|
|
|
|
// Identity AAL is not 2, we refresh:
|
|
|
|
// The identity was apparently fetched without credentials. Let's hydrate them.
|
|
if len(sess.Identity.Credentials) == 0 {
|
|
if err := s.r.PrivilegedIdentityPool().HydrateIdentityAssociations(ctx, sess.Identity, identity.ExpandCredentials); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Great, now we determine the identity's available AAL
|
|
if err := sess.Identity.SetAvailableAAL(ctx, s.r.IdentityManager()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// We override the result with our newly computed values
|
|
available, valid := sess.Identity.InternalAvailableAAL.ToAAL()
|
|
if !valid {
|
|
// Unlikely to happen because SetAvailableAAL will either return an error, or a valid value - but not no error and an invalid value.
|
|
return errors.WithStack(x.PseudoPanic.WithReasonf("Unable to determine available authentication methods for session: %s", sess.ID))
|
|
}
|
|
|
|
switch available {
|
|
case identity.NoAuthenticatorAssuranceLevel:
|
|
// The identity has AAL0, the session has AAL1, we're good.
|
|
return nil
|
|
case identity.AuthenticatorAssuranceLevel1:
|
|
// The identity has AAL1, the session has AAL1, we're good.
|
|
return nil
|
|
case identity.AuthenticatorAssuranceLevel2:
|
|
// The identity has AAL2, the session has AAL1, we need to upgrade the session.
|
|
|
|
// Since we ended up here, it also means that `sess.Identity.InternalAvailableAAL` was `aal1` and is now `aal2`.
|
|
// Let's update the database.
|
|
if managerOpts.upsertAAL {
|
|
if err := s.r.PrivilegedIdentityPool().UpdateIdentityColumns(ctx, sess.Identity, "available_aal"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return NewErrAALNotSatisfied(loginURL.String())
|
|
}
|
|
|
|
return errors.Errorf("requested unknown aal: %s", requestedAAL)
|
|
}
|
|
|
|
func (s *ManagerHTTP) SessionAddAuthenticationMethods(ctx context.Context, sid uuid.UUID, ams ...AuthenticationMethod) (err error) {
|
|
ctx, span := s.r.Tracer(ctx).Tracer().Start(ctx, "sessions.ManagerHTTP.SessionAddAuthenticationMethods")
|
|
defer otelx.End(span, &err)
|
|
|
|
// Since we added the method, it also means that we have authenticated it
|
|
sess, err := s.r.SessionPersister().GetSession(ctx, sid, ExpandNothing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, m := range ams {
|
|
sess.CompletedLoginForMethod(m)
|
|
}
|
|
// Completing an authentication method is an authentication event, so the
|
|
// session's authenticated_at is refreshed as well. This matches login
|
|
// flows, which set authenticated_at when a second factor completes.
|
|
sess.AuthenticatedAt = time.Now().UTC()
|
|
sess.SetAuthenticatorAssuranceLevel()
|
|
return s.r.SessionPersister().UpsertSession(ctx, sess)
|
|
}
|
|
|
|
func (s *ManagerHTTP) MaybeRedirectAPICodeFlow(w http.ResponseWriter, r *http.Request, f flow.Flow, sessionID uuid.UUID, uiNode node.UiNodeGroup) (handled bool, err error) {
|
|
ctx, span := s.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.ManagerHTTP.MaybeRedirectAPICodeFlow")
|
|
defer otelx.End(span, &err)
|
|
|
|
if uiNode != node.OpenIDConnectGroup {
|
|
return false, nil
|
|
}
|
|
|
|
code, ok, _ := s.r.SessionTokenExchangePersister().CodeForFlow(ctx, f.GetID())
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
|
|
returnTo := s.r.Config().SelfServiceBrowserDefaultReturnTo(ctx)
|
|
if redirecter, ok := f.(flow.FlowWithRedirect); ok {
|
|
r, err := redir.SecureRedirectTo(r, returnTo, redirecter.SecureRedirectToOpts(ctx, s.r)...)
|
|
if err == nil {
|
|
returnTo = r
|
|
}
|
|
}
|
|
|
|
if err = s.r.SessionTokenExchangePersister().UpdateSessionOnExchanger(ctx, f.GetID(), sessionID); err != nil {
|
|
return false, errors.WithStack(err)
|
|
}
|
|
|
|
q := returnTo.Query()
|
|
q.Set("code", code.ReturnToCode)
|
|
returnTo.RawQuery = q.Encode()
|
|
http.Redirect(w, r, returnTo.String(), http.StatusSeeOther)
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (s *ManagerHTTP) ActivateSession(r *http.Request, session *Session, i *identity.Identity, authenticatedAt time.Time) (err error) {
|
|
ctx, span := s.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.ManagerHTTP.ActivateSession", trace.WithAttributes(
|
|
attribute.String("session.id", session.ID.String()),
|
|
attribute.String("identity.id", session.ID.String()),
|
|
attribute.String("authenticated_at", session.ID.String()),
|
|
))
|
|
defer otelx.End(span, &err)
|
|
|
|
if i == nil {
|
|
return errors.WithStack(x.PseudoPanic.WithReasonf("Identity must not be nil when activating a session."))
|
|
}
|
|
|
|
if !i.IsActive() {
|
|
return errors.WithStack(ErrIdentityDisabled().WithDetail("identity_id", i.ID))
|
|
}
|
|
|
|
if err := s.r.IdentityManager().RefreshAvailableAAL(ctx, i); err != nil {
|
|
return err
|
|
}
|
|
|
|
session.Identity = i
|
|
session.IdentityID = i.ID
|
|
|
|
session.Active = true
|
|
session.IssuedAt = authenticatedAt
|
|
session.ExpiresAt = authenticatedAt.Add(s.r.Config().OrganizationSessionLifespan(ctx, session.OrganizationID()))
|
|
session.AuthenticatedAt = authenticatedAt
|
|
|
|
session.SetSessionDeviceInformation(r.WithContext(ctx))
|
|
session.SetAuthenticatorAssuranceLevel()
|
|
|
|
span.SetAttributes(
|
|
attribute.String("identity.available_aal", session.Identity.InternalAvailableAAL.String),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ManagerHTTP) IsPrivileged(ctx context.Context, session *Session) bool {
|
|
if session == nil {
|
|
return false
|
|
}
|
|
|
|
if session.Identity == nil {
|
|
return false
|
|
}
|
|
|
|
if session.AuthenticatedAt.IsZero() {
|
|
return false
|
|
}
|
|
|
|
privilegedSessionLifespan := s.r.Config().SelfServiceFlowSettingsPrivilegedSessionMaxAge(ctx)
|
|
if privilegedSessionLifespan <= 0 {
|
|
return true
|
|
}
|
|
|
|
return session.AuthenticatedAt.Add(privilegedSessionLifespan).After(time.Now())
|
|
}
|