diff --git a/apps/api/project.json b/apps/api/project.json index 6ec1c00bb..30a97a6f2 100644 --- a/apps/api/project.json +++ b/apps/api/project.json @@ -179,7 +179,7 @@ "generate" ], "command": "go build -cover -race -tags integration -o .artifacts/bin/$(go env GOOS)/$(go env GOARCH)/zitadel.test main.go", - "cache": true, + "cache": false, "inputs": [ { "runtime": "go env GOOS" diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 72e266b27..02384119e 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -774,6 +774,17 @@ EncryptionKeys: EncryptionKeyID: "idpConfigKey" # ZITADEL_ENCRYPTIONKEYS_IDPCONFIG_ENCRYPTIONKEYID DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_IDPCONFIG_DECRYPTIONKEYIDS (comma separated list) OIDC: + # Accept opaque tokens (access, session, refresh tokens, PATs etc) created by older ZITADEL versions. + # Legacy tokens are vulnerable to certain crypto attacks, + # so this should only be enabled during a migration and disabled afterwards. + # After upgrading ZITADEL, new values will use authenticated encryption. + # When this option is enabled tokens emitted from before the upgrade will still be accepted until they expire, + # allowing gradual rollout of new tokens. + # This allows upgrading ZITADEL without forcing logout of all users (and invalidating all PATs) + # This option should be disabled as soon as all tokens have been replaced and PATs rotated. + # Note: older versions of ZITADEL aren't able to verify newer tokens. + # Downgrading ZITADEL will always logout all users that obtained a token after the upgrade. + LegacyToken: true # ZITADEL_ENCRYPTIONKEYS_OIDC_LEGACYTOKEN EncryptionKeyID: "oidcKey" # ZITADEL_ENCRYPTIONKEYS_OIDC_ENCRYPTIONKEYID DecryptionKeyIDs: # ZITADEL_ENCRYPTIONKEYS_OIDC_DECRYPTIONKEYIDS (comma separated list) SAML: diff --git a/cmd/encryption/encryption_keys.go b/cmd/encryption/encryption_keys.go index 9a26d572c..52ff15d46 100644 --- a/cmd/encryption/encryption_keys.go +++ b/cmd/encryption/encryption_keys.go @@ -40,7 +40,7 @@ type EncryptionKeyConfig struct { type EncryptionKeys struct { DomainVerification crypto.EncryptionAlgorithm IDPConfig crypto.EncryptionAlgorithm - OIDC crypto.EncryptionAlgorithm + OIDC crypto.AuthEncryptionAlgorithm SAML crypto.EncryptionAlgorithm OTP crypto.EncryptionAlgorithm SMS crypto.EncryptionAlgorithm diff --git a/cmd/mirror/projections.go b/cmd/mirror/projections.go index 3b7f1219a..f61b02ffe 100644 --- a/cmd/mirror/projections.go +++ b/cmd/mirror/projections.go @@ -203,9 +203,9 @@ func projections( keys.SMS, keys.User, keys.DomainVerification, - keys.OIDC, keys.SAML, keys.Target, + keys.OIDC, &http.Client{}, func(ctx context.Context, permission, orgID, resourceID string) (err error) { return internal_authz.CheckPermission(ctx, authZRepo, config.SystemAuthZ.RolePermissionMappings, config.InternalAuthZ.RolePermissionMappings, permission, orgID, resourceID) diff --git a/cmd/setup/03.go b/cmd/setup/03.go index 1497e9679..c995f6069 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -86,12 +86,12 @@ func (mig *FirstInstance) Execute(ctx context.Context, _ eventstore.Event) error nil, userAlg, nil, + nil, + nil, oidcEncryption, nil, nil, nil, - nil, - nil, 0, 0, 0, diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 7787349e6..3e533351f 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -565,9 +565,9 @@ func startCommandsQueries( keys.SMS, keys.User, keys.DomainVerification, - keys.OIDC, keys.SAML, keys.Target, + keys.OIDC, &http.Client{}, permissionCheck, sessionTokenVerifier, diff --git a/cmd/start/start.go b/cmd/start/start.go index 00829f83a..02b77a993 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -288,9 +288,9 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server keys.SMS, keys.User, keys.DomainVerification, - keys.OIDC, keys.SAML, keys.Target, + keys.OIDC, &http.Client{}, permissionCheck, sessionTokenVerifier, diff --git a/go.mod b/go.mod index a576ff1fe..ca9af000b 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.13 github.com/georgysavva/scany/v2 v2.1.4 github.com/go-chi/chi/v5 v5.2.5 - github.com/go-jose/go-jose/v4 v4.1.3 + github.com/go-jose/go-jose/v4 v4.1.4 github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-resty/resty/v2 v2.17.2 github.com/go-webauthn/webauthn v0.10.2 @@ -100,7 +100,7 @@ require ( github.com/veqryn/slog-context/otel v0.9.0 github.com/zitadel/exifremove v0.1.0 github.com/zitadel/logging v0.7.0 - github.com/zitadel/oidc/v3 v3.45.5 + github.com/zitadel/oidc/v3 v3.47.0 github.com/zitadel/passwap v0.11.0 github.com/zitadel/saml v0.4.1 github.com/zitadel/schema v1.3.2 @@ -109,7 +109,7 @@ require ( go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 - go.opentelemetry.io/otel v1.42.0 + go.opentelemetry.io/otel v1.43.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 @@ -121,11 +121,11 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 go.opentelemetry.io/otel/log v0.18.0 - go.opentelemetry.io/otel/metric v1.42.0 + go.opentelemetry.io/otel/metric v1.43.0 go.opentelemetry.io/otel/sdk v1.42.0 go.opentelemetry.io/otel/sdk/log v0.18.0 go.opentelemetry.io/otel/sdk/metric v1.42.0 - go.opentelemetry.io/otel/trace v1.42.0 + go.opentelemetry.io/otel/trace v1.43.0 go.uber.org/mock v0.6.0 golang.org/x/crypto v0.49.0 golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 diff --git a/go.sum b/go.sum index ca9ae2d27..56ba294f2 100644 --- a/go.sum +++ b/go.sum @@ -257,8 +257,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -837,8 +837,8 @@ github.com/zitadel/exifremove v0.1.0 h1:qD50ezWsfeeqfcvs79QyyjVfK+snN12v0U0deaU8 github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek= github.com/zitadel/logging v0.7.0 h1:eugftwMM95Wgqwftsvj81isL0JK/hoScVqp/7iA2adQ= github.com/zitadel/logging v0.7.0/go.mod h1:9A6h9feBF/3u0IhA4uffdzSDY7mBaf7RE78H5sFMINQ= -github.com/zitadel/oidc/v3 v3.45.5 h1:CubfcXQiqtysk+FZyIcvj1+1ayvdSV89v5xWu5asrDQ= -github.com/zitadel/oidc/v3 v3.45.5/go.mod h1:MKHUazeiNX/jxRc6HD/Dv9qhL/wNuzrJAadBEGXiBeE= +github.com/zitadel/oidc/v3 v3.47.0 h1:ryH8tWkBZ+WHFYi//uoeW2mjg/PsV8YVcMn/GQyqhZI= +github.com/zitadel/oidc/v3 v3.47.0/go.mod h1:xhsEkuRmwxuU7elQ1isxIou983rEJAJdM7fCQnJq2lQ= github.com/zitadel/passwap v0.11.0 h1:EL1A8dWz83xiJYru2326i+chkVKVR7YZq5Wep1GKQ3k= github.com/zitadel/passwap v0.11.0/go.mod h1:Jn7sqawaxrwRL24byEJ1CVIAmznR6LsMsKcCfO/dIsg= github.com/zitadel/saml v0.4.1 h1:4PPOAvnUFcfwOFIAFZ3oGTMD+tKVeF96ySfGBf5j3IU= @@ -866,8 +866,8 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs= @@ -892,8 +892,8 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1J go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg= go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw= @@ -902,8 +902,8 @@ go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/K go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= diff --git a/internal/api/authz/session_token.go b/internal/api/authz/session_token.go index 6b0aa112c..7ec5608e7 100644 --- a/internal/api/authz/session_token.go +++ b/internal/api/authz/session_token.go @@ -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") diff --git a/internal/api/grpc/oidc/v2/oidc.go b/internal/api/grpc/oidc/v2/oidc.go index d56d6da05..53c5c10e6 100644 --- a/internal/api/grpc/oidc/v2/oidc.go +++ b/internal/api/grpc/oidc/v2/oidc.go @@ -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()) -} diff --git a/internal/api/grpc/oidc/v2/server.go b/internal/api/grpc/oidc/v2/server.go index 3d8f78a8a..17c96502b 100644 --- a/internal/api/grpc/oidc/v2/server.go +++ b/internal/api/grpc/oidc/v2/server.go @@ -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, } } diff --git a/internal/api/oidc/auth_request.go b/internal/api/oidc/auth_request.go index 6ea81b260..7ac41185a 100644 --- a/internal/api/oidc/auth_request.go +++ b/internal/api/oidc/auth_request.go @@ -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 } diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 022204de3..6309549f6 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -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, diff --git a/internal/api/oidc/server.go b/internal/api/oidc/server.go index e2a6f3b30..4e9042ef9 100644 --- a/internal/api/oidc/server.go +++ b/internal/api/oidc/server.go @@ -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 diff --git a/internal/api/oidc/token.go b/internal/api/oidc/token.go index f3222be05..41e3f21f1 100644 --- a/internal/api/oidc/token.go +++ b/internal/api/oidc/token.go @@ -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()) -} diff --git a/internal/api/oidc/token_code.go b/internal/api/oidc/token_code.go index 3da071a21..e52bd2039 100644 --- a/internal/api/oidc/token_code.go +++ b/internal/api/oidc/token_code.go @@ -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") } diff --git a/internal/auth/repository/eventsourcing/eventstore/refresh_token.go b/internal/auth/repository/eventsourcing/eventstore/refresh_token.go index 90b98aaf8..8c52faf32 100644 --- a/internal/auth/repository/eventsourcing/eventstore/refresh_token.go +++ b/internal/auth/repository/eventsourcing/eventstore/refresh_token.go @@ -22,7 +22,7 @@ type RefreshTokenRepo struct { Eventstore *eventstore.Eventstore View *view.View SearchLimit uint64 - KeyAlgorithm crypto.EncryptionAlgorithm + KeyAlgorithm crypto.AuthAlgorithm } func (r *RefreshTokenRepo) RefreshTokenByToken(ctx context.Context, refreshToken string) (*usr_model.RefreshTokenView, error) { diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 81dc8abd5..dadf6ad68 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -32,7 +32,7 @@ type EsRepository struct { eventstore.OrgRepository } -func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *database.DB, esV2 *eventstore2.Eventstore, oidcEncryption crypto.EncryptionAlgorithm, userEncryption crypto.EncryptionAlgorithm) (*EsRepository, error) { +func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *database.DB, esV2 *eventstore2.Eventstore, oidcEncryption crypto.AuthAlgorithm, userEncryption crypto.EncryptionAlgorithm) (*EsRepository, error) { view, err := auth_view.StartView(dbClient, oidcEncryption, queries, esV2) if err != nil { return nil, err diff --git a/internal/auth/repository/eventsourcing/view/view.go b/internal/auth/repository/eventsourcing/view/view.go index c67844dba..c459f7baa 100644 --- a/internal/auth/repository/eventsourcing/view/view.go +++ b/internal/auth/repository/eventsourcing/view/view.go @@ -12,12 +12,12 @@ import ( type View struct { Db *gorm.DB client *database.DB - keyAlgorithm crypto.EncryptionAlgorithm + keyAlgorithm crypto.AuthAlgorithm query *query.Queries es *eventstore.Eventstore } -func StartView(sqlClient *database.DB, keyAlgorithm crypto.EncryptionAlgorithm, queries *query.Queries, es *eventstore.Eventstore) (*View, error) { +func StartView(sqlClient *database.DB, keyAlgorithm crypto.AuthAlgorithm, queries *query.Queries, es *eventstore.Eventstore) (*View, error) { gorm, err := gorm.Open("postgres", sqlClient.DB) if err != nil { return nil, err diff --git a/internal/authz/authz.go b/internal/authz/authz.go index cfd034de1..04365855e 100644 --- a/internal/authz/authz.go +++ b/internal/authz/authz.go @@ -9,6 +9,6 @@ import ( "github.com/zitadel/zitadel/internal/query" ) -func Start(queries *query.Queries, es *eventstore.Eventstore, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure bool) (repository.Repository, error) { - return eventsourcing.Start(queries, es, dbClient, keyEncryptionAlgorithm, externalSecure) +func Start(queries *query.Queries, es *eventstore.Eventstore, dbClient *database.DB, authAlgorithm crypto.AuthAlgorithm, externalSecure bool) (repository.Repository, error) { + return eventsourcing.Start(queries, es, dbClient, authAlgorithm, externalSecure) } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index 25ce2b142..d2bb93dbc 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -2,7 +2,6 @@ package eventstore import ( "context" - "encoding/base64" "fmt" "slices" "strings" @@ -30,11 +29,11 @@ import ( ) type TokenVerifierRepo struct { - TokenVerificationKey crypto.EncryptionAlgorithm - Eventstore *eventstore.Eventstore - View *view.View - Query *query.Queries - ExternalSecure bool + AuthAlgorithm crypto.AuthAlgorithm + Eventstore *eventstore.Eventstore + View *view.View + Query *query.Queries + ExternalSecure bool } func (repo *TokenVerifierRepo) Health() error { @@ -306,7 +305,7 @@ func (repo *TokenVerifierRepo) getUserEvents(ctx context.Context, userID, instan func (repo *TokenVerifierRepo) getTokenIDAndSubject(ctx context.Context, accessToken string) (tokenID string, subject string, valid bool) { // accessToken can be either opaque or JWT // let's try opaque first: - tokenIDSubject, err := repo.decryptAccessToken(accessToken) + tokenIDSubject, err := repo.AuthAlgorithm.DecryptToken(accessToken) if err != nil { logging.WithError(err).Warn("token verifier repo: decrypt access token") // if decryption did not work, it might be a JWT @@ -329,18 +328,6 @@ func (repo *TokenVerifierRepo) jwtTokenVerifier(ctx context.Context) *op.AccessT return op.NewAccessTokenVerifier(http_util.DomainContext(ctx).Origin(), keySet) } -func (repo *TokenVerifierRepo) decryptAccessToken(token string) (string, error) { - tokenData, err := base64.RawURLEncoding.DecodeString(token) - if err != nil { - return "", zerrors.ThrowUnauthenticated(nil, "APP-ASdgg", "invalid token") - } - tokenIDSubject, err := repo.TokenVerificationKey.DecryptString(tokenData, repo.TokenVerificationKey.EncryptionKeyID()) - if err != nil { - return "", zerrors.ThrowUnauthenticated(nil, "APP-8EF0zZ", "invalid token") - } - return tokenIDSubject, nil -} - func verifyAudience(audience []string, verifierClientID, projectID string) error { for _, aud := range audience { if verifierClientID == aud || projectID == aud { diff --git a/internal/authz/repository/eventsourcing/repository.go b/internal/authz/repository/eventsourcing/repository.go index 66a9a59fd..03a18daba 100644 --- a/internal/authz/repository/eventsourcing/repository.go +++ b/internal/authz/repository/eventsourcing/repository.go @@ -17,7 +17,7 @@ type EsRepository struct { authz_es.TokenVerifierRepo } -func Start(queries *query.Queries, es *eventstore.Eventstore, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure bool) (repository.Repository, error) { +func Start(queries *query.Queries, es *eventstore.Eventstore, dbClient *database.DB, authAlgorithm crypto.AuthAlgorithm, externalSecure bool) (repository.Repository, error) { view, err := authz_view.StartView(dbClient, queries) if err != nil { return nil, err @@ -28,11 +28,11 @@ func Start(queries *query.Queries, es *eventstore.Eventstore, dbClient *database Queries: queries, }, authz_es.TokenVerifierRepo{ - TokenVerificationKey: keyEncryptionAlgorithm, - Eventstore: es, - View: view, - Query: queries, - ExternalSecure: externalSecure, + AuthAlgorithm: authAlgorithm, + Eventstore: es, + View: view, + Query: queries, + ExternalSecure: externalSecure, }, }, nil } diff --git a/internal/command/command.go b/internal/command/command.go index f235100d0..68bbbd2fc 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -80,6 +80,7 @@ type Commands struct { webauthnConfig *webauthn_helper.Config keySize int keyAlgorithm crypto.EncryptionAlgorithm + authAlgorithm crypto.AuthAlgorithm certificateAlgorithm crypto.EncryptionAlgorithm certKeySize int privateKeyLifetime time.Duration @@ -129,7 +130,8 @@ func StartCommands( externalDomain string, externalSecure bool, externalPort uint16, - idpConfigEncryption, otpEncryption, smtpEncryption, smsEncryption, userEncryption, domainVerificationEncryption, oidcEncryption, samlEncryption, targetEncryption crypto.EncryptionAlgorithm, + idpConfigEncryption, otpEncryption, smtpEncryption, smsEncryption, userEncryption, domainVerificationEncryption, samlEncryption, targetEncryption crypto.EncryptionAlgorithm, + oidcEncryption crypto.AuthEncryptionAlgorithm, httpClient *http.Client, permissionCheck domain.PermissionCheck, sessionTokenVerifier func(ctx context.Context, sessionToken string, sessionID string, tokenID string) (err error), @@ -187,6 +189,7 @@ func StartCommands( domainVerificationGenerator: crypto.NewEncryptionGenerator(defaults.DomainVerification.VerificationGenerator, domainVerificationEncryption), domainVerificationValidator: api_http.ValidateDomain, keyAlgorithm: oidcEncryption, + authAlgorithm: oidcEncryption, certificateAlgorithm: samlEncryption, webauthnConfig: webAuthN, httpClient: httpClient, diff --git a/internal/command/crypto_test.go b/internal/command/crypto_test.go index 03a5f6197..33f9dc6c3 100644 --- a/internal/command/crypto_test.go +++ b/internal/command/crypto_test.go @@ -99,6 +99,20 @@ func testSecretGeneratorAddedEvent(typ domain.SecretGeneratorType) *instance.Sec ) } +type mockAuthCrypto struct{} + +func (m *mockAuthCrypto) EncryptToken(data string) (string, error) { + return data, nil +} + +func (m *mockAuthCrypto) DecryptToken(token string) (string, error) { + return token, nil +} + +func (m *mockAuthCrypto) LegacyTokenEnabled() bool { + return false +} + func Test_newCryptoCode(t *testing.T) { type args struct { typ domain.SecretGeneratorType diff --git a/internal/command/device_auth_test.go b/internal/command/device_auth_test.go index 8c8faac7b..3abf9c21e 100644 --- a/internal/command/device_auth_test.go +++ b/internal/command/device_auth_test.go @@ -1490,7 +1490,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { Header: http.Header{"foo": []string{"bar"}}, }, Reason: domain.TokenReasonAuthRequest, - RefreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID-rt_refreshTokenID:userID + RefreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", SessionID: "sessionID", }, }, @@ -1504,6 +1504,7 @@ func TestCommands_CreateOIDCSessionFromDeviceAuth(t *testing.T) { defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime, defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime, keyAlgorithm: tt.fields.keyAlgorithm, + authAlgorithm: &mockAuthCrypto{}, } got, err := c.CreateOIDCSessionFromDeviceAuth(tt.args.ctx, tt.args.deviceCode, tt.args.backChannelLogoutURI) c.jobs.Wait() diff --git a/internal/command/instance.go b/internal/command/instance.go index 94f4583b4..63ff4731f 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -681,7 +681,7 @@ func setupMachineAdmin(commands *Commands, validations *[]preparation.Validation if err != nil { return nil, nil, err } - *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm)) + *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.authAlgorithm)) } if machine.MachineKey != nil { machineKey = NewMachineKey(orgID, userID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type) @@ -705,7 +705,7 @@ func setupLoginClient(commands *Commands, validations *[]preparation.Validation, if err != nil { return nil, err } - *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.keyAlgorithm)) + *validations = append(*validations, prepareAddPersonalAccessToken(pat, commands.authAlgorithm)) } return pat, nil } diff --git a/internal/command/instance_test.go b/internal/command/instance_test.go index 68fe79e33..b22f995f7 100644 --- a/internal/command/instance_test.go +++ b/internal/command/instance_test.go @@ -1053,6 +1053,7 @@ func TestCommandSide_setupAdmins(t *testing.T) { zitadelRoles: tt.fields.roles, userPasswordHasher: tt.fields.userPasswordHasher, keyAlgorithm: tt.fields.keyAlgorithm, + authAlgorithm: &mockAuthCrypto{}, } validations := make([]preparation.Validation, 0) owner, pat, mk, loginClientPat, err := setupAdmins(r, &validations, tt.args.instanceAgg, tt.args.orgAgg, tt.args.machine, tt.args.human, tt.args.loginClient) @@ -1223,6 +1224,7 @@ func TestCommandSide_setupDefaultOrg(t *testing.T) { zitadelRoles: tt.fields.roles, userPasswordHasher: tt.fields.userPasswordHasher, keyAlgorithm: tt.fields.keyAlgorithm, + authAlgorithm: &mockAuthCrypto{}, } validations := make([]preparation.Validation, 0) pat, mk, loginClientPat, err := setupDefaultOrg(tt.args.ctx, r, &validations, tt.args.instanceAgg, tt.args.orgName, tt.args.machine, tt.args.human, tt.args.loginClient, tt.args.ids) @@ -1395,6 +1397,7 @@ func TestCommandSide_setUpInstance(t *testing.T) { userPasswordHasher: tt.fields.userPasswordHasher, keyAlgorithm: tt.fields.keyAlgorithm, GenerateDomain: tt.fields.generateDomain, + authAlgorithm: &mockAuthCrypto{}, } validations, pat, mk, loginClientPat, err := setUpInstance(tt.args.ctx, r, tt.args.setup) diff --git a/internal/command/oidc_session.go b/internal/command/oidc_session.go index ff0d53289..3909a865b 100644 --- a/internal/command/oidc_session.go +++ b/internal/command/oidc_session.go @@ -2,7 +2,6 @@ package command import ( "context" - "encoding/base64" "fmt" "strings" "time" @@ -314,7 +313,7 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, userID, resource return &OIDCSessionEvents{ commands: c, idGenerator: c.idGenerator, - encryptionAlg: c.keyAlgorithm, + authAlg: c.authAlgorithm, events: pending, oidcSessionWriteModel: NewOIDCSessionWriteModel(sessionID, resourceOwner), userStateModel: userStateModel, @@ -325,11 +324,7 @@ func (c *Commands) newOIDCSessionAddEvents(ctx context.Context, userID, resource } func (c *Commands) decryptRefreshToken(refreshToken string) (sessionID, refreshTokenID string, err error) { - decoded, err := base64.RawURLEncoding.DecodeString(refreshToken) - if err != nil { - return "", "", zerrors.ThrowInvalidArgument(err, "OIDCS-Cux9a", "Errors.User.RefreshToken.Invalid") - } - decrypted, err := c.keyAlgorithm.DecryptString(decoded, c.keyAlgorithm.EncryptionKeyID()) + decrypted, err := c.authAlgorithm.DecryptToken(refreshToken) if err != nil { return "", "", zerrors.ThrowInvalidArgument(err, "OIDCS-Jei0i", "Errors.User.RefreshToken.Invalid") } @@ -372,7 +367,7 @@ func (c *Commands) newOIDCSessionUpdateEvents(ctx context.Context, refreshToken return &OIDCSessionEvents{ commands: c, idGenerator: c.idGenerator, - encryptionAlg: c.keyAlgorithm, + authAlg: c.authAlgorithm, oidcSessionWriteModel: sessionWriteModel, accessTokenLifetime: accessTokenLifetime, refreshTokenLifeTime: refreshTokenLifeTime, @@ -383,7 +378,7 @@ func (c *Commands) newOIDCSessionUpdateEvents(ctx context.Context, refreshToken type OIDCSessionEvents struct { commands *Commands idGenerator id.Generator - encryptionAlg crypto.EncryptionAlgorithm + authAlg crypto.AuthAlgorithm events []eventstore.Command oidcSessionWriteModel *OIDCSessionWriteModel userStateModel *UserV2WriteModel @@ -502,11 +497,11 @@ func (c *OIDCSessionEvents) generateRefreshToken(userID string) (refreshTokenID, return "", "", err } refreshTokenID = RefreshTokenPrefix + refreshTokenID - token, err := c.encryptionAlg.Encrypt([]byte(fmt.Sprintf(oidcTokenFormat, c.oidcSessionWriteModel.OIDCRefreshTokenID(refreshTokenID), userID))) + token, err := c.authAlg.EncryptToken(fmt.Sprintf(oidcTokenFormat, c.oidcSessionWriteModel.OIDCRefreshTokenID(refreshTokenID), userID)) if err != nil { return "", "", err } - return refreshTokenID, base64.RawURLEncoding.EncodeToString(token), nil + return refreshTokenID, token, nil } func (c *OIDCSessionEvents) PushEvents(ctx context.Context) (*OIDCSession, error) { diff --git a/internal/command/oidc_session_test.go b/internal/command/oidc_session_test.go index 4ee95cb7f..717afc9d5 100644 --- a/internal/command/oidc_session_test.go +++ b/internal/command/oidc_session_test.go @@ -440,7 +440,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { Header: http.Header{"foo": []string{"bar"}}, }, Reason: domain.TokenReasonAuthRequest, - RefreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID-rt_refreshTokenID:userID + RefreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", }, state: "state", }, @@ -584,7 +584,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { Header: http.Header{"foo": []string{"bar"}}, }, Reason: domain.TokenReasonAuthRequest, - RefreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID-rt_refreshTokenID:userID + RefreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", }, state: "state", }, @@ -721,6 +721,7 @@ func TestCommands_CreateOIDCSessionFromAuthRequest(t *testing.T) { defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime, defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime, keyAlgorithm: tt.fields.keyAlgorithm, + authAlgorithm: &mockAuthCrypto{}, } c.setMilestonesCompletedForTest("instanceID") gotSession, gotState, err := c.CreateOIDCSessionFromAuthRequest(tt.args.ctx, tt.args.authRequestID, tt.args.complianceCheck, tt.args.needRefreshToken, tt.args.backChannelLogoutURI) @@ -1137,7 +1138,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { UserID: "user2", Issuer: "foo.com", }, - RefreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID-rt_refreshTokenID:userID + RefreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", }, }, { @@ -1610,6 +1611,7 @@ func TestCommands_CreateOIDCSession(t *testing.T) { defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime, keyAlgorithm: tt.fields.keyAlgorithm, checkPermission: tt.fields.checkPermission, + authAlgorithm: &mockAuthCrypto{}, } c.setMilestonesCompletedForTest("instanceID") got, err := c.CreateOIDCSession(tt.args.ctx, @@ -1703,7 +1705,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { }, args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID + refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", //V2_oidcSessionID:rt_refreshTokenID:userID complianceCheck: mockRefreshTokenComplianceChecker(nil), }, res{ @@ -1732,7 +1734,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { }, args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID + refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", //V2_oidcSessionID:rt_refreshTokenID:userID complianceCheck: mockRefreshTokenComplianceChecker(nil), }, res{ @@ -1765,7 +1767,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { }, args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID + refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", //V2_oidcSessionID:rt_refreshTokenID:userID complianceCheck: mockRefreshTokenComplianceChecker(nil), }, res{ @@ -1821,7 +1823,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { }, args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID + refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", //V2_oidcSessionID:rt_refreshTokenID:userID scope: []string{"openid", "offline_access"}, complianceCheck: mockRefreshTokenComplianceChecker(nil), }, @@ -1881,7 +1883,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { }, args{ ctx: authz.WithInstanceID(context.Background(), "instanceID"), - refreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDp1c2VySUQ", //V2_oidcSessionID:rt_refreshTokenID:userID + refreshToken: "V2_oidcSessionID-rt_refreshTokenID:userID", //V2_oidcSessionID:rt_refreshTokenID:userID scope: []string{"openid", "offline_access"}, complianceCheck: mockRefreshTokenComplianceChecker(nil), }, @@ -1892,7 +1894,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { ClientID: "clientID", UserID: "userID", Audience: []string{"audience"}, - RefreshToken: "VjJfb2lkY1Nlc3Npb25JRC1ydF9yZWZyZXNoVG9rZW5JRDI6dXNlcklE", // V2_oidcSessionID-rt_refreshTokenID2:userID% + RefreshToken: "V2_oidcSessionID-rt_refreshTokenID2:userID", // V2_oidcSessionID-rt_refreshTokenID2:userID% Expiration: time.Time{}.Add(time.Hour), Scope: []string{"openid", "profile", "offline_access"}, AuthMethods: []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, @@ -1914,6 +1916,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) { defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime, defaultRefreshTokenIdleLifetime: tt.fields.defaultRefreshTokenIdleLifetime, keyAlgorithm: tt.fields.keyAlgorithm, + authAlgorithm: &mockAuthCrypto{}, } got, err := c.ExchangeOIDCSessionRefreshAndAccessToken(tt.args.ctx, tt.args.refreshToken, tt.args.scope, tt.args.complianceCheck) require.ErrorIs(t, err, tt.res.err) diff --git a/internal/command/org.go b/internal/command/org.go index ca2165a6d..a525292e4 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -164,7 +164,7 @@ func (c *orgSetupCommands) setupOrgAdminMachine(orgAgg *org.Aggregate, machine * } pat.TokenID = tokenID c.pats = append(c.pats, pat) - c.validations = append(c.validations, prepareAddPersonalAccessToken(pat, c.commands.keyAlgorithm)) + c.validations = append(c.validations, prepareAddPersonalAccessToken(pat, c.commands.authAlgorithm)) } if machine.MachineKey != nil { machineKey = NewMachineKey(orgAgg.ID, machine.Machine.AggregateID, machine.MachineKey.ExpirationDate, machine.MachineKey.Type) diff --git a/internal/command/org_test.go b/internal/command/org_test.go index b9605fbb3..0d8c51f05 100644 --- a/internal/command/org_test.go +++ b/internal/command/org_test.go @@ -2096,7 +2096,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { Scopes: []string{openid.ScopeOpenID}, AllowedUserType: domain.UserTypeMachine, TokenID: "tokenID", - Token: "dG9rZW5JRDp1c2VySUQ", // token + Token: "tokenID:userID", }, }, }, @@ -2151,6 +2151,7 @@ func TestCommandSide_SetUpOrg(t *testing.T) { Role: domain.RoleOrgOwner, }, }, + authAlgorithm: &mockAuthCrypto{}, } got, err := r.SetUpOrg(tt.args.ctx, tt.args.setupOrg, tt.args.allowInitialMail, tt.args.permissionCheck, tt.args.userIDs...) assert.ErrorIs(t, err, tt.res.err) diff --git a/internal/command/session.go b/internal/command/session.go index 547b5d520..7f67e0715 100644 --- a/internal/command/session.go +++ b/internal/command/session.go @@ -3,7 +3,6 @@ package command import ( "bytes" "context" - "encoding/base64" "fmt" "time" @@ -451,17 +450,17 @@ func (c *Commands) sessionUserResourceOwner(ctx context.Context, model *SessionW return r.resourceOwner, nil } -func sessionTokenCreator(idGenerator id.Generator, sessionAlg crypto.EncryptionAlgorithm) func(sessionID string) (id string, token string, err error) { +func sessionTokenCreator(idGenerator id.Generator, sessionAlg crypto.AuthAlgorithm) func(sessionID string) (id string, token string, err error) { return func(sessionID string) (id string, token string, err error) { id, err = idGenerator.Next() if err != nil { return "", "", err } - encrypted, err := sessionAlg.Encrypt([]byte(fmt.Sprintf(authz.SessionTokenFormat, sessionID, id))) + token, err = sessionAlg.EncryptToken(fmt.Sprintf(authz.SessionTokenFormat, sessionID, id)) if err != nil { return "", "", err } - return id, base64.RawURLEncoding.EncodeToString(encrypted), nil + return id, token, nil } } diff --git a/internal/command/user_personal_access_token.go b/internal/command/user_personal_access_token.go index f37953f3d..8b5b1216d 100644 --- a/internal/command/user_personal_access_token.go +++ b/internal/command/user_personal_access_token.go @@ -2,7 +2,6 @@ package command import ( "context" - "encoding/base64" "time" "github.com/zitadel/zitadel/internal/command/preparation" @@ -85,7 +84,7 @@ func (c *Commands) AddPersonalAccessToken(ctx context.Context, pat *PersonalAcce return nil, err } } - validation := prepareAddPersonalAccessToken(pat, c.keyAlgorithm) + validation := prepareAddPersonalAccessToken(pat, c.authAlgorithm) cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, validation) if err != nil { return nil, err @@ -101,7 +100,7 @@ func (c *Commands) AddPersonalAccessToken(ctx context.Context, pat *PersonalAcce }, nil } -func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.EncryptionAlgorithm) preparation.Validation { +func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.AuthAlgorithm) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { if err := pat.valid(); err != nil { return nil, err @@ -114,7 +113,7 @@ func prepareAddPersonalAccessToken(pat *PersonalAccessToken, algorithm crypto.En if err != nil { return nil, err } - pat.Token, err = createToken(algorithm, writeModel.TokenID, writeModel.AggregateID) + pat.Token, err = algorithm.EncryptToken(pat.TokenID + ":" + pat.AggregateID) if err != nil { return nil, err } @@ -173,14 +172,6 @@ func prepareRemovePersonalAccessToken(pat *PersonalAccessToken) preparation.Vali } } -func createToken(algorithm crypto.EncryptionAlgorithm, tokenID, userID string) (string, error) { - encrypted, err := algorithm.Encrypt([]byte(tokenID + ":" + userID)) - if err != nil { - return "", err - } - return base64.RawURLEncoding.EncodeToString(encrypted), nil -} - func getPersonalAccessTokenWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, tokenID, resourceOwner string, check PermissionCheck) (_ *PersonalAccessTokenWriteModel, err error) { writeModel := NewPersonalAccessTokenWriteModel(userID, tokenID, resourceOwner) events, err := filter(ctx, writeModel.Query()) diff --git a/internal/command/user_personal_access_token_test.go b/internal/command/user_personal_access_token_test.go index 2341d52b6..5cfb8e270 100644 --- a/internal/command/user_personal_access_token_test.go +++ b/internal/command/user_personal_access_token_test.go @@ -2,7 +2,6 @@ package command import ( "context" - "encoding/base64" "testing" "time" @@ -203,7 +202,7 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { want: &domain.ObjectDetails{ ResourceOwner: "org1", }, - token: base64.RawURLEncoding.EncodeToString([]byte("token1:user1")), + token: "token1:user1", }, }, { @@ -251,16 +250,17 @@ func TestCommands_AddPersonalAccessToken(t *testing.T) { want: &domain.ObjectDetails{ ResourceOwner: "org1", }, - token: base64.RawURLEncoding.EncodeToString([]byte("token1:user1")), + token: "token1:user1", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Commands{ - eventstore: tt.fields.eventstore, - idGenerator: tt.fields.idGenerator, - keyAlgorithm: tt.fields.keyAlgorithm, + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + keyAlgorithm: tt.fields.keyAlgorithm, + authAlgorithm: &mockAuthCrypto{}, } got, err := c.AddPersonalAccessToken(tt.args.ctx, tt.args.pat) if tt.res.err == nil { diff --git a/internal/crypto/aes.go b/internal/crypto/aes.go index f57a78fb8..9e5e59610 100644 --- a/internal/crypto/aes.go +++ b/internal/crypto/aes.go @@ -5,9 +5,12 @@ import ( "crypto/cipher" "crypto/rand" "encoding/base64" + "errors" "io" "unicode/utf8" + "github.com/go-jose/go-jose/v4" + "github.com/zitadel/zitadel/internal/zerrors" ) @@ -17,6 +20,7 @@ type AESCrypto struct { keys map[string]string encryptionKeyID string keyIDs []string + legacyToken bool } func NewAESCrypto(config *KeyConfig, keyStorage KeyStorage) (*AESCrypto, error) { @@ -28,6 +32,7 @@ func NewAESCrypto(config *KeyConfig, keyStorage KeyStorage) (*AESCrypto, error) keys: keys, encryptionKeyID: config.EncryptionKeyID, keyIDs: ids, + legacyToken: config.LegacyToken, }, nil } @@ -69,6 +74,81 @@ func (a *AESCrypto) DecryptionKeyIDs() []string { return a.keyIDs } +func (a *AESCrypto) EncryptToken(data string) (string, error) { + encrypter, err := jose.NewEncrypter(jose.A256GCM, jose.Recipient{ + Algorithm: jose.A256GCMKW, + Key: a.encryptionKey(), + KeyID: a.encryptionKeyID, + }, nil) + if err != nil { + return "", zerrors.ThrowInternal(err, "CRYPTO-Woox2", "Errors.Internal") + } + + encrypted, err := encrypter.Encrypt([]byte(data)) + if err != nil { + return "", zerrors.ThrowInternal(err, "CRYPTO-Woox3", "Errors.Internal") + } + + serialized, err := encrypted.CompactSerialize() + if err != nil { + return "", zerrors.ThrowInternal(err, "CRYPT-Woox4", "Errors.Internal") + } + return serialized, nil +} + +func (a *AESCrypto) DecryptToken(value string) (string, error) { + key, err := a.decryptionKey(a.encryptionKeyID) + if err != nil { + return "", err + } + decrypted, err := a.decryptA256GCM(value, key) + if err == nil { + return decrypted, nil + } + if !a.legacyToken { + return "", err + } + decrypted, err2 := a.decryptLegacyToken(value, key) + if err2 != nil { + return "", errors.Join(err, err2) + } + return decrypted, nil +} + +func (a *AESCrypto) LegacyTokenEnabled() bool { + return a.legacyToken +} + +func (a *AESCrypto) decryptA256GCM(value string, key string) (string, error) { + jwe, err := jose.ParseEncrypted(value, []jose.KeyAlgorithm{jose.A256GCMKW}, []jose.ContentEncryption{jose.A256GCM}) + if err != nil { + return "", zerrors.ThrowPreconditionFailed(err, "CRYPT-ha6Oh", "malformed encrypted value") + } + decrypted, err := jwe.Decrypt(key) + if err != nil { + return "", zerrors.ThrowUnauthenticated(err, "CRYPT-OhN2u", "failed to decrypt value") + } + if !utf8.Valid(decrypted) { + return "", zerrors.ThrowPreconditionFailed(err, "CRYPT-Koh1u", "non-UTF-8 in decrypted string") + } + return string(decrypted), nil +} + +func (a *AESCrypto) decryptLegacyToken(value string, key string) (string, error) { + text, err := base64.RawURLEncoding.DecodeString(value) + if err != nil { + return "", zerrors.ThrowPreconditionFailed(err, "CRYPT-Eep6o", "malformed encrypted value") + } + decrypted, err := DecryptAES(text, key) + if err != nil { + return "", err + } + if !utf8.Valid(decrypted) { + return "", zerrors.ThrowPreconditionFailed(err, "CRYPT-ohGh6", "non-UTF-8 in decrypted string") + } + return string(decrypted), nil +} + func (a *AESCrypto) encryptionKey() string { return a.keys[a.encryptionKeyID] } diff --git a/internal/crypto/aes_test.go b/internal/crypto/aes_test.go index 128fd6c4d..e72c648e4 100644 --- a/internal/crypto/aes_test.go +++ b/internal/crypto/aes_test.go @@ -2,6 +2,7 @@ package crypto import ( "context" + "encoding/base64" "errors" "testing" "unicode/utf8" @@ -42,6 +43,18 @@ func newTestAESCrypto(t testing.TB) *AESCrypto { return aesCrypto } +func newTestAESCryptoLegacyToken(t testing.TB) *AESCrypto { + keyConfig := &KeyConfig{ + LegacyToken: true, + EncryptionKeyID: "keyID", + DecryptionKeyIDs: []string{"keyID"}, + } + keys := Keys{"keyID": "ThisKeyNeedsToHave32Characters!!"} + aesCrypto, err := NewAESCrypto(keyConfig, &mockKeyStorage{keys: keys}) + require.NoError(t, err) + return aesCrypto +} + func TestAESCrypto_DecryptString(t *testing.T) { aesCrypto := newTestAESCrypto(t) const input = "SecretData" @@ -107,3 +120,122 @@ func FuzzAESCrypto_DecryptString(f *testing.F) { assert.True(t, utf8.ValidString(got), "result is not valid UTF-8") }) } + +func TestAESCrypto_EncryptToken(t *testing.T) { + tests := []struct { + name string + config *KeyConfig + keyStorage KeyStorage + value string + want bool + wantErr error + }{ + { + name: "ok", + config: &KeyConfig{ + EncryptionKeyID: "keyID", + DecryptionKeyIDs: []string{"keyID"}, + }, + keyStorage: &mockKeyStorage{keys: Keys{"keyID": "ThisKeyNeedsToHave32Characters!!"}}, + value: "SecretData", + want: true, + wantErr: nil, + }, + { + name: "empty key error", + config: &KeyConfig{ + EncryptionKeyID: "keyID", + DecryptionKeyIDs: []string{"keyID"}, + }, + keyStorage: &mockKeyStorage{keys: Keys{"keyID": ""}}, + value: "SecretData", + want: false, + wantErr: zerrors.ThrowInternal(nil, "CRYPTO-Woox3", "Errors.Internal"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := NewAESCrypto(tt.config, tt.keyStorage) + require.NoError(t, err) + got, gotErr := a.EncryptToken(tt.value) + require.ErrorIs(t, gotErr, tt.wantErr) + if tt.want { + assert.NotEmpty(t, got, "expected non-empty result") + } + }) + } +} + +func TestAESCrypto_DecryptToken(t *testing.T) { + tests := []struct { + name string + crypto func(testing.TB) *AESCrypto + payload func(*testing.T) string + want string + wantErr error + }{ + { + name: "ok", + crypto: newTestAESCrypto, + payload: func(t *testing.T) string { + a := newTestAESCrypto(t) + crypted, err := a.EncryptToken("SecretData") + require.NoError(t, err) + return crypted + }, + want: "SecretData", + wantErr: nil, + }, + { + name: "malformed encrypted value", + crypto: newTestAESCrypto, + payload: func(t *testing.T) string { + return "malformed" + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "CRYPT-ha6Oh", "malformed encrypted value"), + }, + { + name: "invalid value", + crypto: newTestAESCrypto, + payload: func(t *testing.T) string { + return "eyJhbGciOiJBMjU2R0NNS1ciLCJlbmMiOiJBMjU2R0NNIiwiaXYiOiJfcUNRelpoVDF4bjJfUjJiIiwia2lkIjoia2V5SUQiLCJ0YWciOiJlcm1EV1oySE5mOGctMHRJa29zdHpRIn0.zGWGOxwPI8iq-ZSwmAqc1Ps8tltCp-g815nj7jY_m9Q.U_tpQDuz8SzrDEAD._YzsLNPi2BrmWA.G0I-gIfkWy6DNhqXPD_EXg" + }, + wantErr: zerrors.ThrowUnauthenticated(nil, "CRYPT-OhN2u", "failed to decrypt value"), + }, + { + name: "legacy decrypt", + crypto: newTestAESCryptoLegacyToken, + payload: func(t *testing.T) string { + a := newTestAESCryptoLegacyToken(t) + encrypted, err := a.Encrypt([]byte("SecretData")) + require.NoError(t, err) + return base64.RawURLEncoding.EncodeToString(encrypted) + }, + want: "SecretData", + }, + { + name: "legacy invalid base64", + crypto: newTestAESCryptoLegacyToken, + payload: func(t *testing.T) string { + return "invalid_base64" + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "CRYPT-ha6Oh", "malformed encrypted value"), + }, + { + name: "legacy invalid token", + crypto: newTestAESCryptoLegacyToken, + payload: func(t *testing.T) string { + return "DEADBEEFDEADBEEFDEADBEEF" + }, + wantErr: zerrors.ThrowPreconditionFailed(nil, "CRYPT-ohGh6", "non-UTF-8 in decrypted string"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := tt.crypto(t) + got, gotErr := a.DecryptToken(tt.payload(t)) + require.ErrorIs(t, gotErr, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go index 5669709e0..30f96d28b 100644 --- a/internal/crypto/crypto.go +++ b/internal/crypto/crypto.go @@ -16,6 +16,10 @@ const ( //go:generate mockgen -typed -package crypto -destination ./crypto.mock.go . EncryptionAlgorithm +// EncryptionAlgorithm is an interface that defines the methods for encrypting and decrypting values, +// used for sensitive data stored in the database. +// These methods are not intended to be used for encrypting data that is sent to the client, +// for this use the [AuthAlgorithm] instead. type EncryptionAlgorithm interface { Algorithm() string EncryptionKeyID() string @@ -28,6 +32,24 @@ type EncryptionAlgorithm interface { DecryptString(hashed []byte, keyID string) (string, error) } +// AuthAlgorithm is an interface that defines the methods for encrypting and decrypting tokens, +// used for sensitive data sent to the client, such as access tokens and refresh tokens. +// Implementation must obfuscate and authenticate the token. +type AuthAlgorithm interface { + // EncryptToken encrypts the token using authenticated encryption with the current encryption key. + EncryptToken(data string) (string, error) + + // DecryptToken authenticates and decrypts the token using the encryption key. + // When the decrypted value contains non-UTF8 characters an error is returned. + DecryptToken(token string) (string, error) + LegacyTokenEnabled() bool +} + +type AuthEncryptionAlgorithm interface { + EncryptionAlgorithm + AuthAlgorithm +} + // CryptoValue is a struct that can be used to store encrypted values in a database. // The struct is compatible with the [driver.Valuer] and database/sql.Scanner interfaces. type CryptoValue struct { diff --git a/internal/crypto/key.go b/internal/crypto/key.go index 5b4a22f69..18a77d8d4 100644 --- a/internal/crypto/key.go +++ b/internal/crypto/key.go @@ -9,6 +9,7 @@ import ( ) type KeyConfig struct { + LegacyToken bool EncryptionKeyID string DecryptionKeyIDs []string } diff --git a/internal/domain/refresh_token.go b/internal/domain/refresh_token.go index 25ab32f45..43084b5c0 100644 --- a/internal/domain/refresh_token.go +++ b/internal/domain/refresh_token.go @@ -1,31 +1,22 @@ package domain import ( - "encoding/base64" "strings" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/zerrors" ) -func NewRefreshToken(userID, tokenID string, algorithm crypto.EncryptionAlgorithm) (string, error) { +func NewRefreshToken(userID, tokenID string, algorithm crypto.AuthAlgorithm) (string, error) { return RefreshToken(userID, tokenID, tokenID, algorithm) } -func RefreshToken(userID, tokenID, token string, algorithm crypto.EncryptionAlgorithm) (string, error) { - encrypted, err := algorithm.Encrypt([]byte(userID + ":" + tokenID + ":" + token)) - if err != nil { - return "", err - } - return base64.RawURLEncoding.EncodeToString(encrypted), nil +func RefreshToken(userID, tokenID, token string, algorithm crypto.AuthAlgorithm) (string, error) { + return algorithm.EncryptToken(userID + ":" + tokenID + ":" + token) } -func FromRefreshToken(refreshToken string, algorithm crypto.EncryptionAlgorithm) (userID, tokenID, token string, err error) { - decoded, err := base64.RawURLEncoding.DecodeString(refreshToken) - if err != nil { - return "", "", "", zerrors.ThrowInvalidArgument(err, "DOMAIN-BGDhn", "Errors.User.RefreshToken.Invalid") - } - decrypted, err := algorithm.DecryptString(decoded, algorithm.EncryptionKeyID()) +func FromRefreshToken(refreshToken string, algorithm crypto.AuthAlgorithm) (userID, tokenID, token string, err error) { + decrypted, err := algorithm.DecryptToken(refreshToken) if err != nil { return "", "", "", zerrors.ThrowInvalidArgument(err, "DOMAIN-rie9A", "Errors.User.RefreshToken.Invalid") } diff --git a/internal/domain/refresh_token_test.go b/internal/domain/refresh_token_test.go index c9d331601..6a610aa1b 100644 --- a/internal/domain/refresh_token_test.go +++ b/internal/domain/refresh_token_test.go @@ -1,7 +1,6 @@ package domain import ( - "encoding/base64" "errors" "testing" @@ -49,12 +48,12 @@ func TestFromRefreshToken(t *testing.T) { refreshToken, err := NewRefreshToken(userID, tokenID, algorithm) require.NoError(t, err) - invalidRefreshToken, err := algorithm.Encrypt([]byte(userID + ":" + tokenID)) + invalidRefreshToken, err := algorithm.EncryptToken(userID + ":" + tokenID) require.NoError(t, err) type args struct { refreshToken string - algorithm crypto.EncryptionAlgorithm + algorithm crypto.AuthAlgorithm } tests := []struct { name string @@ -65,18 +64,18 @@ func TestFromRefreshToken(t *testing.T) { wantErr error }{ { - name: "invalid base64", + name: "invalid encoding", args: args{"~~~", algorithm}, - wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-BGDhn", "Errors.User.RefreshToken.Invalid"), + wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-rie9A", "Errors.User.RefreshToken.Invalid"), }, { name: "short cipher text", args: args{"DEADBEEF", algorithm}, - wantErr: zerrors.ThrowInvalidArgument(err, "DOMAIN-rie9A", "Errors.User.RefreshToken.Invalid"), + wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-rie9A", "Errors.User.RefreshToken.Invalid"), }, { name: "incorrect amount of segments", - args: args{base64.RawURLEncoding.EncodeToString(invalidRefreshToken), algorithm}, + args: args{invalidRefreshToken, algorithm}, wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-Se8oh", "Errors.User.RefreshToken.Invalid"), }, { @@ -101,6 +100,7 @@ func TestFromRefreshToken(t *testing.T) { // Fuzz test invalid inputs. None of the inputs should result in a success. func FuzzFromRefreshToken(f *testing.F) { keyConfig := &crypto.KeyConfig{ + LegacyToken: true, EncryptionKeyID: "keyID", DecryptionKeyIDs: []string{"keyID"}, } @@ -108,13 +108,13 @@ func FuzzFromRefreshToken(f *testing.F) { algorithm, err := crypto.NewAESCrypto(keyConfig, &mockKeyStorage{keys: keys}) require.NoError(f, err) - invalidRefreshToken, err := algorithm.Encrypt([]byte("userID:tokenID")) + invalidRefreshToken, err := algorithm.EncryptToken("userID:tokenID") require.NoError(f, err) tests := []string{ - "~~~", // invalid base64 - "DEADBEEF", // short cipher text - base64.RawURLEncoding.EncodeToString(invalidRefreshToken), // incorrect amount of segments + "~~~", // invalid base64 + "DEADBEEF", // short cipher text + invalidRefreshToken, // incorrect amount of segments } for _, tc := range tests { f.Add(tc) diff --git a/proto/zitadel/oidc/v2/oidc_service.proto b/proto/zitadel/oidc/v2/oidc_service.proto index 6ad6c4f23..55b391056 100644 --- a/proto/zitadel/oidc/v2/oidc_service.proto +++ b/proto/zitadel/oidc/v2/oidc_service.proto @@ -274,10 +274,10 @@ message Session { // Token of the session used to login the user. This token verifies that the session is valid. string session_token = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 500}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; - max_length: 200; + max_length: 500; }, (google.api.field_behavior) = REQUIRED ]; @@ -315,10 +315,10 @@ message GetDeviceAuthorizationRequestResponse { message AuthorizeOrDenyDeviceAuthorizationRequest { // The device authorization id returned when submitting the user code. string device_authorization_id = 1 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 500}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; - max_length: 200; + max_length: 500; }, (google.api.field_behavior) = REQUIRED ]; diff --git a/proto/zitadel/oidc/v2beta/oidc_service.proto b/proto/zitadel/oidc/v2beta/oidc_service.proto index 42b5fae55..3d81bfad2 100644 --- a/proto/zitadel/oidc/v2beta/oidc_service.proto +++ b/proto/zitadel/oidc/v2beta/oidc_service.proto @@ -210,10 +210,10 @@ message Session { ]; string session_token = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 500}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; - max_length: 200; + max_length: 500; description: "Token to verify the session is valid"; } ]; diff --git a/proto/zitadel/saml/v2/saml_service.proto b/proto/zitadel/saml/v2/saml_service.proto index 339f90ca8..888d86740 100644 --- a/proto/zitadel/saml/v2/saml_service.proto +++ b/proto/zitadel/saml/v2/saml_service.proto @@ -213,11 +213,11 @@ message Session { // Token to verify the session is valid. string session_token = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 500}, (google.api.field_behavior) = REQUIRED, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; - max_length: 200; + max_length: 500; } ]; } diff --git a/proto/zitadel/session/v2/session_service.proto b/proto/zitadel/session/v2/session_service.proto index 21548cbf8..6a3cf433a 100644 --- a/proto/zitadel/session/v2/session_service.proto +++ b/proto/zitadel/session/v2/session_service.proto @@ -327,10 +327,10 @@ message GetSessionRequest{ // - the security token provided in the authorization header has the same user agent as the session // - the caller is granted the permission session.read permission on either the instance or on the checked user's organization optional string session_token = 2 [ - (validate.rules).string = {min_len: 1, max_len: 200}, + (validate.rules).string = {min_len: 1, max_len: 500}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { min_length: 1; - max_length: 200; + max_length: 500; example: "\"222430354126975533\""; } ]; diff --git a/proto/zitadel/session/v2beta/session_service.proto b/proto/zitadel/session/v2beta/session_service.proto index a17938e9e..c8b216d59 100644 --- a/proto/zitadel/session/v2beta/session_service.proto +++ b/proto/zitadel/session/v2beta/session_service.proto @@ -329,10 +329,10 @@ message SetSessionRequest{ } ]; string session_token = 2 [ - (validate.rules).string = {min_len: 0, max_len: 200}, + (validate.rules).string = {min_len: 0, max_len: 500}, (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - min_length: 1; - max_length: 200; + min_length: 0; + max_length: 500; description: "\"DEPRECATED: this field is ignored.\""; } ];