feat(oidc): token exchange impersonation (#7516)

* add token exchange feature flag

* allow setting reason and actor to access tokens

* impersonation

* set token types and scopes in response

* upgrade oidc to working draft state

* fix tests

* audience and scope validation

* id toke and jwt as input

* return id tokens

* add grant type  token exchange to app config

* add integration tests

* check and deny actors in api calls

* fix instance setting tests by triggering projection on write and cleanup

* insert sleep statements again

* solve linting issues

* add translations

* pin oidc v3.15.0

* resolve comments, add event translation

* fix refreshtoken test

* use ValidateAuthReqScopes from oidc

* apparently the linter can't make up its mind

* persist actor thru refresh tokens and check in tests

* remove unneeded triggers
This commit is contained in:
Tim Möhlmann 2024-03-20 12:18:46 +02:00 committed by GitHub
parent b338171585
commit 6398349c24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
104 changed files with 2149 additions and 248 deletions

27
cmd/setup/24.go Normal file
View File

@ -0,0 +1,27 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 24.sql
addTokenActor string
)
type AddActorToAuthTokens struct {
dbClient *database.DB
}
func (mig *AddActorToAuthTokens) Execute(ctx context.Context, _ eventstore.Event) error {
_, err := mig.dbClient.ExecContext(ctx, addTokenActor)
return err
}
func (mig *AddActorToAuthTokens) String() string {
return "24_add_actor_col_to_auth_tokens"
}

2
cmd/setup/24.sql Normal file
View File

@ -0,0 +1,2 @@
ALTER TABLE auth.tokens ADD COLUMN actor jsonb;
ALTER TABLE auth.refresh_tokens ADD COLUMN actor jsonb;

View File

@ -102,6 +102,7 @@ type Steps struct {
s21AddBlockFieldToLimits *AddBlockFieldToLimits
s22ActiveInstancesIndex *ActiveInstanceEvents
s23CorrectGlobalUniqueConstraints *CorrectGlobalUniqueConstraints
s24AddActorToAuthTokens *AddActorToAuthTokens
}
func MustNewSteps(v *viper.Viper) *Steps {

View File

@ -136,6 +136,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s21AddBlockFieldToLimits = &AddBlockFieldToLimits{dbClient: queryDBClient}
steps.s22ActiveInstancesIndex = &ActiveInstanceEvents{dbClient: queryDBClient}
steps.s23CorrectGlobalUniqueConstraints = &CorrectGlobalUniqueConstraints{dbClient: esPusherDBClient}
steps.s24AddActorToAuthTokens = &AddActorToAuthTokens{dbClient: queryDBClient}
err = projection.Create(ctx, projectionDBClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@ -172,6 +173,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s20AddByUserSessionIndex,
steps.s22ActiveInstancesIndex,
steps.s23CorrectGlobalUniqueConstraints,
steps.s24AddActorToAuthTokens,
} {
mustExecuteMigration(ctx, eventstoreClient, step, "migration failed")
}

View File

@ -115,6 +115,7 @@ export class AppCreateComponent implements OnInit, OnDestroy {
{ type: OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT, checked: false, disabled: true },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN, checked: false, disabled: true },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE, checked: false, disabled: true },
{ type: OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE, checked: false, disabled: true },
];
public readonly separatorKeysCodes: number[] = [ENTER, COMMA, SPACE];

View File

@ -105,6 +105,7 @@ export class AppDetailComponent implements OnInit, OnDestroy {
OIDCGrantType.OIDC_GRANT_TYPE_IMPLICIT,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
];
public oidcAppTypes: OIDCAppType[] = [
OIDCAppType.OIDC_APP_TYPE_WEB,

View File

@ -213,6 +213,22 @@ export function getAuthMethodFromPartialConfig(config: {
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
]);
const codeWithExchange = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
]);
const codeWithRefreshAndExchange = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_BASIC,
]);
const pkce = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE],
@ -237,6 +253,22 @@ export function getAuthMethodFromPartialConfig(config: {
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
]);
const postWithExchange = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE, OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
]);
const postWithRefreshAndExchange = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
].sort(),
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_POST,
]);
const deviceCode = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE],
@ -263,6 +295,33 @@ export function getAuthMethodFromPartialConfig(config: {
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const deviceCodeWithCodeAndExchange = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const deviceCodeWithCodeAndRefreshAndExchange = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[
OIDCGrantType.OIDC_GRANT_TYPE_AUTHORIZATION_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE,
OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN,
OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE,
],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const deviceCodeWithExchange = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE, OIDCGrantType.OIDC_GRANT_TYPE_TOKEN_EXCHANGE],
OIDCAuthMethodType.OIDC_AUTH_METHOD_TYPE_NONE,
]);
const deviceCodeWithRefresh = JSON.stringify([
[OIDCResponseType.OIDC_RESPONSE_TYPE_CODE],
[OIDCGrantType.OIDC_GRANT_TYPE_DEVICE_CODE, OIDCGrantType.OIDC_GRANT_TYPE_REFRESH_TOKEN],
@ -292,6 +351,10 @@ export function getAuthMethodFromPartialConfig(config: {
return CODE_METHOD.key;
case codeWithRefresh:
return CODE_METHOD.key;
case codeWithExchange:
return CODE_METHOD.key;
case codeWithRefreshAndExchange:
return CODE_METHOD.key;
case pkce:
return PKCE_METHOD.key;
@ -302,6 +365,10 @@ export function getAuthMethodFromPartialConfig(config: {
return POST_METHOD.key;
case postWithRefresh:
return POST_METHOD.key;
case postWithExchange:
return POST_METHOD.key;
case postWithRefreshAndExchange:
return POST_METHOD.key;
case deviceCode:
return DEVICE_CODE_METHOD.key;
@ -309,8 +376,14 @@ export function getAuthMethodFromPartialConfig(config: {
return DEVICE_CODE_METHOD.key;
case deviceCodeWithRefresh:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithExchange:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithCodeAndRefresh:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithCodeAndExchange:
return DEVICE_CODE_METHOD.key;
case deviceCodeWithCodeAndRefreshAndExchange:
return DEVICE_CODE_METHOD.key;
case pkjwt:
return PK_JWT_METHOD.key;

View File

@ -2270,7 +2270,8 @@
"0": "Код за оторизация",
"1": "имплицитно",
"2": "Опресняване на токена",
"3": "Код на устройството"
"3": "Код на устройството",
"4": "Размяна на токени"
},
"AUTHMETHOD": {
"0": "Основен",

View File

@ -2289,7 +2289,8 @@
"0": "Autorizační kód",
"1": "Implicitní",
"2": "Obnovovací Token",
"3": "Kód zařízení"
"3": "Kód zařízení",
"4": "Výměna tokenů"
},
"AUTHMETHOD": {
"0": "Základní",

View File

@ -2279,7 +2279,8 @@
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token",
"3": "Device Code"
"3": "Device Code",
"4": "Token Exchange"
},
"AUTHMETHOD": {
"0": "Basic",

View File

@ -2298,7 +2298,8 @@
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token",
"3": "Device Code"
"3": "Device Code",
"4": "Token Exchange"
},
"AUTHMETHOD": {
"0": "Basic",

View File

@ -2277,7 +2277,8 @@
"0": "Código de autorización",
"1": "Implícito",
"2": "Token de refresco",
"3": "Device Code"
"3": "Device Code",
"4": "Intercambio de tokens"
},
"AUTHMETHOD": {
"0": "Básico",

View File

@ -2280,7 +2280,8 @@
"0": "Code d'autorisation",
"1": "Implicite",
"2": "Rafraîchir le jeton",
"3": "Device Code"
"3": "Device Code",
"4": "Échange de jetons"
},
"AUTHMETHOD": {
"0": "Basic",

View File

@ -2280,7 +2280,8 @@
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token",
"3": "Device Code"
"3": "Device Code",
"4": "Token Exchange"
},
"AUTHMETHOD": {
"0": "Basic",

View File

@ -2271,7 +2271,8 @@
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token",
"3": "Device Code"
"3": "Device Code",
"4": "Token Exchange"
},
"AUTHMETHOD": {
"0": "Basic",

View File

@ -2277,7 +2277,8 @@
"0": "Код на Овластување",
"1": "Implicit",
"2": "Токен за Освежување",
"3": "Код од Уред"
"3": "Код од Уред",
"4": "Размена на токени"
},
"AUTHMETHOD": {
"0": "Basic",

View File

@ -2298,7 +2298,8 @@
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token",
"3": "Device Code"
"3": "Device Code",
"4": "Token Exchange"
},
"AUTHMETHOD": {
"0": "Basic",

View File

@ -2280,7 +2280,8 @@
"0": "Kod autoryzacyjny",
"1": "Implicite",
"2": "Token odświeżający",
"3": "Device Code"
"3": "Device Code",
"4": "Wymiana tokenów"
},
"AUTHMETHOD": {
"0": "Podstawowy",

View File

@ -2275,7 +2275,8 @@
"0": "Código de Autorização",
"1": "Implícito",
"2": "Token de Atualização",
"3": "Código do Dispositivo"
"3": "Código do Dispositivo",
"4": "Troca de tokens"
},
"AUTHMETHOD": {
"0": "Básico",

View File

@ -2393,7 +2393,8 @@
"0": "Код авторизации",
"1": "Скрытый",
"2": "Обновить токен",
"3": "Код устройства"
"3": "Код устройства",
"4": "Обмен токенов"
},
"AUTHMETHOD": {
"0": "Basic",

View File

@ -2279,7 +2279,8 @@
"0": "Authorization Code",
"1": "Implicit",
"2": "Refresh Token",
"3": "Device Code"
"3": "Device Code",
"4": "Token Exchange"
},
"AUTHMETHOD": {
"0": "Basic",

6
go.mod
View File

@ -3,7 +3,7 @@ module github.com/zitadel/zitadel
go 1.21.1
// https://go.dev/doc/toolchain
toolchain go1.21.5
toolchain go1.21.8
require (
cloud.google.com/go/storage v1.39.0
@ -24,7 +24,7 @@ require (
github.com/drone/envsubst v1.0.3
github.com/envoyproxy/protoc-gen-validate v1.0.4
github.com/fatih/color v1.16.0
github.com/go-jose/go-jose/v3 v3.0.2
github.com/go-jose/go-jose/v3 v3.0.3
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-webauthn/webauthn v0.10.1
github.com/gorilla/csrf v1.7.2
@ -62,7 +62,7 @@ require (
github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203
github.com/ttacon/libphonenumber v1.2.1
github.com/zitadel/logging v0.6.0
github.com/zitadel/oidc/v3 v3.14.0
github.com/zitadel/oidc/v3 v3.18.0
github.com/zitadel/passwap v0.5.0
github.com/zitadel/saml v0.1.3
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0

8
go.sum
View File

@ -218,8 +218,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/v3 v3.0.2 h1:2Edjn8Nrb44UvTdp84KU0bBPs1cO7noRCybtS3eJEUQ=
github.com/go-jose/go-jose/v3 v3.0.2/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
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=
@ -779,8 +779,8 @@ github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/zitadel/logging v0.6.0 h1:t5Nnt//r+m2ZhhoTmoPX+c96pbMarqJvW1Vq6xFTank=
github.com/zitadel/logging v0.6.0/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
github.com/zitadel/oidc/v3 v3.14.0 h1:k2tXbWsWiAb0/KqbrZzg6V0zHHpNAQbkq5SV0OVFHm8=
github.com/zitadel/oidc/v3 v3.14.0/go.mod h1:eFpGSMDvzdpq+ru2Dr3gWWdZCT6BqPk2QbY3qA7rU9I=
github.com/zitadel/oidc/v3 v3.18.0 h1:NGdxLIYbuvaIqc/Na1fu61wBXIbqufp7LsFNV1bXOQw=
github.com/zitadel/oidc/v3 v3.18.0/go.mod h1:tY75hMcm07McpPXzvgvFTNPefPYDnHRYZQZVn9gtAps=
github.com/zitadel/passwap v0.5.0 h1:kFMoRyo0GnxtOz7j9+r/CsRwSCjHGRaAKoUe69NwPvs=
github.com/zitadel/passwap v0.5.0/go.mod h1:uqY7D3jqdTFcKsW0Q3Pcv5qDMmSHpVTzUZewUKC1KZA=
github.com/zitadel/saml v0.1.3 h1:LI4DOCVyyU1qKPkzs3vrGcA5J3H4pH3+CL9zr9ShkpM=

View File

@ -182,7 +182,7 @@ func checkOrigin(ctx context.Context, origins []string) error {
func extractBearerToken(token string) (part string, err error) {
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", zerrors.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
return "", zerrors.ThrowUnauthenticated(nil, "AUTH-toLo1", "invalid auth header")
}
return parts[1], nil
}

View File

@ -5,8 +5,12 @@ import (
"github.com/muhlemmer/gu"
"github.com/zitadel/logging"
object_pb "github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/eventstore/handler/v2"
"github.com/zitadel/zitadel/internal/query/projection"
admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin"
)
@ -17,6 +21,9 @@ func (s *Server) ActivateFeatureLoginDefaultOrg(ctx context.Context, _ *admin_pb
if err != nil {
return nil, err
}
_, err = projection.InstanceFeatureProjection.Trigger(ctx, handler.WithAwaitRunning())
logging.OnError(err).Warn("trigger instance feature projection")
return &admin_pb.ActivateFeatureLoginDefaultOrgResponse{
Details: object_pb.DomainToChangeDetailsPb(details),
}, nil

View File

@ -23,6 +23,14 @@ func TestServer_GetSecurityPolicy(t *testing.T) {
EnableImpersonation: true,
})
require.NoError(t, err)
t.Cleanup(func() {
_, err := Client.SetSecurityPolicy(AdminCTX, &admin_pb.SetSecurityPolicyRequest{
EnableIframeEmbedding: false,
AllowedOrigins: []string{},
EnableImpersonation: false,
})
require.NoError(t, err)
})
tests := []struct {
name string

View File

@ -14,6 +14,7 @@ func systemFeaturesToCommand(req *feature_pb.SetSystemFeaturesRequest) *command.
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
LegacyIntrospection: req.OidcLegacyIntrospection,
UserSchema: req.UserSchema,
TokenExchange: req.OidcTokenExchange,
}
}
@ -24,6 +25,7 @@ func systemFeaturesToPb(f *query.SystemFeatures) *feature_pb.GetSystemFeaturesRe
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
UserSchema: featureSourceToFlagPb(&f.UserSchema),
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
}
}
@ -33,6 +35,7 @@ func instanceFeaturesToCommand(req *feature_pb.SetInstanceFeaturesRequest) *comm
TriggerIntrospectionProjections: req.OidcTriggerIntrospectionProjections,
LegacyIntrospection: req.OidcLegacyIntrospection,
UserSchema: req.UserSchema,
TokenExchange: req.OidcTokenExchange,
}
}
@ -43,6 +46,7 @@ func instanceFeaturesToPb(f *query.InstanceFeatures) *feature_pb.GetInstanceFeat
OidcTriggerIntrospectionProjections: featureSourceToFlagPb(&f.TriggerIntrospectionProjections),
OidcLegacyIntrospection: featureSourceToFlagPb(&f.LegacyIntrospection),
UserSchema: featureSourceToFlagPb(&f.UserSchema),
OidcTokenExchange: featureSourceToFlagPb(&f.TokenExchange),
}
}

View File

@ -22,12 +22,14 @@ func Test_systemFeaturesToCommand(t *testing.T) {
OidcTriggerIntrospectionProjections: gu.Ptr(false),
OidcLegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
OidcTokenExchange: gu.Ptr(true),
}
want := &command.SystemFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
TokenExchange: gu.Ptr(true),
}
got := systemFeaturesToCommand(arg)
assert.Equal(t, want, got)
@ -56,6 +58,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
Level: feature.LevelSystem,
Value: true,
},
TokenExchange: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: false,
},
}
want := &feature_pb.GetSystemFeaturesResponse{
Details: &object.Details{
@ -79,6 +85,10 @@ func Test_systemFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
OidcTokenExchange: &feature_pb.FeatureFlag{
Enabled: false,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := systemFeaturesToPb(arg)
assert.Equal(t, want, got)
@ -90,12 +100,14 @@ func Test_instanceFeaturesToCommand(t *testing.T) {
OidcTriggerIntrospectionProjections: gu.Ptr(false),
OidcLegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
OidcTokenExchange: gu.Ptr(true),
}
want := &command.InstanceFeatures{
LoginDefaultOrg: gu.Ptr(true),
TriggerIntrospectionProjections: gu.Ptr(false),
LegacyIntrospection: nil,
UserSchema: gu.Ptr(true),
TokenExchange: gu.Ptr(true),
}
got := instanceFeaturesToCommand(arg)
assert.Equal(t, want, got)
@ -124,6 +136,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Level: feature.LevelInstance,
Value: true,
},
TokenExchange: query.FeatureSource[bool]{
Level: feature.LevelSystem,
Value: false,
},
}
want := &feature_pb.GetInstanceFeaturesResponse{
Details: &object.Details{
@ -147,6 +163,10 @@ func Test_instanceFeaturesToPb(t *testing.T) {
Enabled: true,
Source: feature_pb.Source_SOURCE_INSTANCE,
},
OidcTokenExchange: &feature_pb.FeatureFlag{
Enabled: false,
Source: feature_pb.Source_SOURCE_SYSTEM,
},
}
got := instanceFeaturesToPb(arg)
assert.Equal(t, want, got)

View File

@ -138,6 +138,8 @@ func OIDCGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app_pb.OIDCGra
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN
case domain.OIDCGrantTypeDeviceCode:
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE
case domain.OIDCGrantTypeTokenExchange:
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE
}
}
return oidcGrantTypes
@ -158,6 +160,8 @@ func OIDCGrantTypesToDomain(grantTypes []app_pb.OIDCGrantType) []domain.OIDCGran
oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken
case app_pb.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE:
oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode
case app_pb.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE:
oidcGrantTypes[i] = domain.OIDCGrantTypeTokenExchange
}
}
return oidcGrantTypes

View File

@ -11,6 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/user/model"
"github.com/zitadel/zitadel/internal/zerrors"
@ -19,13 +20,17 @@ import (
type accessToken struct {
tokenID string
userID string
resourceOwner string
subject string
clientID string
audience []string
scope []string
authMethods []domain.UserAuthMethodType
authTime time.Time
tokenCreation time.Time
tokenExpiration time.Time
isPAT bool
actor *domain.TokenActor
}
var ErrInvalidTokenFormat = errors.New("invalid token format")
@ -67,6 +72,7 @@ func accessTokenV1(tokenID, subject string, token *model.TokenView) *accessToken
return &accessToken{
tokenID: tokenID,
userID: token.UserID,
resourceOwner: token.ResourceOwner,
subject: subject,
clientID: token.ApplicationID,
audience: token.Audience,
@ -74,6 +80,7 @@ func accessTokenV1(tokenID, subject string, token *model.TokenView) *accessToken
tokenCreation: token.CreationDate,
tokenExpiration: token.Expiration,
isPAT: token.IsPAT,
actor: token.Actor,
}
}
@ -81,17 +88,21 @@ func accessTokenV2(tokenID, subject string, token *query.OIDCSessionAccessTokenR
return &accessToken{
tokenID: tokenID,
userID: token.UserID,
resourceOwner: token.ResourceOwner,
subject: subject,
clientID: token.ClientID,
audience: token.Audience,
scope: token.Scope,
authMethods: token.AuthMethods,
authTime: token.AuthTime,
tokenCreation: token.AccessTokenCreation,
tokenExpiration: token.AccessTokenExpiration,
actor: token.Actor,
}
}
func (s *Server) assertClientScopesForPAT(ctx context.Context, token *accessToken, clientID, projectID string) error {
token.audience = append(token.audience, clientID)
token.audience = append(token.audience, clientID, projectID)
projectIDQuery, err := query.NewProjectRoleProjectIDSearchQuery(projectID)
if err != nil {
return zerrors.ThrowInternal(err, "OIDC-Cyc78", "Errors.Internal")

View File

@ -21,6 +21,9 @@ const (
//
// [RFC 8176, section 2]: https://datatracker.ietf.org/doc/html/rfc8176#section-2
func AuthMethodTypesToAMR(methodTypes []domain.UserAuthMethodType) []string {
if methodTypes == nil {
return nil // make sure amr is omitted when not provided / supported
}
amr := make([]string, 0, 4)
var factors, otp int
for _, methodType := range methodTypes {
@ -34,7 +37,8 @@ func AuthMethodTypesToAMR(methodTypes []domain.UserAuthMethodType) []string {
case domain.UserAuthMethodTypeU2F:
amr = append(amr, UserPresence)
factors++
case domain.UserAuthMethodTypeTOTP,
case domain.UserAuthMethodTypeOTP,
domain.UserAuthMethodTypeTOTP,
domain.UserAuthMethodTypeOTPSMS,
domain.UserAuthMethodTypeOTPEmail:
// a user could use multiple (t)otp, which is a factor, but still will be returned as a single `otp` entry
@ -55,3 +59,33 @@ func AuthMethodTypesToAMR(methodTypes []domain.UserAuthMethodType) []string {
}
return amr
}
func AMRToAuthMethodTypes(amr []string) []domain.UserAuthMethodType {
authMethods := make([]domain.UserAuthMethodType, 0, len(amr))
var (
userPresence bool
mfa bool
)
for _, entry := range amr {
switch entry {
case Password, PWD:
authMethods = append(authMethods, domain.UserAuthMethodTypePassword)
case OTP:
authMethods = append(authMethods, domain.UserAuthMethodTypeOTP)
case UserPresence:
userPresence = true
case MFA:
mfa = true
}
}
if userPresence {
if mfa {
authMethods = append(authMethods, domain.UserAuthMethodTypePasswordless)
} else {
authMethods = append(authMethods, domain.UserAuthMethodTypeU2F)
}
}
return authMethods
}

View File

@ -22,7 +22,7 @@ func TestAMR(t *testing.T) {
args{
nil,
},
[]string{},
nil,
},
{
"pw checked",

View File

@ -207,27 +207,18 @@ func (o *OPStorage) CreateAccessToken(ctx context.Context, req op.TokenRequest)
err = oidcError(err)
span.EndWithError(err)
}()
var userAgentID, applicationID, userOrgID string
switch authReq := req.(type) {
case *AuthRequest:
userAgentID = authReq.AgentID
applicationID = authReq.ApplicationID
userOrgID = authReq.UserOrgID
case *AuthRequestV2:
// trigger activity log for authentication for user
if authReq, ok := req.(*AuthRequestV2); ok {
activity.Trigger(ctx, "", authReq.CurrentAuthRequest.UserID, activity.OIDCAccessToken, o.eventstore.FilterToQueryReducer)
return o.command.AddOIDCSessionAccessToken(setContextUserSystem(ctx), authReq.GetID())
case op.IDTokenRequest:
applicationID = authReq.GetClientID()
}
userAgentID, applicationID, userOrgID, authTime, amr, reason, actor := getInfoFromRequest(req)
accessTokenLifetime, _, _, _, err := o.getOIDCSettings(ctx)
if err != nil {
return "", time.Time{}, err
}
resp, err := o.command.AddUserToken(setContextUserSystem(ctx), userOrgID, userAgentID, applicationID, req.GetSubject(), req.GetAudience(), req.GetScopes(), accessTokenLifetime) //PLANNED: lifetime from client
resp, err := o.command.AddUserToken(setContextUserSystem(ctx), userOrgID, userAgentID, applicationID, req.GetSubject(), req.GetAudience(), req.GetScopes(), amr, accessTokenLifetime, authTime, reason, actor)
if err != nil {
return "", time.Time{}, err
}
@ -256,7 +247,7 @@ func (o *OPStorage) CreateAccessAndRefreshTokens(ctx context.Context, req op.Tok
return o.command.ExchangeOIDCSessionRefreshAndAccessToken(setContextUserSystem(ctx), tokenReq.OIDCSessionWriteModel.AggregateID, refreshToken, tokenReq.RequestedScopes)
}
userAgentID, applicationID, userOrgID, authTime, authMethodsReferences := getInfoFromRequest(req)
userAgentID, applicationID, userOrgID, authTime, authMethodsReferences, reason, actor := getInfoFromRequest(req)
scopes, err := o.assertProjectRoleScopes(ctx, applicationID, req.GetScopes())
if err != nil {
return "", "", time.Time{}, zerrors.ThrowPreconditionFailed(err, "OIDC-Df2fq", "Errors.Internal")
@ -272,7 +263,7 @@ func (o *OPStorage) CreateAccessAndRefreshTokens(ctx context.Context, req op.Tok
resp, token, err := o.command.AddAccessAndRefreshToken(setContextUserSystem(ctx), userOrgID, userAgentID, applicationID, req.GetSubject(),
refreshToken, req.GetAudience(), scopes, authMethodsReferences, accessTokenLifetime,
refreshTokenIdleExpiration, refreshTokenExpiration, authTime) //PLANNED: lifetime from client
refreshTokenIdleExpiration, refreshTokenExpiration, authTime, reason, actor) //PLANNED: lifetime from client
if err != nil {
if zerrors.IsErrorInvalidArgument(err) {
err = oidc.ErrInvalidGrant().WithParent(err)
@ -285,16 +276,20 @@ func (o *OPStorage) CreateAccessAndRefreshTokens(ctx context.Context, req op.Tok
return resp.TokenID, token, resp.Expiration, nil
}
func getInfoFromRequest(req op.TokenRequest) (string, string, string, time.Time, []string) {
func getInfoFromRequest(req op.TokenRequest) (agentID string, clientID string, userOrgID string, authTime time.Time, amr []string, reason domain.TokenReason, actor *domain.TokenActor) {
switch r := req.(type) {
case *AuthRequest:
return r.AgentID, r.ApplicationID, r.UserOrgID, r.AuthTime, r.GetAMR()
return r.AgentID, r.ApplicationID, r.UserOrgID, r.AuthTime, r.GetAMR(), domain.TokenReasonAuthRequest, nil
case *RefreshTokenRequest:
return r.UserAgentID, r.ClientID, "", r.AuthTime, r.AuthMethodsReferences
return r.UserAgentID, r.ClientID, "", r.AuthTime, r.AuthMethodsReferences, domain.TokenReasonRefresh, r.Actor
case op.IDTokenRequest:
return "", r.GetClientID(), "", r.GetAuthTime(), r.GetAMR()
return "", r.GetClientID(), "", r.GetAuthTime(), r.GetAMR(), domain.TokenReasonAuthRequest, nil
case *oidc.JWTTokenRequest:
return "", "", "", r.GetAuthTime(), nil, domain.TokenReasonJWTProfile, nil
case *clientCredentialsRequest:
return "", "", "", time.Time{}, nil, domain.TokenReasonClientCredentials, nil
default:
return "", "", "", time.Time{}, nil
return "", "", "", time.Time{}, nil, domain.TokenReasonAuthRequest, nil
}
}

View File

@ -308,3 +308,7 @@ func (r *RefreshTokenRequest) GetSubject() string {
func (r *RefreshTokenRequest) SetCurrentScopes(scopes []string) {
r.Scopes = scopes
}
func (r *RefreshTokenRequest) GetActor() *oidc.ActorClaims {
return actorDomainToClaims(r.Actor)
}

View File

@ -27,15 +27,17 @@ import (
)
const (
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
ScopeProjectsRoles = "urn:zitadel:iam:org:projects:roles"
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
ClaimProjectRolesFormat = "urn:zitadel:iam:org:project:%s:roles"
ScopeUserMetaData = "urn:zitadel:iam:user:metadata"
ClaimUserMetaData = ScopeUserMetaData
ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner"
ClaimResourceOwner = ScopeResourceOwner + ":"
ClaimActionLogFormat = "urn:zitadel:iam:action:%s:log"
ScopeProjectRolePrefix = "urn:zitadel:iam:org:project:role:"
ScopeProjectsRoles = "urn:zitadel:iam:org:projects:roles"
ClaimProjectRoles = "urn:zitadel:iam:org:project:roles"
ClaimProjectRolesFormat = "urn:zitadel:iam:org:project:%s:roles"
ScopeUserMetaData = "urn:zitadel:iam:user:metadata"
ClaimUserMetaData = ScopeUserMetaData
ScopeResourceOwner = "urn:zitadel:iam:user:resourceowner"
ClaimResourceOwnerID = ScopeResourceOwner + ":id"
ClaimResourceOwnerName = ScopeResourceOwner + ":name"
ClaimResourceOwnerPrimaryDomain = ScopeResourceOwner + ":primary_domain"
ClaimActionLogFormat = "urn:zitadel:iam:action:%s:log"
oidcCtx = "oidc"
)
@ -868,9 +870,9 @@ func (o *OPStorage) assertUserResourceOwner(ctx context.Context, userID string)
return nil, err
}
return map[string]string{
ClaimResourceOwner + "id": resourceOwner.ID,
ClaimResourceOwner + "name": resourceOwner.Name,
ClaimResourceOwner + "primary_domain": resourceOwner.Domain,
ClaimResourceOwnerID: resourceOwner.ID,
ClaimResourceOwnerName: resourceOwner.Name,
ClaimResourceOwnerPrimaryDomain: resourceOwner.Domain,
}, nil
}

View File

@ -215,6 +215,8 @@ func grantTypeToOIDC(grantType domain.OIDCGrantType) oidc.GrantType {
return oidc.GrantTypeRefreshToken
case domain.OIDCGrantTypeDeviceCode:
return oidc.GrantTypeDeviceCode
case domain.OIDCGrantTypeTokenExchange:
return oidc.GrantTypeTokenExchange
default:
return oidc.GrantTypeCode
}

View File

@ -213,9 +213,9 @@ func assertIntrospection(
assertOIDCTime(t, introspection.UpdatedAt, User.GetDetails().GetChangeDate().AsTime())
require.NotNil(t, introspection.Claims)
assert.Equal(t, User.Details.ResourceOwner, introspection.Claims[oidc_api.ClaimResourceOwner+"id"])
assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"name"])
assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwner+"primary_domain"])
assert.Equal(t, User.Details.ResourceOwner, introspection.Claims[oidc_api.ClaimResourceOwnerID])
assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwnerName])
assert.NotEmpty(t, introspection.Claims[oidc_api.ClaimResourceOwnerPrimaryDomain])
}
// TestServer_VerifyClient tests verification by running code flow tests

View File

@ -106,16 +106,19 @@ func (s *Server) Introspect(ctx context.Context, r *op.Request[op.IntrospectionR
return nil, err
}
introspectionResp := &oidc.IntrospectionResponse{
Active: true,
Scope: token.scope,
ClientID: token.clientID,
TokenType: oidc.BearerToken,
Expiration: oidc.FromTime(token.tokenExpiration),
IssuedAt: oidc.FromTime(token.tokenCreation),
NotBefore: oidc.FromTime(token.tokenCreation),
Audience: token.audience,
Issuer: op.IssuerFromContext(ctx),
JWTID: token.tokenID,
Active: true,
Scope: token.scope,
ClientID: token.clientID,
TokenType: oidc.BearerToken,
Expiration: oidc.FromTime(token.tokenExpiration),
IssuedAt: oidc.FromTime(token.tokenCreation),
AuthTime: oidc.FromTime(token.authTime),
NotBefore: oidc.FromTime(token.tokenCreation),
Audience: token.audience,
AuthenticationMethodsReferences: AuthMethodTypesToAMR(token.authMethods),
Issuer: op.IssuerFromContext(ctx),
JWTID: token.tokenID,
Actor: actorDomainToClaims(token.actor),
}
introspectionResp.SetUserInfo(userInfo)
return op.NewResponse(introspectionResp), nil

View File

@ -311,7 +311,7 @@ func (o *OPStorage) SigningKey(ctx context.Context) (key op.SigningKey, err erro
return err
}
if key == nil {
return zerrors.ThrowInternal(nil, "test", "test")
return zerrors.ThrowNotFound(nil, "OIDC-ve4Qu", "Errors.Internal")
}
return nil
})

View File

@ -174,13 +174,6 @@ func (s *Server) JWTProfile(ctx context.Context, r *op.Request[oidc.JWTProfileGr
return s.LegacyServer.JWTProfile(ctx, r)
}
func (s *Server) TokenExchange(ctx context.Context, r *op.ClientRequest[oidc.TokenExchangeRequest]) (_ *op.Response, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return s.LegacyServer.TokenExchange(ctx, r)
}
func (s *Server) ClientCredentialsExchange(ctx context.Context, r *op.ClientRequest[oidc.ClientCredentialsRequest]) (_ *op.Response, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()

View File

@ -0,0 +1,347 @@
package oidc
import (
"context"
"slices"
"time"
"github.com/zitadel/oidc/v3/pkg/crypto"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/oidc/v3/pkg/op"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
const (
UserIDTokenType oidc.TokenType = "urn:zitadel:params:oauth:token-type:user_id"
// TokenTypeNA is set when the returned Token Exchange access token value can't be used as an access token.
// For example, when it is an ID Token.
// See [RFC 8693, section 2.2.1, token_type](https://www.rfc-editor.org/rfc/rfc8693#section-2.2.1)
TokenTypeNA = "N_A"
)
func init() {
oidc.AllTokenTypes = append(oidc.AllTokenTypes, UserIDTokenType)
}
func (s *Server) TokenExchange(ctx context.Context, r *op.ClientRequest[oidc.TokenExchangeRequest]) (_ *op.Response, err error) {
resp, err := s.tokenExchange(ctx, r)
if err != nil {
return nil, oidcError(err)
}
return resp, nil
}
func (s *Server) tokenExchange(ctx context.Context, r *op.ClientRequest[oidc.TokenExchangeRequest]) (_ *op.Response, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
if !authz.GetFeatures(ctx).TokenExchange {
return nil, zerrors.ThrowPreconditionFailed(nil, "OIDC-oan4I", "Errors.TokenExchange.FeatureDisabled")
}
if len(r.Data.Resource) > 0 {
return nil, oidc.ErrInvalidTarget().WithDescription("resource parameter not supported")
}
client, ok := r.Client.(*Client)
if !ok {
// not supposed to happen, but just preventing a panic if it does.
return nil, zerrors.ThrowInternal(nil, "OIDC-eShi5", "Error.Internal")
}
subjectToken, err := s.verifyExchangeToken(ctx, client, r.Data.SubjectToken, r.Data.SubjectTokenType, oidc.AllTokenTypes...)
if err != nil {
return nil, oidc.ErrInvalidRequest().WithParent(err).WithDescription("subject_token invalid")
}
actorToken := subjectToken // see [createExchangeTokens] comment.
if subjectToken.tokenType == UserIDTokenType || subjectToken.tokenType == oidc.JWTTokenType || r.Data.ActorToken != "" {
if !authz.GetInstance(ctx).EnableImpersonation() {
return nil, zerrors.ThrowPermissionDenied(nil, "OIDC-Fae5w", "Errors.TokenExchange.Impersonation.PolicyDisabled")
}
actorToken, err = s.verifyExchangeToken(ctx, client, r.Data.ActorToken, r.Data.ActorTokenType, oidc.AccessTokenType, oidc.IDTokenType, oidc.RefreshTokenType)
if err != nil {
return nil, oidc.ErrInvalidRequest().WithParent(err).WithDescription("actor_token invalid")
}
ctx = authz.SetCtxData(ctx, authz.CtxData{
UserID: actorToken.userID,
OrgID: actorToken.resourceOwner,
})
}
audience, err := validateTokenExchangeAudience(r.Data.Audience, subjectToken.audience, actorToken.audience)
if err != nil {
return nil, err
}
scopes, err := validateTokenExchangeScopes(client, r.Data.Scopes, subjectToken.scopes, actorToken.scopes)
if err != nil {
return nil, err
}
resp, err := s.createExchangeTokens(ctx, r.Data.RequestedTokenType, client, subjectToken, actorToken, audience, scopes)
if err != nil {
return nil, err
}
return op.NewResponse(resp), nil
}
// verifyExchangeToken verifies the passed token based on the token type. It is safe to pass both from the request as-is.
// A list of allowed token types must be passed to determine which types are trusted at a particular stage of the token exchange.
func (s *Server) verifyExchangeToken(ctx context.Context, client *Client, token string, tokenType oidc.TokenType, allowed ...oidc.TokenType) (*exchangeToken, error) {
if token == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "OIDC-lei0O", "Errors.TokenExchange.Token.Missing")
}
if tokenType == "" {
return nil, zerrors.ThrowInvalidArgument(nil, "OIDC-sei9V", "Errors.TokenExchange.Token.TypeMissing")
}
if !slices.Contains(allowed, tokenType) {
return nil, zerrors.ThrowInvalidArgument(nil, "OIDC-OZ1ie", "Errors.TokenExchange.Token.TypeNotAllowed")
}
switch tokenType {
case oidc.AccessTokenType:
token, err := s.verifyAccessToken(ctx, token)
if err != nil {
return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Osh3t", "Errors.TokenExchange.Token.Invalid")
}
if token.isPAT {
if err = s.assertClientScopesForPAT(ctx, token, client.GetID(), client.client.ProjectID); err != nil {
return nil, err
}
}
return accessToExchangeToken(token, op.IssuerFromContext(ctx)), nil
case oidc.IDTokenType:
verifier := op.NewIDTokenHintVerifier(op.IssuerFromContext(ctx), s.idTokenHintKeySet)
claims, err := op.VerifyIDTokenHint[*oidc.IDTokenClaims](ctx, token, verifier)
if err != nil {
return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Rei0f", "Errors.TokenExchange.Token.Invalid")
}
resourceOwner, ok := claims.Claims[ClaimResourceOwnerID].(string)
if !ok || resourceOwner == "" {
user, err := s.query.GetUserByID(ctx, false, token)
if err != nil {
return nil, zerrors.ThrowPermissionDenied(err, "OIDC-aD0Oo", "Errors.TokenExchange.Token.Invalid")
}
resourceOwner = user.ResourceOwner
}
return idTokenClaimsToExchangeToken(claims, resourceOwner), nil
case oidc.JWTTokenType:
resourceOwner := new(string)
verifier := op.NewJWTProfileVerifierKeySet(keySetMap(client.client.PublicKeys), op.IssuerFromContext(ctx), time.Hour, client.client.ClockSkew, s.jwtProfileUserCheck(ctx, resourceOwner))
jwt, err := op.VerifyJWTAssertion(ctx, token, verifier)
if err != nil {
return nil, zerrors.ThrowPermissionDenied(err, "OIDC-eiS6o", "Errors.TokenExchange.Token.Invalid")
}
return jwtToExchangeToken(jwt, *resourceOwner), nil
case UserIDTokenType:
user, err := s.query.GetUserByID(ctx, false, token)
if err != nil {
return nil, zerrors.ThrowPermissionDenied(err, "OIDC-Nee6r", "Errors.TokenExchange.Token.Invalid")
}
return userToExchangeToken(user), nil
case oidc.RefreshTokenType:
fallthrough
default:
return nil, zerrors.ThrowInvalidArgument(nil, "OIDC-oda4R", "Errors.TokenExchange.Token.TypeNotSupported")
}
}
func (s *Server) jwtProfileUserCheck(ctx context.Context, resourceOwner *string) op.JWTProfileVerifierOption {
return op.SubjectCheck(func(request *oidc.JWTTokenRequest) error {
user, err := s.query.GetUserByID(ctx, false, request.Subject)
if err != nil {
return zerrors.ThrowPermissionDenied(err, "OIDC-Nee6r", "Errors.TokenExchange.Token.Invalid")
}
*resourceOwner = user.ResourceOwner
return nil
})
}
func validateTokenExchangeScopes(client *Client, requestedScopes, subjectScopes, actorScopes []string) ([]string, error) {
// Scope always has 1 empty string is the space delimited array was an empty string.
scopes := slices.DeleteFunc(requestedScopes, func(s string) bool {
return s == ""
})
if len(scopes) == 0 {
scopes = subjectScopes
}
if len(scopes) == 0 {
scopes = actorScopes
}
return op.ValidateAuthReqScopes(client, scopes)
}
func validateTokenExchangeAudience(requestedAudience, subjectAudience, actorAudience []string) ([]string, error) {
if len(requestedAudience) == 0 {
if len(subjectAudience) > 0 {
return subjectAudience, nil
}
if len(actorAudience) > 0 {
return actorAudience, nil
}
}
if slices.Equal(requestedAudience, subjectAudience) || slices.Equal(requestedAudience, actorAudience) {
return requestedAudience, nil
}
//nolint:gocritic
allowedAudience := append(subjectAudience, actorAudience...)
for _, a := range requestedAudience {
if !slices.Contains(allowedAudience, a) {
return nil, oidc.ErrInvalidTarget().WithDescription("audience %q not found in subject or actor token", a)
}
}
return requestedAudience, nil
}
// createExchangeTokens prepares the final tokens to be returned to the client.
// The subjectToken is used to set the new token's subject and resource owner.
// The actorToken is used to set the new token's auth time AMR and actor.
// Both tokens may point to the same object (subjectToken) in case of a regular Token Exchange.
// When the subject and actor Tokens point to different objects, the new tokens will be for impersonation / delegation.
func (s *Server) createExchangeTokens(ctx context.Context, tokenType oidc.TokenType, client *Client, subjectToken, actorToken *exchangeToken, audience, scopes []string) (_ *oidc.TokenExchangeResponse, err error) {
var (
userInfo *oidc.UserInfo
signingKey op.SigningKey
)
if slices.Contains(scopes, oidc.ScopeOpenID) || tokenType == oidc.JWTTokenType || tokenType == oidc.IDTokenType {
projectID := client.client.ProjectID
userInfo, err = s.userInfo(ctx, subjectToken.userID, projectID, scopes, []string{projectID})
if err != nil {
return nil, err
}
signingKey, err = s.Provider().Storage().SigningKey(ctx)
if err != nil {
return nil, err
}
}
resp := &oidc.TokenExchangeResponse{
Scopes: scopes,
}
reason := domain.TokenReasonExchange
actor := actorToken.actor
if subjectToken != actorToken {
reason = domain.TokenReasonImpersonation
actor = actorToken.nestedActor()
}
switch tokenType {
case oidc.AccessTokenType, "":
resp.AccessToken, resp.RefreshToken, resp.ExpiresIn, err = s.createExchangeAccessToken(ctx, client, subjectToken.resourceOwner, subjectToken.userID, audience, scopes, actorToken.authMethods, actorToken.authTime, reason, actor)
resp.TokenType = oidc.BearerToken
resp.IssuedTokenType = oidc.AccessTokenType
case oidc.JWTTokenType:
resp.AccessToken, resp.RefreshToken, resp.ExpiresIn, err = s.createExchangeJWT(ctx, signingKey, client, subjectToken.resourceOwner, subjectToken.userID, audience, scopes, actorToken.authMethods, actorToken.authTime, reason, actor, userInfo.Claims)
resp.TokenType = oidc.BearerToken
resp.IssuedTokenType = oidc.JWTTokenType
case oidc.IDTokenType:
resp.AccessToken, resp.ExpiresIn, err = s.createExchangeIDToken(ctx, signingKey, client, subjectToken.userID, "", audience, userInfo, actorToken.authMethods, actorToken.authTime, reason, actor)
resp.TokenType = TokenTypeNA
resp.IssuedTokenType = oidc.IDTokenType
case oidc.RefreshTokenType, UserIDTokenType:
fallthrough
default:
err = zerrors.ThrowInvalidArgument(nil, "OIDC-wai5E", "Errors.TokenExchange.Token.TypeNotSupported")
}
if err != nil {
return nil, err
}
if slices.Contains(scopes, oidc.ScopeOpenID) && tokenType != oidc.IDTokenType {
resp.IDToken, _, err = s.createExchangeIDToken(ctx, signingKey, client, subjectToken.userID, resp.AccessToken, audience, userInfo, actorToken.authMethods, actorToken.authTime, reason, actor)
if err != nil {
return nil, err
}
}
return resp, nil
}
func (s *Server) createExchangeAccessToken(ctx context.Context, client *Client, resourceOwner, userID string, audience, scopes []string, authMethods []domain.UserAuthMethodType, authTime time.Time, reason domain.TokenReason, actor *domain.TokenActor) (accessToken string, refreshToken string, exp uint64, err error) {
tokenInfo, refreshToken, err := s.createAccessTokenCommands(ctx, client, resourceOwner, userID, audience, scopes, authMethods, authTime, reason, actor)
if err != nil {
return "", "", 0, err
}
accessToken, err = op.CreateBearerToken(tokenInfo.TokenID, userID, s.Provider().Crypto())
if err != nil {
return "", "", 0, err
}
return accessToken, refreshToken, timeToOIDCExpiresIn(tokenInfo.Expiration), nil
}
func (s *Server) createExchangeJWT(ctx context.Context, signingKey op.SigningKey, client *Client, resourceOwner, userID string, audience, scopes []string, authMethods []domain.UserAuthMethodType, authTime time.Time, reason domain.TokenReason, actor *domain.TokenActor, privateClaims map[string]any) (accessToken string, refreshToken string, exp uint64, err error) {
tokenInfo, refreshToken, err := s.createAccessTokenCommands(ctx, client, resourceOwner, userID, audience, scopes, authMethods, authTime, reason, actor)
if err != nil {
return "", "", 0, err
}
expTime := tokenInfo.Expiration.Add(client.ClockSkew())
claims := oidc.NewAccessTokenClaims(op.IssuerFromContext(ctx), userID, tokenInfo.Audience, expTime, tokenInfo.TokenID, client.GetID(), client.ClockSkew())
claims.Actor = actorDomainToClaims(tokenInfo.Actor)
claims.Claims = privateClaims
signer, err := op.SignerFromKey(signingKey)
if err != nil {
return "", "", 0, err
}
accessToken, err = crypto.Sign(claims, signer)
if err != nil {
return "", "", 0, nil
}
return accessToken, refreshToken, timeToOIDCExpiresIn(expTime), nil
}
func (s *Server) createExchangeIDToken(ctx context.Context, signingKey op.SigningKey, client *Client, userID, accessToken string, audience []string, userInfo *oidc.UserInfo, authMethods []domain.UserAuthMethodType, authTime time.Time, reason domain.TokenReason, actor *domain.TokenActor) (idToken string, exp uint64, err error) {
expTime := time.Now().Add(client.IDTokenLifetime()).Add(client.ClockSkew())
claims := oidc.NewIDTokenClaims(op.IssuerFromContext(ctx), userID, audience, expTime, authTime, "", "", AuthMethodTypesToAMR(authMethods), client.GetID(), client.ClockSkew())
claims.Actor = actorDomainToClaims(actor)
claims.SetUserInfo(userInfo)
if accessToken != "" {
claims.AccessTokenHash, err = oidc.ClaimHash(accessToken, signingKey.SignatureAlgorithm())
if err != nil {
return "", 0, err
}
}
signer, err := op.SignerFromKey(signingKey)
if err != nil {
return "", 0, err
}
idToken, err = crypto.Sign(claims, signer)
return idToken, timeToOIDCExpiresIn(expTime), err
}
func timeToOIDCExpiresIn(exp time.Time) uint64 {
return uint64(time.Until(exp) / time.Second)
}
func (s *Server) createAccessTokenCommands(ctx context.Context, client *Client, resourceOwner, userID string, audience, scopes []string, authMethods []domain.UserAuthMethodType, authTime time.Time, reason domain.TokenReason, actor *domain.TokenActor) (tokenInfo *domain.Token, refreshToken string, err error) {
settings := client.client.Settings
if slices.Contains(scopes, oidc.ScopeOfflineAccess) {
return s.command.AddAccessAndRefreshToken(
ctx, resourceOwner, "", client.GetID(), userID, "", audience, scopes, AuthMethodTypesToAMR(authMethods),
settings.AccessTokenLifetime, settings.RefreshTokenIdleExpiration, settings.RefreshTokenExpiration,
authTime, reason, actor,
)
}
tokenInfo, err = s.command.AddUserToken(
ctx, resourceOwner, "", client.GetID(), userID, audience, scopes, AuthMethodTypesToAMR(authMethods),
settings.AccessTokenLifetime,
authTime, reason, actor,
)
return tokenInfo, "", err
}

View File

@ -0,0 +1,98 @@
package oidc
import (
"time"
"github.com/zitadel/oidc/v3/pkg/oidc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
)
type exchangeToken struct {
tokenType oidc.TokenType
userID string
issuer string
resourceOwner string
authTime time.Time
authMethods []domain.UserAuthMethodType
actor *domain.TokenActor
audience []string
scopes []string
}
func (et *exchangeToken) nestedActor() *domain.TokenActor {
return &domain.TokenActor{
Actor: et.actor,
UserID: et.userID,
Issuer: et.issuer,
}
}
func accessToExchangeToken(token *accessToken, issuer string) *exchangeToken {
return &exchangeToken{
tokenType: oidc.AccessTokenType,
userID: token.userID,
issuer: issuer,
resourceOwner: token.resourceOwner,
authMethods: token.authMethods,
actor: token.actor,
audience: token.audience,
scopes: token.scope,
}
}
func idTokenClaimsToExchangeToken(claims *oidc.IDTokenClaims, resourceOwner string) *exchangeToken {
return &exchangeToken{
tokenType: oidc.IDTokenType,
userID: claims.Subject,
issuer: claims.Issuer,
resourceOwner: resourceOwner,
authTime: claims.GetAuthTime(),
authMethods: AMRToAuthMethodTypes(claims.AuthenticationMethodsReferences),
actor: actorClaimsToDomain(claims.Actor),
audience: claims.Audience,
}
}
func actorClaimsToDomain(actor *oidc.ActorClaims) *domain.TokenActor {
if actor == nil {
return nil
}
return &domain.TokenActor{
Actor: actorClaimsToDomain(actor.Actor),
UserID: actor.Subject,
Issuer: actor.Issuer,
}
}
func actorDomainToClaims(actor *domain.TokenActor) *oidc.ActorClaims {
if actor == nil {
return nil
}
return &oidc.ActorClaims{
Actor: actorDomainToClaims(actor.Actor),
Subject: actor.UserID,
Issuer: actor.Issuer,
}
}
func jwtToExchangeToken(jwt *oidc.JWTTokenRequest, resourceOwner string) *exchangeToken {
return &exchangeToken{
tokenType: oidc.JWTTokenType,
userID: jwt.Subject,
issuer: jwt.Issuer,
resourceOwner: resourceOwner,
scopes: jwt.Scopes,
authTime: jwt.IssuedAt.AsTime(),
// audience omitted as we don't thrust audiences not signed by us
}
}
func userToExchangeToken(user *query.User) *exchangeToken {
return &exchangeToken{
tokenType: UserIDTokenType,
userID: user.ID,
resourceOwner: user.ResourceOwner,
}
}

View File

@ -0,0 +1,590 @@
//go:build integration
package oidc_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/client"
"github.com/zitadel/oidc/v3/pkg/client/rp"
"github.com/zitadel/oidc/v3/pkg/client/rs"
"github.com/zitadel/oidc/v3/pkg/client/tokenexchange"
"github.com/zitadel/oidc/v3/pkg/crypto"
"github.com/zitadel/oidc/v3/pkg/oidc"
oidc_api "github.com/zitadel/zitadel/internal/api/oidc"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/pkg/grpc/admin"
feature "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
)
func setTokenExchangeFeature(t *testing.T, value bool) {
iamCTX := Tester.WithAuthorization(CTX, integration.IAMOwner)
_, err := Tester.Client.FeatureV2.SetInstanceFeatures(iamCTX, &feature.SetInstanceFeaturesRequest{
OidcTokenExchange: proto.Bool(value),
})
require.NoError(t, err)
time.Sleep(time.Second)
}
func resetFeatures(t *testing.T) {
iamCTX := Tester.WithAuthorization(CTX, integration.IAMOwner)
_, err := Tester.Client.FeatureV2.ResetInstanceFeatures(iamCTX, &feature.ResetInstanceFeaturesRequest{})
require.NoError(t, err)
time.Sleep(time.Second)
}
func setImpersonationPolicy(t *testing.T, value bool) {
iamCTX := Tester.WithAuthorization(CTX, integration.IAMOwner)
policy, err := Tester.Client.Admin.GetSecurityPolicy(iamCTX, &admin.GetSecurityPolicyRequest{})
require.NoError(t, err)
if policy.GetPolicy().GetEnableImpersonation() != value {
_, err = Tester.Client.Admin.SetSecurityPolicy(iamCTX, &admin.SetSecurityPolicyRequest{
EnableImpersonation: value,
})
require.NoError(t, err)
}
time.Sleep(time.Second)
}
func createMachineUserPATWithMembership(t *testing.T, roles ...string) (userID, pat string) {
iamCTX := Tester.WithAuthorization(CTX, integration.IAMOwner)
userID, pat, err := Tester.CreateMachineUserPATWithMembership(iamCTX, roles...)
require.NoError(t, err)
return userID, pat
}
func accessTokenVerifier(ctx context.Context, server rs.ResourceServer, subject, actorSubject string) func(t *testing.T, token string) {
return func(t *testing.T, token string) {
resp, err := rs.Introspect[*oidc.IntrospectionResponse](ctx, server, token)
require.NoError(t, err)
assert.True(t, resp.Active)
if subject != "" {
assert.Equal(t, subject, resp.Subject)
}
if actorSubject != "" {
require.NotNil(t, resp.Actor)
assert.Equal(t, actorSubject, resp.Actor.Subject)
}
}
}
func idTokenVerifier(ctx context.Context, provider rp.RelyingParty, subject, actorSubject string) func(t *testing.T, token string) {
return func(t *testing.T, token string) {
verifier := provider.IDTokenVerifier()
resp, err := rp.VerifyIDToken[*oidc.IDTokenClaims](ctx, token, verifier)
require.NoError(t, err)
if subject != "" {
assert.Equal(t, subject, resp.Subject)
}
if actorSubject != "" {
require.NotNil(t, resp.Actor)
assert.Equal(t, actorSubject, resp.Actor.Subject)
}
}
}
func refreshTokenVerifier(ctx context.Context, provider rp.RelyingParty, subject, actorSubject string) func(t *testing.T, token string) {
return func(t *testing.T, token string) {
clientAssertion, err := client.SignedJWTProfileAssertion(provider.OAuthConfig().ClientID, []string{provider.Issuer()}, time.Hour, provider.Signer())
require.NoError(t, err)
tokens, err := rp.RefreshTokens[*oidc.IDTokenClaims](ctx, provider, token, clientAssertion, oidc.ClientAssertionTypeJWTAssertion)
require.NoError(t, err)
if subject != "" {
assert.Equal(t, subject, tokens.IDTokenClaims.Subject)
}
if actorSubject != "" {
require.NotNil(t, tokens.IDTokenClaims.Actor)
assert.Equal(t, actorSubject, tokens.IDTokenClaims.Actor.Subject)
}
}
}
func TestServer_TokenExchange(t *testing.T) {
t.Cleanup(func() {
resetFeatures(t)
setImpersonationPolicy(t, false)
})
client, keyData, err := Tester.CreateOIDCTokenExchangeClient(CTX)
require.NoError(t, err)
signer, err := rp.SignerFromKeyFile(keyData)()
require.NoError(t, err)
exchanger, err := tokenexchange.NewTokenExchangerJWTProfile(CTX, Tester.OIDCIssuer(), client.GetClientId(), signer)
require.NoError(t, err)
time.Sleep(time.Second)
iamUserID, iamImpersonatorPAT := createMachineUserPATWithMembership(t, "IAM_ADMIN_IMPERSONATOR")
orgUserID, orgImpersonatorPAT := createMachineUserPATWithMembership(t, "ORG_ADMIN_IMPERSONATOR")
serviceUserID, noPermPAT := createMachineUserPATWithMembership(t)
// exchange some tokens for later use
setTokenExchangeFeature(t, true)
teResp, err := tokenexchange.ExchangeToken(CTX, exchanger, noPermPAT, oidc.AccessTokenType, "", "", nil, nil, nil, oidc.AccessTokenType)
require.NoError(t, err)
patScopes := oidc.SpaceDelimitedArray{"openid", "profile", "urn:zitadel:iam:user:metadata", "urn:zitadel:iam:user:resourceowner"}
relyingParty, err := rp.NewRelyingPartyOIDC(CTX, Tester.OIDCIssuer(), client.GetClientId(), "", "", []string{"openid"}, rp.WithJWTProfile(rp.SignerFromKeyFile(keyData)))
require.NoError(t, err)
resourceServer, err := Tester.CreateResourceServerJWTProfile(CTX, keyData)
require.NoError(t, err)
type settings struct {
tokenExchangeFeature bool
impersonationPolicy bool
}
type args struct {
SubjectToken string
SubjectTokenType oidc.TokenType
ActorToken string
ActorTokenType oidc.TokenType
Resource []string
Audience []string
Scopes []string
RequestedTokenType oidc.TokenType
}
type result struct {
issuedTokenType oidc.TokenType
tokenType string
expiresIn uint64
scopes oidc.SpaceDelimitedArray
verifyAccessToken func(t *testing.T, token string)
verifyRefreshToken func(t *testing.T, token string)
verifyIDToken func(t *testing.T, token string)
}
tests := []struct {
name string
settings settings
args args
want result
wantErr bool
}{
{
name: "feature disabled error",
settings: settings{
tokenExchangeFeature: false,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
},
wantErr: true,
},
{
name: "unsupported resource parameter",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
Resource: []string{"https://example.com"},
},
wantErr: true,
},
{
name: "invalid subject token",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: "foo",
SubjectTokenType: oidc.AccessTokenType,
},
wantErr: true,
},
{
name: "EXCHANGE: access token to default",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, serviceUserID, ""),
verifyIDToken: idTokenVerifier(CTX, relyingParty, serviceUserID, ""),
},
},
{
name: "EXCHANGE: access token to access token",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
RequestedTokenType: oidc.AccessTokenType,
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, serviceUserID, ""),
verifyIDToken: idTokenVerifier(CTX, relyingParty, serviceUserID, ""),
},
},
{
name: "EXCHANGE: access token to JWT",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
RequestedTokenType: oidc.JWTTokenType,
},
want: result{
issuedTokenType: oidc.JWTTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, serviceUserID, ""),
verifyIDToken: idTokenVerifier(CTX, relyingParty, serviceUserID, ""),
},
},
{
name: "EXCHANGE: access token to ID Token",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
RequestedTokenType: oidc.IDTokenType,
},
want: result{
issuedTokenType: oidc.IDTokenType,
tokenType: "N_A",
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: idTokenVerifier(CTX, relyingParty, serviceUserID, ""),
verifyIDToken: func(t *testing.T, token string) {
assert.Empty(t, token)
},
},
},
{
name: "EXCHANGE: refresh token not allowed",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: teResp.RefreshToken,
SubjectTokenType: oidc.RefreshTokenType,
RequestedTokenType: oidc.IDTokenType,
},
wantErr: true,
},
{
name: "EXCHANGE: alternate scope for refresh token",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
RequestedTokenType: oidc.AccessTokenType,
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile"},
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess, "profile"},
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, serviceUserID, ""),
verifyIDToken: idTokenVerifier(CTX, relyingParty, serviceUserID, ""),
verifyRefreshToken: refreshTokenVerifier(CTX, relyingParty, "", ""),
},
},
{
name: "EXCHANGE: access token, requested token type not supported error",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
RequestedTokenType: oidc.RefreshTokenType,
},
wantErr: true,
},
{
name: "EXCHANGE: access token, invalid audience",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: noPermPAT,
SubjectTokenType: oidc.AccessTokenType,
RequestedTokenType: oidc.AccessTokenType,
Audience: []string{"foo", "bar"},
},
wantErr: true,
},
{
name: "IMPERSONATION: subject: userID, actor: access token, policy disabled error",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: false,
},
args: args{
SubjectToken: User.GetUserId(),
SubjectTokenType: oidc_api.UserIDTokenType,
RequestedTokenType: oidc.AccessTokenType,
ActorToken: orgImpersonatorPAT,
ActorTokenType: oidc.AccessTokenType,
},
wantErr: true,
},
{
name: "IMPERSONATION: subject: userID, actor: access token, membership not found error",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: true,
},
args: args{
SubjectToken: User.GetUserId(),
SubjectTokenType: oidc_api.UserIDTokenType,
RequestedTokenType: oidc.AccessTokenType,
ActorToken: noPermPAT,
ActorTokenType: oidc.AccessTokenType,
},
wantErr: true,
},
{
name: "IAM IMPERSONATION: subject: userID, actor: access token, success",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: true,
},
args: args{
SubjectToken: User.GetUserId(),
SubjectTokenType: oidc_api.UserIDTokenType,
RequestedTokenType: oidc.AccessTokenType,
ActorToken: iamImpersonatorPAT,
ActorTokenType: oidc.AccessTokenType,
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, User.GetUserId(), iamUserID),
verifyIDToken: idTokenVerifier(CTX, relyingParty, User.GetUserId(), iamUserID),
},
},
{
name: "ORG IMPERSONATION: subject: userID, actor: access token, success",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: true,
},
args: args{
SubjectToken: User.GetUserId(),
SubjectTokenType: oidc_api.UserIDTokenType,
RequestedTokenType: oidc.AccessTokenType,
ActorToken: orgImpersonatorPAT,
ActorTokenType: oidc.AccessTokenType,
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, User.GetUserId(), orgUserID),
verifyIDToken: idTokenVerifier(CTX, relyingParty, User.GetUserId(), orgUserID),
},
},
{
name: "ORG IMPERSONATION: subject: access token, actor: access token, success",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: true,
},
args: args{
SubjectToken: teResp.AccessToken,
SubjectTokenType: oidc.AccessTokenType,
RequestedTokenType: oidc.AccessTokenType,
ActorToken: orgImpersonatorPAT,
ActorTokenType: oidc.AccessTokenType,
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, serviceUserID, orgUserID),
verifyIDToken: idTokenVerifier(CTX, relyingParty, serviceUserID, orgUserID),
},
},
{
name: "ORG IMPERSONATION: subject: ID token, actor: access token, success",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: true,
},
args: args{
SubjectToken: teResp.IDToken,
SubjectTokenType: oidc.IDTokenType,
RequestedTokenType: oidc.AccessTokenType,
ActorToken: orgImpersonatorPAT,
ActorTokenType: oidc.AccessTokenType,
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, serviceUserID, orgUserID),
verifyIDToken: idTokenVerifier(CTX, relyingParty, serviceUserID, orgUserID),
},
},
{
name: "ORG IMPERSONATION: subject: JWT, actor: access token, success",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: true,
},
args: args{
SubjectToken: func() string {
token, err := crypto.Sign(&oidc.JWTTokenRequest{
Issuer: client.GetClientId(),
Subject: User.GetUserId(),
Audience: oidc.Audience{Tester.OIDCIssuer()},
ExpiresAt: oidc.FromTime(time.Now().Add(time.Hour)),
IssuedAt: oidc.FromTime(time.Now().Add(-time.Second)),
}, signer)
require.NoError(t, err)
return token
}(),
SubjectTokenType: oidc.JWTTokenType,
RequestedTokenType: oidc.AccessTokenType,
ActorToken: orgImpersonatorPAT,
ActorTokenType: oidc.AccessTokenType,
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: patScopes,
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, User.GetUserId(), orgUserID),
verifyIDToken: idTokenVerifier(CTX, relyingParty, User.GetUserId(), orgUserID),
},
},
{
name: "ORG IMPERSONATION: subject: access token, actor: access token, with refresh token, success",
settings: settings{
tokenExchangeFeature: true,
impersonationPolicy: true,
},
args: args{
SubjectToken: teResp.AccessToken,
SubjectTokenType: oidc.AccessTokenType,
RequestedTokenType: oidc.AccessTokenType,
ActorToken: orgImpersonatorPAT,
ActorTokenType: oidc.AccessTokenType,
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess},
},
want: result{
issuedTokenType: oidc.AccessTokenType,
tokenType: oidc.BearerToken,
expiresIn: 43100,
scopes: []string{oidc.ScopeOpenID, oidc.ScopeOfflineAccess},
verifyAccessToken: accessTokenVerifier(CTX, resourceServer, serviceUserID, orgUserID),
verifyIDToken: idTokenVerifier(CTX, relyingParty, serviceUserID, orgUserID),
verifyRefreshToken: refreshTokenVerifier(CTX, relyingParty, serviceUserID, orgUserID),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setTokenExchangeFeature(t, tt.settings.tokenExchangeFeature)
setImpersonationPolicy(t, tt.settings.impersonationPolicy)
got, err := tokenexchange.ExchangeToken(CTX, exchanger, tt.args.SubjectToken, tt.args.SubjectTokenType, tt.args.ActorToken, tt.args.ActorTokenType, tt.args.Resource, tt.args.Audience, tt.args.Scopes, tt.args.RequestedTokenType)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want.issuedTokenType, got.IssuedTokenType)
assert.Equal(t, tt.want.tokenType, got.TokenType)
assert.Greater(t, got.ExpiresIn, tt.want.expiresIn)
assert.Equal(t, tt.want.scopes, got.Scopes)
if tt.want.verifyAccessToken != nil {
tt.want.verifyAccessToken(t, got.AccessToken)
}
if tt.want.verifyRefreshToken != nil {
tt.want.verifyRefreshToken(t, got.RefreshToken)
}
if tt.want.verifyIDToken != nil {
tt.want.verifyIDToken(t, got.IDToken)
}
})
}
}
// This test tries to call the zitadel API with an impersonated token,
// which should fail.
func TestImpersonation_API_Call(t *testing.T) {
client, keyData, err := Tester.CreateOIDCTokenExchangeClient(CTX)
require.NoError(t, err)
signer, err := rp.SignerFromKeyFile(keyData)()
require.NoError(t, err)
exchanger, err := tokenexchange.NewTokenExchangerJWTProfile(CTX, Tester.OIDCIssuer(), client.GetClientId(), signer)
require.NoError(t, err)
resourceServer, err := Tester.CreateResourceServerJWTProfile(CTX, keyData)
require.NoError(t, err)
setTokenExchangeFeature(t, true)
setImpersonationPolicy(t, true)
t.Cleanup(func() {
resetFeatures(t)
setImpersonationPolicy(t, false)
})
iamUserID, iamImpersonatorPAT := createMachineUserPATWithMembership(t, "IAM_ADMIN_IMPERSONATOR")
iamOwner := Tester.Users.Get(integration.FirstInstanceUsersKey, integration.IAMOwner)
// impersonating the IAM owner!
resp, err := tokenexchange.ExchangeToken(CTX, exchanger, iamOwner.Token, oidc.AccessTokenType, iamImpersonatorPAT, oidc.AccessTokenType, nil, nil, nil, oidc.AccessTokenType)
require.NoError(t, err)
accessTokenVerifier(CTX, resourceServer, iamOwner.ID, iamUserID)
impersonatedCTX := Tester.WithAuthorizationToken(CTX, resp.AccessToken)
_, err = Tester.Client.Admin.GetAllowedLanguages(impersonatedCTX, &admin.GetAllowedLanguagesRequest{})
status := status.Convert(err)
assert.Equal(t, codes.PermissionDenied, status.Code())
assert.Equal(t, "Errors.TokenExchange.Token.NotForAPI (APP-wai8O)", status.Message())
}

View File

@ -29,7 +29,7 @@ func (s *Server) userInfo(ctx context.Context, userID, projectID string, scope,
return userInfo, s.userinfoFlows(ctx, qu, userInfo)
}
// prepareRoles scans the requested scopes, appends to roleAudiendce and returns the requestedRoles.
// prepareRoles scans the requested scopes, appends to roleAudience and returns the requestedRoles.
//
// When [ScopeProjectsRoles] is present and roleAudience was empty,
// project IDs with the [domain.ProjectIDScope] prefix are added to the roleAudience.
@ -153,9 +153,9 @@ func setUserInfoMetadata(metadata []query.UserMetadata, out *oidc.UserInfo) {
func setUserInfoOrgClaims(user *query.OIDCUserInfo, out *oidc.UserInfo) {
if org := user.Org; org != nil {
out.AppendClaims(ClaimResourceOwner+"id", org.ID)
out.AppendClaims(ClaimResourceOwner+"name", org.Name)
out.AppendClaims(ClaimResourceOwner+"primary_domain", org.PrimaryDomain)
out.AppendClaims(ClaimResourceOwnerID, org.ID)
out.AppendClaims(ClaimResourceOwnerName, org.Name)
out.AppendClaims(ClaimResourceOwnerPrimaryDomain, org.PrimaryDomain)
}
}

View File

@ -349,9 +349,9 @@ func Test_userInfoToOIDC(t *testing.T) {
},
want: &oidc.UserInfo{
Claims: map[string]any{
ClaimResourceOwner + "id": "orgID",
ClaimResourceOwner + "name": "orgName",
ClaimResourceOwner + "primary_domain": "orgDomain",
ClaimResourceOwnerID: "orgID",
ClaimResourceOwnerName: "orgName",
ClaimResourceOwnerPrimaryDomain: "orgDomain",
},
},
},
@ -377,10 +377,10 @@ func Test_userInfoToOIDC(t *testing.T) {
},
want: &oidc.UserInfo{
Claims: map[string]any{
domain.OrgIDClaim: "orgID",
ClaimResourceOwner + "id": "orgID",
ClaimResourceOwner + "name": "orgName",
ClaimResourceOwner + "primary_domain": "orgDomain",
domain.OrgIDClaim: "orgID",
ClaimResourceOwnerID: "orgID",
ClaimResourceOwnerName: "orgName",
ClaimResourceOwnerPrimaryDomain: "orgDomain",
},
},
},

View File

@ -112,6 +112,9 @@ func (repo *TokenVerifierRepo) verifyAccessTokenV1(ctx context.Context, tokenID,
_, tokenSpan := tracing.NewNamedSpan(ctx, "token")
token, err := repo.tokenByID(ctx, tokenID, subject)
if token.Actor != nil {
return "", "", "", "", "", zerrors.ThrowPermissionDenied(nil, "APP-wai8O", "Errors.TokenExchange.Token.NotForAPI")
}
tokenSpan.EndWithError(err)
if err != nil {
return "", "", "", "", "", zerrors.ThrowUnauthenticated(err, "APP-BxUSiL", "invalid token")
@ -136,6 +139,9 @@ func (repo *TokenVerifierRepo) verifyAccessTokenV2(ctx context.Context, token, v
if err != nil {
return "", "", "", err
}
if activeToken.Actor != nil {
return "", "", "", zerrors.ThrowPermissionDenied(nil, "APP-Shi0J", "Errors.TokenExchange.Token.NotForAPI")
}
if err = verifyAudience(activeToken.Audience, verifierClientID, projectID); err != nil {
return "", "", "", err
}

View File

@ -16,13 +16,15 @@ type InstanceFeatures struct {
TriggerIntrospectionProjections *bool
LegacyIntrospection *bool
UserSchema *bool
TokenExchange *bool
}
func (m *InstanceFeatures) isEmpty() bool {
return m.LoginDefaultOrg == nil &&
m.TriggerIntrospectionProjections == nil &&
m.LegacyIntrospection == nil &&
m.UserSchema == nil
m.UserSchema == nil &&
m.TokenExchange == nil
}
func (c *Commands) SetInstanceFeatures(ctx context.Context, f *InstanceFeatures) (*domain.ObjectDetails, error) {

View File

@ -55,6 +55,7 @@ func (m *InstanceFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
feature_v2.InstanceLegacyIntrospectionEventType,
feature_v2.InstanceUserSchemaEventType,
feature_v2.InstanceTokenExchangeEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -64,6 +65,7 @@ func (m *InstanceFeaturesWriteModel) reduceReset() {
m.TriggerIntrospectionProjections = nil
m.LegacyIntrospection = nil
m.UserSchema = nil
m.TokenExchange = nil
}
func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[bool]) error {
@ -80,6 +82,8 @@ func (m *InstanceFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEven
m.TriggerIntrospectionProjections = &event.Value
case feature.KeyLegacyIntrospection:
m.LegacyIntrospection = &event.Value
case feature.KeyTokenExchange:
m.TokenExchange = &event.Value
case feature.KeyUserSchema:
m.UserSchema = &event.Value
}
@ -92,6 +96,7 @@ func (wm *InstanceFeaturesWriteModel) setCommands(ctx context.Context, f *Instan
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LoginDefaultOrg, f.LoginDefaultOrg, feature_v2.InstanceLoginDefaultOrgEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.InstanceTriggerIntrospectionProjectionsEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.InstanceLegacyIntrospectionEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.InstanceTokenExchangeEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.InstanceUserSchemaEventType)
return cmds
}

View File

@ -35,6 +35,9 @@ func (wm *InstanceSecurityPolicyWriteModel) Reduce() error {
if e.AllowedOrigins != nil {
wm.AllowedOrigins = *e.AllowedOrigins
}
if e.EnableImpersonation != nil {
wm.EnableImpersonation = *e.EnableImpersonation
}
}
}
return wm.WriteModel.Reduce()

View File

@ -23,6 +23,7 @@ import (
type expect func(mockRepository *mock.MockRepository)
// Deprecated: use expectEventstore
func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
m := mock.NewRepo(t)
for _, e := range expects {
@ -37,6 +38,9 @@ func eventstoreExpect(t *testing.T, expects ...expect) *eventstore.Eventstore {
return es
}
// expectEventstore defines expectations for the Eventstore and is initialized within the scope of a (sub) test.
// This allows proper reporting of the test name, instead of reporting on the top-level
// of the Test function being run.
func expectEventstore(expects ...expect) func(*testing.T) *eventstore.Eventstore {
return func(t *testing.T) *eventstore.Eventstore {
return eventstoreExpect(t, expects...)

View File

@ -11,6 +11,7 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/id"
"github.com/zitadel/zitadel/internal/repository/authrequest"
@ -35,7 +36,7 @@ func (c *Commands) AddOIDCSessionAccessToken(ctx context.Context, authRequestID
return "", time.Time{}, err
}
cmd.AddSession(ctx)
if err = cmd.AddAccessToken(ctx, cmd.authRequestWriteModel.Scope); err != nil {
if err = cmd.AddAccessToken(ctx, cmd.authRequestWriteModel.Scope, domain.TokenReasonAuthRequest, nil); err != nil {
return "", time.Time{}, err
}
cmd.SetAuthRequestSuccessful(ctx)
@ -52,7 +53,7 @@ func (c *Commands) AddOIDCSessionRefreshAndAccessToken(ctx context.Context, auth
return "", "", time.Time{}, err
}
cmd.AddSession(ctx)
if err = cmd.AddAccessToken(ctx, cmd.authRequestWriteModel.Scope); err != nil {
if err = cmd.AddAccessToken(ctx, cmd.authRequestWriteModel.Scope, domain.TokenReasonAuthRequest, nil); err != nil {
return "", "", time.Time{}, err
}
if err = cmd.AddRefreshToken(ctx); err != nil {
@ -69,7 +70,7 @@ func (c *Commands) ExchangeOIDCSessionRefreshAndAccessToken(ctx context.Context,
if err != nil {
return "", "", time.Time{}, err
}
if err = cmd.AddAccessToken(ctx, scope); err != nil {
if err = cmd.AddAccessToken(ctx, scope, domain.TokenReasonRefresh, nil); err != nil {
return "", "", time.Time{}, err
}
if err = cmd.RenewRefreshToken(ctx); err != nil {
@ -290,13 +291,13 @@ func (c *OIDCSessionEvents) SetAuthRequestSuccessful(ctx context.Context) {
c.events = append(c.events, authrequest.NewSucceededEvent(ctx, c.authRequestWriteModel.aggregate))
}
func (c *OIDCSessionEvents) AddAccessToken(ctx context.Context, scope []string) error {
func (c *OIDCSessionEvents) AddAccessToken(ctx context.Context, scope []string, reason domain.TokenReason, actor *domain.TokenActor) error {
accessTokenID, err := c.idGenerator.Next()
if err != nil {
return err
}
c.accessTokenID = AccessTokenPrefix + accessTokenID
c.events = append(c.events, oidcsession.NewAccessTokenAddedEvent(ctx, c.oidcSessionWriteModel.aggregate, c.accessTokenID, scope, c.accessTokenLifetime))
c.events = append(c.events, oidcsession.NewAccessTokenAddedEvent(ctx, c.oidcSessionWriteModel.aggregate, c.accessTokenID, scope, c.accessTokenLifetime, reason, actor))
return nil
}

View File

@ -23,6 +23,8 @@ type OIDCSessionWriteModel struct {
AccessTokenID string
AccessTokenCreation time.Time
AccessTokenExpiration time.Time
AccessTokenReason domain.TokenReason
AccessTokenActor *domain.TokenActor
RefreshTokenID string
RefreshToken string
RefreshTokenExpiration time.Time
@ -100,6 +102,8 @@ func (wm *OIDCSessionWriteModel) reduceAdded(e *oidcsession.AddedEvent) {
func (wm *OIDCSessionWriteModel) reduceAccessTokenAdded(e *oidcsession.AccessTokenAddedEvent) {
wm.AccessTokenID = e.ID
wm.AccessTokenExpiration = e.CreationDate().Add(e.Lifetime)
wm.AccessTokenReason = e.Reason
wm.AccessTokenActor = e.Actor
}
func (wm *OIDCSessionWriteModel) reduceAccessTokenRevoked(e *oidcsession.AccessTokenRevokedEvent) {

View File

@ -196,7 +196,7 @@ func TestCommands_AddOIDCSessionAccessToken(t *testing.T) {
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid"}, time.Hour),
"at_accessTokenID", []string{"openid"}, time.Hour, domain.TokenReasonAuthRequest, nil),
authrequest.NewSucceededEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
),
),
@ -397,7 +397,7 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"userID", "sessionID", "clientID", []string{"audience"}, []string{"openid", "offline_access"}, []domain.UserAuthMethodType{domain.UserAuthMethodTypePassword}, testNow),
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"rt_refreshTokenID", 7*24*time.Hour, 24*time.Hour),
authrequest.NewSucceededEvent(context.Background(), &authrequest.NewAggregate("V2_authRequestID", "instanceID").Aggregate),
@ -441,7 +441,7 @@ func TestCommands_AddOIDCSessionRefreshAndAccessToken(t *testing.T) {
func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
eventstore func(*testing.T) *eventstore.Eventstore
idGenerator id.Generator
defaultAccessTokenLifetime time.Duration
defaultRefreshTokenLifetime time.Duration
@ -469,7 +469,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
{
"invalid refresh token format error",
fields{
eventstore: eventstoreExpect(t),
eventstore: expectEventstore(),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
},
args{
@ -484,7 +484,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
{
"inactive session error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(),
),
keyAlgorithm: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
@ -501,7 +501,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
{
"invalid refresh token error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@ -509,7 +509,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
),
eventFromEventPusher(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
"accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
),
),
@ -527,7 +527,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
{
"expired refresh token error",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusher(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@ -535,7 +535,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
),
eventFromEventPusher(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
eventFromEventPusher(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@ -557,7 +557,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
{
"refresh successful",
fields{
eventstore: eventstoreExpect(t,
eventstore: expectEventstore(
expectFilter(
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@ -565,7 +565,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@ -575,7 +575,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
expectFilter(), // token lifetime
expectPush(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "offline_access"}, time.Hour, domain.TokenReasonRefresh, nil),
oidcsession.NewRefreshTokenRenewedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"rt_refreshTokenID2", 24*time.Hour),
),
@ -602,7 +602,7 @@ func TestCommands_ExchangeOIDCSessionRefreshAndAccessToken(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Commands{
eventstore: tt.fields.eventstore,
eventstore: tt.fields.eventstore(t),
idGenerator: tt.fields.idGenerator,
defaultAccessTokenLifetime: tt.fields.defaultAccessTokenLifetime,
defaultRefreshTokenLifetime: tt.fields.defaultRefreshTokenLifetime,
@ -682,7 +682,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
),
eventFromEventPusher(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
),
),
@ -707,7 +707,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
),
eventFromEventPusher(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
eventFromEventPusher(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@ -736,7 +736,7 @@ func TestCommands_OIDCSessionByRefreshToken(t *testing.T) {
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@ -892,7 +892,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) {
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
@ -969,7 +969,7 @@ func TestCommands_RevokeOIDCSessionToken(t *testing.T) {
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewAccessTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour),
"at_accessTokenID", []string{"openid", "profile", "offline_access"}, time.Hour, domain.TokenReasonAuthRequest, nil),
),
eventFromEventPusherWithCreationDateNow(
oidcsession.NewRefreshTokenAddedEvent(context.Background(), &oidcsession.NewAggregate("V2_oidcSessionID", "org1").Aggregate,

View File

@ -12,6 +12,7 @@ type SystemFeatures struct {
LoginDefaultOrg *bool
TriggerIntrospectionProjections *bool
LegacyIntrospection *bool
TokenExchange *bool
UserSchema *bool
}
@ -19,7 +20,8 @@ func (m *SystemFeatures) isEmpty() bool {
return m.LoginDefaultOrg == nil &&
m.TriggerIntrospectionProjections == nil &&
m.LegacyIntrospection == nil &&
m.UserSchema == nil
m.UserSchema == nil &&
m.TokenExchange == nil
}
func (c *Commands) SetSystemFeatures(ctx context.Context, f *SystemFeatures) (*domain.ObjectDetails, error) {

View File

@ -50,6 +50,7 @@ func (m *SystemFeaturesWriteModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
feature_v2.SystemLegacyIntrospectionEventType,
feature_v2.SystemUserSchemaEventType,
feature_v2.SystemTokenExchangeEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -58,6 +59,7 @@ func (m *SystemFeaturesWriteModel) reduceReset() {
m.LoginDefaultOrg = nil
m.TriggerIntrospectionProjections = nil
m.LegacyIntrospection = nil
m.TokenExchange = nil
m.UserSchema = nil
}
@ -77,6 +79,8 @@ func (m *SystemFeaturesWriteModel) reduceBoolFeature(event *feature_v2.SetEvent[
m.LegacyIntrospection = &event.Value
case feature.KeyUserSchema:
m.UserSchema = &event.Value
case feature.KeyTokenExchange:
m.TokenExchange = &event.Value
}
return nil
}
@ -88,6 +92,7 @@ func (wm *SystemFeaturesWriteModel) setCommands(ctx context.Context, f *SystemFe
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TriggerIntrospectionProjections, f.TriggerIntrospectionProjections, feature_v2.SystemTriggerIntrospectionProjectionsEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.LegacyIntrospection, f.LegacyIntrospection, feature_v2.SystemLegacyIntrospectionEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.UserSchema, f.UserSchema, feature_v2.SystemUserSchemaEventType)
cmds = appendFeatureUpdate(ctx, cmds, aggregate, wm.TokenExchange, f.TokenExchange, feature_v2.SystemTokenExchangeEventType)
return cmds
}

View File

@ -232,16 +232,29 @@ func (c *Commands) RemoveUser(ctx context.Context, userID, resourceOwner string,
return writeModelToObjectDetails(&existingUser.WriteModel), nil
}
func (c *Commands) AddUserToken(ctx context.Context, orgID, agentID, clientID, userID string, audience, scopes []string, lifetime time.Duration) (*domain.Token, error) {
func (c *Commands) AddUserToken(
ctx context.Context,
orgID,
agentID,
clientID,
userID string,
audience,
scopes,
authMethodsReferences []string,
lifetime time.Duration,
authTime time.Time,
reason domain.TokenReason,
actor *domain.TokenActor,
) (*domain.Token, error) {
if userID == "" { //do not check for empty orgID (JWT Profile requests won't provide it, so service user requests fail)
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Dbge4", "Errors.IDMissing")
}
userWriteModel := NewUserWriteModel(userID, orgID)
event, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, "", audience, scopes, lifetime)
cmds, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, "", audience, scopes, authMethodsReferences, lifetime, authTime, reason, actor)
if err != nil {
return nil, err
}
_, err = c.eventstore.Push(ctx, event)
_, err = c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, err
}
@ -264,7 +277,7 @@ func (c *Commands) RevokeAccessToken(ctx context.Context, userID, orgID, tokenID
return writeModelToObjectDetails(&accessTokenWriteModel.WriteModel), nil
}
func (c *Commands) addUserToken(ctx context.Context, userWriteModel *UserWriteModel, agentID, clientID, refreshTokenID string, audience, scopes []string, lifetime time.Duration) (*user.UserTokenAddedEvent, *domain.Token, error) {
func (c *Commands) addUserToken(ctx context.Context, userWriteModel *UserWriteModel, agentID, clientID, refreshTokenID string, audience, scopes, authMethodsReferences []string, lifetime time.Duration, authTime time.Time, reason domain.TokenReason, actor *domain.TokenActor) ([]eventstore.Command, *domain.Token, error) {
err := c.eventstore.FilterToQueryReducer(ctx, userWriteModel)
if err != nil {
return nil, nil, err
@ -273,10 +286,24 @@ func (c *Commands) addUserToken(ctx context.Context, userWriteModel *UserWriteMo
return nil, nil, zerrors.ThrowNotFound(nil, "COMMAND-1d6Gg", "Errors.User.NotFound")
}
//nolint:contextcheck
userAgg := UserAggregateFromWriteModel(&userWriteModel.WriteModel)
var cmds []eventstore.Command
if reason == domain.TokenReasonImpersonation {
if err := c.checkPermission(ctx, "impersonation", userWriteModel.ResourceOwner, userWriteModel.AggregateID); err != nil {
return nil, nil, err
}
cmds = append(cmds, user.NewUserImpersonatedEvent(ctx, userAgg, clientID, actor))
}
audience = domain.AddAudScopeToAudience(ctx, audience, scopes)
preferredLanguage := ""
existingHuman, err := c.getHumanWriteModelByID(ctx, userWriteModel.AggregateID, userWriteModel.ResourceOwner)
if err != nil {
return nil, nil, err
}
if existingHuman != nil {
preferredLanguage = existingHuman.PreferredLanguage.String()
}
@ -286,21 +313,25 @@ func (c *Commands) addUserToken(ctx context.Context, userWriteModel *UserWriteMo
return nil, nil, err
}
userAgg := UserAggregateFromWriteModel(&userWriteModel.WriteModel)
return user.NewUserTokenAddedEvent(ctx, userAgg, tokenID, clientID, agentID, preferredLanguage, refreshTokenID, audience, scopes, expiration),
&domain.Token{
ObjectRoot: models.ObjectRoot{
AggregateID: userWriteModel.AggregateID,
},
TokenID: tokenID,
UserAgentID: agentID,
ApplicationID: clientID,
RefreshTokenID: refreshTokenID,
Audience: audience,
Scopes: scopes,
Expiration: expiration,
PreferredLanguage: preferredLanguage,
}, nil
cmds = append(cmds,
user.NewUserTokenAddedEvent(ctx, userAgg, tokenID, clientID, agentID, preferredLanguage, refreshTokenID, audience, scopes, authMethodsReferences, authTime, expiration, reason, actor),
)
return cmds, &domain.Token{
ObjectRoot: models.ObjectRoot{
AggregateID: userWriteModel.AggregateID,
},
TokenID: tokenID,
UserAgentID: agentID,
ApplicationID: clientID,
RefreshTokenID: refreshTokenID,
Audience: audience,
Scopes: scopes,
Expiration: expiration,
PreferredLanguage: preferredLanguage,
Reason: reason,
Actor: actor,
}, nil
}
func (c *Commands) removeAccessToken(ctx context.Context, userID, orgID, tokenID string) (*user.UserTokenRemovedEvent, *UserAccessTokenWriteModel, error) {

View File

@ -19,6 +19,8 @@ type UserAccessTokenWriteModel struct {
Scopes []string
Expiration time.Time
PreferredLanguage string
Reason domain.TokenReason
Actor *domain.TokenActor
UserState domain.UserState
}
@ -71,6 +73,8 @@ func (wm *UserAccessTokenWriteModel) Reduce() error {
wm.Expiration = e.Expiration
wm.PreferredLanguage = e.PreferredLanguage
wm.UserState = domain.UserStateActive
wm.Reason = e.Reason
wm.Actor = e.Actor
if e.Expiration.Before(time.Now()) {
wm.UserState = domain.UserStateDeleted
}

View File

@ -24,11 +24,13 @@ func (c *Commands) AddAccessAndRefreshToken(
refreshIdleExpiration,
refreshExpiration time.Duration,
authTime time.Time,
reason domain.TokenReason,
actor *domain.TokenActor,
) (accessToken *domain.Token, newRefreshToken string, err error) {
if refreshToken == "" {
return c.AddNewRefreshTokenAndAccessToken(ctx, userID, orgID, agentID, clientID, audience, scopes, authMethodsReferences, refreshExpiration, accessLifetime, refreshIdleExpiration, authTime)
return c.AddNewRefreshTokenAndAccessToken(ctx, userID, orgID, agentID, clientID, audience, scopes, authMethodsReferences, refreshExpiration, accessLifetime, refreshIdleExpiration, authTime, reason, actor)
}
return c.RenewRefreshTokenAndAccessToken(ctx, userID, orgID, refreshToken, agentID, clientID, audience, scopes, refreshIdleExpiration, accessLifetime)
return c.RenewRefreshTokenAndAccessToken(ctx, userID, orgID, refreshToken, agentID, clientID, audience, scopes, refreshIdleExpiration, accessLifetime, actor)
}
func (c *Commands) AddNewRefreshTokenAndAccessToken(
@ -44,6 +46,8 @@ func (c *Commands) AddNewRefreshTokenAndAccessToken(
accessLifetime,
refreshIdleExpiration time.Duration,
authTime time.Time,
reason domain.TokenReason,
actor *domain.TokenActor,
) (accessToken *domain.Token, newRefreshToken string, err error) {
if userID == "" || clientID == "" {
return nil, "", zerrors.ThrowInvalidArgument(nil, "COMMAND-adg4r", "Errors.IDMissing")
@ -53,15 +57,16 @@ func (c *Commands) AddNewRefreshTokenAndAccessToken(
if err != nil {
return nil, "", err
}
accessTokenEvent, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, refreshTokenID, audience, scopes, accessLifetime)
cmds, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, refreshTokenID, audience, scopes, authMethodsReferences, accessLifetime, authTime, reason, actor)
if err != nil {
return nil, "", err
}
refreshTokenEvent, newRefreshToken, err := c.addRefreshToken(ctx, accessToken, authMethodsReferences, authTime, refreshIdleExpiration, refreshExpiration)
refreshTokenEvent, newRefreshToken, err := c.addRefreshToken(ctx, accessToken, authMethodsReferences, authTime, refreshIdleExpiration, refreshExpiration, actor)
if err != nil {
return nil, "", err
}
_, err = c.eventstore.Push(ctx, accessTokenEvent, refreshTokenEvent)
cmds = append(cmds, refreshTokenEvent)
_, err = c.eventstore.Push(ctx, cmds...)
if err != nil {
return nil, "", err
}
@ -79,17 +84,18 @@ func (c *Commands) RenewRefreshTokenAndAccessToken(
scopes []string,
idleExpiration,
accessLifetime time.Duration,
actor *domain.TokenActor,
) (accessToken *domain.Token, newRefreshToken string, err error) {
refreshTokenEvent, refreshTokenID, newRefreshToken, err := c.renewRefreshToken(ctx, userID, orgID, refreshToken, idleExpiration)
renewed, err := c.renewRefreshToken(ctx, userID, orgID, refreshToken, idleExpiration)
if err != nil {
return nil, "", err
}
userWriteModel := NewUserWriteModel(userID, orgID)
accessTokenEvent, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, refreshTokenID, audience, scopes, accessLifetime)
cmds, accessToken, err := c.addUserToken(ctx, userWriteModel, agentID, clientID, renewed.tokenID, audience, scopes, renewed.authMethodsReferences, accessLifetime, renewed.authTime, domain.TokenReasonRefresh, actor)
if err != nil {
return nil, "", err
}
_, err = c.eventstore.Push(ctx, accessTokenEvent, refreshTokenEvent)
_, err = c.eventstore.Push(ctx, append(cmds, renewed.event)...)
if err != nil {
return nil, "", err
}
@ -128,7 +134,7 @@ func (c *Commands) RevokeRefreshTokens(ctx context.Context, userID, orgID string
return err
}
func (c *Commands) addRefreshToken(ctx context.Context, accessToken *domain.Token, authMethodsReferences []string, authTime time.Time, idleExpiration, expiration time.Duration) (*user.HumanRefreshTokenAddedEvent, string, error) {
func (c *Commands) addRefreshToken(ctx context.Context, accessToken *domain.Token, authMethodsReferences []string, authTime time.Time, idleExpiration, expiration time.Duration, actor *domain.TokenActor) (*user.HumanRefreshTokenAddedEvent, string, error) {
refreshToken, err := domain.NewRefreshToken(accessToken.AggregateID, accessToken.RefreshTokenID, c.keyAlgorithm)
if err != nil {
return nil, "", err
@ -136,46 +142,60 @@ func (c *Commands) addRefreshToken(ctx context.Context, accessToken *domain.Toke
refreshTokenWriteModel := NewHumanRefreshTokenWriteModel(accessToken.AggregateID, accessToken.ResourceOwner, accessToken.RefreshTokenID)
userAgg := UserAggregateFromWriteModel(&refreshTokenWriteModel.WriteModel)
return user.NewHumanRefreshTokenAddedEvent(ctx, userAgg, accessToken.RefreshTokenID, accessToken.ApplicationID, accessToken.UserAgentID,
accessToken.PreferredLanguage, accessToken.Audience, accessToken.Scopes, authMethodsReferences, authTime, idleExpiration, expiration),
accessToken.PreferredLanguage, accessToken.Audience, accessToken.Scopes, authMethodsReferences, authTime, idleExpiration, expiration, actor),
refreshToken, nil
}
func (c *Commands) renewRefreshToken(ctx context.Context, userID, orgID, refreshToken string, idleExpiration time.Duration) (event *user.HumanRefreshTokenRenewedEvent, refreshTokenID, newRefreshToken string, err error) {
type renewedRefreshToken struct {
event *user.HumanRefreshTokenRenewedEvent
authTime time.Time
authMethodsReferences []string
tokenID string
token string
}
func (c *Commands) renewRefreshToken(ctx context.Context, userID, orgID, refreshToken string, idleExpiration time.Duration) (*renewedRefreshToken, error) {
if refreshToken == "" {
return nil, "", "", zerrors.ThrowInvalidArgument(nil, "COMMAND-DHrr3", "Errors.IDMissing")
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-DHrr3", "Errors.IDMissing")
}
tokenUserID, tokenID, token, err := domain.FromRefreshToken(refreshToken, c.keyAlgorithm)
if err != nil {
return nil, "", "", err
return nil, err
}
if tokenUserID != userID {
return nil, "", "", zerrors.ThrowInvalidArgument(nil, "COMMAND-Ht2g2", "Errors.User.RefreshToken.Invalid")
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Ht2g2", "Errors.User.RefreshToken.Invalid")
}
refreshTokenWriteModel := NewHumanRefreshTokenWriteModel(userID, orgID, tokenID)
err = c.eventstore.FilterToQueryReducer(ctx, refreshTokenWriteModel)
if err != nil {
return nil, "", "", err
return nil, err
}
if refreshTokenWriteModel.UserState != domain.UserStateActive {
return nil, "", "", zerrors.ThrowInvalidArgument(nil, "COMMAND-BHnhs", "Errors.User.RefreshToken.Invalid")
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-BHnhs", "Errors.User.RefreshToken.Invalid")
}
if refreshTokenWriteModel.RefreshToken != token ||
refreshTokenWriteModel.IdleExpiration.Before(time.Now()) ||
refreshTokenWriteModel.Expiration.Before(time.Now()) {
return nil, "", "", zerrors.ThrowInvalidArgument(nil, "COMMAND-Vr43e", "Errors.User.RefreshToken.Invalid")
return nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-Vr43e", "Errors.User.RefreshToken.Invalid")
}
newToken, err := c.idGenerator.Next()
if err != nil {
return nil, "", "", err
return nil, err
}
newRefreshToken, err = domain.RefreshToken(userID, tokenID, newToken, c.keyAlgorithm)
newRefreshToken, err := domain.RefreshToken(userID, tokenID, newToken, c.keyAlgorithm)
if err != nil {
return nil, "", "", err
return nil, err
}
userAgg := UserAggregateFromWriteModel(&refreshTokenWriteModel.WriteModel)
return user.NewHumanRefreshTokenRenewedEvent(ctx, userAgg, tokenID, newToken, idleExpiration), tokenID, newRefreshToken, nil
return &renewedRefreshToken{
event: user.NewHumanRefreshTokenRenewedEvent(ctx, userAgg, tokenID, newToken, idleExpiration),
authTime: refreshTokenWriteModel.AuthTime,
authMethodsReferences: refreshTokenWriteModel.AuthMethodsReferences,
tokenID: tokenID,
token: newRefreshToken,
}, nil
}
func (c *Commands) removeRefreshToken(ctx context.Context, userID, orgID, tokenID string) (*user.HumanRefreshTokenRemovedEvent, *HumanRefreshTokenWriteModel, error) {

View File

@ -15,10 +15,13 @@ type HumanRefreshTokenWriteModel struct {
TokenID string
RefreshToken string
UserState domain.UserState
IdleExpiration time.Time
Expiration time.Time
UserAgentID string
UserState domain.UserState
AuthTime time.Time
IdleExpiration time.Time
Expiration time.Time
UserAgentID string
AuthMethodsReferences []string
Actor *domain.TokenActor
}
func NewHumanRefreshTokenWriteModel(userID, resourceOwner, tokenID string) *HumanRefreshTokenWriteModel {
@ -64,7 +67,10 @@ func (wm *HumanRefreshTokenWriteModel) Reduce() error {
wm.IdleExpiration = e.CreationDate().Add(e.IdleExpiration)
wm.Expiration = e.CreationDate().Add(e.Expiration)
wm.UserState = domain.UserStateActive
wm.AuthTime = e.AuthTime
wm.UserAgentID = e.UserAgentID
wm.AuthMethodsReferences = e.AuthMethodsReferences
wm.Actor = e.Actor
case *user.HumanRefreshTokenRenewedEvent:
if wm.UserState == domain.UserStateActive {
wm.RefreshToken = e.RefreshToken

View File

@ -7,6 +7,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/oidc/v3/pkg/oidc"
"go.uber.org/mock/gomock"
@ -40,6 +41,8 @@ func TestCommands_AddAccessAndRefreshToken(t *testing.T) {
authTime time.Time
refreshIdleExpiration time.Duration
refreshExpiration time.Duration
reason domain.TokenReason
actor *domain.TokenActor
}
type res struct {
token *domain.Token
@ -135,6 +138,7 @@ func TestCommands_AddAccessAndRefreshToken(t *testing.T) {
time.Now(),
1*time.Hour,
24*time.Hour,
nil,
)),
eventFromEventPusher(user.NewHumanRefreshTokenRemovedEvent(
context.Background(),
@ -173,6 +177,7 @@ func TestCommands_AddAccessAndRefreshToken(t *testing.T) {
time.Now(),
-1*time.Hour,
24*time.Hour,
nil,
)),
),
),
@ -292,7 +297,7 @@ func TestCommands_AddAccessAndRefreshToken(t *testing.T) {
keyAlgorithm: tt.fields.keyAlgorithm,
}
got, gotRefresh, err := c.AddAccessAndRefreshToken(tt.args.ctx, tt.args.orgID, tt.args.agentID, tt.args.clientID, tt.args.userID, tt.args.refreshToken,
tt.args.audience, tt.args.scopes, tt.args.authMethodsReferences, tt.args.lifetime, tt.args.refreshIdleExpiration, tt.args.refreshExpiration, tt.args.authTime)
tt.args.audience, tt.args.scopes, tt.args.authMethodsReferences, tt.args.lifetime, tt.args.refreshIdleExpiration, tt.args.refreshExpiration, tt.args.authTime, tt.args.reason, tt.args.actor)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -372,6 +377,7 @@ func TestCommands_RevokeRefreshToken(t *testing.T) {
time.Now(),
1*time.Hour,
10*time.Hour,
nil,
)),
),
expectPushFailed(zerrors.ThrowInternal(nil, "ERROR", "internal"),
@ -411,6 +417,7 @@ func TestCommands_RevokeRefreshToken(t *testing.T) {
time.Now(),
1*time.Hour,
10*time.Hour,
nil,
)),
),
expectPush(
@ -506,6 +513,7 @@ func TestCommands_RevokeRefreshTokens(t *testing.T) {
time.Now(),
1*time.Hour,
10*time.Hour,
nil,
)),
),
expectFilter(),
@ -539,6 +547,7 @@ func TestCommands_RevokeRefreshTokens(t *testing.T) {
time.Now(),
1*time.Hour,
10*time.Hour,
nil,
)),
),
expectFilter(
@ -555,6 +564,7 @@ func TestCommands_RevokeRefreshTokens(t *testing.T) {
time.Now(),
1*time.Hour,
10*time.Hour,
nil,
)),
),
expectPushFailed(zerrors.ThrowInternal(nil, "ERROR", "internal"),
@ -599,6 +609,7 @@ func TestCommands_RevokeRefreshTokens(t *testing.T) {
time.Now(),
1*time.Hour,
10*time.Hour,
nil,
)),
),
expectFilter(
@ -615,6 +626,7 @@ func TestCommands_RevokeRefreshTokens(t *testing.T) {
time.Now(),
1*time.Hour,
10*time.Hour,
nil,
)),
),
expectPush(
@ -691,6 +703,7 @@ func TestCommands_addRefreshToken(t *testing.T) {
authTime time.Time
idleExpiration time.Duration
expiration time.Duration
actor *domain.TokenActor
}
type res struct {
event *user.HumanRefreshTokenAddedEvent
@ -745,6 +758,7 @@ func TestCommands_addRefreshToken(t *testing.T) {
authTime,
1*time.Hour,
10*time.Hour,
nil,
),
refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:refreshTokenID:refreshTokenID")),
},
@ -756,7 +770,7 @@ func TestCommands_addRefreshToken(t *testing.T) {
eventstore: tt.fields.eventstore,
keyAlgorithm: tt.fields.keyAlgorithm,
}
gotEvent, gotRefreshToken, err := c.addRefreshToken(tt.args.ctx, tt.args.accessToken, tt.args.authMethodsReferences, tt.args.authTime, tt.args.idleExpiration, tt.args.expiration)
gotEvent, gotRefreshToken, err := c.addRefreshToken(tt.args.ctx, tt.args.accessToken, tt.args.authMethodsReferences, tt.args.authTime, tt.args.idleExpiration, tt.args.expiration, tt.args.actor)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -788,13 +802,13 @@ func TestCommands_renewRefreshToken(t *testing.T) {
event *user.HumanRefreshTokenRenewedEvent
refreshTokenID string
newRefreshToken string
err func(error) bool
}
tests := []struct {
name string
fields fields
args args
res res
name string
fields fields
args args
want *renewedRefreshToken
wantErr func(error) bool
}{
{
name: "empty token, error",
@ -804,9 +818,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
args: args{
ctx: context.Background(),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
wantErr: zerrors.IsErrorInvalidArgument,
},
{
name: "invalid token, error",
@ -818,9 +830,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
ctx: context.Background(),
refreshToken: "invalid",
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
wantErr: zerrors.IsErrorInvalidArgument,
},
{
name: "invalid token (invalid userID), error",
@ -834,9 +844,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
orgID: "orgID",
refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID2:tokenID:token")),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
wantErr: zerrors.IsErrorInvalidArgument,
},
{
name: "token inactive, error",
@ -856,6 +864,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
time.Now(),
1*time.Hour,
24*time.Hour,
nil,
)),
eventFromEventPusher(user.NewHumanRefreshTokenRemovedEvent(
context.Background(),
@ -872,9 +881,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
orgID: "orgID",
refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:token")),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
wantErr: zerrors.IsErrorInvalidArgument,
},
{
name: "token expired, error",
@ -894,6 +901,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
time.Now(),
1*time.Hour,
24*time.Hour,
nil,
)),
),
),
@ -905,9 +913,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
orgID: "orgID",
refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:tokenID")),
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
wantErr: zerrors.IsErrorInvalidArgument,
},
{
name: "user deactivated, error",
@ -927,6 +933,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
time.Now(),
1*time.Hour,
24*time.Hour,
nil,
)),
eventFromEventPusher(
user.NewUserDeactivatedEvent(
@ -945,9 +952,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:tokenID")),
idleExpiration: 1 * time.Hour,
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
wantErr: zerrors.IsErrorInvalidArgument,
},
{
name: "user signedout, error",
@ -967,6 +972,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
time.Now(),
1*time.Hour,
24*time.Hour,
nil,
)),
eventFromEventPusher(
user.NewHumanSignedOutEvent(
@ -986,9 +992,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:tokenID")),
idleExpiration: 1 * time.Hour,
},
res: res{
err: zerrors.IsErrorInvalidArgument,
},
wantErr: zerrors.IsErrorInvalidArgument,
},
{
name: "token renewed, ok",
@ -1008,6 +1012,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
time.Now(),
1*time.Hour,
24*time.Hour,
nil,
)),
),
),
@ -1021,7 +1026,7 @@ func TestCommands_renewRefreshToken(t *testing.T) {
refreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:tokenID")),
idleExpiration: 1 * time.Hour,
},
res: res{
want: &renewedRefreshToken{
event: user.NewHumanRefreshTokenRenewedEvent(
context.Background(),
&user.NewAggregate("userID", "orgID").Aggregate,
@ -1029,8 +1034,9 @@ func TestCommands_renewRefreshToken(t *testing.T) {
"refreshToken1",
1*time.Hour,
),
refreshTokenID: "tokenID",
newRefreshToken: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:refreshToken1")),
authMethodsReferences: []string{"password"},
tokenID: "tokenID",
token: base64.RawURLEncoding.EncodeToString([]byte("userID:tokenID:refreshToken1")),
},
},
}
@ -1041,17 +1047,16 @@ func TestCommands_renewRefreshToken(t *testing.T) {
idGenerator: tt.fields.idGenerator,
keyAlgorithm: tt.fields.keyAlgorithm,
}
gotEvent, gotRefreshTokenID, gotNewRefreshToken, err := c.renewRefreshToken(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.refreshToken, tt.args.idleExpiration)
if tt.res.err == nil {
assert.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
got, err := c.renewRefreshToken(tt.args.ctx, tt.args.userID, tt.args.orgID, tt.args.refreshToken, tt.args.idleExpiration)
if tt.wantErr != nil && !tt.wantErr(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.event, gotEvent)
assert.Equal(t, tt.res.refreshTokenID, gotRefreshTokenID)
assert.Equal(t, tt.res.newRefreshToken, gotNewRefreshToken)
if tt.wantErr == nil {
require.NoError(t, err)
assert.Equal(t, tt.want.event, got.event)
assert.Equal(t, tt.want.authMethodsReferences, got.authMethodsReferences)
assert.Equal(t, tt.want.tokenID, got.tokenID)
assert.Equal(t, tt.want.token, got.token)
}
})
}

View File

@ -1440,14 +1440,18 @@ func TestCommandSide_AddUserToken(t *testing.T) {
}
type (
args struct {
ctx context.Context
orgID string
agentID string
clientID string
userID string
audience []string
scopes []string
lifetime time.Duration
ctx context.Context
orgID string
agentID string
clientID string
userID string
audience []string
scopes []string
authMethodsReferences []string
lifetime time.Duration
authTime time.Time
reason domain.TokenReason
actor *domain.TokenActor
}
)
type res struct {
@ -1500,7 +1504,7 @@ func TestCommandSide_AddUserToken(t *testing.T) {
eventstore: tt.fields.eventstore,
idGenerator: tt.fields.idGenerator,
}
got, err := r.AddUserToken(tt.args.ctx, tt.args.orgID, tt.args.agentID, tt.args.clientID, tt.args.userID, tt.args.audience, tt.args.scopes, tt.args.lifetime)
got, err := r.AddUserToken(tt.args.ctx, tt.args.orgID, tt.args.agentID, tt.args.clientID, tt.args.userID, tt.args.audience, tt.args.scopes, tt.args.authMethodsReferences, tt.args.lifetime, tt.args.authTime, tt.args.reason, tt.args.actor)
if tt.res.err == nil {
assert.NoError(t, err)
}
@ -1565,7 +1569,11 @@ func TestCommands_RevokeAccessToken(t *testing.T) {
"refreshTokenID",
[]string{"clientID"},
[]string{"openid"},
[]string{"password"},
time.Now(),
time.Now(),
domain.TokenReasonAuthRequest,
nil,
),
),
),
@ -1597,7 +1605,11 @@ func TestCommands_RevokeAccessToken(t *testing.T) {
"refreshTokenID",
[]string{"clientID"},
[]string{"openid"},
[]string{"password"},
time.Now(),
time.Now().Add(5*time.Hour),
domain.TokenReasonAuthRequest,
nil,
),
),
),

View File

@ -91,6 +91,7 @@ const (
OIDCGrantTypeImplicit
OIDCGrantTypeRefreshToken
OIDCGrantTypeDeviceCode
OIDCGrantTypeTokenExchange
)
type OIDCApplicationType int32

View File

@ -20,6 +20,8 @@ type Token struct {
Expiration time.Time
Scopes []string
PreferredLanguage string
Reason TokenReason
Actor *TokenActor
}
func AddAudScopeToAudience(ctx context.Context, audience, scopes []string) []string {
@ -44,3 +46,22 @@ func addProjectID(audience []string, projectID string) []string {
}
return append(audience, projectID)
}
//go:generate enumer -type TokenReason -transform snake -trimprefix TokenReason -json
type TokenReason int
const (
TokenReasonUnspecified TokenReason = iota
TokenReasonAuthRequest
TokenReasonRefresh
TokenReasonJWTProfile
TokenReasonClientCredentials
TokenReasonExchange
TokenReasonImpersonation
)
type TokenActor struct {
Actor *TokenActor `json:"actor,omitempty"`
UserID string `json:"user_id,omitempty"`
Issuer string `json:"issuer,omitempty"`
}

View File

@ -0,0 +1,116 @@
// Code generated by "enumer -type TokenReason -transform snake -trimprefix TokenReason -json"; DO NOT EDIT.
package domain
import (
"encoding/json"
"fmt"
"strings"
)
const _TokenReasonName = "unspecifiedauth_requestrefreshjwt_profileclient_credentialsexchangeimpersonation"
var _TokenReasonIndex = [...]uint8{0, 11, 23, 30, 41, 59, 67, 80}
const _TokenReasonLowerName = "unspecifiedauth_requestrefreshjwt_profileclient_credentialsexchangeimpersonation"
func (i TokenReason) String() string {
if i < 0 || i >= TokenReason(len(_TokenReasonIndex)-1) {
return fmt.Sprintf("TokenReason(%d)", i)
}
return _TokenReasonName[_TokenReasonIndex[i]:_TokenReasonIndex[i+1]]
}
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
func _TokenReasonNoOp() {
var x [1]struct{}
_ = x[TokenReasonUnspecified-(0)]
_ = x[TokenReasonAuthRequest-(1)]
_ = x[TokenReasonRefresh-(2)]
_ = x[TokenReasonJWTProfile-(3)]
_ = x[TokenReasonClientCredentials-(4)]
_ = x[TokenReasonExchange-(5)]
_ = x[TokenReasonImpersonation-(6)]
}
var _TokenReasonValues = []TokenReason{TokenReasonUnspecified, TokenReasonAuthRequest, TokenReasonRefresh, TokenReasonJWTProfile, TokenReasonClientCredentials, TokenReasonExchange, TokenReasonImpersonation}
var _TokenReasonNameToValueMap = map[string]TokenReason{
_TokenReasonName[0:11]: TokenReasonUnspecified,
_TokenReasonLowerName[0:11]: TokenReasonUnspecified,
_TokenReasonName[11:23]: TokenReasonAuthRequest,
_TokenReasonLowerName[11:23]: TokenReasonAuthRequest,
_TokenReasonName[23:30]: TokenReasonRefresh,
_TokenReasonLowerName[23:30]: TokenReasonRefresh,
_TokenReasonName[30:41]: TokenReasonJWTProfile,
_TokenReasonLowerName[30:41]: TokenReasonJWTProfile,
_TokenReasonName[41:59]: TokenReasonClientCredentials,
_TokenReasonLowerName[41:59]: TokenReasonClientCredentials,
_TokenReasonName[59:67]: TokenReasonExchange,
_TokenReasonLowerName[59:67]: TokenReasonExchange,
_TokenReasonName[67:80]: TokenReasonImpersonation,
_TokenReasonLowerName[67:80]: TokenReasonImpersonation,
}
var _TokenReasonNames = []string{
_TokenReasonName[0:11],
_TokenReasonName[11:23],
_TokenReasonName[23:30],
_TokenReasonName[30:41],
_TokenReasonName[41:59],
_TokenReasonName[59:67],
_TokenReasonName[67:80],
}
// TokenReasonString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func TokenReasonString(s string) (TokenReason, error) {
if val, ok := _TokenReasonNameToValueMap[s]; ok {
return val, nil
}
if val, ok := _TokenReasonNameToValueMap[strings.ToLower(s)]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to TokenReason values", s)
}
// TokenReasonValues returns all values of the enum
func TokenReasonValues() []TokenReason {
return _TokenReasonValues
}
// TokenReasonStrings returns a slice of all String values of the enum
func TokenReasonStrings() []string {
strs := make([]string, len(_TokenReasonNames))
copy(strs, _TokenReasonNames)
return strs
}
// IsATokenReason returns "true" if the value is listed in the enum definition. "false" otherwise
func (i TokenReason) IsATokenReason() bool {
for _, v := range _TokenReasonValues {
if i == v {
return true
}
}
return false
}
// MarshalJSON implements the json.Marshaler interface for TokenReason
func (i TokenReason) MarshalJSON() ([]byte, error) {
return json.Marshal(i.String())
}
// UnmarshalJSON implements the json.Unmarshaler interface for TokenReason
func (i *TokenReason) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return fmt.Errorf("TokenReason should be a string, got %s", data)
}
var err error
*i, err = TokenReasonString(s)
return err
}

View File

@ -42,6 +42,7 @@ const (
UserAuthMethodTypeIDP
UserAuthMethodTypeOTPSMS
UserAuthMethodTypeOTPEmail
UserAuthMethodTypeOTP // generic OTP when parsing AMR from OIDC
userAuthMethodTypeCount
)
@ -59,7 +60,8 @@ func HasMFA(methods []UserAuthMethodType) bool {
UserAuthMethodTypeTOTP,
UserAuthMethodTypeOTPSMS,
UserAuthMethodTypeOTPEmail,
UserAuthMethodTypeIDP:
UserAuthMethodTypeIDP,
UserAuthMethodTypeOTP:
factors++
case UserAuthMethodTypeUnspecified,
userAuthMethodTypeCount:

View File

@ -9,6 +9,7 @@ const (
KeyTriggerIntrospectionProjections
KeyLegacyIntrospection
KeyUserSchema
KeyTokenExchange
)
//go:generate enumer -type Level -transform snake -trimprefix Level
@ -29,4 +30,5 @@ type Features struct {
TriggerIntrospectionProjections bool `json:"trigger_introspection_projections,omitempty"`
LegacyIntrospection bool `json:"legacy_introspection,omitempty"`
UserSchema bool `json:"user_schema,omitempty"`
TokenExchange bool `json:"token_exchange,omitempty"`
}

View File

@ -7,11 +7,11 @@ import (
"strings"
)
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schema"
const _KeyName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchange"
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92}
var _KeyIndex = [...]uint8{0, 11, 28, 61, 81, 92, 106}
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schema"
const _KeyLowerName = "unspecifiedlogin_default_orgtrigger_introspection_projectionslegacy_introspectionuser_schematoken_exchange"
func (i Key) String() string {
if i < 0 || i >= Key(len(_KeyIndex)-1) {
@ -29,21 +29,24 @@ func _KeyNoOp() {
_ = x[KeyTriggerIntrospectionProjections-(2)]
_ = x[KeyLegacyIntrospection-(3)]
_ = x[KeyUserSchema-(4)]
_ = x[KeyTokenExchange-(5)]
}
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema}
var _KeyValues = []Key{KeyUnspecified, KeyLoginDefaultOrg, KeyTriggerIntrospectionProjections, KeyLegacyIntrospection, KeyUserSchema, KeyTokenExchange}
var _KeyNameToValueMap = map[string]Key{
_KeyName[0:11]: KeyUnspecified,
_KeyLowerName[0:11]: KeyUnspecified,
_KeyName[11:28]: KeyLoginDefaultOrg,
_KeyLowerName[11:28]: KeyLoginDefaultOrg,
_KeyName[28:61]: KeyTriggerIntrospectionProjections,
_KeyLowerName[28:61]: KeyTriggerIntrospectionProjections,
_KeyName[61:81]: KeyLegacyIntrospection,
_KeyLowerName[61:81]: KeyLegacyIntrospection,
_KeyName[81:92]: KeyUserSchema,
_KeyLowerName[81:92]: KeyUserSchema,
_KeyName[0:11]: KeyUnspecified,
_KeyLowerName[0:11]: KeyUnspecified,
_KeyName[11:28]: KeyLoginDefaultOrg,
_KeyLowerName[11:28]: KeyLoginDefaultOrg,
_KeyName[28:61]: KeyTriggerIntrospectionProjections,
_KeyLowerName[28:61]: KeyTriggerIntrospectionProjections,
_KeyName[61:81]: KeyLegacyIntrospection,
_KeyLowerName[61:81]: KeyLegacyIntrospection,
_KeyName[81:92]: KeyUserSchema,
_KeyLowerName[81:92]: KeyUserSchema,
_KeyName[92:106]: KeyTokenExchange,
_KeyLowerName[92:106]: KeyTokenExchange,
}
var _KeyNames = []string{
@ -52,6 +55,7 @@ var _KeyNames = []string{
_KeyName[28:61],
_KeyName[61:81],
_KeyName[81:92],
_KeyName[92:106],
}
// KeyString retrieves an enum value from the enum constants string name.

View File

@ -23,13 +23,16 @@ import (
"github.com/zitadel/zitadel/pkg/grpc/user"
)
func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType, devMode bool) (*management.AddOIDCAppResponse, error) {
func (s *Tester) CreateOIDCClient(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, appType app.OIDCAppType, authMethod app.OIDCAuthMethodType, devMode bool, grantTypes ...app.OIDCGrantType) (*management.AddOIDCAppResponse, error) {
if len(grantTypes) == 0 {
grantTypes = []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN}
}
return s.Client.Mgmt.AddOIDCApp(ctx, &management.AddOIDCAppRequest{
ProjectId: projectID,
Name: fmt.Sprintf("app-%d", time.Now().UnixNano()),
RedirectUris: []string{redirectURI},
ResponseTypes: []app.OIDCResponseType{app.OIDCResponseType_OIDC_RESPONSE_TYPE_CODE},
GrantTypes: []app.OIDCGrantType{app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN},
GrantTypes: grantTypes,
AppType: appType,
AuthMethodType: authMethod,
PostLogoutRedirectUris: []string{logoutRedirectURI},
@ -53,8 +56,8 @@ func (s *Tester) CreateOIDCWebClientBasic(ctx context.Context, redirectURI, logo
return s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_BASIC, false)
}
func (s *Tester) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logoutRedirectURI, projectID string) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
client, err = s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, false)
func (s *Tester) CreateOIDCWebClientJWT(ctx context.Context, redirectURI, logoutRedirectURI, projectID string, grantTypes ...app.OIDCGrantType) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
client, err = s.CreateOIDCClient(ctx, redirectURI, logoutRedirectURI, projectID, app.OIDCAppType_OIDC_APP_TYPE_WEB, app.OIDCAuthMethodType_OIDC_AUTH_METHOD_TYPE_PRIVATE_KEY_JWT, false, grantTypes...)
if err != nil {
return nil, nil, err
}
@ -113,6 +116,16 @@ func (s *Tester) CreateOIDCImplicitFlowClient(ctx context.Context, redirectURI s
})
}
func (s *Tester) CreateOIDCTokenExchangeClient(ctx context.Context) (client *management.AddOIDCAppResponse, keyData []byte, err error) {
project, err := s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),
})
if err != nil {
return nil, nil, err
}
return s.CreateOIDCWebClientJWT(ctx, "", "", project.GetId(), app.OIDCGrantType_OIDC_GRANT_TYPE_TOKEN_EXCHANGE, app.OIDCGrantType_OIDC_GRANT_TYPE_AUTHORIZATION_CODE, app.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN)
}
func (s *Tester) CreateProject(ctx context.Context) (*management.AddProjectResponse, error) {
return s.Client.Mgmt.AddProject(ctx, &management.AddProjectRequest{
Name: fmt.Sprintf("project-%d", time.Now().UnixNano()),

View File

@ -0,0 +1,57 @@
package integration
import (
"context"
"strings"
"time"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/management"
)
func (s *Tester) CreateMachineUserPATWithMembership(ctx context.Context, roles ...string) (id, pat string, err error) {
user := s.CreateMachineUser(ctx)
patResp, err := s.Client.Mgmt.AddPersonalAccessToken(ctx, &management.AddPersonalAccessTokenRequest{
UserId: user.GetUserId(),
ExpirationDate: timestamppb.New(time.Now().Add(24 * time.Hour)),
})
if err != nil {
return "", "", err
}
orgRoles := make([]string, 0, len(roles))
iamRoles := make([]string, 0, len(roles))
for _, role := range roles {
if strings.HasPrefix(role, "ORG_") {
orgRoles = append(orgRoles, role)
}
if strings.HasPrefix(role, "IAM_") {
iamRoles = append(iamRoles, role)
}
}
if len(orgRoles) > 0 {
_, err := s.Client.Mgmt.AddOrgMember(ctx, &management.AddOrgMemberRequest{
UserId: user.GetUserId(),
Roles: orgRoles,
})
if err != nil {
return "", "", err
}
}
if len(iamRoles) > 0 {
_, err := s.Client.Admin.AddIAMMember(ctx, &admin.AddIAMMemberRequest{
UserId: user.GetUserId(),
Roles: iamRoles,
})
if err != nil {
return "", "", err
}
}
return user.GetUserId(), patResp.GetToken(), nil
}

View File

@ -28,6 +28,8 @@ type OIDCSessionAccessTokenReadModel struct {
AccessTokenID string
AccessTokenCreation time.Time
AccessTokenExpiration time.Time
Reason domain.TokenReason
Actor *domain.TokenActor
}
func newOIDCSessionAccessTokenReadModel(id string) *OIDCSessionAccessTokenReadModel {
@ -84,6 +86,8 @@ func (wm *OIDCSessionAccessTokenReadModel) reduceAccessTokenAdded(e *oidcsession
wm.AccessTokenID = e.ID
wm.AccessTokenCreation = e.CreationDate()
wm.AccessTokenExpiration = e.CreationDate().Add(e.Lifetime)
wm.Reason = e.Reason
wm.Actor = e.Actor
}
func (wm *OIDCSessionAccessTokenReadModel) reduceTokenRevoked(e eventstore.Event) {

View File

@ -29,7 +29,12 @@ keys as (
group by identifier
),
settings as (
select instance_id, json_build_object('access_token_lifetime', access_token_lifetime, 'id_token_lifetime', id_token_lifetime) as settings
select instance_id, json_build_object(
'access_token_lifetime', access_token_lifetime,
'id_token_lifetime', id_token_lifetime,
'refresh_token_idle_expiration', refresh_token_idle_expiration,
'refresh_token_expiration', refresh_token_expiration
) as settings
from projections.oidc_settings2
where aggregate_id = $1
and instance_id = $1

View File

@ -12,6 +12,7 @@ type InstanceFeatures struct {
TriggerIntrospectionProjections FeatureSource[bool]
LegacyIntrospection FeatureSource[bool]
UserSchema FeatureSource[bool]
TokenExchange FeatureSource[bool]
}
func (q *Queries) GetInstanceFeatures(ctx context.Context, cascade bool) (_ *InstanceFeatures, err error) {

View File

@ -61,6 +61,7 @@ func (m *InstanceFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.InstanceTriggerIntrospectionProjectionsEventType,
feature_v2.InstanceLegacyIntrospectionEventType,
feature_v2.InstanceUserSchemaEventType,
feature_v2.InstanceTokenExchangeEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -73,6 +74,7 @@ func (m *InstanceFeaturesReadModel) reduceReset() {
m.instance.TriggerIntrospectionProjections = FeatureSource[bool]{}
m.instance.LegacyIntrospection = FeatureSource[bool]{}
m.instance.UserSchema = FeatureSource[bool]{}
m.instance.TokenExchange = FeatureSource[bool]{}
}
func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
@ -83,6 +85,7 @@ func (m *InstanceFeaturesReadModel) populateFromSystem() bool {
m.instance.TriggerIntrospectionProjections = m.system.TriggerIntrospectionProjections
m.instance.LegacyIntrospection = m.system.LegacyIntrospection
m.instance.UserSchema = m.system.UserSchema
m.instance.TokenExchange = m.system.TokenExchange
return true
}
@ -104,6 +107,8 @@ func (m *InstanceFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent
dst = &m.instance.LegacyIntrospection
case feature.KeyUserSchema:
dst = &m.instance.UserSchema
case feature.KeyTokenExchange:
dst = &m.instance.TokenExchange
}
*dst = FeatureSource[bool]{
Level: level,

View File

@ -75,6 +75,10 @@ func (*instanceFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.InstanceUserSchemaEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: feature_v2.InstanceTokenExchangeEventType,
Reduce: reduceInstanceSetFeature[bool],
},
{
Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(InstanceDomainInstanceIDCol),

View File

@ -67,6 +67,10 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
Event: feature_v2.SystemUserSchemaEventType,
Reduce: reduceSystemSetFeature[bool],
},
{
Event: feature_v2.SystemTokenExchangeEventType,
Reduce: reduceSystemSetFeature[bool],
},
},
}}
}

View File

@ -19,6 +19,7 @@ type SystemFeatures struct {
TriggerIntrospectionProjections FeatureSource[bool]
LegacyIntrospection FeatureSource[bool]
UserSchema FeatureSource[bool]
TokenExchange FeatureSource[bool]
}
func (q *Queries) GetSystemFeatures(ctx context.Context) (_ *SystemFeatures, err error) {

View File

@ -49,6 +49,7 @@ func (m *SystemFeaturesReadModel) Query() *eventstore.SearchQueryBuilder {
feature_v2.SystemTriggerIntrospectionProjectionsEventType,
feature_v2.SystemLegacyIntrospectionEventType,
feature_v2.SystemUserSchemaEventType,
feature_v2.SystemTokenExchangeEventType,
).
Builder().ResourceOwner(m.ResourceOwner)
}
@ -75,6 +76,8 @@ func (m *SystemFeaturesReadModel) reduceBoolFeature(event *feature_v2.SetEvent[b
dst = &m.system.LegacyIntrospection
case feature.KeyUserSchema:
dst = &m.system.UserSchema
case feature.KeyTokenExchange:
dst = &m.system.TokenExchange
}
*dst = FeatureSource[bool]{

View File

@ -10,9 +10,11 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, SystemTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, SystemTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceResetEventType, eventstore.GenericEventMapper[ResetEvent])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLoginDefaultOrgEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceTriggerIntrospectionProjectionsEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceLegacyIntrospectionEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceUserSchemaEventType, eventstore.GenericEventMapper[SetEvent[bool]])
eventstore.RegisterFilterEventMapper(AggregateType, InstanceTokenExchangeEventType, eventstore.GenericEventMapper[SetEvent[bool]])
}

View File

@ -16,12 +16,14 @@ var (
SystemTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTriggerIntrospectionProjections)
SystemLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyLegacyIntrospection)
SystemUserSchemaEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyUserSchema)
SystemTokenExchangeEventType = setEventTypeFromFeature(feature.LevelSystem, feature.KeyTokenExchange)
InstanceResetEventType = resetEventTypeFromFeature(feature.LevelInstance)
InstanceLoginDefaultOrgEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLoginDefaultOrg)
InstanceTriggerIntrospectionProjectionsEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTriggerIntrospectionProjections)
InstanceLegacyIntrospectionEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyLegacyIntrospection)
InstanceUserSchemaEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyUserSchema)
InstanceTokenExchangeEventType = setEventTypeFromFeature(feature.LevelInstance, feature.KeyTokenExchange)
)
const (

View File

@ -71,9 +71,11 @@ func NewAddedEvent(ctx context.Context,
type AccessTokenAddedEvent struct {
eventstore.BaseEvent `json:"-"`
ID string `json:"id"`
Scope []string `json:"scope"`
Lifetime time.Duration `json:"lifetime"`
ID string `json:"id,omitempty"`
Scope []string `json:"scope,omitempty"`
Lifetime time.Duration `json:"lifetime,omitempty"`
Reason domain.TokenReason `json:"reason,omitempty"`
Actor *domain.TokenActor `json:"actor,omitempty"`
}
func (e *AccessTokenAddedEvent) Payload() interface{} {
@ -94,6 +96,8 @@ func NewAccessTokenAddedEvent(
id string,
scope []string,
lifetime time.Duration,
reason domain.TokenReason,
actor *domain.TokenActor,
) *AccessTokenAddedEvent {
return &AccessTokenAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@ -104,6 +108,8 @@ func NewAccessTokenAddedEvent(
ID: id,
Scope: scope,
Lifetime: lifetime,
Reason: reason,
Actor: actor,
}
}

View File

@ -42,6 +42,7 @@ func init() {
eventstore.RegisterFilterEventMapper(AggregateType, UserReactivatedType, UserReactivatedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, UserRemovedType, UserRemovedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, UserTokenAddedType, UserTokenAddedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, UserImpersonatedType, eventstore.GenericEventMapper[UserImpersonatedEvent])
eventstore.RegisterFilterEventMapper(AggregateType, UserTokenRemovedType, UserTokenRemovedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, UserDomainClaimedType, DomainClaimedEventMapper)
eventstore.RegisterFilterEventMapper(AggregateType, UserDomainClaimedSentType, DomainClaimedSentEventMapper)

View File

@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore"
"github.com/zitadel/zitadel/internal/zerrors"
)
@ -18,16 +19,17 @@ const (
type HumanRefreshTokenAddedEvent struct {
eventstore.BaseEvent `json:"-"`
TokenID string `json:"tokenId"`
ClientID string `json:"clientId"`
UserAgentID string `json:"userAgentId"`
Audience []string `json:"audience"`
Scopes []string `json:"scopes"`
AuthMethodsReferences []string `json:"authMethodReferences"`
AuthTime time.Time `json:"authTime"`
IdleExpiration time.Duration `json:"idleExpiration"`
Expiration time.Duration `json:"expiration"`
PreferredLanguage string `json:"preferredLanguage"`
TokenID string `json:"tokenId"`
ClientID string `json:"clientId"`
UserAgentID string `json:"userAgentId"`
Audience []string `json:"audience"`
Scopes []string `json:"scopes"`
AuthMethodsReferences []string `json:"authMethodReferences"`
AuthTime time.Time `json:"authTime"`
IdleExpiration time.Duration `json:"idleExpiration"`
Expiration time.Duration `json:"expiration"`
PreferredLanguage string `json:"preferredLanguage"`
Actor *domain.TokenActor `json:"actor,omitempty"`
}
func (e *HumanRefreshTokenAddedEvent) Payload() interface{} {
@ -55,6 +57,7 @@ func NewHumanRefreshTokenAddedEvent(
authTime time.Time,
idleExpiration,
expiration time.Duration,
actor *domain.TokenActor,
) *HumanRefreshTokenAddedEvent {
return &HumanRefreshTokenAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@ -72,6 +75,7 @@ func NewHumanRefreshTokenAddedEvent(
IdleExpiration: idleExpiration,
Expiration: expiration,
PreferredLanguage: preferredLanguage,
Actor: actor,
}
}

View File

@ -20,6 +20,7 @@ const (
UserRemovedType = userEventTypePrefix + "removed"
UserTokenAddedType = userEventTypePrefix + "token.added"
UserTokenRemovedType = userEventTypePrefix + "token.removed"
UserImpersonatedType = userEventTypePrefix + "impersonated"
UserDomainClaimedType = userEventTypePrefix + "domain.claimed"
UserDomainClaimedSentType = userEventTypePrefix + "domain.claimed.sent"
UserUserNameChangedType = userEventTypePrefix + "username.changed"
@ -209,14 +210,18 @@ func UserRemovedEventMapper(event eventstore.Event) (eventstore.Event, error) {
type UserTokenAddedEvent struct {
eventstore.BaseEvent `json:"-"`
TokenID string `json:"tokenId"`
ApplicationID string `json:"applicationId"`
UserAgentID string `json:"userAgentId"`
RefreshTokenID string `json:"refreshTokenID,omitempty"`
Audience []string `json:"audience"`
Scopes []string `json:"scopes"`
Expiration time.Time `json:"expiration"`
PreferredLanguage string `json:"preferredLanguage"`
TokenID string `json:"tokenId,omitempty"`
ApplicationID string `json:"applicationId,omitempty"`
UserAgentID string `json:"userAgentId,omitempty"`
RefreshTokenID string `json:"refreshTokenID,omitempty"`
Audience []string `json:"audience,omitempty"`
Scopes []string `json:"scopes,omitempty"`
AuthMethodsReferences []string `json:"authMethodsReferences,omitempty"`
AuthTime time.Time `json:"authTime,omitempty"`
Expiration time.Time `json:"expiration,omitempty"`
PreferredLanguage string `json:"preferredLanguage,omitempty"`
Reason domain.TokenReason `json:"reason,omitempty"`
Actor *domain.TokenActor `json:"actor,omitempty"`
}
func (e *UserTokenAddedEvent) Payload() interface{} {
@ -236,8 +241,12 @@ func NewUserTokenAddedEvent(
preferredLanguage,
refreshTokenID string,
audience,
scopes []string,
scopes,
authMethodsReferences []string,
authTime,
expiration time.Time,
reason domain.TokenReason,
actor *domain.TokenActor,
) *UserTokenAddedEvent {
return &UserTokenAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
@ -253,6 +262,8 @@ func NewUserTokenAddedEvent(
Scopes: scopes,
Expiration: expiration,
PreferredLanguage: preferredLanguage,
Reason: reason,
Actor: actor,
}
}
@ -268,6 +279,42 @@ func UserTokenAddedEventMapper(event eventstore.Event) (eventstore.Event, error)
return tokenAdded, nil
}
type UserImpersonatedEvent struct {
eventstore.BaseEvent `json:"-"`
ApplicationID string `json:"applicationId,omitempty"`
Actor *domain.TokenActor `json:"actor,omitempty"`
}
func (e *UserImpersonatedEvent) Payload() interface{} {
return e
}
func (e *UserImpersonatedEvent) UniqueConstraints() []*eventstore.UniqueConstraint {
return nil
}
func (e *UserImpersonatedEvent) SetBaseEvent(base *eventstore.BaseEvent) {
e.BaseEvent = *base
}
func NewUserImpersonatedEvent(
ctx context.Context,
aggregate *eventstore.Aggregate,
applicationID string,
actor *domain.TokenActor,
) *UserTokenAddedEvent {
return &UserTokenAddedEvent{
BaseEvent: *eventstore.NewBaseEventForPush(
ctx,
aggregate,
UserImpersonatedType,
),
ApplicationID: applicationID,
Actor: actor,
}
}
type UserTokenRemovedEvent struct {
eventstore.BaseEvent `json:"-"`

View File

@ -573,6 +573,17 @@ Errors:
NotActive: Потребителската схема не е активна
NotInactive: Потребителската схема не е неактивна
NotExists: Потребителската схема не съществува
TokenExchange:
FeatureDisabled: Функцията Token Exchange е деактивирана за вашето копие. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Токенът липсва
Invalid: Токенът е невалиден
TypeMissing: Липсва тип токен
TypeNotAllowed: Типът токен не е разрешен
TypeNotSupported: Типът токен не се поддържа
NotForAPI: Имитирани токени не са разрешени за API
Impersonation:
PolicyDisabled: Имитирането е деактивирано в политиката за сигурност на екземпляра
AggregateTypes:
action: Действие
@ -609,6 +620,7 @@ EventTypes:
token:
added: Токенът за достъп е създаден
removed: Токенът за достъп е премахнат
impersonated: Имитиран потребител
username:
reserved: Потребителското име е запазено
released: Потребителското име е освободено

View File

@ -553,6 +553,17 @@ Errors:
NotActive: Uživatelské schéma není aktivní
NotInactive: Uživatelské schéma není neaktivní
NotExists: Uživatelské schéma neexistuje
TokenExchange:
FeatureDisabled: Funkce Token Exchange je pro vaši instanci zakázána. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Token chybí
Invalid: Token je neplatný
TypeMissing: Chybí typ tokenu
TypeNotAllowed: Typ tokenu není povolen
TypeNotSupported: Typ tokenu není podporován
NotForAPI: Zosobněné tokeny nejsou pro API povoleny
Impersonation:
PolicyDisabled: Zosobnění je zakázáno v zásadách zabezpečení instance
AggregateTypes:
action: Akce
@ -589,6 +600,7 @@ EventTypes:
token:
added: Přístupový token vytvořen
removed: Přístupový token odstraněn
impersonated: Usuario suplantado
username:
reserved: Uživatelské jméno rezervováno
released: Uživatelské jméno uvolněno

View File

@ -556,6 +556,17 @@ Errors:
NotActive: Benutzerschema nicht aktiv
NotInactive: Benutzerschema nicht inaktiv
NotExists: Benutzerschema existiert nicht
TokenExchange:
FeatureDisabled: Die Token-Austauschfunktion ist für Ihre Instanz deaktiviert. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Token fehlt
Invalid: Token ist ungültig
TypeMissing: Der Tokentyp fehlt
TypeNotAllowed: Der Tokentyp ist nicht zulässig
TypeNotSupported: Der Tokentyp wird nicht unterstützt
NotForAPI: Imitierte Token sind für die API nicht zulässig
Impersonation:
PolicyDisabled: Der Identitätswechsel ist in der Sicherheitsrichtlinie der Instanz deaktiviert
AggregateTypes:
action: Action
@ -592,6 +603,7 @@ EventTypes:
token:
added: Access Token ausgestellt
removed: Access Token gelöscht
impersonated: Benutzer hat sich als Benutzer ausgegeben
username:
reserved: Benutzername reserviert
released: Benutzername freigegeben

View File

@ -556,6 +556,17 @@ Errors:
NotActive: User Schema not active
NotInactive: User Schema not inactive
NotExists: User Schema does not exist
TokenExchange:
FeatureDisabled: Token Exchange feature is disabled for your instance. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Token is missing
Invalid: Token is invalid
TypeMissing: Token type is missing
TypeNotAllowed: Token type is not allowed
TypeNotSupported: Token type is not supported
NotForAPI: Impersonated tokens not allowed for API
Impersonation:
PolicyDisabled: Impersonation is disabled in the instance security policy
AggregateTypes:
action: Action
@ -592,6 +603,7 @@ EventTypes:
token:
added: Access Token created
removed: Access Token removed
impersonated: User impersonated
username:
reserved: Username reserved
released: Username released

View File

@ -556,6 +556,17 @@ Errors:
NotActive: Esquema de usuario no activo
NotInactive: Esquema de usuario no inactivo
NotExists: El esquema de usuario no existe
TokenExchange:
FeatureDisabled: La función de intercambio de tokens está deshabilitada para su instancia. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Falta la ficha
Invalid: El token no es válido
TypeMissing: Falta el tipo de token
TypeNotAllowed: El tipo de token no está permitido
TypeNotSupported: El tipo de token no es compatible
NotForAPI: Tokens suplantados no permitidos para API
Impersonation:
PolicyDisabled: La suplantación está deshabilitada en la política de seguridad de la instancia.
AggregateTypes:
action: Acción
@ -592,6 +603,7 @@ EventTypes:
token:
added: Token de acceso creado
removed: Token de acceso eliminado
impersonated: Usuario suplantado
username:
reserved: Nombre de usuario reservado
released: Nombre de usuario liberado

View File

@ -556,6 +556,17 @@ Errors:
NotActive: Schéma utilisateur non actif
NotInactive: Le schéma utilisateur n'est pas inactif
NotExists: Le schéma utilisateur n'existe pas
TokenExchange:
FeatureDisabled: La fonctionnalité Token Exchange est désactivée pour votre instance. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Le jeton est manquant
Invalid: Le jeton n'est pas valide
TypeMissing: Le type de jeton est manquantg
TypeNotAllowed: Le type de jeton n'est pas autorisé
TypeNotSupported: Le type de jeton n'est pas pris en charge
NotForAPI: Les jetons usurpés d'identité ne sont pas autorisés pour l'API
Impersonation:
PolicyDisabled: L'usurpation d'identité est désactivée dans la politique de sécurité de l'instance
AggregateTypes:
action: Action
@ -591,6 +602,7 @@ EventTypes:
failed: La vérification de l'initialisation a échoué
token:
added: Jeton d'accès créé
impersonated: Utilisateur usurpé l'identité
username:
reserved: Nom d'utilisateur réservé
released: Nom d'utilisateur libéré

View File

@ -557,6 +557,17 @@ Errors:
NotActive: Schema utente non attivo
NotInactive: Schema utente non inattivo
NotExists: Lo schema utente non esiste
TokenExchange:
FeatureDisabled: La funzionalità di scambio token è disabilitata per la tua istanza. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Manca il gettone
Invalid: Il token non è valido
TypeMissing: Manca il tipo di token
TypeNotAllowed: Il tipo di token non è consentito
TypeNotSupported: Il tipo di token non è supportato
NotForAPI: Token rappresentati non consentiti per l'API
Impersonation:
PolicyDisabled: La rappresentazione è disabilitata nella policy di sicurezza dell'istanza
AggregateTypes:
action: Azione
@ -592,6 +603,7 @@ EventTypes:
failed: Controllo dell'inizializzazione fallito
token:
added: Access Token creato
impersonated: Utente impersonificato
username:
reserved: Nome utente riservato
released: Nome utente rilasciato

View File

@ -545,6 +545,17 @@ Errors:
NotActive: ユーザースキーマがアクティブではありません
NotInactive: ユーザースキーマが非アクティブではありません
NotExists: ユーザースキーマが存在しません
TokenExchange:
FeatureDisabled: インスタンスではトークン交換機能が無効になっています。 https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: トークンがありません
Invalid: トークンが無効です
TypeMissing: トークンの種類がありません
TypeNotAllowed: トークンの種類は許可されていません
TypeNotSupported: トークンタイプはサポートされていません
NotForAPI: 偽装されたトークンは API では許可されません
Impersonation:
PolicyDisabled: インスタンスのセキュリティ ポリシーで偽装が無効になっています
AggregateTypes:
action: アクション
@ -581,6 +592,7 @@ EventTypes:
token:
added: アクセストークンの作成
removed: アクセストークンの削除
impersonated: ユーザーがなりすました
username:
reserved: ユーザー名の予約
released: ユーザー名の解放

View File

@ -555,6 +555,17 @@ Errors:
NotActive: Корисничката шема не е активна
NotInactive: Корисничката шема не е неактивна
NotExists: Корисничката шема не постои
TokenExchange:
FeatureDisabled: Функцијата за размена на токени е оневозможена на вашиот пример. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Недостасува токен
Invalid: Токенот е неважечки
TypeMissing: Недостасува тип на токен
TypeNotAllowed: Типот на токен не е дозволен
TypeNotSupported: Типот на токен не е поддржан
NotForAPI: Имитирани токени не се дозволени за API
Impersonation:
PolicyDisabled: Имитирањето е оневозможено во политиката за безбедност на примерот
AggregateTypes:
action: Акција
@ -591,6 +602,7 @@ EventTypes:
token:
added: Креиран е токен за пристап
removed: Токенот за пристап е отстранет
impersonated: Корисникот имитиран
username:
reserved: Корисничкото име е резервирано
released: Корисничкото име е ослободено

View File

@ -556,6 +556,17 @@ Errors:
NotActive: Gebruikersschema niet actief
NotInactive: Gebruikersschema niet inactief
NotExists: Gebruikersschema bestaat niet
TokenExchange:
FeatureDisabled: De Token Exchange-functie is uitgeschakeld voor uw instantie. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Token ontbreekt
Invalid: Token is ongeldig
TypeMissing: Tokentype ontbreekt
TypeNotAllowed: Tokentype is niet toegestaan
TypeNotSupported: Tokentype wordt niet ondersteund
NotForAPI: Nagebootste tokens zijn niet toegestaan voor API
Impersonation:
PolicyDisabled: Nabootsing van identiteit is uitgeschakeld in het beveiligingsbeleid van de instantie.
AggregateTypes:
action: Actie
@ -592,6 +603,7 @@ EventTypes:
token:
added: Toegangstoken aangemaakt
removed: Toegangstoken verwijderd
impersonated: Gebruiker nagebootst
username:
reserved: Gebruikersnaam gereserveerd
released: Gebruikersnaam vrijgegeven

View File

@ -556,6 +556,17 @@ Errors:
NotActive: Schemat użytkownika nieaktywny
NotInactive: Schemat użytkownika nie jest nieaktywny
NotExists: Schemat użytkownika nie istnieje
TokenExchange:
FeatureDisabled: Funkcja wymiany tokenów jest wyłączona dla Twojej instancji. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Brak tokena
Invalid: Token jest nieprawidłowy
TypeMissing: Brak typu tokena
TypeNotAllowed: Typ tokenu jest niedozwolony
TypeNotSupported: Typ tokena nie jest obsługiwany
NotForAPI: Podrabiane tokeny nie są dozwolone w interfejsie API
Impersonation:
PolicyDisabled: Podszywanie się jest wyłączone w polityce bezpieczeństwa instancji
AggregateTypes:
action: Działanie
@ -592,6 +603,7 @@ EventTypes:
token:
added: Token dostępu utworzony
removed: Token dostępu usunięty
impersonated: Użytkownik podszywał się pod użytkownika
username:
reserved: Nazwa użytkownika zarezerwowana
released: Nazwa użytkownika zwolniona

View File

@ -550,6 +550,17 @@ Errors:
NotActive: Esquema do usuário não ativo
NotInactive: Esquema do usuário não inativo
NotExists: O esquema do usuário não existe
TokenExchange:
FeatureDisabled: O recurso Token Exchange está desabilitado para sua instância. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: O token está faltando
Invalid: O token é inválido
TypeMissing: O tipo de token está faltando
TypeNotAllowed: O tipo de token não é permitido
TypeNotSupported: O tipo de token não é compatível
NotForAPI: Tokens personificados não permitidos para API
Impersonation:
PolicyDisabled: A representação está desativada na política de segurança da instância
AggregateTypes:
action: Ação
@ -586,6 +597,7 @@ EventTypes:
token:
added: Token de acesso criado
removed: Token de acesso removido
impersonated: Usuário personificado
username:
reserved: Nome de usuário reservado
released: Nome de usuário liberado

View File

@ -544,6 +544,17 @@ Errors:
NotActive: Пользовательская схема не активна
NotInactive: Пользовательская схема не неактивна
NotExists: Пользовательская схема не существует
TokenExchange:
FeatureDisabled: Функция обмена токенами отключена для вашего экземпляра. https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: Токен отсутствует
Invalid: Токен недействителен
TypeMissing: Тип токена отсутствует
TypeNotAllowed: Тип токена недопустим.
TypeNotSupported: Тип токена не поддерживается
NotForAPI: Олицетворенные токены не разрешены для API.
Impersonation:
PolicyDisabled: Олицетворение отключено в политике безопасности экземпляра.
AggregateTypes:
action: Действие
@ -580,6 +591,7 @@ EventTypes:
token:
added: Токен доступа создан
removed: Токен доступа удалён
impersonated: Пользователь олицетворяет себя
username:
reserved: Имя пользователя зарезервировано
released: Имя пользователя опубликовано

View File

@ -556,6 +556,17 @@ Errors:
NotActive: 用户架构未激活
NotInactive: 用户架构未处于非活动状态
NotExists: 用户架构不存在
TokenExchange:
FeatureDisabled: 您的实例已禁用令牌交换功能。 https://zitadel.com/docs/apis/resources/feature_service_v2/feature-service-set-instance-features
Token:
Missing: 令牌丢失
Invalid: 令牌无效
TypeMissing: 缺少令牌类型
TypeNotAllowed: 不允许的令牌类型
TypeNotSupported: 不支持令牌类型
NotForAPI: API 不允许使用模拟令牌
Impersonation:
PolicyDisabled: 实例安全策略中禁用模拟
AggregateTypes:
action: 动作
@ -591,6 +602,7 @@ EventTypes:
failed: 初始化检查失败
token:
added: 已创建访问令牌
impersonated: 用户冒充
username:
reserved: 保留用户名
released: 用户名已发布

View File

@ -23,6 +23,7 @@ type RefreshTokenView struct {
Scopes []string
Sequence uint64
Token string
Actor *domain.TokenActor
}
type RefreshTokenSearchRequest struct {

View File

@ -22,6 +22,8 @@ type TokenView struct {
PreferredLanguage string
RefreshTokenID string
IsPAT bool
Reason domain.TokenReason
Actor *domain.TokenActor
}
type TokenSearchRequest struct {

View File

@ -39,6 +39,7 @@ type RefreshTokenView struct {
Expiration time.Time `json:"-" gorm:"column:expiration"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
InstanceID string `json:"instanceID" gorm:"column:instance_id;primary_key"`
Actor TokenActor `json:"actor" gorm:"column:actor"`
}
func RefreshTokenViewsToModel(tokens []*RefreshTokenView) []*usr_model.RefreshTokenView {
@ -66,6 +67,7 @@ func RefreshTokenViewToModel(token *RefreshTokenView) *usr_model.RefreshTokenVie
IdleExpiration: token.IdleExpiration,
Expiration: token.Expiration,
Sequence: token.Sequence,
Actor: token.Actor.TokenActor,
}
}
@ -135,6 +137,7 @@ func (t *RefreshTokenView) appendAddedEvent(event eventstore.Event) error {
t.Scopes = e.Scopes
t.Token = e.TokenID
t.UserAgentID = e.UserAgentID
t.Actor = TokenActor{e.Actor}
return nil
}

Some files were not shown because too many files have changed in this diff Show More