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

browser

package module
v0.6.1 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 34 Imported by: 0

README

gossoclient

A Go browser-session client using OIDC, PostgreSQL and encrypted refresh-token storage. It provides verified identity proofs; your application owns local accounts, identity linking and permissions.

The supported protocol is an AuthHub-compatible OIDC profile, not a general-purpose adapter for every OIDC provider.

Module: github.com/assurrussa/gossoclient
Packages: browser at the module root, and ingress

import browser "github.com/assurrussa/gossoclient"

Requirements

  • Go 1.27.2 or newer
  • An application-owned PostgreSQL database or isolated schema
  • A registered confidential OIDC client and project
  • Explicit HTTPS issuer, application origin and callback URLs
  • A protected client secret and exactly 32 raw storage-key bytes

The caller supplies and owns *sql.DB. The client neither opens nor closes the pool and does not apply migrations automatically. The protected-file implementation is Linux-oriented; cross-platform support is not advertised.

Configure the client

Apply browser.Schema() through your application's explicit setup or migration process. Use an isolated PostgreSQL schema and configure every pooled connection consistently. Do not use the identity provider's database schema.

When sharing the host application's existing pool, use NewPostgresStoreWithSchema(ctx, db, "app_sso"). It borrows that exact *sql.DB and schema-qualifies every session query, including row locks. Schema names are case-sensitive ASCII identifiers ([A-Za-z_][A-Za-z0-9_]*, at most 63 bytes). The old constructor and table names remain unchanged.

Create and migrate the selected schema in the host's normal migration process. Schema() still returns the existing unqualified DDL: apply it on an explicitly owned transaction with a safely quoted SET LOCAL search_path for that schema. Never issue a session-level SET search_path on a shared pool. The SDK does not create schemas, migrate, open another pool, change pool settings or close the host's pool. Existing rows can stay in the same schema without a data migration.

After applying the schema:

store, err := browser.NewPostgresStore(ctx, db)
if err != nil {
    return err
}

options := []browser.Option{
    browser.WithIssuer(issuer),
    browser.WithClientID(clientID),
    browser.WithProjectID(projectID),
    browser.WithOrigin(origin),
    browser.WithCallback(origin + "/callback"),
    browser.WithStore(store),
    browser.WithClientSecret(clientSecret),
    browser.WithStorageKey(storageKey),
    browser.WithRevocationEndpoint(revocationURL),
}

if centralLogoutURL != "" {
    options = append(options,
        browser.WithCentralLogoutURL(centralLogoutURL))
}

client, err := browser.NewWithOptions(ctx, options...)
if err != nil {
    return err
}
Require verified authentication methods (opt-in)

Roll out the SDK upgrade with the option omitted first. Enable WithRequiredAMR("mfa") only after the deployed AuthHub issuer signs trustworthy AMR for completed authentication and preserves it correctly across refresh. Upgrading this SDK alone remains compatible with an older issuer in opt-out mode; enabling the requirement against an issuer that omits AMR denies login and recovered sessions. Do not infer rollout readiness from MFA enrollment.

Add browser.WithRequiredAMR("mfa") to the options to require the exact, case-sensitive mfa method in every admitted proof. Identity.AMR is an independent copy of the methods in the current cryptographically verified ID token. auth_time retains its existing meaning and refresh pins. Enrollment hints (authhub_mfa), profile metadata, access tokens and OAuth response fields never establish authentication strength.

The requirement applies at callback, browser verification, stored/recovered proof admission (Check/Valid), and refresh. Missing, empty, malformed or ambiguous AMR cannot satisfy it. Methods must be nonempty printable ASCII strings without spaces; values are not normalized. Duplicate method values are valid. Duplicate or case-aliased outer amr claim names are ambiguous and suppressed. A refresh replaces AMR entirely, never inheriting old methods.

Without the option, missing or malformed signed AMR is treated as absent and existing login behavior is preserved. Stored legacy proofs without AMR remain usable only when no method is required; malformed stored JSON fails closed. No schema migration is needed: verified AMR is part of the existing durable proof JSON. Proof getters copy the methods so callers cannot change authority.

On a requirement failure, errors.Is(err, browser.ErrRequiredAMR) is true. This is intentionally distinct from ErrDenied and does not match it: ordinary fresh-admission handling must not turn a strength-policy failure into a loop. The callback returns HTTP 401 with an explicit error and no redirect. Browser admission/refresh terminates the unsatisfied local session; Check and Valid remain read-only and deny it. No proof or host session may be created on denial. This option does not request or initiate step-up. Do not automatically redirect failures back into login: show a terminal error and offer only an explicit user-controlled next action.

Load secrets from protected application inputs. Do not put them in source, URLs or logs.

Issuer, client ID, project ID, origin, callback, store, secret and storage key are required explicitly. Missing, nil, malformed and duplicate options fail construction. Repeating an identical option also fails.

Issuer and application origin must be canonical HTTPS origins without trailing slashes and must have different hostnames. Different ports alone do not isolate browser cookies. Callback must equal the application origin plus /callback. Project ID must be a nonzero canonical UUID.

For an AuthHub issuer that guarantees an immutable client-to-project registration and never reassigns a retained client ID, replace WithProjectID(projectID) with WithAuthHubClientBoundProject(). These options are mutually exclusive. The exact issuer and client audience remain mandatory; only a fully verified signed ID token can supply the canonical, nonzero project_id. Every proof and binding retains that project, and refresh, validity and logout checks preserve its pin. No first-login project cache or unsigned discovery/profile field supplies authority. Do not select this mode for a generic provider without the same immutable-registration guarantee.

Provider endpoints

WithRevocationEndpoint selects an explicit URL and takes precedence over discovery. If omitted, discovery must provide a valid revocation_endpoint. The strict constructor never guesses a provider-specific revocation path.

WithCentralLogoutURL supplies an optional browser sign-out landing page. If omitted, View.CentralLogout is empty and your renderer must hide that link. A landing-page link is not confirmation of issuer-wide logout.

Both URLs must remain on the configured HTTPS issuer origin. Cross-origin URLs, userinfo, queries, fragments and ambiguous escaped paths are rejected. Backend HTTP redirects are forbidden, including revocation redirects. Explicitly empty endpoint options are errors; omit the option instead.

ValidateConfig and ValidateIssuerEndpoint(issuer, endpoint) support network-free configuration checks before opening the database or reading protected inputs. Use ValidateAuthHubClientConfig instead of ValidateConfig for the explicit client-bound AuthHub profile; it requires an empty ProjectID.

Other supported options
  • WithFreshness: maximum signed-proof freshness, capped at five minutes; zero uses that cap
  • WithIssuerCAFile: public PEM trust roots for this client, replacing system roots
  • WithIngress: an immutable, explicitly verified private HTTP ingress policy behind an HTTPS edge; omitted or nil means direct TLS
  • WithHTTPSReverseProxy(): explicitly use the host application's HTTPS-only reverse proxy, instead of WithIngress

WithHTTPSReverseProxy() relies on deployment isolation: the backend HTTP listener must not be published or reachable outside the trusted HTTPS edge, and the edge must enforce HTTPS for all browser traffic. The SDK cannot verify those deployment conditions. It does not inspect Forwarded or X-Forwarded-*, fabricate r.TLS, or use request headers to choose its origin. Exact configured Host, Origin/CSRF checks and Secure cookies remain enforced. Outbound issuer TLS and certificate/hostname validation are unchanged; ordinary publicly trusted HTTPS uses system roots and needs no custom CA file. The reverse-proxy option and WithIngress are mutually exclusive. Omitting both continues to require direct TLS.

There are no options to disable issuer TLS validation, CSRF protection or freshness limits, and no arbitrary HTTP-client or transport hooks.

Storage-key bytes are copied when the option is created and again for each construction. Configuration and the store wrapper are frozen during construction. The database remains caller-owned and must stay open while the client is in use.

HTTP integration

client.Handler(render) serves:

  • GET /: application-owned rendering
  • POST /login: begin sign-in
  • GET /callback: consume the OIDC callback
  • POST /logout: terminate the browser session

The separate Begin, Callback and Logout methods enforce those same paths and methods. POST forms must include View.CSRF and meet the same-origin checks. Callback success installs durable state and redirects to /; it does not create a local user.

Render View.CentralLogout only when nonempty.

Call Verify(request) on each protected browser request. Resolve the verified issuer and subject to an explicitly linked local account, then check your application's own permissions. Standard username claims and profile/project metadata are display data, not account-linking or authorization evidence. The optional, separately signed authhub_identifiers claim has the bounded versioned contract below.

Canonical AuthHub identifiers

Identity.AuthHubIdentifiers is nil for old tokens and providers that omit the dedicated signed authhub_identifiers claim. Version 1 carries a required canonical, case-sensitive login and an optional lowercase email_alias, derived by AuthHub from its operator-administered canonical records. The alias is a login lookup identifier, not verified mailbox ownership. Invalid versions or malformed/noncanonical values fail verification. No standard or profile claim is promoted into this type, and returned snapshots are copied.

These are issuance-time claims with the proof's existing freshness deadline, not live account lookups, immutable identity pins or permissions. Refresh may return renamed canonical identifiers for the same subject. Only an explicitly authorized host linking policy for the pinned issuer may use this claim. Identifier-based first-link should require a new successful callback; never use a restored old browser proof as evidence of a current identifier assignment. Preserve issuer/subject as the durable link and administer all local roles locally. This does not eliminate the normal issuance-to-admission race.

Proofs and host sessions

A Proof is an immutable snapshot with a fixed deadline. Never convert that deadline into a rolling local TTL. Identity.SessionExpiresAt is the verified provider session deadline; Identity.AbsoluteUntil is additionally capped by the existing local browser-session lifetime. Token exp caps proof freshness, not the full login lifetime. Missing or malformed session_expires_at fails closed; the client never substitutes auth_time + 7 days.

Stored proofs from older versions cannot authorize locally until upgraded. Verify can upgrade them through one ordinary, single-owner refresh with a new strictly verified signed deadline, preserving identity and login lineage. Check does not refresh and denies those legacy snapshots. Unknown refresh outcomes are not replayed. An unsuccessful upgrade never authorizes from the old cached proof; fresh interactive login is needed when refresh cannot recover.

For a host-owned session, persist the complete Proof.Binding(), including LoginGeneration and the original deadline. Bindings belong in protected application state, never browser forms, query parameters or headers.

Check(ctx, binding) validates that saved binding against durable state without network access or refresh. It checks the exact generation and identity pins and cannot extend the original deadline. Refresh requires admission of a newly verified proof.

Use errors.Is:

  • ErrRequiredAMR: terminal authentication-strength policy failure; show an error with manual remediation, never automatically restart sign-in
  • ErrDenied: expired, terminal, superseded or mismatched authority; require fresh admission
  • ErrUnavailable: validity could not be confirmed; fail closed without creating or extending authorization

Central revocation is observed at refresh or the existing signed freshness deadline. Check is not online provider introspection. Unknown exchange or refresh outcomes are not automatically replayed.

Logout

RevokeBinding represents intentional user logout. The host must authenticate that intent and enforce CSRF protection.

It can end refresh descendants of the same original login, including after freshness expires. An older binding cannot terminate a distinct newer login. Native session replacement or cleanup must not call it merely to detach an old host session.

Local terminal state commits before remote revocation:

  • nil: local termination and the binding's remote revocation were confirmed
  • ErrRevocationUnconfirmed: local termination committed, but remote revocation was not confirmed
  • ErrUnavailable: local termination was not confirmed; no remote revocation follows that uncertain write
  • ErrDenied: the binding no longer identifies the permitted logout target

None of these results promises issuer-wide logout.

LogoutFromHost(w, r) adapts a native local host logout when there is also an unlinked RP browser cookie. Call it only AFTER the host authenticates intentional logout and verifies its own session-bound CSRF token. It is not a standalone public route or a CSRF bypass for Logout. It still requires POST, the exact configured Origin, trusted ingress and an unambiguous browser cookie.

It captures the current RP row once without Verify/refresh, then uses the same generation-fenced logout implementation. For an already admitted external host session, always use RevokeBinding on the saved ORIGINAL binding instead; capturing a new browser snapshot could otherwise target a newer login. Missing cookies or missing rows are explicit 303 local-only no-ops without cookie changes. Invalid input returns 400 unconfirmed, storage errors 503 unconfirmed; both preserve cookies. Existing active/pending row outcomes follow the table below.

The HTTP Logout helper preserves these distinctions through status codes and the compatibility header X-AuthHub-Logout-Scope:

Result Response
Binding revocation confirmed 303, binding-revoked, cookie cleared
Anonymous/pending/ended local logout 303 to /?logout=local-only, cookie cleared
Local termination confirmed, remote outcome unconfirmed 503, local-only, cookie cleared
Local write unconfirmed 503, unconfirmed, cookie preserved
Logout superseded by a newer login 409, unconfirmed, newer cookie preserved

Hosts using RevokeBinding directly own their cookie-clearing and response behavior; the HTTP helper's response contract does not replace that integration.

Supported provider profile

The client uses coreos/go-oidc/v3 and golang.org/x/oauth2. Its additional profile requirements remain enforced:

  • RS256 ID tokens with the exact issuer and single registered audience
  • Authorization code flow with state, nonce and S256 PKCE
  • Client-secret Basic authentication
  • Same-origin authorization, token, JWKS and selected revocation endpoints
  • Required project_id, token_use: "id", canonical UUID sid and positive auth_time
  • Optional bounded authhub_profile and authhub_project metadata
  • Optional signed authhub_identifiers: {version: 1, login, email_alias?} with canonical bounds
  • A new refresh token and verified ID token on successful refresh
  • Required signed RFC3339 session_expires_at in the verified ID token
  • Preserved identity/session pins and provider deadline across refresh, bounded freshness and a non-extending local absolute lifetime
  • TLS 1.3 for provider connections

Configurable URLs do not remove these requirements or establish interoperability with another provider.

Compatibility

The existing New(ctx, Config, Options) API remains an AuthHub-profile compatibility entry point. It retains the relative routes /oauth2/revoke and /sso/logout. Prefer NewWithOptions for new integrations; its strict configuration has no such defaults.

Both constructors share the same session implementation. The following formats are retained for compatibility:

  • The __Host-sso-example cookie
  • Existing sso_example_* SQL objects
  • Encrypted-state associated-data formats
  • Stored identity fields, binding pins and LoginGeneration

Renaming these is a data/session migration, not cosmetic cleanup. Legacy rows or bindings without the login-lineage marker fail closed and require fresh sign-in.

Testing

Use a disposable PostgreSQL database for SSO_TEST_DATABASE. Durable tests create and remove isolated schemas. Without this setting, those tests skip; a passing unit-only run does not establish durable-session coverage.

go test -race ./...
go vet ./...
golangci-lint fmt --diff
golangci-lint run --timeout=3m

Test the selected provider profile and your application integration before deployment. Authentication failures and uncertain storage/network outcomes must remain fail-closed.

Optional MFA setup reminder

Identity.AuthHubMFA is populated only from the verified, profile-scoped authhub_mfa ID-token claim. Version 1 includes explicit enrolled and setup_available booleans. It is an advisory issuance snapshot, not amr, MFA authentication strength, a session pin, or an admission rule.

Missing, malformed, duplicate-field, incomplete, and unknown-version hints are hidden without breaking authentication. Normal refresh replaces or clears the snapshot; no extra polling or refresh is needed. Proof.Identity returns a copy. Only use it while the owning Proof is valid, and show a nonblocking reminder only when unenrolled and setup-available. Use an application-configured, validated account URL on the trusted issuer, never a link from token metadata. Do not interrupt successful login or infer factor enrollment from amr/profile.

License

MIT. Dependencies retain their respective licenses.

SQLite storage

SQLite is an alternative durable backend with the same generation, login-lineage, one-use callback, refresh-uncertainty and logout fences. The application imports its SQLite driver, opens its own database, applies SQLiteSchema() explicitly, and passes NewSQLiteStore(ctx, db) to WithStore (or Options.Store). PostgreSQL callers and Schema() retain their existing behavior.

Use SQLite 3.35 or newer. The adapter never changes PRAGMA settings, user_version, connection limits or database lifetime. Configure durability, foreign keys and busy timeout on every connection according to your application's requirements. Use a file-backed database for persistence; a plain :memory: DSN creates separate databases per connection. The tables use exact INTEGER Unix nanoseconds, and stored deadlines must fit that representation (approximately years 1678–2262). There is no automatic PostgreSQL-to-SQLite migration.

Write transitions acquire BEGIN IMMEDIATE before reading deadlines and use one borrowed connection, including with MaxOpenConns(1). The transaction ends before issuer HTTP begins. A claimed or uncertain refresh is never automatically returned to ready. Applications must not modify the store tables themselves.

The shared conformance suite runs against modernc.org/sqlite (one-connection and pooled configurations) and PostgreSQL. PostgreSQL cases still require SSO_TEST_DATABASE; a run with those cases skipped is not full conformance.

Internally, application session logic calls a private transition interface. PostgreSQL and SQLite implement it directly; driver selection and immutable handle snapshots happen at construction. Backend SQL, row/write locks and ambiguous-transaction cleanup remain inside each adapter.

Optional signed email snapshots

Email claims remain opt-in for existing consumers. Add WithEmailScope() to NewWithOptions, or set Config.RequestEmail = true with New, to request openid profile email instead of the unchanged openid profile default. The issuer/client registration must support that scope.

With this option, Proof.Identity().Email and EmailVerified come only from the cryptographically verified ID token. Malformed/null email claims, non-boolean verification flags, and a true flag without a valid address deny that proof. Missing claims or a false flag never imply verification. There is no fallback to authhub_identifiers.email_alias, profile metadata, UserInfo, or browser input. Without the option, unsolicited email claims retain the previous ignored behavior.

These are issuance snapshots, not immutable identity keys or local authority. A new verified refresh snapshot can change or remove email/verification; it never changes the older Proof value. Durable JSON stores retain the fields across restart, without a schema migration. Legacy rows without them remain unverified. Generation, exact issuer/subject/client/project/session pins and all original freshness/absolute bounds retain their existing checks.

The SDK does not globally require verified email. A host that requires it must check both a nonempty Email and EmailVerified for enrollment, together with its current project proof, invitation/link, membership and role policy. Never auto-link accounts by matching email. This feature neither verifies a mailbox itself nor makes an email alias verified; the issuer must run the actual mailbox challenge before signing a true claim.

The existing signed AuthHubIdentifiers.Login contract is unchanged. Login, email and display/preferred username can change between issuance snapshots; only exact issuer plus subject identifies the external account. None of those mutable values, including a true email verification claim, grants a local role, membership or automatic identity link.

Transactional callback integrations

Use Client.CallbackResult(w, r) when a host must provision local state only after a newly completed callback. It shares the existing ingress, browser/state, PKCE/nonce, one-use code exchange, request limits and durable session protocol. On success it returns an opaque CallbackOutcome after a confirmed installation and exact-generation re-read. Proof() is immutable and retains the original deadline; ReturnPath() is the validated path stored by Begin, bound to that login state. Callback query parameters and an existing cookie cannot mint it.

HTTP ownership is explicit: on error the SDK has already written the failure response, so return immediately. On success only security headers are set: the host owns the success response and may first commit its local transaction. Do not call Callback again, exchange the code twice, or infer success from a redirect status/Location. Existing Callback and Handler still return a 303 redirect. A local transaction failure does not undo a committed RP callback; deny local access and require a new state-bound login for another enrollment attempt. The outcome grants no local identity link, role or membership. Validate exact issuer/client/project/subject, current native state and any required signed email claims; mutable login/email/profile changes never change identity or grant authority. Revalidate the original proof via read-only Check at commit.

Documentation

Overview

Package browser implements AuthHub relying-party browser sessions using the reviewed OIDC libraries and durable generation fences. It supplies identity proofs, never local user linking or application permissions.

Index

Constants

This section is empty.

Variables

View Source
var ErrDenied = errFenced

ErrDenied means this proof/session cannot authorize the current request. Expired, terminal and superseded generations require a new admission.

View Source
var ErrRequiredAMR = errors.New("required authentication method not satisfied")

ErrRequiredAMR means the verified proof lacks the required authentication method. It deliberately does not match ErrDenied: a host must show a terminal policy error, not automatically retry login or initiate fresh admission.

View Source
var ErrRevocationUnconfirmed = errors.New("local logout committed; central revocation unconfirmed")

ErrRevocationUnconfirmed means local terminal state committed, but central revocation could not be confirmed. Never replay refresh or resurrect a binding.

View Source
var ErrUnavailable = errors.New("session validity unavailable")

ErrUnavailable means the boundary could not confirm validity. Fail closed; do not mint or extend a host session. A later request may retry validity, but must never independently replay an OAuth exchange or claimed refresh.

Functions

func ReadClientSecret

func ReadClientSecret(path, clientID string) (string, error)

ReadClientSecret reads the exact restart-bound maintenance candidate format.

func ReadProtected

func ReadProtected(path string, limit int64) ([]byte, error)

ReadProtected loads an explicitly supplied Linux protected file with the reviewed ownership, mode, size and no-symlink checks. It never prints contents.

func SQLiteSchema added in v0.4.0

func SQLiteSchema() string

SQLiteSchema returns application-owned DDL. Apply it explicitly before construction. Configure durability and busy timeout on the application's connections; this package never changes PRAGMA settings or user_version. Timestamps are signed INTEGER Unix nanoseconds, never floating-point values.

func Schema

func Schema() string

Schema returns the reviewed RP schema. Existing table names are retained for compatibility; isolate each application's rows using its PostgreSQL schema.

func ValidateAuthHubClientConfig added in v0.2.0

func ValidateAuthHubClientConfig(c Config) error

ValidateAuthHubClientConfig checks the same network-free settings for WithAuthHubClientBoundProject. ProjectID must be empty; the issuer must satisfy that option's immutable-client registration contract.

func ValidateConfig

func ValidateConfig(c Config) error

ValidateConfig checks exact origins and identity pins without network access.

func ValidateIssuerEndpoint

func ValidateIssuerEndpoint(issuer, endpoint string) error

ValidateIssuerEndpoint checks an explicit provider endpoint without network access. Validate application identity/origin settings with ValidateConfig too. For an absent optional central logout link, omit this check and its option.

Types

type Binding

type Binding struct {
	Reference        string    `json:"reference"`
	Generation       int64     `json:"generation"`
	LoginGeneration  int64     `json:"loginGeneration"`
	Deadline         time.Time `json:"deadline"`
	Issuer           string    `json:"issuer"`
	Subject          string    `json:"subject"`
	ProjectID        string    `json:"projectId"`
	ClientID         string    `json:"clientId"`
	CentralSessionID string    `json:"centralSessionId"`
	AuthTime         int64     `json:"authTime"`
	AbsoluteUntil    time.Time `json:"absoluteUntil"`
}

Binding is persisted inside a protected host-owned external session after successful admission. Never accept it from browser form/query/header input. The host must preserve the original Deadline unchanged; it is not a TTL. LoginGeneration identifies a local login admission, preserved across refresh. Zero markers from legacy rows/bindings require a fresh sign-in.

type CallbackOutcome added in v0.5.0

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

CallbackOutcome is an immutable, successfully committed callback result. A zero outcome conveys no authority. Its return path was saved with the one-use state/PKCE/browser-bound login, never read from the callback query.

func (CallbackOutcome) Proof added in v0.5.0

func (o CallbackOutcome) Proof() Proof

Proof returns a copy of the original, fixed-deadline identity snapshot.

func (CallbackOutcome) ReturnPath added in v0.5.0

func (o CallbackOutcome) ReturnPath() string

ReturnPath returns the exact validated root-relative path saved at Begin.

type CanonicalIdentifiers added in v0.2.0

type CanonicalIdentifiers struct {
	Version    int    `json:"version"`
	Login      string `json:"login"`
	EmailAlias string `json:"email_alias,omitempty"`
}

CanonicalIdentifiers is an optional AuthHub-specific signed identity claim. Version 1 asserts that Login and EmailAlias come from the issuer's canonical, operator-administered account records, never arbitrary profile metadata. Login is case-sensitive. EmailAlias is lowercase and optional; it is a login alias, NOT proof of mailbox ownership or an email_verified assertion. These are current issuance snapshots, not immutable subject/session pins. Hosts may use them only under an explicit account-linking policy for their trusted issuer. They grant no local role or permission. Issuer/Subject remains the durable identity key; absence must never fall back to profile metadata.

type Client

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

Client is safe for concurrent HTTP use. The caller owns the database lifetime.

func New

func New(ctx context.Context, c Config, o Options) (*Client, error)

New retains the original AuthHub-profile API and provider-relative logout routes for existing callers. New integrations should use NewWithOptions. New performs bounded OIDC discovery using the configured issuer trust roots.

func NewWithOptions

func NewWithOptions(ctx context.Context, values ...Option) (*Client, error)

NewWithOptions validates explicit identity, origin and protected inputs before discovery. It uses a supplied or discovered revocation endpoint, never a guessed provider route. No central logout link is exposed unless configured. Configuration is frozen at construction; only the caller-owned database and an immutable ingress policy are shared. Protocol and session fences are the same as New; this constructor does not imply general OIDC-provider support.

func (*Client) Begin

func (c *Client) Begin(w http.ResponseWriter, r *http.Request)

Begin handles the exact POST /login form using the library CSRF/browser state.

func (*Client) Callback

func (c *Client) Callback(w http.ResponseWriter, r *http.Request)

Callback handles the exact GET /callback. Success installs durable state and redirects; admit the local user on a subsequent Verify, never from query data.

func (*Client) CallbackResult added in v0.5.0

func (c *Client) CallbackResult(w http.ResponseWriter, r *http.Request) (CallbackOutcome, error)

CallbackResult handles exactly GET /callback using the same ingress, nonce, state, PKCE, durable generation and concurrency protections as Callback. Success returns only after installation and a current durable re-read; it writes security headers but NO success status, body, cookie or redirect. The host then owns success delivery and may perform transactional enrollment. On error the failure HTTP response is ALREADY written; the host must return without writing another response. No outcome may authorize a failed callback. This method never calls Verify or refreshes a previous browser session.

func (*Client) Check

func (c *Client) Check(ctx context.Context, b Binding) (Proof, error)

Check validates a previously admitted host-owned binding after restart without network access or refresh. An edited deadline beyond the durable proof cap is rejected. The host must also match the returned issuer/subject to its explicit local user link and recheck its own ACL; this method grants no local roles.

func (*Client) Handler

func (c *Client) Handler(render func(http.ResponseWriter, *http.Request, View)) http.Handler

Handler serves GET /, POST /login, GET /callback and POST /logout. render owns the landing UI, explicit local user resolution and local permissions. Form POSTs must carry View.CSRF. Other application routes can call Verify directly.

func (*Client) Logout

func (c *Client) Logout(w http.ResponseWriter, r *http.Request)

Logout handles the exact POST /logout. Local terminal generation commits before bounded central revocation. Failed confirmation is not a success acknowledgement; the host must still fail closed at its Check/Valid boundary. A stale login returns 409 and preserves the newer cookie. Local termination with unconfirmed remote revoke/decryption returns 503 and clears the cookie. Anonymous/pending logout redirects to /?logout=local-only. The response's X-AuthHub-Logout-Scope is binding-revoked, local-only, or unconfirmed; it never promises issuer-wide logout. Refresh descendants retain the original intent.

func (*Client) LogoutFromHost added in v0.2.0

func (c *Client) LogoutFromHost(w http.ResponseWriter, r *http.Request)

LogoutFromHost terminates the current browser's RP session after the host has authenticated intentional logout and verified its OWN CSRF token. Never mount this method directly as an unauthenticated handler: Origin is defense in depth, not a substitute for the host's session-bound CSRF check.

Use this only for a native local host session with no saved external binding. For an admitted external session use RevokeBinding with its ORIGINAL binding; re-reading the browser here would turn stale host intent into newer login intent.

The request must be POST with the exact configured Origin and trusted ingress. No library form/CSRF field is required. The browser snapshot is captured once, without Verify, authorization or refresh, and the ordinary Logout terminal generation fence and response scopes apply. A missing cookie or missing row returns 303 local-only without setting a cookie. Malformed/duplicate cookies, invalid ingress/origin and storage errors preserve cookies and fail closed.

func (*Client) RevokeBinding

func (c *Client) RevokeBinding(ctx context.Context, b Binding) error

RevokeBinding is intentional user logout, not session-replacement cleanup. It ends the current generation and refresh descendants of this same login. A subsequent login has a different LoginGeneration and cannot be revoked by this old binding, even when all central identity claims are unchanged. Expired freshness does not cancel logout intent. A strictly newer generation may have tightened AbsoluteUntil during legacy signed-deadline upgrade; that same-lineage descendant remains a permitted logout target. Unknown local commit returns ErrUnavailable; no automatic retry or remote revoke follows it. ErrRevocationUnconfirmed confirms local termination only. Caller must enforce logout request authentication and CSRF protection.

func (*Client) Valid

func (c *Client) Valid(ctx context.Context, p Proof) error

Valid checks an external session's original proof against current durable state. It never refreshes, extends deadlines or resurrects a terminal session. Refresh generation changes require a newly admitted proof. Central revocation is bounded by the original signed freshness deadline, at most five minutes.

func (*Client) Verify

func (c *Client) Verify(r *http.Request) (Proof, error)

Verify checks browser binding, trusted ingress and durable validity, renewing only through the reviewed single-owner refresh CAS. No cached identity is returned on a failed check. Returned proofs have their own fixed deadline.

type Config

type Config struct {
	Issuer, ClientID, ProjectID, Origin, Callback string
	Freshness                                     time.Duration
	IssuerCAFile                                  string
	Ingress                                       *ingress.Policy
	// RequestEmail opts into requesting and validating signed OIDC email claims.
	// It does not require email or grant local linking, membership or roles.
	RequestEmail bool
}

Config pins the authority, application origin, client and project. Callback is exactly Origin + /callback. Zero Freshness means the reviewed five-minute cap. Ingress must be nil for direct TLS or an explicitly constructed trusted edge.

type Identity

type Identity struct {
	// Email is retained only from a signed ID token when email scope is opted in.
	// EmailVerified asserts mailbox ownership at issuance, never local authority.
	Email         string `json:"email,omitempty"`
	EmailVerified bool   `json:"email_verified,omitempty"`
	// AuthHubIdentifiers is only populated from the dedicated, signed claim.
	// Profile and standard email/username claims never supply this authority.
	AuthHubIdentifiers *CanonicalIdentifiers `json:"authhub_identifiers,omitempty"`
	// AMR contains only methods from the current verified signed ID token.
	// Enrollment, profile, access-token and prior-token claims never supply it.
	AMR []string `json:"amr,omitempty"`
	// AuthHubMFA is an advisory issuance snapshot, never authentication strength.
	AuthHubMFA    *MFAEnrollment `json:"authhub_mfa,omitempty"`
	Issuer        string         `json:"issuer"`
	ClientID      string         `json:"client_id"`
	Subject       string         `json:"subject"`
	ProjectID     string         `json:"project_id"`
	SessionID     string         `json:"sid"`
	AuthTime      int64          `json:"auth_time"`
	IssuedAt      time.Time      `json:"issued_at"`
	FreshUntil    time.Time      `json:"fresh_until"`
	AbsoluteUntil time.Time      `json:"absolute_until"`
	// SessionExpiresAt is the verified provider deadline, before local caps.
	// Zero identifies legacy stored proofs that must refresh before admission.
	SessionExpiresAt time.Time         `json:"session_expires_at"`
	Profile          map[string]string `json:"profile,omitempty"`
	Project          map[string]string `json:"project,omitempty"`
}

Identity contains independently verified OIDC identity claims.

type MFAEnrollment added in v0.3.0

type MFAEnrollment struct {
	Version        int  `json:"version"`
	Enrolled       bool `json:"enrolled"`
	SetupAvailable bool `json:"setup_available"`
}

MFAEnrollment is the optional signed authhub_mfa issuance snapshot. It is display-only: never use it as MFA proof, admission policy, or a session pin. Only a verified Identity from a currently valid Proof supplies this hint. Recheck through normal session renewal, not polling or a separate refresh. Setup links must use the application's explicitly configured trusted issuer.

func (*MFAEnrollment) UnmarshalJSON added in v0.3.0

func (v *MFAEnrollment) UnmarshalJSON(raw []byte) error

UnmarshalJSON keeps a malformed stored advisory hint from breaking an otherwise valid legacy identity. Version zero suppresses its projection.

type Option

type Option func(*options)

Option configures a new client. Use the typed With functions; duplicate options, including repeated identical values, and nil options are rejected.

func WithAuthHubClientBoundProject added in v0.2.0

func WithAuthHubClientBoundProject() Option

WithAuthHubClientBoundProject selects the AuthHub immutable-client profile instead of an explicit WithProjectID pin. Use only when the configured issuer guarantees a client ID belongs permanently to one project and never reuses that ID for another project. The project comes solely from a fully verified signed ID token and remains pinned in each durable proof, refresh and binding. No project is learned from unsigned metadata or cached as a first-login pin. This option is not safe for a generic provider without the same guarantee.

func WithCallback

func WithCallback(value string) Option

WithCallback supplies the registered callback, exactly origin plus /callback.

func WithCentralLogoutURL

func WithCentralLogoutURL(value string) Option

WithCentralLogoutURL sets an optional browser sign-out landing URL. It must remain on the issuer origin and conveys no guarantee of issuer-wide logout.

func WithClientID

func WithClientID(value string) Option

WithClientID supplies the explicitly registered client identifier.

func WithClientSecret

func WithClientSecret(value string) Option

WithClientSecret supplies a protected client secret. It is never logged.

func WithCurrentSessionLogoutEndpoint added in v0.3.0

func WithCurrentSessionLogoutEndpoint(value string) Option

WithCurrentSessionLogoutEndpoint opts intentional user logout into AuthHub's token-bound current-session contract. Cleanup still uses ordinary revocation. The endpoint must confirm X-AuthHub-Logout-Scope: current-session; HTTP 200 alone is insufficient. This option does not clear the issuer browser cookie.

func WithEmailScope added in v0.5.0

func WithEmailScope() Option

WithEmailScope requests the OIDC email scope and retains strictly typed, signed email/email_verified claims in each proof snapshot. Missing or false verification never becomes true. Email is not required; the host must enforce its enrollment policy and must never infer local linking or roles from email. Omit this option to retain the existing scopes and email-claim behavior.

func WithFreshness

func WithFreshness(value time.Duration) Option

WithFreshness sets a cap no greater than five minutes. Zero retains that cap.

func WithHTTPSReverseProxy added in v0.2.0

func WithHTTPSReverseProxy() Option

WithHTTPSReverseProxy explicitly delegates incoming transport security to the host application's HTTPS-only reverse proxy. The backend HTTP listener must not be publicly reachable or accept requests outside that trusted edge. This option does not verify network isolation, inspect forwarding headers or fabricate TLS state. Exact Host, Origin/CSRF checks and Secure cookies remain enforced, and issuer HTTPS verification is unchanged. Do not combine it with WithIngress; without either option, requests require direct TLS.

func WithIngress

func WithIngress(value *ingress.Policy) Option

WithIngress selects an immutable verified edge policy. Nil means direct TLS.

func WithIssuer

func WithIssuer(value string) Option

WithIssuer supplies the exact canonical HTTPS issuer origin.

func WithIssuerCAFile

func WithIssuerCAFile(value string) Option

WithIssuerCAFile selects bounded public PEM trust roots read at construction.

func WithOrigin

func WithOrigin(value string) Option

WithOrigin supplies the exact application HTTPS origin, distinct from issuer.

func WithProjectID

func WithProjectID(value string) Option

WithProjectID supplies the nonzero canonical project UUID to pin in tokens.

func WithRequiredAMR added in v0.6.0

func WithRequiredAMR(value string) Option

WithRequiredAMR requires the exact case-sensitive authentication method in every verified ID token and durable proof. Missing or malformed AMR fails closed. It does not request step-up, start sign-in, or infer strength from enrollment hints. Omit this option to accept providers without AMR.

func WithRevocationEndpoint

func WithRevocationEndpoint(value string) Option

WithRevocationEndpoint explicitly pins the revocation URL and takes precedence over discovery. Without this option, valid revocation discovery is required.

func WithStorageKey

func WithStorageKey(value []byte) Option

WithStorageKey snapshots exactly 32 raw key bytes when this option is created. Reusing the option gives each construction a separate copy.

func WithStore

func WithStore(value Store) Option

WithStore supplies a store constructed by NewPostgresStore or NewSQLiteStore. The caller still owns its database; constructing a client does not transfer pool ownership.

type Options

type Options struct {
	Store        Store
	ClientSecret string
	StorageKey   []byte
}

Options supplies protected inputs. New does not log, generate or persist keys. Store is mandatory: there is no in-memory fallback. No customizable token transport or clock is exposed in the consumer API.

type PostgresStore

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

PostgresStore owns no connection pool. The caller owns db and must apply Schema in an application-owned schema before construction. There are no authority/internal imports and no automatic migrations.

func NewPostgresStore

func NewPostgresStore(ctx context.Context, db *sql.DB) (*PostgresStore, error)

NewPostgresStore fails closed on absent or partial durable schema.

func NewPostgresStoreWithSchema added in v0.2.0

func NewPostgresStoreWithSchema(ctx context.Context, db *sql.DB, schemaName string) (*PostgresStore, error)

NewPostgresStoreWithSchema borrows the caller's existing pool and qualifies every session query with schemaName. The schema must already be migrated; construction never opens/closes a pool or changes connection search_path. Names are case-sensitive ASCII identifiers of at most 63 bytes.

type Proof

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

Proof is an immutable snapshot minted by Client.Verify or Handler. Copying a proof preserves its original deadline. Call Valid at each external session admission/validity boundary; do not turn Deadline into a rolling local TTL. The reference is opaque app-owned durable state, not the central OIDC sid.

func (Proof) Binding

func (p Proof) Binding() Binding

Binding returns the durable reference, exact generation and immutable deadline.

func (Proof) Deadline

func (p Proof) Deadline() time.Time

Deadline returns the fixed minimum of verified freshness and absolute lifetime.

func (Proof) Generation

func (p Proof) Generation() int64

Generation returns the exact durable generation checked for this proof.

func (Proof) Identity

func (p Proof) Identity() Identity

Identity returns a copy of verified claims. Metadata is display-only, never ACL.

func (Proof) SessionReference

func (p Proof) SessionReference() string

SessionReference returns the nonsecret durable browser-session reference.

type SQLiteStore added in v0.4.0

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

SQLiteStore borrows an application-owned SQLite database. It never opens, closes, migrates or changes PRAGMA settings on that database.

func NewSQLiteStore added in v0.4.0

func NewSQLiteStore(ctx context.Context, db *sql.DB) (*SQLiteStore, error)

NewSQLiteStore validates existing tables without migrating or owning db. SQLite 3.35 or newer is required for UPDATE RETURNING.

type Store added in v0.4.0

type Store interface {
	// contains filtered or unexported methods
}

Store is a durable session store constructed by this package. Its private method deliberately prevents custom implementations from bypassing the session transition protocol. The application owns the database lifetime.

type View

type View struct {
	CSRF  string
	Proof *Proof
	// CentralLogout is empty unless an explicit central sign-out URL was set.
	// Renderers must omit the link when it is empty. Legacy New retains its route.
	CentralLogout string
}

View is supplied to application-owned rendering after browser/session checks. A nil Proof conveys no authenticated identity, including outage recovery pages.

Directories

Path Synopsis
Package ingress defines the explicit trusted single-host HTTP boundary behind a HTTPS edge.
Package ingress defines the explicit trusted single-host HTTP boundary behind a HTTPS edge.

Jump to

Keyboard shortcuts

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