2020-12-31 09:42:48 +01:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-01-01 12:28:33 +01:00
|
|
|
"crypto/sha1"
|
|
|
|
"database/sql"
|
|
|
|
"encoding/hex"
|
2020-12-31 09:42:48 +01:00
|
|
|
"html/template"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2020-12-31 19:11:06 +01:00
|
|
|
"github.com/go-openapi/runtime"
|
2020-12-31 09:42:48 +01:00
|
|
|
"github.com/go-playground/form/v4"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
|
|
"github.com/gorilla/csrf"
|
|
|
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
|
|
|
"github.com/ory/hydra-client-go/client/admin"
|
|
|
|
"github.com/ory/hydra-client-go/models"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
|
2021-01-01 09:20:49 +01:00
|
|
|
commonServices "git.cacert.org/oidc_login/common/services"
|
2021-01-01 12:28:33 +01:00
|
|
|
"git.cacert.org/oidc_login/idp/services"
|
2020-12-31 09:42:48 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type loginHandler struct {
|
|
|
|
adminClient *admin.Client
|
2020-12-31 19:11:06 +01:00
|
|
|
bundle *i18n.Bundle
|
2021-01-01 12:28:33 +01:00
|
|
|
context context.Context
|
2020-12-31 13:19:21 +01:00
|
|
|
logger *log.Logger
|
2020-12-31 19:11:06 +01:00
|
|
|
loginTemplate *template.Template
|
2021-01-01 09:20:49 +01:00
|
|
|
messageCatalog *commonServices.MessageCatalog
|
2020-12-31 09:42:48 +01:00
|
|
|
}
|
|
|
|
|
2020-12-31 19:11:06 +01:00
|
|
|
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"
|
|
|
|
)
|
|
|
|
|
2020-12-31 09:42:48 +01:00
|
|
|
type LoginInformation struct {
|
|
|
|
Email string `form:"email" validate:"required,email"`
|
|
|
|
Password string `form:"password" validate:"required"`
|
|
|
|
}
|
|
|
|
|
2020-12-31 13:19:21 +01:00
|
|
|
func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
2020-12-31 09:42:48 +01:00
|
|
|
var err error
|
2020-12-31 13:19:21 +01:00
|
|
|
challenge := r.URL.Query().Get("login_challenge")
|
2020-12-31 19:11:06 +01:00
|
|
|
h.logger.Debugf("received login challenge %s\n", challenge)
|
2021-01-01 12:28:33 +01:00
|
|
|
accept := r.Header.Get("Accept-Language")
|
|
|
|
localizer := i18n.NewLocalizer(h.bundle, accept)
|
|
|
|
|
2020-12-31 09:42:48 +01:00
|
|
|
validate := validator.New()
|
|
|
|
|
2020-12-31 13:19:21 +01:00
|
|
|
switch r.Method {
|
2020-12-31 09:42:48 +01:00
|
|
|
case http.MethodGet:
|
2020-12-31 19:11:06 +01:00
|
|
|
// render login form
|
2021-01-01 12:28:33 +01:00
|
|
|
h.renderLoginForm(w, r, map[string]string{}, &LoginInformation{}, localizer)
|
2020-12-31 09:42:48 +01:00
|
|
|
break
|
|
|
|
case http.MethodPost:
|
|
|
|
var loginInfo LoginInformation
|
|
|
|
|
|
|
|
// validate input
|
|
|
|
decoder := form.NewDecoder()
|
2020-12-31 13:19:21 +01:00
|
|
|
err = decoder.Decode(&loginInfo, r.Form)
|
2020-12-31 09:42:48 +01:00
|
|
|
if err != nil {
|
2020-12-31 13:19:21 +01:00
|
|
|
h.logger.Error(err)
|
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
2020-12-31 09:42:48 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
err := validate.Struct(&loginInfo)
|
|
|
|
if err != nil {
|
|
|
|
errors := make(map[string]string)
|
|
|
|
for _, err := range err.(validator.ValidationErrors) {
|
2020-12-31 13:19:21 +01:00
|
|
|
accept := r.Header.Get("Accept-Language")
|
2020-12-31 19:11:06 +01:00
|
|
|
errors[err.Field()] = h.messageCatalog.LookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(h.bundle, accept))
|
2020-12-31 09:42:48 +01:00
|
|
|
}
|
2021-01-01 12:28:33 +01:00
|
|
|
h.renderLoginForm(w, r, errors, &loginInfo, localizer)
|
|
|
|
return
|
|
|
|
}
|
2020-12-31 09:42:48 +01:00
|
|
|
|
2021-01-01 12:28:33 +01:00
|
|
|
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)
|
2020-12-31 09:42:48 +01:00
|
|
|
return
|
|
|
|
}
|
2021-01-01 12:28:33 +01:00
|
|
|
defer func() { _ = stmt.Close() }()
|
2020-12-31 09:42:48 +01:00
|
|
|
|
2021-01-01 12:28:33 +01:00
|
|
|
// 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
|
2020-12-31 09:42:48 +01:00
|
|
|
// GET user data
|
2021-01-01 12:28:33 +01:00
|
|
|
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)
|
2020-12-31 19:11:06 +01:00
|
|
|
return
|
2021-01-01 12:28:33 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
|
|
|
|
w.WriteHeader(http.StatusFound)
|
2020-12-31 09:42:48 +01:00
|
|
|
}
|
|
|
|
break
|
|
|
|
default:
|
2020-12-31 13:19:21 +01:00
|
|
|
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
2020-12-31 09:42:48 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-01 12:28:33 +01:00
|
|
|
func (h *loginHandler) renderLoginForm(w http.ResponseWriter, r *http.Request, errors map[string]string, info *LoginInformation, localizer *i18n.Localizer) {
|
|
|
|
trans := func(label string) string {
|
|
|
|
return h.messageCatalog.LookupMessage(label, nil, localizer)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{
|
|
|
|
"Title": trans("LoginTitle"),
|
|
|
|
csrf.TemplateTag: csrf.TemplateField(r),
|
|
|
|
"LabelEmail": trans("LabelEmail"),
|
|
|
|
"LabelPassword": trans("LabelPassword"),
|
|
|
|
"LabelLogin": trans("LabelLogin"),
|
|
|
|
"Email": info.Email,
|
|
|
|
"errors": errors,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
h.logger.Error(err)
|
|
|
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-01 09:20:49 +01:00
|
|
|
func NewLoginHandler(ctx context.Context, logger *log.Logger) (*loginHandler, error) {
|
|
|
|
loginTemplate, err := template.ParseFiles(
|
|
|
|
"templates/idp/base.gohtml", "templates/idp/login.gohtml")
|
2020-12-31 09:42:48 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &loginHandler{
|
2020-12-31 19:11:06 +01:00
|
|
|
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
|
2021-01-01 09:20:49 +01:00
|
|
|
bundle: commonServices.GetI18nBundle(ctx),
|
2021-01-01 12:28:33 +01:00
|
|
|
context: ctx,
|
2020-12-31 13:19:21 +01:00
|
|
|
logger: logger,
|
2020-12-31 09:42:48 +01:00
|
|
|
loginTemplate: loginTemplate,
|
2021-01-01 09:20:49 +01:00
|
|
|
messageCatalog: commonServices.GetMessageCatalog(ctx),
|
2020-12-31 09:42:48 +01:00
|
|
|
}, nil
|
|
|
|
}
|