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

browser

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 31 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.0; the module selects toolchain Go 1.27.1
  • 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.

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
}

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.

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.

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

There are no options to disable TLS, issuer 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. Email and profile/project metadata are display data, not account-linking or authorization evidence.

Proofs and host sessions

A Proof is an immutable snapshot with a fixed deadline. Never convert that deadline into a rolling local TTL.

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:

  • 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.

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
  • A new refresh token and verified ID token on successful refresh
  • Preserved identity/session pins across refresh, bounded freshness and a seven-day 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.

License

MIT. Dependencies retain their respective licenses.

Documentation

Overview

Package browser implements AuthHub relying-party browser sessions using the reviewed OIDC libraries and durable PostgreSQL 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 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 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 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 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) 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) 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. AbsoluteUntil is a pin. 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
}

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 {
	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"`
	Profile       map[string]string `json:"profile,omitempty"`
	Project       map[string]string `json:"project,omitempty"`
}

Identity contains independently verified OIDC identity claims.

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 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 WithFreshness

func WithFreshness(value time.Duration) Option

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

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 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 *PostgresStore) Option

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

type Options

type Options struct {
	Store        *PostgresStore
	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.

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 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