mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-02 10:18:31 +00:00

# Which Problems Are Solved When retrieving the information of an IdP intent, depending on the IdP type (e.g. Apple), there was issue when mapping the stored (event) information back to the specific IdP type, potentially leading to a panic. # How the Problems Are Solved - Correctly initialize the user struct to map the information to. # Additional Changes none # Additional Context - reported by a support request - needs backport to 3.x and 2.x (cherry picked from commit 1b2fd23e0b6fe21e144df85e449cf45b59bb4ed9)
370 lines
12 KiB
Go
370 lines
12 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
oidc_pkg "github.com/zitadel/oidc/v3/pkg/oidc"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
|
|
"github.com/zitadel/zitadel/internal/command"
|
|
"github.com/zitadel/zitadel/internal/crypto"
|
|
"github.com/zitadel/zitadel/internal/domain"
|
|
"github.com/zitadel/zitadel/internal/idp"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/apple"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/azuread"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/github"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/google"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
|
"github.com/zitadel/zitadel/internal/idp/providers/saml"
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
object_pb "github.com/zitadel/zitadel/pkg/grpc/object/v2"
|
|
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
|
)
|
|
|
|
func (s *Server) StartIdentityProviderIntent(ctx context.Context, req *user.StartIdentityProviderIntentRequest) (_ *user.StartIdentityProviderIntentResponse, err error) {
|
|
switch t := req.GetContent().(type) {
|
|
case *user.StartIdentityProviderIntentRequest_Urls:
|
|
return s.startIDPIntent(ctx, req.GetIdpId(), t.Urls)
|
|
case *user.StartIdentityProviderIntentRequest_Ldap:
|
|
return s.startLDAPIntent(ctx, req.GetIdpId(), t.Ldap)
|
|
default:
|
|
return nil, zerrors.ThrowUnimplementedf(nil, "USERv2-S2g21", "type oneOf %T in method StartIdentityProviderIntent not implemented", t)
|
|
}
|
|
}
|
|
|
|
func (s *Server) startIDPIntent(ctx context.Context, idpID string, urls *user.RedirectURLs) (*user.StartIdentityProviderIntentResponse, error) {
|
|
state, session, err := s.command.AuthFromProvider(ctx, idpID, s.idpCallback(ctx), s.samlRootURL(ctx, idpID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, details, err := s.command.CreateIntent(ctx, state, idpID, urls.GetSuccessUrl(), urls.GetFailureUrl(), authz.GetInstance(ctx).InstanceID(), session.PersistentParameters())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
content, redirect := session.GetAuth(ctx)
|
|
if redirect {
|
|
return &user.StartIdentityProviderIntentResponse{
|
|
Details: object.DomainToDetailsPb(details),
|
|
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: content},
|
|
}, nil
|
|
}
|
|
return &user.StartIdentityProviderIntentResponse{
|
|
Details: object.DomainToDetailsPb(details),
|
|
NextStep: &user.StartIdentityProviderIntentResponse_PostForm{
|
|
PostForm: []byte(content),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) startLDAPIntent(ctx context.Context, idpID string, ldapCredentials *user.LDAPCredentials) (*user.StartIdentityProviderIntentResponse, error) {
|
|
intentWriteModel, details, err := s.command.CreateIntent(ctx, "", idpID, "", "", authz.GetInstance(ctx).InstanceID(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
externalUser, userID, session, err := s.ldapLogin(ctx, intentWriteModel.IDPID, ldapCredentials.GetUsername(), ldapCredentials.GetPassword())
|
|
if err != nil {
|
|
if err := s.command.FailIDPIntent(ctx, intentWriteModel, err.Error()); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, err
|
|
}
|
|
token, err := s.command.SucceedLDAPIDPIntent(ctx, intentWriteModel, externalUser, userID, session)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &user.StartIdentityProviderIntentResponse{
|
|
Details: object.DomainToDetailsPb(details),
|
|
NextStep: &user.StartIdentityProviderIntentResponse_IdpIntent{
|
|
IdpIntent: &user.IDPIntent{
|
|
IdpIntentId: intentWriteModel.AggregateID,
|
|
IdpIntentToken: token,
|
|
UserId: userID,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (s *Server) checkLinkedExternalUser(ctx context.Context, idpID, externalUserID string) (string, error) {
|
|
idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
queries := []query.SearchQuery{
|
|
idQuery, externalIDQuery,
|
|
}
|
|
links, err := s.query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(links.Links) == 1 {
|
|
return links.Links[0].UserID, nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func (s *Server) ldapLogin(ctx context.Context, idpID, username, password string) (idp.User, string, *ldap.Session, error) {
|
|
provider, err := s.command.GetProvider(ctx, idpID, "", "")
|
|
if err != nil {
|
|
return nil, "", nil, err
|
|
}
|
|
ldapProvider, ok := provider.(*ldap.Provider)
|
|
if !ok {
|
|
return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "IDP-9a02j2n2bh", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
|
}
|
|
session := ldapProvider.GetSession(username, password)
|
|
externalUser, err := session.FetchUser(ctx)
|
|
if errors.Is(err, ldap.ErrFailedLogin) || errors.Is(err, ldap.ErrNoSingleUser) {
|
|
return nil, "", nil, zerrors.ThrowInvalidArgument(nil, "COMMAND-nzun2i", "Errors.User.ExternalIDP.LoginFailed")
|
|
}
|
|
if err != nil {
|
|
return nil, "", nil, err
|
|
}
|
|
userID, err := s.checkLinkedExternalUser(ctx, idpID, externalUser.GetID())
|
|
if err != nil {
|
|
return nil, "", nil, err
|
|
}
|
|
return externalUser, userID, session, nil
|
|
}
|
|
|
|
func (s *Server) RetrieveIdentityProviderIntent(ctx context.Context, req *user.RetrieveIdentityProviderIntentRequest) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
|
|
intent, err := s.command.GetIntentWriteModel(ctx, req.GetIdpIntentId(), "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := s.checkIntentToken(req.GetIdpIntentToken(), intent.AggregateID); err != nil {
|
|
return nil, err
|
|
}
|
|
if intent.State != domain.IDPIntentStateSucceeded {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-nme4gszsvx", "Errors.Intent.NotSucceeded")
|
|
}
|
|
if time.Now().After(intent.ExpiresAt()) {
|
|
return nil, zerrors.ThrowPreconditionFailed(nil, "IDP-SAf42", "Errors.Intent.Expired")
|
|
}
|
|
idpIntent, err := idpIntentToIDPIntentPb(intent, s.idpAlg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if idpIntent.UserId == "" {
|
|
provider, err := s.command.GetProvider(ctx, idpIntent.IdpInformation.IdpId, "", "")
|
|
if err != nil && !errors.Is(err, oidc_pkg.ErrDiscoveryFailed) {
|
|
return nil, err
|
|
}
|
|
var idpUser idp.User
|
|
switch p := provider.(type) {
|
|
case *apple.Provider:
|
|
idpUser, err = unmarshalIdpUser(intent.IDPUser, apple.InitUser())
|
|
case *oauth.Provider:
|
|
idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User())
|
|
case *oidc.Provider:
|
|
idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser())
|
|
case *jwt.Provider:
|
|
idpUser, err = unmarshalIdpUser(intent.IDPUser, &jwt.User{})
|
|
case *azuread.Provider:
|
|
idpUser, err = unmarshalRawIdpUser(intent.IDPUser, p.User())
|
|
case *github.Provider:
|
|
idpUser, err = unmarshalIdpUser(intent.IDPUser, &github.User{})
|
|
case *gitlab.Provider:
|
|
idpUser, err = unmarshalIdpUser(intent.IDPUser, oidc.InitUser())
|
|
case *google.Provider:
|
|
idpUser, err = unmarshalIdpUser(intent.IDPUser, google.InitUser())
|
|
case *saml.Provider:
|
|
idpUser, err = unmarshalIdpUser(intent.IDPUser, &saml.UserMapper{})
|
|
case *ldap.Provider:
|
|
idpUser, err = unmarshalIdpUser(intent.IDPUser, &ldap.User{})
|
|
default:
|
|
return nil, zerrors.ThrowInvalidArgument(nil, "IDP-7rPBbls4Zn", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
idpIntent.AddHumanUser = idpUserToAddHumanUser(idpUser, idpIntent.IdpInformation.IdpId)
|
|
}
|
|
return idpIntent, nil
|
|
}
|
|
|
|
type rawUserMapper struct {
|
|
RawInfo map[string]interface{}
|
|
}
|
|
|
|
func unmarshalRawIdpUser(idpUserData []byte, idpUser idp.User) (idp.User, error) {
|
|
userMapper := &rawUserMapper{}
|
|
if err := json.Unmarshal(idpUserData, userMapper); err != nil {
|
|
return nil, err
|
|
}
|
|
idpUserData, err := json.Marshal(userMapper.RawInfo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return unmarshalIdpUser(idpUserData, idpUser)
|
|
}
|
|
|
|
func unmarshalIdpUser(idpUserData []byte, idpUser idp.User) (idp.User, error) {
|
|
if err := json.Unmarshal(idpUserData, idpUser); err != nil {
|
|
return nil, err
|
|
}
|
|
return idpUser, nil
|
|
}
|
|
|
|
func idpIntentToIDPIntentPb(intent *command.IDPIntentWriteModel, alg crypto.EncryptionAlgorithm) (_ *user.RetrieveIdentityProviderIntentResponse, err error) {
|
|
rawInformation := new(structpb.Struct)
|
|
err = rawInformation.UnmarshalJSON(intent.IDPUser)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
information := &user.RetrieveIdentityProviderIntentResponse{
|
|
IdpInformation: &user.IDPInformation{
|
|
IdpId: intent.IDPID,
|
|
UserId: intent.IDPUserID,
|
|
UserName: intent.IDPUserName,
|
|
RawInformation: rawInformation,
|
|
},
|
|
UserId: intent.UserID,
|
|
}
|
|
information.Details = intentToDetailsPb(intent)
|
|
// OAuth / OIDC
|
|
if intent.IDPIDToken != "" || intent.IDPAccessToken != nil {
|
|
information.IdpInformation.Access, err = idpOAuthTokensToPb(intent.IDPIDToken, intent.IDPAccessToken, alg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// LDAP
|
|
if intent.IDPEntryAttributes != nil {
|
|
access, err := IDPEntryAttributesToPb(intent.IDPEntryAttributes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
information.IdpInformation.Access = access
|
|
}
|
|
// SAML
|
|
if intent.Assertion != nil {
|
|
assertion, err := crypto.Decrypt(intent.Assertion, alg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
information.IdpInformation.Access = IDPSAMLResponseToPb(assertion)
|
|
}
|
|
return information, nil
|
|
}
|
|
|
|
func idpOAuthTokensToPb(idpIDToken string, idpAccessToken *crypto.CryptoValue, alg crypto.EncryptionAlgorithm) (_ *user.IDPInformation_Oauth, err error) {
|
|
var idToken *string
|
|
if idpIDToken != "" {
|
|
idToken = &idpIDToken
|
|
}
|
|
var accessToken string
|
|
if idpAccessToken != nil {
|
|
accessToken, err = crypto.DecryptString(idpAccessToken, alg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &user.IDPInformation_Oauth{
|
|
Oauth: &user.IDPOAuthAccessInformation{
|
|
AccessToken: accessToken,
|
|
IdToken: idToken,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func intentToDetailsPb(intent *command.IDPIntentWriteModel) *object_pb.Details {
|
|
return &object_pb.Details{
|
|
Sequence: intent.ProcessedSequence,
|
|
ChangeDate: timestamppb.New(intent.ChangeDate),
|
|
ResourceOwner: intent.ResourceOwner,
|
|
}
|
|
}
|
|
|
|
func IDPEntryAttributesToPb(entryAttributes map[string][]string) (*user.IDPInformation_Ldap, error) {
|
|
values := make(map[string]interface{}, 0)
|
|
for k, v := range entryAttributes {
|
|
intValues := make([]interface{}, len(v))
|
|
for i, value := range v {
|
|
intValues[i] = value
|
|
}
|
|
values[k] = intValues
|
|
}
|
|
attributes, err := structpb.NewStruct(values)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &user.IDPInformation_Ldap{
|
|
Ldap: &user.IDPLDAPAccessInformation{
|
|
Attributes: attributes,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func IDPSAMLResponseToPb(assertion []byte) *user.IDPInformation_Saml {
|
|
return &user.IDPInformation_Saml{
|
|
Saml: &user.IDPSAMLAccessInformation{
|
|
Assertion: assertion,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *Server) checkIntentToken(token string, intentID string) error {
|
|
return crypto.CheckToken(s.idpAlg, token, intentID)
|
|
}
|
|
|
|
func idpUserToAddHumanUser(idpUser idp.User, idpID string) *user.AddHumanUserRequest {
|
|
addHumanUser := &user.AddHumanUserRequest{
|
|
Profile: &user.SetHumanProfile{
|
|
GivenName: idpUser.GetFirstName(),
|
|
FamilyName: idpUser.GetLastName(),
|
|
},
|
|
Email: &user.SetHumanEmail{
|
|
Email: string(idpUser.GetEmail()),
|
|
Verification: &user.SetHumanEmail_SendCode{},
|
|
},
|
|
Metadata: make([]*user.SetMetadataEntry, 0),
|
|
IdpLinks: []*user.IDPLink{
|
|
{
|
|
IdpId: idpID,
|
|
UserId: idpUser.GetID(),
|
|
UserName: idpUser.GetPreferredUsername(),
|
|
},
|
|
},
|
|
}
|
|
if username := idpUser.GetPreferredUsername(); username != "" {
|
|
addHumanUser.Username = &username
|
|
}
|
|
if nickName := idpUser.GetNickname(); nickName != "" {
|
|
addHumanUser.Profile.NickName = &nickName
|
|
}
|
|
if displayName := idpUser.GetDisplayName(); displayName != "" {
|
|
addHumanUser.Profile.DisplayName = &displayName
|
|
}
|
|
if lang := idpUser.GetPreferredLanguage().String(); lang != "" {
|
|
addHumanUser.Profile.PreferredLanguage = &lang
|
|
}
|
|
if isEmailVerified := idpUser.IsEmailVerified(); isEmailVerified {
|
|
addHumanUser.Email.Verification = &user.SetHumanEmail_IsVerified{IsVerified: isEmailVerified}
|
|
}
|
|
if phone := idpUser.GetPhone(); phone != "" {
|
|
addHumanUser.Phone = &user.SetHumanPhone{
|
|
Phone: string(phone),
|
|
Verification: &user.SetHumanPhone_SendCode{},
|
|
}
|
|
if isPhoneVerified := idpUser.IsPhoneVerified(); isPhoneVerified {
|
|
addHumanUser.Phone.Verification = &user.SetHumanPhone_IsVerified{IsVerified: isPhoneVerified}
|
|
}
|
|
}
|
|
return addHumanUser
|
|
}
|