zitadel/internal/api/grpc/management/idp_converter.go
Livio Spring 35df5f61fc
fix(saml): improve error handling (#8928)
# Which Problems Are Solved

There are multiple issues with the metadata and error handling of SAML:
- When providing a SAML metadata for an IdP, which cannot be processed,
the error will only be noticed once a user tries to use the IdP.
- Parsing for metadata with any other encoding than UTF-8 fails.
- Metadata containing an enclosing EntitiesDescriptor around
EntityDescriptor cannot be parsed.
- Metadata's `validUntil` value is always set to 48 hours, which causes
issues on external providers, if processed from a manual down/upload.
- If a SAML response cannot be parsed, only a generic "Authentication
failed" error is returned, the cause is hidden to the user and also to
actions.

# How the Problems Are Solved

- Return parsing errors after create / update and retrieval of an IdP in
the API.
- Prevent the creation and update of an IdP in case of a parsing
failure.
- Added decoders for encodings other than UTF-8 (including ASCII,
windows and ISO, [currently
supported](efd25daf28/encoding/ianaindex/ianaindex.go (L156)))
- Updated parsing to handle both `EntitiesDescriptor` and
`EntityDescriptor` as root element
- `validUntil` will automatically set to the certificate's expiration
time
- Unwrapped the hidden error to be returned. The Login UI will still
only provide a mostly generic error, but action can now access the
underlying error.

# Additional Changes

None

# Additional Context

reported by a customer

(cherry picked from commit ffe9570776)
2024-12-03 11:42:58 +01:00

513 lines
17 KiB
Go

package management
import (
"context"
"github.com/crewjam/saml"
"github.com/muhlemmer/gu"
"github.com/zitadel/zitadel/internal/api/authz"
idp_grpc "github.com/zitadel/zitadel/internal/api/grpc/idp"
"github.com/zitadel/zitadel/internal/api/grpc/object"
"github.com/zitadel/zitadel/internal/command"
"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/zerrors"
idp_pb "github.com/zitadel/zitadel/pkg/grpc/idp"
mgmt_pb "github.com/zitadel/zitadel/pkg/grpc/management"
)
func AddOIDCIDPRequestToDomain(req *mgmt_pb.AddOrgOIDCIDPRequest) *domain.IDPConfig {
return &domain.IDPConfig{
Name: req.Name,
OIDCConfig: addOIDCIDPRequestToDomainOIDCIDPConfig(req),
StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType),
Type: domain.IDPConfigTypeOIDC,
AutoRegister: req.AutoRegister,
}
}
func addOIDCIDPRequestToDomainOIDCIDPConfig(req *mgmt_pb.AddOrgOIDCIDPRequest) *domain.OIDCIDPConfig {
return &domain.OIDCIDPConfig{
ClientID: req.ClientId,
ClientSecretString: req.ClientSecret,
Issuer: req.Issuer,
Scopes: req.Scopes,
IDPDisplayNameMapping: idp_grpc.MappingFieldToDomain(req.DisplayNameMapping),
UsernameMapping: idp_grpc.MappingFieldToDomain(req.UsernameMapping),
}
}
func AddJWTIDPRequestToDomain(req *mgmt_pb.AddOrgJWTIDPRequest) *domain.IDPConfig {
return &domain.IDPConfig{
Name: req.Name,
JWTConfig: addJWTIDPRequestToDomainJWTIDPConfig(req),
StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType),
Type: domain.IDPConfigTypeJWT,
AutoRegister: req.AutoRegister,
}
}
func addJWTIDPRequestToDomainJWTIDPConfig(req *mgmt_pb.AddOrgJWTIDPRequest) *domain.JWTIDPConfig {
return &domain.JWTIDPConfig{
JWTEndpoint: req.JwtEndpoint,
Issuer: req.Issuer,
KeysEndpoint: req.KeysEndpoint,
HeaderName: req.HeaderName,
}
}
func updateIDPToDomain(req *mgmt_pb.UpdateOrgIDPRequest) *domain.IDPConfig {
return &domain.IDPConfig{
IDPConfigID: req.IdpId,
Name: req.Name,
StylingType: idp_grpc.IDPStylingTypeToDomain(req.StylingType),
AutoRegister: req.AutoRegister,
}
}
func updateOIDCConfigToDomain(req *mgmt_pb.UpdateOrgIDPOIDCConfigRequest) *domain.OIDCIDPConfig {
return &domain.OIDCIDPConfig{
IDPConfigID: req.IdpId,
ClientID: req.ClientId,
ClientSecretString: req.ClientSecret,
Issuer: req.Issuer,
Scopes: req.Scopes,
IDPDisplayNameMapping: idp_grpc.MappingFieldToDomain(req.DisplayNameMapping),
UsernameMapping: idp_grpc.MappingFieldToDomain(req.UsernameMapping),
}
}
func updateJWTConfigToDomain(req *mgmt_pb.UpdateOrgIDPJWTConfigRequest) *domain.JWTIDPConfig {
return &domain.JWTIDPConfig{
IDPConfigID: req.IdpId,
JWTEndpoint: req.JwtEndpoint,
Issuer: req.Issuer,
KeysEndpoint: req.KeysEndpoint,
HeaderName: req.HeaderName,
}
}
func listIDPsToModel(ctx context.Context, req *mgmt_pb.ListOrgIDPsRequest) (queries *query.IDPSearchQueries, err error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
q, err := idpQueriesToModel(req.Queries)
if err != nil {
return nil, err
}
resourceOwnerQuery, err := query.NewIDPResourceOwnerListSearchQuery(authz.GetInstance(ctx).InstanceID(), authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
q = append(q, resourceOwnerQuery)
return &query.IDPSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
SortingColumn: idp_grpc.FieldNameToModel(req.SortingColumn),
},
Queries: q,
}, nil
}
func idpQueriesToModel(queries []*mgmt_pb.IDPQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = idpQueryToModel(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func idpQueryToModel(idpQuery *mgmt_pb.IDPQuery) (query.SearchQuery, error) {
switch q := idpQuery.Query.(type) {
case *mgmt_pb.IDPQuery_IdpNameQuery:
return query.NewIDPNameSearchQuery(object.TextMethodToQuery(q.IdpNameQuery.Method), q.IdpNameQuery.Name)
case *mgmt_pb.IDPQuery_IdpIdQuery:
return query.NewIDPIDSearchQuery(q.IdpIdQuery.Id)
case *mgmt_pb.IDPQuery_OwnerTypeQuery:
return query.NewIDPOwnerTypeSearchQuery(idp_grpc.IDPProviderTypeFromPb(q.OwnerTypeQuery.OwnerType))
default:
return nil, zerrors.ThrowInvalidArgument(nil, "MANAG-WtLPV", "List.Query.Invalid")
}
}
func userLinksToDomain(idps []*query.IDPUserLink) []*domain.UserIDPLink {
links := make([]*domain.UserIDPLink, len(idps))
for i, idp := range idps {
links[i] = &domain.UserIDPLink{
ObjectRoot: models.ObjectRoot{
AggregateID: idp.UserID,
ResourceOwner: idp.ResourceOwner,
},
IDPConfigID: idp.IDPID,
ExternalUserID: idp.ProvidedUserID,
DisplayName: idp.ProvidedUsername,
}
}
return links
}
func listProvidersToQuery(ctx context.Context, req *mgmt_pb.ListProvidersRequest) (*query.IDPTemplateSearchQueries, error) {
offset, limit, asc := object.ListQueryToModel(req.Query)
queries, err := providerQueriesToQuery(req.Queries)
if err != nil {
return nil, err
}
resourceOwnerQuery, err := query.NewIDPTemplateResourceOwnerListSearchQuery(authz.GetInstance(ctx).InstanceID(), authz.GetCtxData(ctx).OrgID)
if err != nil {
return nil, err
}
queries = append(queries, resourceOwnerQuery)
return &query.IDPTemplateSearchQueries{
SearchRequest: query.SearchRequest{
Offset: offset,
Limit: limit,
Asc: asc,
},
Queries: queries,
}, nil
}
func providerQueriesToQuery(queries []*mgmt_pb.ProviderQuery) (q []query.SearchQuery, err error) {
q = make([]query.SearchQuery, len(queries))
for i, query := range queries {
q[i], err = providerQueryToQuery(query)
if err != nil {
return nil, err
}
}
return q, nil
}
func providerQueryToQuery(idpQuery *mgmt_pb.ProviderQuery) (query.SearchQuery, error) {
switch q := idpQuery.Query.(type) {
case *mgmt_pb.ProviderQuery_IdpNameQuery:
return query.NewIDPTemplateNameSearchQuery(object.TextMethodToQuery(q.IdpNameQuery.Method), q.IdpNameQuery.Name)
case *mgmt_pb.ProviderQuery_IdpIdQuery:
return query.NewIDPTemplateIDSearchQuery(q.IdpIdQuery.Id)
case *mgmt_pb.ProviderQuery_OwnerTypeQuery:
return query.NewIDPTemplateOwnerTypeSearchQuery(idp_grpc.IDPProviderTypeFromPb(q.OwnerTypeQuery.OwnerType))
default:
return nil, zerrors.ThrowInvalidArgument(nil, "ORG-Dr2aa", "List.Query.Invalid")
}
}
func addGenericOAuthProviderToCommand(req *mgmt_pb.AddGenericOAuthProviderRequest) command.GenericOAuthProvider {
return command.GenericOAuthProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
AuthorizationEndpoint: req.AuthorizationEndpoint,
TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes,
IDAttribute: req.IdAttribute,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGenericOAuthProviderToCommand(req *mgmt_pb.UpdateGenericOAuthProviderRequest) command.GenericOAuthProvider {
return command.GenericOAuthProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
AuthorizationEndpoint: req.AuthorizationEndpoint,
TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes,
IDAttribute: req.IdAttribute,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGenericOIDCProviderToCommand(req *mgmt_pb.AddGenericOIDCProviderRequest) command.GenericOIDCProvider {
return command.GenericOIDCProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IsIDTokenMapping: req.IsIdTokenMapping,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGenericOIDCProviderToCommand(req *mgmt_pb.UpdateGenericOIDCProviderRequest) command.GenericOIDCProvider {
return command.GenericOIDCProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IsIDTokenMapping: req.IsIdTokenMapping,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addJWTProviderToCommand(req *mgmt_pb.AddJWTProviderRequest) command.JWTProvider {
return command.JWTProvider{
Name: req.Name,
Issuer: req.Issuer,
JWTEndpoint: req.JwtEndpoint,
KeyEndpoint: req.KeysEndpoint,
HeaderName: req.HeaderName,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateJWTProviderToCommand(req *mgmt_pb.UpdateJWTProviderRequest) command.JWTProvider {
return command.JWTProvider{
Name: req.Name,
Issuer: req.Issuer,
JWTEndpoint: req.JwtEndpoint,
KeyEndpoint: req.KeysEndpoint,
HeaderName: req.HeaderName,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addAzureADProviderToCommand(req *mgmt_pb.AddAzureADProviderRequest) command.AzureADProvider {
return command.AzureADProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Tenant: idp_grpc.AzureADTenantToCommand(req.Tenant),
EmailVerified: req.EmailVerified,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
Scopes: req.Scopes,
}
}
func updateAzureADProviderToCommand(req *mgmt_pb.UpdateAzureADProviderRequest) command.AzureADProvider {
return command.AzureADProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Tenant: idp_grpc.AzureADTenantToCommand(req.Tenant),
EmailVerified: req.EmailVerified,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
Scopes: req.Scopes,
}
}
func addGitHubProviderToCommand(req *mgmt_pb.AddGitHubProviderRequest) command.GitHubProvider {
return command.GitHubProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGitHubProviderToCommand(req *mgmt_pb.UpdateGitHubProviderRequest) command.GitHubProvider {
return command.GitHubProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGitHubEnterpriseProviderToCommand(req *mgmt_pb.AddGitHubEnterpriseServerProviderRequest) command.GitHubEnterpriseProvider {
return command.GitHubEnterpriseProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
AuthorizationEndpoint: req.AuthorizationEndpoint,
TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGitHubEnterpriseProviderToCommand(req *mgmt_pb.UpdateGitHubEnterpriseServerProviderRequest) command.GitHubEnterpriseProvider {
return command.GitHubEnterpriseProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
AuthorizationEndpoint: req.AuthorizationEndpoint,
TokenEndpoint: req.TokenEndpoint,
UserEndpoint: req.UserEndpoint,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGitLabProviderToCommand(req *mgmt_pb.AddGitLabProviderRequest) command.GitLabProvider {
return command.GitLabProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGitLabProviderToCommand(req *mgmt_pb.UpdateGitLabProviderRequest) command.GitLabProvider {
return command.GitLabProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGitLabSelfHostedProviderToCommand(req *mgmt_pb.AddGitLabSelfHostedProviderRequest) command.GitLabSelfHostedProvider {
return command.GitLabSelfHostedProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGitLabSelfHostedProviderToCommand(req *mgmt_pb.UpdateGitLabSelfHostedProviderRequest) command.GitLabSelfHostedProvider {
return command.GitLabSelfHostedProvider{
Name: req.Name,
Issuer: req.Issuer,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addGoogleProviderToCommand(req *mgmt_pb.AddGoogleProviderRequest) command.GoogleProvider {
return command.GoogleProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateGoogleProviderToCommand(req *mgmt_pb.UpdateGoogleProviderRequest) command.GoogleProvider {
return command.GoogleProvider{
Name: req.Name,
ClientID: req.ClientId,
ClientSecret: req.ClientSecret,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addLDAPProviderToCommand(req *mgmt_pb.AddLDAPProviderRequest) command.LDAPProvider {
return command.LDAPProvider{
Name: req.Name,
Servers: req.Servers,
StartTLS: req.StartTls,
BaseDN: req.BaseDn,
BindDN: req.BindDn,
BindPassword: req.BindPassword,
UserBase: req.UserBase,
UserObjectClasses: req.UserObjectClasses,
UserFilters: req.UserFilters,
Timeout: req.Timeout.AsDuration(),
LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateLDAPProviderToCommand(req *mgmt_pb.UpdateLDAPProviderRequest) command.LDAPProvider {
return command.LDAPProvider{
Name: req.Name,
Servers: req.Servers,
StartTLS: req.StartTls,
BaseDN: req.BaseDn,
BindDN: req.BindDn,
BindPassword: req.BindPassword,
UserBase: req.UserBase,
UserObjectClasses: req.UserObjectClasses,
UserFilters: req.UserFilters,
Timeout: req.Timeout.AsDuration(),
LDAPAttributes: idp_grpc.LDAPAttributesToCommand(req.Attributes),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addAppleProviderToCommand(req *mgmt_pb.AddAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateAppleProviderToCommand(req *mgmt_pb.UpdateAppleProviderRequest) command.AppleProvider {
return command.AppleProvider{
Name: req.Name,
ClientID: req.ClientId,
TeamID: req.TeamId,
KeyID: req.KeyId,
PrivateKey: req.PrivateKey,
Scopes: req.Scopes,
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func addSAMLProviderToCommand(req *mgmt_pb.AddSAMLProviderRequest) *command.SAMLProvider {
var nameIDFormat *domain.SAMLNameIDFormat
if req.NameIdFormat != nil {
nameIDFormat = gu.Ptr(idp_grpc.SAMLNameIDFormatToDomain(req.GetNameIdFormat()))
}
return &command.SAMLProvider{
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: req.GetTransientMappingAttributeName(),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func updateSAMLProviderToCommand(req *mgmt_pb.UpdateSAMLProviderRequest) *command.SAMLProvider {
var nameIDFormat *domain.SAMLNameIDFormat
if req.NameIdFormat != nil {
nameIDFormat = gu.Ptr(idp_grpc.SAMLNameIDFormatToDomain(req.GetNameIdFormat()))
}
return &command.SAMLProvider{
Name: req.Name,
Metadata: req.GetMetadataXml(),
MetadataURL: req.GetMetadataUrl(),
Binding: bindingToCommand(req.Binding),
WithSignedRequest: req.WithSignedRequest,
NameIDFormat: nameIDFormat,
TransientMappingAttributeName: req.GetTransientMappingAttributeName(),
IDPOptions: idp_grpc.OptionsToCommand(req.ProviderOptions),
}
}
func bindingToCommand(binding idp_pb.SAMLBinding) string {
switch binding {
case idp_pb.SAMLBinding_SAML_BINDING_UNSPECIFIED:
return ""
case idp_pb.SAMLBinding_SAML_BINDING_POST:
return saml.HTTPPostBinding
case idp_pb.SAMLBinding_SAML_BINDING_REDIRECT:
return saml.HTTPRedirectBinding
case idp_pb.SAMLBinding_SAML_BINDING_ARTIFACT:
return saml.HTTPArtifactBinding
default:
return ""
}
}