mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+1
-1
@@ -565,9 +565,9 @@ func startCommandsQueries(
|
||||
keys.SMS,
|
||||
keys.User,
|
||||
keys.DomainVerification,
|
||||
keys.OIDC,
|
||||
keys.SAML,
|
||||
keys.Target,
|
||||
keys.OIDC,
|
||||
&http.Client{},
|
||||
permissionCheck,
|
||||
sessionTokenVerifier,
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type KeyConfig struct {
|
||||
LegacyToken bool
|
||||
EncryptionKeyID string
|
||||
DecryptionKeyIDs []string
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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\"";
|
||||
}
|
||||
];
|
||||
|
||||
@@ -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.\"";
|
||||
}
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user