feat: extend start/retrieve idp intent for Zitadel provider (#12424)

# Which Problems Are Solved

This PR extends `StartIdentityProviderIntent` and
`RetrieveIdentityProviderIntent` for the new Zitadel provider.

# How the Problems Are Solved
- Added `domain.IDPTypeZitadel` cases to both the instance and org
switches in `NewAllIDPWriteModel` to return Zitadel provider when
`GetProvider` is called
- Added Zitadel provider to RetrieveIdentityProviderIntent's provider
type-switch
- Added new `zitadel.go` provider, an OIDC wrapper with forced PKCE
- Added unit/integration tests

# Additional Changes


# Additional Context
- Closes  https://github.com/zitadel/zitadel/issues/12050

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
This commit is contained in:
gayathri
2026-07-17 13:24:18 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 53d7920349
commit b91221143b
8 changed files with 755 additions and 0 deletions
+39
View File
@@ -26,6 +26,7 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
saml2 "github.com/zitadel/zitadel/internal/idp/providers/saml"
"github.com/zitadel/zitadel/internal/idp/providers/saml/requesttracker"
"github.com/zitadel/zitadel/internal/idp/providers/zitadel"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/repository/idpconfig"
"github.com/zitadel/zitadel/internal/repository/instance"
@@ -2279,6 +2280,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
writeModel.model = NewGoogleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeApple:
writeModel.model = NewAppleInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeZitadel:
writeModel.model = NewInstanceZitadelIDPWriteModel(resourceOwner, id)
case domain.IDPTypeSAML:
writeModel.samlModel = NewSAMLInstanceIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified:
@@ -2310,6 +2313,8 @@ func NewAllIDPWriteModel(resourceOwner string, instanceBool bool, id string, idp
writeModel.model = NewGoogleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeApple:
writeModel.model = NewAppleOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeZitadel:
writeModel.model = NewZitadelOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeSAML:
writeModel.samlModel = NewSAMLOrgIDPWriteModel(resourceOwner, id)
case domain.IDPTypeUnspecified:
@@ -2481,3 +2486,37 @@ func (wm *ZitadelIDPWriteModel) NewChanges(
}
return changes, nil
}
func (wm *ZitadelIDPWriteModel) ToProvider(callbackURL string, idpAlg crypto.EncryptionAlgorithm, httpClient *http.Client) (providers.Provider, error) {
secret, err := crypto.DecryptString(wm.ClientSecret, idpAlg)
if err != nil {
return nil, err
}
opts := make([]oidc.ProviderOpts, 1, 7)
opts[0] = oidc.WithSelectAccount()
if wm.IsCreationAllowed {
opts = append(opts, oidc.WithCreationAllowed())
}
if wm.IsLinkingAllowed {
opts = append(opts, oidc.WithLinkingAllowed())
}
if wm.IsAutoCreation {
opts = append(opts, oidc.WithAutoCreation())
}
if wm.IsAutoUpdate {
opts = append(opts, oidc.WithAutoUpdate())
}
return zitadel.New(
wm.Issuer,
wm.ClientID,
secret,
callbackURL,
wm.Scopes,
httpClient,
opts...,
)
}
func (wm *ZitadelIDPWriteModel) GetProviderOptions() idp.Options {
return wm.Options
}
+69
View File
@@ -17,6 +17,7 @@ import (
"github.com/zitadel/zitadel/internal/domain"
providers "github.com/zitadel/zitadel/internal/idp"
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
"github.com/zitadel/zitadel/internal/idp/providers/zitadel"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -167,6 +168,19 @@ func TestCommands_AllIDPWriteModel(t *testing.T) {
err: nil,
},
},
{
name: "writemodel instance zitadel",
args: args{
resourceOwner: "owner",
instanceBool: true,
id: "id",
idpType: domain.IDPTypeZitadel,
},
res: res{
writeModelType: &InstanceZitadelIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel instance saml",
args: args{
@@ -322,6 +336,19 @@ func TestCommands_AllIDPWriteModel(t *testing.T) {
err: nil,
},
},
{
name: "writemodel org zitadel",
args: args{
resourceOwner: "owner",
instanceBool: false,
id: "id",
idpType: domain.IDPTypeZitadel,
},
res: res{
writeModelType: &OrgZitadelIDPWriteModel{},
err: nil,
},
},
{
name: "writemodel org saml",
args: args{
@@ -424,6 +451,48 @@ func TestOIDCIDPWriteModel_ToProvider_WithPKCE(t *testing.T) {
assertProviderUsesPKCE(t, provider)
}
func TestZitadelIDPWriteModel_ToProvider(t *testing.T) {
var (
issuer string
discoveryRequested atomic.Bool
)
handler := func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != oidc_pkg.DiscoveryEndpoint {
http.NotFound(w, r)
return
}
discoveryRequested.Store(true)
w.Header().Set("Content-Type", "application/json")
// assert, not require: FailNow must not be called from the server's handler goroutine
assert.NoError(t, json.NewEncoder(w).Encode(&oidc_pkg.DiscoveryConfiguration{
Issuer: issuer,
AuthorizationEndpoint: issuer + "/authorize",
TokenEndpoint: issuer + "/token",
UserinfoEndpoint: issuer + "/userinfo",
}))
}
server := httptest.NewServer(http.HandlerFunc(handler))
t.Cleanup(server.Close)
issuer = server.URL
wm := &ZitadelIDPWriteModel{
Name: "ZITADEL",
Issuer: issuer,
ClientID: "clientID",
ClientSecret: &crypto.CryptoValue{Algorithm: "plain", Crypted: []byte("clientSecret"), KeyID: "keyID"},
Scopes: []string{"openid"},
}
provider, err := wm.ToProvider("https://zitadel.example.com/idps/callback", plainTextEncryption{}, http.DefaultClient)
require.NoError(t, err)
require.IsType(t, &zitadel.Provider{}, provider)
require.True(t, discoveryRequested.Load(), "expected OIDC discovery to be requested")
// the ZITADEL provider always enforces PKCE
assertProviderUsesPKCE(t, provider)
}
func assertProviderUsesPKCE(t *testing.T, provider providers.Provider) {
t.Helper()