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
- Variables
- type API
- type APIConfig
- type CredentialOptions
- type IssueInput
- type IssueOptions
- type IssueOutput
- type Issued
- type ListOutput
- type Option
- type RevokeInput
- type Service
- func (s *Service) Authenticate(ctx context.Context, token string) (*Token, error)
- func (s *Service) CredentialFunc(opts CredentialOptions) func(r *http.Request) (scim.Credential, error)
- func (s *Service) Issue(ctx context.Context, actor, orgID uuid.UUID, opts IssueOptions) (*Issued, error)
- func (s *Service) List(ctx context.Context, actor, orgID uuid.UUID) ([]Token, error)
- func (s *Service) Revoke(ctx context.Context, actor, orgID, tokenID uuid.UUID) error
- func (s *Service) Rotate(ctx context.Context, actor, orgID uuid.UUID, opts IssueOptions) (*Issued, error)
- type SlugInput
- type SlugResolver
- type Status
- type Token
Constants ¶
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.
const DefaultLastUsedInterval = time.Minute
DefaultLastUsedInterval is how often a token's last use is written to the database at most.
const MaxValid = 2
MaxValid is the number of tokens an organization may hold valid at once: the current token and the previous one.
const SubjectPrefix = "scim-token:"
SubjectPrefix starts the SCIM auth subject of a request authenticated with a stored token; the token ID follows.
Variables ¶
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.
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 ¶
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 WithLastUsedInterval ¶
WithLastUsedInterval sets how often a token's last use is written at most. Default: DefaultLastUsedInterval.
func WithLogger ¶
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 (*Service) Authenticate ¶
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) Revoke ¶
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.