fix: use of generic oauth provider (#5345)

Adds a id_attribute to the GenericOAuthProvider, which is used to map the external User. Further mapping can be done in actions by using the `rawInfo` of the new `ctx.v1.providerInfo` field.
This commit is contained in:
Livio Spring 2023-03-03 11:38:49 +01:00 committed by GitHub
parent cfe00ef0d0
commit 2efa305e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 456 additions and 98 deletions

View File

@ -26,6 +26,9 @@ The first parameter contains the following fields
This is a verification errors string representation. If the verification succeeds, this is "none" This is a verification errors string representation. If the verification succeeds, this is "none"
- `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request) - `authRequest` [*auth request*](/docs/apis/actions/objects#auth-request)
- `httpRequest` [*http request*](/docs/apis/actions/objects#http-request) - `httpRequest` [*http request*](/docs/apis/actions/objects#http-request)
- `providerInfo` *Any*
Returns the response of the provider. In case the provider is a Generic OAuth Provider, the information is accessible through:
- `rawInfo` *Any*
- `api` - `api`
The second parameter contains the following fields The second parameter contains the following fields
- `v1` - `v1`

View File

@ -210,6 +210,7 @@ func addGenericOAuthProviderToCommand(req *admin_pb.AddGenericOAuthProviderReque
TokenEndpoint: req.TokenEndpoint, TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint, UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes, Scopes: req.Scopes,
IDAttribute: req.IdAttribute,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
} }
} }
@ -223,6 +224,7 @@ func updateGenericOAuthProviderToCommand(req *admin_pb.UpdateGenericOAuthProvide
TokenEndpoint: req.TokenEndpoint, TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint, UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes, Scopes: req.Scopes,
IDAttribute: req.IdAttribute,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
} }
} }

View File

@ -227,6 +227,7 @@ func addGenericOAuthProviderToCommand(req *mgmt_pb.AddGenericOAuthProviderReques
TokenEndpoint: req.TokenEndpoint, TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint, UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes, Scopes: req.Scopes,
IDAttribute: req.IdAttribute,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
} }
} }
@ -240,6 +241,7 @@ func updateGenericOAuthProviderToCommand(req *mgmt_pb.UpdateGenericOAuthProvider
TokenEndpoint: req.TokenEndpoint, TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint, UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes, Scopes: req.Scopes,
IDAttribute: req.IdAttribute,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions), IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
} }
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/zitadel/zitadel/internal/actions/object" "github.com/zitadel/zitadel/internal/actions/object"
"github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/idp"
) )
func (l *Login) runPostExternalAuthenticationActions( func (l *Login) runPostExternalAuthenticationActions(
@ -20,6 +21,7 @@ func (l *Login) runPostExternalAuthenticationActions(
tokens *oidc.Tokens, tokens *oidc.Tokens,
authRequest *domain.AuthRequest, authRequest *domain.AuthRequest,
httpRequest *http.Request, httpRequest *http.Request,
idpUser idp.User,
authenticationError error, authenticationError error,
) (*domain.ExternalUser, error) { ) (*domain.ExternalUser, error) {
ctx := httpRequest.Context() ctx := httpRequest.Context()
@ -86,6 +88,9 @@ func (l *Login) runPostExternalAuthenticationActions(
actions.SetFields("externalUser", func(c *actions.FieldConfig) interface{} { actions.SetFields("externalUser", func(c *actions.FieldConfig) interface{} {
return object.UserFromExternalUser(c, user) return object.UserFromExternalUser(c, user)
}), }),
actions.SetFields("providerInfo", func(c *actions.FieldConfig) interface{} {
return c.Runtime.ToValue(idpUser)
}),
actions.SetFields("authRequest", object.AuthRequestField(authRequest)), actions.SetFields("authRequest", object.AuthRequestField(authRequest)),
actions.SetFields("httpRequest", object.HTTPRequestField(httpRequest)), actions.SetFields("httpRequest", object.HTTPRequestField(httpRequest)),
actions.SetFields("authError", authErrStr), actions.SetFields("authError", authErrStr),
@ -337,18 +342,39 @@ func (l *Login) runPostCreationActions(
} }
func tokenCtxFields(tokens *oidc.Tokens) []actions.FieldOption { func tokenCtxFields(tokens *oidc.Tokens) []actions.FieldOption {
var accessToken, idToken string
getClaim := func(claim string) interface{} {
return nil
}
claimsJSON := func() (string, error) {
return "", nil
}
if tokens == nil {
return []actions.FieldOption{ return []actions.FieldOption{
actions.SetFields("accessToken", tokens.AccessToken), actions.SetFields("accessToken", accessToken),
actions.SetFields("idToken", tokens.IDToken), actions.SetFields("idToken", idToken),
actions.SetFields("getClaim", func(claim string) interface{} { actions.SetFields("getClaim", getClaim),
actions.SetFields("claimsJSON", claimsJSON),
}
}
accessToken = tokens.AccessToken
idToken = tokens.IDToken
if tokens.IDTokenClaims != nil {
getClaim = func(claim string) interface{} {
return tokens.IDTokenClaims.GetClaim(claim) return tokens.IDTokenClaims.GetClaim(claim)
}), }
actions.SetFields("claimsJSON", func() (string, error) { claimsJSON = func() (string, error) {
c, err := json.Marshal(tokens.IDTokenClaims) c, err := json.Marshal(tokens.IDTokenClaims)
if err != nil { if err != nil {
return "", err return "", err
} }
return string(c), nil return string(c), nil
}), }
}
return []actions.FieldOption{
actions.SetFields("accessToken", accessToken),
actions.SetFields("idToken", idToken),
actions.SetFields("getClaim", getClaim),
actions.SetFields("claimsJSON", claimsJSON),
} }
} }

View File

@ -20,6 +20,7 @@ import (
"github.com/zitadel/zitadel/internal/idp" "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/google" "github.com/zitadel/zitadel/internal/idp/providers/google"
"github.com/zitadel/zitadel/internal/idp/providers/jwt" "github.com/zitadel/zitadel/internal/idp/providers/jwt"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc" openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/query"
) )
@ -134,14 +135,15 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
} }
var provider idp.Provider var provider idp.Provider
switch identityProvider.Type { switch identityProvider.Type {
case domain.IDPTypeOAuth:
provider, err = l.oauthProvider(r.Context(), identityProvider)
case domain.IDPTypeOIDC: case domain.IDPTypeOIDC:
provider, err = l.oidcProvider(r.Context(), identityProvider) provider, err = l.oidcProvider(r.Context(), identityProvider)
case domain.IDPTypeJWT: case domain.IDPTypeJWT:
provider, err = l.jwtProvider(identityProvider) provider, err = l.jwtProvider(identityProvider)
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
provider, err = l.googleProvider(r.Context(), identityProvider) provider, err = l.googleProvider(r.Context(), identityProvider)
case domain.IDPTypeOAuth, case domain.IDPTypeLDAP,
domain.IDPTypeLDAP,
domain.IDPTypeAzureAD, domain.IDPTypeAzureAD,
domain.IDPTypeGitHub, domain.IDPTypeGitHub,
domain.IDPTypeGitHubEE, domain.IDPTypeGitHubEE,
@ -177,33 +179,39 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context()) userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID) authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
if err != nil { if err != nil {
l.externalAuthFailed(w, r, authReq, nil, err) l.externalAuthFailed(w, r, authReq, nil, nil, err)
return return
} }
identityProvider, err := l.getIDPByID(r, authReq.SelectedIDPConfigID) identityProvider, err := l.getIDPByID(r, authReq.SelectedIDPConfigID)
if err != nil { if err != nil {
l.externalAuthFailed(w, r, authReq, nil, err) l.externalAuthFailed(w, r, authReq, nil, nil, err)
return return
} }
var provider idp.Provider var provider idp.Provider
var session idp.Session var session idp.Session
switch identityProvider.Type { switch identityProvider.Type {
case domain.IDPTypeOAuth:
provider, err = l.oauthProvider(r.Context(), identityProvider)
if err != nil {
l.externalAuthFailed(w, r, authReq, nil, nil, err)
return
}
session = &oauth.Session{Provider: provider.(*oauth.Provider), Code: data.Code}
case domain.IDPTypeOIDC: case domain.IDPTypeOIDC:
provider, err = l.oidcProvider(r.Context(), identityProvider) provider, err = l.oidcProvider(r.Context(), identityProvider)
if err != nil { if err != nil {
l.externalAuthFailed(w, r, authReq, nil, err) l.externalAuthFailed(w, r, authReq, nil, nil, err)
return return
} }
session = &openid.Session{Provider: provider.(*openid.Provider), Code: data.Code} session = &openid.Session{Provider: provider.(*openid.Provider), Code: data.Code}
case domain.IDPTypeGoogle: case domain.IDPTypeGoogle:
provider, err = l.googleProvider(r.Context(), identityProvider) provider, err = l.googleProvider(r.Context(), identityProvider)
if err != nil { if err != nil {
l.externalAuthFailed(w, r, authReq, nil, err) l.externalAuthFailed(w, r, authReq, nil, nil, err)
return return
} }
session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code} session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code}
case domain.IDPTypeJWT, case domain.IDPTypeJWT,
domain.IDPTypeOAuth,
domain.IDPTypeLDAP, domain.IDPTypeLDAP,
domain.IDPTypeAzureAD, domain.IDPTypeAzureAD,
domain.IDPTypeGitHub, domain.IDPTypeGitHub,
@ -219,7 +227,7 @@ func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Reque
user, err := session.FetchUser(r.Context()) user, err := session.FetchUser(r.Context())
if err != nil { if err != nil {
l.externalAuthFailed(w, r, authReq, tokens(session), err) l.externalAuthFailed(w, r, authReq, tokens(session), user, err)
return return
} }
l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.renderNextStep) l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.renderNextStep)
@ -236,7 +244,7 @@ func (l *Login) handleExternalUserAuthenticated(
callback func(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest), callback func(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest),
) { ) {
externalUser := mapIDPUserToExternalUser(user, provider.ID) externalUser := mapIDPUserToExternalUser(user, provider.ID)
externalUser, err := l.runPostExternalAuthenticationActions(externalUser, tokens(session), authReq, r, nil) externalUser, err := l.runPostExternalAuthenticationActions(externalUser, tokens(session), authReq, r, user, nil)
if err != nil { if err != nil {
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
return return
@ -600,6 +608,31 @@ func (l *Login) jwtProvider(identityProvider *query.IDPTemplate) (*jwt.Provider,
) )
} }
func (l *Login) oauthProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*oauth.Provider, error) {
secret, err := crypto.DecryptString(identityProvider.OAuthIDPTemplate.ClientSecret, l.idpConfigAlg)
if err != nil {
return nil, err
}
config := &oauth2.Config{
ClientID: identityProvider.OAuthIDPTemplate.ClientID,
ClientSecret: secret,
Endpoint: oauth2.Endpoint{
AuthURL: identityProvider.OAuthIDPTemplate.AuthorizationEndpoint,
TokenURL: identityProvider.OAuthIDPTemplate.TokenEndpoint,
},
RedirectURL: l.baseURL(ctx) + EndpointExternalLoginCallback,
Scopes: identityProvider.OAuthIDPTemplate.Scopes,
}
return oauth.New(
config,
identityProvider.Name,
identityProvider.OAuthIDPTemplate.UserEndpoint,
func() idp.User {
return oauth.NewUserMapper(identityProvider.OAuthIDPTemplate.IDAttribute)
},
)
}
func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error { func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
if len(userGrants) == 0 { if len(userGrants) == 0 {
return nil return nil
@ -613,11 +646,8 @@ func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserG
return nil return nil
} }
func (l *Login) externalAuthFailed(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, tokens *oidc.Tokens, err error) { func (l *Login) externalAuthFailed(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, tokens *oidc.Tokens, user idp.User, err error) {
if tokens == nil { if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens, authReq, r, user, err); actionErr != nil {
tokens = &oidc.Tokens{Token: &oauth2.Token{}}
}
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens, authReq, r, err); actionErr != nil {
logging.WithError(err).Error("both external user authentication and action post authentication failed") logging.WithError(err).Error("both external user authentication and action post authentication failed")
} }
l.renderLogin(w, r, authReq, err) l.renderLogin(w, r, authReq, err)

View File

@ -66,8 +66,7 @@ func (l *Login) handleJWTRequest(w http.ResponseWriter, r *http.Request) {
func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, identityProvider *query.IDPTemplate) { func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, identityProvider *query.IDPTemplate) {
token, err := getToken(r, identityProvider.JWTIDPTemplate.HeaderName) token, err := getToken(r, identityProvider.JWTIDPTemplate.HeaderName)
if err != nil { if err != nil {
emptyTokens := &oidc.Tokens{Token: &oauth2.Token{}} if _, actionErr := l.runPostExternalAuthenticationActions(new(domain.ExternalUser), nil, authReq, r, nil, err); actionErr != nil {
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emptyTokens, authReq, r, err); actionErr != nil {
logging.WithError(err).Error("both external user authentication and action post authentication failed") logging.WithError(err).Error("both external user authentication and action post authentication failed")
} }
@ -76,8 +75,7 @@ func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, auth
} }
provider, err := l.jwtProvider(identityProvider) provider, err := l.jwtProvider(identityProvider)
if err != nil { if err != nil {
emptyTokens := &oidc.Tokens{Token: &oauth2.Token{}} if _, actionErr := l.runPostExternalAuthenticationActions(new(domain.ExternalUser), nil, authReq, r, nil, err); actionErr != nil {
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emptyTokens, authReq, r, err); actionErr != nil {
logging.WithError(err).Error("both external user authentication and action post authentication failed") logging.WithError(err).Error("both external user authentication and action post authentication failed")
} }
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)
@ -86,7 +84,7 @@ func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, auth
session := &jwt.Session{Provider: provider, Tokens: &oidc.Tokens{IDToken: token, Token: &oauth2.Token{}}} session := &jwt.Session{Provider: provider, Tokens: &oidc.Tokens{IDToken: token, Token: &oauth2.Token{}}}
user, err := session.FetchUser(r.Context()) user, err := session.FetchUser(r.Context())
if err != nil { if err != nil {
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens(session), authReq, r, err); actionErr != nil { if _, actionErr := l.runPostExternalAuthenticationActions(new(domain.ExternalUser), tokens(session), authReq, r, user, err); actionErr != nil {
logging.WithError(err).Error("both external user authentication and action post authentication failed") logging.WithError(err).Error("both external user authentication and action post authentication failed")
} }
l.renderError(w, r, authReq, err) l.renderError(w, r, authReq, err)

View File

@ -16,6 +16,7 @@ type GenericOAuthProvider struct {
TokenEndpoint string TokenEndpoint string
UserEndpoint string UserEndpoint string
Scopes []string Scopes []string
IDAttribute string
IDPOptions idp.Options IDPOptions idp.Options
} }

View File

@ -21,6 +21,7 @@ type OAuthIDPWriteModel struct {
TokenEndpoint string TokenEndpoint string
UserEndpoint string UserEndpoint string
Scopes []string Scopes []string
IDAttribute string
idp.Options idp.Options
State domain.IDPState State domain.IDPState
@ -48,6 +49,7 @@ func (wm *OAuthIDPWriteModel) reduceAddedEvent(e *idp.OAuthIDPAddedEvent) {
wm.TokenEndpoint = e.TokenEndpoint wm.TokenEndpoint = e.TokenEndpoint
wm.UserEndpoint = e.UserEndpoint wm.UserEndpoint = e.UserEndpoint
wm.Scopes = e.Scopes wm.Scopes = e.Scopes
wm.IDAttribute = e.IDAttribute
wm.State = domain.IDPStateActive wm.State = domain.IDPStateActive
} }
@ -73,6 +75,9 @@ func (wm *OAuthIDPWriteModel) reduceChangedEvent(e *idp.OAuthIDPChangedEvent) {
if e.Scopes != nil { if e.Scopes != nil {
wm.Scopes = e.Scopes wm.Scopes = e.Scopes
} }
if e.IDAttribute != nil {
wm.IDAttribute = *e.IDAttribute
}
wm.Options.ReduceChanges(e.OptionChanges) wm.Options.ReduceChanges(e.OptionChanges)
} }
@ -83,7 +88,8 @@ func (wm *OAuthIDPWriteModel) NewChanges(
secretCrypto crypto.Crypto, secretCrypto crypto.Crypto,
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint string, userEndpoint,
idAttribute string,
scopes []string, scopes []string,
options idp.Options, options idp.Options,
) ([]idp.OAuthIDPChanges, error) { ) ([]idp.OAuthIDPChanges, error) {
@ -115,6 +121,9 @@ func (wm *OAuthIDPWriteModel) NewChanges(
if !reflect.DeepEqual(wm.Scopes, scopes) { if !reflect.DeepEqual(wm.Scopes, scopes) {
changes = append(changes, idp.ChangeOAuthScopes(scopes)) changes = append(changes, idp.ChangeOAuthScopes(scopes))
} }
if wm.IDAttribute != idAttribute {
changes = append(changes, idp.ChangeOAuthIDAttribute(idAttribute))
}
opts := wm.Options.Changes(options) opts := wm.Options.Changes(options)
if !opts.IsZero() { if !opts.IsZero() {
changes = append(changes, idp.ChangeOAuthOptions(opts)) changes = append(changes, idp.ChangeOAuthOptions(opts))

View File

@ -273,6 +273,9 @@ func (c *Commands) prepareAddInstanceOAuthProvider(a *instance.Aggregate, writeM
if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" { if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Fb8jk", "Errors.Invalid.Argument") return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Fb8jk", "Errors.Invalid.Argument")
} }
if provider.IDAttribute = strings.TrimSpace(provider.IDAttribute); provider.IDAttribute == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-sdf3f", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
events, err := filter(ctx, writeModel.Query()) events, err := filter(ctx, writeModel.Query())
if err != nil { if err != nil {
@ -297,6 +300,7 @@ func (c *Commands) prepareAddInstanceOAuthProvider(a *instance.Aggregate, writeM
provider.AuthorizationEndpoint, provider.AuthorizationEndpoint,
provider.TokenEndpoint, provider.TokenEndpoint,
provider.UserEndpoint, provider.UserEndpoint,
provider.IDAttribute,
provider.Scopes, provider.Scopes,
provider.IDPOptions, provider.IDPOptions,
), ),
@ -322,6 +326,9 @@ func (c *Commands) prepareUpdateInstanceOAuthProvider(a *instance.Aggregate, wri
if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" { if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Fb8jk", "Errors.Invalid.Argument") return nil, caos_errs.ThrowInvalidArgument(nil, "INST-Fb8jk", "Errors.Invalid.Argument")
} }
if provider.IDAttribute = strings.TrimSpace(provider.IDAttribute); provider.IDAttribute == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INST-asf3fs", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
events, err := filter(ctx, writeModel.Query()) events, err := filter(ctx, writeModel.Query())
if err != nil { if err != nil {
@ -345,6 +352,7 @@ func (c *Commands) prepareUpdateInstanceOAuthProvider(a *instance.Aggregate, wri
provider.AuthorizationEndpoint, provider.AuthorizationEndpoint,
provider.TokenEndpoint, provider.TokenEndpoint,
provider.UserEndpoint, provider.UserEndpoint,
provider.IDAttribute,
provider.Scopes, provider.Scopes,
provider.IDPOptions, provider.IDPOptions,
) )

View File

@ -67,7 +67,8 @@ func (wm *InstanceOAuthIDPWriteModel) NewChangedEvent(
secretCrypto crypto.Crypto, secretCrypto crypto.Crypto,
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint string, userEndpoint,
idAttribute string,
scopes []string, scopes []string,
options idp.Options, options idp.Options,
) (*instance.OAuthIDPChangedEvent, error) { ) (*instance.OAuthIDPChangedEvent, error) {
@ -80,6 +81,7 @@ func (wm *InstanceOAuthIDPWriteModel) NewChangedEvent(
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint, userEndpoint,
idAttribute,
scopes, scopes,
options, options,
) )

View File

@ -145,6 +145,27 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) {
err: caos_errors.IsErrorInvalidArgument, err: caos_errors.IsErrorInvalidArgument,
}, },
}, },
{
"invalid id attribute",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
provider: GenericOAuthProvider{
Name: "name",
ClientID: "clientID",
ClientSecret: "clientSecret",
AuthorizationEndpoint: "auth",
TokenEndpoint: "token",
UserEndpoint: "user",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{ {
name: "ok", name: "ok",
fields: fields{ fields: fields{
@ -167,6 +188,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) {
"auth", "auth",
"token", "token",
"user", "user",
"idAttribute",
nil, nil,
idp.Options{}, idp.Options{},
)), )),
@ -185,6 +207,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) {
AuthorizationEndpoint: "auth", AuthorizationEndpoint: "auth",
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
IDAttribute: "idAttribute",
}, },
}, },
res: res{ res: res{
@ -214,6 +237,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) {
"auth", "auth",
"token", "token",
"user", "user",
"idAttribute",
[]string{"user"}, []string{"user"},
idp.Options{ idp.Options{
IsCreationAllowed: true, IsCreationAllowed: true,
@ -238,6 +262,7 @@ func TestCommandSide_AddInstanceGenericOAuthIDP(t *testing.T) {
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
Scopes: []string{"user"}, Scopes: []string{"user"},
IDAttribute: "idAttribute",
IDPOptions: idp.Options{ IDPOptions: idp.Options{
IsCreationAllowed: true, IsCreationAllowed: true,
IsLinkingAllowed: true, IsLinkingAllowed: true,
@ -390,6 +415,26 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) {
err: caos_errors.IsErrorInvalidArgument, err: caos_errors.IsErrorInvalidArgument,
}, },
}, },
{
"invalid id attribute",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: authz.WithInstanceID(context.Background(), "instance1"),
id: "id1",
provider: GenericOAuthProvider{
Name: "name",
ClientID: "clientID",
AuthorizationEndpoint: "auth",
TokenEndpoint: "token",
UserEndpoint: "user",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{ {
name: "not found", name: "not found",
fields: fields{ fields: fields{
@ -406,6 +451,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) {
AuthorizationEndpoint: "auth", AuthorizationEndpoint: "auth",
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
IDAttribute: "idAttribute",
}, },
}, },
res: res{ res: res{
@ -431,6 +477,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) {
"auth", "auth",
"token", "token",
"user", "user",
"idAttribute",
nil, nil,
idp.Options{}, idp.Options{},
)), )),
@ -446,6 +493,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) {
AuthorizationEndpoint: "auth", AuthorizationEndpoint: "auth",
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
IDAttribute: "idAttribute",
}, },
}, },
res: res{ res: res{
@ -471,6 +519,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) {
"auth", "auth",
"token", "token",
"user", "user",
"idAttribute",
nil, nil,
idp.Options{}, idp.Options{},
)), )),
@ -496,6 +545,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) {
idp.ChangeOAuthTokenEndpoint("new token"), idp.ChangeOAuthTokenEndpoint("new token"),
idp.ChangeOAuthUserEndpoint("new user"), idp.ChangeOAuthUserEndpoint("new user"),
idp.ChangeOAuthScopes([]string{"openid", "profile"}), idp.ChangeOAuthScopes([]string{"openid", "profile"}),
idp.ChangeOAuthIDAttribute("newAttribute"),
idp.ChangeOAuthOptions(idp.OptionChanges{ idp.ChangeOAuthOptions(idp.OptionChanges{
IsCreationAllowed: &t, IsCreationAllowed: &t,
IsLinkingAllowed: &t, IsLinkingAllowed: &t,
@ -523,6 +573,7 @@ func TestCommandSide_UpdateInstanceGenericOAuthIDP(t *testing.T) {
TokenEndpoint: "new token", TokenEndpoint: "new token",
UserEndpoint: "new user", UserEndpoint: "new user",
Scopes: []string{"openid", "profile"}, Scopes: []string{"openid", "profile"},
IDAttribute: "newAttribute",
IDPOptions: idp.Options{ IDPOptions: idp.Options{
IsCreationAllowed: true, IsCreationAllowed: true,
IsLinkingAllowed: true, IsLinkingAllowed: true,

View File

@ -262,6 +262,9 @@ func (c *Commands) prepareAddOrgOAuthProvider(a *org.Aggregate, writeModel *OrgO
if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" { if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Fb8jk", "Errors.Invalid.Argument") return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Fb8jk", "Errors.Invalid.Argument")
} }
if provider.IDAttribute = strings.TrimSpace(provider.IDAttribute); provider.IDAttribute == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-sadf3d", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
events, err := filter(ctx, writeModel.Query()) events, err := filter(ctx, writeModel.Query())
if err != nil { if err != nil {
@ -286,6 +289,7 @@ func (c *Commands) prepareAddOrgOAuthProvider(a *org.Aggregate, writeModel *OrgO
provider.AuthorizationEndpoint, provider.AuthorizationEndpoint,
provider.TokenEndpoint, provider.TokenEndpoint,
provider.UserEndpoint, provider.UserEndpoint,
provider.IDAttribute,
provider.Scopes, provider.Scopes,
provider.IDPOptions, provider.IDPOptions,
), ),
@ -314,6 +318,9 @@ func (c *Commands) prepareUpdateOrgOAuthProvider(a *org.Aggregate, writeModel *O
if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" { if provider.UserEndpoint = strings.TrimSpace(provider.UserEndpoint); provider.UserEndpoint == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Fb8jk", "Errors.Invalid.Argument") return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-Fb8jk", "Errors.Invalid.Argument")
} }
if provider.IDAttribute = strings.TrimSpace(provider.IDAttribute); provider.IDAttribute == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-SAe4gh", "Errors.Invalid.Argument")
}
return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) {
events, err := filter(ctx, writeModel.Query()) events, err := filter(ctx, writeModel.Query())
if err != nil { if err != nil {
@ -337,6 +344,7 @@ func (c *Commands) prepareUpdateOrgOAuthProvider(a *org.Aggregate, writeModel *O
provider.AuthorizationEndpoint, provider.AuthorizationEndpoint,
provider.TokenEndpoint, provider.TokenEndpoint,
provider.UserEndpoint, provider.UserEndpoint,
provider.IDAttribute,
provider.Scopes, provider.Scopes,
provider.IDPOptions, provider.IDPOptions,
) )

View File

@ -69,7 +69,8 @@ func (wm *OrgOAuthIDPWriteModel) NewChangedEvent(
secretCrypto crypto.Crypto, secretCrypto crypto.Crypto,
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint string, userEndpoint,
idAttribute string,
scopes []string, scopes []string,
options idp.Options, options idp.Options,
) (*org.OAuthIDPChangedEvent, error) { ) (*org.OAuthIDPChangedEvent, error) {
@ -82,6 +83,7 @@ func (wm *OrgOAuthIDPWriteModel) NewChangedEvent(
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint, userEndpoint,
idAttribute,
scopes, scopes,
options, options,
) )

View File

@ -150,6 +150,28 @@ func TestCommandSide_AddOrgGenericOAuthIDP(t *testing.T) {
err: caos_errors.IsErrorInvalidArgument, err: caos_errors.IsErrorInvalidArgument,
}, },
}, },
{
"invalid id attribute",
fields{
eventstore: eventstoreExpect(t),
idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id1"),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
provider: GenericOAuthProvider{
Name: "name",
ClientID: "clientID",
ClientSecret: "clientSecret",
AuthorizationEndpoint: "auth",
TokenEndpoint: "token",
UserEndpoint: "user",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{ {
name: "ok", name: "ok",
fields: fields{ fields: fields{
@ -170,6 +192,7 @@ func TestCommandSide_AddOrgGenericOAuthIDP(t *testing.T) {
"auth", "auth",
"token", "token",
"user", "user",
"idAttribute",
nil, nil,
idp.Options{}, idp.Options{},
)), )),
@ -188,6 +211,7 @@ func TestCommandSide_AddOrgGenericOAuthIDP(t *testing.T) {
AuthorizationEndpoint: "auth", AuthorizationEndpoint: "auth",
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
IDAttribute: "idAttribute",
}, },
}, },
res: res{ res: res{
@ -215,6 +239,7 @@ func TestCommandSide_AddOrgGenericOAuthIDP(t *testing.T) {
"auth", "auth",
"token", "token",
"user", "user",
"idAttribute",
[]string{"user"}, []string{"user"},
idp.Options{ idp.Options{
IsCreationAllowed: true, IsCreationAllowed: true,
@ -239,6 +264,7 @@ func TestCommandSide_AddOrgGenericOAuthIDP(t *testing.T) {
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
Scopes: []string{"user"}, Scopes: []string{"user"},
IDAttribute: "idAttribute",
IDPOptions: idp.Options{ IDPOptions: idp.Options{
IsCreationAllowed: true, IsCreationAllowed: true,
IsLinkingAllowed: true, IsLinkingAllowed: true,
@ -398,6 +424,27 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) {
err: caos_errors.IsErrorInvalidArgument, err: caos_errors.IsErrorInvalidArgument,
}, },
}, },
{
"invalid id attribute",
fields{
eventstore: eventstoreExpect(t),
},
args{
ctx: context.Background(),
resourceOwner: "org1",
id: "id1",
provider: GenericOAuthProvider{
Name: "name",
ClientID: "clientID",
AuthorizationEndpoint: "auth",
TokenEndpoint: "token",
UserEndpoint: "user",
},
},
res{
err: caos_errors.IsErrorInvalidArgument,
},
},
{ {
name: "not found", name: "not found",
fields: fields{ fields: fields{
@ -415,6 +462,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) {
AuthorizationEndpoint: "auth", AuthorizationEndpoint: "auth",
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
IDAttribute: "idAttribute",
}, },
}, },
res: res{ res: res{
@ -440,6 +488,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) {
"auth", "auth",
"token", "token",
"user", "user",
"idAttribute",
nil, nil,
idp.Options{}, idp.Options{},
)), )),
@ -456,6 +505,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) {
AuthorizationEndpoint: "auth", AuthorizationEndpoint: "auth",
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
IDAttribute: "idAttribute",
}, },
}, },
res: res{ res: res{
@ -481,6 +531,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) {
"auth", "auth",
"token", "token",
"user", "user",
"idAttribute",
nil, nil,
idp.Options{}, idp.Options{},
)), )),
@ -504,6 +555,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) {
idp.ChangeOAuthTokenEndpoint("new token"), idp.ChangeOAuthTokenEndpoint("new token"),
idp.ChangeOAuthUserEndpoint("new user"), idp.ChangeOAuthUserEndpoint("new user"),
idp.ChangeOAuthScopes([]string{"openid", "profile"}), idp.ChangeOAuthScopes([]string{"openid", "profile"}),
idp.ChangeOAuthIDAttribute("newAttribute"),
idp.ChangeOAuthOptions(idp.OptionChanges{ idp.ChangeOAuthOptions(idp.OptionChanges{
IsCreationAllowed: &t, IsCreationAllowed: &t,
IsLinkingAllowed: &t, IsLinkingAllowed: &t,
@ -531,6 +583,7 @@ func TestCommandSide_UpdateOrgGenericOAuthIDP(t *testing.T) {
TokenEndpoint: "new token", TokenEndpoint: "new token",
UserEndpoint: "new user", UserEndpoint: "new user",
Scopes: []string{"openid", "profile"}, Scopes: []string{"openid", "profile"},
IDAttribute: "newAttribute",
IDPOptions: idp.Options{ IDPOptions: idp.Options{
IsCreationAllowed: true, IsCreationAllowed: true,
IsLinkingAllowed: true, IsLinkingAllowed: true,

View File

@ -2,6 +2,8 @@ package oauth
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strconv"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -11,92 +13,97 @@ import (
var _ idp.User = (*UserMapper)(nil) var _ idp.User = (*UserMapper)(nil)
// UserMapper is an implementation of [idp.User]. // UserMapper is an implementation of [idp.User].
// It can be used in ZITADEL actions to map the raw `info` // It can be used in ZITADEL actions to map the `RawInfo`
type UserMapper struct { type UserMapper struct {
ID string idAttribute string
FirstName string RawInfo map[string]interface{}
LastName string }
DisplayName string
NickName string func NewUserMapper(idAttribute string) *UserMapper {
PreferredUsername string return &UserMapper{
Email string idAttribute: idAttribute,
EmailVerified bool RawInfo: make(map[string]interface{}),
Phone string }
PhoneVerified bool
PreferredLanguage string
AvatarURL string
Profile string
info map[string]interface{}
} }
func (u *UserMapper) UnmarshalJSON(data []byte) error { func (u *UserMapper) UnmarshalJSON(data []byte) error {
if u.info == nil { return json.Unmarshal(data, &u.RawInfo)
u.info = make(map[string]interface{})
}
return json.Unmarshal(data, &u.info)
} }
// GetID is an implementation of the [idp.User] interface. // GetID is an implementation of the [idp.User] interface.
func (u *UserMapper) GetID() string { func (u *UserMapper) GetID() string {
return u.ID id, ok := u.RawInfo[u.idAttribute]
if !ok {
return ""
}
switch i := id.(type) {
case string:
return i
case int:
return strconv.Itoa(i)
case float64:
return strconv.FormatFloat(i, 'f', -1, 64)
default:
return fmt.Sprint(i)
}
} }
// GetFirstName is an implementation of the [idp.User] interface. // GetFirstName is an implementation of the [idp.User] interface.
func (u *UserMapper) GetFirstName() string { func (u *UserMapper) GetFirstName() string {
return u.FirstName return ""
} }
// GetLastName is an implementation of the [idp.User] interface. // GetLastName is an implementation of the [idp.User] interface.
func (u *UserMapper) GetLastName() string { func (u *UserMapper) GetLastName() string {
return u.LastName return ""
} }
// GetDisplayName is an implementation of the [idp.User] interface. // GetDisplayName is an implementation of the [idp.User] interface.
func (u *UserMapper) GetDisplayName() string { func (u *UserMapper) GetDisplayName() string {
return u.DisplayName return ""
} }
// GetNickname is an implementation of the [idp.User] interface. // GetNickname is an implementation of the [idp.User] interface.
func (u *UserMapper) GetNickname() string { func (u *UserMapper) GetNickname() string {
return u.NickName return ""
} }
// GetPreferredUsername is an implementation of the [idp.User] interface. // GetPreferredUsername is an implementation of the [idp.User] interface.
func (u *UserMapper) GetPreferredUsername() string { func (u *UserMapper) GetPreferredUsername() string {
return u.PreferredUsername return ""
} }
// GetEmail is an implementation of the [idp.User] interface. // GetEmail is an implementation of the [idp.User] interface.
func (u *UserMapper) GetEmail() string { func (u *UserMapper) GetEmail() string {
return u.Email return ""
} }
// IsEmailVerified is an implementation of the [idp.User] interface. // IsEmailVerified is an implementation of the [idp.User] interface.
func (u *UserMapper) IsEmailVerified() bool { func (u *UserMapper) IsEmailVerified() bool {
return u.EmailVerified return false
} }
// GetPhone is an implementation of the [idp.User] interface. // GetPhone is an implementation of the [idp.User] interface.
func (u *UserMapper) GetPhone() string { func (u *UserMapper) GetPhone() string {
return u.Phone return ""
} }
// IsPhoneVerified is an implementation of the [idp.User] interface. // IsPhoneVerified is an implementation of the [idp.User] interface.
func (u *UserMapper) IsPhoneVerified() bool { func (u *UserMapper) IsPhoneVerified() bool {
return u.PhoneVerified return false
} }
// GetPreferredLanguage is an implementation of the [idp.User] interface. // GetPreferredLanguage is an implementation of the [idp.User] interface.
func (u *UserMapper) GetPreferredLanguage() language.Tag { func (u *UserMapper) GetPreferredLanguage() language.Tag {
return language.Make(u.PreferredLanguage) return language.Und
} }
// GetAvatarURL is an implementation of the [idp.User] interface. // GetAvatarURL is an implementation of the [idp.User] interface.
func (u *UserMapper) GetAvatarURL() string { func (u *UserMapper) GetAvatarURL() string {
return u.AvatarURL return ""
} }
// GetProfile is an implementation of the [idp.User] interface. // GetProfile is an implementation of the [idp.User] interface.
func (u *UserMapper) GetProfile() string { func (u *UserMapper) GetProfile() string {
return u.Profile return ""
} }

View File

@ -93,9 +93,7 @@ func TestProvider_FetchUser(t *testing.T) {
Reply(http.StatusInternalServerError) Reply(http.StatusInternalServerError)
}, },
userMapper: func() idp.User { userMapper: func() idp.User {
return &UserMapper{ return NewUserMapper("userID")
ID: "userID",
}
}, },
authURL: "https://oauth2.com/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=user&state=testState", authURL: "https://oauth2.com/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=user&state=testState",
tokens: &oidc.Tokens{ tokens: &oidc.Tokens{
@ -135,7 +133,7 @@ func TestProvider_FetchUser(t *testing.T) {
}) })
}, },
userMapper: func() idp.User { userMapper: func() idp.User {
return &UserMapper{} return NewUserMapper("userID")
}, },
authURL: "https://issuer.com/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=user&state=testState", authURL: "https://issuer.com/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=user&state=testState",
tokens: &oidc.Tokens{ tokens: &oidc.Tokens{
@ -147,12 +145,13 @@ func TestProvider_FetchUser(t *testing.T) {
}, },
want: want{ want: want{
user: &UserMapper{ user: &UserMapper{
info: map[string]interface{}{ idAttribute: "userID",
RawInfo: map[string]interface{}{
"userID": "id", "userID": "id",
"custom": "claim", "custom": "claim",
}, },
}, },
id: "", id: "id",
firstName: "", firstName: "",
lastName: "", lastName: "",
displayName: "", displayName: "",
@ -202,7 +201,7 @@ func TestProvider_FetchUser(t *testing.T) {
}) })
}, },
userMapper: func() idp.User { userMapper: func() idp.User {
return &UserMapper{} return NewUserMapper("userID")
}, },
authURL: "https://issuer.com/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=user&state=testState", authURL: "https://issuer.com/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=user&state=testState",
tokens: nil, tokens: nil,
@ -210,12 +209,13 @@ func TestProvider_FetchUser(t *testing.T) {
}, },
want: want{ want: want{
user: &UserMapper{ user: &UserMapper{
info: map[string]interface{}{ idAttribute: "userID",
RawInfo: map[string]interface{}{
"userID": "id", "userID": "id",
"custom": "claim", "custom": "claim",
}, },
}, },
id: "", id: "id",
firstName: "", firstName: "",
lastName: "", lastName: "",
displayName: "", displayName: "",

View File

@ -54,6 +54,7 @@ type OAuthIDPTemplate struct {
TokenEndpoint string TokenEndpoint string
UserEndpoint string UserEndpoint string
Scopes database.StringArray Scopes database.StringArray
IDAttribute string
} }
type OIDCIDPTemplate struct { type OIDCIDPTemplate struct {
@ -196,6 +197,10 @@ var (
name: projection.OAuthScopesCol, name: projection.OAuthScopesCol,
table: oauthIdpTemplateTable, table: oauthIdpTemplateTable,
} }
OAuthIDAttributeCol = Column{
name: projection.OAuthIDAttributeCol,
table: oauthIdpTemplateTable,
}
) )
var ( var (
@ -505,6 +510,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
OAuthTokenEndpointCol.identifier(), OAuthTokenEndpointCol.identifier(),
OAuthUserEndpointCol.identifier(), OAuthUserEndpointCol.identifier(),
OAuthScopesCol.identifier(), OAuthScopesCol.identifier(),
OAuthIDAttributeCol.identifier(),
// oidc // oidc
OIDCIDCol.identifier(), OIDCIDCol.identifier(),
OIDCIssuerCol.identifier(), OIDCIssuerCol.identifier(),
@ -564,6 +570,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
oauthTokenEndpoint := sql.NullString{} oauthTokenEndpoint := sql.NullString{}
oauthUserEndpoint := sql.NullString{} oauthUserEndpoint := sql.NullString{}
oauthScopes := database.StringArray{} oauthScopes := database.StringArray{}
oauthIDAttribute := sql.NullString{}
oidcID := sql.NullString{} oidcID := sql.NullString{}
oidcIssuer := sql.NullString{} oidcIssuer := sql.NullString{}
@ -627,6 +634,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
&oauthTokenEndpoint, &oauthTokenEndpoint,
&oauthUserEndpoint, &oauthUserEndpoint,
&oauthScopes, &oauthScopes,
&oauthIDAttribute,
// oidc // oidc
&oidcID, &oidcID,
&oidcIssuer, &oidcIssuer,
@ -686,6 +694,7 @@ func prepareIDPTemplateByIDQuery(ctx context.Context, db prepareDatabase) (sq.Se
TokenEndpoint: oauthTokenEndpoint.String, TokenEndpoint: oauthTokenEndpoint.String,
UserEndpoint: oauthUserEndpoint.String, UserEndpoint: oauthUserEndpoint.String,
Scopes: oauthScopes, Scopes: oauthScopes,
IDAttribute: oauthIDAttribute.String,
} }
} }
if oidcID.Valid { if oidcID.Valid {
@ -770,6 +779,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
OAuthTokenEndpointCol.identifier(), OAuthTokenEndpointCol.identifier(),
OAuthUserEndpointCol.identifier(), OAuthUserEndpointCol.identifier(),
OAuthScopesCol.identifier(), OAuthScopesCol.identifier(),
OAuthIDAttributeCol.identifier(),
// oidc // oidc
OIDCIDCol.identifier(), OIDCIDCol.identifier(),
OIDCIssuerCol.identifier(), OIDCIssuerCol.identifier(),
@ -833,6 +843,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
oauthTokenEndpoint := sql.NullString{} oauthTokenEndpoint := sql.NullString{}
oauthUserEndpoint := sql.NullString{} oauthUserEndpoint := sql.NullString{}
oauthScopes := database.StringArray{} oauthScopes := database.StringArray{}
oauthIDAttribute := sql.NullString{}
oidcID := sql.NullString{} oidcID := sql.NullString{}
oidcIssuer := sql.NullString{} oidcIssuer := sql.NullString{}
@ -896,6 +907,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
&oauthTokenEndpoint, &oauthTokenEndpoint,
&oauthUserEndpoint, &oauthUserEndpoint,
&oauthScopes, &oauthScopes,
&oauthIDAttribute,
// oidc // oidc
&oidcID, &oidcID,
&oidcIssuer, &oidcIssuer,
@ -954,6 +966,7 @@ func prepareIDPTemplatesQuery(ctx context.Context, db prepareDatabase) (sq.Selec
TokenEndpoint: oauthTokenEndpoint.String, TokenEndpoint: oauthTokenEndpoint.String,
UserEndpoint: oauthUserEndpoint.String, UserEndpoint: oauthUserEndpoint.String,
Scopes: oauthScopes, Scopes: oauthScopes,
IDAttribute: oauthIDAttribute.String,
} }
} }
if oidcID.Valid { if oidcID.Valid {

View File

@ -29,13 +29,14 @@ var (
` projections.idp_templates2.is_auto_creation,` + ` projections.idp_templates2.is_auto_creation,` +
` projections.idp_templates2.is_auto_update,` + ` projections.idp_templates2.is_auto_update,` +
// oauth // oauth
` projections.idp_templates2_oauth.idp_id,` + ` projections.idp_templates2_oauth2.idp_id,` +
` projections.idp_templates2_oauth.client_id,` + ` projections.idp_templates2_oauth2.client_id,` +
` projections.idp_templates2_oauth.client_secret,` + ` projections.idp_templates2_oauth2.client_secret,` +
` projections.idp_templates2_oauth.authorization_endpoint,` + ` projections.idp_templates2_oauth2.authorization_endpoint,` +
` projections.idp_templates2_oauth.token_endpoint,` + ` projections.idp_templates2_oauth2.token_endpoint,` +
` projections.idp_templates2_oauth.user_endpoint,` + ` projections.idp_templates2_oauth2.user_endpoint,` +
` projections.idp_templates2_oauth.scopes,` + ` projections.idp_templates2_oauth2.scopes,` +
` projections.idp_templates2_oauth2.id_attribute,` +
// oidc // oidc
` projections.idp_templates2_oidc.idp_id,` + ` projections.idp_templates2_oidc.idp_id,` +
` projections.idp_templates2_oidc.issuer,` + ` projections.idp_templates2_oidc.issuer,` +
@ -77,7 +78,7 @@ var (
` projections.idp_templates2_ldap.avatar_url_attribute,` + ` projections.idp_templates2_ldap.avatar_url_attribute,` +
` projections.idp_templates2_ldap.profile_attribute` + ` projections.idp_templates2_ldap.profile_attribute` +
` FROM projections.idp_templates2` + ` FROM projections.idp_templates2` +
` LEFT JOIN projections.idp_templates2_oauth ON projections.idp_templates2.id = projections.idp_templates2_oauth.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_oauth.instance_id` + ` LEFT JOIN projections.idp_templates2_oauth2 ON projections.idp_templates2.id = projections.idp_templates2_oauth2.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_oauth2.instance_id` +
` LEFT JOIN projections.idp_templates2_oidc ON projections.idp_templates2.id = projections.idp_templates2_oidc.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_oidc.instance_id` + ` LEFT JOIN projections.idp_templates2_oidc ON projections.idp_templates2.id = projections.idp_templates2_oidc.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_oidc.instance_id` +
` LEFT JOIN projections.idp_templates2_jwt ON projections.idp_templates2.id = projections.idp_templates2_jwt.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_jwt.instance_id` + ` LEFT JOIN projections.idp_templates2_jwt ON projections.idp_templates2.id = projections.idp_templates2_jwt.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_jwt.instance_id` +
` LEFT JOIN projections.idp_templates2_google ON projections.idp_templates2.id = projections.idp_templates2_google.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_google.instance_id` + ` LEFT JOIN projections.idp_templates2_google ON projections.idp_templates2.id = projections.idp_templates2_google.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_google.instance_id` +
@ -105,6 +106,7 @@ var (
"token_endpoint", "token_endpoint",
"user_endpoint", "user_endpoint",
"scopes", "scopes",
"id_attribute",
// oidc config // oidc config
"id_id", "id_id",
"issuer", "issuer",
@ -160,13 +162,14 @@ var (
` projections.idp_templates2.is_auto_creation,` + ` projections.idp_templates2.is_auto_creation,` +
` projections.idp_templates2.is_auto_update,` + ` projections.idp_templates2.is_auto_update,` +
// oauth // oauth
` projections.idp_templates2_oauth.idp_id,` + ` projections.idp_templates2_oauth2.idp_id,` +
` projections.idp_templates2_oauth.client_id,` + ` projections.idp_templates2_oauth2.client_id,` +
` projections.idp_templates2_oauth.client_secret,` + ` projections.idp_templates2_oauth2.client_secret,` +
` projections.idp_templates2_oauth.authorization_endpoint,` + ` projections.idp_templates2_oauth2.authorization_endpoint,` +
` projections.idp_templates2_oauth.token_endpoint,` + ` projections.idp_templates2_oauth2.token_endpoint,` +
` projections.idp_templates2_oauth.user_endpoint,` + ` projections.idp_templates2_oauth2.user_endpoint,` +
` projections.idp_templates2_oauth.scopes,` + ` projections.idp_templates2_oauth2.scopes,` +
` projections.idp_templates2_oauth2.id_attribute,` +
// oidc // oidc
` projections.idp_templates2_oidc.idp_id,` + ` projections.idp_templates2_oidc.idp_id,` +
` projections.idp_templates2_oidc.issuer,` + ` projections.idp_templates2_oidc.issuer,` +
@ -209,7 +212,7 @@ var (
` projections.idp_templates2_ldap.profile_attribute,` + ` projections.idp_templates2_ldap.profile_attribute,` +
` COUNT(*) OVER ()` + ` COUNT(*) OVER ()` +
` FROM projections.idp_templates2` + ` FROM projections.idp_templates2` +
` LEFT JOIN projections.idp_templates2_oauth ON projections.idp_templates2.id = projections.idp_templates2_oauth.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_oauth.instance_id` + ` LEFT JOIN projections.idp_templates2_oauth2 ON projections.idp_templates2.id = projections.idp_templates2_oauth2.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_oauth2.instance_id` +
` LEFT JOIN projections.idp_templates2_oidc ON projections.idp_templates2.id = projections.idp_templates2_oidc.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_oidc.instance_id` + ` LEFT JOIN projections.idp_templates2_oidc ON projections.idp_templates2.id = projections.idp_templates2_oidc.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_oidc.instance_id` +
` LEFT JOIN projections.idp_templates2_jwt ON projections.idp_templates2.id = projections.idp_templates2_jwt.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_jwt.instance_id` + ` LEFT JOIN projections.idp_templates2_jwt ON projections.idp_templates2.id = projections.idp_templates2_jwt.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_jwt.instance_id` +
` LEFT JOIN projections.idp_templates2_google ON projections.idp_templates2.id = projections.idp_templates2_google.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_google.instance_id` + ` LEFT JOIN projections.idp_templates2_google ON projections.idp_templates2.id = projections.idp_templates2_google.idp_id AND projections.idp_templates2.instance_id = projections.idp_templates2_google.instance_id` +
@ -237,6 +240,7 @@ var (
"token_endpoint", "token_endpoint",
"user_endpoint", "user_endpoint",
"scopes", "scopes",
"id_attribute",
// oidc config // oidc config
"id_id", "id_id",
"issuer", "issuer",
@ -339,6 +343,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
"token", "token",
"user", "user",
database.StringArray{"profile"}, database.StringArray{"profile"},
"id-attribute",
// oidc // oidc
nil, nil,
nil, nil,
@ -404,6 +409,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
Scopes: []string{"profile"}, Scopes: []string{"profile"},
IDAttribute: "id-attribute",
}, },
}, },
}, },
@ -436,6 +442,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
"idp-id", "idp-id",
"issuer", "issuer",
@ -531,6 +538,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -626,6 +634,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -720,6 +729,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -833,6 +843,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -957,6 +968,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -1079,6 +1091,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -1176,6 +1189,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -1239,6 +1253,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -1302,6 +1317,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
"token", "token",
"user", "user",
database.StringArray{"profile"}, database.StringArray{"profile"},
"id-attribute",
// oidc // oidc
nil, nil,
nil, nil,
@ -1365,6 +1381,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
"idp-id-oidc", "idp-id-oidc",
"issuer", "issuer",
@ -1428,6 +1445,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
nil, nil,
nil, nil,
nil, nil,
nil,
// oidc // oidc
nil, nil,
nil, nil,
@ -1561,6 +1579,7 @@ func Test_IDPTemplateTemplatesPrepares(t *testing.T) {
TokenEndpoint: "token", TokenEndpoint: "token",
UserEndpoint: "user", UserEndpoint: "user",
Scopes: []string{"profile"}, Scopes: []string{"profile"},
IDAttribute: "id-attribute",
}, },
}, },
{ {

View File

@ -81,6 +81,10 @@ func (p *idpLoginPolicyLinkProjection) reducers() []handler.AggregateReducer {
Event: org.IDPConfigRemovedEventType, Event: org.IDPConfigRemovedEventType,
Reduce: p.reduceIDPConfigRemoved, Reduce: p.reduceIDPConfigRemoved,
}, },
{
Event: org.IDPRemovedEventType,
Reduce: p.reduceIDPRemoved,
},
{ {
Event: org.OrgRemovedEventType, Event: org.OrgRemovedEventType,
Reduce: p.reduceOwnerRemoved, Reduce: p.reduceOwnerRemoved,
@ -106,6 +110,10 @@ func (p *idpLoginPolicyLinkProjection) reducers() []handler.AggregateReducer {
Event: instance.IDPConfigRemovedEventType, Event: instance.IDPConfigRemovedEventType,
Reduce: p.reduceIDPConfigRemoved, Reduce: p.reduceIDPConfigRemoved,
}, },
{
Event: instance.IDPRemovedEventType,
Reduce: p.reduceIDPRemoved,
},
{ {
Event: instance.InstanceRemovedEventType, Event: instance.InstanceRemovedEventType,
Reduce: reduceInstanceRemovedHelper(IDPUserLinkInstanceIDCol), Reduce: reduceInstanceRemovedHelper(IDPUserLinkInstanceIDCol),
@ -209,6 +217,27 @@ func (p *idpLoginPolicyLinkProjection) reduceIDPConfigRemoved(event eventstore.E
), nil ), nil
} }
func (p *idpLoginPolicyLinkProjection) reduceIDPRemoved(event eventstore.Event) (*handler.Statement, error) {
var idpID string
switch e := event.(type) {
case *org.IDPRemovedEvent:
idpID = e.ID
case *instance.IDPRemovedEvent:
idpID = e.ID
default:
return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SFED3", "reduce.wrong.event.type %v", []eventstore.EventType{org.IDPRemovedEventType, instance.IDPRemovedEventType})
}
return crdb.NewDeleteStatement(event,
[]handler.Condition{
handler.NewCond(IDPLoginPolicyLinkIDPIDCol, idpID),
handler.NewCond(IDPLoginPolicyLinkResourceOwnerCol, event.Aggregate().ResourceOwner),
handler.NewCond(IDPLoginPolicyLinkInstanceIDCol, event.Aggregate().InstanceID),
},
), nil
}
func (p *idpLoginPolicyLinkProjection) reducePolicyRemoved(event eventstore.Event) (*handler.Statement, error) { func (p *idpLoginPolicyLinkProjection) reducePolicyRemoved(event eventstore.Event) (*handler.Statement, error) {
e, ok := event.(*org.LoginPolicyRemovedEvent) e, ok := event.(*org.LoginPolicyRemovedEvent)
if !ok { if !ok {

View File

@ -331,6 +331,66 @@ func TestIDPLoginPolicyLinkProjection_reduces(t *testing.T) {
}, },
}, },
}, },
{
name: "org IDPRemovedEvent",
args: args{
event: getEvent(testEvent(
repository.EventType(org.IDPRemovedEventType),
org.AggregateType,
[]byte(`{
"id": "id"
}`),
), org.IDPRemovedEventMapper),
},
reduce: (&idpLoginPolicyLinkProjection{}).reduceIDPRemoved,
want: wantReduce{
aggregateType: org.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.idp_login_policy_links4 WHERE (idp_id = $1) AND (resource_owner = $2) AND (instance_id = $3)",
expectedArgs: []interface{}{
"id",
"ro-id",
"instance-id",
},
},
},
},
},
},
{
name: "iam IDPRemovedEvent",
args: args{
event: getEvent(testEvent(
repository.EventType(instance.IDPRemovedEventType),
instance.AggregateType,
[]byte(`{
"id": "id"
}`),
), instance.IDPRemovedEventMapper),
},
reduce: (&idpLoginPolicyLinkProjection{}).reduceIDPRemoved,
want: wantReduce{
aggregateType: instance.AggregateType,
sequence: 15,
previousSequence: 10,
executer: &testExecuter{
executions: []execution{
{
expectedStmt: "DELETE FROM projections.idp_login_policy_links4 WHERE (idp_id = $1) AND (resource_owner = $2) AND (instance_id = $3)",
expectedArgs: []interface{}{
"id",
"ro-id",
"instance-id",
},
},
},
},
},
},
{ {
name: "org.reduceOwnerRemoved", name: "org.reduceOwnerRemoved",
reduce: (&idpLoginPolicyLinkProjection{}).reduceOwnerRemoved, reduce: (&idpLoginPolicyLinkProjection{}).reduceOwnerRemoved,

View File

@ -24,7 +24,7 @@ const (
IDPTemplateGoogleTable = IDPTemplateTable + "_" + IDPTemplateGoogleSuffix IDPTemplateGoogleTable = IDPTemplateTable + "_" + IDPTemplateGoogleSuffix
IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix IDPTemplateLDAPTable = IDPTemplateTable + "_" + IDPTemplateLDAPSuffix
IDPTemplateOAuthSuffix = "oauth" IDPTemplateOAuthSuffix = "oauth2"
IDPTemplateOIDCSuffix = "oidc" IDPTemplateOIDCSuffix = "oidc"
IDPTemplateJWTSuffix = "jwt" IDPTemplateJWTSuffix = "jwt"
IDPTemplateGoogleSuffix = "google" IDPTemplateGoogleSuffix = "google"
@ -54,6 +54,7 @@ const (
OAuthTokenEndpointCol = "token_endpoint" OAuthTokenEndpointCol = "token_endpoint"
OAuthUserEndpointCol = "user_endpoint" OAuthUserEndpointCol = "user_endpoint"
OAuthScopesCol = "scopes" OAuthScopesCol = "scopes"
OAuthIDAttributeCol = "id_attribute"
OIDCIDCol = "idp_id" OIDCIDCol = "idp_id"
OIDCInstanceIDCol = "instance_id" OIDCInstanceIDCol = "instance_id"
@ -139,6 +140,7 @@ func newIDPTemplateProjection(ctx context.Context, config crdb.StatementHandlerC
crdb.NewColumn(OAuthTokenEndpointCol, crdb.ColumnTypeText), crdb.NewColumn(OAuthTokenEndpointCol, crdb.ColumnTypeText),
crdb.NewColumn(OAuthUserEndpointCol, crdb.ColumnTypeText), crdb.NewColumn(OAuthUserEndpointCol, crdb.ColumnTypeText),
crdb.NewColumn(OAuthScopesCol, crdb.ColumnTypeTextArray, crdb.Nullable()), crdb.NewColumn(OAuthScopesCol, crdb.ColumnTypeTextArray, crdb.Nullable()),
crdb.NewColumn(OAuthIDAttributeCol, crdb.ColumnTypeText),
}, },
crdb.NewPrimaryKey(OAuthInstanceIDCol, OAuthIDCol), crdb.NewPrimaryKey(OAuthInstanceIDCol, OAuthIDCol),
IDPTemplateOAuthSuffix, IDPTemplateOAuthSuffix,
@ -417,6 +419,7 @@ func (p *idpTemplateProjection) reduceOAuthIDPAdded(event eventstore.Event) (*ha
handler.NewCol(OAuthTokenEndpointCol, idpEvent.TokenEndpoint), handler.NewCol(OAuthTokenEndpointCol, idpEvent.TokenEndpoint),
handler.NewCol(OAuthUserEndpointCol, idpEvent.UserEndpoint), handler.NewCol(OAuthUserEndpointCol, idpEvent.UserEndpoint),
handler.NewCol(OAuthScopesCol, database.StringArray(idpEvent.Scopes)), handler.NewCol(OAuthScopesCol, database.StringArray(idpEvent.Scopes)),
handler.NewCol(OAuthIDAttributeCol, idpEvent.IDAttribute),
}, },
crdb.WithTableSuffix(IDPTemplateOAuthSuffix), crdb.WithTableSuffix(IDPTemplateOAuthSuffix),
), ),
@ -1176,6 +1179,9 @@ func reduceOAuthIDPChangedColumns(idpEvent idp.OAuthIDPChangedEvent) []handler.C
if idpEvent.Scopes != nil { if idpEvent.Scopes != nil {
oauthCols = append(oauthCols, handler.NewCol(OAuthScopesCol, database.StringArray(idpEvent.Scopes))) oauthCols = append(oauthCols, handler.NewCol(OAuthScopesCol, database.StringArray(idpEvent.Scopes)))
} }
if idpEvent.IDAttribute != nil {
oauthCols = append(oauthCols, handler.NewCol(OAuthIDAttributeCol, *idpEvent.IDAttribute))
}
return oauthCols return oauthCols
} }

View File

@ -154,6 +154,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
"tokenEndpoint": "token", "tokenEndpoint": "token",
"userEndpoint": "user", "userEndpoint": "user",
"scopes": ["profile"], "scopes": ["profile"],
"idAttribute": "id-attribute",
"isCreationAllowed": true, "isCreationAllowed": true,
"isLinkingAllowed": true, "isLinkingAllowed": true,
"isAutoCreation": true, "isAutoCreation": true,
@ -188,7 +189,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
}, },
}, },
{ {
expectedStmt: "INSERT INTO projections.idp_templates2_oauth (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", expectedStmt: "INSERT INTO projections.idp_templates2_oauth2 (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"idp-id", "idp-id",
"instance-id", "instance-id",
@ -198,6 +199,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
"token", "token",
"user", "user",
database.StringArray{"profile"}, database.StringArray{"profile"},
"id-attribute",
}, },
}, },
}, },
@ -223,6 +225,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
"tokenEndpoint": "token", "tokenEndpoint": "token",
"userEndpoint": "user", "userEndpoint": "user",
"scopes": ["profile"], "scopes": ["profile"],
"idAttribute": "id-attribute",
"isCreationAllowed": true, "isCreationAllowed": true,
"isLinkingAllowed": true, "isLinkingAllowed": true,
"isAutoCreation": true, "isAutoCreation": true,
@ -257,7 +260,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
}, },
}, },
{ {
expectedStmt: "INSERT INTO projections.idp_templates2_oauth (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", expectedStmt: "INSERT INTO projections.idp_templates2_oauth2 (idp_id, instance_id, client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"idp-id", "idp-id",
"instance-id", "instance-id",
@ -267,6 +270,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
"token", "token",
"user", "user",
database.StringArray{"profile"}, database.StringArray{"profile"},
"id-attribute",
}, },
}, },
}, },
@ -304,7 +308,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
}, },
}, },
{ {
expectedStmt: "UPDATE projections.idp_templates2_oauth SET client_id = $1 WHERE (idp_id = $2) AND (instance_id = $3)", expectedStmt: "UPDATE projections.idp_templates2_oauth2 SET client_id = $1 WHERE (idp_id = $2) AND (instance_id = $3)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"id", "id",
"idp-id", "idp-id",
@ -334,6 +338,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
"tokenEndpoint": "token", "tokenEndpoint": "token",
"userEndpoint": "user", "userEndpoint": "user",
"scopes": ["profile"], "scopes": ["profile"],
"idAttribute": "id-attribute",
"isCreationAllowed": true, "isCreationAllowed": true,
"isLinkingAllowed": true, "isLinkingAllowed": true,
"isAutoCreation": true, "isAutoCreation": true,
@ -363,7 +368,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
}, },
}, },
{ {
expectedStmt: "UPDATE projections.idp_templates2_oauth SET (client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes) = ($1, $2, $3, $4, $5, $6) WHERE (idp_id = $7) AND (instance_id = $8)", expectedStmt: "UPDATE projections.idp_templates2_oauth2 SET (client_id, client_secret, authorization_endpoint, token_endpoint, user_endpoint, scopes, id_attribute) = ($1, $2, $3, $4, $5, $6, $7) WHERE (idp_id = $8) AND (instance_id = $9)",
expectedArgs: []interface{}{ expectedArgs: []interface{}{
"client_id", "client_id",
anyArg{}, anyArg{},
@ -371,6 +376,7 @@ func TestIDPTemplateProjection_reducesOAuth(t *testing.T) {
"token", "token",
"user", "user",
database.StringArray{"profile"}, database.StringArray{"profile"},
"id-attribute",
"idp-id", "idp-id",
"instance-id", "instance-id",
}, },

View File

@ -20,6 +20,7 @@ type OAuthIDPAddedEvent struct {
TokenEndpoint string `json:"tokenEndpoint,omitempty"` TokenEndpoint string `json:"tokenEndpoint,omitempty"`
UserEndpoint string `json:"userEndpoint,omitempty"` UserEndpoint string `json:"userEndpoint,omitempty"`
Scopes []string `json:"scopes,omitempty"` Scopes []string `json:"scopes,omitempty"`
IDAttribute string `json:"idAttribute,omitempty"`
Options Options
} }
@ -31,7 +32,8 @@ func NewOAuthIDPAddedEvent(
clientSecret *crypto.CryptoValue, clientSecret *crypto.CryptoValue,
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint string, userEndpoint,
idAttribute string,
scopes []string, scopes []string,
options Options, options Options,
) *OAuthIDPAddedEvent { ) *OAuthIDPAddedEvent {
@ -45,6 +47,7 @@ func NewOAuthIDPAddedEvent(
TokenEndpoint: tokenEndpoint, TokenEndpoint: tokenEndpoint,
UserEndpoint: userEndpoint, UserEndpoint: userEndpoint,
Scopes: scopes, Scopes: scopes,
IDAttribute: idAttribute,
Options: options, Options: options,
} }
} }
@ -81,6 +84,7 @@ type OAuthIDPChangedEvent struct {
TokenEndpoint *string `json:"tokenEndpoint,omitempty"` TokenEndpoint *string `json:"tokenEndpoint,omitempty"`
UserEndpoint *string `json:"userEndpoint,omitempty"` UserEndpoint *string `json:"userEndpoint,omitempty"`
Scopes []string `json:"scopes,omitempty"` Scopes []string `json:"scopes,omitempty"`
IDAttribute *string `json:"idAttribute,omitempty"`
OptionChanges OptionChanges
} }
@ -151,6 +155,12 @@ func ChangeOAuthScopes(scopes []string) func(*OAuthIDPChangedEvent) {
} }
} }
func ChangeOAuthIDAttribute(idAttribute string) func(*OAuthIDPChangedEvent) {
return func(e *OAuthIDPChangedEvent) {
e.IDAttribute = &idAttribute
}
}
func (e *OAuthIDPChangedEvent) Data() interface{} { func (e *OAuthIDPChangedEvent) Data() interface{} {
return e return e
} }

View File

@ -36,7 +36,8 @@ func NewOAuthIDPAddedEvent(
clientSecret *crypto.CryptoValue, clientSecret *crypto.CryptoValue,
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint string, userEndpoint,
idAttribute string,
scopes []string, scopes []string,
options idp.Options, options idp.Options,
) *OAuthIDPAddedEvent { ) *OAuthIDPAddedEvent {
@ -55,6 +56,7 @@ func NewOAuthIDPAddedEvent(
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint, userEndpoint,
idAttribute,
scopes, scopes,
options, options,
), ),

View File

@ -36,7 +36,8 @@ func NewOAuthIDPAddedEvent(
clientSecret *crypto.CryptoValue, clientSecret *crypto.CryptoValue,
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint string, userEndpoint,
idAttribute string,
scopes []string, scopes []string,
options idp.Options, options idp.Options,
) *OAuthIDPAddedEvent { ) *OAuthIDPAddedEvent {
@ -55,6 +56,7 @@ func NewOAuthIDPAddedEvent(
authorizationEndpoint, authorizationEndpoint,
tokenEndpoint, tokenEndpoint,
userEndpoint, userEndpoint,
idAttribute,
scopes, scopes,
options, options,
), ),

View File

@ -4343,7 +4343,9 @@ message AddGenericOAuthProviderRequest {
string token_endpoint = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; string token_endpoint = 5 [(validate.rules).string = {min_len: 1, max_len: 200}];
string user_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; string user_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
repeated string scopes = 7 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; repeated string scopes = 7 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}];
zitadel.idp.v1.Options provider_options = 8; // identifying attribute of the user in the response of the user_endpoint
string id_attribute = 8 [(validate.rules).string = {min_len: 1, max_len: 200}];
zitadel.idp.v1.Options provider_options = 9;
} }
message AddGenericOAuthProviderResponse { message AddGenericOAuthProviderResponse {
@ -4361,7 +4363,9 @@ message UpdateGenericOAuthProviderRequest {
string token_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; string token_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
string user_endpoint = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; string user_endpoint = 7 [(validate.rules).string = {min_len: 1, max_len: 200}];
repeated string scopes = 8 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; repeated string scopes = 8 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}];
zitadel.idp.v1.Options provider_options = 9; // identifying attribute of the user in the response of the user_endpoint
string id_attribute = 9 [(validate.rules).string = {min_len: 1, max_len: 200}];
zitadel.idp.v1.Options provider_options = 10;
} }
message UpdateGenericOAuthProviderResponse { message UpdateGenericOAuthProviderResponse {

View File

@ -275,6 +275,7 @@ message OAuthConfig {
string token_endpoint = 3; string token_endpoint = 3;
string user_endpoint = 4; string user_endpoint = 4;
repeated string scopes = 5; repeated string scopes = 5;
string id_attribute = 6;
} }
message GenericOIDCConfig { message GenericOIDCConfig {

View File

@ -11017,7 +11017,9 @@ message AddGenericOAuthProviderRequest {
string token_endpoint = 5 [(validate.rules).string = {min_len: 1, max_len: 200}]; string token_endpoint = 5 [(validate.rules).string = {min_len: 1, max_len: 200}];
string user_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; string user_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
repeated string scopes = 7 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; repeated string scopes = 7 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}];
zitadel.idp.v1.Options provider_options = 8; // identifying attribute of the user in the response of the user_endpoint
string id_attribute = 8 [(validate.rules).string = {min_len: 1, max_len: 200}];
zitadel.idp.v1.Options provider_options = 9;
} }
message AddGenericOAuthProviderResponse { message AddGenericOAuthProviderResponse {
@ -11035,7 +11037,9 @@ message UpdateGenericOAuthProviderRequest {
string token_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; string token_endpoint = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
string user_endpoint = 7 [(validate.rules).string = {min_len: 1, max_len: 200}]; string user_endpoint = 7 [(validate.rules).string = {min_len: 1, max_len: 200}];
repeated string scopes = 8 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}]; repeated string scopes = 8 [(validate.rules).repeated = {max_items: 20, items: {string: {min_len: 1, max_len: 100}}}];
zitadel.idp.v1.Options provider_options = 9; // identifying attribute of the user in the response of the user_endpoint
string id_attribute = 9 [(validate.rules).string = {min_len: 1, max_len: 200}];
zitadel.idp.v1.Options provider_options = 10;
} }
message UpdateGenericOAuthProviderResponse { message UpdateGenericOAuthProviderResponse {