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
@@ -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",
},
},
},
},
},