auth

package
v0.0.9 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package auth provides authentication primitives for FlowCatalyst SDK applications: JWT validation (RS256 via JWKS, or HS256 via a shared secret), the OAuth2 authorization-code flow with PKCE, and a client_credentials TokenProvider that plugs into client.WithTokenProvider.

A token minted by FlowCatalyst validates identically from any of the SDKs (TypeScript, Laravel, Go).

Resource server (token validation)

v := auth.NewTokenValidator(auth.TokenValidatorConfig{
    IssuerURL: "https://auth.flowcatalyst.io",
    Audience:  "my-app",
})
ctx, err := v.ValidateBearer(r.Context(), r.Header.Get("Authorization"))
if err != nil { /* 401 */ }
if ctx.HasRole("admin") && ctx.HasClientAccess("clt_123") { /* allow */ }

OAuth2 authorization code flow (web app)

oauth := auth.NewOAuthClient(auth.OAuthConfig{
    IssuerURL:    "https://auth.flowcatalyst.io",
    ClientID:     "my-app",
    ClientSecret: "secret",
    RedirectURI:  "https://myapp.example.com/callback",
})
url, params := oauth.AuthorizeURL()
// ... redirect, store params in session, then on callback:
tokens, err := oauth.ExchangeCode(ctx, code, params.PKCE.CodeVerifier)

Service-to-service (client_credentials)

tp := auth.NewClientCredentialsProvider(auth.ClientCredentialsConfig{
    IssuerURL:    "https://auth.flowcatalyst.io",
    ClientID:     "svc-app",
    ClientSecret: "...",
})
c := client.New("https://api.flowcatalyst.io", client.WithTokenProvider(tp.Token))

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrTokenExpired  = &Error{Kind: KindTokenExpired}
	ErrInvalidToken  = &Error{Kind: KindInvalidToken}
	ErrDiscovery     = &Error{Kind: KindDiscovery}
	ErrTokenExchange = &Error{Kind: KindTokenExchange}
	ErrConfig        = &Error{Kind: KindConfig}
	ErrCrypto        = &Error{Kind: KindCrypto}
)

Sentinel errors. Use errors.Is to branch on them.

Functions

This section is empty.

Types

type AccessTokenClaims

type AccessTokenClaims struct {
	// Sub is the principal id, e.g. "prn_0HZXEQ5Y8JY5Z".
	Sub string `json:"sub"`
	// Iss is the issuer URL.
	Iss string `json:"iss"`
	// Aud is the audience. Accepts string or []string on the wire.
	Aud Audience `json:"aud"`
	// Exp is the expiration time (Unix seconds).
	Exp int64 `json:"exp"`
	// Iat is the issued-at time (Unix seconds).
	Iat int64 `json:"iat"`
	// Nbf is the not-before time (Unix seconds).
	Nbf int64 `json:"nbf"`
	// Jti is the JWT id.
	Jti string `json:"jti"`
	// PrincipalType is "USER" or "SERVICE". The wire field name is
	// "type".
	PrincipalType string `json:"type"`
	// Scope is "ANCHOR", "PARTNER", or "CLIENT".
	Scope string `json:"scope"`
	// Email is present for USER principals, absent for SERVICE.
	Email string `json:"email,omitempty"`
	// Name is the display name.
	Name string `json:"name"`
	// Clients are the client ids this principal can access. ["*"] for
	// anchor users.
	Clients []string `json:"clients"`
	// Roles are the role codes assigned to this principal.
	Roles []string `json:"roles,omitempty"`
	// Applications are the application codes derived from roles, e.g.
	// "operant" from "operant:admin". Always present on tokens issued
	// by FC but absent on older tokens.
	Applications []string `json:"applications,omitempty"`
}

AccessTokenClaims is the JWT payload shape issued by FlowCatalyst's /oauth/token endpoint. JSON tags match the wire contract byte-for-byte so the same token deserialises identically across SDKs.

func (*AccessTokenClaims) HasClientAccess

func (c *AccessTokenClaims) HasClientAccess(clientID string) bool

HasClientAccess reports whether the principal can access the given client. Anchor principals (clients == ["*"]) always return true.

func (*AccessTokenClaims) HasRole

func (c *AccessTokenClaims) HasRole(role string) bool

HasRole reports whether the principal has the given role code.

func (*AccessTokenClaims) IsAnchor

func (c *AccessTokenClaims) IsAnchor() bool

IsAnchor reports whether this is an anchor (platform-wide) principal.

func (*AccessTokenClaims) IsService

func (c *AccessTokenClaims) IsService() bool

IsService reports whether this is a service account.

func (*AccessTokenClaims) PrincipalID

func (c *AccessTokenClaims) PrincipalID() string

PrincipalID returns the Sub claim.

type Audience

type Audience string

Audience is a JWT aud claim. RFC 7519 allows aud to be either a string or an array of strings. FlowCatalyst-minted tokens emit a single string, but tokens minted with other tooling (e.g. jwx itself) emit an array. We accept both forms on unmarshal and always marshal as a single string for wire parity.

func (Audience) String

func (a Audience) String() string

func (*Audience) UnmarshalJSON

func (a *Audience) UnmarshalJSON(b []byte) error

UnmarshalJSON accepts either a string or a one-element string array. Multi-element arrays take the first entry.

type AuthContext

type AuthContext struct {
	Claims AccessTokenClaims
	Token  string
}

AuthContext is a validated token plus the raw JWT, ready for authorization checks and forwarding to downstream services.

func NewAuthContext

func NewAuthContext(claims AccessTokenClaims, token string) *AuthContext

NewAuthContext constructs an AuthContext.

func (*AuthContext) BearerToken

func (a *AuthContext) BearerToken() string

BearerToken returns the raw JWT for forwarding to downstream services.

func (*AuthContext) ClientIDs

func (a *AuthContext) ClientIDs() []string

func (*AuthContext) Email

func (a *AuthContext) Email() string

func (*AuthContext) HasClientAccess

func (a *AuthContext) HasClientAccess(clientID string) bool

func (*AuthContext) HasRole

func (a *AuthContext) HasRole(role string) bool

func (*AuthContext) IsAnchor

func (a *AuthContext) IsAnchor() bool

func (*AuthContext) IsService

func (a *AuthContext) IsService() bool

func (*AuthContext) Name

func (a *AuthContext) Name() string

func (*AuthContext) PrincipalID

func (a *AuthContext) PrincipalID() string

func (*AuthContext) Roles

func (a *AuthContext) Roles() []string

type AuthorizeParams

type AuthorizeParams struct {
	PKCE  PkceChallenge
	State string
	Nonce string
}

AuthorizeParams is the session-stored side of an authorize-URL call.

type ClientCredentialsConfig

type ClientCredentialsConfig struct {
	// IssuerURL is the FlowCatalyst OIDC server.
	IssuerURL string
	// ClientID is the service-account / confidential client id.
	ClientID string
	// ClientSecret is the matching secret.
	ClientSecret string
	// Scopes requested. May be empty.
	Scopes []string
	// Audience requested. May be empty.
	Audience string
	// SafetyWindow is how long before expiry to refresh proactively.
	// Defaults to 60s.
	SafetyWindow time.Duration
	// HTTPClient overrides the default transport. Optional.
	HTTPClient *http.Client
}

ClientCredentialsConfig configures a ClientCredentialsProvider.

type ClientCredentialsProvider

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

ClientCredentialsProvider obtains and refreshes service-account access tokens via the OAuth2 client_credentials grant. Plug it into client.WithTokenProvider:

cc := auth.NewClientCredentialsProvider(...)
c := client.New(base, client.WithTokenProvider(cc.Token))

func NewClientCredentialsProvider

func NewClientCredentialsProvider(cfg ClientCredentialsConfig) *ClientCredentialsProvider

NewClientCredentialsProvider builds a provider with sensible defaults.

func (*ClientCredentialsProvider) Invalidate

func (p *ClientCredentialsProvider) Invalidate()

Invalidate clears the cached token. The next Token call will fetch.

func (*ClientCredentialsProvider) Token

Token returns a valid access token, fetching a fresh one if the cached token has expired or is within the safety window. Safe for concurrent use.

type Error

type Error struct {
	Kind    ErrorKind
	Message string
}

Error categorises auth failures. Use errors.Is on the sentinels below.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches by Kind so errors.Is(err, ErrTokenExpired) works regardless of the wrapped message.

type ErrorKind

type ErrorKind int

ErrorKind enumerates auth error categories.

const (
	// KindTokenExpired — JWT exp has passed.
	KindTokenExpired ErrorKind = iota
	// KindInvalidToken — bad signature, wrong issuer/audience, malformed.
	KindInvalidToken
	// KindDiscovery — OIDC discovery or JWKS fetch failed.
	KindDiscovery
	// KindTokenExchange — /oauth/token or related endpoint failed.
	KindTokenExchange
	// KindConfig — bad configuration (missing required field, bad key length).
	KindConfig
	// KindCrypto — cryptographic operation failed.
	KindCrypto
)

type HmacTokenValidator

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

HmacTokenValidator validates HS256-signed JWTs using a shared secret. Use for development or when your app shares a signing secret with FlowCatalyst. For production OIDC integration, prefer TokenValidator (RS256 via JWKS).

func NewHmacTokenValidator

func NewHmacTokenValidator(secret, issuer, audience string) *HmacTokenValidator

NewHmacTokenValidator builds an HS256 validator.

func (*HmacTokenValidator) Validate

func (v *HmacTokenValidator) Validate(token string) (*AuthContext, error)

Validate verifies the HS256 signature, issuer, audience, and exp/nbf.

func (*HmacTokenValidator) ValidateBearer

func (v *HmacTokenValidator) ValidateBearer(authHeader string) (*AuthContext, error)

ValidateBearer strips the "Bearer " prefix and calls Validate.

type IntrospectionResponse

type IntrospectionResponse struct {
	Active    bool   `json:"active"`
	Scope     string `json:"scope,omitempty"`
	ClientID  string `json:"client_id,omitempty"`
	Username  string `json:"username,omitempty"`
	TokenType string `json:"token_type,omitempty"`
	Exp       int64  `json:"exp,omitempty"`
	Iat       int64  `json:"iat,omitempty"`
	Sub       string `json:"sub,omitempty"`
}

IntrospectionResponse is the body of /oauth/introspect (RFC 7662).

type JwksCache

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

JwksCache wraps jwx's jwk.Cache so callers don't have to deal with the underlying lifetime context.

func NewJwksCache

func NewJwksCache(httpClient *http.Client, refreshInterval time.Duration) *JwksCache

NewJwksCache builds a cache with the given refresh interval.

func (*JwksCache) Get

func (c *JwksCache) Get(ctx context.Context, url string) (jwk.Set, error)

Get returns the current key set for the URL, fetching on first call.

func (*JwksCache) Refresh

func (c *JwksCache) Refresh(ctx context.Context, url string) error

Refresh forces a re-fetch for the URL.

func (*JwksCache) Register

func (c *JwksCache) Register(url string) error

Register adds a JWKS URL to the cache with auto-refresh.

type OAuthClient

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

OAuthClient wraps the OAuth2 authorization-code flow + the OIDC session-end (RP-initiated logout) endpoint.

func NewOAuthClient

func NewOAuthClient(cfg OAuthConfig) *OAuthClient

NewOAuthClient builds an OAuthClient. Scopes default to openid/profile/email.

func (*OAuthClient) AuthorizeURL

func (c *OAuthClient) AuthorizeURL() (string, AuthorizeParams)

AuthorizeURL builds the URL to redirect users to for login, plus the session-stored verifier/state/nonce to validate the callback.

func (*OAuthClient) ExchangeCode

func (c *OAuthClient) ExchangeCode(ctx context.Context, code, codeVerifier string) (*TokenResponse, error)

ExchangeCode exchanges an authorization code for tokens. Call this from your callback handler after validating state and PKCE.

func (*OAuthClient) IntrospectToken

func (c *OAuthClient) IntrospectToken(ctx context.Context, token string) (*IntrospectionResponse, error)

IntrospectToken posts to /oauth/introspect and returns the response.

func (*OAuthClient) LogoutURL

func (c *OAuthClient) LogoutURL(postLogoutRedirectURI, idTokenHint, state string) string

LogoutURL builds the RP-Initiated Logout URL.

When postLogoutRedirectURI is non-empty, idTokenHint must also be set — FlowCatalyst uses the hint's aud claim to verify the redirect URI against the client's registered postLogoutRedirectUris (OIDC RP- Initiated Logout 1.0 §2). Omitting the hint causes the OP to refuse the redirect.

func (*OAuthClient) RefreshToken

func (c *OAuthClient) RefreshToken(ctx context.Context, refreshToken string) (*TokenResponse, error)

RefreshToken exchanges a refresh token for a fresh access token.

func (*OAuthClient) RevokeToken

func (c *OAuthClient) RevokeToken(ctx context.Context, token string) error

RevokeToken revokes an access or refresh token per RFC 7009. A 200 response is required; anything else returns an Error.

func (*OAuthClient) UserInfo

func (c *OAuthClient) UserInfo(ctx context.Context, accessToken string) (*UserInfoResponse, error)

UserInfo fetches /oauth/userinfo using the access token.

type OAuthConfig

type OAuthConfig struct {
	// IssuerURL is the FlowCatalyst OIDC server base URL.
	IssuerURL string
	// ClientID is the registered OAuth client id.
	ClientID string
	// ClientSecret is the OAuth client secret (omit for public clients).
	ClientSecret string
	// RedirectURI is the application's callback URL.
	RedirectURI string
	// Scopes requested. Defaults to ["openid", "profile", "email"].
	Scopes []string
	// HTTPClient overrides the default transport. Optional.
	HTTPClient *http.Client
}

OAuthConfig configures an OAuthClient for the authorization-code flow.

type PkceChallenge

type PkceChallenge struct {
	// CodeVerifier is the secret. Store in session; send on token exchange.
	CodeVerifier string
	// CodeChallenge is sent in the authorize URL.
	CodeChallenge string
	// CodeChallengeMethod is always "S256".
	CodeChallengeMethod string
}

PkceChallenge is a one-shot verifier/challenge pair for the OAuth2 authorization-code flow with PKCE (RFC 7636).

func NewPkceChallenge

func NewPkceChallenge() PkceChallenge

NewPkceChallenge generates a fresh PKCE pair using S256.

type TokenResponse

type TokenResponse struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	ExpiresIn    int64  `json:"expires_in"`
	RefreshToken string `json:"refresh_token,omitempty"`
	IDToken      string `json:"id_token,omitempty"`
	Scope        string `json:"scope,omitempty"`
}

TokenResponse is the body of /oauth/token.

type TokenValidator

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

TokenValidator verifies RS256 JWTs against a JWKS fetched via OIDC discovery.

func NewTokenValidator

func NewTokenValidator(cfg TokenValidatorConfig) *TokenValidator

NewTokenValidator builds a TokenValidator. The first call to Validate performs OIDC discovery to find the JWKS URL.

func (*TokenValidator) RefreshJWKS

func (v *TokenValidator) RefreshJWKS(ctx context.Context) error

RefreshJWKS forces an immediate re-fetch of the JWKS, useful after a known key rotation. Subsequent Validate calls will see the new keys.

func (*TokenValidator) Validate

func (v *TokenValidator) Validate(ctx context.Context, token string) (*AuthContext, error)

Validate verifies signature, issuer, audience, and exp/nbf claims.

func (*TokenValidator) ValidateBearer

func (v *TokenValidator) ValidateBearer(ctx context.Context, authHeader string) (*AuthContext, error)

ValidateBearer strips the "Bearer " prefix and calls Validate.

type TokenValidatorConfig

type TokenValidatorConfig struct {
	// IssuerURL is the FlowCatalyst OIDC server (e.g. https://auth.flowcatalyst.io).
	IssuerURL string
	// Audience is your application's expected aud claim. Defaults to "flowcatalyst".
	Audience string
	// JWKSRefreshInterval is how often jwx checks for key rotation.
	// Defaults to 1 hour.
	JWKSRefreshInterval time.Duration
	// ClockSkew is the acceptable difference for exp/nbf checks.
	// Defaults to 60s.
	ClockSkew time.Duration
	// HTTPClient is the transport used for discovery + JWKS fetch.
	// Defaults to a 15-second-timeout client.
	HTTPClient *http.Client
}

TokenValidatorConfig configures a TokenValidator. IssuerURL and Audience are required; everything else has a sensible default.

type UserInfoResponse

type UserInfoResponse struct {
	Sub           string         `json:"sub"`
	Name          string         `json:"name,omitempty"`
	Email         string         `json:"email,omitempty"`
	EmailVerified *bool          `json:"email_verified,omitempty"`
	Extra         map[string]any `json:"-"`
}

UserInfoResponse is the body of /oauth/userinfo. Extra carries any custom claims the platform adds.

func (*UserInfoResponse) UnmarshalJSON

func (u *UserInfoResponse) UnmarshalJSON(b []byte) error

UnmarshalJSON keeps standard fields typed and bucketing extras.

Jump to

Keyboard shortcuts

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