oidc

package
v1.2.1 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 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
	// 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) 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