scimtoken

package
v0.31.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package scimtoken issues, rotates, revokes and authenticates the bearer tokens a tenant organization's identity provider presents to the SCIM API.

Each token belongs to one organization. Only a SHA-256 hash of a token is stored; the plaintext is returned once, when the token is issued.

Rotation keeps a rollback: issuing token N leaves the previous token N-1 valid and revokes N-2 and older in the same transaction, so the identity provider can be switched to N and, if that fails, left on N-1. Revoking N ends the overlap early or rolls back a bad N, making N-1 current again. Authenticate accepts at most the two newest unrevoked, unexpired tokens of an organization.

Index

Constants

View Source
const (
	// Prefix starts every token, so a leaked token is recognizable.
	Prefix = "sfscim_"
	// RandomBytes is the number of random bytes in a token (256 bits).
	RandomBytes = 32
	// BodyLength is the length of the base62 encoding of RandomBytes.
	BodyLength = 43
	// ChecksumLength is the length of the base62 encoding of the checksum.
	ChecksumLength = 6
	// Length is the length of a token.
	Length = len(Prefix) + BodyLength + ChecksumLength
	// DisplayLength is the length of the token prefix stored and shown to
	// identify a token: Prefix and the first 8 characters of the body.
	DisplayLength = len(Prefix) + 8
)

Token format.

A token is Prefix, then BodyLength base62 characters encoding 256 random bits, then ChecksumLength base62 characters encoding the CRC-32 (IEEE) of the body:

sfscim_<43 base62 characters><6 base62 characters>

A secret scanner can match it with the regular expression `sfscim_[0-9A-Za-z]{49}` and confirm a match offline with the checksum. The checksum also lets Authenticate refuse a mistyped or truncated token without a database lookup. It is not a secret and adds no strength.

View Source
const DefaultLastUsedInterval = time.Minute

DefaultLastUsedInterval is how often a token's last use is written to the database at most.

View Source
const MaxValid = 2

MaxValid is the number of tokens an organization may hold valid at once: the current token and the previous one.

View Source
const SubjectPrefix = "scim-token:"

SubjectPrefix starts the SCIM auth subject of a request authenticated with a stored token; the token ID follows.

Variables

View Source
var (
	// ErrInvalidToken is returned by Authenticate for a token that is
	// malformed, unknown, revoked, expired, superseded, or whose
	// organization is gone. Callers answer it with 401 and do not tell the
	// cases apart.
	ErrInvalidToken = errors.New("scimtoken: invalid token")
	// ErrForbidden is returned when the actor may not manage the
	// organization's SCIM tokens.
	ErrForbidden = errors.New("scimtoken: forbidden")
	// ErrNotFound is returned when the token does not exist in the
	// organization.
	ErrNotFound = errors.New("scimtoken: token not found")
	// ErrOrganizationNotFound is returned when the organization does not
	// exist or is deleted.
	ErrOrganizationNotFound = errors.New("scimtoken: organization not found")
	// ErrTokenExists is returned by Issue when the organization already
	// holds a valid token; use Rotate.
	ErrTokenExists = errors.New("scimtoken: organization already has a SCIM token; rotate it instead")
	// ErrConflict is returned when another issue or rotation for the same
	// organization committed first. Retry if a further rotation is still
	// wanted.
	ErrConflict = errors.New("scimtoken: concurrent rotation for this organization")
)

Errors returned by the service.

Functions

This section is empty.

Types

type API

type API struct {
	// contains filtered or unexported fields
}

API serves token management over HTTP: list, issue, rotate and revoke an organization's SCIM tokens. The Service decides who may call it.

func NewAPI

func NewAPI(service *Service, config APIConfig) (*API, error)

NewAPI returns the token management API. Mount it beside the organization API, behind the same authentication middleware.

func (*API) Router

func (a *API) Router() chi.Router

Router returns the API's router.

func (*API) ServeHTTP

func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

type APIConfig

type APIConfig struct {
	// BasePath prefixes every endpoint (default: "/api/v1"), matching the
	// organization API's, so the endpoints sit under
	// {BasePath}/organizations/{slug}/scim-tokens.
	BasePath string
	// Organizations resolves the organization slug in the path. Required.
	Organizations SlugResolver
	// Caller identifies the authenticated principal (default:
	// requestprincipal.PrincipalUUID, set by requestprincipal.Middleware).
	Caller func(ctx context.Context) (uuid.UUID, error)
	// Logger is the logger (default: slog.Default()).
	Logger *slog.Logger
}

APIConfig configures the token management API.

type CredentialOptions

type CredentialOptions struct {
	// Scopes are the SCIM scopes granted to a tenant token. Default:
	// scim.ScopeFull.
	Scopes []string

	// Platform authenticates platform operator credentials. It is
	// consulted only for bearer tokens that are not in this package's
	// token format, and its credential must have scim.Scope.Platform set;
	// any other credential is refused. When nil, which is the default,
	// only tenant tokens are accepted.
	Platform func(r *http.Request) (scim.Credential, error)
}

CredentialOptions configure Service.CredentialFunc.

type IssueInput

type IssueInput struct {
	Slug string `path:"slug" doc:"Organization slug"`
	Body struct {
		Description      string `json:"description,omitempty" maxLength:"255" doc:"Optional note, such as the identity provider"`
		ExpiresInSeconds int64  `json:"expires_in_seconds,omitempty" minimum:"0" doc:"Lifetime in seconds; 0 or absent means no expiry"`
	}
}

IssueInput is the body of an issue or rotate request.

type IssueOptions

type IssueOptions struct {
	// Description is an optional note, such as the identity provider's
	// name.
	Description string
	// ExpiresIn bounds the token's lifetime. Zero means no expiry.
	ExpiresIn time.Duration
}

IssueOptions describe a new token.

type IssueOutput

type IssueOutput struct {
	Body *Issued
}

IssueOutput carries the new token, shown only once.

type Issued

type Issued struct {
	Token    string `json:"token"`
	Metadata Token  `json:"metadata"`
	// Revoked lists the tokens the rotation revoked (N-2 and older).
	Revoked []uuid.UUID `json:"revoked,omitempty"`
}

Issued is a newly issued token. Token is the plaintext, available only here; hand it to the identity provider and do not store it.

type ListOutput

type ListOutput struct {
	Body struct {
		Tokens []Token `json:"tokens"`
	}
}

ListOutput lists an organization's tokens.

type Option

type Option func(*Service)

Option configures a Service.

func WithAuditEmitter

func WithAuditEmitter(e audit.Emitter) Option

WithAuditEmitter records issue, rotate and revoke events and authentication failures. Events never carry a token or its hash.

func WithAuthorizer

func WithAuthorizer(a authz.Authorizer) Option

WithAuthorizer sets the authorizer that decides who may issue, rotate, revoke and list an organization's tokens: holders of authz.PermOrgSettings on the organization (its owners and admins) and, when the authorizer implements authz.PlatformAuthorizer, platform administrators. Without it every management call is refused.

func WithClientAddress

func WithClientAddress(res *clientaddr.Resolver) Option

WithClientAddress sets how the client address recorded in audit events is found (see clientaddr). Default: the direct peer.

func WithClock

func WithClock(now func() time.Time) Option

WithClock sets the time source, for tests.

func WithLastUsedInterval

func WithLastUsedInterval(d time.Duration) Option

WithLastUsedInterval sets how often a token's last use is written at most. Default: DefaultLastUsedInterval.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets the logger. Default: slog.Default().

type RevokeInput

type RevokeInput struct {
	Slug    string `path:"slug" doc:"Organization slug"`
	TokenID string `path:"token_id" format:"uuid" doc:"Token ID"`
}

RevokeInput names the token to revoke.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service manages SCIM tokens in the identity database.

func New

func New(client *ent.Client, opts ...Option) *Service

New returns a Service over client.

func (*Service) Authenticate

func (s *Service) Authenticate(ctx context.Context, token string) (*Token, error)

Authenticate returns the token's metadata when token is one of the two newest valid tokens of an active organization, and ErrInvalidToken otherwise. A token's last use is written at most once per WithLastUsedInterval.

func (*Service) CredentialFunc

func (s *Service) CredentialFunc(opts CredentialOptions) func(r *http.Request) (scim.Credential, error)

CredentialFunc returns a credential function for scim.API.CredentialMiddleware. A bearer token in this package's format is authenticated with Authenticate and bound to its organization as a tenant (scim.WithTenant). Other bearer tokens go to opts.Platform when it is set, and are refused otherwise.

func (*Service) Issue

func (s *Service) Issue(ctx context.Context, actor, orgID uuid.UUID, opts IssueOptions) (*Issued, error)

Issue issues the organization's first token. It returns ErrTokenExists when the organization already holds a valid token.

func (*Service) List

func (s *Service) List(ctx context.Context, actor, orgID uuid.UUID) ([]Token, error)

List returns the organization's tokens, newest first, with their status.

func (*Service) Revoke

func (s *Service) Revoke(ctx context.Context, actor, orgID, tokenID uuid.UUID) error

Revoke revokes one of the organization's tokens. Revoking the current token makes the previous one current: a rollback. Revoking a token that is already revoked does nothing.

func (*Service) Rotate

func (s *Service) Rotate(ctx context.Context, actor, orgID uuid.UUID, opts IssueOptions) (*Issued, error)

Rotate issues a new current token N, keeps the previous current token as N-1, and revokes every older token, in one transaction. The plaintext of N is returned once. With no valid token it issues the first one. Concurrent rotations of one organization do not both commit: the later one returns ErrConflict.

type SlugInput

type SlugInput struct {
	Slug string `path:"slug" doc:"Organization slug"`
}

SlugInput names the organization.

type SlugResolver

type SlugResolver interface {
	ResolveSlug(ctx context.Context, slug string) (*orgsvc.Organization, bool, error)
}

SlugResolver finds an organization by its slug; organization.Service implements it.

type Status

type Status string

Status describes a token's standing.

const (
	// StatusCurrent is the newest valid token.
	StatusCurrent Status = "current"
	// StatusPrevious is the valid token before the current one, kept so
	// the identity provider can be rolled back.
	StatusPrevious Status = "previous"
	// StatusSuperseded is an unrevoked, unexpired token older than the
	// previous one. Authenticate refuses it.
	StatusSuperseded Status = "superseded"
	// StatusExpired is a token past its expiry.
	StatusExpired Status = "expired"
	// StatusRevoked is a revoked token.
	StatusRevoked Status = "revoked"
)

Token statuses.

type Token

type Token struct {
	ID             uuid.UUID  `json:"id"`
	OrganizationID uuid.UUID  `json:"organization_id"`
	Generation     int        `json:"generation"`
	Prefix         string     `json:"prefix"`
	Description    string     `json:"description,omitempty"`
	CreatedBy      *uuid.UUID `json:"created_by,omitempty"`
	CreatedAt      time.Time  `json:"created_at"`
	LastUsedAt     *time.Time `json:"last_used_at,omitempty"`
	ExpiresAt      *time.Time `json:"expires_at,omitempty"`
	RevokedAt      *time.Time `json:"revoked_at,omitempty"`
	RevokedBy      *uuid.UUID `json:"revoked_by,omitempty"`
	Status         Status     `json:"status"`
}

Token is a token's metadata. It never carries the token or its hash.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL