Documentation
¶
Overview ¶
Package revocation turns the security events a Receiver gets into revoked tokens: a session revoked, an account disabled or purged, a SCIM resource deactivated or deleted. A Revoker records each in a storage.RevocationStore, and answers whether a token the application already validated has since been revoked — as a call, or as net/http middleware.
rev, err := revocation.New(revocation.Options{
Store: memstore.NewRevocationStore(),
Issuers: revocation.SameIssuer, // the identity provider is its own Transmitter
Events: revocation.RecommendedEvents(),
Retention: 24 * time.Hour, // at least the longest token lifetime
Assurance: ssf.AssuranceDevelopment, // production needs a durable store
})
if err := rev.Register(rx); err != nil { // handle the events of Options.Events
return err // rx's Registry lacks one of them
}
api := rev.Middleware(func(r *http.Request) (revocation.Token, bool) {
claims, ok := validatedClaims(r) // the application's own token check
return revocation.Token{Issuer: claims.Iss, Subject: claims.Sub, SessionID: claims.Sid, IssuedAt: claims.Iat}, ok
}, apiHandler)
Options.Issuers says which token issuer each Transmitter speaks for; its events revoke tokens of that issuer and no other. Within it, an event's subject maps to what it revokes: an iss_sub subject, or a complex subject's iss_sub "user", naming that issuer, to that user's tokens; a complex subject's "session" to that session's tokens — for session-revoked, only the session if the subject names one — nothing if it is another issuer's, or of a format other than opaque and iss_sub — unless it is Options.AllSessions, which stands for all of them. Email subjects map only with Options.MatchEmail. Subjects with no default mapping — SCIM resources, whose identifiers are not token subjects — need Options.KeysFor, which is held to the same issuer.
Tokens issued at or before the event are revoked; tokens issued later — the user signing in again — are not.
Index ¶
Examples ¶
Constants ¶
const MaxClockSkewBound = time.Hour
MaxClockSkewBound bounds Options.MaxClockSkew. Clocks an hour apart are broken, not skewed; a larger allowance would also revoke the tokens of a user who signs in again long after the revocation.
const MaxRetention = 10 * 365 * 24 * time.Hour
MaxRetention bounds Options.Retention: long enough for any token, short enough that a revocation's expiry is always representable by every store.
Variables ¶
This section is empty.
Functions ¶
func RecommendedEvents ¶
RecommendedEvents returns the event types a Revoker usually handles: CAEP session-revoked, RISC account-disabled, account-purged and the deprecated sessions-revoked, and SCIM deactivate and delete. Credential-change is not among them: a changed password need not end every session.
Types ¶
type Options ¶
type Options struct {
// Store records revocations. Required: memstore.NewRevocationStore
// for development, or a durable store such as storage/sqlstore's.
Store storage.RevocationStore
// Issuers says which token issuer each Transmitter speaks for. A SET
// from a Transmitter it does not name revokes nothing, and neither
// does a subject naming any other issuer — so one Transmitter cannot
// revoke the users of another identity provider. Required.
Issuers TokenIssuers
// Events lists the event types that revoke tokens. Required;
// RecommendedEvents is the usual choice.
Events []ssf.EventType
// Retention is how long a revocation is kept. It must be at least the
// lifetime of the longest-lived token it should catch, and at most
// MaxRetention. Required.
Retention time.Duration
// Assurance is the deployment the Revoker is for. Required. Under
// ssf.AssuranceProduction its store must declare itself durable
// (storage.Capabilities): an in-memory one forgets its revocations on
// restart, and revoked tokens are accepted again.
Assurance ssf.Assurance
// HorizontallyScaled declares that several Revoker instances share the
// store. Under ssf.AssuranceProduction, it must then declare itself
// consistent across instances.
HorizontallyScaled bool
// MaxClockSkew is how far a token's "iat" may run ahead of the
// revocation time and still count as issued before it, for identity
// providers whose clocks run ahead of the Transmitter's. Zero allows
// none; it may not be negative or more than MaxClockSkewBound.
MaxClockSkew time.Duration
// MatchEmail also maps email subjects, so a Token's Email is checked:
// for an application whose users are known by email address. Off by
// default, since an address can change hands. Addresses are scoped to
// the token issuer and compared ignoring ASCII case only.
MatchEmail bool
// AllSessions, if set, is a session identifier that means every
// session of the subject's user, for Transmitters that revoke all of a
// user's sessions by naming a placeholder session — "ALL", say. A
// session-revoked event whose complex subject's "session" is exactly
// AllSessions revokes the user's tokens instead of a session of that
// name, and revokes nothing if the subject names no user. Off by
// default: no specification defines such a value, and a session could
// genuinely carry it. It may not begin or end with white space.
AllSessions string
// KeysFor, if set, maps a SET to what it revokes instead of the
// default mapping — for subjects that mapping does not cover, such as
// SCIM resources. It is called only for SETs from a Transmitter
// Issuers trusts, with the token issuer Issuers gives for it, and only
// keys naming that issuer are recorded: a Transmitter revokes the
// tokens of its own identity provider and no other. Returning no keys
// revokes nothing.
KeysFor func(set ssf.SET, tokenIssuer string) []storage.RevocationKey
// OnRevoke, if set, is called after a SET's revocations are recorded —
// to end the application's own sessions, say. It runs on the
// Receiver's goroutine for the SET and must not block for long. An
// error fails the SET's handling, so the Transmitter delivers it again.
OnRevoke func(ctx context.Context, keys []storage.RevocationKey, set ssf.SET) error
// Logger receives what Middleware cannot answer from, such as a
// store failure behind a 503. Defaults to slog.Default().
Logger *slog.Logger
// Now returns the current time. Defaults to time.Now.
Now func() time.Time
}
Options configures a Revoker. Store, Issuers, Events, Retention and Assurance are required: there are no implicit defaults, and RecommendedEvents is the usual choice for Events.
type Revoker ¶
type Revoker struct {
// contains filtered or unexported fields
}
Revoker records the revocations security events mean and checks tokens against them.
func (*Revoker) Handle ¶
Handle records the revocation set means, if its event type is one of Options.Events, then calls Options.OnRevoke. Any other SET is ignored.
func (*Revoker) IsRevoked ¶
IsRevoked reports whether tok's user or session, or its email address, was revoked at or after tok was issued — allowing for MaxClockSkew.
func (*Revoker) Middleware ¶
func (r *Revoker) Middleware(extract func(*http.Request) (Token, bool), next http.Handler) http.Handler
Middleware refuses requests whose token has been revoked. extract returns the token of a request the application has authenticated, or false for one it has not, which passes through untouched: Middleware checks revocation, not authentication. A revoked token gets 401 with an RFC 6750 invalid_token challenge; a store failure gets 503, failing closed, and is logged to Options.Logger.
Example ¶
A session-revoked event refuses the tokens of that session issued before it, and no others.
package main
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"time"
ssf "github.com/idfoundry/ssfgo"
"github.com/idfoundry/ssfgo/caep"
"github.com/idfoundry/ssfgo/revocation"
"github.com/idfoundry/ssfgo/storage/memstore"
)
func main() {
const idp = "https://idp.example.com" // its own Transmitter
rev, err := revocation.New(revocation.Options{
Store: memstore.NewRevocationStore(),
Issuers: revocation.SameIssuer,
Events: revocation.RecommendedEvents(),
Retention: 24 * time.Hour,
Assurance: ssf.AssuranceDevelopment,
})
if err != nil {
panic(err)
}
issued := time.Now().Add(-time.Minute)
// What rx.Register(rev) would do as the SET arrives.
err = rev.Handle(context.Background(), ssf.SET{
Issuer: idp,
IssuedAt: time.Now(),
Subject: ssf.ComplexSubject{
User: ssf.IssSubSubject{Issuer: idp, Subject: "alice"},
Session: ssf.IssSubSubject{Issuer: idp, Subject: "session-1"},
},
Event: caep.SessionRevoked{},
})
if err != nil {
panic(err)
}
api := rev.Middleware(func(r *http.Request) (revocation.Token, bool) {
// The application's own token check would go here.
return revocation.Token{Issuer: idp, Subject: "alice", SessionID: r.Header.Get("Session"), IssuedAt: issued}, true
}, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
for _, session := range []string{"session-1", "session-2"} {
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Session", session)
rec := httptest.NewRecorder()
api.ServeHTTP(rec, req)
fmt.Println(session, rec.Code)
}
}
Output: session-1 401 session-2 200
func (*Revoker) Register ¶
Register installs Handle on rx for each of Options.Events, replacing any handler already registered for them. An application with its own handlers for those events calls Handle from them instead.
It fails, installing nothing, if rx's Registry lacks any of the events: the Receiver would neither request nor accept them, so they would never revoke anything. Register their packages' events — risc.Register, scim.Register — or leave them out of Options.Events.
type StaticTokenIssuers ¶
StaticTokenIssuers maps Transmitter issuers to the token issuers they speak for, compared exactly. A Transmitter it does not list is not trusted.
func (StaticTokenIssuers) TokenIssuer ¶
func (m StaticTokenIssuers) TokenIssuer(setIssuer string) (string, bool)
TokenIssuer implements TokenIssuers.
type Token ¶
type Token struct {
// Issuer is the identity provider that issued the token ("iss").
Issuer string
// Subject is the user's identifier at Issuer ("sub").
Subject string
// SessionID is the session the token belongs to ("sid").
SessionID string
// Email is the user's email address, checked only for revocations
// recorded with Options.MatchEmail.
Email string
// IssuedAt is when the token was issued ("iat").
IssuedAt time.Time
}
Token is what a Revoker needs to know about a token the application has already validated. Any field may be empty except IssuedAt; a token with no IssuedAt counts as issued before every revocation.
type TokenIssuers ¶
type TokenIssuers interface {
// TokenIssuer returns the token issuer for the Transmitter whose SETs
// carry setIssuer, and false if that Transmitter is not trusted.
TokenIssuer(setIssuer string) (string, bool)
}
TokenIssuers says which token issuer each Transmitter speaks for: the "iss" of the access tokens whose users and sessions its events concern.
var SameIssuer TokenIssuers = sameIssuer{}
SameIssuer is the TokenIssuers for an identity provider that is its own Transmitter: each Transmitter speaks for tokens carrying its own issuer. It trusts every Transmitter the Receiver accepts SETs from, so a Revoker shared by Receivers of several identity providers needs StaticTokenIssuers instead.