mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-02 02:08:19 +00:00
feat(login): use new IDP templates (#5315)
The login uses the new template based IDPs with backwards compatibility for old IDPs
This commit is contained in:
parent
abacb6c5aa
commit
48f9815b7c
@ -152,7 +152,11 @@ func (s *Server) UpdateIDPJWTConfig(ctx context.Context, req *admin_pb.UpdateIDP
|
||||
}
|
||||
|
||||
func (s *Server) GetProviderByID(ctx context.Context, req *admin_pb.GetProviderByIDRequest) (*admin_pb.GetProviderByIDResponse, error) {
|
||||
idp, err := s.query.IDPTemplateByIDAndResourceOwner(ctx, true, req.Id, authz.GetInstance(ctx).InstanceID(), false)
|
||||
instanceIDQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, instanceIDQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -84,13 +84,11 @@ func IDPUserLinkToPb(link *query.IDPUserLink) *idp_pb.IDPUserLink {
|
||||
}
|
||||
}
|
||||
|
||||
func IDPTypeToPb(idpType domain.IDPConfigType) idp_pb.IDPType {
|
||||
func IDPTypeToPb(idpType domain.IDPType) idp_pb.IDPType {
|
||||
switch idpType {
|
||||
case domain.IDPConfigTypeOIDC:
|
||||
case domain.IDPTypeOIDC:
|
||||
return idp_pb.IDPType_IDP_TYPE_OIDC
|
||||
case domain.IDPConfigTypeSAML:
|
||||
return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED
|
||||
case domain.IDPConfigTypeJWT:
|
||||
case domain.IDPTypeJWT:
|
||||
return idp_pb.IDPType_IDP_TYPE_JWT
|
||||
default:
|
||||
return idp_pb.IDPType_IDP_TYPE_UNSPECIFIED
|
||||
|
@ -144,7 +144,11 @@ func (s *Server) UpdateOrgIDPJWTConfig(ctx context.Context, req *mgmt_pb.UpdateO
|
||||
}
|
||||
|
||||
func (s *Server) GetProviderByID(ctx context.Context, req *mgmt_pb.GetProviderByIDRequest) (*mgmt_pb.GetProviderByIDResponse, error) {
|
||||
idp, err := s.query.IDPTemplateByIDAndResourceOwner(ctx, true, req.Id, authz.GetCtxData(ctx).OrgID, false)
|
||||
orgIDQuery, err := query.NewIDPTemplateResourceOwnerSearchQuery(authz.GetCtxData(ctx).OrgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idp, err := s.query.IDPTemplateByID(ctx, true, req.Id, false, orgIDQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/actions/object"
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
)
|
||||
|
||||
func (l *Login) runPostExternalAuthenticationActions(
|
||||
@ -21,18 +20,13 @@ func (l *Login) runPostExternalAuthenticationActions(
|
||||
tokens *oidc.Tokens,
|
||||
authRequest *domain.AuthRequest,
|
||||
httpRequest *http.Request,
|
||||
config *iam_model.IDPConfigView,
|
||||
authenticationError error,
|
||||
) (*domain.ExternalUser, error) {
|
||||
ctx := httpRequest.Context()
|
||||
|
||||
resourceOwner := authRequest.RequestedOrgID
|
||||
if resourceOwner == "" {
|
||||
resourceOwner = config.AggregateID
|
||||
}
|
||||
instance := authz.GetInstance(ctx)
|
||||
if resourceOwner == instance.InstanceID() {
|
||||
resourceOwner = instance.DefaultOrganisationID()
|
||||
resourceOwner = authz.GetInstance(ctx).DefaultOrganisationID()
|
||||
}
|
||||
triggerActions, err := l.query.GetActiveActionsByFlowAndTriggerType(ctx, domain.FlowTypeExternalAuthentication, domain.TriggerTypePostAuthentication, resourceOwner, false)
|
||||
if err != nil {
|
||||
|
@ -1,538 +0,0 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
const (
|
||||
queryIDPConfigID = "idpConfigID"
|
||||
tmplExternalNotFoundOption = "externalnotfoundoption"
|
||||
)
|
||||
|
||||
type externalIDPData struct {
|
||||
IDPConfigID string `schema:"idpConfigID"`
|
||||
}
|
||||
|
||||
type externalIDPCallbackData struct {
|
||||
State string `schema:"state"`
|
||||
Code string `schema:"code"`
|
||||
}
|
||||
|
||||
type externalNotFoundOptionFormData struct {
|
||||
externalRegisterFormData
|
||||
Link bool `schema:"linkbutton"`
|
||||
AutoRegister bool `schema:"autoregisterbutton"`
|
||||
ResetLinking bool `schema:"resetlinking"`
|
||||
TermsConfirm bool `schema:"terms-confirm"`
|
||||
}
|
||||
|
||||
type externalNotFoundOptionData struct {
|
||||
baseData
|
||||
externalNotFoundOptionFormData
|
||||
ExternalIDPID string
|
||||
ExternalIDPUserID string
|
||||
ExternalIDPUserDisplayName string
|
||||
ShowUsername bool
|
||||
ShowUsernameSuffix bool
|
||||
OrgRegister bool
|
||||
ExternalEmail string
|
||||
ExternalEmailVerified bool
|
||||
ExternalPhone string
|
||||
ExternalPhoneVerified bool
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalLoginStep(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPConfigID string) {
|
||||
for _, idp := range authReq.AllowedExternalIDPs {
|
||||
if idp.IDPConfigID == selectedIDPConfigID {
|
||||
l.handleIDP(w, r, authReq, selectedIDPConfigID)
|
||||
return
|
||||
}
|
||||
}
|
||||
l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "VIEW-Fsj7f", "Errors.User.ExternalIDP.NotAllowed"))
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalIDPData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if authReq == nil {
|
||||
l.defaultRedirect(w, r)
|
||||
return
|
||||
}
|
||||
l.handleIDP(w, r, authReq, data.IDPConfigID)
|
||||
}
|
||||
|
||||
func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPConfigID string) {
|
||||
idpConfig, err := l.getIDPConfigByID(r, selectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if !idpConfig.IsOIDC {
|
||||
l.handleJWTAuthorize(w, r, authReq, idpConfig)
|
||||
return
|
||||
}
|
||||
l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalLoginCallback)
|
||||
}
|
||||
|
||||
func (l *Login) handleOIDCAuthorize(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) {
|
||||
provider, err := l.getRPConfig(r.Context(), idpConfig, callbackEndpoint)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, rp.AuthURL(authReq.ID, provider, rp.WithPrompt(oidc.PromptSelectAccount)), http.StatusFound)
|
||||
}
|
||||
|
||||
func (l *Login) handleJWTAuthorize(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView) {
|
||||
redirect, err := url.Parse(idpConfig.JWTEndpoint)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
q := redirect.Query()
|
||||
q.Set(QueryAuthRequestID, authReq.ID)
|
||||
userAgentID, ok := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
if !ok {
|
||||
l.renderLogin(w, r, authReq, errors.ThrowPreconditionFailed(nil, "LOGIN-dsgg3", "Errors.AuthRequest.UserAgentNotFound"))
|
||||
return
|
||||
}
|
||||
nonce, err := l.idpConfigAlg.Encrypt([]byte(userAgentID))
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
q.Set(queryUserAgentID, base64.RawURLEncoding.EncodeToString(nonce))
|
||||
redirect.RawQuery = q.Encode()
|
||||
http.Redirect(w, r, redirect.String(), http.StatusFound)
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalIDPCallbackData)
|
||||
err := l.getParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if idpConfig.IsOIDC {
|
||||
provider, err := l.getRPConfig(r.Context(), idpConfig, EndpointExternalLoginCallback)
|
||||
if err != nil {
|
||||
emtpyTokens := &oidc.Tokens{Token: &oauth2.Token{}}
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emtpyTokens, authReq, r, idpConfig, err); actionErr != nil {
|
||||
logging.WithError(err).Error("both external user authentication and action post authentication failed")
|
||||
}
|
||||
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
tokens, err := rp.CodeExchange(r.Context(), data.Code, provider)
|
||||
if err != nil {
|
||||
emtpyTokens := &oidc.Tokens{Token: &oauth2.Token{}}
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emtpyTokens, authReq, r, idpConfig, err); actionErr != nil {
|
||||
logging.WithError(err).Error("both external user authentication and action post authentication failed")
|
||||
}
|
||||
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.handleExternalUserAuthenticated(w, r, authReq, idpConfig, userAgentID, tokens)
|
||||
return
|
||||
}
|
||||
|
||||
err = errors.ThrowPreconditionFailed(nil, "RP-asff2", "Errors.ExternalIDP.IDPTypeNotImplemented")
|
||||
emtpyTokens := &oidc.Tokens{Token: &oauth2.Token{}}
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emtpyTokens, authReq, r, idpConfig, err); actionErr != nil {
|
||||
logging.WithError(err).Error("both external user authentication and action post authentication failed")
|
||||
}
|
||||
|
||||
l.renderError(w, r, authReq, err)
|
||||
}
|
||||
|
||||
func (l *Login) getRPConfig(ctx context.Context, idpConfig *iam_model.IDPConfigView, callbackEndpoint string) (rp.RelyingParty, error) {
|
||||
oidcClientSecret, err := crypto.DecryptString(idpConfig.OIDCClientSecret, l.idpConfigAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if idpConfig.OIDCIssuer != "" {
|
||||
return rp.NewRelyingPartyOIDC(idpConfig.OIDCIssuer, idpConfig.OIDCClientID, oidcClientSecret, l.baseURL(ctx)+callbackEndpoint, idpConfig.OIDCScopes, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second)))
|
||||
}
|
||||
if idpConfig.OAuthAuthorizationEndpoint == "" || idpConfig.OAuthTokenEndpoint == "" {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "RP-4n0fs", "Errors.IdentityProvider.InvalidConfig")
|
||||
}
|
||||
oauth2Config := &oauth2.Config{
|
||||
ClientID: idpConfig.OIDCClientID,
|
||||
ClientSecret: oidcClientSecret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: idpConfig.OAuthAuthorizationEndpoint,
|
||||
TokenURL: idpConfig.OAuthTokenEndpoint,
|
||||
},
|
||||
RedirectURL: l.baseURL(ctx) + callbackEndpoint,
|
||||
Scopes: idpConfig.OIDCScopes,
|
||||
}
|
||||
return rp.NewRelyingPartyOAuth(oauth2Config, rp.WithVerifierOpts(rp.WithIssuedAtOffset(3*time.Second)))
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalUserAuthenticated(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) {
|
||||
externalUser := l.mapTokenToLoginUser(tokens, idpConfig)
|
||||
externalUser, err := l.runPostExternalAuthenticationActions(externalUser, tokens, authReq, r, idpConfig, nil)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, userAgentID, externalUser, domain.BrowserInfoFromRequest(r))
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
err = nil
|
||||
}
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
|
||||
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
|
||||
orgIAMPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
human, idpLinking, _ := l.mapExternalUserToLoginUser(orgIAMPolicy, externalUser, idpConfig)
|
||||
if !idpConfig.AutoRegister {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLinking, err)
|
||||
return
|
||||
}
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLinking, err)
|
||||
return
|
||||
}
|
||||
l.handleAutoRegister(w, r, authReq, false)
|
||||
return
|
||||
}
|
||||
if len(externalUser.Metadatas) > 0 {
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, userAgentID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, externalUser.Metadatas...)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
l.renderNextStep(w, r, authReq)
|
||||
}
|
||||
|
||||
func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.DomainPolicy, human *domain.Human, externalIDP *domain.UserIDPLink, err error) {
|
||||
var errID, errMessage string
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
if orgIAMPolicy == nil {
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
|
||||
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
|
||||
orgIAMPolicy, err = l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if human == nil || externalIDP == nil {
|
||||
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1]
|
||||
human, externalIDP, _ = l.mapExternalUserToLoginUser(orgIAMPolicy, linkingUser, idpConfig)
|
||||
}
|
||||
|
||||
var resourceOwner string
|
||||
if authReq != nil {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
if resourceOwner == "" {
|
||||
resourceOwner = authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
}
|
||||
labelPolicy, err := l.getLabelPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := externalNotFoundOptionData{
|
||||
baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage),
|
||||
externalNotFoundOptionFormData: externalNotFoundOptionFormData{
|
||||
externalRegisterFormData: externalRegisterFormData{
|
||||
Email: human.EmailAddress,
|
||||
Username: human.Username,
|
||||
Firstname: human.FirstName,
|
||||
Lastname: human.LastName,
|
||||
Nickname: human.NickName,
|
||||
Language: human.PreferredLanguage.String(),
|
||||
},
|
||||
},
|
||||
ExternalIDPID: externalIDP.IDPConfigID,
|
||||
ExternalIDPUserID: externalIDP.ExternalUserID,
|
||||
ExternalIDPUserDisplayName: externalIDP.DisplayName,
|
||||
ExternalEmail: human.EmailAddress,
|
||||
ExternalEmailVerified: human.IsEmailVerified,
|
||||
ShowUsername: orgIAMPolicy.UserLoginMustBeDomain,
|
||||
ShowUsernameSuffix: !labelPolicy.HideLoginNameSuffix,
|
||||
OrgRegister: orgIAMPolicy.UserLoginMustBeDomain,
|
||||
}
|
||||
if human.Phone != nil {
|
||||
data.Phone = human.PhoneNumber
|
||||
data.ExternalPhone = human.PhoneNumber
|
||||
data.ExternalPhoneVerified = human.IsPhoneVerified
|
||||
}
|
||||
funcs := map[string]interface{}{
|
||||
"selectedLanguage": func(l string) bool {
|
||||
return data.Language == l
|
||||
},
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplExternalNotFoundOption], data, funcs)
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalNotFoundOptionFormData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
if data.Link {
|
||||
l.renderLogin(w, r, authReq, nil)
|
||||
return
|
||||
} else if data.ResetLinking {
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err = l.authRepo.ResetLinkingUsers(r.Context(), authReq.ID, userAgentID)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
}
|
||||
l.handleLogin(w, r)
|
||||
return
|
||||
}
|
||||
l.handleAutoRegister(w, r, authReq, true)
|
||||
}
|
||||
|
||||
func (l *Login) handleAutoRegister(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, userNotFound bool) {
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
|
||||
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
|
||||
orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, nil, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
if len(authReq.LinkingUsers) == 0 {
|
||||
l.renderError(w, r, authReq, errors.ThrowPreconditionFailed(nil, "LOGIN-asfg3", "Errors.ExternalIDP.NoExternalUserData"))
|
||||
return
|
||||
}
|
||||
|
||||
linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1]
|
||||
if userNotFound {
|
||||
data := new(externalNotFoundOptionFormData)
|
||||
err := l.getParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
linkingUser = l.mapExternalNotFoundOptionFormDataToLoginUser(data)
|
||||
}
|
||||
|
||||
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, linkingUser, idpConfig)
|
||||
|
||||
user, metadata, err = l.runPreCreationActions(authReq, r, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, nil, nil, err)
|
||||
return
|
||||
}
|
||||
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, userAgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r))
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, user, externalIDP, err)
|
||||
return
|
||||
}
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
userGrants, err := l.runPostCreationActions(authReq.UserID, authReq, r, resourceOwner, domain.FlowTypeExternalAuthentication)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderNextStep(w, r, authReq)
|
||||
}
|
||||
|
||||
func (l *Login) mapExternalNotFoundOptionFormDataToLoginUser(formData *externalNotFoundOptionFormData) *domain.ExternalUser {
|
||||
isEmailVerified := formData.ExternalEmailVerified && formData.Email == formData.ExternalEmail
|
||||
isPhoneVerified := formData.ExternalPhoneVerified && formData.Phone == formData.ExternalPhone
|
||||
return &domain.ExternalUser{
|
||||
IDPConfigID: formData.ExternalIDPConfigID,
|
||||
ExternalUserID: formData.ExternalIDPExtUserID,
|
||||
PreferredUsername: formData.Username,
|
||||
DisplayName: formData.Email,
|
||||
FirstName: formData.Firstname,
|
||||
LastName: formData.Lastname,
|
||||
NickName: formData.Nickname,
|
||||
Email: formData.Email,
|
||||
IsEmailVerified: isEmailVerified,
|
||||
Phone: formData.Phone,
|
||||
IsPhoneVerified: isPhoneVerified,
|
||||
PreferredLanguage: language.Make(formData.Language),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Login) mapTokenToLoginUser(tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) *domain.ExternalUser {
|
||||
displayName := tokens.IDTokenClaims.GetPreferredUsername()
|
||||
if displayName == "" && tokens.IDTokenClaims.GetEmail() != "" {
|
||||
displayName = tokens.IDTokenClaims.GetEmail()
|
||||
}
|
||||
switch idpConfig.OIDCIDPDisplayNameMapping {
|
||||
case iam_model.OIDCMappingFieldEmail:
|
||||
if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" {
|
||||
displayName = tokens.IDTokenClaims.GetEmail()
|
||||
}
|
||||
}
|
||||
|
||||
externalUser := &domain.ExternalUser{
|
||||
IDPConfigID: idpConfig.IDPConfigID,
|
||||
ExternalUserID: tokens.IDTokenClaims.GetSubject(),
|
||||
PreferredUsername: tokens.IDTokenClaims.GetPreferredUsername(),
|
||||
DisplayName: displayName,
|
||||
FirstName: tokens.IDTokenClaims.GetGivenName(),
|
||||
LastName: tokens.IDTokenClaims.GetFamilyName(),
|
||||
NickName: tokens.IDTokenClaims.GetNickname(),
|
||||
Email: tokens.IDTokenClaims.GetEmail(),
|
||||
IsEmailVerified: tokens.IDTokenClaims.IsEmailVerified(),
|
||||
PreferredLanguage: tokens.IDTokenClaims.GetLocale(),
|
||||
}
|
||||
|
||||
if tokens.IDTokenClaims.GetPhoneNumber() != "" {
|
||||
externalUser.Phone = tokens.IDTokenClaims.GetPhoneNumber()
|
||||
externalUser.IsPhoneVerified = tokens.IDTokenClaims.IsPhoneNumberVerified()
|
||||
}
|
||||
return externalUser
|
||||
}
|
||||
func (l *Login) mapExternalUserToLoginUser(orgIamPolicy *query.DomainPolicy, linkingUser *domain.ExternalUser, idpConfig *iam_model.IDPConfigView) (*domain.Human, *domain.UserIDPLink, []*domain.Metadata) {
|
||||
username := linkingUser.PreferredUsername
|
||||
switch idpConfig.OIDCUsernameMapping {
|
||||
case iam_model.OIDCMappingFieldEmail:
|
||||
if linkingUser.IsEmailVerified && linkingUser.Email != "" && username == "" {
|
||||
username = linkingUser.Email
|
||||
}
|
||||
}
|
||||
if username == "" {
|
||||
username = linkingUser.Email
|
||||
}
|
||||
|
||||
if orgIamPolicy.UserLoginMustBeDomain {
|
||||
index := strings.LastIndex(username, "@")
|
||||
if index > 1 {
|
||||
username = username[:index]
|
||||
}
|
||||
}
|
||||
|
||||
human := &domain.Human{
|
||||
Username: username,
|
||||
Profile: &domain.Profile{
|
||||
FirstName: linkingUser.FirstName,
|
||||
LastName: linkingUser.LastName,
|
||||
PreferredLanguage: linkingUser.PreferredLanguage,
|
||||
NickName: linkingUser.NickName,
|
||||
},
|
||||
Email: &domain.Email{
|
||||
EmailAddress: linkingUser.Email,
|
||||
IsEmailVerified: linkingUser.IsEmailVerified,
|
||||
},
|
||||
}
|
||||
if linkingUser.Phone != "" {
|
||||
human.Phone = &domain.Phone{
|
||||
PhoneNumber: linkingUser.Phone,
|
||||
IsPhoneVerified: linkingUser.IsPhoneVerified,
|
||||
}
|
||||
}
|
||||
|
||||
displayName := linkingUser.PreferredUsername
|
||||
switch idpConfig.OIDCIDPDisplayNameMapping {
|
||||
case iam_model.OIDCMappingFieldEmail:
|
||||
if linkingUser.IsEmailVerified && linkingUser.Email != "" && displayName == "" {
|
||||
displayName = linkingUser.Email
|
||||
}
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = linkingUser.Email
|
||||
}
|
||||
|
||||
externalIDP := &domain.UserIDPLink{
|
||||
IDPConfigID: idpConfig.IDPConfigID,
|
||||
ExternalUserID: linkingUser.ExternalUserID,
|
||||
DisplayName: displayName,
|
||||
}
|
||||
return human, externalIDP, linkingUser.Metadatas
|
||||
}
|
707
internal/api/ui/login/external_provider_handler.go
Normal file
707
internal/api/ui/login/external_provider_handler.go
Normal file
@ -0,0 +1,707 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/google"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
const (
|
||||
queryIDPConfigID = "idpConfigID"
|
||||
tmplExternalNotFoundOption = "externalnotfoundoption"
|
||||
)
|
||||
|
||||
type externalIDPData struct {
|
||||
IDPConfigID string `schema:"idpConfigID"`
|
||||
}
|
||||
|
||||
type externalIDPCallbackData struct {
|
||||
State string `schema:"state"`
|
||||
Code string `schema:"code"`
|
||||
}
|
||||
|
||||
type externalNotFoundOptionFormData struct {
|
||||
externalRegisterFormData
|
||||
Link bool `schema:"linkbutton"`
|
||||
AutoRegister bool `schema:"autoregisterbutton"`
|
||||
ResetLinking bool `schema:"resetlinking"`
|
||||
TermsConfirm bool `schema:"terms-confirm"`
|
||||
}
|
||||
|
||||
type externalNotFoundOptionData struct {
|
||||
baseData
|
||||
externalNotFoundOptionFormData
|
||||
IsLinkingAllowed bool
|
||||
IsCreationAllowed bool
|
||||
ExternalIDPID string
|
||||
ExternalIDPUserID string
|
||||
ExternalIDPUserDisplayName string
|
||||
ShowUsername bool
|
||||
ShowUsernameSuffix bool
|
||||
OrgRegister bool
|
||||
ExternalEmail string
|
||||
ExternalEmailVerified bool
|
||||
ExternalPhone string
|
||||
ExternalPhoneVerified bool
|
||||
}
|
||||
|
||||
type externalRegisterFormData struct {
|
||||
ExternalIDPConfigID string `schema:"external-idp-config-id"`
|
||||
ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"`
|
||||
ExternalIDPDisplayName string `schema:"external-idp-display-name"`
|
||||
ExternalEmail string `schema:"external-email"`
|
||||
ExternalEmailVerified bool `schema:"external-email-verified"`
|
||||
Email string `schema:"email"`
|
||||
Username string `schema:"username"`
|
||||
Firstname string `schema:"firstname"`
|
||||
Lastname string `schema:"lastname"`
|
||||
Nickname string `schema:"nickname"`
|
||||
ExternalPhone string `schema:"external-phone"`
|
||||
ExternalPhoneVerified bool `schema:"external-phone-verified"`
|
||||
Phone string `schema:"phone"`
|
||||
Language string `schema:"language"`
|
||||
TermsConfirm bool `schema:"terms-confirm"`
|
||||
}
|
||||
|
||||
// handleExternalLoginStep is called as nextStep
|
||||
func (l *Login) handleExternalLoginStep(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, selectedIDPID string) {
|
||||
for _, idp := range authReq.AllowedExternalIDPs {
|
||||
if idp.IDPConfigID == selectedIDPID {
|
||||
l.handleIDP(w, r, authReq, selectedIDPID)
|
||||
return
|
||||
}
|
||||
}
|
||||
l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "VIEW-Fsj7f", "Errors.User.ExternalIDP.NotAllowed"))
|
||||
}
|
||||
|
||||
// handleExternalLogin is called when a user selects the idp on the login page
|
||||
func (l *Login) handleExternalLogin(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalIDPData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if authReq == nil {
|
||||
l.defaultRedirect(w, r)
|
||||
return
|
||||
}
|
||||
l.handleIDP(w, r, authReq, data.IDPConfigID)
|
||||
}
|
||||
|
||||
// handleExternalRegister is called when a user selects the idp on the register options page
|
||||
func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalIDPData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.handleIDP(w, r, authReq, data.IDPConfigID)
|
||||
}
|
||||
|
||||
// handleIDP start the authentication of the selected IDP
|
||||
// it will redirect to the IDPs auth page
|
||||
func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, id string) {
|
||||
identityProvider, err := l.getIDPByID(r, id)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, identityProvider.ID, userAgentID)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
var provider idp.Provider
|
||||
switch identityProvider.Type {
|
||||
case domain.IDPTypeOIDC:
|
||||
provider, err = l.oidcProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeJWT:
|
||||
provider, err = l.jwtProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeGoogle:
|
||||
provider, err = l.googleProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeOAuth,
|
||||
domain.IDPTypeLDAP,
|
||||
domain.IDPTypeAzureAD,
|
||||
domain.IDPTypeGitHub,
|
||||
domain.IDPTypeGitHubEE,
|
||||
domain.IDPTypeGitLab,
|
||||
domain.IDPTypeGitLabSelfHosted,
|
||||
domain.IDPTypeUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "LOGIN-AShek", "Errors.ExternalIDP.IDPTypeNotImplemented"))
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
session, err := provider.BeginAuth(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, session.GetAuthURL(), http.StatusFound)
|
||||
}
|
||||
|
||||
// handleExternalLoginCallback handles the callback from a IDP
|
||||
// and tries to extract the user with the provided data
|
||||
func (l *Login) handleExternalLoginCallback(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalIDPCallbackData)
|
||||
err := l.getParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, nil, err)
|
||||
return
|
||||
}
|
||||
identityProvider, err := l.getIDPByID(r, authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, nil, err)
|
||||
return
|
||||
}
|
||||
var provider idp.Provider
|
||||
var session idp.Session
|
||||
switch identityProvider.Type {
|
||||
case domain.IDPTypeOIDC:
|
||||
provider, err = l.oidcProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, nil, err)
|
||||
return
|
||||
}
|
||||
session = &openid.Session{Provider: provider.(*openid.Provider), Code: data.Code}
|
||||
case domain.IDPTypeGoogle:
|
||||
provider, err = l.googleProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, nil, err)
|
||||
return
|
||||
}
|
||||
session = &openid.Session{Provider: provider.(*google.Provider).Provider, Code: data.Code}
|
||||
case domain.IDPTypeJWT,
|
||||
domain.IDPTypeOAuth,
|
||||
domain.IDPTypeLDAP,
|
||||
domain.IDPTypeAzureAD,
|
||||
domain.IDPTypeGitHub,
|
||||
domain.IDPTypeGitHubEE,
|
||||
domain.IDPTypeGitLab,
|
||||
domain.IDPTypeGitLabSelfHosted,
|
||||
domain.IDPTypeUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "LOGIN-SFefg", "Errors.ExternalIDP.IDPTypeNotImplemented"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := session.FetchUser(r.Context())
|
||||
if err != nil {
|
||||
l.externalAuthFailed(w, r, authReq, tokens(session), err)
|
||||
return
|
||||
}
|
||||
l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.renderNextStep)
|
||||
}
|
||||
|
||||
// handleExternalUserAuthenticated maps the IDP user, checks for a corresponding externalID
|
||||
func (l *Login) handleExternalUserAuthenticated(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
authReq *domain.AuthRequest,
|
||||
provider *query.IDPTemplate,
|
||||
session idp.Session,
|
||||
user idp.User,
|
||||
callback func(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest),
|
||||
) {
|
||||
externalUser := mapIDPUserToExternalUser(user, provider.ID)
|
||||
externalUser, err := l.runPostExternalAuthenticationActions(externalUser, tokens(session), authReq, r, nil)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r))
|
||||
if err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.externalUserNotExisting(w, r, authReq, provider, externalUser)
|
||||
return
|
||||
}
|
||||
if provider.IsAutoUpdate || len(externalUser.Metadatas) > 0 {
|
||||
// read current auth request state (incl. authorized user)
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if provider.IsAutoUpdate {
|
||||
err = l.updateExternalUser(r.Context(), authReq, externalUser)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(externalUser.Metadatas) > 0 {
|
||||
_, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, externalUser.Metadatas...)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
callback(w, r, authReq)
|
||||
}
|
||||
|
||||
// externalUserNotExisting is called if an externalAuthentication couldn't find a corresponding externalID
|
||||
// possible solutions are:
|
||||
//
|
||||
// * auto creation
|
||||
// * external not found overview:
|
||||
// - creation by user
|
||||
// - linking to existing user
|
||||
func (l *Login) externalUserNotExisting(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, provider *query.IDPTemplate, externalUser *domain.ExternalUser) {
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
|
||||
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
|
||||
orgIAMPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
human, idpLink, _ := mapExternalUserToLoginUser(externalUser, orgIAMPolicy.UserLoginMustBeDomain)
|
||||
// if auto creation or creation itself is disabled, send the user to the notFoundOption
|
||||
if !provider.IsCreationAllowed || !provider.IsAutoCreation {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, err)
|
||||
return
|
||||
}
|
||||
|
||||
// reload auth request, to ensure current state (checked external login)
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIAMPolicy, human, idpLink, err)
|
||||
return
|
||||
}
|
||||
l.autoCreateExternalUser(w, r, authReq)
|
||||
}
|
||||
|
||||
// autoCreateExternalUser takes the externalUser and creates it automatically (without user interaction)
|
||||
func (l *Login) autoCreateExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||
if len(authReq.LinkingUsers) == 0 {
|
||||
l.renderError(w, r, authReq, errors.ThrowPreconditionFailed(nil, "LOGIN-asfg3", "Errors.ExternalIDP.NoExternalUserData"))
|
||||
return
|
||||
}
|
||||
|
||||
// TODO (LS): how do we get multiple and why do we use the last of them (taken as is)?
|
||||
linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1]
|
||||
|
||||
l.registerExternalUser(w, r, authReq, linkingUser)
|
||||
}
|
||||
|
||||
// renderExternalNotFoundOption renders a page, where the user is able to edit the IDP data,
|
||||
// create a new externalUser of link to existing on (based on the IDP template)
|
||||
func (l *Login) renderExternalNotFoundOption(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.DomainPolicy, human *domain.Human, idpLink *domain.UserIDPLink, err error) {
|
||||
var errID, errMessage string
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
if orgIAMPolicy == nil {
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
|
||||
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
|
||||
orgIAMPolicy, err = l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if human == nil || idpLink == nil {
|
||||
|
||||
// TODO (LS): how do we get multiple and why do we use the last of them (taken as is)?
|
||||
linkingUser := authReq.LinkingUsers[len(authReq.LinkingUsers)-1]
|
||||
human, idpLink, _ = mapExternalUserToLoginUser(linkingUser, orgIAMPolicy.UserLoginMustBeDomain)
|
||||
}
|
||||
|
||||
var resourceOwner string
|
||||
if authReq != nil {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
if resourceOwner == "" {
|
||||
resourceOwner = authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
}
|
||||
labelPolicy, err := l.getLabelPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
idpTemplate, err := l.getIDPByID(r, idpLink.IDPConfigID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := externalNotFoundOptionData{
|
||||
baseData: l.getBaseData(r, authReq, "ExternalNotFound.Title", "ExternalNotFound.Description", errID, errMessage),
|
||||
externalNotFoundOptionFormData: externalNotFoundOptionFormData{
|
||||
externalRegisterFormData: externalRegisterFormData{
|
||||
Email: human.EmailAddress,
|
||||
Username: human.Username,
|
||||
Firstname: human.FirstName,
|
||||
Lastname: human.LastName,
|
||||
Nickname: human.NickName,
|
||||
Language: human.PreferredLanguage.String(),
|
||||
},
|
||||
},
|
||||
IsLinkingAllowed: idpTemplate.IsLinkingAllowed,
|
||||
IsCreationAllowed: idpTemplate.IsCreationAllowed,
|
||||
ExternalIDPID: idpLink.IDPConfigID,
|
||||
ExternalIDPUserID: idpLink.ExternalUserID,
|
||||
ExternalIDPUserDisplayName: idpLink.DisplayName,
|
||||
ExternalEmail: human.EmailAddress,
|
||||
ExternalEmailVerified: human.IsEmailVerified,
|
||||
ShowUsername: orgIAMPolicy.UserLoginMustBeDomain,
|
||||
ShowUsernameSuffix: !labelPolicy.HideLoginNameSuffix,
|
||||
OrgRegister: orgIAMPolicy.UserLoginMustBeDomain,
|
||||
}
|
||||
if human.Phone != nil {
|
||||
data.Phone = human.PhoneNumber
|
||||
data.ExternalPhone = human.PhoneNumber
|
||||
data.ExternalPhoneVerified = human.IsPhoneVerified
|
||||
}
|
||||
funcs := map[string]interface{}{
|
||||
"selectedLanguage": func(l string) bool {
|
||||
return data.Language == l
|
||||
},
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplExternalNotFoundOption], data, funcs)
|
||||
}
|
||||
|
||||
// handleExternalNotFoundOptionCheck takes the data from the submitted externalNotFound page
|
||||
// and either links or creates an externalUser
|
||||
func (l *Login) handleExternalNotFoundOptionCheck(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalNotFoundOptionFormData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
idpTemplate, err := l.getIDPByID(r, authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
// if the user click on the cancel button / back icon
|
||||
if data.ResetLinking {
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err = l.authRepo.ResetLinkingUsers(r.Context(), authReq.ID, userAgentID)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
}
|
||||
l.handleLogin(w, r)
|
||||
return
|
||||
}
|
||||
// if the user selects the linking button
|
||||
if data.Link {
|
||||
if !idpTemplate.IsLinkingAllowed {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, errors.ThrowPreconditionFailed(nil, "LOGIN-AS3ff", "Errors.ExternalIDP.LinkingNotAllowed"))
|
||||
return
|
||||
}
|
||||
l.renderLogin(w, r, authReq, nil)
|
||||
return
|
||||
}
|
||||
// if the user selects the creation button
|
||||
if !idpTemplate.IsCreationAllowed {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, errors.ThrowPreconditionFailed(nil, "LOGIN-dsfd3", "Errors.ExternalIDP.CreationNotAllowed"))
|
||||
return
|
||||
}
|
||||
linkingUser := mapExternalNotFoundOptionFormDataToLoginUser(data)
|
||||
l.registerExternalUser(w, r, authReq, linkingUser)
|
||||
}
|
||||
|
||||
// registerExternalUser creates an externalUser with the provided data
|
||||
// incl. execution of pre and post creation actions
|
||||
//
|
||||
// it is called from either the [autoCreateExternalUser] or [handleExternalNotFoundOptionCheck]
|
||||
func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) {
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
|
||||
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
|
||||
orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
user, externalIDP, metadata := mapExternalUserToLoginUser(externalUser, orgIamPolicy.UserLoginMustBeDomain)
|
||||
|
||||
user, metadata, err = l.runPreCreationActions(authReq, r, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication)
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, nil, nil, err)
|
||||
return
|
||||
}
|
||||
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r))
|
||||
if err != nil {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, orgIamPolicy, user, externalIDP, err)
|
||||
return
|
||||
}
|
||||
// read auth request again to get current state including userID
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
userGrants, err := l.runPostCreationActions(authReq.UserID, authReq, r, resourceOwner, domain.FlowTypeExternalAuthentication)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderNextStep(w, r, authReq)
|
||||
}
|
||||
|
||||
// updateExternalUser will update the existing user (email, phone, profile) with data provided by the IDP
|
||||
func (l *Login) updateExternalUser(ctx context.Context, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) error {
|
||||
user, err := l.query.GetUserByID(ctx, true, authReq.UserID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user.Human == nil {
|
||||
return errors.ThrowPreconditionFailed(nil, "LOGIN-WLTce", "Errors.User.NotHuman")
|
||||
}
|
||||
if externalUser.Email != "" && externalUser.Email != user.Human.Email && externalUser.IsEmailVerified != user.Human.IsEmailVerified {
|
||||
emailCodeGenerator, err := l.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyEmailCode, l.userCodeAlg)
|
||||
logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update email")
|
||||
if err == nil {
|
||||
_, err = l.command.ChangeHumanEmail(setContext(ctx, authReq.UserOrgID),
|
||||
&domain.Email{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: authReq.UserID},
|
||||
EmailAddress: externalUser.Email,
|
||||
IsEmailVerified: externalUser.IsEmailVerified,
|
||||
},
|
||||
emailCodeGenerator)
|
||||
logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update email")
|
||||
}
|
||||
}
|
||||
if externalUser.Phone != "" && externalUser.Phone != user.Human.Phone && externalUser.IsPhoneVerified != user.Human.IsPhoneVerified {
|
||||
phoneCodeGenerator, err := l.query.InitEncryptionGenerator(ctx, domain.SecretGeneratorTypeVerifyPhoneCode, l.userCodeAlg)
|
||||
logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update phone")
|
||||
if err == nil {
|
||||
_, err = l.command.ChangeHumanPhone(setContext(ctx, authReq.UserOrgID),
|
||||
&domain.Phone{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: authReq.UserID},
|
||||
PhoneNumber: externalUser.Phone,
|
||||
IsPhoneVerified: externalUser.IsPhoneVerified,
|
||||
},
|
||||
authReq.UserOrgID,
|
||||
phoneCodeGenerator)
|
||||
logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update phone")
|
||||
}
|
||||
}
|
||||
if externalUser.FirstName != user.Human.FirstName ||
|
||||
externalUser.LastName != user.Human.LastName ||
|
||||
externalUser.NickName != user.Human.NickName ||
|
||||
externalUser.DisplayName != user.Human.DisplayName ||
|
||||
externalUser.PreferredLanguage != user.Human.PreferredLanguage {
|
||||
_, err = l.command.ChangeHumanProfile(setContext(ctx, authReq.UserOrgID), &domain.Profile{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: authReq.UserID},
|
||||
FirstName: externalUser.FirstName,
|
||||
LastName: externalUser.LastName,
|
||||
NickName: externalUser.NickName,
|
||||
DisplayName: externalUser.DisplayName,
|
||||
PreferredLanguage: externalUser.PreferredLanguage,
|
||||
Gender: user.Human.Gender,
|
||||
})
|
||||
logging.WithFields("authReq", authReq.ID, "user", authReq.UserID).OnError(err).Error("unable to update profile")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Login) googleProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*google.Provider, error) {
|
||||
errorHandler := func(w http.ResponseWriter, r *http.Request, errorType string, errorDesc string, state string) {
|
||||
logging.Errorf("token exchanged failed: %s - %s (state: %s)", errorType, errorType, state)
|
||||
rp.DefaultErrorHandler(w, r, errorType, errorDesc, state)
|
||||
}
|
||||
openid.WithRelyingPartyOption(rp.WithErrorHandler(errorHandler))
|
||||
secret, err := crypto.DecryptString(identityProvider.GoogleIDPTemplate.ClientSecret, l.idpConfigAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return google.New(
|
||||
identityProvider.GoogleIDPTemplate.ClientID,
|
||||
secret,
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.GoogleIDPTemplate.Scopes,
|
||||
)
|
||||
}
|
||||
|
||||
func (l *Login) oidcProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*openid.Provider, error) {
|
||||
secret, err := crypto.DecryptString(identityProvider.OIDCIDPTemplate.ClientSecret, l.idpConfigAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openid.New(identityProvider.Name,
|
||||
identityProvider.OIDCIDPTemplate.Issuer,
|
||||
identityProvider.OIDCIDPTemplate.ClientID,
|
||||
secret,
|
||||
l.baseURL(ctx)+EndpointExternalLoginCallback,
|
||||
identityProvider.OIDCIDPTemplate.Scopes,
|
||||
openid.DefaultMapper,
|
||||
)
|
||||
}
|
||||
|
||||
func (l *Login) jwtProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*jwt.Provider, error) {
|
||||
return jwt.New(
|
||||
identityProvider.Name,
|
||||
identityProvider.JWTIDPTemplate.Issuer,
|
||||
identityProvider.JWTIDPTemplate.Endpoint,
|
||||
identityProvider.JWTIDPTemplate.KeysEndpoint,
|
||||
identityProvider.JWTIDPTemplate.HeaderName,
|
||||
l.idpConfigAlg,
|
||||
)
|
||||
}
|
||||
|
||||
func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
|
||||
if len(userGrants) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, grant := range userGrants {
|
||||
_, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Login) externalAuthFailed(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, tokens *oidc.Tokens, err error) {
|
||||
if tokens == nil {
|
||||
tokens = &oidc.Tokens{Token: &oauth2.Token{}}
|
||||
}
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens, authReq, r, err); actionErr != nil {
|
||||
logging.WithError(err).Error("both external user authentication and action post authentication failed")
|
||||
}
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
}
|
||||
|
||||
// tokens extracts the oidc.Tokens for backwards compatibility of PostExternalAuthenticationActions
|
||||
func tokens(session idp.Session) *oidc.Tokens {
|
||||
switch s := session.(type) {
|
||||
case *openid.Session:
|
||||
return s.Tokens
|
||||
case *jwt.Session:
|
||||
return s.Tokens
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapIDPUserToExternalUser(user idp.User, id string) *domain.ExternalUser {
|
||||
return &domain.ExternalUser{
|
||||
IDPConfigID: id,
|
||||
ExternalUserID: user.GetID(),
|
||||
PreferredUsername: user.GetPreferredUsername(),
|
||||
DisplayName: user.GetDisplayName(),
|
||||
FirstName: user.GetFirstName(),
|
||||
LastName: user.GetLastName(),
|
||||
NickName: user.GetNickname(),
|
||||
Email: user.GetEmail(),
|
||||
IsEmailVerified: user.IsEmailVerified(),
|
||||
PreferredLanguage: user.GetPreferredLanguage(),
|
||||
Phone: user.GetPhone(),
|
||||
IsPhoneVerified: user.IsPhoneVerified(),
|
||||
}
|
||||
}
|
||||
|
||||
func mapExternalUserToLoginUser(externalUser *domain.ExternalUser, mustBeDomain bool) (*domain.Human, *domain.UserIDPLink, []*domain.Metadata) {
|
||||
username := externalUser.PreferredUsername
|
||||
if mustBeDomain {
|
||||
index := strings.LastIndex(username, "@")
|
||||
if index > 1 {
|
||||
username = username[:index]
|
||||
}
|
||||
}
|
||||
human := &domain.Human{
|
||||
Username: username,
|
||||
Profile: &domain.Profile{
|
||||
FirstName: externalUser.FirstName,
|
||||
LastName: externalUser.LastName,
|
||||
PreferredLanguage: externalUser.PreferredLanguage,
|
||||
NickName: externalUser.NickName,
|
||||
DisplayName: externalUser.DisplayName,
|
||||
},
|
||||
Email: &domain.Email{
|
||||
EmailAddress: externalUser.Email,
|
||||
IsEmailVerified: externalUser.IsEmailVerified,
|
||||
},
|
||||
}
|
||||
if externalUser.Phone != "" {
|
||||
human.Phone = &domain.Phone{
|
||||
PhoneNumber: externalUser.Phone,
|
||||
IsPhoneVerified: externalUser.IsPhoneVerified,
|
||||
}
|
||||
}
|
||||
externalIDP := &domain.UserIDPLink{
|
||||
IDPConfigID: externalUser.IDPConfigID,
|
||||
ExternalUserID: externalUser.ExternalUserID,
|
||||
DisplayName: externalUser.DisplayName,
|
||||
}
|
||||
return human, externalIDP, externalUser.Metadatas
|
||||
}
|
||||
|
||||
func mapExternalNotFoundOptionFormDataToLoginUser(formData *externalNotFoundOptionFormData) *domain.ExternalUser {
|
||||
isEmailVerified := formData.ExternalEmailVerified && formData.Email == formData.ExternalEmail
|
||||
isPhoneVerified := formData.ExternalPhoneVerified && formData.Phone == formData.ExternalPhone
|
||||
return &domain.ExternalUser{
|
||||
IDPConfigID: formData.ExternalIDPConfigID,
|
||||
ExternalUserID: formData.ExternalIDPExtUserID,
|
||||
PreferredUsername: formData.Username,
|
||||
DisplayName: formData.Email,
|
||||
FirstName: formData.Firstname,
|
||||
LastName: formData.Lastname,
|
||||
NickName: formData.Nickname,
|
||||
Email: formData.Email,
|
||||
IsEmailVerified: isEmailVerified,
|
||||
Phone: formData.Phone,
|
||||
IsPhoneVerified: isPhoneVerified,
|
||||
PreferredLanguage: language.Make(formData.Language),
|
||||
}
|
||||
}
|
@ -1,304 +0,0 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
const (
|
||||
tmplExternalRegisterOverview = "externalregisteroverview"
|
||||
)
|
||||
|
||||
type externalRegisterFormData struct {
|
||||
ExternalIDPConfigID string `schema:"external-idp-config-id"`
|
||||
ExternalIDPExtUserID string `schema:"external-idp-ext-user-id"`
|
||||
ExternalIDPDisplayName string `schema:"external-idp-display-name"`
|
||||
ExternalEmail string `schema:"external-email"`
|
||||
ExternalEmailVerified bool `schema:"external-email-verified"`
|
||||
Email string `schema:"email"`
|
||||
Username string `schema:"username"`
|
||||
Firstname string `schema:"firstname"`
|
||||
Lastname string `schema:"lastname"`
|
||||
Nickname string `schema:"nickname"`
|
||||
ExternalPhone string `schema:"external-phone"`
|
||||
ExternalPhoneVerified bool `schema:"external-phone-verified"`
|
||||
Phone string `schema:"phone"`
|
||||
Language string `schema:"language"`
|
||||
TermsConfirm bool `schema:"terms-confirm"`
|
||||
}
|
||||
|
||||
type externalRegisterData struct {
|
||||
baseData
|
||||
externalRegisterFormData
|
||||
ExternalIDPID string
|
||||
ExternalIDPUserID string
|
||||
ExternalIDPUserDisplayName string
|
||||
ShowUsername bool
|
||||
ShowUsernameSuffix bool
|
||||
OrgRegister bool
|
||||
ExternalEmail string
|
||||
ExternalEmailVerified bool
|
||||
ExternalPhone string
|
||||
ExternalPhoneVerified bool
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalRegister(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalIDPData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.handleExternalRegisterByConfigID(w, r, authReq, data.IDPConfigID)
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalRegisterByConfigID(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, configID string) {
|
||||
if authReq == nil {
|
||||
l.defaultRedirect(w, r)
|
||||
return
|
||||
}
|
||||
idpConfig, err := l.getIDPConfigByID(r, configID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err = l.authRepo.SelectExternalIDP(r.Context(), authReq.ID, idpConfig.IDPConfigID, userAgentID)
|
||||
if err != nil {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if !idpConfig.IsOIDC {
|
||||
l.handleJWTAuthorize(w, r, authReq, idpConfig)
|
||||
return
|
||||
}
|
||||
l.handleOIDCAuthorize(w, r, authReq, idpConfig, EndpointExternalRegisterCallback)
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalRegisterCallback(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalIDPCallbackData)
|
||||
err := l.getParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
authReq, err := l.authRepo.AuthRequestByID(r.Context(), data.State, userAgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
provider, err := l.getRPConfig(r.Context(), idpConfig, EndpointExternalRegisterCallback)
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
tokens, err := rp.CodeExchange(r.Context(), data.Code, provider)
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.handleExternalUserRegister(w, r, authReq, idpConfig, userAgentID, tokens)
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalUserRegister(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, userAgentID string, tokens *oidc.Tokens) {
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
if authReq.RequestedOrgID != "" {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
externalUser, externalIDP := l.mapTokenToLoginHumanAndExternalIDP(tokens, idpConfig)
|
||||
externalUser, err := l.runPostExternalAuthenticationActions(externalUser, tokens, authReq, r, idpConfig, nil)
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if idpConfig.AutoRegister {
|
||||
l.registerExternalUser(w, r, authReq, externalUser)
|
||||
return
|
||||
}
|
||||
orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
labelPolicy, err := l.getLabelPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderExternalRegisterOverview(w, r, authReq, orgIamPolicy, externalUser, externalIDP, labelPolicy.HideLoginNameSuffix, nil)
|
||||
}
|
||||
|
||||
func (l *Login) registerExternalUser(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, externalUser *domain.ExternalUser) {
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
|
||||
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, externalUser, idpConfig)
|
||||
user, metadata, err = l.runPreCreationActions(authReq, r, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication)
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r))
|
||||
if err != nil {
|
||||
l.renderRegisterOption(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
// read auth request again to get current state including userID
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
userGrants, err := l.runPostCreationActions(authReq.UserID, authReq, r, resourceOwner, domain.FlowTypeExternalAuthentication)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderNextStep(w, r, authReq)
|
||||
}
|
||||
|
||||
func (l *Login) renderExternalRegisterOverview(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, orgIAMPolicy *query.DomainPolicy, externalUser *domain.ExternalUser, idp *domain.UserIDPLink, hideLoginNameSuffix bool, err error) {
|
||||
var errID, errMessage string
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
|
||||
translator := l.getTranslator(r.Context(), authReq)
|
||||
data := externalRegisterData{
|
||||
baseData: l.getBaseData(r, authReq, "ExternalRegistrationUserOverview.Title", "ExternalRegistrationUserOverview.Description", errID, errMessage),
|
||||
externalRegisterFormData: externalRegisterFormData{
|
||||
Email: externalUser.Email,
|
||||
Username: externalUser.PreferredUsername,
|
||||
Firstname: externalUser.FirstName,
|
||||
Lastname: externalUser.LastName,
|
||||
Nickname: externalUser.NickName,
|
||||
Language: externalUser.PreferredLanguage.String(),
|
||||
},
|
||||
ExternalIDPID: idp.IDPConfigID,
|
||||
ExternalIDPUserID: idp.ExternalUserID,
|
||||
ExternalIDPUserDisplayName: idp.DisplayName,
|
||||
ExternalEmail: externalUser.Email,
|
||||
ExternalEmailVerified: externalUser.IsEmailVerified,
|
||||
ShowUsername: orgIAMPolicy.UserLoginMustBeDomain,
|
||||
OrgRegister: orgIAMPolicy.UserLoginMustBeDomain,
|
||||
ShowUsernameSuffix: !hideLoginNameSuffix,
|
||||
}
|
||||
data.Phone = externalUser.Phone
|
||||
data.ExternalPhone = externalUser.Phone
|
||||
data.ExternalPhoneVerified = externalUser.IsPhoneVerified
|
||||
|
||||
funcs := map[string]interface{}{
|
||||
"selectedLanguage": func(l string) bool {
|
||||
return data.Language == l
|
||||
},
|
||||
}
|
||||
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplExternalRegisterOverview], data, funcs)
|
||||
}
|
||||
|
||||
func (l *Login) handleExternalRegisterCheck(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(externalRegisterFormData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
resourceOwner := authz.GetInstance(r.Context()).DefaultOrganisationID()
|
||||
|
||||
if authReq.RequestedOrgID != "" && authReq.RequestedOrgID != resourceOwner {
|
||||
resourceOwner = authReq.RequestedOrgID
|
||||
}
|
||||
|
||||
user := l.mapExternalRegisterDataToUser(data)
|
||||
l.registerExternalUser(w, r, authReq, user)
|
||||
}
|
||||
|
||||
func (l *Login) mapTokenToLoginHumanAndExternalIDP(tokens *oidc.Tokens, idpConfig *iam_model.IDPConfigView) (*domain.ExternalUser, *domain.UserIDPLink) {
|
||||
displayName := tokens.IDTokenClaims.GetPreferredUsername()
|
||||
switch idpConfig.OIDCIDPDisplayNameMapping {
|
||||
case iam_model.OIDCMappingFieldEmail:
|
||||
if tokens.IDTokenClaims.IsEmailVerified() && tokens.IDTokenClaims.GetEmail() != "" {
|
||||
displayName = tokens.IDTokenClaims.GetEmail()
|
||||
}
|
||||
}
|
||||
if displayName == "" {
|
||||
displayName = tokens.IDTokenClaims.GetEmail()
|
||||
}
|
||||
|
||||
externalUser := &domain.ExternalUser{
|
||||
IDPConfigID: idpConfig.IDPConfigID,
|
||||
ExternalUserID: tokens.IDTokenClaims.GetSubject(),
|
||||
PreferredUsername: tokens.IDTokenClaims.GetPreferredUsername(),
|
||||
DisplayName: displayName,
|
||||
FirstName: tokens.IDTokenClaims.GetGivenName(),
|
||||
LastName: tokens.IDTokenClaims.GetFamilyName(),
|
||||
NickName: tokens.IDTokenClaims.GetNickname(),
|
||||
Email: tokens.IDTokenClaims.GetEmail(),
|
||||
IsEmailVerified: tokens.IDTokenClaims.IsEmailVerified(),
|
||||
PreferredLanguage: tokens.IDTokenClaims.GetLocale(),
|
||||
}
|
||||
|
||||
if tokens.IDTokenClaims.GetPhoneNumber() != "" {
|
||||
externalUser.Phone = tokens.IDTokenClaims.GetPhoneNumber()
|
||||
externalUser.IsPhoneVerified = tokens.IDTokenClaims.IsPhoneNumberVerified()
|
||||
}
|
||||
|
||||
externalIDP := &domain.UserIDPLink{
|
||||
IDPConfigID: idpConfig.IDPConfigID,
|
||||
ExternalUserID: tokens.IDTokenClaims.GetSubject(),
|
||||
DisplayName: displayName,
|
||||
}
|
||||
return externalUser, externalIDP
|
||||
}
|
||||
|
||||
func (l *Login) mapExternalRegisterDataToUser(data *externalRegisterFormData) *domain.ExternalUser {
|
||||
isEmailVerified := data.ExternalEmailVerified && data.Email == data.ExternalEmail
|
||||
isPhoneVerified := data.ExternalPhoneVerified && data.Phone == data.ExternalPhone
|
||||
return &domain.ExternalUser{
|
||||
IDPConfigID: data.ExternalIDPConfigID,
|
||||
ExternalUserID: data.ExternalIDPExtUserID,
|
||||
PreferredUsername: data.Username,
|
||||
DisplayName: data.Email,
|
||||
FirstName: data.Firstname,
|
||||
LastName: data.Lastname,
|
||||
NickName: data.Nickname,
|
||||
PreferredLanguage: language.Make(data.Language),
|
||||
Email: data.Email,
|
||||
IsEmailVerified: isEmailVerified,
|
||||
Phone: data.Phone,
|
||||
IsPhoneVerified: isPhoneVerified,
|
||||
}
|
||||
}
|
@ -6,17 +6,16 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
type jwtRequest struct {
|
||||
@ -50,12 +49,12 @@ func (l *Login) handleJWTRequest(w http.ResponseWriter, r *http.Request) {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
|
||||
idpConfig, err := l.getIDPByID(r, authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if idpConfig.IsOIDC {
|
||||
if idpConfig.Type != domain.IDPTypeJWT {
|
||||
if err != nil {
|
||||
l.renderError(w, r, nil, err)
|
||||
return
|
||||
@ -64,50 +63,39 @@ func (l *Login) handleJWTRequest(w http.ResponseWriter, r *http.Request) {
|
||||
l.handleJWTExtraction(w, r, authReq, idpConfig)
|
||||
}
|
||||
|
||||
func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView) {
|
||||
token, err := getToken(r, idpConfig.JWTHeaderName)
|
||||
func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, identityProvider *query.IDPTemplate) {
|
||||
token, err := getToken(r, identityProvider.JWTIDPTemplate.HeaderName)
|
||||
if err != nil {
|
||||
emtpyTokens := &oidc.Tokens{Token: &oauth2.Token{}}
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emtpyTokens, authReq, r, idpConfig, err); actionErr != nil {
|
||||
emptyTokens := &oidc.Tokens{Token: &oauth2.Token{}}
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emptyTokens, authReq, r, err); actionErr != nil {
|
||||
logging.WithError(err).Error("both external user authentication and action post authentication failed")
|
||||
}
|
||||
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
tokenClaims, err := validateToken(r.Context(), token, idpConfig)
|
||||
tokens := &oidc.Tokens{IDToken: token, IDTokenClaims: tokenClaims, Token: &oauth2.Token{}}
|
||||
provider, err := l.jwtProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens, authReq, r, idpConfig, err); actionErr != nil {
|
||||
emptyTokens := &oidc.Tokens{Token: &oauth2.Token{}}
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, emptyTokens, authReq, r, err); actionErr != nil {
|
||||
logging.WithError(err).Error("both external user authentication and action post authentication failed")
|
||||
}
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
externalUser := l.mapTokenToLoginUser(tokens, idpConfig)
|
||||
externalUser, err = l.runPostExternalAuthenticationActions(externalUser, tokens, authReq, r, idpConfig, nil)
|
||||
session := &jwt.Session{Provider: provider, Tokens: &oidc.Tokens{IDToken: token, Token: &oauth2.Token{}}}
|
||||
user, err := session.FetchUser(r.Context())
|
||||
if err != nil {
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(&domain.ExternalUser{}, tokens(session), authReq, r, err); actionErr != nil {
|
||||
logging.WithError(err).Error("both external user authentication and action post authentication failed")
|
||||
}
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
metadata := externalUser.Metadatas
|
||||
err = l.authRepo.CheckExternalUserLogin(setContext(r.Context(), ""), authReq.ID, authReq.AgentID, externalUser, domain.BrowserInfoFromRequest(r))
|
||||
if err != nil {
|
||||
l.jwtExtractionUserNotFound(w, r, authReq, idpConfig, tokens, err)
|
||||
return
|
||||
}
|
||||
if len(metadata) > 0 {
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
_, err = l.command.BulkSetUserMetadata(setContext(r.Context(), authReq.UserOrgID), authReq.UserID, authReq.UserOrgID, metadata...)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.jwtCallback)
|
||||
}
|
||||
|
||||
func (l *Login) jwtCallback(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest) {
|
||||
redirect, err := l.redirectToJWTCallback(r.Context(), authReq)
|
||||
if err != nil {
|
||||
l.renderError(w, r, nil, err)
|
||||
@ -116,73 +104,6 @@ func (l *Login) handleJWTExtraction(w http.ResponseWriter, r *http.Request, auth
|
||||
http.Redirect(w, r, redirect, http.StatusFound)
|
||||
}
|
||||
|
||||
func (l *Login) jwtExtractionUserNotFound(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, idpConfig *iam_model.IDPConfigView, tokens *oidc.Tokens, err error) {
|
||||
if errors.IsNotFound(err) {
|
||||
err = nil
|
||||
}
|
||||
if !idpConfig.AutoRegister {
|
||||
l.renderExternalNotFoundOption(w, r, authReq, nil, nil, nil, err)
|
||||
return
|
||||
}
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
resourceOwner := l.getOrgID(r, authReq)
|
||||
orgIamPolicy, err := l.getOrgDomainPolicy(r, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
user, externalIDP, metadata := l.mapExternalUserToLoginUser(orgIamPolicy, authReq.LinkingUsers[len(authReq.LinkingUsers)-1], idpConfig)
|
||||
user, metadata, err = l.runPreCreationActions(authReq, r, user, metadata, resourceOwner, domain.FlowTypeExternalAuthentication)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.authRepo.AutoRegisterExternalUser(setContext(r.Context(), resourceOwner), user, externalIDP, nil, authReq.ID, authReq.AgentID, resourceOwner, metadata, domain.BrowserInfoFromRequest(r))
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
authReq, err = l.authRepo.AuthRequestByID(r.Context(), authReq.ID, authReq.AgentID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
userGrants, err := l.runPostCreationActions(authReq.UserID, authReq, r, resourceOwner, domain.FlowTypeExternalAuthentication)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
err = l.appendUserGrants(r.Context(), userGrants, resourceOwner)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
redirect, err := l.redirectToJWTCallback(r.Context(), authReq)
|
||||
if err != nil {
|
||||
l.renderError(w, r, nil, err)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, redirect, http.StatusFound)
|
||||
}
|
||||
|
||||
func (l *Login) appendUserGrants(ctx context.Context, userGrants []*domain.UserGrant, resourceOwner string) error {
|
||||
if len(userGrants) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, grant := range userGrants {
|
||||
_, err := l.command.AddUserGrant(setContext(ctx, resourceOwner), grant, resourceOwner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Login) redirectToJWTCallback(ctx context.Context, authReq *domain.AuthRequest) (string, error) {
|
||||
redirect, err := url.Parse(l.baseURL(ctx) + EndpointJWTCallback)
|
||||
if err != nil {
|
||||
@ -221,52 +142,18 @@ func (l *Login) handleJWTCallback(w http.ResponseWriter, r *http.Request) {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
idpConfig, err := l.authRepo.GetIDPConfigByID(r.Context(), authReq.SelectedIDPConfigID)
|
||||
idpConfig, err := l.getIDPByID(r, authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if idpConfig.IsOIDC {
|
||||
if idpConfig.Type != domain.IDPTypeJWT {
|
||||
l.renderLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderNextStep(w, r, authReq)
|
||||
}
|
||||
|
||||
func validateToken(ctx context.Context, token string, config *iam_model.IDPConfigView) (oidc.IDTokenClaims, error) {
|
||||
logging.Log("LOGIN-ADf42").Debug("begin token validation")
|
||||
offset := 3 * time.Second
|
||||
maxAge := time.Hour
|
||||
claims := oidc.EmptyIDTokenClaims()
|
||||
payload, err := oidc.ParseToken(token, claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = oidc.CheckIssuer(claims, config.JWTIssuer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logging.Log("LOGIN-Dfg22").Debug("begin signature validation")
|
||||
keySet := rp.NewRemoteKeySet(http.DefaultClient, config.JWTKeysEndpoint)
|
||||
if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !claims.GetExpiration().IsZero() {
|
||||
if err = oidc.CheckExpiration(claims, offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if !claims.GetIssuedAt().IsZero() {
|
||||
if err = oidc.CheckIssuedAt(claims, maxAge, offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func getToken(r *http.Request, headerName string) (string, error) {
|
||||
if headerName == "" {
|
||||
headerName = http_util.Authorization
|
||||
|
@ -3,7 +3,6 @@ package login
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
)
|
||||
|
||||
@ -18,8 +17,8 @@ func (l *Login) getOrgDomainPolicy(r *http.Request, orgID string) (*query.Domain
|
||||
return l.query.DomainPolicyByOrg(r.Context(), false, orgID, false)
|
||||
}
|
||||
|
||||
func (l *Login) getIDPConfigByID(r *http.Request, idpConfigID string) (*iam_model.IDPConfigView, error) {
|
||||
return l.authRepo.GetIDPConfigByID(r.Context(), idpConfigID)
|
||||
func (l *Login) getIDPByID(r *http.Request, id string) (*query.IDPTemplate, error) {
|
||||
return l.query.IDPTemplateByID(r.Context(), false, id, false)
|
||||
}
|
||||
|
||||
func (l *Login) getLoginPolicy(r *http.Request, orgID string) (*query.LoginPolicy, error) {
|
||||
|
@ -42,7 +42,7 @@ func (l *Login) renderRegisterOption(w http.ResponseWriter, r *http.Request, aut
|
||||
if err == nil {
|
||||
// if only external allowed with a single idp then use that
|
||||
if !allowed && externalAllowed && len(authReq.AllowedExternalIDPs) == 1 {
|
||||
l.handleExternalRegisterByConfigID(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID)
|
||||
l.handleIDP(w, r, authReq, authReq.AllowedExternalIDPs[0].IDPConfigID)
|
||||
return
|
||||
}
|
||||
// if only direct registration is allowed, show the form
|
||||
|
@ -69,7 +69,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
||||
tmplChangePasswordDone: "change_password_done.html",
|
||||
tmplRegisterOption: "register_option.html",
|
||||
tmplRegister: "register.html",
|
||||
tmplExternalRegisterOverview: "external_register_overview.html",
|
||||
tmplLogoutDone: "logout_done.html",
|
||||
tmplRegisterOrg: "register_org.html",
|
||||
tmplChangeUsername: "change_username.html",
|
||||
@ -193,9 +192,6 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
||||
"orgRegistrationUrl": func() string {
|
||||
return path.Join(r.pathPrefix, EndpointRegisterOrg)
|
||||
},
|
||||
"externalRegistrationUrl": func() string {
|
||||
return path.Join(r.pathPrefix, EndpointExternalRegister)
|
||||
},
|
||||
"changeUsernameUrl": func() string {
|
||||
return path.Join(r.pathPrefix, EndpointChangeUsername)
|
||||
},
|
||||
@ -220,8 +216,8 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
||||
"hasRegistration": func() bool {
|
||||
return true
|
||||
},
|
||||
"idpProviderClass": func(stylingType domain.IDPConfigStylingType) string {
|
||||
return stylingType.GetCSSClass()
|
||||
"idpProviderClass": func(idpType domain.IDPType) string {
|
||||
return idpType.GetCSSClass()
|
||||
},
|
||||
}
|
||||
var err error
|
||||
|
@ -95,8 +95,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
|
||||
router.HandleFunc(EndpointRegister, login.handleRegister).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointRegister, login.handleRegisterCheck).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointExternalRegister, login.handleExternalRegister).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointExternalRegister, login.handleExternalRegisterCheck).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalRegisterCallback).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointExternalRegisterCallback, login.handleExternalLoginCallback).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointLogoutDone, login.handleLogoutDone).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointDynamicResources, login.handleDynamicResources).Methods(http.MethodGet)
|
||||
router.PathPrefix(EndpointResources).Handler(login.handleResources(staticDir)).Methods(http.MethodGet)
|
||||
|
@ -370,6 +370,8 @@ Errors:
|
||||
ExternalUserIDEmpty: Externe User ID ist leer
|
||||
UserDisplayNameEmpty: Benutzer Anzeige Name ist leer
|
||||
NoExternalUserData: Keine externe User Daten erhalten
|
||||
CreationNotAllowed: Erstellen eines neuen User ist auf diesem Provider nicht erlaubt
|
||||
LinkingNotAllowed: Linken eines Users ist auf diesem Provider nicht erlaubt
|
||||
GrantRequired: Der Login an diese Applikation ist nicht möglich. Der Benutzer benötigt mindestens eine Berechtigung an der Applikation. Bitte melde dich bei deinem Administrator.
|
||||
ProjectRequired: Der Login an diese Applikation ist nicht möglich. Die Organisation des Benutzer benötigt Berechtigung auf das Projekt. Bitte melde dich bei deinem Administrator.
|
||||
IdentityProvider:
|
||||
|
@ -370,6 +370,8 @@ Errors:
|
||||
ExternalUserIDEmpty: External User ID is empty
|
||||
UserDisplayNameEmpty: User Display Name is empty
|
||||
NoExternalUserData: No external User Data received
|
||||
CreationNotAllowed: Creation of a new user is not allowed on this Provider
|
||||
LinkingNotAllowed: Linking of a user is not allowed on this Provider
|
||||
GrantRequired: Login not possible. The user is required to have at least one grant on the application. Please contact your administrator.
|
||||
ProjectRequired: Login not possible. The organisation of the user must be granted to the project. Please contact your administrator.
|
||||
IdentityProvider:
|
||||
|
@ -370,6 +370,8 @@ Errors:
|
||||
ExternalUserIDEmpty: L'ID de l'utilisateur externe est vide
|
||||
UserDisplayNameEmpty: Le nom d'affichage de l'utilisateur est vide
|
||||
NoExternalUserData: Aucune donnée d'utilisateur externe reçue
|
||||
CreationNotAllowed : La création d'un nouvel utilisateur n'est pas autorisée sur ce fournisseur.
|
||||
LinkingNotAllowed : La création d'un lien vers un utilisateur n'est pas autorisée pour ce fournisseur.
|
||||
GrantRequired: Connexion impossible. L'utilisateur doit avoir au moins une subvention sur l'application. Veuillez contacter votre administrateur.
|
||||
ProjectRequired: Connexion impossible. L'organisation de l'utilisateur doit être accordée au projet. Veuillez contacter votre administrateur.
|
||||
IdentityProvider:
|
||||
|
@ -370,6 +370,8 @@ Errors:
|
||||
ExternalUserIDEmpty: L'ID utente esterno è vuoto
|
||||
UserDisplayNameEmpty: Il nome visualizzato dell'utente è vuoto
|
||||
NoExternalUserData: Nessun dato utente esterno ricevuto
|
||||
CreationNotAllowed: La creazione di un nuovo utente non è consentita su questo provider.
|
||||
LinkingNotAllowed: Il collegamento di un utente non è consentito su questo provider.
|
||||
GrantRequired: Accesso non possibile. L'utente deve avere almeno una sovvenzione sull'applicazione. Contatta il tuo amministratore.
|
||||
ProjectRequired: Accesso non possibile. L'organizzazione dell'utente deve essere concessa al progetto. Contatta il tuo amministratore.
|
||||
IdentityProvider:
|
||||
|
@ -370,6 +370,8 @@ Errors:
|
||||
ExternalUserIDEmpty: Identyfikator użytkownika zewnętrznego jest pusty
|
||||
UserDisplayNameEmpty: Nazwa wyświetlana użytkownika jest pusta
|
||||
NoExternalUserData: Nie otrzymano danych użytkownika zewnętrznego
|
||||
CreationNotAllowed: Tworzenie nowego użytkownika nie jest dozwolone w tym Providencie
|
||||
LinkingNotAllowed: Linkowanie użytkownika nie jest dozwolone na tym Providencie
|
||||
GrantRequired: Logowanie nie jest możliwe. Użytkownik musi posiadać przynajmniej jedno uprawnienie w aplikacji. Skontaktuj się z administratorem.
|
||||
ProjectRequired: Logowanie nie jest możliwe. Organizacja użytkownika musi zostać udzielona projektowi. Skontaktuj się z administratorem.
|
||||
IdentityProvider:
|
||||
|
@ -370,6 +370,8 @@ Errors:
|
||||
ExternalUserIDEmpty: 外部用户 ID 为空
|
||||
UserDisplayNameEmpty: 用户显示名称为空
|
||||
NoExternalUserData: 未收到外部用户数据
|
||||
CreationNotAllowed: 不允许在该供应商上创建新用户
|
||||
LinkingNotAllowed: 在此提供者上不允许链接一个用户
|
||||
GrantRequired: 无法登录,用户需要在应用程序上拥有至少一项授权,请联系您的管理员。
|
||||
ProjectRequired: 无法登录,用户的组织必须授予项目,请联系您的管理员。
|
||||
IdentityProvider:
|
||||
|
@ -118,13 +118,17 @@
|
||||
<i class="lgn-icon-arrow-left-solid"></i>
|
||||
</button>
|
||||
|
||||
{{ if .IsLinkingAllowed }}
|
||||
<button type="submit" formaction="{{ externalNotFoundOptionUrl "linkbutton"}}" class="lgn-raised-button lgn-primary" name="linkbutton" value="true">
|
||||
{{t "ExternalNotFound.LinkButtonText"}}
|
||||
</button>
|
||||
<span class="fill-space"></span>
|
||||
{{ end }}
|
||||
<span class="fill-space"></span>
|
||||
{{ if .IsCreationAllowed }}
|
||||
<button type="submit" formaction="{{ externalNotFoundOptionUrl "autoregisterbutton"}}" class="lgn-raised-button lgn-primary" name="autoregisterbutton" value="true">
|
||||
{{t "ExternalNotFound.AutoRegisterButtonText"}}
|
||||
</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
@ -1,128 +0,0 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<div class="lgn-head">
|
||||
<h1>{{t "ExternalRegistrationUserOverview.Title"}}</h1>
|
||||
<p>{{t "ExternalRegistrationUserOverview.Description"}}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<form action="{{ externalRegistrationUrl }}" method="POST">
|
||||
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}" />
|
||||
<input type="hidden" id="external-idp-config-id" name="external-idp-config-id" value="{{ .ExternalIDPID }}" />
|
||||
<input type="hidden" id="external-idp-ext-user-id" name="external-idp-ext-user-id" value="{{ .ExternalIDPUserID }}" />
|
||||
<input type="hidden" id="external-idp-display-name" name="external-idp-display-name" value="{{ .ExternalIDPUserDisplayName }}" />
|
||||
<input type="hidden" id="external-email" name="external-email" value="{{ .ExternalEmail }}" />
|
||||
<input type="hidden" id="external-email-verified" name="external-email-verified" value="{{ .ExternalEmailVerified }}" />
|
||||
<input type="hidden" id="external-phone" name="external-phone" value="{{ .ExternalPhone }}" />
|
||||
<input type="hidden" id="external-phone-verified" name="external-phone-verified" value="{{ .ExternalPhoneVerified }}" />
|
||||
|
||||
<div class="lgn-register">
|
||||
|
||||
<div class="double-col">
|
||||
<div class="lgn-field">
|
||||
<label class="lgn-label" for="firstname">{{t "ExternalRegistrationUserOverview.FirstnameLabel"}}</label>
|
||||
<input class="lgn-input" type="text" id="firstname" name="firstname" autocomplete="given-name"
|
||||
value="{{ .Firstname }}" autofocus required>
|
||||
</div>
|
||||
<div class="lgn-field">
|
||||
<label class="lgn-label" for="lastname">{{t "ExternalRegistrationUserOverview.LastnameLabel"}}</label>
|
||||
<input class="lgn-input" type="text" id="lastname" name="lastname" autocomplete="family-name"
|
||||
value="{{ .Lastname }}" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lgn-field double">
|
||||
<label class="lgn-label" for="username">{{t "ExternalRegistrationUserOverview.UsernameLabel"}}</label>
|
||||
<div class="lgn-suffix-wrapper">
|
||||
<input class="lgn-input lgn-suffix-input" type="text" id="username" name="username"
|
||||
value="{{ .Username }}" required>
|
||||
{{if .ShowUsernameSuffix}}
|
||||
<span id="default-login-suffix" lgnsuffix class="loginname-suffix">@{{.PrimaryDomain}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lgn-field double">
|
||||
<label class="lgn-label" for="email">{{t "ExternalRegistrationUserOverview.EmailLabel"}}</label>
|
||||
<input class="lgn-input" type="email" id="email" name="email" autocomplete="email" value="{{ .Email }}" required>
|
||||
</div>
|
||||
|
||||
<div class="lgn-field double">
|
||||
<label class="lgn-label" for="phone">{{t "ExternalRegistrationUserOverview.PhoneLabel"}}</label>
|
||||
<input class="lgn-input" type="text" id="phone" name="phone" autocomplete="tel" value="{{ .Phone }}">
|
||||
</div>
|
||||
|
||||
<div class="double-col">
|
||||
<div class="lgn-field">
|
||||
<label class="lgn-label" for="languages">{{t "ExternalRegistrationUserOverview.LanguageLabel"}}</label>
|
||||
<select id="languages" name="language">
|
||||
<option value=""></option>
|
||||
<option value="de" id="de" {{if (selectedLanguage "de")}} selected {{end}}>{{t "ExternalRegistrationUserOverview.German"}}
|
||||
</option>
|
||||
<option value="en" id="en" {{if (selectedLanguage "en")}} selected {{end}}>{{t "ExternalRegistrationUserOverview.English"}}
|
||||
</option>
|
||||
<option value="it" id="it" {{if (selectedLanguage "it")}} selected {{end}}>{{t "ExternalRegistrationUserOverview.Italian"}}
|
||||
</option>
|
||||
<option value="fr" id="fr" {{if (selectedLanguage "fr")}} selected {{end}}>{{t "ExternalRegistrationUserOverview.French"}}
|
||||
</option>
|
||||
<option value="zh" id="zh" {{if (selectedLanguage "zh")}} selected {{end}}>{{t "ExternalRegistrationUserOverview.Chinese"}}
|
||||
</option>
|
||||
<option value="pl" id="pl" {{if (selectedLanguage "pl")}} selected {{end}}>{{t "ExternalRegistrationUserOverview.Polish"}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ if or .TOSLink .PrivacyLink }}
|
||||
<div class="lgn-field">
|
||||
<label class="lgn-label">{{t "ExternalRegistrationUserOverview.TosAndPrivacyLabel"}}</label>
|
||||
{{ if .TOSLink }}
|
||||
<div class="lgn-checkbox">
|
||||
<input type="checkbox" id="register-term-confirmation"
|
||||
name="register-term-confirmation" required>
|
||||
<label for="register-term-confirmation">
|
||||
{{t "ExternalRegistrationUserOverview.TosConfirm"}}
|
||||
<a class="tos-link" target="_blank" href="{{ .TOSLink }}" rel="noopener noreferrer">
|
||||
{{t "ExternalRegistrationUserOverview.TosLinkText"}}
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
{{end}}
|
||||
{{ if and .TOSLink .PrivacyLink }}
|
||||
<br />
|
||||
{{end}}
|
||||
{{ if .PrivacyLink }}
|
||||
<div class="lgn-checkbox">
|
||||
<input type="checkbox" id="register-term-confirmation-privacy"
|
||||
name="register-term-confirmation-privacy" required>
|
||||
<label for="register-term-confirmation-privacy">
|
||||
{{t "ExternalRegistrationUserOverview.PrivacyConfirm"}}
|
||||
<a class="tos-link" target="_blank" href="{{ .PrivacyLink}}" rel="noopener noreferrer">
|
||||
{{t "ExternalRegistrationUserOverview.PrivacyLinkText"}}
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="lgn-actions">
|
||||
<a class="lgn-stroked-button" href="{{ registerOptionUrl }}">
|
||||
{{t "ExternalRegistrationUserOverview.BackButtonText"}}
|
||||
</a>
|
||||
<span class="fill-space"></span>
|
||||
<button class="lgn-raised-button lgn-primary" id="submit-button" type="submit">{{t "ExternalRegistrationUserOverview.NextButtonText"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="{{ resourceUrl "scripts/input_suffix_offset.js" }}"></script>
|
||||
<script src="{{ resourceUrl "scripts/form_submit.js" }}"></script>
|
||||
<script src="{{ resourceUrl "scripts/default_form_validation.js" }}"></script>
|
||||
|
||||
{{template "main-bottom" .}}
|
@ -47,7 +47,7 @@
|
||||
{{ $reqid := .AuthReqID}}
|
||||
{{range $provider := .IDPProviders}}
|
||||
<a href="{{ externalIDPAuthURL $reqid $provider.IDPConfigID}}"
|
||||
class="lgn-idp {{idpProviderClass $provider.StylingType}}">
|
||||
class="lgn-idp {{idpProviderClass $provider.IDPType}}">
|
||||
<span class="logo"></span>
|
||||
<span class="provider-name">{{$provider.Name}}</span>
|
||||
</a>
|
||||
|
@ -27,7 +27,7 @@
|
||||
{{ $reqid := .AuthReqID}}
|
||||
{{range $provider := .IDPProviders}}
|
||||
<a href="{{ externalIDPRegisterURL $reqid $provider.IDPConfigID}}"
|
||||
class="lgn-idp {{idpProviderClass $provider.StylingType}}">
|
||||
class="lgn-idp {{idpProviderClass $provider.IDPType}}">
|
||||
<span class="logo"></span>
|
||||
<span class="provider-name">{{$provider.Name}}</span>
|
||||
</a>
|
||||
|
@ -17,8 +17,6 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
iam_view_model "github.com/zitadel/zitadel/internal/iam/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
project_view_model "github.com/zitadel/zitadel/internal/project/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
@ -81,7 +79,7 @@ type lockoutPolicyViewProvider interface {
|
||||
}
|
||||
|
||||
type idpProviderViewProvider interface {
|
||||
IDPProvidersByAggregateIDAndState(string, string, iam_model.IDPConfigState) ([]*iam_view_model.IDPProviderView, error)
|
||||
IDPLoginPolicyLinks(context.Context, string, *query.IDPLoginPolicyLinksSearchQuery, bool) (*query.IDPLoginPolicyLinks, error)
|
||||
}
|
||||
|
||||
type idpUserLinksProvider interface {
|
||||
@ -554,13 +552,11 @@ func (repo *AuthRequestRepo) getLoginPolicyAndIDPProviders(ctx context.Context,
|
||||
if !policy.AllowExternalIDPs {
|
||||
return policy, nil, nil
|
||||
}
|
||||
idpProviders, err := getLoginPolicyIDPProviders(repo.IDPProviderViewProvider, authz.GetInstance(ctx).InstanceID(), orgID, policy.IsDefault)
|
||||
idpProviders, err := getLoginPolicyIDPProviders(ctx, repo.IDPProviderViewProvider, authz.GetInstance(ctx).InstanceID(), orgID, policy.IsDefault)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
providers := iam_model.IdpProviderViewsToDomain(idpProviders)
|
||||
return policy, providers, nil
|
||||
return policy, idpProviders, nil
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) fillPolicies(ctx context.Context, request *domain.AuthRequest) error {
|
||||
@ -850,16 +846,32 @@ func (repo *AuthRequestRepo) checkSelectedExternalIDP(request *domain.AuthReques
|
||||
}
|
||||
|
||||
func (repo *AuthRequestRepo) checkExternalUserLogin(ctx context.Context, request *domain.AuthRequest, idpConfigID, externalUserID string) (err error) {
|
||||
var externalIDP *user_view_model.ExternalIDPView
|
||||
if request.RequestedOrgID != "" {
|
||||
externalIDP, err = repo.View.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, request.RequestedOrgID, request.InstanceID)
|
||||
} else {
|
||||
externalIDP, err = repo.View.ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID, request.InstanceID)
|
||||
}
|
||||
idQuery, err := query.NewIDPUserLinkIDPIDSearchQuery(idpConfigID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, externalIDP.UserID, false)
|
||||
externalIDQuery, err := query.NewIDPUserLinksExternalIDSearchQuery(externalUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queries := []query.SearchQuery{
|
||||
idQuery, externalIDQuery,
|
||||
}
|
||||
if request.RequestedOrgID != "" {
|
||||
orgIDQuery, err := query.NewIDPUserLinksResourceOwnerSearchQuery(idpConfigID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
queries = append(queries, orgIDQuery)
|
||||
}
|
||||
links, err := repo.Query.IDPUserLinks(ctx, &query.IDPUserLinksSearchQuery{Queries: queries}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(links.Links) != 1 {
|
||||
return errors.ThrowNotFound(nil, "AUTH-Sf8sd", "Errors.ExternalIDP.NotFound")
|
||||
}
|
||||
user, err := activeUserByID(ctx, repo.UserViewProvider, repo.UserEventProvider, repo.OrgViewProvider, repo.LockoutPolicyViewProvider, links.Links[0].UserID, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -1233,19 +1245,25 @@ func setOrgID(ctx context.Context, orgViewProvider orgViewProvider, request *dom
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoginPolicyIDPProviders(provider idpProviderViewProvider, iamID, orgID string, defaultPolicy bool) ([]*iam_model.IDPProviderView, error) {
|
||||
if defaultPolicy {
|
||||
idpProviders, err := provider.IDPProvidersByAggregateIDAndState(iamID, iamID, iam_model.IDPConfigStateActive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iam_view_model.IDPProviderViewsToModel(idpProviders), nil
|
||||
func getLoginPolicyIDPProviders(ctx context.Context, provider idpProviderViewProvider, iamID, orgID string, defaultPolicy bool) ([]*domain.IDPProvider, error) {
|
||||
resourceOwner := iamID
|
||||
if !defaultPolicy {
|
||||
resourceOwner = orgID
|
||||
}
|
||||
idpProviders, err := provider.IDPProvidersByAggregateIDAndState(orgID, iamID, iam_model.IDPConfigStateActive)
|
||||
links, err := provider.IDPLoginPolicyLinks(ctx, resourceOwner, &query.IDPLoginPolicyLinksSearchQuery{}, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iam_view_model.IDPProviderViewsToModel(idpProviders), nil
|
||||
providers := make([]*domain.IDPProvider, len(links.Links))
|
||||
for i, link := range links.Links {
|
||||
providers[i] = &domain.IDPProvider{
|
||||
Type: link.OwnerType,
|
||||
IDPConfigID: link.IDPID,
|
||||
Name: link.IDPName,
|
||||
IDPType: link.IDPType,
|
||||
}
|
||||
}
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func checkVerificationTimeMaxAge(verificationTime time.Time, lifetime time.Duration, request *domain.AuthRequest) bool {
|
||||
|
@ -22,14 +22,6 @@ type OrgRepository struct {
|
||||
Query *query.Queries
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error) {
|
||||
idpConfig, err := repo.View.IDPConfigByID(idpConfigID, authz.GetInstance(ctx).InstanceID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return iam_view_model.IDPConfigViewToModel(idpConfig), nil
|
||||
}
|
||||
|
||||
func (repo *OrgRepository) GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error) {
|
||||
policy, err := repo.Query.PasswordComplexityPolicyByOrg(ctx, true, authz.GetCtxData(ctx).OrgID, false)
|
||||
if err != nil {
|
||||
|
@ -41,14 +41,6 @@ func Register(ctx context.Context, configs Configs, bulkLimit, errorCount uint64
|
||||
handler{view, bulkLimit, configs.cycleDuration("UserSession"), errorCount, es}, queries),
|
||||
newToken(ctx,
|
||||
handler{view, bulkLimit, configs.cycleDuration("Token"), errorCount, es}),
|
||||
newIDPConfig(ctx,
|
||||
handler{view, bulkLimit, configs.cycleDuration("IDPConfig"), errorCount, es}),
|
||||
newIDPProvider(ctx,
|
||||
handler{view, bulkLimit, configs.cycleDuration("IDPProvider"), errorCount, es},
|
||||
systemDefaults, queries),
|
||||
newExternalIDP(ctx,
|
||||
handler{view, bulkLimit, configs.cycleDuration("ExternalIDP"), errorCount, es},
|
||||
systemDefaults, queries),
|
||||
newRefreshToken(ctx, handler{view, bulkLimit, configs.cycleDuration("RefreshToken"), errorCount, es}),
|
||||
newOrgProjectMapping(ctx, handler{view, bulkLimit, configs.cycleDuration("OrgProjectMapping"), errorCount, es}),
|
||||
}
|
||||
|
@ -1,142 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
iam_view_model "github.com/zitadel/zitadel/internal/iam/repository/view/model"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
const (
|
||||
idpConfigTable = "auth.idp_configs2"
|
||||
)
|
||||
|
||||
type IDPConfig struct {
|
||||
handler
|
||||
subscription *v1.Subscription
|
||||
}
|
||||
|
||||
func newIDPConfig(ctx context.Context, h handler) *IDPConfig {
|
||||
idpConfig := &IDPConfig{
|
||||
handler: h,
|
||||
}
|
||||
|
||||
idpConfig.subscribe(ctx)
|
||||
|
||||
return idpConfig
|
||||
}
|
||||
|
||||
func (i *IDPConfig) subscribe(ctx context.Context) {
|
||||
i.subscription = i.es.Subscribe(i.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range i.subscription.Events {
|
||||
query.ReduceEvent(ctx, i, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *IDPConfig) ViewModel() string {
|
||||
return idpConfigTable
|
||||
}
|
||||
|
||||
func (i *IDPConfig) Subscription() *v1.Subscription {
|
||||
return i.subscription
|
||||
}
|
||||
|
||||
func (_ *IDPConfig) AggregateTypes() []models.AggregateType {
|
||||
return []models.AggregateType{org.AggregateType, instance.AggregateType}
|
||||
}
|
||||
|
||||
func (i *IDPConfig) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := i.view.GetLatestIDPConfigSequence(instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (i *IDPConfig) EventQuery(instanceIDs []string) (*models.SearchQuery, error) {
|
||||
sequences, err := i.view.GetLatestIDPConfigSequences(instanceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newSearchQuery(sequences, i.AggregateTypes(), instanceIDs), nil
|
||||
}
|
||||
|
||||
func (i *IDPConfig) Reduce(event *models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case org.AggregateType:
|
||||
err = i.processIdpConfig(iam_model.IDPProviderTypeOrg, event)
|
||||
case instance.AggregateType:
|
||||
err = i.processIdpConfig(iam_model.IDPProviderTypeSystem, event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *IDPConfig) processIdpConfig(providerType iam_model.IDPProviderType, event *models.Event) (err error) {
|
||||
idp := new(iam_view_model.IDPConfigView)
|
||||
switch eventstore.EventType(event.Type) {
|
||||
case org.IDPConfigAddedEventType,
|
||||
instance.IDPConfigAddedEventType:
|
||||
err = idp.AppendEvent(providerType, event)
|
||||
case org.IDPConfigChangedEventType, instance.IDPConfigChangedEventType,
|
||||
org.IDPOIDCConfigAddedEventType, instance.IDPOIDCConfigAddedEventType,
|
||||
org.IDPOIDCConfigChangedEventType, instance.IDPOIDCConfigChangedEventType,
|
||||
org.IDPJWTConfigAddedEventType, instance.IDPJWTConfigAddedEventType,
|
||||
org.IDPJWTConfigChangedEventType, instance.IDPJWTConfigChangedEventType:
|
||||
err = idp.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idp, err = i.view.IDPConfigByID(idp.IDPConfigID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = idp.AppendEvent(providerType, event)
|
||||
case org.IDPConfigDeactivatedEventType, instance.IDPConfigDeactivatedEventType,
|
||||
org.IDPConfigReactivatedEventType, instance.IDPConfigReactivatedEventType:
|
||||
err = idp.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
idp, err = i.view.IDPConfigByID(idp.IDPConfigID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = idp.AppendEvent(providerType, event)
|
||||
case org.IDPConfigRemovedEventType, instance.IDPConfigRemovedEventType:
|
||||
err = idp.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.view.DeleteIDPConfig(idp.IDPConfigID, event)
|
||||
case instance.InstanceRemovedEventType:
|
||||
return i.view.DeleteInstanceIDPs(event)
|
||||
case org.OrgRemovedEventType:
|
||||
return i.view.UpdateOrgOwnerRemovedIDPs(event)
|
||||
default:
|
||||
return i.view.ProcessedIDPConfigSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.view.PutIDPConfig(idp, event)
|
||||
}
|
||||
|
||||
func (i *IDPConfig) OnError(event *models.Event, err error) error {
|
||||
logging.WithFields("id", event.AggregateID).WithError(err).Warn("something went wrong in idp config handler")
|
||||
return spooler.HandleError(event, err, i.view.GetLatestIDPConfigFailedEvent, i.view.ProcessedIDPConfigFailedEvent, i.view.ProcessedIDPConfigSequence, i.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (i *IDPConfig) OnSuccess(instanceIDs []string) error {
|
||||
return spooler.HandleSuccess(i.view.UpdateIDPConfigSpoolerRunTimestamp, instanceIDs)
|
||||
}
|
@ -1,204 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
iam_view_model "github.com/zitadel/zitadel/internal/iam/repository/view/model"
|
||||
query2 "github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
)
|
||||
|
||||
const (
|
||||
idpProviderTable = "auth.idp_providers2"
|
||||
)
|
||||
|
||||
type IDPProvider struct {
|
||||
handler
|
||||
systemDefaults systemdefaults.SystemDefaults
|
||||
subscription *v1.Subscription
|
||||
queries *query2.Queries
|
||||
}
|
||||
|
||||
func newIDPProvider(
|
||||
ctx context.Context,
|
||||
h handler,
|
||||
defaults systemdefaults.SystemDefaults,
|
||||
queries *query2.Queries,
|
||||
) *IDPProvider {
|
||||
idpProvider := &IDPProvider{
|
||||
handler: h,
|
||||
systemDefaults: defaults,
|
||||
queries: queries,
|
||||
}
|
||||
|
||||
idpProvider.subscribe(ctx)
|
||||
|
||||
return idpProvider
|
||||
}
|
||||
|
||||
func (i *IDPProvider) subscribe(ctx context.Context) {
|
||||
i.subscription = i.es.Subscribe(i.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range i.subscription.Events {
|
||||
query.ReduceEvent(ctx, i, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *IDPProvider) ViewModel() string {
|
||||
return idpProviderTable
|
||||
}
|
||||
|
||||
func (i *IDPProvider) Subscription() *v1.Subscription {
|
||||
return i.subscription
|
||||
}
|
||||
|
||||
func (_ *IDPProvider) AggregateTypes() []models.AggregateType {
|
||||
return []es_models.AggregateType{instance.AggregateType, org.AggregateType}
|
||||
}
|
||||
|
||||
func (i *IDPProvider) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := i.view.GetLatestIDPProviderSequence(instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (i *IDPProvider) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) {
|
||||
sequences, err := i.view.GetLatestIDPProviderSequences(instanceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newSearchQuery(sequences, i.AggregateTypes(), instanceIDs), nil
|
||||
}
|
||||
|
||||
func (i *IDPProvider) Reduce(event *models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case instance.AggregateType, org.AggregateType:
|
||||
err = i.processIdpProvider(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *IDPProvider) processIdpProvider(event *models.Event) (err error) {
|
||||
provider := new(iam_view_model.IDPProviderView)
|
||||
switch eventstore.EventType(event.Type) {
|
||||
case instance.LoginPolicyIDPProviderAddedEventType, org.LoginPolicyIDPProviderAddedEventType:
|
||||
err = provider.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = i.fillData(provider)
|
||||
case instance.LoginPolicyIDPProviderRemovedEventType, instance.LoginPolicyIDPProviderCascadeRemovedEventType,
|
||||
org.LoginPolicyIDPProviderRemovedEventType, org.LoginPolicyIDPProviderCascadeRemovedEventType:
|
||||
err = provider.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.view.DeleteIDPProvider(event.AggregateID, provider.IDPConfigID, event.InstanceID, event)
|
||||
case instance.IDPConfigChangedEventType, org.IDPConfigChangedEventType:
|
||||
esConfig := new(iam_view_model.IDPConfigView)
|
||||
providerType := iam_model.IDPProviderTypeSystem
|
||||
if event.AggregateID != event.InstanceID {
|
||||
providerType = iam_model.IDPProviderTypeOrg
|
||||
}
|
||||
err = esConfig.AppendEvent(providerType, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providers, err := i.view.IDPProvidersByIDPConfigID(esConfig.IDPConfigID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := new(query2.IDP)
|
||||
if event.AggregateID == event.InstanceID {
|
||||
config, err = i.getDefaultIDPConfig(event.InstanceID, esConfig.IDPConfigID)
|
||||
} else {
|
||||
config, err = i.getOrgIDPConfig(event.InstanceID, event.AggregateID, esConfig.IDPConfigID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, provider := range providers {
|
||||
i.fillConfigData(provider, config)
|
||||
}
|
||||
return i.view.PutIDPProviders(event, providers...)
|
||||
case org.LoginPolicyRemovedEventType:
|
||||
return i.view.DeleteIDPProvidersByAggregateID(event.AggregateID, event.InstanceID, event)
|
||||
case instance.InstanceRemovedEventType:
|
||||
return i.view.DeleteInstanceIDPProviders(event)
|
||||
case org.OrgRemovedEventType:
|
||||
return i.view.UpdateOrgOwnerRemovedIDPProviders(event)
|
||||
default:
|
||||
return i.view.ProcessedIDPProviderSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.view.PutIDPProvider(provider, event)
|
||||
}
|
||||
|
||||
func (i *IDPProvider) fillData(provider *iam_view_model.IDPProviderView) (err error) {
|
||||
var config *query2.IDP
|
||||
if provider.IDPProviderType == int32(iam_model.IDPProviderTypeSystem) {
|
||||
config, err = i.getDefaultIDPConfig(provider.InstanceID, provider.IDPConfigID)
|
||||
} else {
|
||||
config, err = i.getOrgIDPConfig(provider.InstanceID, provider.AggregateID, provider.IDPConfigID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.fillConfigData(provider, config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IDPProvider) fillConfigData(provider *iam_view_model.IDPProviderView, config *query2.IDP) {
|
||||
provider.Name = config.Name
|
||||
provider.StylingType = int32(config.StylingType)
|
||||
if config.OIDCIDP != nil {
|
||||
provider.IDPConfigType = int32(domain.IDPConfigTypeOIDC)
|
||||
} else if config.JWTIDP != nil {
|
||||
provider.IDPConfigType = int32(domain.IDPConfigTypeJWT)
|
||||
}
|
||||
switch config.State {
|
||||
case domain.IDPConfigStateActive:
|
||||
provider.IDPState = int32(iam_model.IDPConfigStateActive)
|
||||
case domain.IDPConfigStateInactive:
|
||||
provider.IDPState = int32(iam_model.IDPConfigStateActive)
|
||||
case domain.IDPConfigStateRemoved:
|
||||
provider.IDPState = int32(iam_model.IDPConfigStateRemoved)
|
||||
default:
|
||||
provider.IDPState = int32(iam_model.IDPConfigStateActive)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IDPProvider) OnError(event *es_models.Event, err error) error {
|
||||
logging.WithFields("id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler")
|
||||
return spooler.HandleError(event, err, i.view.GetLatestIDPProviderFailedEvent, i.view.ProcessedIDPProviderFailedEvent, i.view.ProcessedIDPProviderSequence, i.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (i *IDPProvider) OnSuccess(instanceIDs []string) error {
|
||||
return spooler.HandleSuccess(i.view.UpdateIDPProviderSpoolerRunTimestamp, instanceIDs)
|
||||
}
|
||||
|
||||
func (i *IDPProvider) getOrgIDPConfig(instanceID, aggregateID, idpConfigID string) (*query2.IDP, error) {
|
||||
return i.queries.IDPByIDAndResourceOwner(withInstanceID(context.Background(), instanceID), false, idpConfigID, aggregateID, false)
|
||||
}
|
||||
|
||||
func (i *IDPProvider) getDefaultIDPConfig(instanceID, idpConfigID string) (*query2.IDP, error) {
|
||||
return i.queries.IDPByIDAndResourceOwner(withInstanceID(context.Background(), instanceID), false, idpConfigID, instanceID, false)
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/config/systemdefaults"
|
||||
caos_errs "github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
v1 "github.com/zitadel/zitadel/internal/eventstore/v1"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/query"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/spooler"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
iam_view_model "github.com/zitadel/zitadel/internal/iam/repository/view/model"
|
||||
query2 "github.com/zitadel/zitadel/internal/query"
|
||||
"github.com/zitadel/zitadel/internal/repository/instance"
|
||||
"github.com/zitadel/zitadel/internal/repository/org"
|
||||
"github.com/zitadel/zitadel/internal/repository/user"
|
||||
usr_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
const (
|
||||
externalIDPTable = "auth.user_external_idps2"
|
||||
)
|
||||
|
||||
type ExternalIDP struct {
|
||||
handler
|
||||
systemDefaults systemdefaults.SystemDefaults
|
||||
subscription *v1.Subscription
|
||||
queries *query2.Queries
|
||||
}
|
||||
|
||||
func newExternalIDP(
|
||||
ctx context.Context,
|
||||
handler handler,
|
||||
defaults systemdefaults.SystemDefaults,
|
||||
queries *query2.Queries,
|
||||
) *ExternalIDP {
|
||||
h := &ExternalIDP{
|
||||
handler: handler,
|
||||
systemDefaults: defaults,
|
||||
queries: queries,
|
||||
}
|
||||
|
||||
h.subscribe(ctx)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) subscribe(ctx context.Context) {
|
||||
i.subscription = i.es.Subscribe(i.AggregateTypes()...)
|
||||
go func() {
|
||||
for event := range i.subscription.Events {
|
||||
query.ReduceEvent(ctx, i, event)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) ViewModel() string {
|
||||
return externalIDPTable
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) Subscription() *v1.Subscription {
|
||||
return i.subscription
|
||||
}
|
||||
|
||||
func (_ *ExternalIDP) AggregateTypes() []es_models.AggregateType {
|
||||
return []es_models.AggregateType{user.AggregateType, instance.AggregateType, org.AggregateType}
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) CurrentSequence(instanceID string) (uint64, error) {
|
||||
sequence, err := i.view.GetLatestExternalIDPSequence(instanceID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sequence.CurrentSequence, nil
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) {
|
||||
sequences, err := i.view.GetLatestExternalIDPSequences(instanceIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newSearchQuery(sequences, i.AggregateTypes(), instanceIDs), nil
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) Reduce(event *es_models.Event) (err error) {
|
||||
switch event.AggregateType {
|
||||
case user.AggregateType:
|
||||
err = i.processUser(event)
|
||||
case instance.AggregateType, org.AggregateType:
|
||||
err = i.processIdpConfig(event)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) processUser(event *es_models.Event) (err error) {
|
||||
externalIDP := new(usr_view_model.ExternalIDPView)
|
||||
switch eventstore.EventType(event.Type) {
|
||||
case user.UserIDPLinkAddedType:
|
||||
err = externalIDP.AppendEvent(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = i.fillData(externalIDP)
|
||||
case user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType:
|
||||
err = externalIDP.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.view.DeleteExternalIDP(externalIDP.ExternalUserID, externalIDP.IDPConfigID, event.InstanceID, event)
|
||||
case user.UserRemovedType:
|
||||
return i.view.DeleteExternalIDPsByUserID(event.AggregateID, event.InstanceID, event)
|
||||
default:
|
||||
return i.view.ProcessedExternalIDPSequence(event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return i.view.PutExternalIDP(externalIDP, event)
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) processIdpConfig(event *es_models.Event) (err error) {
|
||||
switch eventstore.EventType(event.Type) {
|
||||
case instance.IDPConfigChangedEventType, org.IDPConfigChangedEventType:
|
||||
configView := new(iam_view_model.IDPConfigView)
|
||||
var config *query2.IDP
|
||||
if eventstore.EventType(event.Type) == instance.IDPConfigChangedEventType {
|
||||
err = configView.AppendEvent(iam_model.IDPProviderTypeSystem, event)
|
||||
} else {
|
||||
err = configView.AppendEvent(iam_model.IDPProviderTypeOrg, event)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exterinalIDPs, err := i.view.ExternalIDPsByIDPConfigID(configView.IDPConfigID, event.InstanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if event.AggregateType == instance.AggregateType {
|
||||
config, err = i.getDefaultIDPConfig(event.InstanceID, configView.IDPConfigID)
|
||||
} else {
|
||||
config, err = i.getOrgIDPConfig(event.InstanceID, event.AggregateID, configView.IDPConfigID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, provider := range exterinalIDPs {
|
||||
i.fillConfigData(provider, config)
|
||||
}
|
||||
return i.view.PutExternalIDPs(event, exterinalIDPs...)
|
||||
case instance.InstanceRemovedEventType:
|
||||
return i.view.DeleteInstanceExternalIDPs(event)
|
||||
case org.OrgRemovedEventType:
|
||||
return i.view.UpdateOrgOwnerRemovedExternalIDPs(event)
|
||||
default:
|
||||
return i.view.ProcessedExternalIDPSequence(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) fillData(externalIDP *usr_view_model.ExternalIDPView) error {
|
||||
config, err := i.getOrgIDPConfig(externalIDP.InstanceID, externalIDP.ResourceOwner, externalIDP.IDPConfigID)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
config, err = i.getDefaultIDPConfig(externalIDP.InstanceID, externalIDP.IDPConfigID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.fillConfigData(externalIDP, config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) fillConfigData(externalIDP *usr_view_model.ExternalIDPView, config *query2.IDP) {
|
||||
externalIDP.IDPName = config.Name
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) OnError(event *es_models.Event, err error) error {
|
||||
logging.WithFields("id", event.AggregateID).WithError(err).Warn("something went wrong in idp provider handler")
|
||||
return spooler.HandleError(event, err, i.view.GetLatestExternalIDPFailedEvent, i.view.ProcessedExternalIDPFailedEvent, i.view.ProcessedExternalIDPSequence, i.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) OnSuccess(instanceIDs []string) error {
|
||||
return spooler.HandleSuccess(i.view.UpdateExternalIDPSpoolerRunTimestamp, instanceIDs)
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) getOrgIDPConfig(instanceID, aggregateID, idpConfigID string) (*query2.IDP, error) {
|
||||
return i.queries.IDPByIDAndResourceOwner(withInstanceID(context.Background(), instanceID), false, idpConfigID, aggregateID, false)
|
||||
}
|
||||
|
||||
func (i *ExternalIDP) getDefaultIDPConfig(instanceID, idpConfigID string) (*query2.IDP, error) {
|
||||
return i.queries.IDPByIDAndResourceOwner(withInstanceID(context.Background(), instanceID), false, idpConfigID, instanceID, false)
|
||||
}
|
@ -80,7 +80,7 @@ func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, c
|
||||
UserViewProvider: view,
|
||||
UserCommandProvider: command,
|
||||
UserEventProvider: &userRepo,
|
||||
IDPProviderViewProvider: view,
|
||||
IDPProviderViewProvider: queries,
|
||||
IDPUserLinksProvider: queries,
|
||||
LockoutPolicyViewProvider: queries,
|
||||
LoginPolicyViewProvider: queries,
|
||||
|
@ -1,97 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view"
|
||||
"github.com/zitadel/zitadel/internal/user/repository/view/model"
|
||||
global_view "github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
externalIDPTable = "auth.user_external_idps2"
|
||||
)
|
||||
|
||||
func (v *View) ExternalIDPByExternalUserIDAndIDPConfigID(externalUserID, idpConfigID, instanceID string) (*model.ExternalIDPView, error) {
|
||||
return view.ExternalIDPByExternalUserIDAndIDPConfigID(v.Db, externalIDPTable, externalUserID, idpConfigID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(externalUserID, idpConfigID, resourceOwner, instanceID string) (*model.ExternalIDPView, error) {
|
||||
return view.ExternalIDPByExternalUserIDAndIDPConfigIDAndResourceOwner(v.Db, externalIDPTable, externalUserID, idpConfigID, resourceOwner, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) ExternalIDPsByIDPConfigID(idpConfigID, instanceID string) ([]*model.ExternalIDPView, error) {
|
||||
return view.ExternalIDPsByIDPConfigID(v.Db, externalIDPTable, idpConfigID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) PutExternalIDP(externalIDP *model.ExternalIDPView, event *models.Event) error {
|
||||
err := view.PutExternalIDP(v.Db, externalIDPTable, externalIDP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedExternalIDPSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) PutExternalIDPs(event *models.Event, externalIDPs ...*model.ExternalIDPView) error {
|
||||
err := view.PutExternalIDPs(v.Db, externalIDPTable, externalIDPs...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedExternalIDPSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) DeleteExternalIDP(externalUserID, idpConfigID, instanceID string, event *models.Event) error {
|
||||
err := view.DeleteExternalIDP(v.Db, externalIDPTable, externalUserID, idpConfigID, instanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedExternalIDPSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) DeleteExternalIDPsByUserID(userID, instanceID string, event *models.Event) error {
|
||||
err := view.DeleteExternalIDPsByUserID(v.Db, externalIDPTable, userID, instanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedExternalIDPSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) DeleteInstanceExternalIDPs(event *models.Event) error {
|
||||
err := view.DeleteInstanceExternalIDPs(v.Db, externalIDPTable, event.InstanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedExternalIDPSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateOrgOwnerRemovedExternalIDPs(event *models.Event) error {
|
||||
err := view.UpdateOrgOwnerRemovedExternalIDPs(v.Db, externalIDPTable, event.InstanceID, event.ResourceOwner)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedExternalIDPSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestExternalIDPSequence(instanceID string) (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(externalIDPTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestExternalIDPSequences(instanceIDs []string) ([]*global_view.CurrentSequence, error) {
|
||||
return v.latestSequences(externalIDPTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedExternalIDPSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(externalIDPTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateExternalIDPSpoolerRunTimestamp(instanceIDs []string) error {
|
||||
return v.updateSpoolerRunSequence(externalIDPTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestExternalIDPFailedEvent(sequence uint64, instanceID string) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(externalIDPTable, instanceID, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedExternalIDPFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
"github.com/zitadel/zitadel/internal/iam/repository/view"
|
||||
iam_es_model "github.com/zitadel/zitadel/internal/iam/repository/view/model"
|
||||
global_view "github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
idpConfigTable = "auth.idp_configs2"
|
||||
)
|
||||
|
||||
func (v *View) IDPConfigByID(idpID, instanceID string) (*iam_es_model.IDPConfigView, error) {
|
||||
return view.IDPByID(v.Db, idpConfigTable, idpID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetIDPConfigsByAggregateID(aggregateID, instanceID string) ([]*iam_es_model.IDPConfigView, error) {
|
||||
return view.GetIDPConfigsByAggregateID(v.Db, idpConfigTable, aggregateID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) SearchIDPConfigs(request *iam_model.IDPConfigSearchRequest) ([]*iam_es_model.IDPConfigView, uint64, error) {
|
||||
return view.SearchIDPs(v.Db, idpConfigTable, request)
|
||||
}
|
||||
|
||||
func (v *View) PutIDPConfig(idp *iam_es_model.IDPConfigView, event *models.Event) error {
|
||||
err := view.PutIDP(v.Db, idpConfigTable, idp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPConfigSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) DeleteIDPConfig(idpID string, event *models.Event) error {
|
||||
err := view.DeleteIDP(v.Db, idpConfigTable, idpID, event.InstanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPConfigSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) DeleteInstanceIDPs(event *models.Event) error {
|
||||
err := view.DeleteInstanceIDPs(v.Db, idpConfigTable, event.InstanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPConfigSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateOrgOwnerRemovedIDPs(event *models.Event) error {
|
||||
err := view.UpdateOrgOwnerRemovedIDPs(v.Db, idpConfigTable, event.InstanceID, event.AggregateID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPConfigSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestIDPConfigSequence(instanceID string) (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(idpConfigTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestIDPConfigSequences(instanceIDs []string) ([]*global_view.CurrentSequence, error) {
|
||||
return v.latestSequences(idpConfigTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedIDPConfigSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(idpConfigTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateIDPConfigSpoolerRunTimestamp(instanceIDs []string) error {
|
||||
return v.updateSpoolerRunSequence(idpConfigTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestIDPConfigFailedEvent(sequence uint64, instanceID string) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(idpConfigTable, instanceID, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedIDPConfigFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/errors"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
iam_model "github.com/zitadel/zitadel/internal/iam/model"
|
||||
"github.com/zitadel/zitadel/internal/iam/repository/view"
|
||||
"github.com/zitadel/zitadel/internal/iam/repository/view/model"
|
||||
global_view "github.com/zitadel/zitadel/internal/view/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
idpProviderTable = "auth.idp_providers2"
|
||||
)
|
||||
|
||||
func (v *View) IDPProviderByAggregateAndIDPConfigID(aggregateID, idpConfigID, instanceID string) (*model.IDPProviderView, error) {
|
||||
return view.GetIDPProviderByAggregateIDAndConfigID(v.Db, idpProviderTable, aggregateID, idpConfigID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) IDPProvidersByIDPConfigID(idpConfigID, instanceID string) ([]*model.IDPProviderView, error) {
|
||||
return view.IDPProvidersByIdpConfigID(v.Db, idpProviderTable, idpConfigID, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) IDPProvidersByAggregateIDAndState(aggregateID, instanceID string, idpConfigState iam_model.IDPConfigState) ([]*model.IDPProviderView, error) {
|
||||
return view.IDPProvidersByAggregateIDAndState(v.Db, idpProviderTable, aggregateID, instanceID, idpConfigState)
|
||||
}
|
||||
|
||||
func (v *View) SearchIDPProviders(request *iam_model.IDPProviderSearchRequest) ([]*model.IDPProviderView, uint64, error) {
|
||||
return view.SearchIDPProviders(v.Db, idpProviderTable, request)
|
||||
}
|
||||
|
||||
func (v *View) PutIDPProvider(provider *model.IDPProviderView, event *models.Event) error {
|
||||
err := view.PutIDPProvider(v.Db, idpProviderTable, provider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPProviderSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) PutIDPProviders(event *models.Event, providers ...*model.IDPProviderView) error {
|
||||
err := view.PutIDPProviders(v.Db, idpProviderTable, providers...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPProviderSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) DeleteIDPProvider(aggregateID, idpConfigID, instanceID string, event *models.Event) error {
|
||||
err := view.DeleteIDPProvider(v.Db, idpProviderTable, aggregateID, idpConfigID, instanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPProviderSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) DeleteIDPProvidersByAggregateID(aggregateID, instanceID string, event *models.Event) error {
|
||||
err := view.DeleteIDPProvidersByAggregateID(v.Db, idpProviderTable, aggregateID, instanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPProviderSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) DeleteInstanceIDPProviders(event *models.Event) error {
|
||||
err := view.DeleteInstanceIDPProviders(v.Db, idpProviderTable, event.InstanceID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPProviderSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateOrgOwnerRemovedIDPProviders(event *models.Event) error {
|
||||
err := view.UpdateOrgOwnerRemovedIDPProviders(v.Db, idpProviderTable, event.InstanceID, event.AggregateID)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedIDPProviderSequence(event)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestIDPProviderSequence(instanceID string) (*global_view.CurrentSequence, error) {
|
||||
return v.latestSequence(idpProviderTable, instanceID)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestIDPProviderSequences(instanceIDs []string) ([]*global_view.CurrentSequence, error) {
|
||||
return v.latestSequences(idpProviderTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedIDPProviderSequence(event *models.Event) error {
|
||||
return v.saveCurrentSequence(idpProviderTable, event)
|
||||
}
|
||||
|
||||
func (v *View) UpdateIDPProviderSpoolerRunTimestamp(instanceIDs []string) error {
|
||||
return v.updateSpoolerRunSequence(idpProviderTable, instanceIDs)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestIDPProviderFailedEvent(sequence uint64, instanceID string) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(idpProviderTable, instanceID, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedIDPProviderFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
type OrgRepository interface {
|
||||
GetIDPConfigByID(ctx context.Context, idpConfigID string) (*iam_model.IDPConfigView, error)
|
||||
GetMyPasswordComplexityPolicy(ctx context.Context) (*iam_model.PasswordComplexityPolicyView, error)
|
||||
GetLoginText(ctx context.Context, orgID string) ([]*domain.CustomText, error)
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
package command
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/repository/idp"
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
"github.com/zitadel/zitadel/internal/command/preparation"
|
||||
"github.com/zitadel/zitadel/internal/repository/idp"
|
||||
)
|
||||
|
||||
type GenericOAuthProvider struct {
|
||||
Name string
|
||||
@ -52,3 +58,34 @@ type LDAPProvider struct {
|
||||
LDAPAttributes idp.LDAPAttributes
|
||||
IDPOptions idp.Options
|
||||
}
|
||||
|
||||
func ExistsIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) {
|
||||
writeModel := NewOrgIDPRemoveWriteModel(orgID, id)
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(events) > 0 {
|
||||
writeModel.AppendEvents(events...)
|
||||
if err := writeModel.Reduce(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return writeModel.State.Exists(), nil
|
||||
}
|
||||
|
||||
instanceWriteModel := NewInstanceIDPRemoveWriteModel(authz.GetInstance(ctx).InstanceID(), id)
|
||||
events, err = filter(ctx, instanceWriteModel.Query())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(events) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
instanceWriteModel.AppendEvents(events...)
|
||||
if err := instanceWriteModel.Reduce(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return instanceWriteModel.State.Exists(), nil
|
||||
}
|
||||
|
@ -236,6 +236,23 @@ func (c *Commands) DeleteInstanceProvider(ctx context.Context, id string) (*doma
|
||||
return pushedEventsToObjectDetails(pushedEvents), nil
|
||||
}
|
||||
|
||||
func ExistsInstanceIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id string) (exists bool, err error) {
|
||||
instanceWriteModel := NewInstanceIDPRemoveWriteModel(authz.GetInstance(ctx).InstanceID(), id)
|
||||
events, err := filter(ctx, instanceWriteModel.Query())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(events) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
instanceWriteModel.AppendEvents(events...)
|
||||
if err := instanceWriteModel.Reduce(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return instanceWriteModel.State.Exists(), nil
|
||||
}
|
||||
|
||||
func (c *Commands) prepareAddInstanceOAuthProvider(a *instance.Aggregate, writeModel *InstanceOAuthIDPWriteModel, provider GenericOAuthProvider) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
|
||||
|
@ -41,8 +41,8 @@ func (c *Commands) AddIDPProviderToDefaultLoginPolicy(ctx context.Context, idpPr
|
||||
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-GVDfe", "Errors.IAM.LoginPolicy.NotFound")
|
||||
}
|
||||
|
||||
_, err = c.getInstanceIDPConfigByID(ctx, idpProvider.IDPConfigID)
|
||||
if err != nil {
|
||||
exists, err := ExistsInstanceIDP(ctx, c.eventstore.Filter, idpProvider.IDPConfigID)
|
||||
if err != nil || !exists {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "INSTANCE-m8fsd", "Errors.IDPConfig.NotExisting")
|
||||
}
|
||||
idpModel := NewInstanceIdentityProviderWriteModel(ctx, idpProvider.IDPConfigID)
|
||||
|
@ -225,6 +225,23 @@ func (c *Commands) DeleteOrgProvider(ctx context.Context, resourceOwner, id stri
|
||||
return pushedEventsToObjectDetails(pushedEvents), nil
|
||||
}
|
||||
|
||||
func ExistsOrgIDP(ctx context.Context, filter preparation.FilterToQueryReducer, id, orgID string) (exists bool, err error) {
|
||||
writeModel := NewOrgIDPRemoveWriteModel(orgID, id)
|
||||
events, err := filter(ctx, writeModel.Query())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(events) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
writeModel.AppendEvents(events...)
|
||||
if err := writeModel.Reduce(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return writeModel.State.Exists(), nil
|
||||
}
|
||||
|
||||
func (c *Commands) prepareAddOrgOAuthProvider(a *org.Aggregate, writeModel *OrgOAuthIDPWriteModel, provider GenericOAuthProvider) preparation.Validation {
|
||||
return func() (preparation.CreateCommands, error) {
|
||||
if provider.Name = strings.TrimSpace(provider.Name); provider.Name == "" {
|
||||
|
@ -146,12 +146,13 @@ func (c *Commands) AddIDPToLoginPolicy(ctx context.Context, resourceOwner string
|
||||
return nil, caos_errs.ThrowNotFound(nil, "Org-Ffgw2", "Errors.Org.LoginPolicy.NotFound")
|
||||
}
|
||||
|
||||
var exists bool
|
||||
if idpProvider.Type == domain.IdentityProviderTypeOrg {
|
||||
_, err = c.getOrgIDPConfigByID(ctx, idpProvider.IDPConfigID, resourceOwner)
|
||||
exists, err = ExistsOrgIDP(ctx, c.eventstore.Filter, idpProvider.IDPConfigID, resourceOwner)
|
||||
} else {
|
||||
_, err = c.getInstanceIDPConfigByID(ctx, idpProvider.IDPConfigID)
|
||||
exists, err = ExistsInstanceIDP(ctx, c.eventstore.Filter, idpProvider.IDPConfigID)
|
||||
}
|
||||
if err != nil {
|
||||
if !exists || err != nil {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "Org-3N9fs", "Errors.IDPConfig.NotExisting")
|
||||
}
|
||||
idpModel := NewOrgIdentityProviderWriteModel(resourceOwner, idpProvider.IDPConfigID)
|
||||
|
@ -66,14 +66,12 @@ func (c *Commands) addUserIDPLink(ctx context.Context, human *eventstore.Aggrega
|
||||
return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-6m9Kd", "Errors.User.ExternalIDP.Invalid")
|
||||
}
|
||||
|
||||
_, err := c.getOrgIDPConfigByID(ctx, link.IDPConfigID, human.ResourceOwner)
|
||||
if caos_errs.IsNotFound(err) {
|
||||
_, err = c.getInstanceIDPConfigByID(ctx, link.IDPConfigID)
|
||||
}
|
||||
if err != nil {
|
||||
exists, err := ExistsIDP(ctx, c.eventstore.Filter, link.IDPConfigID, human.ResourceOwner)
|
||||
if !exists || err != nil {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(err, "COMMAND-39nfs", "Errors.IDPConfig.NotExisting")
|
||||
}
|
||||
return user.NewUserIDPLinkAddedEvent(ctx, human, link.IDPConfigID, link.DisplayName, link.ExternalUserID), nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Commands) RemoveUserIDPLink(ctx context.Context, link *domain.UserIDPLink) (*domain.ObjectDetails, error) {
|
||||
|
@ -34,3 +34,12 @@ const (
|
||||
IDPTypeGitLabSelfHosted
|
||||
IDPTypeGoogle
|
||||
)
|
||||
|
||||
func (t IDPType) GetCSSClass() string {
|
||||
switch t { //nolint:exhaustive
|
||||
case IDPTypeGoogle:
|
||||
return "google"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,8 @@ type JWTIDPConfig struct {
|
||||
HeaderName string
|
||||
}
|
||||
|
||||
// IDPConfigType
|
||||
// Deprecated: use [IDPType]
|
||||
type IDPConfigType int32
|
||||
|
||||
const (
|
||||
@ -85,6 +87,8 @@ func (f IDPConfigType) Valid() bool {
|
||||
return f >= 0 && f < idpConfigTypeCount
|
||||
}
|
||||
|
||||
// IDPConfigState
|
||||
// Deprecated: use [IDPStateType]
|
||||
type IDPConfigState int32
|
||||
|
||||
const (
|
||||
@ -104,6 +108,8 @@ func (s IDPConfigState) Exists() bool {
|
||||
return s != IDPConfigStateUnspecified && s != IDPConfigStateRemoved
|
||||
}
|
||||
|
||||
// IDPConfigStylingType
|
||||
// Deprecated: use a concrete provider
|
||||
type IDPConfigStylingType int32
|
||||
|
||||
const (
|
||||
|
@ -55,10 +55,10 @@ type IDPProvider struct {
|
||||
Type IdentityProviderType
|
||||
IDPConfigID string
|
||||
|
||||
Name string
|
||||
StylingType IDPConfigStylingType
|
||||
IDPConfigType IDPConfigType
|
||||
IDPState IDPConfigState
|
||||
Name string
|
||||
StylingType IDPConfigStylingType // deprecated
|
||||
IDPType IDPType
|
||||
IDPState IDPConfigState
|
||||
}
|
||||
|
||||
func (p IDPProvider) IsValid() bool {
|
||||
|
@ -68,65 +68,3 @@ func (r *IDPProviderSearchRequest) EnsureLimit(limit uint64) error {
|
||||
func (r *IDPProviderSearchRequest) AppendAggregateIDQuery(aggregateID string) {
|
||||
r.Queries = append(r.Queries, &IDPProviderSearchQuery{Key: IDPProviderSearchKeyAggregateID, Method: domain.SearchMethodEquals, Value: aggregateID})
|
||||
}
|
||||
|
||||
func IdpProviderViewsToDomain(idpProviders []*IDPProviderView) []*domain.IDPProvider {
|
||||
providers := make([]*domain.IDPProvider, len(idpProviders))
|
||||
for i, provider := range idpProviders {
|
||||
p := &domain.IDPProvider{
|
||||
IDPConfigID: provider.IDPConfigID,
|
||||
Type: idpProviderTypeToDomain(provider.IDPProviderType),
|
||||
Name: provider.Name,
|
||||
IDPConfigType: idpConfigTypeToDomain(provider.IDPConfigType),
|
||||
StylingType: idpStylingTypeToDomain(provider.StylingType),
|
||||
IDPState: idpStateToDomain(provider.IDPState),
|
||||
}
|
||||
providers[i] = p
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
func idpProviderTypeToDomain(idpType IDPProviderType) domain.IdentityProviderType {
|
||||
switch idpType {
|
||||
case IDPProviderTypeSystem:
|
||||
return domain.IdentityProviderTypeSystem
|
||||
case IDPProviderTypeOrg:
|
||||
return domain.IdentityProviderTypeOrg
|
||||
default:
|
||||
return domain.IdentityProviderTypeSystem
|
||||
}
|
||||
}
|
||||
|
||||
func idpConfigTypeToDomain(idpType IdpConfigType) domain.IDPConfigType {
|
||||
switch idpType {
|
||||
case IDPConfigTypeOIDC:
|
||||
return domain.IDPConfigTypeOIDC
|
||||
case IDPConfigTypeSAML:
|
||||
return domain.IDPConfigTypeSAML
|
||||
case IDPConfigTypeJWT:
|
||||
return domain.IDPConfigTypeJWT
|
||||
default:
|
||||
return domain.IDPConfigTypeOIDC
|
||||
}
|
||||
}
|
||||
|
||||
func idpStylingTypeToDomain(stylingType IDPStylingType) domain.IDPConfigStylingType {
|
||||
switch stylingType {
|
||||
case IDPStylingTypeGoogle:
|
||||
return domain.IDPConfigStylingTypeGoogle
|
||||
default:
|
||||
return domain.IDPConfigStylingTypeUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
func idpStateToDomain(state IDPConfigState) domain.IDPConfigState {
|
||||
switch state {
|
||||
case IDPConfigStateActive:
|
||||
return domain.IDPConfigStateActive
|
||||
case IDPConfigStateInactive:
|
||||
return domain.IDPConfigStateInactive
|
||||
case IDPConfigStateRemoved:
|
||||
return domain.IDPConfigStateRemoved
|
||||
default:
|
||||
return domain.IDPConfigStateActive
|
||||
}
|
||||
}
|
||||
|
@ -18,14 +18,14 @@ type Provider struct {
|
||||
}
|
||||
|
||||
// New creates a GitLab.com provider using the [oidc.Provider] (OIDC generic provider)
|
||||
func New(clientID, clientSecret, redirectURI string, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
return NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI, options...)
|
||||
func New(clientID, clientSecret, redirectURI string, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
return NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI, scopes, options...)
|
||||
}
|
||||
|
||||
// NewCustomIssuer creates a GitLab provider using the [oidc.Provider] (OIDC generic provider)
|
||||
// with a custom issuer for self-managed instances
|
||||
func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, oidc.DefaultMapper, options...)
|
||||
func NewCustomIssuer(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, options ...oidc.ProviderOpts) (*Provider, error) {
|
||||
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, oidc.DefaultMapper, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
opts []oidc.ProviderOpts
|
||||
}
|
||||
tests := []struct {
|
||||
@ -29,6 +30,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
},
|
||||
want: &oidc.Session{
|
||||
AuthURL: "https://gitlab.com/oauth/authorize?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState",
|
||||
@ -40,7 +42,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.opts...)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.opts...)
|
||||
r.NoError(err)
|
||||
|
||||
session, err := provider.BeginAuth(context.Background(), "testState")
|
||||
|
@ -22,6 +22,7 @@ func TestProvider_FetchUser(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
httpMock func()
|
||||
authURL string
|
||||
code string
|
||||
@ -55,6 +56,7 @@ func TestProvider_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
httpMock: func() {
|
||||
gock.New("https://gitlab.com/oauth").
|
||||
Get("/userinfo").
|
||||
@ -74,6 +76,7 @@ func TestProvider_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
httpMock: func() {
|
||||
gock.New("https://gitlab.com/oauth").
|
||||
Get("/userinfo").
|
||||
@ -110,6 +113,7 @@ func TestProvider_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
httpMock: func() {
|
||||
gock.New("https://gitlab.com/oauth").
|
||||
Get("/userinfo").
|
||||
@ -161,7 +165,7 @@ func TestProvider_FetchUser(t *testing.T) {
|
||||
|
||||
// call the real discovery endpoint
|
||||
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.options...)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.options...)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &oidc.Session{
|
||||
|
@ -20,8 +20,8 @@ type Provider struct {
|
||||
}
|
||||
|
||||
// New creates a Google provider using the [oidc.Provider] (OIDC generic provider)
|
||||
func New(clientID, clientSecret, redirectURI string, opts ...oidc.ProviderOpts) (*Provider, error) {
|
||||
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, userMapper, opts...)
|
||||
func New(clientID, clientSecret, redirectURI string, scopes []string, opts ...oidc.ProviderOpts) (*Provider, error) {
|
||||
rp, err := oidc.New(name, issuer, clientID, clientSecret, redirectURI, scopes, userMapper, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@ -28,6 +29,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
},
|
||||
want: &oidc.Session{
|
||||
AuthURL: "https://accounts.google.com/o/oauth2/v2/auth?client_id=clientID&redirect_uri=redirectURI&response_type=code&scope=openid&state=testState",
|
||||
@ -39,7 +41,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
|
||||
r.NoError(err)
|
||||
|
||||
session, err := provider.BeginAuth(context.Background(), "testState")
|
||||
|
@ -22,6 +22,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
httpMock func()
|
||||
authURL string
|
||||
code string
|
||||
@ -55,6 +56,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
httpMock: func() {
|
||||
gock.New("https://openidconnect.googleapis.com").
|
||||
Get("/v1/userinfo").
|
||||
@ -74,6 +76,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
httpMock: func() {
|
||||
gock.New("https://openidconnect.googleapis.com").
|
||||
Get("/v1/userinfo").
|
||||
@ -110,6 +113,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
httpMock: func() {
|
||||
gock.New("https://openidconnect.googleapis.com").
|
||||
Get("/v1/userinfo").
|
||||
@ -162,7 +166,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
|
||||
// call the real discovery endpoint
|
||||
gock.New(issuer).Get(openid.DiscoveryEndpoint).EnableNetworking()
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI)
|
||||
provider, err := New(tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &oidc.Session{
|
||||
|
@ -18,7 +18,6 @@ const (
|
||||
var _ idp.Provider = (*Provider)(nil)
|
||||
|
||||
var (
|
||||
ErrNoTokens = errors.New("no tokens provided")
|
||||
ErrMissingUserAgentID = errors.New("userAgentID missing")
|
||||
)
|
||||
|
||||
|
@ -2,7 +2,13 @@ package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v2/pkg/client/rp"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
@ -11,8 +17,14 @@ import (
|
||||
|
||||
var _ idp.Session = (*Session)(nil)
|
||||
|
||||
var (
|
||||
ErrNoTokens = errors.New("no tokens provided")
|
||||
ErrInvalidToken = errors.New("invalid tokens provided")
|
||||
)
|
||||
|
||||
// Session is the [idp.Session] implementation for the JWT provider
|
||||
type Session struct {
|
||||
*Provider
|
||||
AuthURL string
|
||||
Tokens *oidc.Tokens
|
||||
}
|
||||
@ -28,9 +40,48 @@ func (s *Session) FetchUser(ctx context.Context) (user idp.User, err error) {
|
||||
if s.Tokens == nil {
|
||||
return nil, ErrNoTokens
|
||||
}
|
||||
s.Tokens.IDTokenClaims, err = s.validateToken(ctx, s.Tokens.IDToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &User{s.Tokens.IDTokenClaims}, nil
|
||||
}
|
||||
|
||||
func (s *Session) validateToken(ctx context.Context, token string) (oidc.IDTokenClaims, error) {
|
||||
logging.Debug("begin token validation")
|
||||
// TODO: be able to specify them in the template: https://github.com/zitadel/zitadel/issues/5322
|
||||
offset := 3 * time.Second
|
||||
maxAge := time.Hour
|
||||
claims := oidc.EmptyIDTokenClaims()
|
||||
payload, err := oidc.ParseToken(token, claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: malformed jwt payload: %v", ErrInvalidToken, err)
|
||||
}
|
||||
|
||||
if err = oidc.CheckIssuer(claims, s.Provider.issuer); err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid issuer: %v", ErrInvalidToken, err)
|
||||
}
|
||||
|
||||
logging.Debug("begin signature validation")
|
||||
keySet := rp.NewRemoteKeySet(http.DefaultClient, s.Provider.keysEndpoint)
|
||||
if err = oidc.CheckSignature(ctx, token, payload, claims, nil, keySet); err != nil {
|
||||
return nil, fmt.Errorf("%w: invalid signature: %v", ErrInvalidToken, err)
|
||||
}
|
||||
|
||||
if !claims.GetExpiration().IsZero() {
|
||||
if err = oidc.CheckExpiration(claims, offset); err != nil {
|
||||
return nil, fmt.Errorf("%w: expired: %v", ErrInvalidToken, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !claims.GetIssuedAt().IsZero() {
|
||||
if err = oidc.CheckIssuedAt(claims, maxAge, offset); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", ErrInvalidToken, err)
|
||||
}
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
type User struct {
|
||||
oidc.IDTokenClaims
|
||||
}
|
||||
|
@ -2,25 +2,37 @@ package jwt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/h2non/gock"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/zitadel/oidc/v2/pkg/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/idp"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
)
|
||||
|
||||
func TestSession_FetchUser(t *testing.T) {
|
||||
type fields struct {
|
||||
authURL string
|
||||
tokens *oidc.Tokens
|
||||
name string
|
||||
issuer string
|
||||
jwtEndpoint string
|
||||
keysEndpoint string
|
||||
headerName string
|
||||
encryptionAlg func(t *testing.T) crypto.EncryptionAlgorithm
|
||||
httpMock func(issuer string)
|
||||
authURL string
|
||||
tokens *oidc.Tokens
|
||||
}
|
||||
type want struct {
|
||||
err func(error) bool
|
||||
user idp.User
|
||||
id string
|
||||
firstName string
|
||||
lastName string
|
||||
@ -41,8 +53,22 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "no tokens",
|
||||
fields: fields{},
|
||||
name: "no tokens",
|
||||
fields: fields{
|
||||
issuer: "https://jwt.com",
|
||||
jwtEndpoint: "https://auth.com/jwt",
|
||||
keysEndpoint: "https://jwt.com/keys",
|
||||
headerName: "jwt-header",
|
||||
encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm {
|
||||
return crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
},
|
||||
httpMock: func(issuer string) {
|
||||
gock.New(issuer).
|
||||
Get("/keys").
|
||||
Reply(200).
|
||||
JSON(keys(t))
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, ErrNoTokens)
|
||||
@ -50,11 +76,53 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful fetch",
|
||||
name: "invalid token",
|
||||
fields: fields{
|
||||
issuer: "https://jwt.com",
|
||||
jwtEndpoint: "https://auth.com/jwt",
|
||||
keysEndpoint: "https://jwt.com/keys",
|
||||
headerName: "jwt-header",
|
||||
encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm {
|
||||
return crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
},
|
||||
httpMock: func(issuer string) {
|
||||
gock.New(issuer).
|
||||
Get("/keys").
|
||||
Reply(200).
|
||||
JSON(keys(t))
|
||||
},
|
||||
authURL: "https://auth.com/jwt?authRequestID=testState",
|
||||
tokens: &oidc.Tokens{
|
||||
Token: &oauth2.Token{},
|
||||
Token: &oauth2.Token{},
|
||||
IDToken: "invalidToken",
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: func(err error) bool {
|
||||
return errors.Is(err, ErrInvalidToken)
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful fetch",
|
||||
fields: fields{
|
||||
issuer: "https://jwt.com",
|
||||
jwtEndpoint: "https://auth.com/jwt",
|
||||
keysEndpoint: "https://jwt.com/keys",
|
||||
headerName: "jwt-header",
|
||||
encryptionAlg: func(t *testing.T) crypto.EncryptionAlgorithm {
|
||||
return crypto.CreateMockEncryptionAlg(gomock.NewController(t))
|
||||
},
|
||||
httpMock: func(issuer string) {
|
||||
gock.New(issuer).
|
||||
Get("/keys").
|
||||
Reply(200).
|
||||
JSON(keys(t))
|
||||
},
|
||||
authURL: "https://auth.com/jwt?authRequestID=testState",
|
||||
tokens: &oidc.Tokens{
|
||||
Token: &oauth2.Token{},
|
||||
IDToken: idToken(t, "https://jwt.com"),
|
||||
IDTokenClaims: func() oidc.IDTokenClaims {
|
||||
claims := oidc.EmptyIDTokenClaims()
|
||||
userinfo := oidc.NewUserInfo()
|
||||
@ -75,25 +143,6 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
user: &User{
|
||||
IDTokenClaims: func() oidc.IDTokenClaims {
|
||||
claims := oidc.EmptyIDTokenClaims()
|
||||
userinfo := oidc.NewUserInfo()
|
||||
userinfo.SetSubject("sub")
|
||||
userinfo.SetPicture("picture")
|
||||
userinfo.SetName("firstname lastname")
|
||||
userinfo.SetEmail("email", true)
|
||||
userinfo.SetGivenName("firstname")
|
||||
userinfo.SetFamilyName("lastname")
|
||||
userinfo.SetNickname("nickname")
|
||||
userinfo.SetPreferredUsername("username")
|
||||
userinfo.SetProfile("profile")
|
||||
userinfo.SetPhone("phone", true)
|
||||
userinfo.SetLocale(language.English)
|
||||
claims.SetUserinfo(userinfo)
|
||||
return claims
|
||||
}(),
|
||||
},
|
||||
id: "sub",
|
||||
firstName: "firstname",
|
||||
lastName: "lastname",
|
||||
@ -112,11 +161,24 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer gock.Off()
|
||||
tt.fields.httpMock(tt.fields.issuer)
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(
|
||||
tt.fields.name,
|
||||
tt.fields.issuer,
|
||||
tt.fields.jwtEndpoint,
|
||||
tt.fields.keysEndpoint,
|
||||
tt.fields.headerName,
|
||||
tt.fields.encryptionAlg(t),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &Session{
|
||||
AuthURL: tt.fields.authURL,
|
||||
Tokens: tt.fields.tokens,
|
||||
Provider: provider,
|
||||
AuthURL: tt.fields.authURL,
|
||||
Tokens: tt.fields.tokens,
|
||||
}
|
||||
|
||||
user, err := session.FetchUser(context.Background())
|
||||
@ -125,7 +187,6 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
}
|
||||
if tt.want.err == nil {
|
||||
a.NoError(err)
|
||||
a.Equal(tt.want.user, user)
|
||||
a.Equal(tt.want.id, user.GetID())
|
||||
a.Equal(tt.want.firstName, user.GetFirstName())
|
||||
a.Equal(tt.want.lastName, user.GetLastName())
|
||||
@ -143,3 +204,96 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func idToken(t *testing.T, issuer string) string {
|
||||
claims := oidc.NewIDTokenClaims(
|
||||
issuer,
|
||||
"sub",
|
||||
[]string{"clientID"},
|
||||
time.Now().Add(1*time.Hour),
|
||||
time.Now().Add(-1*time.Minute),
|
||||
"",
|
||||
"",
|
||||
nil,
|
||||
"clientID",
|
||||
0,
|
||||
)
|
||||
info := oidc.NewUserInfo()
|
||||
info.SetSubject("sub")
|
||||
info.SetGivenName("firstname")
|
||||
info.SetFamilyName("lastname")
|
||||
info.SetName("firstname lastname")
|
||||
info.SetNickname("nickname")
|
||||
info.SetPreferredUsername("username")
|
||||
info.SetEmail("email", true)
|
||||
info.SetPhone("phone", true)
|
||||
info.SetLocale(language.English)
|
||||
info.SetPicture("picture")
|
||||
info.SetProfile("profile")
|
||||
claims.SetUserinfo(info)
|
||||
privateKey, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAs38btwb3c7r0tMaQpGvBmY+mPwMU/LpfuPoC0k2t4RsKp0fv
|
||||
40SMl50CRrHgk395wch8PMPYbl3+8TtYAJuyrFALIj3Ff1UcKIk0hOH5DDsfh7/q
|
||||
2wFuncTmS6bifYo8CfSq2vDGnM7nZnEvxY/MfSydZdcmIqlkUpfQmtzExw9+tSe5
|
||||
Dxq6gn5JtlGgLgZGt69r5iMMrTEGhhVAXzNuMZbmlCoBru+rC8ITlTX/0V1ZcsSb
|
||||
L8tYWhthyu9x6yjo1bH85wiVI4gs0MhU8f2a+kjL/KGZbR14Ua2eo6tonBZLC5DH
|
||||
WM2TkYXgRCDPufjcgmzN0Lm91E4P8KvBcvly6QIDAQABAoIBAQCPj1nbSPcg2KZe
|
||||
73FAD+8HopyUSSK//1AP4eXfzcEECVy77g0u9+R6XlkzsZCsZ4g6NN8ounqfyw3c
|
||||
YlpAIkcFCf/dowoSjT+4LASVQyatYZwWNqjgAIU4KgMG/rKnNahPTiBYe7peMB1j
|
||||
EaPjnt8uPkCk8y7NCi3y4Pk24tt/WM5KbJK2NQhUi1csGnleDfE+0blV0l/e6C68
|
||||
W5cbnbWAroMqae/Yon3XVZiXX0m+l2f6ZzIgKaD18J+eEM8FjJC+jQKiRe1i9v3K
|
||||
nQrLwh/gn8J10FcbKn3xqslKVidzASIrNIzHT9j/Z5T9NXuAKa7IV2x+Dtdus+wq
|
||||
iBsUunwBAoGBANpYew+8i9vDwK4/SefduDTuzJ0H9lWTjtbiWQ+KYZoeJ7q3/qns
|
||||
jsmi+mjxkXxXg1RrGbNbjtbl3RXXIrUeeBB0lglRJUjc3VK7VvNoyXIWsiqhCspH
|
||||
IJ9Yuknv4mXB01m/glbSCS/xu4RTgf5aOG4jUiRb9+dCIpvDxI9gbXEVAoGBANJz
|
||||
hIJkplIJ+biTi3G1Oz17qkUkInNXzAEzKD9Atoz5AIAiR1ivOMLOlbucfjevw/Nw
|
||||
TnpkMs9xqCefKupTlsriXtZI88m7ZKzAmolYsPolOy/Jhi31h9JFVTEfKGqVS+dk
|
||||
A4ndhgdW9RUeNJPY2YVCARXQrWpueweQDA1cNaeFAoGAPJsYtXqBW6PPRM5+ZiSt
|
||||
78tk8iV2o7RMjqrPS7f+dXfvUS2nO2VVEPTzCtQarOfhpToBLT65vD6bimdn09w8
|
||||
OV0TFEz4y2u65y7m6LNqTwertpdy1ki97l0DgGhccCBH2P6GYDD2qd8wTH+dcot6
|
||||
ZF/begopGoDJ+HBzi9SZLC0CgYBZzPslHMevyBvr++GLwrallKhiWnns1/DwLiEl
|
||||
ZHrBCtuA0Z+6IwLIdZiE9tEQ+ApYTXrfVPQteqUzSwLn/IUiy5eGPpjwYushoAoR
|
||||
Q2w5QTvRN1/vKo8rVXR1woLfgBdkhFPSN1mitiNcQIhU8jpXV4PZCDOHb99FqdzK
|
||||
sqcedQKBgQCOmgbqxGsnT2WQhoOdzln+NOo6Tx+FveLLqat2KzpY59W4noeI2Awn
|
||||
HfIQgWUAW9dsjVVOXMP1jhq8U9hmH/PFWA11V/iCdk1NTxZEw87VAOeWuajpdDHG
|
||||
+iex349j8h2BcQ4Zd0FWu07gGFnS/yuDJPn6jBhRusdieEcxLRjTKg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Key: privateKey, Algorithm: "RS256"}, &jose.SignerOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jws, err := signer.Sign(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
idToken, err := jws.CompactSerialize()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return idToken
|
||||
}
|
||||
|
||||
func keys(t *testing.T) *jose.JSONWebKeySet {
|
||||
privateKey, err := crypto.BytesToPublicKey([]byte(`-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs38btwb3c7r0tMaQpGvB
|
||||
mY+mPwMU/LpfuPoC0k2t4RsKp0fv40SMl50CRrHgk395wch8PMPYbl3+8TtYAJuy
|
||||
rFALIj3Ff1UcKIk0hOH5DDsfh7/q2wFuncTmS6bifYo8CfSq2vDGnM7nZnEvxY/M
|
||||
fSydZdcmIqlkUpfQmtzExw9+tSe5Dxq6gn5JtlGgLgZGt69r5iMMrTEGhhVAXzNu
|
||||
MZbmlCoBru+rC8ITlTX/0V1ZcsSbL8tYWhthyu9x6yjo1bH85wiVI4gs0MhU8f2a
|
||||
+kjL/KGZbR14Ua2eo6tonBZLC5DHWM2TkYXgRCDPufjcgmzN0Lm91E4P8KvBcvly
|
||||
6QIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return &jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: privateKey, Algorithm: "RS256", Use: oidc.KeyUseSignature}}}
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ var DefaultMapper UserInfoMapper = func(info oidc.UserInfo) idp.User {
|
||||
}
|
||||
|
||||
// New creates a generic OIDC provider
|
||||
func New(name, issuer, clientID, clientSecret, redirectURI string, userInfoMapper UserInfoMapper, options ...ProviderOpts) (provider *Provider, err error) {
|
||||
func New(name, issuer, clientID, clientSecret, redirectURI string, scopes []string, userInfoMapper UserInfoMapper, options ...ProviderOpts) (provider *Provider, err error) {
|
||||
provider = &Provider{
|
||||
name: name,
|
||||
userInfoMapper: userInfoMapper,
|
||||
@ -76,13 +76,27 @@ func New(name, issuer, clientID, clientSecret, redirectURI string, userInfoMappe
|
||||
for _, option := range options {
|
||||
option(provider)
|
||||
}
|
||||
provider.RelyingParty, err = rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, []string{oidc.ScopeOpenID}, provider.options...)
|
||||
provider.RelyingParty, err = rp.NewRelyingPartyOIDC(issuer, clientID, clientSecret, redirectURI, setDefaultScope(scopes), provider.options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// setDefaultScope ensures that at least openid ist set
|
||||
// if none is provided it will request `openid profile email phone`
|
||||
func setDefaultScope(scopes []string) []string {
|
||||
if len(scopes) == 0 {
|
||||
return []string{oidc.ScopeOpenID, oidc.ScopeProfile, oidc.ScopeEmail, oidc.ScopePhone}
|
||||
}
|
||||
for _, scope := range scopes {
|
||||
if scope == oidc.ScopeOpenID {
|
||||
return scopes
|
||||
}
|
||||
}
|
||||
return append(scopes, oidc.ScopeOpenID)
|
||||
}
|
||||
|
||||
// Name implements the [idp.Provider] interface
|
||||
func (p *Provider) Name() string {
|
||||
return p.name
|
||||
|
@ -20,6 +20,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
userMapper func(info oidc.UserInfo) idp.User
|
||||
httpMock func(issuer string)
|
||||
}
|
||||
@ -36,6 +37,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
userMapper: DefaultMapper,
|
||||
httpMock: func(issuer string) {
|
||||
gock.New(issuer).
|
||||
@ -59,7 +61,7 @@ func TestProvider_BeginAuth(t *testing.T) {
|
||||
a := assert.New(t)
|
||||
r := require.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.userMapper)
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper)
|
||||
r.NoError(err)
|
||||
|
||||
session, err := provider.BeginAuth(context.Background(), "testState")
|
||||
@ -77,6 +79,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
userMapper func(info oidc.UserInfo) idp.User
|
||||
opts []ProviderOpts
|
||||
httpMock func(issuer string)
|
||||
@ -102,6 +105,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
userMapper: DefaultMapper,
|
||||
opts: nil,
|
||||
httpMock: func(issuer string) {
|
||||
@ -133,6 +137,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
userMapper: DefaultMapper,
|
||||
opts: []ProviderOpts{
|
||||
WithLinkingAllowed(),
|
||||
@ -169,7 +174,7 @@ func TestProvider_Options(t *testing.T) {
|
||||
tt.fields.httpMock(tt.fields.issuer)
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.userMapper, tt.fields.opts...)
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper, tt.fields.opts...)
|
||||
require.NoError(t, err)
|
||||
|
||||
a.Equal(tt.want.name, provider.Name())
|
||||
|
@ -27,6 +27,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID string
|
||||
clientSecret string
|
||||
redirectURI string
|
||||
scopes []string
|
||||
userMapper func(oidc.UserInfo) idp.User
|
||||
httpMock func(issuer string)
|
||||
authURL string
|
||||
@ -62,6 +63,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
userMapper: DefaultMapper,
|
||||
httpMock: func(issuer string) {
|
||||
gock.New(issuer).
|
||||
@ -93,6 +95,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
userMapper: DefaultMapper,
|
||||
httpMock: func(issuer string) {
|
||||
gock.New(issuer).
|
||||
@ -141,6 +144,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
userMapper: DefaultMapper,
|
||||
httpMock: func(issuer string) {
|
||||
gock.New(issuer).
|
||||
@ -201,6 +205,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
clientID: "clientID",
|
||||
clientSecret: "clientSecret",
|
||||
redirectURI: "redirectURI",
|
||||
scopes: []string{"openid"},
|
||||
userMapper: DefaultMapper,
|
||||
httpMock: func(issuer string) {
|
||||
gock.New(issuer).
|
||||
@ -254,7 +259,7 @@ func TestSession_FetchUser(t *testing.T) {
|
||||
tt.fields.httpMock(tt.fields.issuer)
|
||||
a := assert.New(t)
|
||||
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.userMapper)
|
||||
provider, err := New(tt.fields.name, tt.fields.issuer, tt.fields.clientID, tt.fields.clientSecret, tt.fields.redirectURI, tt.fields.scopes, tt.fields.userMapper)
|
||||
require.NoError(t, err)
|
||||
|
||||
session := &Session{
|
||||
|
@ -15,9 +15,10 @@ import (
|
||||
)
|
||||
|
||||
type IDPLoginPolicyLink struct {
|
||||
IDPID string
|
||||
IDPName string
|
||||
IDPType domain.IDPConfigType
|
||||
IDPID string
|
||||
IDPName string
|
||||
IDPType domain.IDPType
|
||||
OwnerType domain.IdentityProviderType
|
||||
}
|
||||
|
||||
type IDPLoginPolicyLinks struct {
|
||||
@ -113,25 +114,28 @@ func (q *Queries) IDPLoginPolicyLinks(ctx context.Context, resourceOwner string,
|
||||
func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPLoginPolicyLinks, error)) {
|
||||
return sq.Select(
|
||||
IDPLoginPolicyLinkIDPIDCol.identifier(),
|
||||
IDPNameCol.identifier(),
|
||||
IDPTypeCol.identifier(),
|
||||
IDPTemplateNameCol.identifier(),
|
||||
IDPTemplateTypeCol.identifier(),
|
||||
IDPTemplateOwnerTypeCol.identifier(),
|
||||
countColumn.identifier()).
|
||||
From(idpLoginPolicyLinkTable.identifier()).
|
||||
LeftJoin(join(IDPIDCol, IDPLoginPolicyLinkIDPIDCol) + db.Timetravel(call.Took(ctx))).
|
||||
LeftJoin(join(IDPTemplateIDCol, IDPLoginPolicyLinkIDPIDCol) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(rows *sql.Rows) (*IDPLoginPolicyLinks, error) {
|
||||
links := make([]*IDPLoginPolicyLink, 0)
|
||||
var count uint64
|
||||
for rows.Next() {
|
||||
var (
|
||||
idpName = sql.NullString{}
|
||||
idpType = sql.NullInt16{}
|
||||
link = new(IDPLoginPolicyLink)
|
||||
idpName = sql.NullString{}
|
||||
idpType = sql.NullInt16{}
|
||||
idpOwnerType = sql.NullInt16{}
|
||||
link = new(IDPLoginPolicyLink)
|
||||
)
|
||||
err := rows.Scan(
|
||||
&link.IDPID,
|
||||
&idpName,
|
||||
&idpType,
|
||||
&idpOwnerType,
|
||||
&count,
|
||||
)
|
||||
if err != nil {
|
||||
@ -140,10 +144,11 @@ func prepareIDPLoginPolicyLinksQuery(ctx context.Context, db prepareDatabase) (s
|
||||
link.IDPName = idpName.String
|
||||
//IDPType 0 is oidc so we have to set unspecified manually
|
||||
if idpType.Valid {
|
||||
link.IDPType = domain.IDPConfigType(idpType.Int16)
|
||||
link.IDPType = domain.IDPType(idpType.Int16)
|
||||
} else {
|
||||
link.IDPType = domain.IDPConfigTypeUnspecified
|
||||
link.IDPType = domain.IDPTypeUnspecified
|
||||
}
|
||||
link.OwnerType = domain.IdentityProviderType(idpOwnerType.Int16)
|
||||
links = append(links, link)
|
||||
}
|
||||
|
||||
|
@ -13,16 +13,18 @@ import (
|
||||
|
||||
var (
|
||||
loginPolicyIDPLinksQuery = regexp.QuoteMeta(`SELECT projections.idp_login_policy_links4.idp_id,` +
|
||||
` projections.idps3.name,` +
|
||||
` projections.idps3.type,` +
|
||||
` projections.idp_templates2.name,` +
|
||||
` projections.idp_templates2.type,` +
|
||||
` projections.idp_templates2.owner_type,` +
|
||||
` COUNT(*) OVER ()` +
|
||||
` FROM projections.idp_login_policy_links4` +
|
||||
` LEFT JOIN projections.idps3 ON projections.idp_login_policy_links4.idp_id = projections.idps3.id AND projections.idp_login_policy_links4.instance_id = projections.idps3.instance_id` +
|
||||
` LEFT JOIN projections.idp_templates2 ON projections.idp_login_policy_links4.idp_id = projections.idp_templates2.id AND projections.idp_login_policy_links4.instance_id = projections.idp_templates2.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`)
|
||||
loginPolicyIDPLinksCols = []string{
|
||||
"idp_id",
|
||||
"name",
|
||||
"type",
|
||||
"owner_type",
|
||||
"count",
|
||||
}
|
||||
)
|
||||
@ -49,7 +51,8 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) {
|
||||
{
|
||||
"idp-id",
|
||||
"idp-name",
|
||||
domain.IDPConfigTypeJWT,
|
||||
domain.IDPTypeJWT,
|
||||
domain.IdentityProviderTypeSystem,
|
||||
},
|
||||
},
|
||||
),
|
||||
@ -60,9 +63,10 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) {
|
||||
},
|
||||
Links: []*IDPLoginPolicyLink{
|
||||
{
|
||||
IDPID: "idp-id",
|
||||
IDPName: "idp-name",
|
||||
IDPType: domain.IDPConfigTypeJWT,
|
||||
IDPID: "idp-id",
|
||||
IDPName: "idp-name",
|
||||
IDPType: domain.IDPTypeJWT,
|
||||
OwnerType: domain.IdentityProviderTypeSystem,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -79,6 +83,7 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) {
|
||||
"idp-id",
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
},
|
||||
},
|
||||
),
|
||||
@ -91,7 +96,7 @@ func Test_IDPLoginPolicyLinkPrepares(t *testing.T) {
|
||||
{
|
||||
IDPID: "idp-id",
|
||||
IDPName: "",
|
||||
IDPType: domain.IDPConfigTypeUnspecified,
|
||||
IDPType: domain.IDPTypeUnspecified,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -386,8 +386,8 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// IDPTemplateByIDAndResourceOwner searches for the requested id in the context of the resource owner and IAM
|
||||
func (q *Queries) IDPTemplateByIDAndResourceOwner(ctx context.Context, shouldTriggerBulk bool, id, resourceOwner string, withOwnerRemoved bool) (_ *IDPTemplate, err error) {
|
||||
// IDPTemplateByID searches for the requested id
|
||||
func (q *Queries) IDPTemplateByID(ctx context.Context, shouldTriggerBulk bool, id string, withOwnerRemoved bool, queries ...SearchQuery) (_ *IDPTemplate, err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
@ -403,20 +403,16 @@ func (q *Queries) IDPTemplateByIDAndResourceOwner(ctx context.Context, shouldTri
|
||||
if !withOwnerRemoved {
|
||||
eq[IDPTemplateOwnerRemovedCol.identifier()] = false
|
||||
}
|
||||
where := sq.And{
|
||||
eq,
|
||||
sq.Or{
|
||||
sq.Eq{IDPTemplateResourceOwnerCol.identifier(): resourceOwner},
|
||||
sq.Eq{IDPTemplateResourceOwnerCol.identifier(): authz.GetInstance(ctx).InstanceID()},
|
||||
},
|
||||
query, scan := prepareIDPTemplateByIDQuery(ctx, q.client)
|
||||
for _, q := range queries {
|
||||
query = q.toQuery(query)
|
||||
}
|
||||
stmt, scan := prepareIDPTemplateByIDQuery(ctx, q.client)
|
||||
query, args, err := stmt.Where(where).ToSql()
|
||||
stmt, args, err := query.Where(eq).ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.ThrowInternal(err, "QUERY-SFAew", "Errors.Query.SQLStatement")
|
||||
return nil, errors.ThrowInternal(err, "QUERY-SFefg", "Errors.Query.SQLStatement")
|
||||
}
|
||||
|
||||
row := q.client.QueryRowContext(ctx, query, args...)
|
||||
row := q.client.QueryRowContext(ctx, stmt, args...)
|
||||
return scan(row)
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ type IDPUserLink struct {
|
||||
ProvidedUserID string
|
||||
ProvidedUsername string
|
||||
ResourceOwner string
|
||||
IDPType domain.IDPConfigType
|
||||
IDPType domain.IDPType
|
||||
}
|
||||
|
||||
type IDPUserLinks struct {
|
||||
@ -127,18 +127,22 @@ func NewIDPUserLinksResourceOwnerSearchQuery(value string) (SearchQuery, error)
|
||||
return NewTextQuery(IDPUserLinkResourceOwnerCol, value, TextEquals)
|
||||
}
|
||||
|
||||
func NewIDPUserLinksExternalIDSearchQuery(value string) (SearchQuery, error) {
|
||||
return NewTextQuery(IDPUserLinkExternalUserIDCol, value, TextEquals)
|
||||
}
|
||||
|
||||
func prepareIDPUserLinksQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Rows) (*IDPUserLinks, error)) {
|
||||
return sq.Select(
|
||||
IDPUserLinkIDPIDCol.identifier(),
|
||||
IDPUserLinkUserIDCol.identifier(),
|
||||
IDPNameCol.identifier(),
|
||||
IDPTemplateNameCol.identifier(),
|
||||
IDPUserLinkExternalUserIDCol.identifier(),
|
||||
IDPUserLinkDisplayNameCol.identifier(),
|
||||
IDPTypeCol.identifier(),
|
||||
IDPTemplateTypeCol.identifier(),
|
||||
IDPUserLinkResourceOwnerCol.identifier(),
|
||||
countColumn.identifier()).
|
||||
From(idpUserLinkTable.identifier()).
|
||||
LeftJoin(join(IDPIDCol, IDPUserLinkIDPIDCol) + db.Timetravel(call.Took(ctx))).
|
||||
LeftJoin(join(IDPTemplateIDCol, IDPUserLinkIDPIDCol) + db.Timetravel(call.Took(ctx))).
|
||||
PlaceholderFormat(sq.Dollar),
|
||||
func(rows *sql.Rows) (*IDPUserLinks, error) {
|
||||
idps := make([]*IDPUserLink, 0)
|
||||
@ -165,9 +169,9 @@ func prepareIDPUserLinksQuery(ctx context.Context, db prepareDatabase) (sq.Selec
|
||||
idp.IDPName = idpName.String
|
||||
//IDPType 0 is oidc so we have to set unspecified manually
|
||||
if idpType.Valid {
|
||||
idp.IDPType = domain.IDPConfigType(idpType.Int16)
|
||||
idp.IDPType = domain.IDPType(idpType.Int16)
|
||||
} else {
|
||||
idp.IDPType = domain.IDPConfigTypeUnspecified
|
||||
idp.IDPType = domain.IDPTypeUnspecified
|
||||
}
|
||||
idps = append(idps, idp)
|
||||
}
|
||||
|
@ -14,14 +14,14 @@ import (
|
||||
var (
|
||||
idpUserLinksQuery = regexp.QuoteMeta(`SELECT projections.idp_user_links3.idp_id,` +
|
||||
` projections.idp_user_links3.user_id,` +
|
||||
` projections.idps3.name,` +
|
||||
` projections.idp_templates2.name,` +
|
||||
` projections.idp_user_links3.external_user_id,` +
|
||||
` projections.idp_user_links3.display_name,` +
|
||||
` projections.idps3.type,` +
|
||||
` projections.idp_templates2.type,` +
|
||||
` projections.idp_user_links3.resource_owner,` +
|
||||
` COUNT(*) OVER ()` +
|
||||
` FROM projections.idp_user_links3` +
|
||||
` LEFT JOIN projections.idps3 ON projections.idp_user_links3.idp_id = projections.idps3.id AND projections.idp_user_links3.instance_id = projections.idps3.instance_id` +
|
||||
` LEFT JOIN projections.idp_templates2 ON projections.idp_user_links3.idp_id = projections.idp_templates2.id AND projections.idp_user_links3.instance_id = projections.idp_templates2.instance_id` +
|
||||
` AS OF SYSTEM TIME '-1 ms'`)
|
||||
idpUserLinksCols = []string{
|
||||
"idp_id",
|
||||
@ -60,7 +60,7 @@ func Test_IDPUserLinkPrepares(t *testing.T) {
|
||||
"idp-name",
|
||||
"external-user-id",
|
||||
"display-name",
|
||||
domain.IDPConfigTypeJWT,
|
||||
domain.IDPTypeJWT,
|
||||
"ro",
|
||||
},
|
||||
},
|
||||
@ -77,7 +77,7 @@ func Test_IDPUserLinkPrepares(t *testing.T) {
|
||||
IDPName: "idp-name",
|
||||
ProvidedUserID: "external-user-id",
|
||||
ProvidedUsername: "display-name",
|
||||
IDPType: domain.IDPConfigTypeJWT,
|
||||
IDPType: domain.IDPTypeJWT,
|
||||
ResourceOwner: "ro",
|
||||
},
|
||||
},
|
||||
@ -114,7 +114,7 @@ func Test_IDPUserLinkPrepares(t *testing.T) {
|
||||
IDPName: "",
|
||||
ProvidedUserID: "external-user-id",
|
||||
ProvidedUsername: "display-name",
|
||||
IDPType: domain.IDPConfigTypeUnspecified,
|
||||
IDPType: domain.IDPTypeUnspecified,
|
||||
ResourceOwner: "ro",
|
||||
},
|
||||
},
|
||||
|
@ -668,7 +668,7 @@ func (p *idpTemplateProjection) reduceOldConfigAdded(event eventstore.Event) (*h
|
||||
handler.NewCol(IDPTemplateStateCol, domain.IDPStateActive),
|
||||
handler.NewCol(IDPTemplateNameCol, idpEvent.Name),
|
||||
handler.NewCol(IDPTemplateOwnerTypeCol, idpOwnerType),
|
||||
handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeOIDC),
|
||||
handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeUnspecified),
|
||||
handler.NewCol(IDPTemplateIsCreationAllowedCol, true),
|
||||
handler.NewCol(IDPTemplateIsLinkingAllowedCol, true),
|
||||
handler.NewCol(IDPTemplateIsAutoCreationCol, idpEvent.AutoRegister),
|
||||
@ -727,6 +727,7 @@ func (p *idpTemplateProjection) reduceOldOIDCConfigAdded(event eventstore.Event)
|
||||
[]handler.Column{
|
||||
handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()),
|
||||
handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()),
|
||||
handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeOIDC),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(IDPTemplateIDCol, idpEvent.IDPConfigID),
|
||||
@ -820,6 +821,7 @@ func (p *idpTemplateProjection) reduceOldJWTConfigAdded(event eventstore.Event)
|
||||
[]handler.Column{
|
||||
handler.NewCol(IDPTemplateChangeDateCol, idpEvent.CreationDate()),
|
||||
handler.NewCol(IDPTemplateSequenceCol, idpEvent.Sequence()),
|
||||
handler.NewCol(IDPTemplateTypeCol, domain.IDPTypeJWT),
|
||||
},
|
||||
[]handler.Condition{
|
||||
handler.NewCond(IDPTemplateIDCol, idpEvent.IDPConfigID),
|
||||
|
@ -4403,7 +4403,7 @@ message AddJWTProviderRequest {
|
||||
string jwt_endpoint = 3 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
string keys_endpoint = 4 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
string header_name = 5 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
zitadel.idp.v1.Options provider_options = 6 [(validate.rules).message = {required: true}];
|
||||
zitadel.idp.v1.Options provider_options = 6;
|
||||
}
|
||||
|
||||
message AddJWTProviderResponse {
|
||||
@ -4418,7 +4418,7 @@ message UpdateJWTProviderRequest {
|
||||
string jwt_endpoint = 4 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
string keys_endpoint = 5 [(validate.rules).string = {max_len: 200}];
|
||||
string header_name = 6 [(validate.rules).string = {min_len: 1, max_len: 200}];
|
||||
zitadel.idp.v1.Options provider_options = 7 [(validate.rules).message = {required: true}];
|
||||
zitadel.idp.v1.Options provider_options = 7;
|
||||
}
|
||||
|
||||
message UpdateJWTProviderResponse {
|
||||
|
Loading…
x
Reference in New Issue
Block a user