fix(oidc): use authenticated encryption for opaque tokens (#12017)

# Which Problems Are Solved

Opaque tokens now use authenticated encryption.

# How the Problems Are Solved

- Upgrade zitadel/oidc to v3.47
- Copy crypto implementation for refresh and session tokens (internal to
zitadel)
- Added config that allows validating old tokens for gradual roll-out

# Additional Changes

- Set NX cache for `integration-test-build` to `false`, working on a
seperate fix.

# Additional Context

- closes #11315

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: muhlemmer <[email protected]>
This commit is contained in:
Tim Möhlmann
2026-04-13 10:59:50 +00:00
committed by GitHub
co-authored by copilot-swe-agent[bot] <198982749+[email protected]> muhlemmer
parent a28ee63b02
commit 01fe34a526
46 changed files with 406 additions and 201 deletions
+4 -13
View File
@@ -2,7 +2,6 @@ package authz
import (
"context"
"encoding/base64"
"fmt"
"strings"
@@ -16,14 +15,10 @@ const (
SessionTokenFormat = SessionTokenPrefix + "%s:%s"
)
func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
func SessionTokenVerifier(algorithm crypto.AuthAlgorithm) func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
return func(ctx context.Context, sessionToken, sessionID, tokenID string) (err error) {
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
if err != nil {
return zerrors.ThrowInvalidArgument(err, "COMMAND-hi6Ph", "Errors.Session.Token.Invalid")
}
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
token, err := algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
token, err := algorithm.DecryptToken(sessionToken)
spanPasswordComparison.EndWithError(err)
if err != nil || token != fmt.Sprintf(SessionTokenFormat, sessionID, tokenID) {
return zerrors.ThrowPermissionDenied(err, "COMMAND-sGr42", "Errors.Session.Token.Invalid")
@@ -32,14 +27,10 @@ func SessionTokenVerifier(algorithm crypto.EncryptionAlgorithm) func(ctx context
}
}
func SessionTokenDecryptor(algorithm crypto.EncryptionAlgorithm) func(ctx context.Context, sessionToken string) (sessionID, tokenID string, err error) {
func SessionTokenDecryptor(algorithm crypto.AuthAlgorithm) func(ctx context.Context, sessionToken string) (sessionID, tokenID string, err error) {
return func(ctx context.Context, sessionToken string) (sessionID, tokenID string, err error) {
decodedToken, err := base64.RawURLEncoding.DecodeString(sessionToken)
if err != nil {
return "", "", zerrors.ThrowInvalidArgument(err, "AUTHZ-hi6Ph", "Errors.Session.Token.Invalid")
}
_, spanPasswordComparison := tracing.NewNamedSpan(ctx, "crypto.CompareHash")
token, err := algorithm.DecryptString(decodedToken, algorithm.EncryptionKeyID())
token, err := algorithm.DecryptToken(sessionToken)
spanPasswordComparison.EndWithError(err)
if err != nil {
return "", "", zerrors.ThrowPermissionDenied(err, "AUTHZ-sGr42", "Errors.Session.Token.Invalid")
+3 -12
View File
@@ -2,7 +2,6 @@ package oidc
import (
"context"
"encoding/base64"
"connectrpc.com/connect"
"github.com/zitadel/logging"
@@ -46,13 +45,13 @@ func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *connect
if err != nil {
return nil, err
}
encrypted, err := s.encryption.Encrypt([]byte(deviceRequest.DeviceCode))
encrypted, err := s.authAlg.EncryptToken(deviceRequest.DeviceCode)
if err != nil {
return nil, err
}
return connect.NewResponse(&oidc_pb.GetDeviceAuthorizationRequestResponse{
DeviceAuthorizationRequest: &oidc_pb.DeviceAuthorizationRequest{
Id: base64.RawURLEncoding.EncodeToString(encrypted),
Id: encrypted,
ClientId: deviceRequest.ClientID,
Scope: deviceRequest.Scopes,
AppName: deviceRequest.AppName,
@@ -62,7 +61,7 @@ func (s *Server) GetDeviceAuthorizationRequest(ctx context.Context, req *connect
}
func (s *Server) AuthorizeOrDenyDeviceAuthorization(ctx context.Context, req *connect.Request[oidc_pb.AuthorizeOrDenyDeviceAuthorizationRequest]) (*connect.Response[oidc_pb.AuthorizeOrDenyDeviceAuthorizationResponse], error) {
deviceCode, err := s.deviceCodeFromID(req.Msg.GetDeviceAuthorizationId())
deviceCode, err := s.authAlg.DecryptToken(req.Msg.GetDeviceAuthorizationId())
if err != nil {
return nil, err
}
@@ -258,11 +257,3 @@ func errorReasonToOIDC(reason oidc_pb.ErrorReason) string {
return "server_error"
}
}
func (s *Server) deviceCodeFromID(deviceAuthID string) (string, error) {
decoded, err := base64.RawURLEncoding.DecodeString(deviceAuthID)
if err != nil {
return "", err
}
return s.encryption.DecryptString(decoded, s.encryption.EncryptionKeyID())
}
+3 -3
View File
@@ -24,7 +24,7 @@ type Server struct {
op *oidc.Server
externalSecure bool
encryption crypto.EncryptionAlgorithm
authAlg crypto.AuthAlgorithm
}
type Config struct{}
@@ -34,14 +34,14 @@ func CreateServer(
query *query.Queries,
op *oidc.Server,
externalSecure bool,
encryption crypto.EncryptionAlgorithm,
authAlg crypto.AuthAlgorithm,
) *Server {
return &Server{
command: command,
query: query,
op: op,
externalSecure: externalSecure,
encryption: encryption,
authAlg: authAlg,
}
}
+1 -11
View File
@@ -2,7 +2,6 @@ package oidc
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/url"
@@ -204,15 +203,6 @@ func (o *OPStorage) AuthRequestByCode(ctx context.Context, code string) (_ op.Au
panic(o.panicErr("AuthRequestByCode"))
}
// decryptGrant decrypts a code or refresh_token
func (o *OPStorage) decryptGrant(grant string) (string, error) {
decodedGrant, err := base64.RawURLEncoding.DecodeString(grant)
if err != nil {
return "", err
}
return o.encAlg.DecryptString(decodedGrant, o.encAlg.EncryptionKeyID())
}
func (o *OPStorage) SaveAuthCode(ctx context.Context, id, code string) (err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() {
@@ -529,7 +519,7 @@ func (o *OPStorage) GetRefreshTokenInfo(ctx context.Context, clientID string, to
span.EndWithError(err)
}()
plainToken, err := o.decryptGrant(token)
plainToken, err := o.authAlg.DecryptToken(token)
if err != nil {
return "", "", op.ErrInvalidRefreshToken
}
+20 -8
View File
@@ -83,7 +83,7 @@ type OPStorage struct {
defaultIdTokenLifetime time.Duration
defaultRefreshTokenIdleExpiration time.Duration
defaultRefreshTokenExpiration time.Duration
encAlg crypto.EncryptionAlgorithm
authAlg crypto.AuthAlgorithm
assetAPIPrefix func(ctx context.Context) string
contextToIssuer func(context.Context) string
federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout]
@@ -118,7 +118,7 @@ func NewServer(
command *command.Commands,
query *query.Queries,
repo repository.Repository,
encryptionAlg crypto.EncryptionAlgorithm,
authAlg crypto.AuthAlgorithm,
targetEncryptionAlgorithm crypto.EncryptionAlgorithm,
cryptoKey []byte,
es *eventstore.Eventstore,
@@ -132,12 +132,24 @@ func NewServer(
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)
storage := newStorage(config, command, query, repo, authAlg, es, ContextToIssuer, federatedLogoutCache)
keyCache := newPublicKeyCache(ctx, config.PublicKeyCacheMaxAge, queryKeyFunc(query))
accessTokenKeySet := newOidcKeySet(keyCache, withKeyExpiryCheck(true))
idTokenHintKeySet := newOidcKeySet(keyCache)
var options []op.Option
alg := op.NewAES256GCMCrypto(opConfig.CryptoKey, "")
if authAlg.LegacyTokenEnabled() {
alg = op.NewCompositeCrypto(
alg,
[]op.Decrypter{
alg,
op.NewAESCrypto(opConfig.CryptoKey),
},
)
}
options := []op.Option{
op.WithCrypto(alg),
}
if !externalSecure {
options = append(options, op.WithAllowInsecure())
}
@@ -173,9 +185,9 @@ func NewServer(
jwksCacheControlMaxAge: config.JWKSCacheControlMaxAge,
fallbackLogger: fallbackLogger,
hasher: hasher,
encAlg: encryptionAlg,
encAlg: authAlg,
targetEncryptionAlgorithm: targetEncryptionAlgorithm,
opCrypto: op.NewAESCrypto(opConfig.CryptoKey),
opCrypto: alg,
assetAPIPrefix: assets.AssetAPI(),
}
metricTypes := []metrics.MetricType{metrics.MetricTypeRequestCount, metrics.MetricTypeStatusCode, metrics.MetricTypeTotalCount}
@@ -247,7 +259,7 @@ func newStorage(
command *command.Commands,
query *query.Queries,
repo repository.Repository,
encAlg crypto.EncryptionAlgorithm,
authAlg crypto.AuthAlgorithm,
es *eventstore.Eventstore,
contextToIssuer func(context.Context) string,
federateLogoutCache cache.Cache[federatedlogout.Index, string, *federatedlogout.FederatedLogout],
@@ -264,7 +276,7 @@ func newStorage(
defaultIdTokenLifetime: config.DefaultIdTokenLifetime,
defaultRefreshTokenIdleExpiration: config.DefaultRefreshTokenIdleExpiration,
defaultRefreshTokenExpiration: config.DefaultRefreshTokenExpiration,
encAlg: encAlg,
authAlg: authAlg,
assetAPIPrefix: assets.AssetAPI(),
contextToIssuer: contextToIssuer,
federateLogoutCache: federateLogoutCache,
+1 -1
View File
@@ -39,7 +39,7 @@ type Server struct {
fallbackLogger *slog.Logger
hasher *crypto.Hasher
signingKeyAlgorithm string
encAlg crypto.EncryptionAlgorithm
encAlg crypto.AuthAlgorithm
targetEncryptionAlgorithm crypto.EncryptionAlgorithm
opCrypto op.Crypto
-13
View File
@@ -2,7 +2,6 @@ package oidc
import (
"context"
"encoding/base64"
"slices"
"time"
@@ -145,15 +144,3 @@ func (s *Server) createJWT(ctx context.Context, client op.Client, session *comma
return crypto.Sign(claims, signer)
}
// decryptCode decrypts a code or refresh_token
func (s *Server) decryptCode(ctx context.Context, code string) (_ string, err error) {
_, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
decoded, err := base64.RawURLEncoding.DecodeString(code)
if err != nil {
return "", err
}
return s.encAlg.DecryptString(decoded, s.encAlg.EncryptionKeyID())
}
+1 -1
View File
@@ -27,7 +27,7 @@ func (s *Server) CodeExchange(ctx context.Context, r *op.ClientRequest[oidc.Acce
return nil, zerrors.ThrowInternal(nil, "OIDC-Ae2ph", "Error.Internal")
}
plainCode, err := s.decryptCode(ctx, r.Data.Code)
plainCode, err := s.encAlg.DecryptToken(r.Data.Code)
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "OIDC-ahLi2", "Errors.User.Code.Invalid")
}