Refactor IDP code
This commit is contained in:
parent
c0e9e88dba
commit
ce1fac0e68
11 changed files with 482 additions and 341 deletions
7
idp/handlers/common.go
Normal file
7
idp/handlers/common.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package handlers
|
||||
|
||||
type handlerContextKey int
|
||||
|
||||
const (
|
||||
CtxAdminClient handlerContextKey = iota
|
||||
)
|
39
idp/handlers/consent.go
Normal file
39
idp/handlers/consent.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ory/hydra-client-go/client/admin"
|
||||
"github.com/ory/hydra-client-go/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type consentHandler struct {
|
||||
adminClient *admin.Client
|
||||
}
|
||||
|
||||
func (c *consentHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
consentChallenge := request.URL.Query().Get("consent_challenge")
|
||||
consentRequest, err := c.adminClient.AcceptConsentRequest(
|
||||
admin.NewAcceptConsentRequestParams().WithConsentChallenge(consentChallenge).WithBody(
|
||||
&models.AcceptConsentRequest{
|
||||
GrantAccessTokenAudience: nil,
|
||||
GrantScope: []string{"openid", "offline"},
|
||||
HandledAt: models.NullTime(time.Now()),
|
||||
Remember: true,
|
||||
RememberFor: 86400,
|
||||
}).WithTimeout(time.Second * 10))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
writer.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
|
||||
writer.WriteHeader(http.StatusFound)
|
||||
}
|
||||
|
||||
func NewConsentHandler(ctx context.Context) *consentHandler {
|
||||
return &consentHandler{
|
||||
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
|
||||
}
|
||||
}
|
157
idp/handlers/login.go
Normal file
157
idp/handlers/login.go
Normal file
|
@ -0,0 +1,157 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
|
||||
"git.cacert.org/oidc_login/idp/services"
|
||||
)
|
||||
|
||||
type loginHandler struct {
|
||||
loginTemplate *template.Template
|
||||
bundle *i18n.Bundle
|
||||
messageCatalog map[string]*i18n.Message
|
||||
adminClient *admin.Client
|
||||
}
|
||||
|
||||
type LoginInformation struct {
|
||||
Email string `form:"email" validate:"required,email"`
|
||||
Password string `form:"password" validate:"required"`
|
||||
}
|
||||
|
||||
func (h *loginHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
var err error
|
||||
challenge := request.URL.Query().Get("login_challenge")
|
||||
log.Debugf("received challenge %s\n", challenge)
|
||||
validate := validator.New()
|
||||
|
||||
switch request.Method {
|
||||
case http.MethodGet:
|
||||
// GET should render login form
|
||||
|
||||
err = h.loginTemplate.Lookup("base").Execute(writer, map[string]interface{}{
|
||||
"Title": "Title",
|
||||
csrf.TemplateTag: csrf.TemplateField(request),
|
||||
"LabelEmail": "Email",
|
||||
"LabelPassword": "Password",
|
||||
"LabelLogin": "Login",
|
||||
"errors": map[string]string{},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
break
|
||||
case http.MethodPost:
|
||||
// POST should perform the action
|
||||
var loginInfo LoginInformation
|
||||
|
||||
// validate input
|
||||
decoder := form.NewDecoder()
|
||||
err = decoder.Decode(&loginInfo, request.Form)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(writer, 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 := request.Header.Get("Accept-Language")
|
||||
errors[err.Field()] = h.lookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(h.bundle, accept))
|
||||
}
|
||||
|
||||
err = h.loginTemplate.Lookup("base").Execute(writer, map[string]interface{}{
|
||||
"Title": "Title",
|
||||
csrf.TemplateTag: csrf.TemplateField(request),
|
||||
"LabelEmail": "Email",
|
||||
"LabelPassword": "Password",
|
||||
"LabelLogin": "Login",
|
||||
"Email": loginInfo.Email,
|
||||
"errors": errors,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GET user data
|
||||
// finish login and redirect to target
|
||||
// TODO: get or generate a user id
|
||||
subject := "a-user-with-an-id"
|
||||
loginRequest, err := h.adminClient.AcceptLoginRequest(
|
||||
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{
|
||||
Acr: "no-creds",
|
||||
Remember: true,
|
||||
RememberFor: 0,
|
||||
Subject: &subject,
|
||||
}).WithTimeout(time.Second * 10))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
writer.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
|
||||
writer.WriteHeader(http.StatusFound)
|
||||
break
|
||||
default:
|
||||
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *loginHandler) lookupErrorMessage(tag string, field string, value interface{}, l *i18n.Localizer) string {
|
||||
var message *i18n.Message
|
||||
message, ok := h.messageCatalog[fmt.Sprintf("%s-%s", field, tag)]
|
||||
if !ok {
|
||||
log.Infof("no specific error message %s-%s", field, tag)
|
||||
message, ok = h.messageCatalog[tag]
|
||||
if !ok {
|
||||
log.Infof("no specific error message %s", tag)
|
||||
message, ok = h.messageCatalog["unknown"]
|
||||
if !ok {
|
||||
log.Error("no default translation found")
|
||||
return tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
translation, err := l.Localize(&i18n.LocalizeConfig{
|
||||
DefaultMessage: message,
|
||||
TemplateData: map[string]interface{}{
|
||||
"Value": value,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return tag
|
||||
}
|
||||
return translation
|
||||
}
|
||||
|
||||
func NewLoginHandler(ctx context.Context) (*loginHandler, error) {
|
||||
loginTemplate, err := template.ParseFiles("templates/base.html", "templates/login.html")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &loginHandler{
|
||||
loginTemplate: loginTemplate,
|
||||
bundle: ctx.Value(services.CtxI18nBundle).(*i18n.Bundle),
|
||||
messageCatalog: ctx.Value(services.CtxI18nCatalog).(map[string]*i18n.Message),
|
||||
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
|
||||
}, nil
|
||||
}
|
55
idp/services/i18n.go
Normal file
55
idp/services/i18n.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
const (
|
||||
CtxI18nBundle contextKey = iota
|
||||
CtxI18nCatalog
|
||||
)
|
||||
|
||||
func InitI18n(ctx context.Context, logger *log.Logger) context.Context {
|
||||
bundle := i18n.NewBundle(language.English)
|
||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
_, err := bundle.LoadMessageFile("de.toml")
|
||||
if err != nil {
|
||||
logger.Warnln("message bundle de.toml not found")
|
||||
}
|
||||
catalog := initMessageCatalog()
|
||||
ctx = context.WithValue(ctx, CtxI18nBundle, bundle)
|
||||
ctx = context.WithValue(ctx, CtxI18nCatalog, catalog)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func initMessageCatalog() map[string]*i18n.Message {
|
||||
messageCatalog := make(map[string]*i18n.Message)
|
||||
messageCatalog["unknown"] = &i18n.Message{
|
||||
ID: "ErrorUnknown",
|
||||
Other: "Unknown error",
|
||||
}
|
||||
messageCatalog["email"] = &i18n.Message{
|
||||
ID: "ErrorEmail",
|
||||
Other: "Please enter a valid email address.",
|
||||
}
|
||||
messageCatalog["Email-required"] = &i18n.Message{
|
||||
ID: "ErrorEmailRequired",
|
||||
Other: "Please enter an email address.",
|
||||
}
|
||||
messageCatalog["required"] = &i18n.Message{
|
||||
ID: "ErrorRequired",
|
||||
Other: "Please enter a value",
|
||||
}
|
||||
messageCatalog["Password-required"] = &i18n.Message{
|
||||
ID: "ErrorPasswordRequired",
|
||||
Other: "Please enter a password.",
|
||||
}
|
||||
return messageCatalog
|
||||
}
|
Reference in a new issue