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

# Which Problems Are Solved

Opaque tokens now use authenticated encryption.

# How the Problems Are Solved

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

# Additional Changes

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

# Additional Context

- closes #11315

---------

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