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 ¶
- Variables
- type AccessToken
- type AuthCode
- type Client
- type Consent
- type MemoryStore
- func (s *MemoryStore) AccessRevoked(_ context.Context, jti string) (bool, error)
- func (s *MemoryStore) Consent(_ context.Context, sub, client string) ([]string, error)
- func (s *MemoryStore) GetRefresh(_ context.Context, h string) (RefreshToken, error)
- func (s *MemoryStore) RevokeAccess(_ context.Context, jti string, exp time.Time) error
- func (s *MemoryStore) RevokeFamily(_ context.Context, family string) error
- func (s *MemoryStore) RevokeRefresh(_ context.Context, h string) error
- func (s *MemoryStore) SaveCode(_ context.Context, h string, c AuthCode) error
- func (s *MemoryStore) SaveConsent(_ context.Context, sub, client string, scopes []string) error
- func (s *MemoryStore) SaveRefresh(_ context.Context, h string, t RefreshToken) error
- func (s *MemoryStore) SaveSigningKey(_ context.Context, pem []byte) error
- func (s *MemoryStore) SigningKey(context.Context) ([]byte, error)
- func (s *MemoryStore) TakeCode(_ context.Context, h string) (AuthCode, error)
- func (s *MemoryStore) UseRefresh(_ context.Context, h string) (bool, error)
- type Options
- type Plugin
- type RefreshToken
- type SQLStore
- func (s *SQLStore) AccessRevoked(ctx context.Context, jti string) (bool, error)
- func (s *SQLStore) Consent(ctx context.Context, sub, client string) ([]string, error)
- func (s *SQLStore) GetRefresh(ctx context.Context, h string) (RefreshToken, error)
- func (s *SQLStore) RevokeAccess(ctx context.Context, jti string, exp time.Time) error
- func (s *SQLStore) RevokeFamily(ctx context.Context, family string) error
- func (s *SQLStore) RevokeRefresh(ctx context.Context, h string) error
- func (s *SQLStore) SaveCode(ctx context.Context, h string, c AuthCode) error
- func (s *SQLStore) SaveConsent(ctx context.Context, sub, client string, scopes []string) error
- func (s *SQLStore) SaveRefresh(ctx context.Context, h string, t RefreshToken) error
- func (s *SQLStore) SaveSigningKey(ctx context.Context, pem []byte) error
- func (s *SQLStore) SigningKey(ctx context.Context) ([]byte, error)
- func (s *SQLStore) TakeCode(ctx context.Context, h string) (AuthCode, error)
- func (s *SQLStore) UseRefresh(ctx context.Context, h string) (bool, error)
- type Store
Constants ¶
This section is empty.
Variables ¶
var ErrInvalidToken = errors.New("oidc: invalid access token")
ErrInvalidToken is returned for missing, malformed, expired or revoked tokens.
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 ¶ added in v1.4.0
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 ¶
AccessRevoked implements Store.
func (*MemoryStore) GetRefresh ¶
func (s *MemoryStore) GetRefresh(_ context.Context, h string) (RefreshToken, error)
GetRefresh implements Store.
func (*MemoryStore) RevokeAccess ¶
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) SaveConsent ¶
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) UseRefresh ¶
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 (*Plugin) Bearer ¶
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
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) }
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 ¶
NewSQLStore returns a store in db.
func (*SQLStore) AccessRevoked ¶
AccessRevoked implements Store.
func (*SQLStore) GetRefresh ¶
GetRefresh implements Store.
func (*SQLStore) RevokeAccess ¶
RevokeAccess implements Store.
func (*SQLStore) RevokeFamily ¶
RevokeFamily implements Store.
func (*SQLStore) RevokeRefresh ¶
RevokeRefresh implements Store.
func (*SQLStore) SaveConsent ¶
SaveConsent implements Store.
func (*SQLStore) SaveRefresh ¶
SaveRefresh implements Store.
func (*SQLStore) SaveSigningKey ¶
SaveSigningKey implements Store.
func (*SQLStore) SigningKey ¶
SigningKey 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.