mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 01:47:33 +00:00
feat: ldap provider login (#5448)
Add the logic to configure and use LDAP provider as an external IDP with a dedicated login GUI.
This commit is contained in:
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/gitlab"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/google"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/jwt"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
|
||||
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
|
||||
"github.com/zitadel/zitadel/internal/query"
|
||||
@@ -157,8 +158,9 @@ func (l *Login) handleIDP(w http.ResponseWriter, r *http.Request, authReq *domai
|
||||
provider, err = l.gitlabSelfHostedProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeGoogle:
|
||||
provider, err = l.googleProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeLDAP,
|
||||
domain.IDPTypeUnspecified:
|
||||
case domain.IDPTypeLDAP:
|
||||
provider, err = l.ldapProvider(r.Context(), identityProvider)
|
||||
case domain.IDPTypeUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
l.renderLogin(w, r, authReq, errors.ThrowInvalidArgument(nil, "LOGIN-AShek", "Errors.ExternalIDP.IDPTypeNotImplemented"))
|
||||
@@ -604,6 +606,69 @@ func (l *Login) updateExternalUser(ctx context.Context, authReq *domain.AuthRequ
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Login) ldapProvider(ctx context.Context, identityProvider *query.IDPTemplate) (*ldap.Provider, error) {
|
||||
password, err := crypto.DecryptString(identityProvider.LDAPIDPTemplate.BindPassword, l.idpConfigAlg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var opts []ldap.ProviderOpts
|
||||
if !identityProvider.LDAPIDPTemplate.StartTLS {
|
||||
opts = append(opts, ldap.WithoutStartTLS())
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.IDAttribute != "" {
|
||||
opts = append(opts, ldap.WithCustomIDAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.IDAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.FirstNameAttribute != "" {
|
||||
opts = append(opts, ldap.WithFirstNameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.FirstNameAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.LastNameAttribute != "" {
|
||||
opts = append(opts, ldap.WithLastNameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.LastNameAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.DisplayNameAttribute != "" {
|
||||
opts = append(opts, ldap.WithDisplayNameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.DisplayNameAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.NickNameAttribute != "" {
|
||||
opts = append(opts, ldap.WithNickNameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.NickNameAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.PreferredUsernameAttribute != "" {
|
||||
opts = append(opts, ldap.WithPreferredUsernameAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.PreferredUsernameAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.EmailAttribute != "" {
|
||||
opts = append(opts, ldap.WithEmailAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.EmailAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.EmailVerifiedAttribute != "" {
|
||||
opts = append(opts, ldap.WithEmailVerifiedAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.EmailVerifiedAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.PhoneAttribute != "" {
|
||||
opts = append(opts, ldap.WithPhoneAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.PhoneAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.PhoneVerifiedAttribute != "" {
|
||||
opts = append(opts, ldap.WithPhoneVerifiedAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.PhoneVerifiedAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.PreferredLanguageAttribute != "" {
|
||||
opts = append(opts, ldap.WithPreferredLanguageAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.PreferredLanguageAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.AvatarURLAttribute != "" {
|
||||
opts = append(opts, ldap.WithAvatarURLAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.AvatarURLAttribute))
|
||||
}
|
||||
if identityProvider.LDAPIDPTemplate.LDAPAttributes.ProfileAttribute != "" {
|
||||
opts = append(opts, ldap.WithProfileAttribute(identityProvider.LDAPIDPTemplate.LDAPAttributes.ProfileAttribute))
|
||||
}
|
||||
return ldap.New(
|
||||
identityProvider.Name,
|
||||
identityProvider.Servers,
|
||||
identityProvider.BaseDN,
|
||||
identityProvider.BindDN,
|
||||
password,
|
||||
identityProvider.UserBase,
|
||||
identityProvider.UserObjectClasses,
|
||||
identityProvider.UserFilters,
|
||||
identityProvider.Timeout,
|
||||
l.baseURL(ctx)+EndpointLDAPLogin+"?"+QueryAuthRequestID+"=",
|
||||
opts...,
|
||||
), 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)
|
||||
|
83
internal/api/ui/login/ldap_handler.go
Normal file
83
internal/api/ui/login/ldap_handler.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package login
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/idp/providers/ldap"
|
||||
)
|
||||
|
||||
const (
|
||||
tmplLDAPLogin = "ldap_login"
|
||||
)
|
||||
|
||||
type ldapFormData struct {
|
||||
Username string `schema:"ldapusername"`
|
||||
Password string `schema:"ldappassword"`
|
||||
ResetExternalIDP bool `schema:"resetexternalidp"`
|
||||
}
|
||||
|
||||
func (l *Login) handleLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
authReq, err := l.getAuthRequest(r)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.renderLDAPLogin(w, r, authReq, nil)
|
||||
}
|
||||
|
||||
func (l *Login) renderLDAPLogin(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
|
||||
var errID, errMessage string
|
||||
if err != nil {
|
||||
errID, errMessage = l.getErrorMessage(r, err)
|
||||
}
|
||||
temp := l.renderer.Templates[tmplLDAPLogin]
|
||||
data := l.getUserData(r, authReq, "Login.Title", "Login.Description", errID, errMessage)
|
||||
l.renderer.RenderTemplate(w, r, l.getTranslator(r.Context(), authReq), temp, data, nil)
|
||||
}
|
||||
|
||||
func (l *Login) handleLDAPCallback(w http.ResponseWriter, r *http.Request) {
|
||||
data := new(ldapFormData)
|
||||
authReq, err := l.getAuthRequestAndParseData(r, data)
|
||||
if err != nil {
|
||||
l.renderError(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
if data.ResetExternalIDP {
|
||||
userAgentID, _ := http_mw.UserAgentIDFromCtx(r.Context())
|
||||
err := l.authRepo.ResetSelectedIDP(r.Context(), authReq.ID, userAgentID)
|
||||
if err != nil {
|
||||
l.renderLDAPLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
l.handleLoginName(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
identityProvider, err := l.getIDPByID(r, authReq.SelectedIDPConfigID)
|
||||
if err != nil {
|
||||
l.renderLDAPLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
|
||||
provider, err := l.ldapProvider(r.Context(), identityProvider)
|
||||
if err != nil {
|
||||
l.renderLDAPLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
session := &ldap.Session{Provider: provider, User: data.Username, Password: data.Password}
|
||||
|
||||
user, err := session.FetchUser(r.Context())
|
||||
if err != nil {
|
||||
if _, actionErr := l.runPostExternalAuthenticationActions(new(domain.ExternalUser), nil, authReq, r, nil, err); actionErr != nil {
|
||||
logging.WithError(err).Error("both external user authentication and action post authentication failed")
|
||||
}
|
||||
l.renderLDAPLogin(w, r, authReq, err)
|
||||
return
|
||||
}
|
||||
l.handleExternalUserAuthenticated(w, r, authReq, identityProvider, session, user, l.renderNextStep)
|
||||
}
|
@@ -76,6 +76,7 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
||||
tmplLinkUsersDone: "link_users_done.html",
|
||||
tmplExternalNotFoundOption: "external_not_found_option.html",
|
||||
tmplLoginSuccess: "login_success.html",
|
||||
tmplLDAPLogin: "ldap_login.html",
|
||||
}
|
||||
funcs := map[string]interface{}{
|
||||
"resourceUrl": func(file string) string {
|
||||
@@ -219,6 +220,9 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
|
||||
"idpProviderClass": func(idpType domain.IDPType) string {
|
||||
return idpType.GetCSSClass()
|
||||
},
|
||||
"ldapUrl": func() string {
|
||||
return path.Join(r.pathPrefix, EndpointLDAPCallback)
|
||||
},
|
||||
}
|
||||
var err error
|
||||
r.Renderer, err = renderer.NewRenderer(
|
||||
|
@@ -15,6 +15,8 @@ const (
|
||||
EndpointExternalLoginCallback = "/login/externalidp/callback"
|
||||
EndpointJWTAuthorize = "/login/jwt/authorize"
|
||||
EndpointJWTCallback = "/login/jwt/callback"
|
||||
EndpointLDAPLogin = "/login/ldap"
|
||||
EndpointLDAPCallback = "/login/ldap/callback"
|
||||
EndpointPasswordlessLogin = "/login/passwordless"
|
||||
EndpointPasswordlessRegistration = "/login/passwordless/init"
|
||||
EndpointPasswordlessPrompt = "/login/passwordless/prompt"
|
||||
@@ -102,6 +104,8 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
|
||||
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrg).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointRegisterOrg, login.handleRegisterOrgCheck).Methods(http.MethodPost)
|
||||
router.HandleFunc(EndpointLoginSuccess, login.handleLoginSuccess).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointLDAPLogin, login.handleLDAP).Methods(http.MethodGet)
|
||||
router.HandleFunc(EndpointLDAPCallback, login.handleLDAPCallback).Methods(http.MethodPost)
|
||||
router.SkipClean(true).Handle("", http.RedirectHandler(HandlerPrefix+"/", http.StatusMovedPermanently))
|
||||
return router
|
||||
}
|
||||
|
@@ -11,6 +11,13 @@ Login:
|
||||
RegisterButtonText: registrieren
|
||||
NextButtonText: weiter
|
||||
|
||||
LDAP:
|
||||
Title: Anmeldung
|
||||
Description: Mit Konto anmelden.
|
||||
LoginNameLabel: Loginname
|
||||
PasswordLabel: Passwort
|
||||
NextButtonText: weiter
|
||||
|
||||
SelectAccount:
|
||||
Title: Account auswählen
|
||||
Description: Wähle deinen Account aus.
|
||||
|
@@ -11,6 +11,13 @@ Login:
|
||||
RegisterButtonText: register
|
||||
NextButtonText: next
|
||||
|
||||
LDAP:
|
||||
Title: Login
|
||||
Description: Enter your login data.
|
||||
LoginNameLabel: Loginname
|
||||
PasswordLabel: Password
|
||||
NextButtonText: next
|
||||
|
||||
SelectAccount:
|
||||
Title: Select account
|
||||
Description: Use your ZITADEL-Account
|
||||
|
@@ -11,6 +11,13 @@ Login:
|
||||
RegisterButtonText: s'inscrire
|
||||
NextButtonText: suivant
|
||||
|
||||
LDAP:
|
||||
Title: Connexion
|
||||
Description: Entrez vos données de connexion.
|
||||
LoginNameLabel: Identifiant
|
||||
PasswordLabel: Mot de passe
|
||||
NextButtonText: suivant
|
||||
|
||||
SelectAccount:
|
||||
Title: Sélectionner un compte
|
||||
Description: Utilisez votre compte ZITADEL
|
||||
|
@@ -11,6 +11,13 @@ Login:
|
||||
RegisterButtonText: registrare
|
||||
NextButtonText: Avanti
|
||||
|
||||
LDAP:
|
||||
Title: Accesso
|
||||
Description: Inserisci i tuoi dati di accesso.
|
||||
LoginNameLabel: Nome di accesso
|
||||
PasswordLabel: Password
|
||||
NextButtonText: Avanti
|
||||
|
||||
SelectAccount:
|
||||
Title: Seleziona l'account
|
||||
Description: Usa il tuo account ZITADEL
|
||||
|
@@ -11,6 +11,13 @@ Login:
|
||||
RegisterButtonText: zarejestruj
|
||||
NextButtonText: dalej
|
||||
|
||||
LDAP:
|
||||
Title: Rejestracja
|
||||
Description: Wprowadź swoje dane logowania.
|
||||
LoginNameLabel: Nazwa użytkownika
|
||||
PasswordLabel: Hasło
|
||||
NextButtonText: dalej
|
||||
|
||||
SelectAccount:
|
||||
Title: Wybierz konto
|
||||
Description: Użyj swojego konta ZITADEL
|
||||
|
@@ -11,6 +11,13 @@ Login:
|
||||
RegisterButtonText: 注册
|
||||
NextButtonText: 继续
|
||||
|
||||
LDAP:
|
||||
Title: 注册
|
||||
Description: 输入您的登录数据。
|
||||
LoginNameLabel: 登录名
|
||||
PasswordLabel: 密码
|
||||
NextButtonText: 继续
|
||||
|
||||
SelectAccount:
|
||||
Title: 选择账户
|
||||
Description: 使用您的 ZITADEL 帐户
|
||||
|
40
internal/api/ui/login/static/templates/ldap_login.html
Normal file
40
internal/api/ui/login/static/templates/ldap_login.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<div class="lgn-head">
|
||||
<h1>{{t "LDAP.Title"}}</h1>
|
||||
<p>{{t "LDAP.Description"}}</p>
|
||||
</div>
|
||||
|
||||
|
||||
<form action="{{ ldapUrl }}" method="POST">
|
||||
|
||||
{{ .CSRF }}
|
||||
|
||||
<input type="hidden" name="authRequestID" value="{{ .AuthReqID }}"/>
|
||||
|
||||
<div class="fields">
|
||||
<label class="lgn-label" for="ldapusername">{{t "LDAP.LoginNameLabel"}}</label>
|
||||
<input class="lgn-input" type="text" id="ldapusername" name="ldapusername" autocomplete="username" autofocus required>
|
||||
</div>
|
||||
<div class="fields">
|
||||
<label class="lgn-label" for="ldappassword">{{t "LDAP.PasswordLabel"}}</label>
|
||||
<input class="lgn-input" type="password" id="ldappassword" name="ldappassword" autocomplete="current-password" required>
|
||||
</div>
|
||||
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="lgn-actions lgn-reverse-order">
|
||||
<button class="lgn-raised-button lgn-primary lgn-initial-focus" id="submit-button" type="submit">
|
||||
{{t "LDAP.NextButtonText"}}
|
||||
</button>
|
||||
<span class="fill-space"></span>
|
||||
<button class="lgn-icon-button lgn-left-action" name="resetexternalidp" value="true" formnovalidate>
|
||||
<i class="lgn-icon-arrow-left-solid"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="{{ resourceUrl " scripts/form_submit.js" }}"></script>
|
||||
<script src="{{ resourceUrl " scripts/default_form_validation.js" }}"></script>
|
||||
|
||||
{{template "main-bottom" .}}
|
Reference in New Issue
Block a user