Files
zitadel/proto/zitadel/idp.proto
Livio Spring 3c99cf82f8 feat: federated logout for SAML IdPs (#9931)
# Which Problems Are Solved

Currently if a user signs in using an IdP, once they sign out of
Zitadel, the corresponding IdP session is not terminated. This can be
the desired behavior. In some cases, e.g. when using a shared computer
it results in a potential security risk, since a follower user might be
able to sign in as the previous using the still open IdP session.

# How the Problems Are Solved

- Admins can enabled a federated logout option on SAML IdPs through the
Admin and Management APIs.
- During the termination of a login V1 session using OIDC end_session
endpoint, Zitadel will check if an IdP was used to authenticate that
session.
- In case there was a SAML IdP used with Federated Logout enabled, it
will intercept the logout process, store the information into the shared
cache and redirect to the federated logout endpoint in the V1 login.
- The V1 login federated logout endpoint checks every request on an
existing cache entry. On success it will create a SAML logout request
for the used IdP and either redirect or POST to the configured SLO
endpoint. The cache entry is updated with a `redirected` state.
- A SLO endpoint is added to the `/idp` handlers, which will handle the
SAML logout responses. At the moment it will check again for an existing
federated logout entry (with state `redirected`) in the cache. On
success, the user is redirected to the initially provided
`post_logout_redirect_uri` from the end_session request.

# Additional Changes

None

# Additional Context

- This PR merges the https://github.com/zitadel/zitadel/pull/9841 and
https://github.com/zitadel/zitadel/pull/9854 to main, additionally
updating the docs on Entra ID SAML.
- closes #9228
- backport to 3.x

---------

Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
Co-authored-by: Zach Hirschtritt <zachary.hirschtritt@klaviyo.com>
(cherry picked from commit 2cf3ef4de4)
2025-05-23 14:59:34 +02:00

605 lines
21 KiB
Protocol Buffer

syntax = "proto3";
import "zitadel/object.proto";
import "validate/validate.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "google/protobuf/duration.proto";
package zitadel.idp.v1;
option go_package ="github.com/zitadel/zitadel/pkg/grpc/idp";
message IDP {
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
}
];
zitadel.v1.ObjectDetails details = 2;
IDPState state = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the state of the identity provider";
}
];
string name = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"google\"";
}
];
IDPStylingType styling_type = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "some identity providers specify the styling of the button to their login";
}
];
IDPOwnerType owner = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the administrator of this identity provider";
}
];
oneof config {
OIDCConfig oidc_config = 7;
JWTConfig jwt_config = 9;
}
bool auto_register = 8;
}
message IDPUserLink {
string user_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
description: "the id of the user"
}
];
string idp_id = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
description: "the id of the identity provider";
}
];
string idp_name = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"google\"";
description: "the name of the identity provider";
}
];
string provided_user_id = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"as-12-df-89\"";
description: "the id of the user provided by the identity provider";
}
];
string provided_user_name = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"gigi.long-neck@gmail.com\"";
description: "the id of the identity provider";
}
];
IDPType idp_type = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "the authorization framework of the identity provider";
}
];
}
message IDPLoginPolicyLink {
string idp_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
description: "the id of the identity provider"
}
];
string idp_name = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"google\"";
description: "the name of the identity provider"
}
];
IDPType idp_type = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"IDP_TYPE_OIDC\"]"
description: "the authorization framework of the identity provider";
}
];
}
enum IDPState {
IDP_STATE_UNSPECIFIED = 0;
IDP_STATE_ACTIVE = 1;
IDP_STATE_INACTIVE = 2;
}
enum IDPStylingType {
STYLING_TYPE_UNSPECIFIED = 0;
STYLING_TYPE_GOOGLE = 1;
}
// authorization framework of the identity provider
enum IDPType {
IDP_TYPE_UNSPECIFIED = 0;
IDP_TYPE_OIDC = 1;
IDP_TYPE_JWT = 3;
}
// the owner of the identity provider.
enum IDPOwnerType {
IDP_OWNER_TYPE_UNSPECIFIED = 0;
// system is managed by the ZITADEL administrators
IDP_OWNER_TYPE_SYSTEM = 1;
// org is managed by de organization administrators
IDP_OWNER_TYPE_ORG = 2;
}
message OIDCConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "client id generated by the identity provider";
}
];
string issuer = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://accounts.google.com\"";
description: "the OIDC issuer of the identity provider";
}
];
repeated string scopes = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request on the identity provider";
}
];
OIDCMappingField display_name_mapping = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "definition which field is mapped to the display name of the user";
}
];
OIDCMappingField username_mapping = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "definition which field is mapped to the email of the user";
}
];
}
enum OIDCMappingField {
OIDC_MAPPING_FIELD_UNSPECIFIED = 0;
OIDC_MAPPING_FIELD_PREFERRED_USERNAME = 1;
OIDC_MAPPING_FIELD_EMAIL = 2;
}
message JWTConfig {
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\"";
description: "the endpoint where the JWT can be extracted";
}
];
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\"";
description: "the issuer of the JWT (for validation)";
}
];
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\"";
description: "the endpoint to the key (JWK) which is used to sign the JWT with";
}
];
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\"";
description: "the name of the header where the JWT is sent in, default is authorization";
}
];
}
message IDPIDQuery {
string id = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
}
];
}
message IDPNameQuery {
string name = 1 [
(validate.rules).string = {max_len: 200},
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"google\"";
}
];
zitadel.v1.TextQueryMethod method = 2 [
(validate.rules).enum.defined_only = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "defines which text equality method is used";
}
];
}
message IDPOwnerTypeQuery {
IDPOwnerType owner_type = 1 [
(validate.rules).enum.defined_only = true,
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "search for custom or global identity providers";
}
];
}
enum IDPFieldName {
IDP_FIELD_NAME_UNSPECIFIED = 0;
IDP_FIELD_NAME_NAME = 1;
}
message Provider {
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629023906488334\"";
}
];
zitadel.v1.ObjectDetails details = 2;
IDPState state = 3;
string name = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"Google\"";
}
];
IDPOwnerType owner = 5;
ProviderType type = 6;
ProviderConfig config = 7;
}
enum ProviderType {
PROVIDER_TYPE_UNSPECIFIED = 0;
PROVIDER_TYPE_OIDC = 1;
PROVIDER_TYPE_JWT = 2;
PROVIDER_TYPE_LDAP = 3;
PROVIDER_TYPE_OAUTH = 4;
PROVIDER_TYPE_AZURE_AD = 5;
PROVIDER_TYPE_GITHUB = 6;
PROVIDER_TYPE_GITHUB_ES = 7;
PROVIDER_TYPE_GITLAB = 8;
PROVIDER_TYPE_GITLAB_SELF_HOSTED = 9;
PROVIDER_TYPE_GOOGLE = 10;
PROVIDER_TYPE_APPLE = 11;
PROVIDER_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 ProviderConfig {
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 OAuthConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "client id generated by the identity provider";
}
];
string authorization_endpoint = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://accounts.google.com/o/oauth2/v2/auth\"";
description: "the endpoint where ZITADEL send the user to authenticate";
}
];
string token_endpoint = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://oauth2.googleapis.com/token\"";
description: "the endpoint where ZITADEL can get the token";
}
];
string user_endpoint = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://openidconnect.googleapis.com/v1/userinfo\"";
description: "the endpoint where ZITADEL can get the user information";
}
];
repeated string scopes = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request on the identity provider";
}
];
string id_attribute = 6 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"user_id\"";
description: "defines how the attribute is called where ZITADEL can get the id of the user";
}
];
// Defines if the Proof Key for Code Exchange (PKCE) is used for the authorization code flow.
bool use_pkce = 7;
}
message GenericOIDCConfig {
string issuer = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"https://accounts.google.com/\"";
description: "the OIDC issuer of the identity provider";
}
];
string client_id = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "client id generated by the identity provider";
}
];
repeated string scopes = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request on the identity provider";
}
];
bool is_id_token_mapping = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
description: "if true, provider information get mapped from the id token, not from the userinfo endpoint";
}
];
// Defines if the Proof Key for Code Exchange (PKCE) is used for the authorization code flow.
bool use_pkce = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "true";
}
];
}
message GitHubConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "the client ID of the GitHub App";
}
];
repeated string scopes = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to GitHub";
}
];
}
message GitHubEnterpriseServerConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "the client ID of the GitHub App";
}
];
string authorization_endpoint = 2;
string token_endpoint = 3;
string user_endpoint = 4;
repeated string scopes = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to GitHub";
}
];
}
message GoogleConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "client id of the Google application";
}
];
repeated string scopes = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to Google";
}
];
}
message GitLabConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "client id of the GitLab application";
}
];
repeated string scopes = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to GitLab";
}
];
}
message GitLabSelfHostedConfig {
string issuer = 1;
string client_id = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "client id of the GitLab application";
}
];
repeated string scopes = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to GitLab";
}
];
}
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;
bytes root_ca = 10;
}
message SAMLConfig {
// Metadata of the SAML identity provider.
bytes metadata_xml = 1;
// Binding which defines the type of communication with the identity provider.
zitadel.idp.v1.SAMLBinding binding = 2;
// Boolean which defines if the authentication requests are signed.
bool with_signed_request = 3;
// `nameid-format` for the SAML Request.
zitadel.idp.v1.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;
// Boolean weather federated logout is enabled. If enabled, ZITADEL will send a logout request to the identity provider,
// if the user terminates the session in ZITADEL. Be sure to provide a SLO endpoint as part of the metadata.
optional bool federated_logout_enabled = 6;
}
message AzureADConfig {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"client-id\"";
description: "client id of the Azure AD application";
}
];
AzureADTenant tenant = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Defines what user accounts should be able to login (Personal, Organizational, All)";
}
];
bool email_verified = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "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)";
}
];
repeated string scopes = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"openid\", \"profile\", \"email\", \"User.Read\"]";
description: "the scopes requested by ZITADEL during the request to Azure AD";
}
];
}
message Options {
bool is_linking_allowed = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if users should be able to manually link an existing ZITADEL user with an external account. Disable if users should only be allowed to link the proposed account in case of active auto_linking.";
}
];
bool is_creation_allowed = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if users should be able to manually create a new account in ZITADEL when using an external account. Disable if users should not be able to edit account information when auto_creation is enabled.";
}
];
bool is_auto_creation = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if a new account in ZITADEL should be created automatically when login with an external account.";
}
];
bool is_auto_update = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if a the ZITADEL account fields should be updated automatically on each login.";
}
];
AutoLinkingOption auto_linking = 5 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
description: "Enable if users should get prompted to link an existing ZITADEL user to an external account if the selected attribute matches.";
}
];
}
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 {
string client_id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"com.client.id\"";
description: "Client id (App ID or Service ID) provided by Apple";
}
];
string team_id = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"ALT03JV3OS\"";
description: "Team ID provided by Apple";
}
];
string key_id = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"OGKDK25KD\"";
description: "ID of the private key generated by Apple";
}
];
repeated string scopes = 4 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "[\"name\", \"email\"]";
description: "the scopes requested by ZITADEL during the request to Apple";
}
];
}