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:
Stefan Benz
2023-03-24 16:18:56 +01:00
committed by GitHub
parent a8bfcc166e
commit 41ff0bbc63
40 changed files with 2240 additions and 1142 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,13 @@ Login:
RegisterButtonText: 注册
NextButtonText: 继续
LDAP:
Title: 注册
Description: 输入您的登录数据。
LoginNameLabel: 登录名
PasswordLabel: 密码
NextButtonText: 继续
SelectAccount:
Title: 选择账户
Description: 使用您的 ZITADEL 帐户

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