Implement client certificate login
This commit is contained in:
parent
714a07f162
commit
7947eaf862
9 changed files with 307 additions and 119 deletions
|
@ -1,3 +1,11 @@
|
||||||
|
[CertLoginIntroText]
|
||||||
|
hash = "sha1-3d611f594a2194a9b99f8b955e49877bd419a567"
|
||||||
|
other = "Du hast ein gültiges Client-Zertifikat für die folgenden E-Mail-Adressen vorgelegt:"
|
||||||
|
|
||||||
|
[CertLoginRequestText]
|
||||||
|
hash = "sha1-c1ad2600848ad6293ae6df6a04b0b2318bb303f9"
|
||||||
|
other = "Willst du dieses Zertifikat für die Anmeldung verwenden oder möchtest du lieber ein anderes Verfahren nutzen?"
|
||||||
|
|
||||||
[ErrorEmail]
|
[ErrorEmail]
|
||||||
hash = "sha1-d2306dd8970ff616631a3501791297f31475e416"
|
hash = "sha1-d2306dd8970ff616631a3501791297f31475e416"
|
||||||
other = "Bitte gib eine gültige E-Mailadresse ein."
|
other = "Bitte gib eine gültige E-Mailadresse ein."
|
||||||
|
@ -48,6 +56,11 @@ other = "Auf der <a href=\"{{ .clientLink }}\">Beschreibungsseite</a> findest du
|
||||||
hash = "sha1-cb8efc74b5b726201321e0924747bf38d39629a1"
|
hash = "sha1-cb8efc74b5b726201321e0924747bf38d39629a1"
|
||||||
other = "Die Anwendung <strong>{{ .client }}</strong> benötigt deine Einwilligungung für die angefragten Berechtigungen."
|
other = "Die Anwendung <strong>{{ .client }}</strong> benötigt deine Einwilligungung für die angefragten Berechtigungen."
|
||||||
|
|
||||||
|
[LabelAcceptCertLogin]
|
||||||
|
description = "Label for a button to accept certificate login"
|
||||||
|
hash = "sha1-95cf27f4bdee62b51ee8bc673d25a46bcceed452"
|
||||||
|
other = "Ja, bitte nutze das Zertifikat"
|
||||||
|
|
||||||
[LabelConsent]
|
[LabelConsent]
|
||||||
hash = "sha1-5e56a367cf99015bbe98488845541db00b7e04f6"
|
hash = "sha1-5e56a367cf99015bbe98488845541db00b7e04f6"
|
||||||
other = "Ich erteile hiermit meine Einwilligung, dass die Anwendung die angefragten Berechtigungen erhalten darf."
|
other = "Ich erteile hiermit meine Einwilligung, dass die Anwendung die angefragten Berechtigungen erhalten darf."
|
||||||
|
@ -57,6 +70,11 @@ description = "Label for a login button"
|
||||||
hash = "sha1-ff00822024fca849fe0cef21237b57218e706852"
|
hash = "sha1-ff00822024fca849fe0cef21237b57218e706852"
|
||||||
other = "Anmelden"
|
other = "Anmelden"
|
||||||
|
|
||||||
|
[LabelRejectCertLogin]
|
||||||
|
description = "Label for a button to reject certificate login"
|
||||||
|
hash = "sha1-911cc305bb66efe162641969aee6b88e5d28e24f"
|
||||||
|
other = "Nein, bitte frag nach meinem Passwort"
|
||||||
|
|
||||||
[LabelSubmit]
|
[LabelSubmit]
|
||||||
hash = "sha1-2dacf65959849884a011f36f76a04eebea94c5ea"
|
hash = "sha1-2dacf65959849884a011f36f76a04eebea94c5ea"
|
||||||
other = "Abschicken"
|
other = "Abschicken"
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
CertLoginIntroText = "You have presented a valid client certificate for the following email addresses:"
|
||||||
|
CertLoginRequestText = "Do you want to use this certificate for authentication or do you want to use a different method?"
|
||||||
ErrorEmail = "Please enter a valid email address."
|
ErrorEmail = "Please enter a valid email address."
|
||||||
ErrorEmailRequired = "Please enter an email address."
|
ErrorEmailRequired = "Please enter an email address."
|
||||||
ErrorPasswordRequired = "Please enter a password."
|
ErrorPasswordRequired = "Please enter a password."
|
||||||
|
@ -26,10 +28,18 @@ other = "Email:"
|
||||||
description = "Label for a password form field"
|
description = "Label for a password form field"
|
||||||
other = "Password:"
|
other = "Password:"
|
||||||
|
|
||||||
|
[LabelAcceptCertLogin]
|
||||||
|
description = "Label for a button to accept certificate login"
|
||||||
|
other = "Yes, please use the certificate"
|
||||||
|
|
||||||
[LabelLogin]
|
[LabelLogin]
|
||||||
description = "Label for a login button"
|
description = "Label for a login button"
|
||||||
other = "Login"
|
other = "Login"
|
||||||
|
|
||||||
|
[LabelRejectCertLogin]
|
||||||
|
description = "Label for a button to reject certificate login"
|
||||||
|
other = "No, please ask for my password"
|
||||||
|
|
||||||
[LogoutLabel]
|
[LogoutLabel]
|
||||||
description = "A label on a logout button or link"
|
description = "A label on a logout button or link"
|
||||||
other = "Logout"
|
other = "Logout"
|
||||||
|
|
|
@ -3,8 +3,10 @@ package main
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -48,12 +50,13 @@ func main() {
|
||||||
config := koanf.New(".")
|
config := koanf.New(".")
|
||||||
|
|
||||||
_ = config.Load(confmap.Provider(map[string]interface{}{
|
_ = config.Load(confmap.Provider(map[string]interface{}{
|
||||||
"server.port": 3000,
|
"server.port": 3000,
|
||||||
"server.name": "login.cacert.localhost",
|
"server.name": "login.cacert.localhost",
|
||||||
"server.key": "certs/idp.cacert.localhost.key",
|
"server.key": "certs/idp.cacert.localhost.key",
|
||||||
"server.certificate": "certs/idp.cacert.localhost.crt.pem",
|
"server.certificate": "certs/idp.cacert.localhost.crt.pem",
|
||||||
"admin.url": "https://hydra.cacert.localhost:4445/",
|
"security.client.ca-file": "certs/client_ca.pem",
|
||||||
"i18n.languages": []string{"en", "de"},
|
"admin.url": "https://hydra.cacert.localhost:4445/",
|
||||||
|
"i18n.languages": []string{"en", "de"},
|
||||||
}, "."), nil)
|
}, "."), nil)
|
||||||
cFiles, _ := f.GetStringSlice("conf")
|
cFiles, _ := f.GetStringSlice("conf")
|
||||||
for _, c := range cFiles {
|
for _, c := range cFiles {
|
||||||
|
@ -133,16 +136,25 @@ func main() {
|
||||||
csrf.SameSite(csrf.SameSiteStrictMode),
|
csrf.SameSite(csrf.SameSiteStrictMode),
|
||||||
csrf.MaxAge(600))
|
csrf.MaxAge(600))
|
||||||
|
|
||||||
|
clientCertPool := x509.NewCertPool()
|
||||||
|
pemBytes, err := ioutil.ReadFile(config.MustString("security.client.ca-file"))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatalf("could not load client CA certificates: %v", err)
|
||||||
|
}
|
||||||
|
clientCertPool.AppendCertsFromPEM(pemBytes)
|
||||||
|
|
||||||
tlsConfig := &tls.Config{
|
tlsConfig := &tls.Config{
|
||||||
ServerName: config.String("server.name"),
|
ServerName: config.String("server.name"),
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
|
ClientAuth: tls.VerifyClientCertIfGiven,
|
||||||
|
ClientCAs: clientCertPool,
|
||||||
}
|
}
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Addr: fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port")),
|
Addr: fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port")),
|
||||||
Handler: tracing(logging(hsts(csrfProtect(router)))),
|
Handler: tracing(logging(hsts(csrfProtect(router)))),
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 20 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 20 * time.Second,
|
||||||
IdleTimeout: 15 * time.Second,
|
IdleTimeout: 30 * time.Second,
|
||||||
TLSConfig: tlsConfig,
|
TLSConfig: tlsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
r.Context(),
|
r.Context(),
|
||||||
`SELECT email, verified, fname, mname, lname, dob, language, modified
|
`SELECT email, verified, fname, mname, lname, dob, language, modified
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = ?
|
WHERE uniqueID = ?
|
||||||
AND LOCKED = 0`,
|
AND LOCKED = 0`,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/go-playground/form/v4"
|
"github.com/go-playground/form/v4"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
"github.com/ory/hydra-client-go/client/admin"
|
"github.com/ory/hydra-client-go/client/admin"
|
||||||
"github.com/ory/hydra-client-go/models"
|
"github.com/ory/hydra-client-go/models"
|
||||||
|
@ -22,27 +23,26 @@ import (
|
||||||
"git.cacert.org/oidc_login/idp/services"
|
"git.cacert.org/oidc_login/idp/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type acrType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClientCertificate acrType = "cert" // client certificate login
|
||||||
|
Password acrType = "password" // regular username + password login
|
||||||
|
// ClientCertificateOTP acrType = "cert+otp"
|
||||||
|
// ClientCertificateToken acrType = "cert+token"
|
||||||
|
// PasswordOTP acrType = "password+otp"
|
||||||
|
// PasswordToken acrType = "password+token"
|
||||||
|
)
|
||||||
|
|
||||||
type loginHandler struct {
|
type loginHandler struct {
|
||||||
adminClient *admin.Client
|
adminClient *admin.Client
|
||||||
bundle *i18n.Bundle
|
bundle *i18n.Bundle
|
||||||
context context.Context
|
context context.Context
|
||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
loginTemplate *template.Template
|
templates map[acrType]*template.Template
|
||||||
messageCatalog *commonServices.MessageCatalog
|
messageCatalog *commonServices.MessageCatalog
|
||||||
}
|
}
|
||||||
|
|
||||||
type acrType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
NoCredentials acrType = "none"
|
|
||||||
ClientCertificate acrType = "cert"
|
|
||||||
ClientCertificateOTP acrType = "cert+otp"
|
|
||||||
ClientCertificateToken acrType = "cert+token"
|
|
||||||
Password acrType = "password"
|
|
||||||
PasswordOTP acrType = "password+otp"
|
|
||||||
PasswordToken acrType = "password+token"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LoginInformation struct {
|
type LoginInformation struct {
|
||||||
Email string `form:"email" validate:"required,email"`
|
Email string `form:"email" validate:"required,email"`
|
||||||
Password string `form:"password" validate:"required"`
|
Password string `form:"password" validate:"required"`
|
||||||
|
@ -55,104 +55,130 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
accept := r.Header.Get("Accept-Language")
|
accept := r.Header.Get("Accept-Language")
|
||||||
localizer := i18n.NewLocalizer(h.bundle, accept)
|
localizer := i18n.NewLocalizer(h.bundle, accept)
|
||||||
|
|
||||||
|
certEmails := h.getCertEmails(r)
|
||||||
|
|
||||||
|
var loginInfo LoginInformation
|
||||||
validate := validator.New()
|
validate := validator.New()
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
// render login form
|
if certEmails != nil {
|
||||||
h.renderLoginForm(w, r, map[string]string{}, &LoginInformation{}, localizer)
|
h.renderRequestForClientCert(w, r, certEmails, localizer)
|
||||||
|
} else {
|
||||||
|
// render login form
|
||||||
|
h.renderLoginForm(w, r, map[string]string{}, &LoginInformation{}, localizer)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
var loginInfo LoginInformation
|
var userId *string
|
||||||
|
var authMethod acrType
|
||||||
|
|
||||||
// validate input
|
if certEmails != nil && r.PostFormValue("action") == "cert-login" {
|
||||||
decoder := form.NewDecoder()
|
if r.PostFormValue("use-certificate") == "" {
|
||||||
err = decoder.Decode(&loginInfo, r.Form)
|
// render login form
|
||||||
if err != nil {
|
h.renderLoginForm(w, r, map[string]string{}, &LoginInformation{}, localizer)
|
||||||
h.logger.Error(err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err := validate.Struct(&loginInfo)
|
|
||||||
if err != nil {
|
|
||||||
errors := make(map[string]string)
|
|
||||||
for _, err := range err.(validator.ValidationErrors) {
|
|
||||||
accept := r.Header.Get("Accept-Language")
|
|
||||||
errors[err.Field()] = h.messageCatalog.LookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(h.bundle, accept))
|
|
||||||
}
|
|
||||||
h.renderLoginForm(w, r, errors, &loginInfo, localizer)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
db := services.GetDb(h.context)
|
|
||||||
|
|
||||||
stmt, err := db.PrepareContext(
|
|
||||||
r.Context(),
|
|
||||||
`SELECT id
|
|
||||||
FROM users
|
|
||||||
WHERE email = ?
|
|
||||||
AND password = ?
|
|
||||||
AND locked = 0`,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Errorf("error preparing login SQL: %v", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func() { _ = stmt.Close() }()
|
|
||||||
|
|
||||||
// FIXME: replace with a real password hash algorithm
|
|
||||||
passwordHash := sha1.Sum([]byte(loginInfo.Password))
|
|
||||||
password := hex.EncodeToString(passwordHash[:])
|
|
||||||
// FIXME: introduce a real opaque identifier (i.e. a UUID)
|
|
||||||
var userId string
|
|
||||||
// GET user data
|
|
||||||
err = stmt.QueryRowContext(r.Context(), loginInfo.Email, password).Scan(&userId)
|
|
||||||
switch {
|
|
||||||
case err == sql.ErrNoRows:
|
|
||||||
errors := map[string]string{
|
|
||||||
"Form": h.messageCatalog.LookupMessage(
|
|
||||||
"WrongOrLockedUserOrInvalidPassword",
|
|
||||||
nil,
|
|
||||||
localizer,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
h.renderLoginForm(w, r, errors, &loginInfo, localizer)
|
|
||||||
return
|
|
||||||
case err != nil:
|
|
||||||
h.logger.Errorf("error performing login SQL: %v", err)
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// finish login and redirect to target
|
|
||||||
loginRequest, err := h.adminClient.AcceptLoginRequest(
|
|
||||||
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{
|
|
||||||
Acr: string(Password),
|
|
||||||
Remember: true,
|
|
||||||
RememberFor: 0,
|
|
||||||
Subject: &userId,
|
|
||||||
}).WithTimeout(time.Second * 10))
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Errorf("error getting login request: %#v", err)
|
|
||||||
http.Error(w, err.Error(), err.(*runtime.APIError).Code)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
|
// perform certificate auth
|
||||||
w.WriteHeader(http.StatusFound)
|
h.logger.Infof("would perform certificate authentication with: %+v", certEmails)
|
||||||
|
userId, err = h.performCertificateLogin(certEmails, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userId == nil {
|
||||||
|
errors := map[string]string{
|
||||||
|
"Form": h.messageCatalog.LookupMessage(
|
||||||
|
"WrongOrLockedUserOrInvalidPassword",
|
||||||
|
nil,
|
||||||
|
localizer,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
h.renderLoginForm(w, r, errors, &loginInfo, localizer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authMethod = ClientCertificate
|
||||||
|
} else {
|
||||||
|
decoder := form.NewDecoder()
|
||||||
|
// validate input
|
||||||
|
err = decoder.Decode(&loginInfo, r.Form)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error(err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := validate.Struct(&loginInfo)
|
||||||
|
if err != nil {
|
||||||
|
errors := make(map[string]string)
|
||||||
|
for _, err := range err.(validator.ValidationErrors) {
|
||||||
|
accept := r.Header.Get("Accept-Language")
|
||||||
|
errors[err.Field()] = h.messageCatalog.LookupErrorMessage(
|
||||||
|
err.Tag(),
|
||||||
|
err.Field(),
|
||||||
|
err.Value(),
|
||||||
|
i18n.NewLocalizer(h.bundle, accept),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
h.renderLoginForm(w, r, errors, &loginInfo, localizer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId, err = h.performUserNamePasswordLogin(&loginInfo, r)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if userId == nil {
|
||||||
|
errors := map[string]string{
|
||||||
|
"Form": h.messageCatalog.LookupMessage(
|
||||||
|
"WrongOrLockedUserOrInvalidPassword",
|
||||||
|
nil,
|
||||||
|
localizer,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
h.renderLoginForm(w, r, errors, &loginInfo, localizer)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
authMethod = Password
|
||||||
}
|
}
|
||||||
break
|
|
||||||
|
// finish login and redirect to target
|
||||||
|
loginRequest, err := h.adminClient.AcceptLoginRequest(
|
||||||
|
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{
|
||||||
|
Acr: string(authMethod),
|
||||||
|
Remember: true,
|
||||||
|
RememberFor: 0,
|
||||||
|
Subject: userId,
|
||||||
|
}).WithTimeout(time.Second * 10))
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("error getting login request: %#v", err)
|
||||||
|
http.Error(w, err.Error(), err.(*runtime.APIError).Code)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
|
||||||
|
w.WriteHeader(http.StatusFound)
|
||||||
default:
|
default:
|
||||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *loginHandler) getCertEmails(r *http.Request) []string {
|
||||||
|
if r.TLS != nil && r.TLS.PeerCertificates != nil && len(r.TLS.PeerCertificates) > 0 {
|
||||||
|
firstCert := r.TLS.PeerCertificates[0]
|
||||||
|
for _, email := range firstCert.EmailAddresses {
|
||||||
|
h.logger.Infof("authenticated with a client certificate for email address %s", email)
|
||||||
|
}
|
||||||
|
return firstCert.EmailAddresses
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *loginHandler) renderLoginForm(w http.ResponseWriter, r *http.Request, errors map[string]string, info *LoginInformation, localizer *i18n.Localizer) {
|
func (h *loginHandler) renderLoginForm(w http.ResponseWriter, r *http.Request, errors map[string]string, info *LoginInformation, localizer *i18n.Localizer) {
|
||||||
trans := func(label string) string {
|
trans := func(label string) string {
|
||||||
return h.messageCatalog.LookupMessage(label, nil, localizer)
|
return h.messageCatalog.LookupMessage(label, nil, localizer)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{
|
err := h.templates[Password].Lookup("base").Execute(w, map[string]interface{}{
|
||||||
"Title": trans("LoginTitle"),
|
"Title": trans("LoginTitle"),
|
||||||
csrf.TemplateTag: csrf.TemplateField(r),
|
csrf.TemplateTag: csrf.TemplateField(r),
|
||||||
"LabelEmail": trans("LabelEmail"),
|
"LabelEmail": trans("LabelEmail"),
|
||||||
|
@ -168,18 +194,121 @@ func (h *loginHandler) renderLoginForm(w http.ResponseWriter, r *http.Request, e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *loginHandler) renderRequestForClientCert(w http.ResponseWriter, r *http.Request, emails []string, localizer *i18n.Localizer) {
|
||||||
|
trans := func(label string) string {
|
||||||
|
return h.messageCatalog.LookupMessage(label, nil, localizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.templates[ClientCertificate].Lookup("base").Execute(w, map[string]interface{}{
|
||||||
|
"Title": trans("LoginTitle"),
|
||||||
|
csrf.TemplateTag: csrf.TemplateField(r),
|
||||||
|
"IntroText": trans("CertLoginIntroText"),
|
||||||
|
"emails": emails,
|
||||||
|
"RequestText": trans("CertLoginRequestText"),
|
||||||
|
"AcceptLabel": trans("LabelAcceptCertLogin"),
|
||||||
|
"RejectLabel": trans("LabelRejectCertLogin"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error(err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *loginHandler) performUserNamePasswordLogin(loginInfo *LoginInformation, r *http.Request) (*string, error) {
|
||||||
|
db := services.GetDb(h.context)
|
||||||
|
|
||||||
|
stmt, err := db.PrepareContext(
|
||||||
|
r.Context(),
|
||||||
|
`SELECT uniqueID
|
||||||
|
FROM users
|
||||||
|
WHERE email = ?
|
||||||
|
AND password = ?
|
||||||
|
AND locked = 0`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("error preparing login SQL: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = stmt.Close() }()
|
||||||
|
|
||||||
|
// FIXME: replace with a real password hash algorithm
|
||||||
|
passwordHash := sha1.Sum([]byte(loginInfo.Password))
|
||||||
|
password := hex.EncodeToString(passwordHash[:])
|
||||||
|
|
||||||
|
var userId string
|
||||||
|
// GET user data
|
||||||
|
err = stmt.QueryRowContext(r.Context(), loginInfo.Email, password).Scan(&userId)
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return nil, nil
|
||||||
|
case err != nil:
|
||||||
|
h.logger.Errorf("error performing login SQL: %v", err)
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
h.logger.Infof("found user %s", userId)
|
||||||
|
return &userId, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *loginHandler) performCertificateLogin(emails []string, r *http.Request) (*string, error) {
|
||||||
|
db := services.GetDb(h.context)
|
||||||
|
|
||||||
|
query, args, err := sqlx.In(
|
||||||
|
`SELECT DISTINCT u.uniqueID
|
||||||
|
FROM users u
|
||||||
|
JOIN email e ON e.memid = u.id
|
||||||
|
WHERE e.email IN (?)
|
||||||
|
AND u.locked = 0`,
|
||||||
|
emails,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("could not parse IN query for certificate login: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stmt, err := db.PreparexContext(r.Context(), query)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Errorf("error preparing login SQL: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = stmt.Close() }()
|
||||||
|
|
||||||
|
var userId string
|
||||||
|
err = stmt.QueryRowContext(r.Context(), args...).Scan(&userId)
|
||||||
|
switch {
|
||||||
|
case err == sql.ErrNoRows:
|
||||||
|
return nil, nil
|
||||||
|
case err != nil:
|
||||||
|
h.logger.Errorf("error performing login SQL: %v", err)
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
h.logger.Infof("found user %s", userId)
|
||||||
|
return &userId, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewLoginHandler(ctx context.Context, logger *log.Logger) (*loginHandler, error) {
|
func NewLoginHandler(ctx context.Context, logger *log.Logger) (*loginHandler, error) {
|
||||||
|
var err error
|
||||||
loginTemplate, err := template.ParseFiles(
|
loginTemplate, err := template.ParseFiles(
|
||||||
"templates/idp/base.gohtml", "templates/idp/login.gohtml")
|
"templates/idp/base.gohtml", "templates/idp/login.gohtml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
clientCertTemplate, err := template.ParseFiles(
|
||||||
|
"templates/idp/base.gohtml", "templates/idp/client_certificate.gohtml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
formTemplates := map[acrType]*template.Template{
|
||||||
|
Password: loginTemplate,
|
||||||
|
ClientCertificate: clientCertTemplate,
|
||||||
|
}
|
||||||
return &loginHandler{
|
return &loginHandler{
|
||||||
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
|
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
|
||||||
bundle: commonServices.GetI18nBundle(ctx),
|
bundle: commonServices.GetI18nBundle(ctx),
|
||||||
context: ctx,
|
context: ctx,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
loginTemplate: loginTemplate,
|
templates: formTemplates,
|
||||||
messageCatalog: commonServices.GetMessageCatalog(ctx),
|
messageCatalog: commonServices.GetMessageCatalog(ctx),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,5 +89,23 @@ func AddMessages(ctx context.Context) {
|
||||||
Description: "Label for a login button",
|
Description: "Label for a login button",
|
||||||
Other: "Login",
|
Other: "Login",
|
||||||
}
|
}
|
||||||
|
messages["CertLoginIntroText"] = &i18n.Message{
|
||||||
|
ID: "CertLoginIntroText",
|
||||||
|
Other: "You have presented a valid client certificate for the following email addresses:",
|
||||||
|
}
|
||||||
|
messages["CertLoginRequestText"] = &i18n.Message{
|
||||||
|
ID: "CertLoginRequestText",
|
||||||
|
Other: "Do you want to use this certificate for authentication or do you want to use a different method?",
|
||||||
|
}
|
||||||
|
messages["LabelAcceptCertLogin"] = &i18n.Message{
|
||||||
|
ID: "LabelAcceptCertLogin",
|
||||||
|
Description: "Label for a button to accept certificate login",
|
||||||
|
Other: "Yes, please use the certificate",
|
||||||
|
}
|
||||||
|
messages["LabelRejectCertLogin"] = &i18n.Message{
|
||||||
|
ID: "LabelRejectCertLogin",
|
||||||
|
Description: "Label for a button to reject certificate login",
|
||||||
|
Other: "No, please ask for my password",
|
||||||
|
}
|
||||||
services.GetMessageCatalog(ctx).AddMessages(messages)
|
services.GetMessageCatalog(ctx).AddMessages(messages)
|
||||||
}
|
}
|
||||||
|
|
14
templates/idp/client_certificate.gohtml
Normal file
14
templates/idp/client_certificate.gohtml
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
<p>{{ .IntroText }}</p>
|
||||||
|
<ul>
|
||||||
|
{{ range .emails }}
|
||||||
|
<li>{{ . }}</li>{{ end }}
|
||||||
|
</ul>
|
||||||
|
<p>{{ .RequestText }}</p>
|
||||||
|
<form method="post">
|
||||||
|
{{ .csrfField }}
|
||||||
|
<input type="hidden" name="action" value="cert-login"/>
|
||||||
|
<button type="submit" name="use-certificate" value="yes">{{ .AcceptLabel }}</button>
|
||||||
|
<button type="submit">{{ .RejectLabel }}</button>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
|
@ -1,6 +1,7 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{{ .csrfField }}
|
{{ .csrfField }}
|
||||||
|
<input type="hidden" name="action" value="password-login"/>
|
||||||
{{ if .errors.Form}}<p>{{ .errors.Form }}</p>{{ end }}
|
{{ if .errors.Form}}<p>{{ .errors.Form }}</p>{{ end }}
|
||||||
{{ if .errors.Email }}<p>{{ .errors.Email }}</p>{{ end }}
|
{{ if .errors.Email }}<p>{{ .errors.Email }}</p>{{ end }}
|
||||||
<label for="email">{{ .LabelEmail }}</label>
|
<label for="email">{{ .LabelEmail }}</label>
|
||||||
|
|
|
@ -1,18 +1,4 @@
|
||||||
[FormLabelEmail]
|
[LabelAcceptCertLogin]
|
||||||
description = "Label for an email form field"
|
description = "Label for a button to accept certificate login"
|
||||||
hash = "sha1-ce1aa6771caccb8c901c6627e7ab5c554e9944da"
|
hash = "sha1-95cf27f4bdee62b51ee8bc673d25a46bcceed452"
|
||||||
other = "E-Mail:"
|
other = "Ja, bitte nutze das Zertifikat"
|
||||||
|
|
||||||
[FormLabelPassword]
|
|
||||||
description = "Label for a password form field"
|
|
||||||
hash = "sha1-a3c9deb12ea191bb380ea7ad076417b1fff14f28"
|
|
||||||
other = "Passwort:"
|
|
||||||
|
|
||||||
[LabelLogin]
|
|
||||||
description = "Label for a login button"
|
|
||||||
hash = "sha1-ff00822024fca849fe0cef21237b57218e706852"
|
|
||||||
other = "Anmelden"
|
|
||||||
|
|
||||||
[LoginTitle]
|
|
||||||
hash = "sha1-4e5a2893bdcc7d239c1db72e4c4ffbe4bea73174"
|
|
||||||
other = "Anmeldung"
|
|
||||||
|
|
Reference in a new issue