snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
436
session/session.go
Normal file
436
session/session.go
Normal file
@@ -0,0 +1,436 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user