oidc

package
v1.4.0 Latest Latest
Warning

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

Go to latest
Published: Nov 10, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package oidc makes a Tern app an OAuth 2.0 authorization server and OpenID Connect provider: other apps (or your own frontends, mobile apps and services) sign users in with it, and your APIs accept its tokens.

provider := oidc.New(oidc.Options{
	Issuer: "https://auth.example.com",
	Clients: []oidc.Client{{
		ID: "wiki", Secret: os.Getenv("WIKI_CLIENT_SECRET"), Name: "Team wiki",
		RedirectURIs: []string{"https://wiki.example.com/auth/example/callback"},
	}},
	Store: store, // oidc.NewSQLStore(db)
	Authenticate: func(ctx *kit.Ctx) (string, error) {
		return sessions.Get(ctx).GetString("user"), nil // "" when signed out
	},
	LoginURL: "/login",
	Claims: func(ctx context.Context, subject string, scopes []string) (map[string]any, error) {
		u, err := users.Get(ctx, subject)
		return map[string]any{"email": u.Email, "email_verified": true, "name": u.Name}, err
	},
})
app, err := ternApp(kit.WithPlugins(sessions.New(secret), provider))

It serves discovery (/.well-known/openid-configuration), keys (/oauth/jwks), and the authorize, token, userinfo, revocation and introspection endpoints under /oauth. Supported: the authorization code flow with PKCE (required for public clients), refresh tokens with rotation and reuse detection, and the client credentials grant. Access and ID tokens are RS256-signed JWTs.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidToken = errors.New("oidc: invalid access token")

ErrInvalidToken is returned for missing, malformed, expired or revoked tokens.

View Source
var ErrNotFound = errors.New("oidc: not found")

ErrNotFound is returned by a Store for missing or expired entries.

Functions

This section is empty.

Types

type AccessToken

type AccessToken struct {
	Subject  string
	ClientID string
	Scopes   []string
	Expires  time.Time
	ID       string
}

AccessToken is a verified access token.

func (*AccessToken) HasScope

func (t *AccessToken) HasScope(scope string) bool

HasScope reports whether the token was granted scope.

type AuthCode

type AuthCode struct {
	ClientID      string    `json:"clientId"`
	RedirectURI   string    `json:"redirectUri"`
	Subject       string    `json:"subject"`
	Scopes        []string  `json:"scopes"`
	Nonce         string    `json:"nonce,omitempty"`
	CodeChallenge string    `json:"codeChallenge,omitempty"`
	AuthTime      time.Time `json:"authTime"`
	Expires       time.Time `json:"expires"`
}

AuthCode is an issued authorization code.

type Client

type Client struct {
	ID string
	// Secret authenticates confidential clients (server-side apps). Leave
	// it empty and set Public for apps that can't keep a secret, such as
	// single-page and mobile apps; they must use PKCE.
	Secret string
	Public bool
	// Name is shown on the consent screen.
	Name string
	// RedirectURIs are the exact URLs codes may be sent to.
	RedirectURIs []string
	// Trusted clients, such as your own apps, skip the consent screen.
	Trusted bool
	// GrantTypes the client may use; the default is authorization_code and
	// refresh_token. Add client_credentials for service accounts.
	GrantTypes []string
	// Scopes the client may request; the default is every supported scope.
	Scopes []string
}

Client is an application allowed to use the provider.

type Consent struct {
	// Client is the app asking, such as "Team wiki".
	Client string `json:"client"`
	// Scopes describe what it may do, such as "See your email address".
	Scopes []string `json:"scopes"`
	// Action is where the form posts, with the Params as hidden inputs and
	// a "decision" of "allow" or "deny" from the buttons.
	Action string            `json:"action"`
	Params map[string]string `json:"params"`
}

Consent is what a consent page shows and posts: see ConsentRequest.

type MemoryStore

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

MemoryStore is a Store in memory, for development and tests.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty MemoryStore.

func (*MemoryStore) AccessRevoked

func (s *MemoryStore) AccessRevoked(_ context.Context, jti string) (bool, error)

AccessRevoked implements Store.

func (*MemoryStore) Consent

func (s *MemoryStore) Consent(_ context.Context, sub, client string) ([]string, error)

Consent implements Store.

func (*MemoryStore) GetRefresh

func (s *MemoryStore) GetRefresh(_ context.Context, h string) (RefreshToken, error)

GetRefresh implements Store.

func (*MemoryStore) RevokeAccess

func (s *MemoryStore) RevokeAccess(_ context.Context, jti string, exp time.Time) error

RevokeAccess implements Store.

func (*MemoryStore) RevokeFamily

func (s *MemoryStore) RevokeFamily(_ context.Context, family string) error

RevokeFamily implements Store.

func (*MemoryStore) RevokeRefresh

func (s *MemoryStore) RevokeRefresh(_ context.Context, h string) error

RevokeRefresh implements Store.

func (*MemoryStore) SaveCode

func (s *MemoryStore) SaveCode(_ context.Context, h string, c AuthCode) error

SaveCode implements Store.

func (*MemoryStore) SaveConsent

func (s *MemoryStore) SaveConsent(_ context.Context, sub, client string, scopes []string) error

SaveConsent implements Store.

func (*MemoryStore) SaveRefresh

func (s *MemoryStore) SaveRefresh(_ context.Context, h string, t RefreshToken) error

SaveRefresh implements Store.

func (*MemoryStore) SaveSigningKey

func (s *MemoryStore) SaveSigningKey(_ context.Context, pem []byte) error

SaveSigningKey implements Store.

func (*MemoryStore) SigningKey

func (s *MemoryStore) SigningKey(context.Context) ([]byte, error)

SigningKey implements Store.

func (*MemoryStore) TakeCode

func (s *MemoryStore) TakeCode(_ context.Context, h string) (AuthCode, error)

TakeCode implements Store.

func (*MemoryStore) UseRefresh

func (s *MemoryStore) UseRefresh(_ context.Context, h string) (bool, error)

UseRefresh implements Store.

type Options

type Options struct {
	// Issuer is the provider's public URL, such as
	// "https://auth.example.com", without a trailing slash. Tokens carry
	// it, and clients check it.
	Issuer  string
	Clients []Client
	// Authenticate returns the signed-in user's subject (a stable ID), or
	// "" when nobody is signed in.
	Authenticate func(ctx *kit.Ctx) (string, error)
	// LoginURL is where signed-out users go, with ?next=<the authorize
	// URL> to come back to.
	LoginURL string
	// ConsentURL is a page of the app that asks users to approve a client,
	// such as one with Tern UI's ConsentScreen, instead of the plugin's
	// own page. It gets the authorization request as its query; its load
	// calls ConsentRequest for what to show.
	ConsentURL string
	// Claims returns the user's claims for ID tokens and userinfo, such as
	// email, email_verified, name and picture. scopes lets you leave out
	// what wasn't asked for. It may be nil.
	Claims func(ctx context.Context, subject string, scopes []string) (map[string]any, error)
	// Store keeps codes, refresh tokens, consents and the signing key. The
	// default is a MemoryStore, which forgets everything on restart.
	Store Store
	// Scopes the provider supports besides openid, profile, email and
	// offline_access, such as "api:read".
	Scopes []string
	// Path is where the endpoints are; the default is "/oauth".
	Path string
	// Lifetimes; the defaults are an hour for access and ID tokens, 30
	// days for refresh tokens and a minute for codes.
	AccessTokenTTL, IDTokenTTL, RefreshTokenTTL, CodeTTL time.Duration
	// SigningKey is an RSA key in PEM. Without it, a key is created and
	// kept in the Store.
	SigningKey []byte
}

Options configure the provider.

type Plugin

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

Plugin is the provider.

func New

func New(opts Options) *Plugin

New returns the provider.

func (*Plugin) Bearer

func (p *Plugin) Bearer(ctx *kit.Ctx, scopes ...string) (*AccessToken, error)

Bearer authenticates an API request by its "Authorization: Bearer" access token, and checks it has every scope given. It answers 401 or 403 errors with the WWW-Authenticate header clients expect (RFC 6750):

func GET(ctx *kit.Ctx) error {
	token, err := provider.Bearer(ctx, "api:read")
	if err != nil {
		return err
	}
	return ctx.JSON(200, projectsOf(token.Subject))
}

func (*Plugin) ConsentRequest added in v1.4.0

func (p *Plugin) ConsentRequest(ctx *kit.Ctx) (Consent, error)

ConsentRequest reads the authorization request on the page at Options.ConsentURL and returns what to show. It checks the request as the authorize endpoint does, which checks it again when the form is posted; a signed-out user is sent to sign in first. It sets X-Frame-Options, so other sites can't frame the page to trick users into approving.

func Load(ctx *kit.Ctx) (oidc.Consent, error) { return provider.ConsentRequest(ctx) }

func (*Plugin) Name

func (*Plugin) Name() string

Name implements kit.Plugin.

func (*Plugin) Setup

func (p *Plugin) Setup(s *kit.Setup) error

Setup implements kit.Plugin.

func (*Plugin) Verify

func (p *Plugin) Verify(ctx context.Context, raw string) (*AccessToken, error)

Verify checks an access token issued by this provider.

type RefreshToken

type RefreshToken struct {
	ClientID string    `json:"clientId"`
	Subject  string    `json:"subject"`
	Scopes   []string  `json:"scopes"`
	Family   string    `json:"family"`
	AuthTime time.Time `json:"authTime"`
	Expires  time.Time `json:"expires"`
	Used     bool      `json:"used"`
}

RefreshToken is an issued refresh token. Tokens refreshed from one another share a Family; using an old one again revokes the family.

type SQLStore

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

SQLStore keeps the provider's state in SQLite or PostgreSQL. It creates its tables (prefixed tern_oidc_) on first use.

func NewSQLStore

func NewSQLStore(db *sql.DB) (*SQLStore, error)

NewSQLStore returns a store in db.

func (*SQLStore) AccessRevoked

func (s *SQLStore) AccessRevoked(ctx context.Context, jti string) (bool, error)

AccessRevoked implements Store.

func (*SQLStore) Consent

func (s *SQLStore) Consent(ctx context.Context, sub, client string) ([]string, error)

Consent implements Store.

func (*SQLStore) GetRefresh

func (s *SQLStore) GetRefresh(ctx context.Context, h string) (RefreshToken, error)

GetRefresh implements Store.

func (*SQLStore) RevokeAccess

func (s *SQLStore) RevokeAccess(ctx context.Context, jti string, exp time.Time) error

RevokeAccess implements Store.

func (*SQLStore) RevokeFamily

func (s *SQLStore) RevokeFamily(ctx context.Context, family string) error

RevokeFamily implements Store.

func (*SQLStore) RevokeRefresh

func (s *SQLStore) RevokeRefresh(ctx context.Context, h string) error

RevokeRefresh implements Store.

func (*SQLStore) SaveCode

func (s *SQLStore) SaveCode(ctx context.Context, h string, c AuthCode) error

SaveCode implements Store.

func (*SQLStore) SaveConsent

func (s *SQLStore) SaveConsent(ctx context.Context, sub, client string, scopes []string) error

SaveConsent implements Store.

func (*SQLStore) SaveRefresh

func (s *SQLStore) SaveRefresh(ctx context.Context, h string, t RefreshToken) error

SaveRefresh implements Store.

func (*SQLStore) SaveSigningKey

func (s *SQLStore) SaveSigningKey(ctx context.Context, pem []byte) error

SaveSigningKey implements Store.

func (*SQLStore) SigningKey

func (s *SQLStore) SigningKey(ctx context.Context) ([]byte, error)

SigningKey implements Store.

func (*SQLStore) TakeCode

func (s *SQLStore) TakeCode(ctx context.Context, h string) (AuthCode, error)

TakeCode implements Store.

func (*SQLStore) UseRefresh

func (s *SQLStore) UseRefresh(ctx context.Context, h string) (bool, error)

UseRefresh implements Store.

type Store

type Store interface {
	SaveCode(ctx context.Context, hash string, c AuthCode) error
	// TakeCode returns and deletes a code, so it can only be used once.
	TakeCode(ctx context.Context, hash string) (AuthCode, error)
	SaveRefresh(ctx context.Context, hash string, t RefreshToken) error
	GetRefresh(ctx context.Context, hash string) (RefreshToken, error)
	// UseRefresh marks a token used and reports whether it was unused.
	UseRefresh(ctx context.Context, hash string) (bool, error)
	RevokeRefresh(ctx context.Context, hash string) error
	RevokeFamily(ctx context.Context, family string) error
	// RevokeAccess records a revoked access token until it expires.
	RevokeAccess(ctx context.Context, jti string, expires time.Time) error
	AccessRevoked(ctx context.Context, jti string) (bool, error)
	SaveConsent(ctx context.Context, subject, clientID string, scopes []string) error
	Consent(ctx context.Context, subject, clientID string) ([]string, error)
	// SigningKey returns the stored signing key (PEM), or ErrNotFound.
	SigningKey(ctx context.Context) ([]byte, error)
	SaveSigningKey(ctx context.Context, pem []byte) error
}

Store keeps the provider's state. Codes and tokens are passed hashed, so a leaked store does not leak usable tokens. Use NewSQLStore in production; MemoryStore forgets everything on restart.

Jump to

Keyboard shortcuts

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