// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package session import ( "context" "database/sql/driver" "encoding/json" "fmt" "net/http" "strings" "time" "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/ory/herodot" "github.com/ory/kratos/identity" "github.com/ory/kratos/text" "github.com/ory/kratos/x" "github.com/ory/x/httpx" "github.com/ory/x/pagination/keysetpagination" "github.com/ory/x/randx" ) func ErrIdentityDisabled() *herodot.DefaultError { return herodot.ErrBadRequest().WithID(text.ErrIDIdentityDisabled).WithError("identity is disabled").WithReason("This account was disabled.") } // lifespanProvider supplies the session lifespan, with optional per-organization overrides. type lifespanProvider interface { SessionLifespan(ctx context.Context) time.Duration OrganizationSessionLifespan(ctx context.Context, orgID uuid.UUID) time.Duration } type refreshWindowProvider interface { SessionRefreshMinTimeLeft(ctx context.Context) time.Duration } // Device corresponding to a Session // // swagger:model sessionDevice type Device struct { // Device record ID // // required: true ID uuid.UUID `json:"id" faker:"-" db:"id"` // SessionID is a helper struct field for gobuffalo.pop. SessionID uuid.UUID `json:"-" faker:"-" db:"session_id"` // IPAddress of the client IPAddress *string `json:"ip_address" faker:"ptr_ipv4" db:"ip_address"` // UserAgent of the client UserAgent *string `json:"user_agent" faker:"-" db:"user_agent"` // Geo Location corresponding to the IP Address Location *string `json:"location" faker:"ptr_geo_location" db:"location"` // Time of capture CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` // Last updated at UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` NID uuid.UUID `json:"-" faker:"-" db:"nid"` IdentityID *uuid.UUID `json:"-" faker:"-" db:"identity_id"` } func (Device) TableName() string { return "session_devices" } // A Session // // swagger:model session type Session struct { // Session ID // // required: true ID uuid.UUID `json:"id" faker:"-" db:"id"` // Active state. If false the session is no longer active. Active bool `json:"active" db:"active"` // The Session Expiry // // When this session expires at. ExpiresAt time.Time `json:"expires_at" db:"expires_at" faker:"time_type"` // The Session Authentication Timestamp // // When this session was authenticated at. If multi-factor authentication was used this // is the time when the last factor was authenticated (e.g. the TOTP code challenge was completed). AuthenticatedAt time.Time `json:"authenticated_at" db:"authenticated_at" faker:"time_type"` // AuthenticationMethod Assurance Level (AAL) // // The authenticator assurance level can be one of "aal1", "aal2", or "aal3". A higher number means that it is harder // for an attacker to compromise the account. // // Generally, "aal1" implies that one authentication factor was used while AAL2 implies that two factors (e.g. // password + TOTP) have been used. // // To learn more about these levels please head over to: https://www.ory.com/kratos/docs/concepts/credentials AuthenticatorAssuranceLevel identity.AuthenticatorAssuranceLevel `faker:"aal_type" db:"aal" json:"authenticator_assurance_level"` // Authentication Method References (AMR) // // A list of authentication methods (e.g. password, oidc, ...) used to issue this session. AMR AuthenticationMethods `db:"authentication_methods" json:"authentication_methods"` // The Session Issuance Timestamp // // When this session was issued at. Usually equal or close to `authenticated_at`. IssuedAt time.Time `json:"issued_at" db:"issued_at" faker:"time_type"` // The Logout Token // // Use this token to log out a user. LogoutToken string `json:"-" db:"logout_token"` // The Session Identity // // The identity that authenticated this session. // // If 2FA is required for the user, and the authentication process only solved the first factor, this field will be // null until the session has been fully authenticated with the second factor. Identity *identity.Identity `json:"identity" faker:"identity" db:"-" belongs_to:"identities" fk_id:"IdentityID"` // Devices has history of all endpoints where the session was used Devices []Device `json:"devices" faker:"-" has_many:"session_devices" fk_id:"session_id"` // IdentityID is a helper struct field for gobuffalo.pop. IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` // CreatedAt is a helper struct field for gobuffalo.pop. CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` // UpdatedAt is a helper struct field for gobuffalo.pop. UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` // Tokenized is the tokenized (e.g. JWT) version of the session. // // It is only set when the `tokenize_as` query parameter was set to a valid tokenize template during calls to `/session/whoami`. Tokenized string `json:"tokenized,omitempty" faker:"-" db:"-"` // The Session Token // // The token of this session. Token string `json:"-" db:"token"` NID uuid.UUID `json:"-" faker:"-" db:"nid"` } func (s Session) PageToken() keysetpagination.PageToken { return keysetpagination.MapPageToken{ "id": s.ID.String(), "created_at": s.CreatedAt.Format(x.MapPaginationDateFormat), } } func (Session) DefaultPageToken() keysetpagination.PageToken { return keysetpagination.MapPageToken{ "id": uuid.Nil.String(), "created_at": time.Date(2200, 12, 31, 23, 59, 59, 0, time.UTC).Format(x.MapPaginationDateFormat), } } func (Session) TableName() string { return "sessions" } // AdminSession is a Session that preserves metadata_admin on the embedded // identity during JSON marshaling. Use this type (or AdminSessions for slices) // when writing responses from admin endpoints. type AdminSession Session func (s AdminSession) MarshalJSON() ([]byte, error) { type sessionAlias Session type adminSession struct { sessionAlias Identity *identity.WithAdminMetadataInJSON `json:"identity"` } session := Session(s) session.Active = session.IsActive() return json.Marshal(adminSession{ sessionAlias: sessionAlias(session), Identity: (*identity.WithAdminMetadataInJSON)(session.Identity), }) } // AdminSessions converts a slice of Session to a slice of AdminSession for // admin endpoint responses that need to include metadata_admin. func AdminSessions(sessions []Session) []AdminSession { result := make([]AdminSession, len(sessions)) for i, s := range sessions { result[i] = AdminSession(s) } return result } func (s *Session) CompletedLoginForMethod(method AuthenticationMethod) { method.CompletedAt = time.Now().UTC() s.AMR = append(s.AMR, method) } func (s *Session) CompletedLoginFor(method identity.CredentialsType, aal identity.AuthenticatorAssuranceLevel) { s.CompletedLoginForMethod(AuthenticationMethod{Method: method, AAL: aal}) } func (s *Session) CompletedLoginForWithProvider(method identity.CredentialsType, aal identity.AuthenticatorAssuranceLevel, providerID string, organizationID string) { s.CompletedLoginForMethod(AuthenticationMethod{ Method: method, AAL: aal, Provider: providerID, Organization: organizationID, }) } // CompletedLoginForOIDC appends an OIDC authentication method to the // session and records the upstream `acr` / `amr` claim values for // auditing. Use this when the caller has access to the upstream claims; // otherwise use CompletedLoginForWithProvider. func (s *Session) CompletedLoginForOIDC(method identity.CredentialsType, aal identity.AuthenticatorAssuranceLevel, providerID, organizationID, upstreamACR string, upstreamAMR []string) { s.CompletedLoginForMethod(AuthenticationMethod{ Method: method, AAL: aal, Provider: providerID, Organization: organizationID, UpstreamACR: upstreamACR, UpstreamAMR: upstreamAMR, }) } func (s *Session) AuthenticatedVia(method identity.CredentialsType) bool { for _, authMethod := range s.AMR { if authMethod.Method == method { return true } } return false } func (s *Session) SetAuthenticatorAssuranceLevel() { if len(s.AMR) == 0 { s.AuthenticatorAssuranceLevel = identity.NoAuthenticatorAssuranceLevel return } var isAAL1, isAAL2 bool for _, amr := range s.AMR { switch amr.AAL { case identity.AuthenticatorAssuranceLevel1: isAAL1 = true case identity.AuthenticatorAssuranceLevel2: isAAL2 = true if m := amr.Method; m == identity.CredentialsTypeOIDC || m == identity.CredentialsTypeSAML { // OIDC or SAML can elevate a session to AAL2 on their own // because the upstream provider performed the MFA itself // (carried over via the `acr` / `amr` claims and the provider's // AAL2ACRValues / AAL2AMRValues config). isAAL1 = true } } } if isAAL1 && isAAL2 { s.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel2 } else { // AAL1 covers every other case: single first-factor methods (password, // OIDC without upstream MFA), an unpaired second-factor entry, or only // recovery credentials. s.AuthenticatorAssuranceLevel = identity.AuthenticatorAssuranceLevel1 } } func NewInactiveSession() *Session { return &Session{ ID: uuid.Nil, Token: x.OrySessionToken + randx.MustString(32, randx.AlphaNum), LogoutToken: x.OryLogoutToken + randx.MustString(32, randx.AlphaNum), Active: false, AuthenticatorAssuranceLevel: identity.NoAuthenticatorAssuranceLevel, } } func (s *Session) SetSessionDeviceInformation(r *http.Request) { device := Device{ SessionID: s.ID, IdentityID: new(s.IdentityID), IPAddress: new(httpx.ClientIP(r)), } agent := r.Header["User-Agent"] if len(agent) > 0 { device.UserAgent = new(strings.Join(agent, " ")) } var clientGeoLocation []string if r.Header.Get("Cf-Ipcity") != "" { clientGeoLocation = append(clientGeoLocation, r.Header.Get("Cf-Ipcity")) } if r.Header.Get("Cf-Ipcountry") != "" { clientGeoLocation = append(clientGeoLocation, r.Header.Get("Cf-Ipcountry")) } loc := strings.Join(clientGeoLocation, ", ") device.Location = &loc s.Devices = append(s.Devices, device) } func (s Session) Declassified() *Session { s.Identity = s.Identity.CopyWithoutCredentials() return &s } func (s *Session) IsActive() bool { return s.Active && s.ExpiresAt.After(time.Now()) && (s.Identity == nil || s.Identity.IsActive()) } func (s *Session) Refresh(ctx context.Context, c lifespanProvider) *Session { s.ExpiresAt = time.Now().Add(c.OrganizationSessionLifespan(ctx, s.OrganizationID())).UTC() return s } // OrganizationID returns the organization ID that issued this session, taken // from the first AMR entry whose Organization field parses as a UUID. Returns // uuid.Nil if no AMR entry carries a parseable organization ID, or if s is // nil. func (s *Session) OrganizationID() uuid.UUID { if s == nil { return uuid.Nil } for _, m := range s.AMR { if m.Organization == "" { continue } id, err := uuid.FromString(m.Organization) if err != nil { continue } return id } return uuid.Nil } func (s *Session) MarshalJSON() ([]byte, error) { type ss Session out := ss(*s) out.Active = s.IsActive() return json.Marshal(out) } func (s *Session) CanBeRefreshed(ctx context.Context, c refreshWindowProvider) bool { return s.ExpiresAt.Add(-c.SessionRefreshMinTimeLeft(ctx)).Before(time.Now()) } // List of (Used) AuthenticationMethods // // A list of authenticators which were used to authenticate the session. // // swagger:model sessionAuthenticationMethods type AuthenticationMethods []AuthenticationMethod // AuthenticationMethod identifies an authentication method // // A singular authenticator used during authentication / login. // // swagger:model sessionAuthenticationMethod type AuthenticationMethod struct { // The method used in this authenticator. Method identity.CredentialsType `json:"method"` // The AAL this method introduced. AAL identity.AuthenticatorAssuranceLevel `json:"aal" faker:"aal_type"` // When the authentication challenge was completed. CompletedAt time.Time `json:"completed_at"` // OIDC or SAML provider id used for authentication Provider string `json:"provider,omitempty"` // The Organization id used for authentication Organization string `json:"organization,omitempty"` // UpstreamACR is the `acr` claim reported by the upstream OIDC // provider, if any. Populated only for OIDC login methods when the // upstream ID token contained an `acr` claim. UpstreamACR string `json:"upstream_acr,omitempty"` // UpstreamAMR is the `amr` claim reported by the upstream OIDC // provider, if any. Populated only for OIDC login methods when the // upstream ID token contained an `amr` claim. UpstreamAMR []string `json:"upstream_amr,omitempty"` } // Scan implements the Scanner interface. func (n *AuthenticationMethod) Scan(value interface{}) error { if value == nil { return nil } v := fmt.Sprintf("%s", value) if len(v) == 0 { return nil } return errors.WithStack(json.Unmarshal([]byte(v), n)) } // Value implements the driver Valuer interface. func (n AuthenticationMethod) Value() (driver.Value, error) { value, err := json.Marshal(n) if err != nil { return nil, errors.WithStack(err) } return string(value), nil } // Scan implements the Scanner interface. func (n *AuthenticationMethods) Scan(value interface{}) error { if value == nil { return nil } v := fmt.Sprintf("%s", value) if len(v) == 0 { return nil } return errors.WithStack(json.Unmarshal([]byte(v), n)) } // Value implements the driver Valuer interface. func (n AuthenticationMethods) Value() (driver.Value, error) { value, err := json.Marshal(n) if err != nil { return nil, errors.WithStack(err) } return string(value), nil }