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:
Livio Spring
2023-02-28 21:20:58 +01:00
committed by GitHub
parent abacb6c5aa
commit 48f9815b7c
62 changed files with 1254 additions and 2165 deletions

View File

@@ -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 {

View File

@@ -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
}

View 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),
}
}

View File

@@ -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,
}
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -370,6 +370,8 @@ Errors:
ExternalUserIDEmpty: 外部用户 ID 为空
UserDisplayNameEmpty: 用户显示名称为空
NoExternalUserData: 未收到外部用户数据
CreationNotAllowed: 不允许在该供应商上创建新用户
LinkingNotAllowed: 在此提供者上不允许链接一个用户
GrantRequired: 无法登录,用户需要在应用程序上拥有至少一项授权,请联系您的管理员。
ProjectRequired: 无法登录,用户的组织必须授予项目,请联系您的管理员。
IdentityProvider:

View File

@@ -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>

View File

@@ -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" .}}

View File

@@ -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>

View File

@@ -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>