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

# Which Problems Are Solved The login v1 allowed to use JWTs as IdP using the JWT IDP. The login V2 uses idp intents for such cases, which were not yet able to handle JWT IdPs. # How the Problems Are Solved - Added handling of JWT IdPs in `StartIdPIntent` and `RetrieveIdPIntent` - The redirect returned by the start, uses the existing `authRequestID` and `userAgentID` parameter names for compatibility reasons. - Added `/idps/jwt` endpoint to handle the proxied (callback) endpoint , which extracts and validates the JWT against the configured endpoint. # Additional Changes None # Additional Context - closes #9758
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.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
|
|
}
|