mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 00:57:33 +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:
@@ -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>
|
||||
|
Reference in New Issue
Block a user