Files
upstream-ory-kratos/session/handler.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

1437 lines
47 KiB
Go

// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package session
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"go.opentelemetry.io/otel/trace"
"github.com/ory/x/httpx"
"github.com/ory/x/logrusx"
"github.com/ory/x/otelx"
"github.com/ory/kratos/x/nosurfx"
"github.com/ory/kratos/x/redir"
"github.com/ory/x/httprouterx"
"github.com/ory/kratos/selfservice/sessiontokenexchange"
"github.com/ory/x/otelx/semconv"
"github.com/ory/x/pagination/migrationpagination"
"github.com/ory/x/pagination/keysetpagination"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/ory/herodot"
"github.com/ory/kratos/driver/config"
"github.com/ory/kratos/identity"
"github.com/ory/kratos/x"
)
type (
// FlowForTokenExchange looks up a login or registration flow by ID.
// It is used by the token exchange handler to surface flow errors when no
// session was created (e.g. because a before-registration webhook rejected
// the request).
FlowForTokenExchange interface {
GetFlowForTokenExchange(ctx context.Context, flowID uuid.UUID) (any, error)
}
FlowForTokenExchangeProvider interface {
FlowForTokenExchange() FlowForTokenExchange
}
handlerDependencies interface {
ManagementProvider
PersistenceProvider
httpx.WriterProvider
otelx.Provider
logrusx.Provider
nosurfx.CSRFProvider
config.Provider
sessiontokenexchange.PersistenceProvider
FlowForTokenExchangeProvider
TokenizerProvider
}
HandlerProvider interface {
SessionHandler() *Handler
}
Handler struct{ r handlerDependencies }
)
func NewHandler(r handlerDependencies) *Handler { return &Handler{r: r} }
const (
RouteCollection = "/sessions"
RouteExchangeCodeForSessionToken = RouteCollection + "/token-exchange" // #nosec G101
RouteWhoami = RouteCollection + "/whoami"
RouteSession = RouteCollection + "/{id}"
)
const (
AdminRouteIdentity = "/identities"
AdminRouteIdentitiesSessions = AdminRouteIdentity + "/{id}/sessions"
AdminRouteSessionExtendId = RouteSession + "/extend"
// ManageSessionsMaxIDs caps the number of explicit IDs accepted per call.
// Picked defensively — not validated against a production dataset.
// Tied to MaxManageSessionsBodySize in cloudlib/ratelimit/inflight: 500 UUIDs
// serialise to ~20 KiB, so 32 KiB has ~40% headroom. Raise both together.
ManageSessionsMaxIDs = 500
// manageSessionsWildcardBatchSize is the per-statement chunk size for the
// wildcard ("*") variants. One HTTP call processes at most this many rows;
// the caller drains larger networks by re-issuing the request while the
// response reports `more: true`.
manageSessionsWildcardBatchSize = 5000
)
func (h *Handler) RegisterAdminRoutes(admin *httprouterx.RouterAdmin) {
admin.GET(RouteCollection, h.adminListSessions)
admin.GET(RouteSession, h.getSession)
admin.DELETE(RouteSession, h.disableSession)
admin.GET(AdminRouteIdentitiesSessions, h.listIdentitySessions)
admin.DELETE(AdminRouteIdentitiesSessions, h.deleteIdentitySessions)
admin.PATCH(AdminRouteSessionExtendId, h.adminSessionExtend)
admin.POST(RouteCollection, h.manageSessions)
admin.DELETE(RouteCollection, redir.RedirectToPublicRoute(h.r))
}
func (h *Handler) RegisterPublicRoutes(public *httprouterx.RouterPublic) {
// We need to completely ignore the whoami/logout path so that we do not accidentally set
// some cookie.
h.r.CSRFHandler().IgnorePath(RouteWhoami)
h.r.CSRFHandler().IgnorePath(RouteCollection)
h.r.CSRFHandler().IgnoreGlob(RouteCollection + "/*")
h.r.CSRFHandler().IgnoreGlob(RouteCollection + "/*/extend")
h.r.CSRFHandler().IgnoreGlob(AdminRouteIdentity + "/*/sessions")
for _, m := range []string{http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodConnect, http.MethodOptions, http.MethodTrace} {
public.Handle(m+" "+RouteWhoami, http.HandlerFunc(h.whoami))
}
public.DELETE(RouteCollection, h.deleteMySessions)
public.DELETE(RouteSession, h.deleteMySession)
public.GET(RouteCollection, h.listMySessions)
public.GET(RouteExchangeCodeForSessionToken, h.exchangeCode)
public.DELETE(AdminRouteIdentitiesSessions, redir.RedirectToAdminRoute(h.r))
}
// Check Session Request Parameters
//
// swagger:parameters toSession
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type toSession struct {
// Set the Session Token when calling from non-browser clients. A session token has a format of `MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj`.
//
// in: header
SessionToken string `json:"X-Session-Token"`
// Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that
// scenario you must include the HTTP Cookie Header which originally was included in the request to your server.
// An example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`.
//
// It is ok if more than one cookie are included here as all other cookies will be ignored.
//
// in: header
Cookie string `json:"Cookie"`
// Returns the session additionally as a token (such as a JWT)
//
// The value of this parameter has to be a valid, configured Ory Session token template. For more information head over to [the documentation](http://ory.sh/docs/identities/session-to-jwt-cors).
//
// in: query
TokenizeAs string `json:"tokenize_as"`
}
// swagger:route GET /sessions/whoami frontend toSession
//
// # Check Who the Current HTTP Session Belongs To
//
// Uses the HTTP Headers in the GET request to determine (e.g. by using checking the cookies) who is authenticated.
// Returns a session object in the body or 401 if the credentials are invalid or no credentials were sent.
// When the request it successful it adds the user ID to the 'X-Kratos-Authenticated-Identity-Id' header
// in the response.
//
// If you call this endpoint from a server-side application, you must forward the HTTP Cookie Header to this endpoint:
//
// ```js
// // pseudo-code example
// router.get('/protected-endpoint', async function (req, res) {
// const session = await client.toSession(undefined, req.header('cookie'))
//
// // console.log(session)
// })
// ```
//
// When calling this endpoint from a non-browser application (e.g. mobile app) you must include the session token:
//
// ```js
// // pseudo-code example
// // ...
// const session = await client.toSession("the-session-token")
//
// // console.log(session)
// ```
//
// When using a token template, the token is included in the `tokenized` field of the session.
//
// ```js
// // pseudo-code example
// // ...
// const session = await client.toSession("the-session-token", { tokenize_as: "example-jwt-template" })
//
// console.log(session.tokenized) // The JWT
// ```
//
// Depending on your configuration this endpoint might return a 403 status code if the session has a lower Authenticator
// Assurance Level (AAL) than is possible for the identity. This can happen if the identity has password + webauthn
// credentials (which would result in AAL2) but the session has only AAL1. If this error occurs, ask the user
// to sign in with the second factor or change the configuration.
//
// 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!
//
// This endpoint authenticates users by checking:
//
// - if the `Cookie` HTTP header was set containing an Ory Kratos Session Cookie;
// - if the `Authorization: bearer <ory-session-token>` HTTP header was set with a valid Ory Kratos Session Token;
// - if the `X-Session-Token` HTTP header was set with a valid Ory Kratos Session Token.
//
// If none of these headers are set or the cookie or token are invalid, the endpoint returns a HTTP 401 status code.
//
// As explained above, this request may fail due to several reasons. The `error.id` can be one of:
//
// - `session_inactive`: No active session was found in the request (e.g. no Ory Session Cookie / Ory Session Token).
// - `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
//
// Schemes: http, https
//
// Responses:
// 200: session
// 401: errorGeneric
// 403: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-high
func (h *Handler) whoami(w http.ResponseWriter, r *http.Request) {
ctx, span := h.r.Tracer(r.Context()).Tracer().Start(r.Context(), "sessions.Handler.whoami")
defer span.End()
// Whoami strips the credentials from the response below (CopyWithoutCredentials), so there is
// no point in loading them from the database: it is by far the most expensive part of fetching
// a session.
s, err := h.r.SessionManager().FetchFromRequest(ctx, r, ExpandEverything, identity.ExpandEverythingButCredentials)
c := h.r.Config()
if err != nil {
// We cache errors (and set cache header only when configured) where no session was found.
if noSess := new(ErrNoActiveSessionFound); c.SessionWhoAmICaching(ctx) && errors.As(err, &noSess) && noSess.CredentialsMissing {
w.Header().Set("Ory-Session-Cache-For", fmt.Sprintf("%d", int64(time.Minute.Seconds())))
}
h.r.Logger().WithRequest(r).WithError(err).Info("No valid session found.")
h.r.Writer().WriteError(w, r, ErrNoSessionFound().WithWrap(err))
return
}
var aalErr *ErrAALNotSatisfied
if err := h.r.SessionManager().DoesSessionSatisfy(ctx, s, c.SessionWhoAmIAAL(ctx),
// For the time being we want to update the AAL in the database if it is unset.
UpsertAAL,
); errors.As(err, &aalErr) {
h.r.Logger().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
} else if err != nil {
h.r.Logger().WithRequest(r).WithError(err).Info("No valid session cookie found.")
h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized().WithWrap(err).WithReasonf("Unable to determine AAL."))
return
}
// s.Devices = nil
s.Identity = s.Identity.CopyWithoutCredentials()
tokenizeTemplate := r.URL.Query().Get("tokenize_as")
if tokenizeTemplate != "" {
if err := h.r.SessionTokenizer().TokenizeSession(ctx, tokenizeTemplate, s); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
}
// Set userId as the X-Kratos-Authenticated-Identity-Id header.
w.Header().Set("X-Kratos-Authenticated-Identity-Id", s.Identity.ID.String())
// Set Cache header only when configured, and when no tokenization is requested.
if c.SessionWhoAmICaching(ctx) && len(tokenizeTemplate) == 0 {
expiry := time.Until(s.ExpiresAt)
if c.SessionWhoAmICachingMaxAge(ctx) > 0 && expiry > c.SessionWhoAmICachingMaxAge(ctx) {
expiry = c.SessionWhoAmICachingMaxAge(ctx)
}
w.Header().Set("Ory-Session-Cache-For", fmt.Sprintf("%0.f", expiry.Seconds()))
}
if err := h.r.SessionManager().RefreshCookie(ctx, w, r, s); err != nil {
h.r.Logger().WithRequest(r).WithError(err).Info("Could not re-issue cookie.")
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().Write(w, r, s)
}
// Delete Identity Session Parameters
//
// swagger:parameters deleteIdentitySessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type deleteIdentitySessions struct {
// ID is the identity's ID.
//
// required: true
// in: path
ID string `json:"id"`
}
// swagger:route DELETE /admin/identities/{id}/sessions identity deleteIdentitySessions
//
// # Delete & Invalidate an Identity's Sessions
//
// Calling this endpoint irrecoverably and permanently deletes and invalidates all sessions that belong to the given Identity.
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 204: emptyResponse
// 400: errorGeneric
// 401: errorGeneric
// 404: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-admin-low
func (h *Handler) deleteIdentitySessions(w http.ResponseWriter, r *http.Request) {
iID, err := uuid.FromString(r.PathValue("id"))
if err != nil {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID"))
return
}
if err := h.r.SessionPersister().DeleteSessionsByIdentity(r.Context(), iID); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
w.WriteHeader(http.StatusNoContent)
}
// Session List Request
//
// The request object for listing sessions in an administrative context.
//
// swagger:parameters listSessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type listSessionsRequest struct {
keysetpagination.RequestParameters
// Active is a boolean flag that filters out sessions based on the state. If no value is provided, all sessions are returned.
//
// required: false
// in: query
Active bool `json:"active"`
// ExpandOptions is a query parameter encoded list of all properties that must be expanded in the Session.
// If no value is provided, the expandable properties are skipped.
//
// required: false
// in: query
ExpandOptions []SessionExpandable `json:"expand"`
}
// Expandable properties of a session
// swagger:enum SessionExpandable
type SessionExpandable string
const (
SessionExpandableIdentity SessionExpandable = "identity"
SessionExpandableDevices SessionExpandable = "devices"
)
// Session List Response
//
// The response given when listing sessions in an administrative context.
//
// swagger:response listSessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type listSessionsResponse struct {
keysetpagination.ResponseHeaders
// The list of sessions found
// in: body
Sessions []Session
}
// swagger:route GET /admin/sessions identity listSessions
//
// # List All Sessions
//
// Listing all sessions that exist.
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 200: listSessions
// 400: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-admin-medium
func (h *Handler) adminListSessions(w http.ResponseWriter, r *http.Request) {
activeRaw := r.URL.Query().Get("active")
activeBool, err := strconv.ParseBool(activeRaw)
if activeRaw != "" && err != nil {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("could not parse parameter active"))
return
}
var active *bool
if activeRaw != "" {
active = &activeBool
}
// Parse request pagination parameters
opts, err := keysetpagination.Parse(r.URL.Query(), keysetpagination.NewMapPageToken)
if err != nil {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("could not parse parameter page_size"))
return
}
var expandables Expandables
if es, ok := r.URL.Query()["expand"]; ok {
for _, e := range es {
expand, ok := ParseExpandable(e)
if !ok {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Could not parse expand option: %s", e)))
return
}
expandables = append(expandables, expand)
}
}
sess, nextPage, err := h.r.SessionPersister().ListSessions(r.Context(), active, opts, expandables)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
u := *r.URL
keysetpagination.Header(w, &u, nextPage)
h.r.Writer().Write(w, r, AdminSessions(sess))
}
// Session Get Request
//
// The request object for getting a session in an administrative context.
//
// swagger:parameters getSession
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type getSession struct {
// ExpandOptions is a query parameter encoded list of all properties that must be expanded in the Session.
// Example - ?expand=Identity&expand=Devices
// If no value is provided, the expandable properties are skipped.
//
// required: false
// in: query
ExpandOptions []SessionExpandable `json:"expand"`
// ID is the session's ID.
//
// required: true
// in: path
ID string `json:"id"`
}
// swagger:route GET /admin/sessions/{id} identity getSession
//
// # Get Session
//
// This endpoint is useful for:
//
// - Getting a session object with all specified expandables that exist in an administrative context.
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 200: session
// 400: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-admin-high
func (h *Handler) getSession(w http.ResponseWriter, r *http.Request) {
if r.PathValue("id") == "whoami" {
// for /admin/sessions/whoami redirect to the public route
redir.RedirectToPublicRoute(h.r)(w, r)
return
}
sID, err := uuid.FromString(r.PathValue("id"))
if err != nil {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID"))
return
}
var expandables Expandables
urlValues := r.URL.Query()
if es, ok := urlValues["expand"]; ok {
for _, e := range es {
expand, ok := ParseExpandable(e)
if !ok {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Could not parse expand option: %s", e)))
return
}
expandables = append(expandables, expand)
}
}
sess, err := h.r.SessionPersister().GetSession(r.Context(), sID, expandables)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().Write(w, r, AdminSession(*sess))
}
// List Identity Sessions Parameters
//
// swagger:parameters listIdentitySessions
// Deactivate Session Parameters
//
// swagger:parameters disableSession
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type disableSession struct {
// ID is the session's ID.
//
// required: true
// in: path
ID string `json:"id"`
}
// swagger:route DELETE /admin/sessions/{id} identity disableSession
//
// # Deactivate a Session
//
// Calling this endpoint deactivates the specified session. Session data is not deleted.
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 204: emptyResponse
// 400: errorGeneric
// 401: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-admin-low
func (h *Handler) disableSession(w http.ResponseWriter, r *http.Request) {
sID, err := uuid.FromString(r.PathValue("id"))
if err != nil {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID"))
return
}
if err := h.r.SessionPersister().RevokeSessionById(r.Context(), sID); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().WriteCode(w, r, http.StatusNoContent, nil)
}
// List Identity Sessions Parameters
//
// swagger:parameters listIdentitySessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type listIdentitySessionsRequest struct {
migrationpagination.RequestParameters
// ID is the identity's ID.
//
// required: true
// in: path
ID string `json:"id"`
// Active is a boolean flag that filters out sessions based on the state. If no value is provided, all sessions are returned.
//
// required: false
// in: query
Active bool `json:"active"`
}
// List Identity Sessions Response
//
// swagger:response listIdentitySessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type listIdentitySessionsResponse struct {
migrationpagination.ResponseHeaderAnnotation
// in: body
Body []Session
}
// swagger:route GET /admin/identities/{id}/sessions identity listIdentitySessions
//
// # List an Identity's Sessions
//
// This endpoint returns all sessions that belong to the given Identity.
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 200: listIdentitySessions
// 400: errorGeneric
// 404: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-admin-medium
func (h *Handler) listIdentitySessions(w http.ResponseWriter, r *http.Request) {
iID, err := uuid.FromString(r.PathValue("id"))
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID")))
return
}
activeRaw := r.URL.Query().Get("active")
activeBool, err := strconv.ParseBool(activeRaw)
if activeRaw != "" && err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError("could not parse parameter active")))
return
}
var active *bool
if activeRaw != "" {
active = &activeBool
}
page, perPage := x.ParsePagination(r)
sess, total, err := h.r.SessionPersister().ListSessionsByIdentity(r.Context(), iID, active, page, perPage, uuid.Nil, ExpandEverything)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
x.PaginationHeader(w, *r.URL, total, page, perPage)
h.r.Writer().Write(w, r, AdminSessions(sess))
}
// Deleted Session Count
//
// swagger:model deleteMySessionsCount
type deleteMySessionsCount struct {
// The number of sessions that were revoked.
Count int `json:"count"`
}
// Disable My Other Session Parameters
//
// swagger:parameters disableMyOtherSessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type disableMyOtherSessions struct {
// Set the Session Token when calling from non-browser clients. A session token has a format of `MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj`.
//
// in: header
SessionToken string `json:"X-Session-Token"`
// Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that
// scenario you must include the HTTP Cookie Header which originally was included in the request to your server.
// An example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`.
//
// It is ok if more than one cookie are included here as all other cookies will be ignored.
//
// in: header
Cookie string `json:"Cookie"`
}
// swagger:route DELETE /sessions frontend disableMyOtherSessions
//
// # Disable my other sessions
//
// Calling this endpoint invalidates all except the current session that belong to the logged-in user.
// Session data are not deleted.
//
// Schemes: http, https
//
// Responses:
// 200: deleteMySessionsCount
// 400: errorGeneric
// 401: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-low
func (h *Handler) deleteMySessions(w http.ResponseWriter, r *http.Request) {
// Only session columns are read below, so skip loading the devices and the identity.
s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r, ExpandNothing, identity.ExpandNothing)
if err != nil {
h.r.Logger().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
}
n, err := h.r.SessionPersister().RevokeSessionsIdentityExcept(r.Context(), s.IdentityID, s.ID)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().WriteCode(w, r, http.StatusOK, &deleteMySessionsCount{Count: n})
}
// Disable My Session Parameters
//
// swagger:parameters disableMySession
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type disableMySession struct {
// ID is the session's ID.
//
// required: true
// in: path
ID string `json:"id"`
// Set the Session Token when calling from non-browser clients. A session token has a format of `MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj`.
//
// in: header
SessionToken string `json:"X-Session-Token"`
// Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that
// scenario you must include the HTTP Cookie Header which originally was included in the request to your server.
// An example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`.
//
// It is ok if more than one cookie are included here as all other cookies will be ignored.
//
// in: header
Cookie string `json:"Cookie"`
}
// swagger:route DELETE /sessions/{id} frontend disableMySession
//
// # Disable one of my sessions
//
// Calling this endpoint invalidates the specified session. The current session cannot be revoked.
// Session data are not deleted.
//
// Schemes: http, https
//
// Responses:
// 204: emptyResponse
// 400: errorGeneric
// 401: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-low
func (h *Handler) deleteMySession(w http.ResponseWriter, r *http.Request) {
sid := r.PathValue("id")
if sid == "whoami" {
// Special case where we actually want to handle the whoami endpoint.
h.whoami(w, r)
return
}
// Only session columns are read below, so skip loading the devices and the identity.
s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r, ExpandNothing, identity.ExpandNothing)
if err != nil {
h.r.Logger().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
}
sessionID, err := uuid.FromString(sid)
if err != nil {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID"))
return
}
if sessionID == s.ID {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("cannot revoke current session").WithDebug("use the logout flow instead"))
return
}
if err := h.r.SessionPersister().RevokeSession(r.Context(), s.IdentityID, sessionID); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().WriteCode(w, r, http.StatusNoContent, nil)
}
// List My Session Parameters
//
// swagger:parameters listMySessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type listMySessionsParameters struct {
migrationpagination.RequestParameters
// Set the Session Token when calling from non-browser clients. A session token has a format of `MP2YWEMeM8MxjkGKpH4dqOQ4Q4DlSPaj`.
//
// in: header
SessionToken string `json:"X-Session-Token"`
// Set the Cookie Header. This is especially useful when calling this endpoint from a server-side application. In that
// scenario you must include the HTTP Cookie Header which originally was included in the request to your server.
// An example of a session in the HTTP Cookie Header is: `ory_kratos_session=a19iOVAbdzdgl70Rq1QZmrKmcjDtdsviCTZx7m9a9yHIUS8Wa9T7hvqyGTsLHi6Qifn2WUfpAKx9DWp0SJGleIn9vh2YF4A16id93kXFTgIgmwIOvbVAScyrx7yVl6bPZnCx27ec4WQDtaTewC1CpgudeDV2jQQnSaCP6ny3xa8qLH-QUgYqdQuoA_LF1phxgRCUfIrCLQOkolX5nv3ze_f==`.
//
// It is ok if more than one cookie are included here as all other cookies will be ignored.
//
// in: header
Cookie string `json:"Cookie"`
}
// List My Session Response
//
// swagger:response listMySessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type listMySessionsResponse struct {
migrationpagination.ResponseHeaderAnnotation
// in: body
Body []Session
}
// swagger:route GET /sessions frontend listMySessions
//
// # Get My Active Sessions
//
// This endpoints returns all other active sessions that belong to the logged-in user.
// The current session can be retrieved by calling the `/sessions/whoami` endpoint.
//
// Schemes: http, https
//
// Responses:
// 200: listMySessions
// 400: errorGeneric
// 401: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-medium
func (h *Handler) listMySessions(w http.ResponseWriter, r *http.Request) {
// Only session columns are read below; DoesSessionSatisfy loads the identity lazily when the
// AAL check needs it.
s, err := h.r.SessionManager().FetchFromRequest(r.Context(), r, ExpandNothing, identity.ExpandNothing)
if err != nil {
h.r.Logger().WithRequest(r).WithError(err).Info("No valid session cookie found.")
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrUnauthorized().WithWrap(err).WithReasonf("No valid session cookie found.")))
return
}
c := h.r.Config()
var aalErr *ErrAALNotSatisfied
if err := h.r.SessionManager().DoesSessionSatisfy(r.Context(), s, c.SessionWhoAmIAAL(r.Context())); errors.As(err, &aalErr) {
h.r.Logger().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
} else if err != nil {
h.r.Logger().WithRequest(r).WithError(err).Info("No valid session cookie found.")
h.r.Writer().WriteError(w, r, herodot.ErrUnauthorized().WithWrap(err).WithReasonf("Unable to determine AAL."))
return
}
page, perPage := x.ParsePagination(r)
sess, total, err := h.r.SessionPersister().ListSessionsByIdentity(r.Context(), s.IdentityID, new(true), page, perPage, s.ID, ExpandEverything)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
x.PaginationHeader(w, *r.URL, total, page, perPage)
h.r.Writer().Write(w, r, sess)
}
type sessionInContext int
const (
sessionInContextKey sessionInContext = iota
)
func (h *Handler) IsAuthenticated(wrap http.HandlerFunc, onUnauthenticated http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// The session is stored in the request context for downstream handlers, which may read
// any of its associations - keep the full expansion.
sess, err := h.r.SessionManager().FetchFromRequest(ctx, r, ExpandEverything, identity.ExpandEverything)
if err != nil {
if onUnauthenticated != nil {
onUnauthenticated(w, r)
return
}
h.r.Writer().WriteError(w, r, errors.WithStack(NewErrNoActiveSessionFound().WithReason("This endpoint can only be accessed with a valid session. Please log in and try again.")))
return
}
wrap(w, r.WithContext(context.WithValue(ctx, sessionInContextKey, sess)))
}
}
// swagger:parameters extendSession
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type extendSession struct {
// ID is the session's ID.
//
// required: true
// in: path
ID string `json:"id"`
}
// swagger:route PATCH /admin/sessions/{id}/extend identity extendSession
//
// # Extend a Session
//
// Calling this endpoint extends the given session ID. If `session.earliest_possible_extend` is set it
// will only extend the session after the specified time has passed.
//
// This endpoint returns per default a 204 No Content response on success. Older Ory Network projects may
// return a 200 OK response with the session in the body. Returning the session as part of the response
// will be deprecated in the future and should not be relied upon.
//
// This endpoint ignores consecutive requests to extend the same session and returns a 404 error in those
// scenarios. This endpoint also returns 404 errors if the session does not exist.
//
// Retrieve the session ID from the `/sessions/whoami` endpoint / `toSession` SDK method.
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 200: session
// 204: emptyResponse
// 400: errorGeneric
// 404: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-admin-low
func (h *Handler) adminSessionExtend(w http.ResponseWriter, r *http.Request) {
id, err := uuid.FromString(r.PathValue("id"))
if err != nil {
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()).WithDebug("could not parse UUID")))
return
}
c := h.r.Config()
if err := h.r.SessionPersister().ExtendSession(r.Context(), id); err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
// Default behavior going forward.
if c.FeatureFlagFasterSessionExtend(r.Context()) {
w.WriteHeader(http.StatusNoContent)
return
}
trace.SpanFromContext(r.Context()).AddEvent(semconv.NewDeprecatedFeatureUsedEvent(r.Context(), "legacy_slower_session_extend"))
// WARNING - this will be deprecated at some point!
s, err := h.r.SessionPersister().GetSession(r.Context(), id, ExpandDefault)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().Write(w, r, s)
}
func (h *Handler) IsNotAuthenticated(wrap http.HandlerFunc, onAuthenticated http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := h.r.SessionManager().SessionActiveForRequest(r.Context(), r); err != nil {
if e := new(ErrNoActiveSessionFound); errors.As(err, &e) {
wrap(w, r)
return
}
h.r.Writer().WriteError(w, r, err)
return
}
if onAuthenticated != nil {
onAuthenticated(w, r)
return
}
h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden().WithReason("This endpoint can only be accessed without a login session. Please log out and try again.")))
}
}
func RedirectOnAuthenticated(d interface{ config.Provider }) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
returnTo, err := redir.SecureRedirectTo(r, d.Config().SelfServiceBrowserDefaultReturnTo(ctx), redir.SecureRedirectAllowSelfServiceURLs(d.Config().SelfPublicURL(ctx)))
if err != nil {
http.Redirect(w, r, d.Config().SelfServiceBrowserDefaultReturnTo(ctx).String(), http.StatusFound)
return
}
http.Redirect(w, r, returnTo.String(), http.StatusFound)
}
}
func RedirectOnUnauthenticated(to string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, to, http.StatusFound)
}
}
func RespondWitherrorGenericOnAuthenticated(h herodot.Writer, err error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.WriteError(w, r, err)
}
}
// Exchange Session Token Parameters
//
// swagger:parameters exchangeSessionToken
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type exchangeSessionToken struct {
// The part of the code return when initializing the flow.
//
// required: true
// in: query
InitCode string `json:"init_code"`
// The part of the code returned by the return_to URL.
//
// required: true
// in: query
ReturnToCode string `json:"return_to_code"`
}
// The Response for Registration Flows via API
//
// swagger:model successfulCodeExchangeResponse
type CodeExchangeResponse struct {
// The Session Token
//
// A session token is equivalent to a session cookie, but it can be sent in the HTTP Authorization
// Header:
//
// Authorization: bearer ${session-token}
//
// The session token is only issued for API flows, not for Browser flows!
Token string `json:"session_token,omitempty"`
// The Session
//
// The session contains information about the user, the session device, and so on.
// This is only available for API flows, not for Browser flows!
//
// required: true
Session *Session `json:"session"`
}
// swagger:route GET /sessions/token-exchange frontend exchangeSessionToken
//
// # Exchange Session Token
//
// Produces:
// - application/json
//
// Schemes: http, https
//
// Responses:
// 200: successfulNativeLogin
// 403: errorGeneric
// 404: errorGeneric
// 410: errorGeneric
// 422: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-public-medium
func (h *Handler) exchangeCode(w http.ResponseWriter, r *http.Request) {
var (
ctx = r.Context()
initCode = r.URL.Query().Get("init_code")
returnToCode = r.URL.Query().Get("return_to_code")
)
if initCode == "" || returnToCode == "" {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithReason(`"init_code" and "return_to_code" query params must be set`))
return
}
e, err := h.r.SessionTokenExchangePersister().GetExchangerFromCode(ctx, initCode, returnToCode)
if err != nil {
// The session might not be set because the flow encountered an error (e.g. a
// before-registration webhook rejected the request). Check whether the exchanger
// exists without requiring a session and, if so, return the flow with its error
// messages so that the client can act on them.
pending, pendingErr := h.r.SessionTokenExchangePersister().GetExchangerFromCodeAllowPending(ctx, initCode, returnToCode)
if pendingErr != nil {
h.r.Logger().WithRequest(r).WithError(pendingErr).Info("Could not look up pending session token exchanger.")
h.r.Writer().WriteError(w, r, herodot.ErrNotFound().WithReason(`no session yet for this "code"`))
return
}
f, fErr := h.r.FlowForTokenExchange().GetFlowForTokenExchange(ctx, pending.FlowID)
if fErr != nil {
h.r.Logger().WithRequest(r).WithError(fErr).Info("Could not look up flow for pending session token exchange.")
h.r.Writer().WriteError(w, r, herodot.ErrNotFound().WithReason(`no session yet for this "code"`))
return
}
h.r.Writer().WriteCode(w, r, http.StatusUnprocessableEntity, f)
return
}
sess, err := h.r.SessionPersister().GetSession(ctx, e.SessionID.UUID, ExpandDefault)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().Write(w, r, &CodeExchangeResponse{
Token: sess.Token,
Session: sess,
})
}
// ManageSessionsAction enumerates the supported actions for the manage-sessions
// endpoint.
//
// swagger:enum ManageSessionsAction
type ManageSessionsAction string
const (
ManageSessionsActionDisable ManageSessionsAction = "disable"
ManageSessionsActionDelete ManageSessionsAction = "delete"
)
// ManageSessionsAllToken is the explicit-consent value that callers must pass
// in `identities` or `sessions` to operate on every row in the network. It
// must be the only element of the array and may not be mixed with explicit
// IDs.
const ManageSessionsAllToken = "*"
// Manage Sessions Body
//
// Body for the bulk session management endpoint. Exactly one of `identities`
// or `sessions` must be provided. To operate on every session in the network,
// pass `identities: ["*"]` — the wildcard must appear alone, never mixed with
// explicit IDs.
//
// swagger:model manageSessionsBody
type ManageSessionsBody struct {
// Action to perform on the matching sessions.
//
// required: true
// enum: disable,delete
Action ManageSessionsAction `json:"action"`
// Identity IDs whose sessions should be disabled or deleted, or `["*"]`
// to operate on every session in the network. Mutually exclusive with
// `sessions`.
Identities []string `json:"identities"`
// Session IDs to disable or delete. Mutually exclusive with `identities`.
// The wildcard `["*"]` is not accepted in this field — pass
// `identities: ["*"]` to scope the operation to every session in the
// network.
Sessions []string `json:"sessions"`
}
// swagger:parameters manageSessions
//
//nolint:deadcode,unused
//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions
type manageSessionsParameters struct {
// in: body
// required: true
Body ManageSessionsBody
}
// Manage Sessions Response
//
// Response body for the bulk session management endpoint. Reports how many
// rows the call processed and, for the wildcard variant, whether the network
// still has matching rows left. Explicit-IDs requests always return
// `more: false`. Wildcard callers drain the network by re-issuing the same
// request while `more` is `true`.
//
// swagger:model manageSessionsResponse
type ManageSessionsResponse struct {
// Number of sessions processed in this call. For `disable`, counts only
// sessions that were active before the call (already-inactive sessions
// are skipped). For `delete`, counts every matching row removed.
Processed int `json:"processed"`
// True when the call reached the per-call batch limit and additional
// matching rows may remain. Always false for explicit-IDs requests.
More bool `json:"more"`
}
// swagger:route POST /admin/sessions identity manageSessions
//
// # Manage sessions in bulk
//
// Disable or delete sessions for a list of identities or a list of sessions in
// a single call. The `action` field selects the operation:
//
// - `disable` — deactivate matching sessions (sets `active = false`, preserves
// audit data).
// - `delete` — permanently delete matching sessions.
//
// Exactly one of `identities` or `sessions` must be provided. To scope the
// operation to every session in the network, pass `identities: ["*"]`; the
// wildcard is not accepted in the `sessions` field. Up to 500 explicit IDs
// are accepted per call.
//
// All requests return `200 OK` with `{processed, more}`. `processed` reports
// how many rows the call affected; for `disable` it counts only sessions
// that were active before the call. `more` is `true` only when a wildcard
// request reached the per-call batch limit and additional rows may remain;
// callers drain the network by re-issuing the same request while `more` is
// `true`. Explicit-IDs requests always return `more: false`.
//
// Consumes:
// - application/json
//
// Schemes: http, https
//
// Security:
// oryAccessToken:
//
// Responses:
// 200: manageSessionsResponse
// 400: errorGeneric
// 401: errorGeneric
// default: errorGeneric
//
// Extensions:
// x-ory-ratelimit-bucket: kratos-admin-low
func (h *Handler) manageSessions(w http.ResponseWriter, r *http.Request) {
var body ManageSessionsBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error()).WithReason("Invalid JSON body."))
return
}
switch body.Action {
case ManageSessionsActionDisable, ManageSessionsActionDelete:
default:
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithReasonf(
"The 'action' field must be one of '%s' or '%s'.",
ManageSessionsActionDisable, ManageSessionsActionDelete))
return
}
identitiesSet := len(body.Identities) > 0
sessionsSet := len(body.Sessions) > 0
if identitiesSet == sessionsSet {
h.r.Writer().WriteError(w, r, herodot.ErrBadRequest().WithReason(
"Exactly one of 'identities' or 'sessions' must be a non-empty array."))
return
}
var resp *ManageSessionsResponse
var err error
switch {
case identitiesSet:
resp, err = h.manageByIdentities(r.Context(), body.Action, body.Identities)
case sessionsSet:
resp, err = h.manageBySessions(r.Context(), body.Action, body.Sessions)
default:
err = errors.WithStack(herodot.ErrInternalServerError().WithReason(
"neither 'identities' nor 'sessions' set after validation"))
}
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}
h.r.Writer().Write(w, r, resp)
}
// manageByIdentities applies the requested action to all sessions belonging
// to the given identity IDs, or to a single bounded batch of every session in
// the network when raw is ["*"]. `more` is set only on the wildcard variant
// when the call hit the per-call batch limit; explicit-IDs always return
// `more: false`.
func (h *Handler) manageByIdentities(ctx context.Context, action ManageSessionsAction, raw []string) (*ManageSessionsResponse, error) {
ids, wildcard, err := parseManageSessionsIDsOrWildcard(raw)
if err != nil {
return nil, herodot.ErrBadRequest().WithReasonf("Could not parse 'identities' field: %s", err)
}
p := h.r.SessionPersister()
switch action {
case ManageSessionsActionDisable:
if wildcard {
return wildcardBatch(ctx, p.RevokeAllSessions)
}
n, err := p.RevokeSessionsByIdentities(ctx, ids)
return explicitBatch(n), err
case ManageSessionsActionDelete:
if wildcard {
return wildcardBatch(ctx, p.DeleteAllSessions)
}
n, err := p.DeleteSessionsByIdentities(ctx, ids)
return explicitBatch(n), err
default:
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf(
"unhandled session action %q", action))
}
}
// manageBySessions applies the requested action to the given explicit session
// IDs. The wildcard ["*"] is not supported in this field; callers that need
// network-wide scope must use `identities: ["*"]`. See manageByIdentities for
// the response-shape contract.
func (h *Handler) manageBySessions(ctx context.Context, action ManageSessionsAction, raw []string) (*ManageSessionsResponse, error) {
ids, err := parseManageSessionsIDs(raw)
if err != nil {
return nil, herodot.ErrBadRequest().WithReasonf("Could not parse 'sessions' field: %s", err)
}
p := h.r.SessionPersister()
switch action {
case ManageSessionsActionDisable:
n, err := p.RevokeSessionsByIDs(ctx, ids)
return explicitBatch(n), err
case ManageSessionsActionDelete:
n, err := p.DeleteSessionsByIDs(ctx, ids)
return explicitBatch(n), err
default:
return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf(
"unhandled session action %q", action))
}
}
// parseManageSessionsIDsOrWildcard recognizes the network-wide wildcard form
// ["*"] and otherwise delegates to parseManageSessionsIDs. Use it in fields
// that accept the wildcard (currently `identities`); fields that do not
// accept wildcard should call parseManageSessionsIDs directly so the token is
// rejected.
func parseManageSessionsIDsOrWildcard(raw []string) (ids []uuid.UUID, wildcard bool, err error) {
if len(raw) == 1 && raw[0] == ManageSessionsAllToken {
return nil, true, nil
}
ids, err = parseManageSessionsIDs(raw)
return ids, false, err
}
// parseManageSessionsIDs interprets a manage-sessions filter array as a list
// of explicit UUIDs and rejects any input containing the wildcard token.
// Callers that accept the wildcard must use parseManageSessionsIDsOrWildcard.
func parseManageSessionsIDs(raw []string) ([]uuid.UUID, error) {
if len(raw) == 0 {
return nil, errors.New("array must not be empty")
}
if len(raw) > ManageSessionsMaxIDs {
return nil, fmt.Errorf("at most %d IDs may be provided per call", ManageSessionsMaxIDs)
}
ids := make([]uuid.UUID, 0, len(raw))
for _, s := range raw {
if s == ManageSessionsAllToken {
return nil, errors.New("the wildcard '*' is not accepted here")
}
id, err := uuid.FromString(s)
if err != nil {
return nil, fmt.Errorf("could not parse %q as UUID: %w", s, err)
}
ids = append(ids, id)
}
return ids, nil
}
// wildcardBatch runs a single chunked bulk-session operation and packages the
// row count plus a `more` flag for the response. `more` is true when the call
// reached the per-call batch limit, signaling that the caller should re-issue
// the request to drain the rest.
//
// When the row count is an exact multiple of the batch size, `more` is set
// even though no rows are left; the caller will issue one extra request that
// returns `{processed: 0, more: false}`. This is intentional — distinguishing
// "exactly batch-size" from "batch-size and more" would cost an extra DB
// query on every call to save one round-trip in a rare edge case.
func wildcardBatch(ctx context.Context, op func(context.Context, int) (int, error)) (*ManageSessionsResponse, error) {
n, err := op(ctx, manageSessionsWildcardBatchSize)
if err != nil {
return nil, err
}
return &ManageSessionsResponse{
Processed: n,
More: n == manageSessionsWildcardBatchSize,
}, nil
}
// explicitBatch packages the row count from an explicit-IDs bulk call.
// `more` is always false because explicit-IDs requests are bounded by
// ManageSessionsMaxIDs and complete in a single statement.
func explicitBatch(n int) *ManageSessionsResponse {
return &ManageSessionsResponse{Processed: n, More: false}
}