mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-15 20:38:00 +00:00
fa8f191812
* feat: v2alpha user service idp endpoints * feat: v2alpha user service intent endpoints * begin idp intents (callback) * some cleanup * runnable idp authentication * cleanup * proto cleanup * retrieve idp info * improve success and failure handling * some unit tests * grpc unit tests * add permission check AddUserIDPLink * feat: v2alpha intent writemodel refactoring * feat: v2alpha intent writemodel refactoring * feat: v2alpha intent writemodel refactoring * provider from write model * fix idp type model and add integration tests * proto cleanup * fix integration test * add missing import * add more integration tests * auth url test * feat: v2alpha intent writemodel refactoring * remove unused functions * check token on RetrieveIdentityProviderInformation * feat: v2alpha intent writemodel refactoring * fix TestServer_RetrieveIdentityProviderInformation * fix test * i18n and linting * feat: v2alpha intent review changes --------- Co-authored-by: Livio Spring <livio.a@gmail.com> Co-authored-by: Tim Möhlmann <tim+github@zitadel.com>
668 lines
16 KiB
Go
668 lines
16 KiB
Go
package command
|
|
|
|
import (
|
|
"context"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/golang/mock/gomock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/zitadel/oidc/v2/pkg/oidc"
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
z_errors "github.com/zitadel/zitadel/internal/errors"
|
|
"github.com/zitadel/zitadel/internal/eventstore"
|
|
"github.com/zitadel/zitadel/internal/id"
|
|
"github.com/zitadel/zitadel/internal/id/mock"
|
|
"github.com/zitadel/zitadel/internal/idp"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
|
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
|
rep_idp "github.com/zitadel/zitadel/internal/repository/idp"
|
|
"github.com/zitadel/zitadel/internal/repository/idpintent"
|
|
"github.com/zitadel/zitadel/internal/repository/instance"
|
|
)
|
|
|
|
func TestCommands_CreateIntent(t *testing.T) {
|
|
type fields struct {
|
|
eventstore *eventstore.Eventstore
|
|
idGenerator id.Generator
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
idpID string
|
|
successURL string
|
|
failureURL string
|
|
resourceOwner string
|
|
}
|
|
type res struct {
|
|
intentID string
|
|
details *domain.ObjectDetails
|
|
err error
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
"error no id generator",
|
|
fields{
|
|
eventstore: eventstoreExpect(t),
|
|
idGenerator: mock.NewIDGeneratorExpectError(t, z_errors.ThrowInternal(nil, "", "error id")),
|
|
},
|
|
args{
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
|
idpID: "",
|
|
successURL: "https://success.url",
|
|
failureURL: "https://failure.url",
|
|
},
|
|
res{
|
|
err: z_errors.ThrowInternal(nil, "", "error id"),
|
|
},
|
|
},
|
|
{
|
|
"error no idpID",
|
|
fields{
|
|
eventstore: eventstoreExpect(t),
|
|
idGenerator: mock.ExpectID(t, "id"),
|
|
},
|
|
args{
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
|
idpID: "",
|
|
successURL: "https://success.url",
|
|
failureURL: "https://failure.url",
|
|
},
|
|
res{
|
|
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j2bk", "Errors.Intent.IDPMissing"),
|
|
},
|
|
},
|
|
{
|
|
"error no successURL",
|
|
fields{
|
|
eventstore: eventstoreExpect(t),
|
|
idGenerator: mock.ExpectID(t, "id"),
|
|
},
|
|
args{
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
|
idpID: "idp",
|
|
successURL: ":",
|
|
failureURL: "https://failure.url",
|
|
},
|
|
res{
|
|
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j3bk", "Errors.Intent.SuccessURLMissing"),
|
|
},
|
|
},
|
|
{
|
|
"error no failureURL",
|
|
fields{
|
|
eventstore: eventstoreExpect(t),
|
|
idGenerator: mock.ExpectID(t, "id"),
|
|
},
|
|
args{
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
|
idpID: "idp",
|
|
successURL: "https://success.url",
|
|
failureURL: ":",
|
|
},
|
|
res{
|
|
err: z_errors.ThrowInvalidArgument(nil, "COMMAND-x8j4bk", "Errors.Intent.FailureURLMissing"),
|
|
},
|
|
},
|
|
{
|
|
"error idp not existing",
|
|
fields{
|
|
eventstore: eventstoreExpect(t,
|
|
expectFilter(),
|
|
expectFilter(),
|
|
expectFilter(),
|
|
),
|
|
idGenerator: mock.ExpectID(t, "id"),
|
|
},
|
|
args{
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
|
idpID: "idp",
|
|
successURL: "https://success.url",
|
|
failureURL: "https://failure.url",
|
|
},
|
|
res{
|
|
err: z_errors.ThrowPreconditionFailed(nil, "COMMAND-39n221fs", "Errors.IDPConfig.NotExisting"),
|
|
},
|
|
},
|
|
{
|
|
"push",
|
|
fields{
|
|
eventstore: eventstoreExpect(t,
|
|
expectFilter(),
|
|
expectFilter(),
|
|
expectFilter(
|
|
eventFromEventPusher(
|
|
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("ro").Aggregate,
|
|
"idp",
|
|
"name",
|
|
"clientID",
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("clientSecret"),
|
|
},
|
|
"auth",
|
|
"token",
|
|
"user",
|
|
"idAttribute",
|
|
nil,
|
|
rep_idp.Options{},
|
|
)),
|
|
),
|
|
expectPush(
|
|
eventPusherToEvents(
|
|
func() eventstore.Command {
|
|
success, _ := url.Parse("https://success.url")
|
|
failure, _ := url.Parse("https://failure.url")
|
|
return idpintent.NewStartedEvent(
|
|
context.Background(),
|
|
&idpintent.NewAggregate("id", "ro").Aggregate,
|
|
success,
|
|
failure,
|
|
"idp",
|
|
)
|
|
}(),
|
|
),
|
|
),
|
|
),
|
|
idGenerator: mock.ExpectID(t, "id"),
|
|
},
|
|
args{
|
|
ctx: context.Background(),
|
|
resourceOwner: "ro",
|
|
idpID: "idp",
|
|
successURL: "https://success.url",
|
|
failureURL: "https://failure.url",
|
|
},
|
|
res{
|
|
intentID: "id",
|
|
details: &domain.ObjectDetails{ResourceOwner: "ro"},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore,
|
|
idGenerator: tt.fields.idGenerator,
|
|
}
|
|
intentID, details, err := c.CreateIntent(tt.args.ctx, tt.args.idpID, tt.args.successURL, tt.args.failureURL, tt.args.resourceOwner)
|
|
require.ErrorIs(t, err, tt.res.err)
|
|
assert.Equal(t, tt.res.intentID, intentID)
|
|
assert.Equal(t, tt.res.details, details)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_AuthURLFromProvider(t *testing.T) {
|
|
type fields struct {
|
|
eventstore *eventstore.Eventstore
|
|
secretCrypto crypto.EncryptionAlgorithm
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
idpID string
|
|
state string
|
|
callbackURL string
|
|
}
|
|
type res struct {
|
|
authURL string
|
|
err error
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
"idp not existing",
|
|
fields{
|
|
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
eventstore: eventstoreExpect(t,
|
|
expectFilter(),
|
|
),
|
|
},
|
|
args{
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
|
idpID: "idp",
|
|
state: "state",
|
|
callbackURL: "url",
|
|
},
|
|
res{
|
|
err: z_errors.ThrowPreconditionFailed(nil, "", ""),
|
|
},
|
|
},
|
|
{
|
|
"idp removed",
|
|
fields{
|
|
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
eventstore: eventstoreExpect(t,
|
|
expectFilter(
|
|
eventFromEventPusherWithInstanceID(
|
|
"instance",
|
|
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
|
"idp",
|
|
"name",
|
|
"clientID",
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("clientSecret"),
|
|
},
|
|
"auth",
|
|
"token",
|
|
"user",
|
|
"idAttribute",
|
|
nil,
|
|
rep_idp.Options{},
|
|
)),
|
|
eventFromEventPusherWithInstanceID(
|
|
"instance",
|
|
instance.NewIDPRemovedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
|
"idp",
|
|
),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args{
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
|
idpID: "idp",
|
|
state: "state",
|
|
callbackURL: "url",
|
|
},
|
|
res{
|
|
err: z_errors.ThrowInternal(nil, "COMMAND-xw921211", "Errors.IDPConfig.NotExisting"),
|
|
},
|
|
},
|
|
{
|
|
"push",
|
|
fields{
|
|
secretCrypto: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
eventstore: eventstoreExpect(t,
|
|
expectFilter(
|
|
eventFromEventPusherWithInstanceID(
|
|
"instance",
|
|
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
|
"idp",
|
|
"name",
|
|
"clientID",
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("clientSecret"),
|
|
},
|
|
"auth",
|
|
"token",
|
|
"user",
|
|
"idAttribute",
|
|
nil,
|
|
rep_idp.Options{},
|
|
)),
|
|
),
|
|
expectFilter(
|
|
eventFromEventPusherWithInstanceID(
|
|
"instance",
|
|
instance.NewOAuthIDPAddedEvent(context.Background(), &instance.NewAggregate("instance").Aggregate,
|
|
"idp",
|
|
"name",
|
|
"clientID",
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("clientSecret"),
|
|
},
|
|
"auth",
|
|
"token",
|
|
"user",
|
|
"idAttribute",
|
|
nil,
|
|
rep_idp.Options{},
|
|
)),
|
|
),
|
|
),
|
|
},
|
|
args{
|
|
ctx: authz.SetCtxData(context.Background(), authz.CtxData{OrgID: "ro"}),
|
|
idpID: "idp",
|
|
state: "state",
|
|
callbackURL: "url",
|
|
},
|
|
res{
|
|
authURL: "auth?client_id=clientID&prompt=select_account&redirect_uri=url&response_type=code&state=state",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore,
|
|
idpConfigEncryption: tt.fields.secretCrypto,
|
|
}
|
|
authURL, err := c.AuthURLFromProvider(tt.args.ctx, tt.args.idpID, tt.args.state, tt.args.callbackURL)
|
|
require.ErrorIs(t, err, tt.res.err)
|
|
assert.Equal(t, tt.res.authURL, authURL)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_SucceedIDPIntent(t *testing.T) {
|
|
type fields struct {
|
|
eventstore *eventstore.Eventstore
|
|
idpConfigEncryption crypto.EncryptionAlgorithm
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
writeModel *IDPIntentWriteModel
|
|
idpUser idp.User
|
|
idpSession idp.Session
|
|
userID string
|
|
}
|
|
type res struct {
|
|
token string
|
|
err error
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
"encryption fails",
|
|
fields{
|
|
idpConfigEncryption: func() crypto.EncryptionAlgorithm {
|
|
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
|
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
|
|
return m
|
|
}(),
|
|
},
|
|
args{
|
|
ctx: context.Background(),
|
|
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
|
},
|
|
res{
|
|
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
|
|
},
|
|
},
|
|
{
|
|
"token encryption fails",
|
|
fields{
|
|
idpConfigEncryption: func() crypto.EncryptionAlgorithm {
|
|
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
|
m.EXPECT().Encrypt(gomock.Any()).DoAndReturn(func(value []byte) ([]byte, error) {
|
|
return value, nil
|
|
})
|
|
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
|
|
return m
|
|
}(),
|
|
},
|
|
args{
|
|
ctx: context.Background(),
|
|
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
|
idpSession: &oauth.Session{
|
|
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: "accessToken",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
res{
|
|
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
|
|
},
|
|
},
|
|
{
|
|
"push",
|
|
fields{
|
|
idpConfigEncryption: crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
eventstore: eventstoreExpect(t,
|
|
expectPush(
|
|
eventPusherToEvents(
|
|
func() eventstore.Command {
|
|
event, _ := idpintent.NewSucceededEvent(
|
|
context.Background(),
|
|
&idpintent.NewAggregate("id", "ro").Aggregate,
|
|
[]byte(`{"RawInfo":{"id":"id"}}`),
|
|
"",
|
|
&crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("accessToken"),
|
|
},
|
|
"",
|
|
)
|
|
return event
|
|
}(),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args{
|
|
ctx: context.Background(),
|
|
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
|
idpSession: &oauth.Session{
|
|
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: "accessToken",
|
|
},
|
|
},
|
|
},
|
|
idpUser: &oauth.UserMapper{
|
|
RawInfo: map[string]interface{}{
|
|
"id": "id",
|
|
},
|
|
},
|
|
},
|
|
res{
|
|
token: "aWQ",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore,
|
|
idpConfigEncryption: tt.fields.idpConfigEncryption,
|
|
}
|
|
got, err := c.SucceedIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.idpUser, tt.args.idpSession, tt.args.userID)
|
|
require.ErrorIs(t, err, tt.res.err)
|
|
assert.Equal(t, tt.res.token, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCommands_FailIDPIntent(t *testing.T) {
|
|
type fields struct {
|
|
eventstore *eventstore.Eventstore
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
writeModel *IDPIntentWriteModel
|
|
reason string
|
|
}
|
|
type res struct {
|
|
err error
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
"push",
|
|
fields{
|
|
eventstore: eventstoreExpect(t,
|
|
expectPush(
|
|
eventPusherToEvents(
|
|
idpintent.NewFailedEvent(
|
|
context.Background(),
|
|
&idpintent.NewAggregate("id", "ro").Aggregate,
|
|
"reason",
|
|
),
|
|
),
|
|
),
|
|
),
|
|
},
|
|
args{
|
|
ctx: context.Background(),
|
|
writeModel: NewIDPIntentWriteModel("id", "ro"),
|
|
reason: "reason",
|
|
},
|
|
res{
|
|
err: nil,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Commands{
|
|
eventstore: tt.fields.eventstore,
|
|
}
|
|
err := c.FailIDPIntent(tt.args.ctx, tt.args.writeModel, tt.args.reason)
|
|
require.ErrorIs(t, err, tt.res.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_tokensForSucceededIDPIntent(t *testing.T) {
|
|
type args struct {
|
|
session idp.Session
|
|
encryptionAlg crypto.EncryptionAlgorithm
|
|
}
|
|
type res struct {
|
|
accessToken *crypto.CryptoValue
|
|
idToken string
|
|
err error
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
res res
|
|
}{
|
|
{
|
|
"no tokens",
|
|
args{
|
|
&ldap.Session{},
|
|
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
res{
|
|
accessToken: nil,
|
|
idToken: "",
|
|
err: nil,
|
|
},
|
|
},
|
|
{
|
|
"token encryption fails",
|
|
args{
|
|
&oauth.Session{
|
|
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: "accessToken",
|
|
},
|
|
},
|
|
},
|
|
func() crypto.EncryptionAlgorithm {
|
|
m := crypto.NewMockEncryptionAlgorithm(gomock.NewController(t))
|
|
m.EXPECT().Encrypt(gomock.Any()).Return(nil, z_errors.ThrowInternal(nil, "id", "encryption failed"))
|
|
return m
|
|
}(),
|
|
},
|
|
res{
|
|
accessToken: nil,
|
|
idToken: "",
|
|
err: z_errors.ThrowInternal(nil, "id", "encryption failed"),
|
|
},
|
|
},
|
|
{
|
|
"oauth tokens",
|
|
args{
|
|
&oauth.Session{
|
|
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: "accessToken",
|
|
},
|
|
},
|
|
},
|
|
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
res{
|
|
accessToken: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("accessToken"),
|
|
},
|
|
idToken: "",
|
|
err: nil,
|
|
},
|
|
},
|
|
{
|
|
"oidc tokens",
|
|
args{
|
|
&openid.Session{
|
|
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
Token: &oauth2.Token{
|
|
AccessToken: "accessToken",
|
|
},
|
|
IDToken: "idToken",
|
|
},
|
|
},
|
|
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
res{
|
|
accessToken: &crypto.CryptoValue{
|
|
CryptoType: crypto.TypeEncryption,
|
|
Algorithm: "enc",
|
|
KeyID: "id",
|
|
Crypted: []byte("accessToken"),
|
|
},
|
|
idToken: "idToken",
|
|
err: nil,
|
|
},
|
|
},
|
|
{
|
|
"jwt tokens",
|
|
args{
|
|
&jwt.Session{
|
|
Tokens: &oidc.Tokens[*oidc.IDTokenClaims]{
|
|
IDToken: "idToken",
|
|
},
|
|
},
|
|
crypto.CreateMockEncryptionAlg(gomock.NewController(t)),
|
|
},
|
|
res{
|
|
accessToken: nil,
|
|
idToken: "idToken",
|
|
err: nil,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotAccessToken, gotIDToken, err := tokensForSucceededIDPIntent(tt.args.session, tt.args.encryptionAlg)
|
|
require.ErrorIs(t, err, tt.res.err)
|
|
assert.Equal(t, tt.res.accessToken, gotAccessToken)
|
|
assert.Equal(t, tt.res.idToken, gotIDToken)
|
|
})
|
|
}
|
|
}
|