mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-19 14:12:52 +00:00
# Which Problems Are Solved The current back-channel logout notification still used a single projection handler for handling terminating sessions and notifying the necessary clients. This can lead to back pressure and delayed notifications. # How the Problems Are Solved The handler now only creates a job in the (river) worker queue to handle the notification. The worker will then search for all necessary clients to be informed and create a job for each. These jobs will then be picked up by the (same) worker(s) again, which will create, sign and send the logout token to the client. If one notification fails, this one job will be retired, but others can still be processed in parallel and ore not affected. Each successful job will still create an event on the session stating the successful notification of the client. The existing `OIDC.DefaultBackChannelLogoutLifetime` configuration has been deprecated in favor of making it part of the new `OIDC.BackChannelLogout` config. # Additional Changes None # Additional Context - feature check is still executed and will be removed seperately for easier review: https://github.com/zitadel/zitadel/issues/11277 - closes https://github.com/zitadel/zitadel/issues/9279 - requires backport to v4.x
274 lines
10 KiB
Go
274 lines
10 KiB
Go
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/zitadel/oidc/v3/pkg/oidc"
|
|
"github.com/zitadel/oidc/v3/pkg/op"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/instrumentation/metrics"
|
|
"github.com/zitadel/zitadel/internal/api/assets"
|
|
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
|
"github.com/zitadel/zitadel/internal/api/http/middleware"
|
|
"github.com/zitadel/zitadel/internal/api/ui/login"
|
|
"github.com/zitadel/zitadel/internal/auth/repository"
|
|
"github.com/zitadel/zitadel/internal/cache"
|
|
"github.com/zitadel/zitadel/internal/command"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain/federatedlogout"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/notification/handlers"
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
type Config struct {
|
|
CodeMethodS256 bool
|
|
AuthMethodPost bool
|
|
AuthMethodPrivateKeyJWT bool
|
|
GrantTypeRefreshToken bool
|
|
RequestObjectSupported bool
|
|
DefaultAccessTokenLifetime time.Duration
|
|
DefaultIdTokenLifetime time.Duration
|
|
DefaultRefreshTokenIdleExpiration time.Duration
|
|
DefaultRefreshTokenExpiration time.Duration
|
|
JWKSCacheControlMaxAge time.Duration
|
|
CustomEndpoints *EndpointConfig
|
|
DeviceAuth *DeviceAuthorizationConfig
|
|
DefaultLoginURLV2 string
|
|
DefaultLogoutURLV2 string
|
|
PublicKeyCacheMaxAge time.Duration
|
|
DefaultBackChannelLogoutLifetime time.Duration
|
|
BackChannelLogout handlers.BackChannelLogoutWorkerConfig
|
|
}
|
|
|
|
// BackChannelLogoutConfig returns the BackChannelLogoutWorkerConfig and takes the deprecated TokenLifetime into account.
|
|
func (c *Config) BackChannelLogoutConfig() *handlers.BackChannelLogoutWorkerConfig {
|
|
if c.DefaultBackChannelLogoutLifetime == 0 {
|
|
return &c.BackChannelLogout
|
|
}
|
|
c.BackChannelLogout.TokenLifetime = c.DefaultBackChannelLogoutLifetime
|
|
return &c.BackChannelLogout
|
|
}
|
|
|
|
type EndpointConfig struct {
|
|
Auth *Endpoint
|
|
Token *Endpoint
|
|
Introspection *Endpoint
|
|
Userinfo *Endpoint
|
|
Revocation *Endpoint
|
|
EndSession *Endpoint
|
|
Keys *Endpoint
|
|
DeviceAuth *Endpoint
|
|
}
|
|
|
|
type Endpoint struct {
|
|
Path string
|
|
URL string
|
|
}
|
|
|
|
type OPStorage struct {
|
|
repo repository.Repository
|
|
command *command.Commands
|
|
query *query.Queries
|
|
eventstore *eventstore.Eventstore
|
|
defaultLoginURL string
|
|
defaultLoginURLV2 string
|
|
defaultLogoutURLV2 string
|
|
defaultAccessTokenLifetime time.Duration
|
|
defaultIdTokenLifetime time.Duration
|
|
defaultRefreshTokenIdleExpiration time.Duration
|
|
defaultRefreshTokenExpiration time.Duration
|
|
encAlg crypto.EncryptionAlgorithm
|
|
assetAPIPrefix func(ctx context.Context) string
|
|
contextToIssuer func(context.Context) string
|
|
federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout]
|
|
}
|
|
|
|
// Provider is used to overload certain [op.Provider] methods
|
|
type Provider struct {
|
|
*op.Provider
|
|
accessTokenKeySet oidc.KeySet
|
|
idTokenHintKeySet oidc.KeySet
|
|
}
|
|
|
|
// IDTokenHintVerifier configures a Verifier and supported signing algorithms based on the Web Key feature in the context.
|
|
func (o *Provider) IDTokenHintVerifier(ctx context.Context) *op.IDTokenHintVerifier {
|
|
return op.NewIDTokenHintVerifier(op.IssuerFromContext(ctx), o.idTokenHintKeySet, op.WithSupportedIDTokenHintSigningAlgorithms(
|
|
supportedSigningAlgs()...,
|
|
))
|
|
}
|
|
|
|
// AccessTokenVerifier configures a Verifier and supported signing algorithms based on the Web Key feature in the context.
|
|
func (o *Provider) AccessTokenVerifier(ctx context.Context) *op.AccessTokenVerifier {
|
|
return op.NewAccessTokenVerifier(op.IssuerFromContext(ctx), o.accessTokenKeySet, op.WithSupportedAccessTokenSigningAlgorithms(
|
|
supportedSigningAlgs()...,
|
|
))
|
|
}
|
|
|
|
func NewServer(
|
|
ctx context.Context,
|
|
config Config,
|
|
defaultLogoutRedirectURI string,
|
|
externalSecure bool,
|
|
command *command.Commands,
|
|
query *query.Queries,
|
|
repo repository.Repository,
|
|
encryptionAlg crypto.EncryptionAlgorithm,
|
|
targetEncryptionAlgorithm crypto.EncryptionAlgorithm,
|
|
cryptoKey []byte,
|
|
es *eventstore.Eventstore,
|
|
userAgentCookie, instanceHandler func(http.Handler) http.Handler,
|
|
accessHandler *middleware.AccessInterceptor,
|
|
fallbackLogger *slog.Logger,
|
|
hashConfig crypto.HashConfig,
|
|
federatedLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout],
|
|
) (*Server, error) {
|
|
opConfig, err := createOPConfig(config, defaultLogoutRedirectURI, cryptoKey)
|
|
if err != nil {
|
|
return nil, zerrors.ThrowInternal(err, "OIDC-EGrqd", "cannot create op config: %w")
|
|
}
|
|
storage := newStorage(config, command, query, repo, encryptionAlg, es, ContextToIssuer, federatedLogoutCache)
|
|
keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query))
|
|
accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true))
|
|
idTokenHintKeySet := newOidcKeySet(keyCache)
|
|
|
|
var options []op.Option
|
|
if !externalSecure {
|
|
options = append(options, op.WithAllowInsecure())
|
|
}
|
|
provider, err := op.NewProvider(
|
|
opConfig,
|
|
storage,
|
|
IssuerFromContext,
|
|
options...,
|
|
)
|
|
if err != nil {
|
|
return nil, zerrors.ThrowInternal(err, "OIDC-DAtg3", "cannot create provider")
|
|
}
|
|
hasher, err := hashConfig.NewHasher()
|
|
if err != nil {
|
|
return nil, zerrors.ThrowInternal(err, "OIDC-Aij4e", "cannot create secret hasher")
|
|
}
|
|
server := &Server{
|
|
LegacyServer: op.NewLegacyServer(&Provider{
|
|
Provider: provider,
|
|
accessTokenKeySet: accessTokenKeySet,
|
|
idTokenHintKeySet: idTokenHintKeySet,
|
|
}, endpoints(config.CustomEndpoints)),
|
|
repo: repo,
|
|
query: query,
|
|
command: command,
|
|
accessTokenKeySet: accessTokenKeySet,
|
|
idTokenHintKeySet: idTokenHintKeySet,
|
|
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
|
defaultLoginURLV2: config.DefaultLoginURLV2,
|
|
defaultLogoutURLV2: config.DefaultLogoutURLV2,
|
|
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime,
|
|
defaultIdTokenLifetime: config.DefaultIdTokenLifetime,
|
|
jwksCacheControlMaxAge: config.JWKSCacheControlMaxAge,
|
|
fallbackLogger: fallbackLogger,
|
|
hasher: hasher,
|
|
encAlg: encryptionAlg,
|
|
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
|
|
opCrypto: op.NewAESCrypto(opConfig.CryptoKey),
|
|
assetAPIPrefix: assets.AssetAPI(),
|
|
}
|
|
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
|
|
server.Handler = op.RegisterLegacyServer(server,
|
|
server.authorizeCallbackHandler,
|
|
op.WithFallbackLogger(fallbackLogger),
|
|
op.WithHTTPMiddleware(
|
|
middleware.MetricsHandler(metricTypes),
|
|
middleware.TraceHandler(),
|
|
middleware.LogHandler("oidc"),
|
|
middleware.NoCacheInterceptor().Handler,
|
|
instanceHandler,
|
|
userAgentCookie,
|
|
http_utils.CopyHeadersToContext,
|
|
accessHandler.HandleWithPublicAuthPathPrefixes(publicAuthPathPrefixes(config.CustomEndpoints)),
|
|
middleware.ActivityHandler,
|
|
))
|
|
|
|
return server, nil
|
|
}
|
|
|
|
func ContextToIssuer(ctx context.Context) string {
|
|
return http_utils.DomainContext(ctx).Origin()
|
|
}
|
|
|
|
func IssuerFromContext(_ bool) (op.IssuerFromRequest, error) {
|
|
return func(r *http.Request) string {
|
|
return ContextToIssuer(r.Context())
|
|
}, nil
|
|
}
|
|
|
|
func publicAuthPathPrefixes(endpoints *EndpointConfig) []string {
|
|
authURL := op.DefaultEndpoints.Authorization.Relative()
|
|
keysURL := op.DefaultEndpoints.JwksURI.Relative()
|
|
if endpoints == nil {
|
|
return []string{oidc.DiscoveryEndpoint, authURL, keysURL}
|
|
}
|
|
if endpoints.Auth != nil && endpoints.Auth.Path != "" {
|
|
authURL = endpoints.Auth.Path
|
|
}
|
|
if endpoints.Keys != nil && endpoints.Keys.Path != "" {
|
|
keysURL = endpoints.Keys.Path
|
|
}
|
|
return []string{oidc.DiscoveryEndpoint, authURL, keysURL}
|
|
}
|
|
|
|
func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []byte) (*op.Config, error) {
|
|
opConfig := &op.Config{
|
|
DefaultLogoutRedirectURI: defaultLogoutRedirectURI,
|
|
CodeMethodS256: config.CodeMethodS256,
|
|
AuthMethodPost: config.AuthMethodPost,
|
|
AuthMethodPrivateKeyJWT: config.AuthMethodPrivateKeyJWT,
|
|
GrantTypeRefreshToken: config.GrantTypeRefreshToken,
|
|
RequestObjectSupported: config.RequestObjectSupported,
|
|
DeviceAuthorization: config.DeviceAuth.toOPConfig(),
|
|
}
|
|
if cryptoLength := len(cryptoKey); cryptoLength != 32 {
|
|
return nil, zerrors.ThrowInternalf(nil, "OIDC-D43gf", "crypto key must be 32 bytes, but is %d", cryptoLength)
|
|
}
|
|
copy(opConfig.CryptoKey[:], cryptoKey)
|
|
return opConfig, nil
|
|
}
|
|
|
|
func newStorage(
|
|
config Config,
|
|
command *command.Commands,
|
|
query *query.Queries,
|
|
repo repository.Repository,
|
|
encAlg crypto.EncryptionAlgorithm,
|
|
es *eventstore.Eventstore,
|
|
contextToIssuer func(context.Context) string,
|
|
federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout],
|
|
) *OPStorage {
|
|
return &OPStorage{
|
|
repo: repo,
|
|
command: command,
|
|
query: query,
|
|
eventstore: es,
|
|
defaultLoginURL: fmt.Sprintf("%s%s?%s=", login.HandlerPrefix, login.EndpointLogin, login.QueryAuthRequestID),
|
|
defaultLoginURLV2: config.DefaultLoginURLV2,
|
|
defaultLogoutURLV2: config.DefaultLogoutURLV2,
|
|
defaultAccessTokenLifetime: config.DefaultAccessTokenLifetime,
|
|
defaultIdTokenLifetime: config.DefaultIdTokenLifetime,
|
|
defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration,
|
|
defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration,
|
|
encAlg: encAlg,
|
|
assetAPIPrefix: assets.AssetAPI(),
|
|
contextToIssuer: contextToIssuer,
|
|
federateLogoutCache: federateLogoutCache,
|
|
}
|
|
}
|
|
|
|
func (o *OPStorage) Health(ctx context.Context) error {
|
|
return o.repo.Health(ctx)
|
|
}
|