Files
upstream-ory-kratos/session/session.go
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

437 lines
14 KiB
Go

// 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
}