Documentation
¶
Overview ¶
Package oidcauth provides OpenID Connect token verification with optional in-memory caching and authorization helpers for roles, scopes, and authorized parties.
Basic usage ¶
verifier, err := oidcauth.New(ctx, oidcauth.Config{
RealmURL: "https://keycloak.example.com/realms/main",
ClientID: "my-app",
ClientSecret: "secret",
})
if err != nil {
log.Fatal(err)
}
claims, err := verifier.Verify(ctx, bearerToken)
if err != nil {
// inspect with errors.Is(err, oidcauth.ErrTokenRevoked), etc.
}
Caching ¶
By default no cache is used and every call to Verify hits the provider. Attach a MemoryCache to avoid redundant network round-trips:
cache := oidcauth.NewMemoryCache(ctx, oidcauth.DefaultCacheDuration) defer cache.Close() verifier, err := oidcauth.New(ctx, config, oidcauth.WithCache(cache))
Custom backends (Redis, Memcached, etc.) can be used by implementing the Cache interface:
type Cache interface {
Get(key string, now time.Time) (Claims, bool)
Set(key string, claims Claims, now time.Time)
}
Role-based authorization ¶
HasRole checks whether a token carries a specific Keycloak client role (resource_access[clientID].roles):
if !verifier.HasRole(claims, "admin") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
Scope-based authorization ¶
HasScope checks whether a token carries a specific OAuth 2.0 scope:
if !verifier.HasScope(claims, "read:data") {
http.Error(w, "insufficient scope", http.StatusForbidden)
return
}
HasAllScopes requires every listed scope to be present:
if !verifier.HasAllScopes(claims, "read:data", "write:data") {
http.Error(w, "insufficient scope", http.StatusForbidden)
return
}
IsAuthorizedParty compares the azp claim against an expected client ID, useful in multi-service architectures where a gateway forwards tokens:
if !verifier.IsAuthorizedParty(claims, "api-gateway") {
http.Error(w, "unauthorized party", http.StatusForbidden)
return
}
Machine-to-machine (client credentials) ¶
Access tokens obtained via the OAuth 2.0 client credentials grant often carry an aud value that does not match the resource server's ClientID, causing the default audience check to fail. Set SkipClientIDCheck: true to bypass that check and validate the caller identity manually instead:
verifier, err := oidcauth.New(ctx, oidcauth.Config{
RealmURL: "https://keycloak.example.com/realms/main",
ClientID: "resource-server",
ClientSecret: "secret",
SkipClientIDCheck: true, // M2M tokens may not carry this client's ID in aud
})
claims, err := verifier.Verify(ctx, token)
if err != nil { /* ... */ }
if !verifier.IsAuthorizedParty(claims, "allowed-service") {
http.Error(w, "unauthorized party", http.StatusForbidden)
return
}
if !verifier.HasAllScopes(claims, "read:data") {
http.Error(w, "insufficient scope", http.StatusForbidden)
return
}
Disabling introspection ¶
By default Verify performs a remote RFC 7662 introspection call to catch revoked-but-not-yet-expired tokens. When introspection is enabled, ClientSecret is required. To rely solely on local JWT verification and skip the introspection round-trip, set DisableIntrospection: true:
verifier, err := oidcauth.New(ctx, oidcauth.Config{
RealmURL: "https://keycloak.example.com/realms/main",
ClientID: "my-app",
DisableIntrospection: true, // no ClientSecret needed; no revocation detection
})
Error handling ¶
All errors wrap one of the package-level sentinels and can be inspected with errors.Is:
switch {
case errors.Is(err, oidcauth.ErrTokenRevoked):
// token was revoked server-side
case errors.Is(err, oidcauth.ErrTokenValidationFailed):
// signature / expiry / audience check failed
case errors.Is(err, oidcauth.ErrIntrospectionFailed):
// could not reach the introspection endpoint
case errors.Is(err, oidcauth.ErrMissingClientSecret):
// ClientSecret not set and introspection is enabled
}
Index ¶
- Constants
- Variables
- type Cache
- type Claims
- type Config
- type Introspection
- type MemoryCache
- type OIDC
- func (o *OIDC) HasAllScopes(claims Claims, scopes ...string) bool
- func (o *OIDC) HasRole(claims Claims, role string) bool
- func (o *OIDC) HasScope(claims Claims, scope string) bool
- func (o *OIDC) IsAuthorizedParty(claims Claims, azp string) bool
- func (o *OIDC) Verify(ctx context.Context, token string) (Claims, error)
- type Option
Constants ¶
const DefaultCacheDuration = 5 * time.Minute
DefaultCacheDuration is the TTL used when no custom duration is provided.
Variables ¶
var ( ErrInvalidRealmURL = errors.New("invalid OIDC configuration URL") ErrEmptyClientID = errors.New("client ID cannot be empty") ErrMissingClientSecret = errors.New("client secret is required when introspection is enabled") ErrTokenValidationFailed = errors.New("token validation failed") ErrProviderInitFailed = errors.New("failed to initialize OIDC provider") ErrIntrospectionFailed = errors.New("token introspection failed") ErrTokenRevoked = errors.New("access token has been revoked") )
Sentinel errors returned by the OIDC verifier.
Functions ¶
This section is empty.
Types ¶
type Cache ¶
type Cache interface {
Get(key string, now time.Time) (Claims, bool)
Set(key string, claims Claims, now time.Time)
}
Cache is the interface for token caching backends. Implement it to plug in Redis, Memcached, or any other store.
type Claims ¶
type Claims struct {
Aud []string `json:"aud"`
AllowedOrigins []string `json:"allowed-origins"`
Jti string `json:"jti"`
Iss string `json:"iss"`
Sub string `json:"sub"`
Typ string `json:"typ"`
Azp string `json:"azp"`
Sid string `json:"sid"`
Acr string `json:"acr"`
Scope string `json:"scope"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
Email string `json:"email"`
Exp float64 `json:"exp"`
Iat float64 `json:"iat"`
AuthTime int `json:"auth_time"`
RealmAccess map[string][]string `json:"realm_access"`
ResourceAccess map[string]map[string][]string `json:"resource_access"`
EmailVerified bool `json:"email_verified"`
}
Claims represents the structure of the claims extracted from an authentication token.
type Config ¶
type Config struct {
RealmURL string // Issuer URL (e.g. https://kc.example.com/realms/main).
ClientID string // OAuth client ID used for audience checks and introspection auth.
ClientSecret string // Confidential client secret used for introspection. Required unless DisableIntrospection is set.
RequestTimeout time.Duration // HTTP timeout for provider calls. Defaults to 30s.
SkipIssuerCheck bool // Disable iss claim validation (test-only).
SkipClientIDCheck bool // Disable aud claim validation against ClientID (test-only).
SkipExpiryCheck bool // Disable exp claim validation (test-only).
DisableIntrospection bool // Skip remote RFC 7662 introspection in Verify; rely only on local JWT verification.
}
Config controls how the OIDC verifier connects to the identity provider and validates tokens. RealmURL and ClientID are required; everything else has a sensible default.
Introspection is enabled by default. When enabled, ClientSecret is required and Verify performs a remote RFC 7662 introspection call to detect revoked-but-not-yet-expired tokens. Set DisableIntrospection to rely solely on local JWT verification (no revocation detection, no provider round-trip).
type Introspection ¶
Introspection represents the result of token introspection, including the claims and the active status of the token.
type MemoryCache ¶
type MemoryCache struct {
// contains filtered or unexported fields
}
MemoryCache is a thread-safe in-memory Cache with TTL-based eviction. Create one with NewMemoryCache and attach it to a verifier via WithCache.
func NewMemoryCache ¶
func NewMemoryCache(ctx context.Context, duration time.Duration) *MemoryCache
NewMemoryCache returns a MemoryCache that caps each entry's TTL at duration and runs a background eviction goroutine until ctx is cancelled or Close is called.
func (*MemoryCache) Close ¶
func (c *MemoryCache) Close()
Close stops the background eviction goroutine. Safe to call multiple times.
type OIDC ¶
type OIDC struct {
// contains filtered or unexported fields
}
OIDC verifies tokens issued by an OpenID Connect provider and exposes helpers for role- and scope-based authorization. A single OIDC value is safe for concurrent use.
func New ¶
New builds an OIDC verifier and discovers the provider's metadata. The supplied context bounds only the discovery call; use WithCache to control caching behaviour.
func (*OIDC) HasAllScopes ¶ added in v0.2.0
HasAllScopes reports whether the given claims include all of the named scopes. Returns true if scopes is empty (vacuous truth).
func (*OIDC) HasRole ¶
HasRole reports whether the given claims include the named role for this verifier's client (i.e. claims.ResourceAccess[ClientID].roles).
func (*OIDC) HasScope ¶ added in v0.2.0
HasScope reports whether the given claims include the named scope. Scopes in claims.Scope are space-separated per RFC 6749.
func (*OIDC) IsAuthorizedParty ¶ added in v0.2.0
IsAuthorizedParty reports whether the claims' azp (authorized party) field matches the provided value.
func (*OIDC) Verify ¶
Verify validates a bearer token and returns its claims.
The flow is:
- cache lookup, return immediately on hit (if a Cache was attached);
- local JWT verification (signature, iss, aud, exp);
- remote introspection to catch revoked-but-not-yet-expired tokens, unless disabled via Config.DisableIntrospection;
- cache the claims (if a Cache was attached).
Any failure in steps 2 or 3 returns ErrTokenValidationFailed; an inactive token returns ErrTokenRevoked. The original error is wrapped so callers using errors.Unwrap can still inspect it.