diff --git a/cmd/start/start.go b/cmd/start/start.go
index f944ef0327..0ecff76a9b 100644
--- a/cmd/start/start.go
+++ b/cmd/start/start.go
@@ -38,6 +38,7 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/auth"
feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta"
+ idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2"
"github.com/zitadel/zitadel/internal/api/grpc/management"
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta"
@@ -437,6 +438,9 @@ func startAPIs(
if err := apis.RegisterService(ctx, feature_v2.CreateServer(commands, queries)); err != nil {
return nil, err
}
+ if err := apis.RegisterService(ctx, idp_v2.CreateServer(commands, queries, permissionCheck)); err != nil {
+ return nil, err
+ }
if err := apis.RegisterService(ctx, action_v3_alpha.CreateServer(config.SystemDefaults, commands, queries, domain.AllFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err
}
diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js
index b65a53d20b..3aad64d9ef 100644
--- a/docs/docusaurus.config.js
+++ b/docs/docusaurus.config.js
@@ -356,6 +356,14 @@ module.exports = {
categoryLinkSource: "auto",
},
},
+ idp_v2: {
+ specPath: ".artifacts/openapi/zitadel/idp/v2/idp_service.swagger.json",
+ outputDir: "docs/apis/resources/idp_service_v2",
+ sidebarOptions: {
+ groupPathsBy: "tag",
+ categoryLinkSource: "auto",
+ },
+ },
},
},
],
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 49107a380c..98667395f1 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -673,12 +673,24 @@ module.exports = {
link: {
type: "generated-index",
title: "Feature Service API",
- slug: "/apis/resources/feature_service/v2",
+ slug: "/apis/resources/feature_service_v2",
description:
'This API is intended to manage features for ZITADEL. Feature settings that are available on multiple "levels", such as instance and organization. The higher level instance acts as a default for the lower level. When a feature is set on multiple levels, the lower level takes precedence. Features can be experimental where ZITADEL will assume a sane default, such as disabled. When over time confidence in such a feature grows, ZITADEL can default to enabling the feature. As a final step we might choose to always enable a feature and remove the setting from this API, reserving the proto field number. Such removal is not considered a breaking change. Setting a removed field will effectively result in a no-op.\n'
},
items: require("./docs/apis/resources/feature_service_v2/sidebar.ts"),
},
+ {
+ type: "category",
+ label: "Identity Provider Lifecycle",
+ link: {
+ type: "generated-index",
+ title: "Identity Provider Service API",
+ slug: "/apis/resources/idp_service_v2",
+ description:
+ 'This API is intended to manage identity providers (IdPs) for ZITADEL.\n'
+ },
+ items: require("./docs/apis/resources/idp_service_v2/sidebar.ts"),
+ },
],
},
{
diff --git a/internal/api/grpc/admin/idp.go b/internal/api/grpc/admin/idp.go
index 8182ad63b9..5528a94dbb 100644
--- a/internal/api/grpc/admin/idp.go
+++ b/internal/api/grpc/admin/idp.go
@@ -157,7 +157,7 @@ func (s *Server) GetProviderByID(ctx context.Context, req *admin_pb.GetProviderB
if err != nil {
return nil, err
}
- idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, instanceIDQuery)
+ idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, nil, instanceIDQuery)
if err != nil {
return nil, err
}
diff --git a/internal/api/grpc/idp/v2/query.go b/internal/api/grpc/idp/v2/query.go
new file mode 100644
index 0000000000..476ac3dcdd
--- /dev/null
+++ b/internal/api/grpc/idp/v2/query.go
@@ -0,0 +1,369 @@
+package idp
+
+import (
+ "context"
+
+ "github.com/crewjam/saml"
+ "github.com/muhlemmer/gu"
+ "google.golang.org/protobuf/types/known/durationpb"
+
+ "github.com/zitadel/zitadel/internal/api/grpc/object/v2"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/idp/providers/azuread"
+ "github.com/zitadel/zitadel/internal/query"
+ idp_rp "github.com/zitadel/zitadel/internal/repository/idp"
+ idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2"
+)
+
+func (s *Server) GetIDPByID(ctx context.Context, req *idp_pb.GetIDPByIDRequest) (*idp_pb.GetIDPByIDResponse, error) {
+ idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, s.checkPermission)
+ if err != nil {
+ return nil, err
+ }
+ return &idp_pb.GetIDPByIDResponse{Idp: idpToPb(idp)}, nil
+}
+
+func idpToPb(idp *query.IDPTemplate) *idp_pb.IDP {
+ return &idp_pb.IDP{
+ Id: idp.ID,
+ Details: object.DomainToDetailsPb(
+ &domain.ObjectDetails{
+ Sequence: idp.Sequence,
+ EventDate: idp.ChangeDate,
+ ResourceOwner: idp.ResourceOwner,
+ }),
+ State: idpStateToPb(idp.State),
+ Name: idp.Name,
+ Type: idpTypeToPb(idp.Type),
+ Config: configToPb(idp),
+ }
+}
+
+func idpStateToPb(state domain.IDPState) idp_pb.IDPState {
+ switch state {
+ case domain.IDPStateActive:
+ return idp_pb.IDPState_IDP_STATE_ACTIVE
+ case domain.IDPStateInactive:
+ return idp_pb.IDPState_IDP_STATE_INACTIVE
+ case domain.IDPStateUnspecified:
+ return idp_pb.IDPState_IDP_STATE_UNSPECIFIED
+ case domain.IDPStateMigrated:
+ return idp_pb.IDPState_IDP_STATE_MIGRATED
+ case domain.IDPStateRemoved:
+ return idp_pb.IDPState_IDP_STATE_REMOVED
+ default:
+ return idp_pb.IDPState_IDP_STATE_UNSPECIFIED
+ }
+}
+
+func idpTypeToPb(idpType domain.IDPType) idp_pb.IDPType {
+ switch idpType {
+ case domain.IDPTypeOIDC:
+ return idp_pb.IDPType_IDP_TYPE_OIDC
+ case domain.IDPTypeJWT:
+ return idp_pb.IDPType_IDP_TYPE_JWT
+ case domain.IDPTypeOAuth:
+ return idp_pb.IDPType_IDP_TYPE_OAUTH
+ case domain.IDPTypeLDAP:
+ return idp_pb.IDPType_IDP_TYPE_LDAP
+ case domain.IDPTypeAzureAD:
+ return idp_pb.IDPType_IDP_TYPE_AZURE_AD
+ case domain.IDPTypeGitHub:
+ return idp_pb.IDPType_IDP_TYPE_GITHUB
+ case domain.IDPTypeGitHubEnterprise:
+ return idp_pb.IDPType_IDP_TYPE_GITHUB_ES
+ case domain.IDPTypeGitLab:
+ return idp_pb.IDPType_IDP_TYPE_GITLAB
+ case domain.IDPTypeGitLabSelfHosted:
+ return idp_pb.IDPType_IDP_TYPE_GITLAB_SELF_HOSTED
+ case domain.IDPTypeGoogle:
+ return idp_pb.IDPType_IDP_TYPE_GOOGLE
+ case domain.IDPTypeApple:
+ return idp_pb.IDPType_IDP_TYPE_APPLE
+ case domain.IDPTypeSAML:
+ return idp_pb.IDPType_IDP_TYPE_SAML
+ case domain.IDPTypeUnspecified:
+ return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED
+ default:
+ return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED
+ }
+}
+
+func configToPb(config *query.IDPTemplate) *idp_pb.IDPConfig {
+ idpConfig := &idp_pb.IDPConfig{
+ Options: &idp_pb.Options{
+ IsLinkingAllowed: config.IsLinkingAllowed,
+ IsCreationAllowed: config.IsCreationAllowed,
+ IsAutoCreation: config.IsAutoCreation,
+ IsAutoUpdate: config.IsAutoUpdate,
+ AutoLinking: autoLinkingOptionToPb(config.AutoLinking),
+ },
+ }
+ if config.OAuthIDPTemplate != nil {
+ oauthConfigToPb(idpConfig, config.OAuthIDPTemplate)
+ return idpConfig
+ }
+ if config.OIDCIDPTemplate != nil {
+ oidcConfigToPb(idpConfig, config.OIDCIDPTemplate)
+ return idpConfig
+ }
+ if config.JWTIDPTemplate != nil {
+ jwtConfigToPb(idpConfig, config.JWTIDPTemplate)
+ return idpConfig
+ }
+ if config.AzureADIDPTemplate != nil {
+ azureConfigToPb(idpConfig, config.AzureADIDPTemplate)
+ return idpConfig
+ }
+ if config.GitHubIDPTemplate != nil {
+ githubConfigToPb(idpConfig, config.GitHubIDPTemplate)
+ return idpConfig
+ }
+ if config.GitHubEnterpriseIDPTemplate != nil {
+ githubEnterpriseConfigToPb(idpConfig, config.GitHubEnterpriseIDPTemplate)
+ return idpConfig
+ }
+ if config.GitLabIDPTemplate != nil {
+ gitlabConfigToPb(idpConfig, config.GitLabIDPTemplate)
+ return idpConfig
+ }
+ if config.GitLabSelfHostedIDPTemplate != nil {
+ gitlabSelfHostedConfigToPb(idpConfig, config.GitLabSelfHostedIDPTemplate)
+ return idpConfig
+ }
+ if config.GoogleIDPTemplate != nil {
+ googleConfigToPb(idpConfig, config.GoogleIDPTemplate)
+ return idpConfig
+ }
+ if config.LDAPIDPTemplate != nil {
+ ldapConfigToPb(idpConfig, config.LDAPIDPTemplate)
+ return idpConfig
+ }
+ if config.AppleIDPTemplate != nil {
+ appleConfigToPb(idpConfig, config.AppleIDPTemplate)
+ return idpConfig
+ }
+ if config.SAMLIDPTemplate != nil {
+ samlConfigToPb(idpConfig, config.SAMLIDPTemplate)
+ return idpConfig
+ }
+ return idpConfig
+}
+
+func autoLinkingOptionToPb(linking domain.AutoLinkingOption) idp_pb.AutoLinkingOption {
+ switch linking {
+ case domain.AutoLinkingOptionUnspecified:
+ return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED
+ case domain.AutoLinkingOptionUsername:
+ return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME
+ case domain.AutoLinkingOptionEmail:
+ return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_EMAIL
+ default:
+ return idp_pb.AutoLinkingOption_AUTO_LINKING_OPTION_UNSPECIFIED
+ }
+}
+
+func oauthConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.OAuthIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_Oauth{
+ Oauth: &idp_pb.OAuthConfig{
+ ClientId: template.ClientID,
+ AuthorizationEndpoint: template.AuthorizationEndpoint,
+ TokenEndpoint: template.TokenEndpoint,
+ UserEndpoint: template.UserEndpoint,
+ Scopes: template.Scopes,
+ IdAttribute: template.IDAttribute,
+ },
+ }
+}
+
+func oidcConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.OIDCIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_Oidc{
+ Oidc: &idp_pb.GenericOIDCConfig{
+ ClientId: template.ClientID,
+ Issuer: template.Issuer,
+ Scopes: template.Scopes,
+ IsIdTokenMapping: template.IsIDTokenMapping,
+ },
+ }
+}
+
+func jwtConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.JWTIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_Jwt{
+ Jwt: &idp_pb.JWTConfig{
+ JwtEndpoint: template.Endpoint,
+ Issuer: template.Issuer,
+ KeysEndpoint: template.KeysEndpoint,
+ HeaderName: template.HeaderName,
+ },
+ }
+}
+
+func azureConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.AzureADIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_AzureAd{
+ AzureAd: &idp_pb.AzureADConfig{
+ ClientId: template.ClientID,
+ Tenant: azureTenantToPb(template.Tenant),
+ EmailVerified: template.IsEmailVerified,
+ Scopes: template.Scopes,
+ },
+ }
+}
+
+func azureTenantToPb(tenant string) *idp_pb.AzureADTenant {
+ var tenantType idp_pb.IsAzureADTenantType
+ switch azuread.TenantType(tenant) {
+ case azuread.CommonTenant:
+ tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_COMMON}
+ case azuread.OrganizationsTenant:
+ tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_ORGANISATIONS}
+ case azuread.ConsumersTenant:
+ tenantType = &idp_pb.AzureADTenant_TenantType{TenantType: idp_pb.AzureADTenantType_AZURE_AD_TENANT_TYPE_CONSUMERS}
+ default:
+ tenantType = &idp_pb.AzureADTenant_TenantId{TenantId: tenant}
+ }
+ return &idp_pb.AzureADTenant{Type: tenantType}
+}
+
+func githubConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitHubIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_Github{
+ Github: &idp_pb.GitHubConfig{
+ ClientId: template.ClientID,
+ Scopes: template.Scopes,
+ },
+ }
+}
+
+func githubEnterpriseConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitHubEnterpriseIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_GithubEs{
+ GithubEs: &idp_pb.GitHubEnterpriseServerConfig{
+ ClientId: template.ClientID,
+ AuthorizationEndpoint: template.AuthorizationEndpoint,
+ TokenEndpoint: template.TokenEndpoint,
+ UserEndpoint: template.UserEndpoint,
+ Scopes: template.Scopes,
+ },
+ }
+}
+
+func gitlabConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitLabIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_Gitlab{
+ Gitlab: &idp_pb.GitLabConfig{
+ ClientId: template.ClientID,
+ Scopes: template.Scopes,
+ },
+ }
+}
+
+func gitlabSelfHostedConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GitLabSelfHostedIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_GitlabSelfHosted{
+ GitlabSelfHosted: &idp_pb.GitLabSelfHostedConfig{
+ ClientId: template.ClientID,
+ Issuer: template.Issuer,
+ Scopes: template.Scopes,
+ },
+ }
+}
+
+func googleConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.GoogleIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_Google{
+ Google: &idp_pb.GoogleConfig{
+ ClientId: template.ClientID,
+ Scopes: template.Scopes,
+ },
+ }
+}
+
+func ldapConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.LDAPIDPTemplate) {
+ var timeout *durationpb.Duration
+ if template.Timeout != 0 {
+ timeout = durationpb.New(template.Timeout)
+ }
+ idpConfig.Config = &idp_pb.IDPConfig_Ldap{
+ Ldap: &idp_pb.LDAPConfig{
+ Servers: template.Servers,
+ StartTls: template.StartTLS,
+ BaseDn: template.BaseDN,
+ BindDn: template.BindDN,
+ UserBase: template.UserBase,
+ UserObjectClasses: template.UserObjectClasses,
+ UserFilters: template.UserFilters,
+ Timeout: timeout,
+ Attributes: ldapAttributesToPb(template.LDAPAttributes),
+ },
+ }
+}
+
+func ldapAttributesToPb(attributes idp_rp.LDAPAttributes) *idp_pb.LDAPAttributes {
+ return &idp_pb.LDAPAttributes{
+ IdAttribute: attributes.IDAttribute,
+ FirstNameAttribute: attributes.FirstNameAttribute,
+ LastNameAttribute: attributes.LastNameAttribute,
+ DisplayNameAttribute: attributes.DisplayNameAttribute,
+ NickNameAttribute: attributes.NickNameAttribute,
+ PreferredUsernameAttribute: attributes.PreferredUsernameAttribute,
+ EmailAttribute: attributes.EmailAttribute,
+ EmailVerifiedAttribute: attributes.EmailVerifiedAttribute,
+ PhoneAttribute: attributes.PhoneAttribute,
+ PhoneVerifiedAttribute: attributes.PhoneVerifiedAttribute,
+ PreferredLanguageAttribute: attributes.PreferredLanguageAttribute,
+ AvatarUrlAttribute: attributes.AvatarURLAttribute,
+ ProfileAttribute: attributes.ProfileAttribute,
+ }
+}
+
+func appleConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.AppleIDPTemplate) {
+ idpConfig.Config = &idp_pb.IDPConfig_Apple{
+ Apple: &idp_pb.AppleConfig{
+ ClientId: template.ClientID,
+ TeamId: template.TeamID,
+ KeyId: template.KeyID,
+ Scopes: template.Scopes,
+ },
+ }
+}
+
+func samlConfigToPb(idpConfig *idp_pb.IDPConfig, template *query.SAMLIDPTemplate) {
+ nameIDFormat := idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT
+ if template.NameIDFormat.Valid {
+ nameIDFormat = nameIDToPb(template.NameIDFormat.V)
+ }
+ idpConfig.Config = &idp_pb.IDPConfig_Saml{
+ Saml: &idp_pb.SAMLConfig{
+ MetadataXml: template.Metadata,
+ Binding: bindingToPb(template.Binding),
+ WithSignedRequest: template.WithSignedRequest,
+ NameIdFormat: nameIDFormat,
+ TransientMappingAttributeName: gu.Ptr(template.TransientMappingAttributeName),
+ },
+ }
+}
+
+func bindingToPb(binding string) idp_pb.SAMLBinding {
+ switch binding {
+ case "":
+ return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED
+ case saml.HTTPPostBinding:
+ return idp_pb.SAMLBinding_SAML_BINDING_POST
+ case saml.HTTPRedirectBinding:
+ return idp_pb.SAMLBinding_SAML_BINDING_REDIRECT
+ case saml.HTTPArtifactBinding:
+ return idp_pb.SAMLBinding_SAML_BINDING_ARTIFACT
+ default:
+ return idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED
+ }
+}
+
+func nameIDToPb(format domain.SAMLNameIDFormat) idp_pb.SAMLNameIDFormat {
+ switch format {
+ case domain.SAMLNameIDFormatUnspecified:
+ return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED
+ case domain.SAMLNameIDFormatEmailAddress:
+ return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_EMAIL_ADDRESS
+ case domain.SAMLNameIDFormatPersistent:
+ return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_PERSISTENT
+ case domain.SAMLNameIDFormatTransient:
+ return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_TRANSIENT
+ default:
+ return idp_pb.SAMLNameIDFormat_SAML_NAME_ID_FORMAT_UNSPECIFIED
+ }
+}
diff --git a/internal/api/grpc/idp/v2/query_integration_test.go b/internal/api/grpc/idp/v2/query_integration_test.go
new file mode 100644
index 0000000000..1135e33547
--- /dev/null
+++ b/internal/api/grpc/idp/v2/query_integration_test.go
@@ -0,0 +1,235 @@
+//go:build integration
+
+package idp_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/zitadel/zitadel/internal/integration"
+ "github.com/zitadel/zitadel/pkg/grpc/idp/v2"
+ "github.com/zitadel/zitadel/pkg/grpc/object/v2"
+)
+
+type idpAttr struct {
+ ID string
+ Name string
+ Details *object.Details
+}
+
+func TestServer_GetIDPByID(t *testing.T) {
+ type args struct {
+ ctx context.Context
+ req *idp.GetIDPByIDRequest
+ dep func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr
+ }
+ tests := []struct {
+ name string
+ args args
+ want *idp.GetIDPByIDResponse
+ wantErr bool
+ }{
+ {
+ name: "idp by ID, no id provided",
+ args: args{
+ IamCTX,
+ &idp.GetIDPByIDRequest{
+ Id: "",
+ },
+ func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr {
+ return nil
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "idp by ID, not found",
+ args: args{
+ IamCTX,
+ &idp.GetIDPByIDRequest{
+ Id: "unknown",
+ },
+ func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr {
+ return nil
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "idp by ID, instance, ok",
+ args: args{
+ IamCTX,
+ &idp.GetIDPByIDRequest{},
+ func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr {
+ name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano())
+ resp := Tester.AddGenericOAuthIDP(ctx, name)
+ request.Id = resp.Id
+ return &idpAttr{
+ resp.GetId(),
+ name,
+ &object.Details{
+ Sequence: resp.Details.Sequence,
+ ChangeDate: resp.Details.ChangeDate,
+ ResourceOwner: resp.Details.ResourceOwner,
+ }}
+ },
+ },
+ want: &idp.GetIDPByIDResponse{
+ Idp: &idp.IDP{
+ Details: &object.Details{
+ ChangeDate: timestamppb.Now(),
+ },
+ State: idp.IDPState_IDP_STATE_ACTIVE,
+ Type: idp.IDPType_IDP_TYPE_OAUTH,
+ Config: &idp.IDPConfig{
+ Config: &idp.IDPConfig_Oauth{
+ Oauth: &idp.OAuthConfig{
+ ClientId: "clientID",
+ AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
+ TokenEndpoint: "https://example.com/oauth/v2/token",
+ UserEndpoint: "https://api.example.com/user",
+ Scopes: []string{"openid", "profile", "email"},
+ IdAttribute: "id",
+ },
+ },
+ Options: &idp.Options{
+ IsLinkingAllowed: true,
+ IsCreationAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "idp by ID, instance, no permission",
+ args: args{
+ UserCTX,
+ &idp.GetIDPByIDRequest{},
+ func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr {
+ name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano())
+ resp := Tester.AddGenericOAuthIDP(IamCTX, name)
+ request.Id = resp.Id
+ return &idpAttr{
+ resp.GetId(),
+ name,
+ &object.Details{
+ Sequence: resp.Details.Sequence,
+ ChangeDate: resp.Details.ChangeDate,
+ ResourceOwner: resp.Details.ResourceOwner,
+ }}
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "idp by ID, org, ok",
+ args: args{
+ CTX,
+ &idp.GetIDPByIDRequest{},
+ func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr {
+ name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano())
+ resp := Tester.AddOrgGenericOAuthIDP(ctx, name)
+ request.Id = resp.Id
+ return &idpAttr{
+ resp.GetId(),
+ name,
+ &object.Details{
+ Sequence: resp.Details.Sequence,
+ ChangeDate: resp.Details.ChangeDate,
+ ResourceOwner: resp.Details.ResourceOwner,
+ }}
+ },
+ },
+ want: &idp.GetIDPByIDResponse{
+ Idp: &idp.IDP{
+ Details: &object.Details{
+ ChangeDate: timestamppb.Now(),
+ },
+ State: idp.IDPState_IDP_STATE_ACTIVE,
+ Type: idp.IDPType_IDP_TYPE_OAUTH,
+ Config: &idp.IDPConfig{
+ Config: &idp.IDPConfig_Oauth{
+ Oauth: &idp.OAuthConfig{
+ ClientId: "clientID",
+ AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
+ TokenEndpoint: "https://example.com/oauth/v2/token",
+ UserEndpoint: "https://api.example.com/user",
+ Scopes: []string{"openid", "profile", "email"},
+ IdAttribute: "id",
+ },
+ },
+ Options: &idp.Options{
+ IsLinkingAllowed: true,
+ IsCreationAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "idp by ID, org, no permission",
+ args: args{
+ UserCTX,
+ &idp.GetIDPByIDRequest{},
+ func(ctx context.Context, request *idp.GetIDPByIDRequest) *idpAttr {
+ name := fmt.Sprintf("GetIDPByID%d", time.Now().UnixNano())
+ resp := Tester.AddOrgGenericOAuthIDP(CTX, name)
+ request.Id = resp.Id
+ return &idpAttr{
+ resp.GetId(),
+ name,
+ &object.Details{
+ Sequence: resp.Details.Sequence,
+ ChangeDate: resp.Details.ChangeDate,
+ ResourceOwner: resp.Details.ResourceOwner,
+ }}
+ },
+ },
+ wantErr: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ idpAttr := tt.args.dep(tt.args.ctx, tt.args.req)
+ retryDuration := time.Minute
+ if ctxDeadline, ok := CTX.Deadline(); ok {
+ retryDuration = time.Until(ctxDeadline)
+ }
+ require.EventuallyWithT(t, func(ttt *assert.CollectT) {
+ got, getErr := Client.GetIDPByID(tt.args.ctx, tt.args.req)
+ assertErr := assert.NoError
+ if tt.wantErr {
+ assertErr = assert.Error
+ }
+ assertErr(ttt, getErr)
+ if getErr != nil {
+ return
+ }
+
+ // set provided info from creation
+ tt.want.Idp.Details = idpAttr.Details
+ tt.want.Idp.Name = idpAttr.Name
+ tt.want.Idp.Id = idpAttr.ID
+
+ // first check for details, mgmt and admin api don't fill the details correctly
+ integration.AssertDetails(t, tt.want.Idp, got.Idp)
+ // then set details
+ tt.want.Idp.Details = got.Idp.Details
+ // to check the rest of the content
+ assert.Equal(ttt, tt.want.Idp, got.Idp)
+ }, retryDuration, time.Second)
+ })
+ }
+}
diff --git a/internal/api/grpc/idp/v2/server.go b/internal/api/grpc/idp/v2/server.go
new file mode 100644
index 0000000000..246e980434
--- /dev/null
+++ b/internal/api/grpc/idp/v2/server.go
@@ -0,0 +1,56 @@
+package idp
+
+import (
+ "google.golang.org/grpc"
+
+ "github.com/zitadel/zitadel/internal/api/authz"
+ "github.com/zitadel/zitadel/internal/api/grpc/server"
+ "github.com/zitadel/zitadel/internal/command"
+ "github.com/zitadel/zitadel/internal/domain"
+ "github.com/zitadel/zitadel/internal/query"
+ "github.com/zitadel/zitadel/pkg/grpc/idp/v2"
+)
+
+var _ idp.IdentityProviderServiceServer = (*Server)(nil)
+
+type Server struct {
+ idp.UnimplementedIdentityProviderServiceServer
+ command *command.Commands
+ query *query.Queries
+
+ checkPermission domain.PermissionCheck
+}
+
+type Config struct{}
+
+func CreateServer(
+ command *command.Commands,
+ query *query.Queries,
+ checkPermission domain.PermissionCheck,
+) *Server {
+ return &Server{
+ command: command,
+ query: query,
+ checkPermission: checkPermission,
+ }
+}
+
+func (s *Server) RegisterServer(grpcServer *grpc.Server) {
+ idp.RegisterIdentityProviderServiceServer(grpcServer, s)
+}
+
+func (s *Server) AppName() string {
+ return idp.IdentityProviderService_ServiceDesc.ServiceName
+}
+
+func (s *Server) MethodPrefix() string {
+ return idp.IdentityProviderService_ServiceDesc.ServiceName
+}
+
+func (s *Server) AuthMethods() authz.MethodMapping {
+ return idp.IdentityProviderService_AuthMethods
+}
+
+func (s *Server) RegisterGateway() server.RegisterGatewayFunc {
+ return idp.RegisterIdentityProviderServiceHandler
+}
diff --git a/internal/api/grpc/idp/v2/server_integration_test.go b/internal/api/grpc/idp/v2/server_integration_test.go
new file mode 100644
index 0000000000..9e8f44a311
--- /dev/null
+++ b/internal/api/grpc/idp/v2/server_integration_test.go
@@ -0,0 +1,40 @@
+//go:build integration
+
+package idp_test
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/zitadel/zitadel/internal/integration"
+ idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2"
+)
+
+var (
+ CTX context.Context
+ IamCTX context.Context
+ UserCTX context.Context
+ SystemCTX context.Context
+ ErrCTX context.Context
+ Tester *integration.Tester
+ Client idp_pb.IdentityProviderServiceClient
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(func() int {
+ ctx, errCtx, cancel := integration.Contexts(time.Hour)
+ defer cancel()
+
+ Tester = integration.NewTester(ctx)
+ defer Tester.Done()
+
+ UserCTX = Tester.WithAuthorization(ctx, integration.Login)
+ IamCTX = Tester.WithAuthorization(ctx, integration.IAMOwner)
+ SystemCTX = Tester.WithAuthorization(ctx, integration.SystemUser)
+ CTX, ErrCTX = Tester.WithAuthorization(ctx, integration.OrgOwner), errCtx
+ Client = Tester.Client.IDPv2
+ return m.Run()
+ }())
+}
diff --git a/internal/api/grpc/management/idp.go b/internal/api/grpc/management/idp.go
index f013015258..66b659d1ea 100644
--- a/internal/api/grpc/management/idp.go
+++ b/internal/api/grpc/management/idp.go
@@ -149,7 +149,7 @@ func (s *Server) GetProviderByID(ctx context.Context, req *mgmt_pb.GetProviderBy
if err != nil {
return nil, err
}
- idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, orgIDQuery)
+ idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, nil, orgIDQuery)
if err != nil {
return nil, err
}
diff --git a/internal/api/ui/login/policy_handler.go b/internal/api/ui/login/policy_handler.go
index e5e6336068..2ff4ac9b78 100644
--- a/internal/api/ui/login/policy_handler.go
+++ b/internal/api/ui/login/policy_handler.go
@@ -18,7 +18,7 @@ func (l *Login) getOrgDomainPolicy(r *http.Request, orgID string) (*query.Domain
}
func (l *Login) getIDPByID(r *http.Request, id string) (*query.IDPTemplate, error) {
- return l.query.IDPTemplateByID(r.Context(), false, id, false)
+ return l.query.IDPTemplateByID(r.Context(), false, id, false, nil)
}
func (l *Login) getLoginPolicy(r *http.Request, orgID string) (*query.LoginPolicy, error) {
diff --git a/internal/domain/permission.go b/internal/domain/permission.go
index cf2d02d426..75d7f792ff 100644
--- a/internal/domain/permission.go
+++ b/internal/domain/permission.go
@@ -33,4 +33,6 @@ const (
PermissionUserCredentialWrite = "user.credential.write"
PermissionSessionWrite = "session.write"
PermissionSessionDelete = "session.delete"
+ PermissionIDPRead = "iam.idp.read"
+ PermissionOrgIDPRead = "org.idp.read"
)
diff --git a/internal/integration/client.go b/internal/integration/client.go
index 947c11508b..a69ce6a1ee 100644
--- a/internal/integration/client.go
+++ b/internal/integration/client.go
@@ -24,22 +24,24 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
"github.com/zitadel/zitadel/internal/idp/providers/saml"
- "github.com/zitadel/zitadel/internal/repository/idp"
+ idp_rp "github.com/zitadel/zitadel/internal/repository/idp"
"github.com/zitadel/zitadel/pkg/grpc/admin"
"github.com/zitadel/zitadel/pkg/grpc/auth"
"github.com/zitadel/zitadel/pkg/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/pkg/grpc/feature/v2beta"
+ "github.com/zitadel/zitadel/pkg/grpc/idp"
+ idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp/v2"
mgmt "github.com/zitadel/zitadel/pkg/grpc/management"
- object "github.com/zitadel/zitadel/pkg/grpc/object/v2"
+ "github.com/zitadel/zitadel/pkg/grpc/object/v2"
oidc_pb "github.com/zitadel/zitadel/pkg/grpc/oidc/v2"
oidc_pb_v2beta "github.com/zitadel/zitadel/pkg/grpc/oidc/v2beta"
- org "github.com/zitadel/zitadel/pkg/grpc/org/v2"
+ "github.com/zitadel/zitadel/pkg/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/pkg/grpc/org/v2beta"
action "github.com/zitadel/zitadel/pkg/grpc/resources/action/v3alpha"
webkey_v3alpha "github.com/zitadel/zitadel/pkg/grpc/resources/webkey/v3alpha"
- session "github.com/zitadel/zitadel/pkg/grpc/session/v2"
+ "github.com/zitadel/zitadel/pkg/grpc/session/v2"
session_v2beta "github.com/zitadel/zitadel/pkg/grpc/session/v2beta"
- settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2"
+ "github.com/zitadel/zitadel/pkg/grpc/settings/v2"
settings_v2beta "github.com/zitadel/zitadel/pkg/grpc/settings/v2beta"
"github.com/zitadel/zitadel/pkg/grpc/system"
user_pb "github.com/zitadel/zitadel/pkg/grpc/user"
@@ -69,6 +71,7 @@ type Client struct {
FeatureV2 feature.FeatureServiceClient
UserSchemaV3 schema.UserSchemaServiceClient
WebKeyV3Alpha webkey_v3alpha.ZITADELWebKeysClient
+ IDPv2 idp_pb.IdentityProviderServiceClient
}
func newClient(cc *grpc.ClientConn) Client {
@@ -93,6 +96,7 @@ func newClient(cc *grpc.ClientConn) Client {
FeatureV2: feature.NewFeatureServiceClient(cc),
UserSchemaV3: schema.NewUserSchemaServiceClient(cc),
WebKeyV3Alpha: webkey_v3alpha.NewZITADELWebKeysClient(cc),
+ IDPv2: idp_pb.NewIdentityProviderServiceClient(cc),
}
}
@@ -367,6 +371,28 @@ func (s *Tester) SetUserPassword(ctx context.Context, userID, password string, c
return resp.GetDetails()
}
+func (s *Tester) AddGenericOAuthIDP(ctx context.Context, name string) *admin.AddGenericOAuthProviderResponse {
+ resp, err := s.Client.Admin.AddGenericOAuthProvider(ctx, &admin.AddGenericOAuthProviderRequest{
+ Name: name,
+ ClientId: "clientID",
+ ClientSecret: "clientSecret",
+ AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
+ TokenEndpoint: "https://example.com/oauth/v2/token",
+ UserEndpoint: "https://api.example.com/user",
+ Scopes: []string{"openid", "profile", "email"},
+ IdAttribute: "id",
+ ProviderOptions: &idp.Options{
+ IsLinkingAllowed: true,
+ IsCreationAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
+ },
+ })
+ logging.OnError(err).Fatal("create generic OAuth idp")
+ return resp
+}
+
func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) string {
ctx = authz.WithInstance(ctx, s.Instance)
id, _, err := s.Commands.AddInstanceGenericOAuthProvider(ctx, command.GenericOAuthProvider{
@@ -378,7 +404,7 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) stri
UserEndpoint: "https://api.example.com/user",
Scopes: []string{"openid", "profile", "email"},
IDAttribute: "id",
- IDPOptions: idp.Options{
+ IDPOptions: idp_rp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
@@ -389,6 +415,28 @@ func (s *Tester) AddGenericOAuthProvider(t *testing.T, ctx context.Context) stri
return id
}
+func (s *Tester) AddOrgGenericOAuthIDP(ctx context.Context, name string) *mgmt.AddGenericOAuthProviderResponse {
+ resp, err := s.Client.Mgmt.AddGenericOAuthProvider(ctx, &mgmt.AddGenericOAuthProviderRequest{
+ Name: name,
+ ClientId: "clientID",
+ ClientSecret: "clientSecret",
+ AuthorizationEndpoint: "https://example.com/oauth/v2/authorize",
+ TokenEndpoint: "https://example.com/oauth/v2/token",
+ UserEndpoint: "https://api.example.com/user",
+ Scopes: []string{"openid", "profile", "email"},
+ IdAttribute: "id",
+ ProviderOptions: &idp.Options{
+ IsLinkingAllowed: true,
+ IsCreationAllowed: true,
+ IsAutoCreation: true,
+ IsAutoUpdate: true,
+ AutoLinking: idp.AutoLinkingOption_AUTO_LINKING_OPTION_USERNAME,
+ },
+ })
+ logging.OnError(err).Fatal("create generic OAuth idp")
+ return resp
+}
+
func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, orgID string) string {
ctx = authz.WithInstance(ctx, s.Instance)
id, _, err := s.Commands.AddOrgGenericOAuthProvider(ctx, orgID,
@@ -401,7 +449,7 @@ func (s *Tester) AddOrgGenericOAuthProvider(t *testing.T, ctx context.Context, o
UserEndpoint: "https://api.example.com/user",
Scopes: []string{"openid", "profile", "email"},
IDAttribute: "id",
- IDPOptions: idp.Options{
+ IDPOptions: idp_rp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
@@ -417,7 +465,7 @@ func (s *Tester) AddSAMLProvider(t *testing.T, ctx context.Context) string {
id, _, err := s.Server.Commands.AddInstanceSAMLProvider(ctx, command.SAMLProvider{
Name: "saml-idp",
Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n \n"),
- IDPOptions: idp.Options{
+ IDPOptions: idp_rp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
@@ -435,7 +483,7 @@ func (s *Tester) AddSAMLRedirectProvider(t *testing.T, ctx context.Context, tran
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"),
TransientMappingAttributeName: transientMappingAttributeName,
- IDPOptions: idp.Options{
+ IDPOptions: idp_rp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
@@ -452,7 +500,7 @@ func (s *Tester) AddSAMLPostProvider(t *testing.T, ctx context.Context) string {
Name: "saml-idp-post",
Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
Metadata: []byte("\n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8Ahs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+aucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWxm+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURNB2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0OBBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uvNONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEfy/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsbGFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTLUzreO96WzlBBMtY=\n \n \n \n \n \n \n \n urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n \n \n"),
- IDPOptions: idp.Options{
+ IDPOptions: idp_rp.Options{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
diff --git a/internal/query/idp_template.go b/internal/query/idp_template.go
index 2b835f0e69..e3250f1ae7 100644
--- a/internal/query/idp_template.go
+++ b/internal/query/idp_template.go
@@ -712,8 +712,29 @@ var (
}
)
-// IDPTemplateByID searches for the requested id
-func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) {
+// IDPTemplateByID searches for the requested id with permission check if necessary
+func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, permissionCheck domain.PermissionCheck, queries ...SearchQuery) (template *IDPTemplate, err error) {
+ idp, err := q.idpTemplateByID(ctx, shouldTriggerBulk, id, withOwnerRemoved, queries...)
+ if err != nil {
+ return nil, err
+ }
+ if permissionCheck != nil {
+ switch idp.OwnerType {
+ case domain.IdentityProviderTypeSystem:
+ if err := permissionCheck(ctx, domain.PermissionIDPRead, idp.ResourceOwner, idp.ID); err != nil {
+ return nil, err
+ }
+ case domain.IdentityProviderTypeOrg:
+ if err := permissionCheck(ctx, domain.PermissionOrgIDPRead, idp.ResourceOwner, idp.ID); err != nil {
+ return nil, err
+ }
+ }
+ }
+ return idp, nil
+}
+
+// idpTemplateByID searches for the requested id
+func (q *Queries) idpTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (template *IDPTemplate, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
diff --git a/pkg/grpc/idp/v2/idp.go b/pkg/grpc/idp/v2/idp.go
new file mode 100644
index 0000000000..514f6c5e33
--- /dev/null
+++ b/pkg/grpc/idp/v2/idp.go
@@ -0,0 +1,4 @@
+package idp
+
+type IsIDPConfig = isIDPConfig_Config
+type IsAzureADTenantType = isAzureADTenant_Type
diff --git a/proto/zitadel/idp/v2/idp.proto b/proto/zitadel/idp/v2/idp.proto
new file mode 100644
index 0000000000..784e717d3a
--- /dev/null
+++ b/proto/zitadel/idp/v2/idp.proto
@@ -0,0 +1,391 @@
+syntax = "proto3";
+
+package zitadel.idp.v2;
+
+import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/object/v2/object.proto";
+import "google/api/annotations.proto";
+import "google/api/field_behavior.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+import "validate/validate.proto";
+import "google/protobuf/duration.proto";
+
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/idp/v2;idp";
+
+message IDP {
+ // Unique identifier for the identity provider.
+ string id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"69629023906488334\"";
+ }
+ ];
+ zitadel.object.v2.Details details = 2;
+ // Current state of the identity provider.
+ IDPState state = 3;
+ string name = 4 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"Google\"";
+ }
+ ];
+ // Type of the identity provider, for example OIDC, JWT, LDAP and SAML.
+ IDPType type = 5;
+ // Configuration for the type of the identity provider.
+ IDPConfig config = 6;
+}
+
+enum IDPState {
+ IDP_STATE_UNSPECIFIED = 0;
+ IDP_STATE_ACTIVE = 1;
+ IDP_STATE_INACTIVE = 2;
+ IDP_STATE_REMOVED = 3;
+ IDP_STATE_MIGRATED = 4;
+}
+
+enum IDPType {
+ IDP_TYPE_UNSPECIFIED = 0;
+ IDP_TYPE_OIDC = 1;
+ IDP_TYPE_JWT = 2;
+ IDP_TYPE_LDAP = 3;
+ IDP_TYPE_OAUTH = 4;
+ IDP_TYPE_AZURE_AD = 5;
+ IDP_TYPE_GITHUB = 6;
+ IDP_TYPE_GITHUB_ES = 7;
+ IDP_TYPE_GITLAB = 8;
+ IDP_TYPE_GITLAB_SELF_HOSTED = 9;
+ IDP_TYPE_GOOGLE = 10;
+ IDP_TYPE_APPLE = 11;
+ IDP_TYPE_SAML = 12;
+}
+
+enum SAMLBinding {
+ SAML_BINDING_UNSPECIFIED = 0;
+ SAML_BINDING_POST = 1;
+ SAML_BINDING_REDIRECT = 2;
+ SAML_BINDING_ARTIFACT = 3;
+}
+
+enum SAMLNameIDFormat {
+ SAML_NAME_ID_FORMAT_UNSPECIFIED = 0;
+ SAML_NAME_ID_FORMAT_EMAIL_ADDRESS = 1;
+ SAML_NAME_ID_FORMAT_PERSISTENT = 2;
+ SAML_NAME_ID_FORMAT_TRANSIENT = 3;
+}
+
+message IDPConfig {
+ Options options = 1;
+ oneof config {
+ LDAPConfig ldap = 2;
+ GoogleConfig google = 3;
+ OAuthConfig oauth = 4;
+ GenericOIDCConfig oidc = 5;
+ JWTConfig jwt = 6;
+ GitHubConfig github = 7;
+ GitHubEnterpriseServerConfig github_es = 8;
+ GitLabConfig gitlab = 9;
+ GitLabSelfHostedConfig gitlab_self_hosted = 10;
+ AzureADConfig azure_ad = 11;
+ AppleConfig apple = 12;
+ SAMLConfig saml = 13;
+ }
+}
+
+message JWTConfig {
+ // The endpoint where the JWT can be extracted.
+ string jwt_endpoint = 1 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"https://accounts.google.com\"";
+ }
+ ];
+ // The issuer of the JWT (for validation).
+ string issuer = 2 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"https://accounts.google.com\"";
+ }
+ ];
+ // The endpoint to the key (JWK) which is used to sign the JWT with.
+ string keys_endpoint = 3 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"https://accounts.google.com/keys\"";
+ }
+ ];
+ // The name of the header where the JWT is sent in, default is authorization.
+ string header_name = 4 [
+ (validate.rules).string = {min_len: 1, max_len: 200},
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"x-auth-token\"";
+ }
+ ];
+}
+
+message OAuthConfig {
+ // Client id generated by the identity provider.
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"client-id\"";
+ }
+ ];
+ // The endpoint where ZITADEL send the user to authenticate.
+ string authorization_endpoint = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"https://accounts.google.com/o/oauth2/v2/auth\"";
+ }
+ ];
+ // The endpoint where ZITADEL can get the token.
+ string token_endpoint = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"https://oauth2.googleapis.com/token\"";
+ }
+ ];
+ // The endpoint where ZITADEL can get the user information.
+ string user_endpoint = 4 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"https://openidconnect.googleapis.com/v1/userinfo\"";
+ }
+ ];
+ // The scopes requested by ZITADEL during the request on the identity provider.
+ repeated string scopes = 5 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"openid\", \"profile\", \"email\"]";
+ }
+ ];
+ // Defines how the attribute is called where ZITADEL can get the id of the user.
+ string id_attribute = 6 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"user_id\"";
+ }
+ ];
+}
+
+message GenericOIDCConfig {
+ // The OIDC issuer of the identity provider.
+ string issuer = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"https://accounts.google.com/\"";
+ }
+ ];
+ // Client id generated by the identity provider.
+ string client_id = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"client-id\"";
+ }
+ ];
+ // The scopes requested by ZITADEL during the request on the identity provider.
+ repeated string scopes = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"openid\", \"profile\", \"email\"]";
+ }
+ ];
+ // If true, provider information get mapped from the id token, not from the userinfo endpoint.
+ bool is_id_token_mapping = 4 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "true";
+ }
+ ];
+}
+
+message GitHubConfig {
+ // The client ID of the GitHub App.
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"client-id\"";
+ }
+ ];
+ // The scopes requested by ZITADEL during the request to GitHub.
+ repeated string scopes = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"openid\", \"profile\", \"email\"]";
+ }
+ ];
+}
+
+message GitHubEnterpriseServerConfig {
+ // The client ID of the GitHub App.
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"client-id\"";
+ }
+ ];
+ string authorization_endpoint = 2;
+ string token_endpoint = 3;
+ string user_endpoint = 4;
+ // The scopes requested by ZITADEL during the request to GitHub.
+ repeated string scopes = 5 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"openid\", \"profile\", \"email\"]";
+ }
+ ];
+}
+
+message GoogleConfig {
+ // Client id of the Google application.
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"client-id\"";
+ }
+ ];
+ // The scopes requested by ZITADEL during the request to Google.
+ repeated string scopes = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"openid\", \"profile\", \"email\"]";
+ }
+ ];
+}
+
+message GitLabConfig {
+ // Client id of the GitLab application.
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"client-id\"";
+ }
+ ];
+ // The scopes requested by ZITADEL during the request to GitLab.
+ repeated string scopes = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"openid\", \"profile\", \"email\"]";
+ }
+ ];
+}
+
+message GitLabSelfHostedConfig {
+ string issuer = 1;
+ // Client id of the GitLab application.
+ string client_id = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"client-id\"";
+ }
+ ];
+ // The scopes requested by ZITADEL during the request to GitLab.
+ repeated string scopes = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"openid\", \"profile\", \"email\"]";
+ }
+ ];
+}
+
+message LDAPConfig {
+ repeated string servers = 1;
+ bool start_tls = 2;
+ string base_dn = 3;
+ string bind_dn = 4;
+ string user_base = 5;
+ repeated string user_object_classes = 6;
+ repeated string user_filters = 7;
+ google.protobuf.Duration timeout = 8;
+ LDAPAttributes attributes = 9;
+}
+
+message SAMLConfig {
+ // Metadata of the SAML identity provider.
+ bytes metadata_xml = 1;
+ // Binding which defines the type of communication with the identity provider.
+ SAMLBinding binding = 2;
+ // Boolean which defines if the authentication requests are signed.
+ bool with_signed_request = 3;
+ // `nameid-format` for the SAML Request.
+ SAMLNameIDFormat name_id_format = 4;
+ // Optional name of the attribute, which will be used to map the user
+ // in case the nameid-format returned is `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
+ optional string transient_mapping_attribute_name = 5;
+}
+
+message AzureADConfig {
+ // Client id of the Azure AD application
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"client-id\"";
+ }
+ ];
+ // Defines what user accounts should be able to login (Personal, Organizational, All).
+ AzureADTenant tenant = 2;
+ // Azure AD doesn't send if the email has been verified. Enable this if the user email should always be added verified in ZITADEL (no verification emails will be sent).
+ bool email_verified = 3;
+ // The scopes requested by ZITADEL during the request to Azure AD.
+ repeated string scopes = 4 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"openid\", \"profile\", \"email\", \"User.Read\"]";
+ }
+ ];
+}
+
+message Options {
+ // Enable if users should be able to link an existing ZITADEL user with an external account.
+ bool is_linking_allowed = 1;
+ // Enable if users should be able to create a new account in ZITADEL when using an external account.
+ bool is_creation_allowed = 2;
+ // Enable if a new account in ZITADEL should be created automatically when login with an external account.
+ bool is_auto_creation = 3;
+ // Enable if a the ZITADEL account fields should be updated automatically on each login.
+ bool is_auto_update = 4;
+ // Enable if users should get prompted to link an existing ZITADEL user to an external account if the selected attribute matches.
+ AutoLinkingOption auto_linking = 5 ;
+}
+
+enum AutoLinkingOption {
+ // AUTO_LINKING_OPTION_UNSPECIFIED disables the auto linking prompt.
+ AUTO_LINKING_OPTION_UNSPECIFIED = 0;
+ // AUTO_LINKING_OPTION_USERNAME will use the username of the external user to check for a corresponding ZITADEL user.
+ AUTO_LINKING_OPTION_USERNAME = 1;
+ // AUTO_LINKING_OPTION_EMAIL will use the email of the external user to check for a corresponding ZITADEL user with the same verified email
+ // Note that in case multiple users match, no prompt will be shown.
+ AUTO_LINKING_OPTION_EMAIL = 2;
+}
+
+message LDAPAttributes {
+ string id_attribute = 1 [(validate.rules).string = {max_len: 200}];
+ string first_name_attribute = 2 [(validate.rules).string = {max_len: 200}];
+ string last_name_attribute = 3 [(validate.rules).string = {max_len: 200}];
+ string display_name_attribute = 4 [(validate.rules).string = {max_len: 200}];
+ string nick_name_attribute = 5 [(validate.rules).string = {max_len: 200}];
+ string preferred_username_attribute = 6 [(validate.rules).string = {max_len: 200}];
+ string email_attribute = 7 [(validate.rules).string = {max_len: 200}];
+ string email_verified_attribute = 8 [(validate.rules).string = {max_len: 200}];
+ string phone_attribute = 9 [(validate.rules).string = {max_len: 200}];
+ string phone_verified_attribute = 10 [(validate.rules).string = {max_len: 200}];
+ string preferred_language_attribute = 11 [(validate.rules).string = {max_len: 200}];
+ string avatar_url_attribute = 12 [(validate.rules).string = {max_len: 200}];
+ string profile_attribute = 13 [(validate.rules).string = {max_len: 200}];
+}
+
+enum AzureADTenantType {
+ AZURE_AD_TENANT_TYPE_COMMON = 0;
+ AZURE_AD_TENANT_TYPE_ORGANISATIONS = 1;
+ AZURE_AD_TENANT_TYPE_CONSUMERS = 2;
+}
+
+message AzureADTenant {
+ oneof type {
+ AzureADTenantType tenant_type = 1;
+ string tenant_id = 2;
+ }
+}
+
+message AppleConfig {
+ // Client id (App ID or Service ID) provided by Apple.
+ string client_id = 1 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"com.client.id\"";
+ }
+ ];
+ // Team ID provided by Apple.
+ string team_id = 2 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"ALT03JV3OS\"";
+ }
+ ];
+ // ID of the private key generated by Apple.
+ string key_id = 3 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "\"OGKDK25KD\"";
+ }
+ ];
+ // The scopes requested by ZITADEL during the request to Apple.
+ repeated string scopes = 4 [
+ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
+ example: "[\"name\", \"email\"]";
+ }
+ ];
+}
\ No newline at end of file
diff --git a/proto/zitadel/idp/v2/idp_service.proto b/proto/zitadel/idp/v2/idp_service.proto
new file mode 100644
index 0000000000..2d5306cea6
--- /dev/null
+++ b/proto/zitadel/idp/v2/idp_service.proto
@@ -0,0 +1,136 @@
+syntax = "proto3";
+
+package zitadel.idp.v2;
+
+import "zitadel/protoc_gen_zitadel/v2/options.proto";
+import "zitadel/object/v2/object.proto";
+import "google/api/annotations.proto";
+import "google/api/field_behavior.proto";
+import "protoc-gen-openapiv2/options/annotations.proto";
+import "validate/validate.proto";
+import "zitadel/idp/v2/idp.proto";
+
+option go_package = "github.com/zitadel/zitadel/pkg/grpc/idp/v2;idp";
+
+option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
+ info: {
+ title: "Identity Provider Service";
+ version: "2.0";
+ description: "This API is intended to manage identity providers (IdPs) in a ZITADEL instance.";
+ contact:{
+ name: "ZITADEL"
+ url: "https://zitadel.com"
+ email: "hi@zitadel.com"
+ }
+ license: {
+ name: "Apache 2.0",
+ url: "https://github.com/zitadel/zitadel/blob/main/LICENSE";
+ };
+ };
+ schemes: HTTPS;
+ schemes: HTTP;
+
+ consumes: "application/json";
+ consumes: "application/grpc";
+
+ produces: "application/json";
+ produces: "application/grpc";
+
+ consumes: "application/grpc-web+proto";
+ produces: "application/grpc-web+proto";
+
+ host: "$CUSTOM-DOMAIN";
+ base_path: "/";
+
+ external_docs: {
+ description: "Detailed information about ZITADEL",
+ url: "https://zitadel.com/docs"
+ }
+ security_definitions: {
+ security: {
+ key: "OAuth2";
+ value: {
+ type: TYPE_OAUTH2;
+ flow: FLOW_ACCESS_CODE;
+ authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
+ token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
+ scopes: {
+ scope: {
+ key: "openid";
+ value: "openid";
+ }
+ scope: {
+ key: "urn:zitadel:iam:org:project:id:zitadel:aud";
+ value: "urn:zitadel:iam:org:project:id:zitadel:aud";
+ }
+ }
+ }
+ }
+ }
+ security: {
+ security_requirement: {
+ key: "OAuth2";
+ value: {
+ scope: "openid";
+ scope: "urn:zitadel:iam:org:project:id:zitadel:aud";
+ }
+ }
+ }
+ responses: {
+ key: "403";
+ value: {
+ description: "Returned when the user does not have permission to access the resource.";
+ schema: {
+ json_schema: {
+ ref: "#/definitions/rpcStatus";
+ }
+ }
+ }
+ }
+ responses: {
+ key: "404";
+ value: {
+ description: "Returned when the resource does not exist.";
+ schema: {
+ json_schema: {
+ ref: "#/definitions/rpcStatus";
+ }
+ }
+ }
+ }
+};
+
+service IdentityProviderService {
+
+ // Get identity provider (IdP) by ID
+ //
+ // Returns an identity provider (social/enterprise login) by its ID, which can be of the type Google, AzureAD, etc.
+ rpc GetIDPByID (GetIDPByIDRequest) returns (GetIDPByIDResponse) {
+ option (google.api.http) = {
+ get: "/v2/idps/{id}"
+ };
+
+ option (zitadel.protoc_gen_zitadel.v2.options) = {
+ auth_option: {
+ permission: "authenticated"
+ }
+ };
+
+ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
+ responses: {
+ key: "200"
+ value: {
+ description: "OK";
+ }
+ };
+ };
+ }
+}
+
+message GetIDPByIDRequest {
+ string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
+}
+
+message GetIDPByIDResponse {
+ zitadel.idp.v2.IDP idp = 1;
+}
diff --git a/proto/zitadel/resources/action/v3alpha/action_service.proto b/proto/zitadel/resources/action/v3alpha/action_service.proto
index 08d57e93e5..228899310a 100644
--- a/proto/zitadel/resources/action/v3alpha/action_service.proto
+++ b/proto/zitadel/resources/action/v3alpha/action_service.proto
@@ -47,7 +47,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
consumes: "application/grpc-web+proto";
produces: "application/grpc-web+proto";
- host: "${ZITADEL_DOMAIN}";
+ host: "$CUSTOM-DOMAIN";
base_path: "/resources/v3alpha/actions";
external_docs: {
@@ -60,8 +60,8 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
value: {
type: TYPE_OAUTH2;
flow: FLOW_ACCESS_CODE;
- authorization_url: "${ZITADEL_DOMAIN}/oauth/v2/authorize";
- token_url: "${ZITADEL_DOMAIN}/oauth/v2/token";
+ authorization_url: "$CUSTOM-DOMAIN/oauth/v2/authorize";
+ token_url: "$CUSTOM-DOMAIN/oauth/v2/token";
scopes: {
scope: {
key: "openid";
@@ -135,7 +135,7 @@ service ZITADELActions {
description: "Target successfully created";
schema: {
json_schema: {
- ref: "#/definitions/CreateTargetResponse";
+ ref: "#/definitions/v3alphaCreateTargetResponse";
}
}
};
@@ -278,7 +278,7 @@ service ZITADELActions {
description: "Execution successfully updated or left unchanged";
schema: {
json_schema: {
- ref: "#/definitions/SetExecutionResponse";
+ ref: "#/definitions/v3alphaSetExecutionResponse";
}
}
};
diff --git a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto
index c79424095b..72263c7b73 100644
--- a/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto
+++ b/proto/zitadel/resources/webkey/v3alpha/webkey_service.proto
@@ -42,7 +42,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
consumes: "application/grpc-web+proto";
produces: "application/grpc-web+proto";
- host: "${ZITADEL_DOMAIN}";
+ host: "$CUSTOM-DOMAIN";
base_path: "/resources/v3alpha/web_keys";
external_docs: {