feat: handle Zitadel provider IdP login via login v1 (#12469)

# Which Problems Are Solved

This PR adds support for using a ZITADEL IdP provider in the login v1 UI
flow and conditionally grants a support-user instance membership based
on a ZITADEL-specific project-roles token claim.

# How the Problems Are Solved

- Introduced ZITADEL IdP handling in the login v1 handler, including
extracting the `urn:zitadel:iam:org:project:roles` claim and granting
`IAM_OWNER_VIEWER` instance membership when configured orgs match.
- Added a new `AddInstanceMemberFromLogin` to assign an instance
membership during the login v1 ZITADEL-IdP flow without permission
checks (as the authorization is established based on the project roles
in the token claim)
- Added unit tests

# Additional Changes

N/A

# Additional Context
- Closes https://github.com/zitadel/zitadel/issues/12446
This commit is contained in:
gayathri
2026-07-28 15:20:46 +02:00
committed by GitHub
parent 318acd4bc3
commit ffd9b39bdb
14 changed files with 908 additions and 492 deletions
@@ -283,12 +283,6 @@ func TestServer_GetIDPByID(t *testing.T) {
ClientId: "test-client",
Issuer: "zitadel.example.com",
Scopes: []string{"email", "profile"},
InstanceRolesInfo: []*idp.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
},
},
},
Options: &idp.Options{
+12 -29
View File
@@ -14,7 +14,6 @@ import (
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/internal/zerrors"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
@@ -544,38 +543,22 @@ func signatureAlgorithmToCommand(signatureAlgorithm idp_pb.SAMLSignatureAlgorith
func addZitadelProviderToCommand(req *mgmt_pb.AddZitadelProviderRequest) command.ZitadelProvider {
return command.ZitadelProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
InstanceRolesInfo: instanceRolesInfoToCommand(req.InstanceRolesInfo),
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateZitadelProviderToCommand(req *mgmt_pb.UpdateZitadelProviderRequest) command.ZitadelProvider {
return command.ZitadelProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
InstanceRolesInfo: instanceRolesInfoToCommand(req.InstanceRolesInfo),
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func instanceRolesInfoToCommand(reqInstanceRolesInfo []*idp_pb.InstanceRolesInfo) []idp.RolesInfo {
if reqInstanceRolesInfo == nil {
return nil
}
instanceRolesInfo := make([]idp.RolesInfo, 0, len(reqInstanceRolesInfo))
for _, info := range reqInstanceRolesInfo {
instanceRolesInfo = append(instanceRolesInfo, idp.RolesInfo{
OrganizationID: info.OrganizationId,
OrganizationDomain: info.OrganizationDomain,
})
}
return instanceRolesInfo
}
@@ -207,34 +207,6 @@ func Test_addZitadelProviderToCommand(t *testing.T) {
req *mgmt_pb.AddZitadelProviderRequest
want command.ZitadelProvider
}{
{
name: "without instance roles info",
req: &mgmt_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"},
ProviderOptions: &idp_pb.Options{
IsLinkingAllowed: false,
IsCreationAllowed: true,
IsAutoCreation: false,
IsAutoUpdate: true,
AutoLinking: 0,
},
},
want: command.ZitadelProvider{
Name: "Zitadel Support IdP",
ClientID: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsAutoCreation: false,
IsLinkingAllowed: false,
IsAutoUpdate: true,
},
},
},
{
name: "all fields filled",
req: &mgmt_pb.AddZitadelProviderRequest{
@@ -249,17 +221,6 @@ func Test_addZitadelProviderToCommand(t *testing.T) {
IsAutoUpdate: true,
AutoLinking: 0,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationId: "org2",
OrganizationDomain: "org2.com",
},
},
},
want: command.ZitadelProvider{
Name: "Zitadel Support IdP",
@@ -272,16 +233,6 @@ func Test_addZitadelProviderToCommand(t *testing.T) {
IsLinkingAllowed: false,
IsAutoUpdate: true,
},
InstanceRolesInfo: []idp.RolesInfo{
{
OrganizationID: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationID: "org2",
OrganizationDomain: "org2.com",
},
},
},
},
}
@@ -301,35 +252,6 @@ func Test_updateZitadelProviderToCommand(t *testing.T) {
req *mgmt_pb.UpdateZitadelProviderRequest
want command.ZitadelProvider
}{
{
name: "without instance roles info",
req: &mgmt_pb.UpdateZitadelProviderRequest{
Name: "Zitadel Support IdP",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"},
ProviderOptions: &idp_pb.Options{
IsLinkingAllowed: false,
IsCreationAllowed: true,
IsAutoCreation: false,
IsAutoUpdate: true,
AutoLinking: 0,
},
},
want: command.ZitadelProvider{
Name: "Zitadel Support IdP",
ClientID: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile", "urn:zitadel:iam:org:project:roles"},
IDPOptions: idp.Options{
IsCreationAllowed: true,
IsAutoCreation: false,
IsLinkingAllowed: false,
IsAutoUpdate: true,
},
InstanceRolesInfo: nil,
},
},
{
name: "all fields filled",
req: &mgmt_pb.UpdateZitadelProviderRequest{
@@ -344,17 +266,6 @@ func Test_updateZitadelProviderToCommand(t *testing.T) {
IsAutoUpdate: true,
AutoLinking: 0,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationId: "org2",
OrganizationDomain: "org2.com",
},
},
},
want: command.ZitadelProvider{
Name: "Zitadel Support IdP",
@@ -367,16 +278,6 @@ func Test_updateZitadelProviderToCommand(t *testing.T) {
IsLinkingAllowed: false,
IsAutoUpdate: true,
},
InstanceRolesInfo: []idp.RolesInfo{
{
OrganizationID: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationID: "org2",
OrganizationDomain: "org2.com",
},
},
},
},
}
@@ -109,7 +109,7 @@ func Test_AddZitadelProvider(t *testing.T) {
),
},
{
name: "missing org ID in instance roles info",
name: "valid request",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.AddZitadelProviderRequest{
@@ -121,87 +121,6 @@ func Test_AddZitadelProvider(t *testing.T) {
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "",
},
},
},
},
wantErr: status.Error(
codes.InvalidArgument,
"invalid AddZitadelProviderRequest.InstanceRolesInfo[0]: embedded message failed validation | caused by: invalid InstanceRolesInfo.OrganizationId: value length must be between 1 and 200 runes, inclusive",
),
},
{
name: "missing org domain in instance roles info",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile"},
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationId: "org2",
},
},
},
},
wantErr: status.Error(
codes.InvalidArgument,
"invalid AddZitadelProviderRequest.InstanceRolesInfo[1]: embedded message failed validation | caused by: invalid InstanceRolesInfo.OrganizationDomain: value length must be between 1 and 200 runes, inclusive",
),
},
{
name: "valid request without instance roles info",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile"},
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
},
},
wantResponse: &mgmt_pb.AddZitadelProviderResponse{
Details: &object_pb.ObjectDetails{
ResourceOwner: Instance.DefaultOrg.Id,
},
},
},
{
name: "valid request with instance roles info",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.AddZitadelProviderRequest{
Name: "Zitadel Support IdP",
Issuer: "zitadel.example.com",
ClientId: "test-client",
ClientSecret: "test-secret",
Scopes: []string{"email", "profile"},
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
},
},
},
wantResponse: &mgmt_pb.AddZitadelProviderResponse{
@@ -318,16 +237,6 @@ func Test_UpdateZitadelProvider(t *testing.T) {
Issuer: "acme.example.com",
ClientId: "test-client",
Scopes: []string{"email", "profile", "openid", "offline_access"},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationId: "org2",
OrganizationDomain: "org2.com",
},
},
},
},
wantResponse: &mgmt_pb.UpdateZitadelProviderResponse{
@@ -347,16 +256,6 @@ func Test_UpdateZitadelProvider(t *testing.T) {
Issuer: "acme.example.com",
ClientId: "test-client",
Scopes: []string{"email", "profile", "openid", "offline_access"},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
{
OrganizationId: "org2",
OrganizationDomain: "org2.com",
},
},
},
},
},
@@ -374,12 +273,6 @@ func Test_UpdateZitadelProvider(t *testing.T) {
ProviderOptions: &idp_pb.Options{
IsAutoCreation: true,
},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org3",
OrganizationDomain: "org3.com",
},
},
},
},
wantResponse: &mgmt_pb.UpdateZitadelProviderResponse{
@@ -401,52 +294,6 @@ func Test_UpdateZitadelProvider(t *testing.T) {
Issuer: "acme.example.com",
ClientId: "test-client",
Scopes: nil, // unset scopes
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org3",
OrganizationDomain: "org3.com",
},
},
},
},
},
},
},
{
name: "update with instance roles info not set, ok",
args: args{
ctx: OrgCTX,
req: &mgmt_pb.UpdateZitadelProviderRequest{
Name: "Zitadel Support IdP updated 2",
Issuer: "acme.example.com",
ClientId: "test-client",
Scopes: []string{"email", "openid"},
ProviderOptions: &idp_pb.Options{
IsCreationAllowed: true,
},
InstanceRolesInfo: nil, // instance roles info isn't set -> will be unset by the API
},
},
wantResponse: &mgmt_pb.UpdateZitadelProviderResponse{
Details: &object_pb.ObjectDetails{
ResourceOwner: Instance.DefaultOrg.Id,
},
},
wantUpdatedProvider: &idp_pb.Provider{
State: idp_pb.IDPState_IDP_STATE_ACTIVE,
Name: "Zitadel Support IdP updated 2",
Owner: idp_pb.IDPOwnerType_IDP_OWNER_TYPE_ORG,
Type: idp_pb.ProviderType_PROVIDER_TYPE_ZITADEL,
Config: &idp_pb.ProviderConfig{
Options: &idp_pb.Options{
IsCreationAllowed: true,
},
Config: &idp_pb.ProviderConfig_Zitadel{
Zitadel: &idp_pb.ZitadelConfig{
Issuer: "acme.example.com",
ClientId: "test-client",
Scopes: []string{"email", "openid"},
InstanceRolesInfo: nil, // instance roles info isn't set
},
},
},
@@ -567,12 +414,6 @@ func Test_GetProviderByID(t *testing.T) {
Issuer: "zitadel.example.com",
ClientId: "test-client",
Scopes: []string{"email", "profile"},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
},
},
},
},
@@ -676,12 +517,6 @@ func Test_ListProviders(t *testing.T) {
Issuer: "zitadel.example.com",
ClientId: "test-client",
Scopes: []string{"email", "profile"},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
},
},
},
},
@@ -731,12 +566,6 @@ func Test_ListProviders(t *testing.T) {
Issuer: "zitadel.example.com",
ClientId: "test-client",
Scopes: []string{"email", "profile"},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
},
},
},
},
@@ -777,12 +606,6 @@ func Test_ListProviders(t *testing.T) {
Issuer: "zitadel.example.com",
ClientId: "test-client",
Scopes: []string{"email", "profile"},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
},
},
},
},
@@ -807,12 +630,6 @@ func Test_ListProviders(t *testing.T) {
Issuer: "zitadel.example.com",
ClientId: "test-client",
Scopes: []string{"email", "profile"},
InstanceRolesInfo: []*idp_pb.InstanceRolesInfo{
{
OrganizationId: "org1",
OrganizationDomain: "org1.com",
},
},
},
},
},