mirror of
https://github.com/zitadel/zitadel.git
synced 2025-07-13 10:48:32 +00:00

<!-- Please inform yourself about the contribution guidelines on submitting a PR here: https://github.com/zitadel/zitadel/blob/main/CONTRIBUTING.md#submit-a-pull-request-pr. Take note of how PR/commit titles should be written and replace the template texts in the sections below. Don't remove any of the sections. It is important that the commit history clearly shows what is changed and why. Important: By submitting a contribution you agree to the terms from our Licensing Policy as described here: https://github.com/zitadel/zitadel/blob/main/LICENSING.md#community-contributions. --> # Which Problems Are Solved The current user V2 API returns a `[]byte` containing a whole HTML document including the form on `StartIdentifyProviderIntent` for intents based on form post (e.g. SAML POST bindings). This is not usable for most clients as they cannot handle that and render a whole page inside their app. For redirect based intents, the url to which the client needs to redirect is returned. # How the Problems Are Solved - Changed the returned type to a new `FormData` message containing the url and a `fields` map. - internal changes: - Session.GetAuth now returns an `Auth` interfacce and error instead of (content string, redirect bool) - Auth interface has two implementations: `RedirectAuth` and `FormAuth` - All use of the GetAuth function now type switch on the returned auth object - A template has been added to the login UI to execute the form post automatically (as is). # Additional Changes - Some intent integration test did not check the redirect url and were wrongly configured. # Additional Context - relates to zitadel/typescript#410
379 lines
13 KiB
Go
379 lines
13 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
|
|
}
|
|
auth, err := session.GetAuth(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch a := auth.(type) {
|
|
case *idp.RedirectAuth:
|
|
return &user.StartIdentityProviderIntentResponse{
|
|
Details: object.DomainToDetailsPb(details),
|
|
NextStep: &user.StartIdentityProviderIntentResponse_AuthUrl{AuthUrl: a.RedirectURL},
|
|
}, nil
|
|
case *idp.FormAuth:
|
|
return &user.StartIdentityProviderIntentResponse{
|
|
Details: object.DomainToDetailsPb(details),
|
|
NextStep: &user.StartIdentityProviderIntentResponse_FormData{
|
|
FormData: &user.FormData{
|
|
Url: a.URL,
|
|
Fields: a.Fields,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
return nil, zerrors.ThrowInvalidArgumentf(nil, "USERv2-3g2j3", "type oneOf %T in method StartIdentityProviderIntent not implemented", auth)
|
|
}
|
|
|
|
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.InitUser())
|
|
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
|
|
}
|