zitadel-oidc/pkg/op/op.go
2024-04-02 14:23:12 +03:00

657 lines
18 KiB
Go

package op
import (
"context"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
jose "github.com/go-jose/go-jose/v3"
"github.com/rs/cors"
"github.com/zitadel/schema"
"go.opentelemetry.io/otel"
"golang.org/x/text/language"
httphelper "github.com/zitadel/oidc/v4/pkg/http"
"github.com/zitadel/oidc/v4/pkg/oidc"
)
const (
healthEndpoint = "/healthz"
readinessEndpoint = "/ready"
authCallbackPathSuffix = "/callback"
defaultAuthorizationEndpoint = "authorize"
defaultTokenEndpoint = "oauth/token"
defaultIntrospectEndpoint = "oauth/introspect"
defaultUserinfoEndpoint = "userinfo"
defaultRevocationEndpoint = "revoke"
defaultEndSessionEndpoint = "end_session"
defaultKeysEndpoint = "keys"
defaultDeviceAuthzEndpoint = "/device_authorization"
)
var (
DefaultEndpoints = &Endpoints{
Authorization: NewEndpoint(defaultAuthorizationEndpoint),
Token: NewEndpoint(defaultTokenEndpoint),
Introspection: NewEndpoint(defaultIntrospectEndpoint),
Userinfo: NewEndpoint(defaultUserinfoEndpoint),
Revocation: NewEndpoint(defaultRevocationEndpoint),
EndSession: NewEndpoint(defaultEndSessionEndpoint),
JwksURI: NewEndpoint(defaultKeysEndpoint),
DeviceAuthorization: NewEndpoint(defaultDeviceAuthzEndpoint),
}
DefaultSupportedClaims = []string{
"sub",
"aud",
"exp",
"iat",
"iss",
"auth_time",
"nonce",
"acr",
"amr",
"c_hash",
"at_hash",
"act",
"scopes",
"client_id",
"azp",
"preferred_username",
"name",
"family_name",
"given_name",
"locale",
"email",
"email_verified",
"phone_number",
"phone_number_verified",
}
defaultCORSOptions = cors.Options{
AllowCredentials: true,
AllowedHeaders: []string{
"Origin",
"Accept",
"Accept-Language",
"Authorization",
"Content-Type",
"X-Requested-With",
},
AllowedMethods: []string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
},
ExposedHeaders: []string{
"Location",
"Content-Length",
},
AllowOriginFunc: func(_ string) bool {
return true
},
}
)
var tracer = otel.Tracer("github.com/zitadel/oidc/pkg/op")
type OpenIDProvider interface {
http.Handler
Configuration
Storage() Storage
Decoder() httphelper.Decoder
Encoder() httphelper.Encoder
IDTokenHintVerifier(context.Context) *IDTokenHintVerifier
AccessTokenVerifier(context.Context) *AccessTokenVerifier
Crypto() Crypto
DefaultLogoutRedirectURI() string
Probes() []ProbesFn
Logger() *slog.Logger
// Deprecated: Provider now implements http.Handler directly.
HttpHandler() http.Handler
}
type HttpInterceptor func(http.Handler) http.Handler
type corsOptioner interface {
CORSOptions() *cors.Options
}
func CreateRouter(o OpenIDProvider, interceptors ...HttpInterceptor) chi.Router {
router := chi.NewRouter()
if co, ok := o.(corsOptioner); ok {
if opts := co.CORSOptions(); opts != nil {
router.Use(cors.New(*opts).Handler)
}
} else {
router.Use(cors.New(defaultCORSOptions).Handler)
}
router.Use(intercept(o.IssuerFromRequest, interceptors...))
router.HandleFunc(healthEndpoint, healthHandler)
router.HandleFunc(readinessEndpoint, readyHandler(o.Probes()))
router.HandleFunc(oidc.DiscoveryEndpoint, discoveryHandler(o, o.Storage()))
router.HandleFunc(o.AuthorizationEndpoint().Relative(), authorizeHandler(o))
router.HandleFunc(authCallbackPath(o), authorizeCallbackHandler(o))
router.HandleFunc(o.TokenEndpoint().Relative(), tokenHandler(o))
router.HandleFunc(o.IntrospectionEndpoint().Relative(), introspectionHandler(o))
router.HandleFunc(o.UserinfoEndpoint().Relative(), userinfoHandler(o))
router.HandleFunc(o.RevocationEndpoint().Relative(), revocationHandler(o))
router.HandleFunc(o.EndSessionEndpoint().Relative(), endSessionHandler(o))
router.HandleFunc(o.KeysEndpoint().Relative(), keysHandler(o.Storage()))
router.HandleFunc(o.DeviceAuthorizationEndpoint().Relative(), DeviceAuthorizationHandler(o))
return router
}
// AuthCallbackURL builds the url for the redirect (with the requestID) after a successful login
func AuthCallbackURL(o OpenIDProvider) func(context.Context, string) string {
return func(ctx context.Context, requestID string) string {
return o.AuthorizationEndpoint().Absolute(IssuerFromContext(ctx)) + authCallbackPathSuffix + "?id=" + requestID
}
}
func authCallbackPath(o OpenIDProvider) string {
return o.AuthorizationEndpoint().Relative() + authCallbackPathSuffix
}
type Config struct {
CryptoKey [32]byte
DefaultLogoutRedirectURI string
CodeMethodS256 bool
AuthMethodPost bool
AuthMethodPrivateKeyJWT bool
GrantTypeRefreshToken bool
RequestObjectSupported bool
SupportedUILocales []language.Tag
SupportedClaims []string
DeviceAuthorization DeviceAuthorizationConfig
}
// Endpoints defines endpoint routes.
type Endpoints struct {
Authorization *Endpoint
Token *Endpoint
Introspection *Endpoint
Userinfo *Endpoint
Revocation *Endpoint
EndSession *Endpoint
CheckSessionIframe *Endpoint
JwksURI *Endpoint
DeviceAuthorization *Endpoint
}
// NewOpenIDProvider creates a provider. The provider provides (with HttpHandler())
// a http.Router that handles a suite of endpoints (some paths can be overridden):
//
// /healthz
// /ready
// /.well-known/openid-configuration
// /oauth/token
// /oauth/introspect
// /callback
// /authorize
// /userinfo
// /revoke
// /end_session
// /keys
// /device_authorization
//
// This does not include login. Login is handled with a redirect that includes the
// request ID. The redirect for logins is specified per-client by Client.LoginURL().
// Successful logins should mark the request as authorized and redirect back to to
// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back
// to the AuthCallbackURL, the request id should be passed as the "id" parameter.
//
// Deprecated: use [NewProvider] with an issuer function direct.
func NewOpenIDProvider(issuer string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return NewProvider(config, storage, StaticIssuer(issuer), opOpts...)
}
// NewForwardedOpenIDProvider tries to establishes the issuer from the request Host.
//
// Deprecated: use [NewProvider] with an issuer function direct.
func NewDynamicOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return NewProvider(config, storage, IssuerFromHost(path), opOpts...)
}
// NewForwardedOpenIDProvider tries to establish the Issuer from a Forwarded request header, if it is set.
// See [IssuerFromForwardedOrHost] for details.
//
// Deprecated: use [NewProvider] with an issuer function direct.
func NewForwardedOpenIDProvider(path string, config *Config, storage Storage, opOpts ...Option) (*Provider, error) {
return NewProvider(config, storage, IssuerFromForwardedOrHost(path), opOpts...)
}
// NewProvider creates a provider with a router on it's embedded http.Handler.
// Issuer is a function that must return the issuer on every request.
// Typically [StaticIssuer], [IssuerFromHost] or [IssuerFromForwardedOrHost] can be used.
//
// The router handles a suite of endpoints (some paths can be overridden):
//
// /healthz
// /ready
// /.well-known/openid-configuration
// /oauth/token
// /oauth/introspect
// /callback
// /authorize
// /userinfo
// /revoke
// /end_session
// /keys
// /device_authorization
//
// This does not include login. Login is handled with a redirect that includes the
// request ID. The redirect for logins is specified per-client by Client.LoginURL().
// Successful logins should mark the request as authorized and redirect back to to
// op.AuthCallbackURL(provider) which is probably /callback. On the redirect back
// to the AuthCallbackURL, the request id should be passed as the "id" parameter.
func NewProvider(config *Config, storage Storage, issuer func(insecure bool) (IssuerFromRequest, error), opOpts ...Option) (_ *Provider, err error) {
keySet := &OpenIDKeySet{storage}
o := &Provider{
config: config,
storage: storage,
accessTokenKeySet: keySet,
idTokenHinKeySet: keySet,
endpoints: DefaultEndpoints,
timer: make(<-chan time.Time),
corsOpts: &defaultCORSOptions,
logger: slog.Default(),
}
for _, optFunc := range opOpts {
if err := optFunc(o); err != nil {
return nil, err
}
}
o.issuer, err = issuer(o.insecure)
if err != nil {
return nil, err
}
o.Handler = CreateRouter(o, o.interceptors...)
o.decoder = schema.NewDecoder()
o.decoder.IgnoreUnknownKeys(true)
o.encoder = oidc.NewEncoder()
o.crypto = NewAESCrypto(config.CryptoKey)
return o, nil
}
type Provider struct {
http.Handler
config *Config
issuer IssuerFromRequest
insecure bool
endpoints *Endpoints
storage Storage
accessTokenKeySet oidc.KeySet
idTokenHinKeySet oidc.KeySet
crypto Crypto
decoder *schema.Decoder
encoder *schema.Encoder
interceptors []HttpInterceptor
timer <-chan time.Time
accessTokenVerifierOpts []AccessTokenVerifierOpt
idTokenHintVerifierOpts []IDTokenHintVerifierOpt
corsOpts *cors.Options
logger *slog.Logger
}
func (o *Provider) IssuerFromRequest(r *http.Request) string {
return o.issuer(r)
}
func (o *Provider) Insecure() bool {
return o.insecure
}
func (o *Provider) AuthorizationEndpoint() *Endpoint {
return o.endpoints.Authorization
}
func (o *Provider) TokenEndpoint() *Endpoint {
return o.endpoints.Token
}
func (o *Provider) IntrospectionEndpoint() *Endpoint {
return o.endpoints.Introspection
}
func (o *Provider) UserinfoEndpoint() *Endpoint {
return o.endpoints.Userinfo
}
func (o *Provider) RevocationEndpoint() *Endpoint {
return o.endpoints.Revocation
}
func (o *Provider) EndSessionEndpoint() *Endpoint {
return o.endpoints.EndSession
}
func (o *Provider) DeviceAuthorizationEndpoint() *Endpoint {
return o.endpoints.DeviceAuthorization
}
func (o *Provider) KeysEndpoint() *Endpoint {
return o.endpoints.JwksURI
}
func (o *Provider) AuthMethodPostSupported() bool {
return o.config.AuthMethodPost
}
func (o *Provider) CodeMethodS256Supported() bool {
return o.config.CodeMethodS256
}
func (o *Provider) AuthMethodPrivateKeyJWTSupported() bool {
return o.config.AuthMethodPrivateKeyJWT
}
func (o *Provider) TokenEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *Provider) GrantTypeRefreshTokenSupported() bool {
return o.config.GrantTypeRefreshToken
}
func (o *Provider) GrantTypeTokenExchangeSupported() bool {
_, ok := o.storage.(TokenExchangeStorage)
return ok
}
func (o *Provider) GrantTypeJWTAuthorizationSupported() bool {
return true
}
func (o *Provider) GrantTypeDeviceCodeSupported() bool {
_, ok := o.storage.(DeviceAuthorizationStorage)
return ok
}
func (o *Provider) IntrospectionAuthMethodPrivateKeyJWTSupported() bool {
return true
}
func (o *Provider) IntrospectionEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *Provider) GrantTypeClientCredentialsSupported() bool {
_, ok := o.storage.(ClientCredentialsStorage)
return ok
}
func (o *Provider) RevocationAuthMethodPrivateKeyJWTSupported() bool {
return true
}
func (o *Provider) RevocationEndpointSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *Provider) RequestObjectSupported() bool {
return o.config.RequestObjectSupported
}
func (o *Provider) RequestObjectSigningAlgorithmsSupported() []string {
return []string{"RS256"}
}
func (o *Provider) SupportedUILocales() []language.Tag {
return o.config.SupportedUILocales
}
func (o *Provider) DeviceAuthorization() DeviceAuthorizationConfig {
return o.config.DeviceAuthorization
}
func (o *Provider) Storage() Storage {
return o.storage
}
func (o *Provider) Decoder() httphelper.Decoder {
return o.decoder
}
func (o *Provider) Encoder() httphelper.Encoder {
return o.encoder
}
func (o *Provider) IDTokenHintVerifier(ctx context.Context) *IDTokenHintVerifier {
return NewIDTokenHintVerifier(IssuerFromContext(ctx), o.idTokenHinKeySet, o.idTokenHintVerifierOpts...)
}
func (o *Provider) JWTProfileVerifier(ctx context.Context) *JWTProfileVerifier {
return NewJWTProfileVerifier(o.Storage(), IssuerFromContext(ctx), 1*time.Hour, time.Second)
}
func (o *Provider) AccessTokenVerifier(ctx context.Context) *AccessTokenVerifier {
return NewAccessTokenVerifier(IssuerFromContext(ctx), o.accessTokenKeySet, o.accessTokenVerifierOpts...)
}
func (o *Provider) Crypto() Crypto {
return o.crypto
}
func (o *Provider) DefaultLogoutRedirectURI() string {
return o.config.DefaultLogoutRedirectURI
}
func (o *Provider) Probes() []ProbesFn {
return []ProbesFn{
ReadyStorage(o.Storage()),
}
}
func (o *Provider) CORSOptions() *cors.Options {
return o.corsOpts
}
func (o *Provider) Logger() *slog.Logger {
return o.logger
}
// Deprecated: Provider now implements http.Handler directly.
func (o *Provider) HttpHandler() http.Handler {
return o
}
type OpenIDKeySet struct {
Storage
}
// VerifySignature implements the oidc.KeySet interface
// providing an implementation for the keys stored in the OP Storage interface
func (o *OpenIDKeySet) VerifySignature(ctx context.Context, jws *jose.JSONWebSignature) ([]byte, error) {
keySet, err := o.Storage.KeySet(ctx)
if err != nil {
return nil, fmt.Errorf("error fetching keys: %w", err)
}
keyID, alg := oidc.GetKeyIDAndAlg(jws)
key, err := oidc.FindMatchingKey(keyID, oidc.KeyUseSignature, alg, jsonWebKeySet(keySet).Keys...)
if err != nil {
return nil, fmt.Errorf("invalid signature: %w", err)
}
return jws.Verify(&key)
}
type Option func(o *Provider) error
// WithAllowInsecure allows the use of http (instead of https) for issuers
// this is not recommended for production use and violates the OIDC specification
func WithAllowInsecure() Option {
return func(o *Provider) error {
o.insecure = true
return nil
}
}
func WithCustomAuthEndpoint(endpoint *Endpoint) Option {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
o.endpoints.Authorization = endpoint
return nil
}
}
func WithCustomTokenEndpoint(endpoint *Endpoint) Option {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
o.endpoints.Token = endpoint
return nil
}
}
func WithCustomIntrospectionEndpoint(endpoint *Endpoint) Option {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
o.endpoints.Introspection = endpoint
return nil
}
}
func WithCustomUserinfoEndpoint(endpoint *Endpoint) Option {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
o.endpoints.Userinfo = endpoint
return nil
}
}
func WithCustomRevocationEndpoint(endpoint *Endpoint) Option {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
o.endpoints.Revocation = endpoint
return nil
}
}
func WithCustomEndSessionEndpoint(endpoint *Endpoint) Option {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
o.endpoints.EndSession = endpoint
return nil
}
}
func WithCustomKeysEndpoint(endpoint *Endpoint) Option {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
o.endpoints.JwksURI = endpoint
return nil
}
}
func WithCustomDeviceAuthorizationEndpoint(endpoint *Endpoint) Option {
return func(o *Provider) error {
if err := endpoint.Validate(); err != nil {
return err
}
o.endpoints.DeviceAuthorization = endpoint
return nil
}
}
// WithCustomEndpoints sets multiple endpoints at once.
// Non of the endpoints may be nil, or an error will
// be returned when the Option used by the Provider.
func WithCustomEndpoints(auth, token, userInfo, revocation, endSession, keys *Endpoint) Option {
return func(o *Provider) error {
for _, e := range []*Endpoint{auth, token, userInfo, revocation, endSession, keys} {
if err := e.Validate(); err != nil {
return err
}
}
o.endpoints.Authorization = auth
o.endpoints.Token = token
o.endpoints.Userinfo = userInfo
o.endpoints.Revocation = revocation
o.endpoints.EndSession = endSession
o.endpoints.JwksURI = keys
return nil
}
}
func WithHttpInterceptors(interceptors ...HttpInterceptor) Option {
return func(o *Provider) error {
o.interceptors = append(o.interceptors, interceptors...)
return nil
}
}
// WithAccessTokenKeySet allows passing a KeySet with public keys for Access Token verification.
// The default KeySet uses the [Storage] interface
func WithAccessTokenKeySet(keySet oidc.KeySet) Option {
return func(o *Provider) error {
o.accessTokenKeySet = keySet
return nil
}
}
func WithAccessTokenVerifierOpts(opts ...AccessTokenVerifierOpt) Option {
return func(o *Provider) error {
o.accessTokenVerifierOpts = opts
return nil
}
}
// WithIDTokenHintKeySet allows passing a KeySet with public keys for ID Token Hint verification.
// The default KeySet uses the [Storage] interface.
func WithIDTokenHintKeySet(keySet oidc.KeySet) Option {
return func(o *Provider) error {
o.idTokenHinKeySet = keySet
return nil
}
}
func WithIDTokenHintVerifierOpts(opts ...IDTokenHintVerifierOpt) Option {
return func(o *Provider) error {
o.idTokenHintVerifierOpts = opts
return nil
}
}
func WithCORSOptions(opts *cors.Options) Option {
return func(o *Provider) error {
o.corsOpts = opts
return nil
}
}
// WithLogger lets a logger other than slog.Default().
func WithLogger(logger *slog.Logger) Option {
return func(o *Provider) error {
o.logger = logger
return nil
}
}
func intercept(i IssuerFromRequest, interceptors ...HttpInterceptor) func(handler http.Handler) http.Handler {
issuerInterceptor := NewIssuerInterceptor(i)
return func(handler http.Handler) http.Handler {
for i := len(interceptors) - 1; i >= 0; i-- {
handler = interceptors[i](handler)
}
return issuerInterceptor.Handler(handler)
}
}