oidc

package
v0.53.0 Latest Latest
Warning

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

Go to latest
Published: Jan 19, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package oidc provides an OpenID Connect (OIDC) client and utilities for token validation and management.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidToken    = errors.New("invalid token")
	ErrTokenValidation = errors.New("token validation failed")
	ErrInvalidClaims   = errors.New("invalid claims format")
)
View Source
var NoTokenFoundError = fmt.Errorf("no token found")

Functions

func ContextFromTLSConfig added in v0.43.9

func ContextFromTLSConfig(cfg tls.ClientConfig) (context.Context, error)

TODO: Factor these functions better

func ContextWithTLSConfig added in v0.43.9

func ContextWithTLSConfig(tlsConfig *stdtls.Config) context.Context

TODO: Factor these functions better

func CustomClaimsFromContext added in v0.40.2

func CustomClaimsFromContext[T jwtvalidator.CustomClaims](ctx context.Context) T

func DeviceCodeUIConsoleQR

func DeviceCodeUIConsoleQR(deviceCode *oauth2.DeviceAuthResponse) error

func DeviceCodeUIConsoleText

func DeviceCodeUIConsoleText(deviceCode *oauth2.DeviceAuthResponse) error

func ExtractClaims

func ExtractClaims[T jwtvalidator.CustomClaims](claims interface{}) (jwtvalidator.RegisteredClaims, T, error)

func ExtractClaimsMap added in v0.46.3

func ExtractClaimsMap(accessToken string) (jwt.MapClaims, error)

func LoadTokenFromFile

func LoadTokenFromFile(filePath string) (*oauth2.Token, error)

func NewHTTPClientFromConfig

func NewHTTPClientFromConfig(config *ClientConfig) (*http.Client, error)

func NewHTTPClientFromConfigWithContext added in v0.43.9

func NewHTTPClientFromConfigWithContext(ctx context.Context, config *ClientConfig) (*http.Client, error)

func NewTokenSourceFromConfig added in v0.41.0

func NewTokenSourceFromConfig(config ClientConfig) oauth2.TokenSource

func NewWaitingTokenSource added in v0.41.0

func NewWaitingTokenSource(ctx context.Context, tokenSource oauth2.TokenSource, interval time.Duration, maxTime time.Duration) oauth2.TokenSource

func NewWaitingTokenSourceFromConfig added in v0.41.0

func NewWaitingTokenSourceFromConfig(ctx context.Context, config ClientConfig, interval time.Duration, maxTime time.Duration) oauth2.TokenSource

func RegisteredClaimsFromContext added in v0.46.3

func RegisteredClaimsFromContext(ctx context.Context) jwtvalidator.RegisteredClaims

func ResolveTokenFromFile added in v0.42.0

func ResolveTokenFromFile(tokenFile string) (*oauth2.Token, error)

func SaveTokenToFile

func SaveTokenToFile(accessToken *oauth2.Token, authFilePath string) error

func StringifyList added in v0.47.0

func StringifyList[T fmt.Stringer](list []T) []string

func WithCustomClaims added in v0.43.0

func WithCustomClaims[T jwtvalidator.CustomClaims](t T) func(e Endpoint)

func WithLabel added in v0.42.15

func WithLabel(key, value string) func(*ValidatorDebugger)

func WithLogger added in v0.42.15

func WithLogger(logger zerolog.Logger) func(*ValidatorDebugger)

Types

type ClaimKey added in v0.46.3

type ClaimKey struct {
	Key   string
	Value interface{}
}

ClaimKey is a claim key predicate

func (*ClaimKey) String added in v0.47.0

func (c *ClaimKey) String() string

func (*ClaimKey) Validate added in v0.46.3

func (c *ClaimKey) Validate(claims jwt.MapClaims) bool

Validate validates the input against the claim key If the value is a string, and the claim is a string it will check if the values are equal If the value is a string, and the claim is a list it will check if the value is in the list

type ClaimPredicate added in v0.46.3

type ClaimPredicate interface {
	Validate(input jwt.MapClaims) bool
	String() string
}

ClaimPredicate defines an interface for validating JWT claims.

func And added in v0.46.3

func And(children ...ClaimPredicate) ClaimPredicate

And combines the children with an AND

func Or added in v0.46.3

func Or(children ...ClaimPredicate) ClaimPredicate

Or combines the children with an OR

func ParseClaimPredicates added in v0.46.3

func ParseClaimPredicates(input interface{}) ClaimPredicate

ParseClaimPredicates parses the input into a claim predicate The input can be a map[string]interface{} or a []map[string]interface{}

type Client

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

Client represents an OIDC client.

func NewClient

func NewClient(endpoint Endpoint, opts ...ClientOpt) *Client

NewClient creates a new OIDC client with the provided endpoint and options.

func NewClientFromConfig

func NewClientFromConfig(config *ClientConfig) (*Client, error)

NewClientFromConfig creates a new OIDC client from the provided configuration.

func (*Client) AuthorizationCodeRedirectFlow added in v0.51.0

func (c *Client) AuthorizationCodeRedirectFlow(ctx context.Context, state string, scopes []string, redirectURI string, opts ...RequestOpt) (string, error)

AuthorizationCodeRedirectFlow generates the authorization URL for the Authorization Code Flow TODO: figure out a better name

func (*Client) AuthorizationCodeToken added in v0.51.0

func (c *Client) AuthorizationCodeToken(ctx context.Context, code string, redirect_uri string, opts ...RequestOpt) (*oauth2.Token, error)

func (*Client) ClientCredentialsToken

func (c *Client) ClientCredentialsToken(ctx context.Context, opts ...RequestOpt) (*oauth2.Token, error)

ClientCredentialsToken gets a token using the client_credentials grant It sends the client_id and client_secret to the token endpoint and gets a token in response

func (*Client) DeviceToken

func (c *Client) DeviceToken(ctx context.Context, scopes ...string) (*oauth2.Token, error)

func (*Client) Endpoint

func (c *Client) Endpoint() Endpoint

func (*Client) GothProvider added in v0.40.1

func (c *Client) GothProvider(callbackURL *url.URL, scopes ...string) (goth.Provider, error)

func (*Client) HTTPClient

func (c *Client) HTTPClient(ctx context.Context, t *oauth2.Token) (*http.Client, error)

func (*Client) IntrospectToken

func (c *Client) IntrospectToken(ctx context.Context, token string) (*IntrospectionResponse, error)

IntrospectToken introspects the token It sends the token to the introspection endpoint and gets the response

func (*Client) RefreshToken added in v0.51.0

func (c *Client) RefreshToken(ctx context.Context, refreshToken string, opts ...RequestOpt) (*oauth2.Token, error)

func (*Client) RefreshingClientCredentialsToken added in v0.51.0

func (c *Client) RefreshingClientCredentialsToken(ctx context.Context, opts ...RequestOpt) (oauth2.TokenSource, error)

func (*Client) TokenSource added in v0.40.0

func (c *Client) TokenSource(t *oauth2.Token) (oauth2.TokenSource, error)

func (*Client) ValidateToken

func (c *Client) ValidateToken(ctx context.Context, token string, audiences []string) (*jwtvalidator.ValidatedClaims, error)

ValidateToken VerifyToke verifies the token and returns the claims It fetches the verification keys from the OIDC server and uses them to verify the token

type ClientConfig

type ClientConfig struct {
	// Provider     EndpointConfig    `json:"provider"` // e.g. "github", "keycloak"
	EndpointConfig `mapstructure:",squash"`
	ClientID       string            `json:"client-id" mapstructure:"client-id"`
	ClientSecret   util.MaskedString `json:"client-secret,omitempty" mapstructure:"client-secret,omitempty"`

	Audience string `json:"audience,omitempty" mapstructure:"audience,omitempty"`

	// do these belong somewhere else?
	TokenFile string `json:"token-file,omitempty" mapstructure:"token-file,omitempty"`

	TLSConfig tls.ClientConfig `json:"tls-client-config,omitempty" mapstructure:"tls-client-config,omitempty"`
}

type ClientOpt

type ClientOpt func(c *Client)

func WithClientID

func WithClientID(clientID string) ClientOpt

func WithClientIDAndSecret

func WithClientIDAndSecret(clientID, clientSecret string) ClientOpt

func WithDeviceCodeUI

func WithDeviceCodeUI(ui DeviceCodeUI) ClientOpt

func WithKeyCacheTTL

func WithKeyCacheTTL(ttl time.Duration) ClientOpt

func WithValidatingSignatureAlgorithm

func WithValidatingSignatureAlgorithm(algorithm jwtvalidator.SignatureAlgorithm) ClientOpt

type Combinator added in v0.46.3

type Combinator func(...ClaimPredicate) ClaimPredicate

Combinator is a function type for combining claim predicates.

type DeviceCodeUI

type DeviceCodeUI func(deviceCode *oauth2.DeviceAuthResponse) error

type Endpoint

type Endpoint interface {
	URL() *url.URL
	DiscoveryEndpoint() (*url.URL, error)
	DiscoveredConfiguration() (*OpenIDConfiguration, error)
	OAuth2Endpoint() (oauth2.Endpoint, error)
}

Endpoint defines the interface for an OpenID Connect provider endpoint.

func NewEndpoint

func NewEndpoint(baseURL string, opts ...EndpointOption) (Endpoint, error)

func NewEndpointFromConfig

func NewEndpointFromConfig(config *EndpointConfig) (Endpoint, error)

func NewGitHubActionsEndpoint added in v0.53.0

func NewGitHubActionsEndpoint(baseURL string) (Endpoint, error)

NewGitHubActionsEndpoint creates a new GitHub Actions OIDC endpoint

func NewGitHubEndpoint

func NewGitHubEndpoint(baseURL string) (Endpoint, error)

func NewKeycloakRealmEndpoint

func NewKeycloakRealmEndpoint(baseURLStr, realm string, opts ...EndpointOption) (Endpoint, error)

type EndpointConfig

type EndpointConfig struct {
	Type          string `json:"type,omitempty" mapstructure:"type,omitempty"`
	URL           string `json:"url" mapstructure:"url"`
	KeycloakRealm string `json:"keycloak-realm,omitempty" mapstructure:"keycloak-realm,omitempty"`
}

type EndpointOption added in v0.43.0

type EndpointOption func(e Endpoint)

EndpointOption is a functional option for configuring an Endpoint.

type GitHubActionsEndpoint added in v0.53.0

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

GitHubActionsEndpoint represents the GitHub Actions OIDC endpoint

func (*GitHubActionsEndpoint) DiscoveredConfiguration added in v0.53.0

func (e *GitHubActionsEndpoint) DiscoveredConfiguration() (*OpenIDConfiguration, error)

DiscoveredConfiguration returns the OIDC configuration by fetching the discovery endpoint

func (*GitHubActionsEndpoint) DiscoveryEndpoint added in v0.53.0

func (e *GitHubActionsEndpoint) DiscoveryEndpoint() (*url.URL, error)

DiscoveryEndpoint returns the OIDC discovery endpoint URL

func (*GitHubActionsEndpoint) OAuth2Endpoint added in v0.53.0

func (e *GitHubActionsEndpoint) OAuth2Endpoint() (oauth2.Endpoint, error)

OAuth2Endpoint returns the OAuth2 endpoint configuration

func (*GitHubActionsEndpoint) URL added in v0.53.0

func (e *GitHubActionsEndpoint) URL() *url.URL

URL returns the base URL for the GitHub Actions OIDC endpoint

type GitHubEndpoint

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

GitHubEndpoint represents the GitHub OAuth endpoint.

func (*GitHubEndpoint) DiscoveredConfiguration

func (e *GitHubEndpoint) DiscoveredConfiguration() (*OpenIDConfiguration, error)

func (*GitHubEndpoint) DiscoveryEndpoint added in v0.40.0

func (e *GitHubEndpoint) DiscoveryEndpoint() (*url.URL, error)

func (*GitHubEndpoint) GothProvider added in v0.40.1

func (e *GitHubEndpoint) GothProvider(clientID, clientSecret string, callbackURL *url.URL, scopes ...string) (goth.Provider, error)

func (*GitHubEndpoint) OAuth2Endpoint

func (e *GitHubEndpoint) OAuth2Endpoint() (oauth2.Endpoint, error)

func (*GitHubEndpoint) URL

func (e *GitHubEndpoint) URL() *url.URL

type GothEndpoint added in v0.40.1

type GothEndpoint interface {
	GothProvider(clientID, clientSecret string, callbackURL *url.URL, scopes ...string) (goth.Provider, error)
}

GothEndpoint defines the interface for endpoints that support Goth provider creation.

type IntrospectionResponse

type IntrospectionResponse struct {
	ExpiresAt                           int      `json:"exp"`
	IssuedAt                            int      `json:"iat"`
	AuthTime                            int      `json:"auth_time"`
	ID                                  string   `json:"jti"`
	Issuer                              string   `json:"iss"`
	Audience                            string   `json:"aud"`
	Subject                             string   `json:"sub"`
	Type                                string   `json:"typ"`
	AuthorizedParty                     string   `json:"azp"`
	SessionID                           string   `json:"sid"`
	AuthenticationContextClassReference string   `json:"acr"`
	AllowedOrigins                      []string `json:"allowed-origins"`
	RealmAccess                         struct {
		Roles []string `json:"roles"`
	} `json:"realm_access"`
	ResourceAccess struct {
		Account struct {
			Roles []string `json:"roles"`
		} `json:"account"`
	} `json:"resource_access"`
	Scope             string   `json:"scope"`
	UserPrincipalName string   `json:"upn"`
	EmailVerified     bool     `json:"email_verified"`
	Name              string   `json:"name"`
	Groups            []string `json:"groups"`
	PreferredUsername string   `json:"preferred_username"`
	GivenName         string   `json:"given_name"`
	FamilyName        string   `json:"family_name"`
	Email             string   `json:"email"`
	ClientId          string   `json:"client_id"`
	Username          string   `json:"username"`
	TokenType         string   `json:"token_type"`
	Active            bool     `json:"active"`
	Website           string   `json:"website"`
	Organisation      []string `json:"org"`
	// contains filtered or unexported fields
}

func (*IntrospectionResponse) Validate

func (c *IntrospectionResponse) Validate(_ context.Context) error

type KeycloakEndpoint

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

KeycloakEndpoint represents a Keycloak OpenID Connect server endpoint.

func NewKeycloakEndpoint

func NewKeycloakEndpoint(baseURLStr string) (*KeycloakEndpoint, error)

func (*KeycloakEndpoint) RealmEndpoint

func (e *KeycloakEndpoint) RealmEndpoint(realm string, opts ...EndpointOption) (Endpoint, error)

type MultiValidator

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

MultiValidator attempts to validate tokens using multiple validators in sequence.

func NewMultiValidator

func NewMultiValidator(validators ...TokenValidator) *MultiValidator

func NewMultiValidatorFromConfig

func NewMultiValidatorFromConfig(configs []ValidatorConfig, opts ...jwtvalidator.Option) (*MultiValidator, error)

func (*MultiValidator) String added in v0.47.0

func (v *MultiValidator) String() string

func (*MultiValidator) ValidateToken

func (v *MultiValidator) ValidateToken(ctx context.Context, tokenString string) (interface{}, error)

type OpenIDConfiguration

type OpenIDConfiguration struct {
	Issuer                                                    string   `json:"issuer"`
	AuthorizationEndpoint                                     string   `json:"authorization_endpoint"`
	TokenEndpoint                                             string   `json:"token_endpoint"`
	UserinfoEndpoint                                          string   `json:"userinfo_endpoint"`
	JWKSURI                                                   string   `json:"jwks_uri"`
	RegistrationEndpoint                                      string   `json:"registration_endpoint"`
	ScopesSupported                                           []string `json:"scopes_supported"`
	ResponseTypesSupported                                    []string `json:"response_types_supported"`
	GrantTypesSupported                                       []string `json:"grant_types_supported"`
	SubjectTypesSupported                                     []string `json:"subject_types_supported"`
	IDTokenSigningAlgValuesSupported                          []string `json:"id_token_signing_alg_values_supported"`
	TokenEndpointAuthMethodsSupported                         []string `json:"token_endpoint_auth_methods_supported"`
	ClaimsSupported                                           []string `json:"claims_supported"`
	CodeChallengeMethodsSupported                             []string `json:"code_challenge_methods_supported"`
	IntrospectionEndpoint                                     string   `json:"introspection_endpoint"`
	EndSessionEndpoint                                        string   `json:"end_session_endpoint"`
	FrontchannelLogoutSessionSupported                        bool     `json:"frontchannel_logout_session_supported"`
	FrontchannelLogoutSupported                               bool     `json:"frontchannel_logout_supported"`
	CheckSessionIframe                                        string   `json:"check_session_iframe"`
	AcrValuesSupported                                        []string `json:"acr_values_supported"`
	IDTokenEncryptionAlgValuesSupported                       []string `json:"id_token_encryption_alg_values_supported"`
	IDTokenEncryptionEncValuesSupported                       []string `json:"id_token_encryption_enc_values_supported"`
	UserinfoSigningAlgValuesSupported                         []string `json:"userinfo_signing_alg_values_supported"`
	UserinfoEncryptionAlgValuesSupported                      []string `json:"userinfo_encryption_alg_values_supported"`
	UserinfoEncryptionEncValuesSupported                      []string `json:"userinfo_encryption_enc_values_supported"`
	RequestObjectSigningAlgValuesSupported                    []string `json:"request_object_signing_alg_values_supported"`
	RequestObjectEncryptionAlgValuesSupported                 []string `json:"request_object_encryption_alg_values_supported"`
	RequestObjectEncryptionEncValuesSupported                 []string `json:"request_object_encryption_enc_values_supported"`
	ResponseModesSupported                                    []string `json:"response_modes_supported"`
	TokenEndpointAuthSigningAlgValuesSupported                []string `json:"token_endpoint_auth_signing_alg_values_supported"`
	IntrospectionEndpointAuthMethodsSupported                 []string `json:"introspection_endpoint_auth_methods_supported"`
	IntrospectionEndpointAuthSigningAlgValuesSupported        []string `json:"introspection_endpoint_auth_signing_alg_values_supported"`
	AuthorizationSigningAlgValuesSupported                    []string `json:"authorization_signing_alg_values_supported"`
	AuthorizationEncryptionAlgValuesSupported                 []string `json:"authorization_encryption_alg_values_supported"`
	AuthorizationEncryptionEncValuesSupported                 []string `json:"authorization_encryption_enc_values_supported"`
	ClaimTypesSupported                                       []string `json:"claim_types_supported"`
	ClaimsParameterSupported                                  bool     `json:"claims_parameter_supported"`
	RequestParameterSupported                                 bool     `json:"request_parameter_supported"`
	RequestURIParameterSupported                              bool     `json:"request_uri_parameter_supported"`
	RequireRequestURIRegistration                             bool     `json:"require_request_uri_registration"`
	TLSClientCertificateBoundAccessTokens                     bool     `json:"tls_client_certificate_bound_access_tokens"`
	RevocationEndpoint                                        string   `json:"revocation_endpoint"`
	RevocationEndpointAuthMethodsSupported                    []string `json:"revocation_endpoint_auth_methods_supported"`
	RevocationEndpointAuthSigningAlgValuesSupported           []string `json:"revocation_endpoint_auth_signing_alg_values_supported"`
	BackchannelLogoutSupported                                bool     `json:"backchannel_logout_supported"`
	BackchannelLogoutSessionSupported                         bool     `json:"backchannel_logout_session_supported"`
	DeviceAuthorizationEndpoint                               string   `json:"device_authorization_endpoint"`
	BackchannelTokenDeliveryModesSupported                    []string `json:"backchannel_token_delivery_modes_supported"`
	BackchannelAuthenticationEndpoint                         string   `json:"backchannel_authentication_endpoint"`
	BackchannelAuthenticationRequestSigningAlgValuesSupported []string `json:"backchannel_authentication_request_signing_alg_values_supported"`
	RequirePushedAuthorizationRequests                        bool     `json:"require_pushed_authorization_requests"`
	PushedAuthorizationRequestEndpoint                        string   `json:"pushed_authorization_request_endpoint"`
	MTLSEndpointAliases                                       struct {
		TokenEndpoint                      string `json:"token_endpoint"`
		RevocationEndpoint                 string `json:"revocation_endpoint"`
		IntrospectionEndpoint              string `json:"introspection_endpoint"`
		DeviceAuthorizationEndpoint        string `json:"device_authorization_endpoint"`
		RegistrationEndpoint               string `json:"registration_endpoint"`
		UserinfoEndpoint                   string `json:"userinfo_endpoint"`
		PushedAuthorizationRequestEndpoint string `json:"pushed_authorization_request_endpoint"`
		BackchannelAuthenticationEndpoint  string `json:"backchannel_authentication_endpoint"`
	} `json:"mtls_endpoint_aliases"`
	AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported"`
}

type PredicateComposer added in v0.47.0

type PredicateComposer interface {
	And(predicates ...ClaimPredicate) ClaimPredicate
	Or(predicates ...ClaimPredicate) ClaimPredicate
}

PredicateComposer defines an interface for composing claim predicates.

type PredicateValidator added in v0.46.3

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

PredicateValidator wraps a TokenValidator and applies additional claim predicate validation.

func NewPredicateValidator added in v0.46.3

func NewPredicateValidator(validator TokenValidator, predicate ClaimPredicate) *PredicateValidator

func (*PredicateValidator) String added in v0.47.0

func (v *PredicateValidator) String() string

func (*PredicateValidator) ValidateToken added in v0.46.3

func (v *PredicateValidator) ValidateToken(ctx context.Context, tokenString string) (interface{}, error)

type RefreshingClientCredentialsTokenSource added in v0.51.0

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

func (*RefreshingClientCredentialsTokenSource) Token added in v0.51.0

type RequestOpt

type RequestOpt func(url.Values)

func WithAudience

func WithAudience(audience string) RequestOpt

type TokenValidator added in v0.42.13

type TokenValidator interface {
	ValidateToken(ctx context.Context, tokenString string) (interface{}, error)
	String() string
}

TokenValidator defines the interface for validating tokens.

func NewValidatorFromConfig

func NewValidatorFromConfig(config *ValidatorConfig, opts ...jwtvalidator.Option) (TokenValidator, error)

func NewValidatorsFromConfig

func NewValidatorsFromConfig(configs []ValidatorConfig, opts ...jwtvalidator.Option) ([]TokenValidator, error)

type TrustConfig

type TrustConfig struct {
	Verifiers []ValidatorConfig `json:"validators" mapstructure:"validators"`
}

type ValidatorConfig added in v0.40.0

type ValidatorConfig struct {
	EndpointConfig     `mapstructure:",squash"`
	Audiences          []string               `json:"audiences" mapstructure:"audiences"`
	Issuer             string                 `json:"issuer" mapstructure:"issuer"`
	CacheTTL           int                    `json:"cache_ttl_seconds" mapsstructure:"cache_ttl_seconds"`
	SignatureAlgorithm string                 `json:"signature_algorithm" mapstructure:"signature_algorithm"`
	AllowedClockSkew   int                    `json:"allowed_clock_skew_seconds" mapstructure:"allowed_clock_skew_seconds"`
	Debug              bool                   `json:"debug" mapstructure:"debug"`
	ClaimPredicate     map[string]interface{} `json:"claim_predicates" mapstructure:"claim_predicates"`
}

type ValidatorDebugOpts added in v0.42.15

type ValidatorDebugOpts func(*ValidatorDebugger)

ValidatorDebugOpts is a functional option for configuring a ValidatorDebugger.

type ValidatorDebugger added in v0.42.13

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

ValidatorDebugger wraps a TokenValidator with debug logging capabilities.

func NewValidatorDebugger added in v0.42.13

func NewValidatorDebugger(validator TokenValidator, opts ...ValidatorDebugOpts) *ValidatorDebugger

func (*ValidatorDebugger) String added in v0.47.0

func (v *ValidatorDebugger) String() string

func (*ValidatorDebugger) ValidateToken added in v0.42.13

func (v *ValidatorDebugger) ValidateToken(ctx context.Context, tokenString string) (interface{}, error)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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