Help us shape the future of Go! Take the 2026 Go Developer Survey

scimtoken

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 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 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