mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
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:
parent
b338171585
commit
6398349c24
27
cmd/setup/24.go
Normal file
27
cmd/setup/24.go
Normal 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
2
cmd/setup/24.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE auth.tokens ADD COLUMN actor jsonb;
|
||||
ALTER TABLE auth.refresh_tokens ADD COLUMN actor jsonb;
|
@ -102,6 +102,7 @@ type Steps struct {
|
||||
s21AddBlockFieldToLimits *AddBlockFieldToLimits
|
||||
s22ActiveInstancesIndex *ActiveInstanceEvents
|
||||
s23CorrectGlobalUniqueConstraints *CorrectGlobalUniqueConstraints
|
||||
s24AddActorToAuthTokens *AddActorToAuthTokens
|
||||
}
|
||||
|
||||
func MustNewSteps(v *viper.Viper) *Steps {
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -2270,7 +2270,8 @@
|
||||
"0": "Код за оторизация",
|
||||
"1": "имплицитно",
|
||||
"2": "Опресняване на токена",
|
||||
"3": "Код на устройството"
|
||||
"3": "Код на устройството",
|
||||
"4": "Размяна на токени"
|
||||
},
|
||||
"AUTHMETHOD": {
|
||||
"0": "Основен",
|
||||
|
@ -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í",
|
||||
|
@ -2279,7 +2279,8 @@
|
||||
"0": "Authorization Code",
|
||||
"1": "Implicit",
|
||||
"2": "Refresh Token",
|
||||
"3": "Device Code"
|
||||
"3": "Device Code",
|
||||
"4": "Token Exchange"
|
||||
},
|
||||
"AUTHMETHOD": {
|
||||
"0": "Basic",
|
||||
|
@ -2298,7 +2298,8 @@
|
||||
"0": "Authorization Code",
|
||||
"1": "Implicit",
|
||||
"2": "Refresh Token",
|
||||
"3": "Device Code"
|
||||
"3": "Device Code",
|
||||
"4": "Token Exchange"
|
||||
},
|
||||
"AUTHMETHOD": {
|
||||
"0": "Basic",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -2280,7 +2280,8 @@
|
||||
"0": "Authorization Code",
|
||||
"1": "Implicit",
|
||||
"2": "Refresh Token",
|
||||
"3": "Device Code"
|
||||
"3": "Device Code",
|
||||
"4": "Token Exchange"
|
||||
},
|
||||
"AUTHMETHOD": {
|
||||
"0": "Basic",
|
||||
|
@ -2271,7 +2271,8 @@
|
||||
"0": "Authorization Code",
|
||||
"1": "Implicit",
|
||||
"2": "Refresh Token",
|
||||
"3": "Device Code"
|
||||
"3": "Device Code",
|
||||
"4": "Token Exchange"
|
||||
},
|
||||
"AUTHMETHOD": {
|
||||
"0": "Basic",
|
||||
|
@ -2277,7 +2277,8 @@
|
||||
"0": "Код на Овластување",
|
||||
"1": "Implicit",
|
||||
"2": "Токен за Освежување",
|
||||
"3": "Код од Уред"
|
||||
"3": "Код од Уред",
|
||||
"4": "Размена на токени"
|
||||
},
|
||||
"AUTHMETHOD": {
|
||||
"0": "Basic",
|
||||
|
@ -2298,7 +2298,8 @@
|
||||
"0": "Authorization Code",
|
||||
"1": "Implicit",
|
||||
"2": "Refresh Token",
|
||||
"3": "Device Code"
|
||||
"3": "Device Code",
|
||||
"4": "Token Exchange"
|
||||
},
|
||||
"AUTHMETHOD": {
|
||||
"0": "Basic",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -2393,7 +2393,8 @@
|
||||
"0": "Код авторизации",
|
||||
"1": "Скрытый",
|
||||
"2": "Обновить токен",
|
||||
"3": "Код устройства"
|
||||
"3": "Код устройства",
|
||||
"4": "Обмен токенов"
|
||||
},
|
||||
"AUTHMETHOD": {
|
||||
"0": "Basic",
|
||||
|
@ -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
6
go.mod
@ -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
8
go.sum
@ -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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ func TestAMR(t *testing.T) {
|
||||
args{
|
||||
nil,
|
||||
},
|
||||
[]string{},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"pw checked",
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
})
|
||||
|
@ -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) }()
|
||||
|
347
internal/api/oidc/token_exchange.go
Normal file
347
internal/api/oidc/token_exchange.go
Normal 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
|
||||
}
|
98
internal/api/oidc/token_exchange_converter.go
Normal file
98
internal/api/oidc/token_exchange_converter.go
Normal 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,
|
||||
}
|
||||
}
|
590
internal/api/oidc/token_exchange_integration_test.go
Normal file
590
internal/api/oidc/token_exchange_integration_test.go
Normal 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())
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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...)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -91,6 +91,7 @@ const (
|
||||
OIDCGrantTypeImplicit
|
||||
OIDCGrantTypeRefreshToken
|
||||
OIDCGrantTypeDeviceCode
|
||||
OIDCGrantTypeTokenExchange
|
||||
)
|
||||
|
||||
type OIDCApplicationType int32
|
||||
|
@ -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"`
|
||||
}
|
||||
|
116
internal/domain/tokenreason_enumer.go
Normal file
116
internal/domain/tokenreason_enumer.go
Normal 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
|
||||
}
|
@ -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:
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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()),
|
||||
|
57
internal/integration/user.go
Normal file
57
internal/integration/user.go
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -67,6 +67,10 @@ func (*systemFeatureProjection) Reducers() []handler.AggregateReducer {
|
||||
Event: feature_v2.SystemUserSchemaEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
{
|
||||
Event: feature_v2.SystemTokenExchangeEventType,
|
||||
Reduce: reduceSystemSetFeature[bool],
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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]{
|
||||
|
@ -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]])
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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:"-"`
|
||||
|
||||
|
@ -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: Потребителското име е освободено
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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é
|
||||
|
@ -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
|
||||
|
@ -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: ユーザー名の解放
|
||||
|
@ -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: Корисничкото име е ослободено
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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: Имя пользователя опубликовано
|
||||
|
@ -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: 用户名已发布
|
||||
|
@ -23,6 +23,7 @@ type RefreshTokenView struct {
|
||||
Scopes []string
|
||||
Sequence uint64
|
||||
Token string
|
||||
Actor *domain.TokenActor
|
||||
}
|
||||
|
||||
type RefreshTokenSearchRequest struct {
|
||||
|
@ -22,6 +22,8 @@ type TokenView struct {
|
||||
PreferredLanguage string
|
||||
RefreshTokenID string
|
||||
IsPAT bool
|
||||
Reason domain.TokenReason
|
||||
Actor *domain.TokenActor
|
||||
}
|
||||
|
||||
type TokenSearchRequest struct {
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user