Implement consent workflow

This commit is contained in:
Jan Dittberner 2020-12-31 19:11:06 +01:00
parent 7a2174ea41
commit e4f17ca315
12 changed files with 382 additions and 140 deletions

View file

@ -5,6 +5,9 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwt/openid"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"git.cacert.org/oidc_login/app/services" "git.cacert.org/oidc_login/app/services"
@ -21,7 +24,7 @@ func Authenticate(oauth2Config *oauth2.Config, clientId string) func(http.Handle
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
if _, ok := session.Values[sessionKeyUserId]; ok { if _, ok := session.Values[sessionKeyIdToken]; ok {
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
@ -47,3 +50,12 @@ func Authenticate(oauth2Config *oauth2.Config, clientId string) func(http.Handle
}) })
} }
} }
func ParseIdToken(token string, keySet *jwk.Set) (openid.Token, error) {
if parsedIdToken, err := jwt.ParseString(token, jwt.WithKeySet(keySet), jwt.WithOpenIDClaims()); err != nil {
return nil, err
} else {
return parsedIdToken.(openid.Token), nil
}
}

View file

@ -6,12 +6,15 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"github.com/lestrrat-go/jwx/jwk"
"git.cacert.org/oidc_login/app/services" "git.cacert.org/oidc_login/app/services"
) )
type indexHandler struct { type indexHandler struct {
logoutUrl string logoutUrl string
serverAddr string serverAddr string
keySet *jwk.Set
} }
func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
@ -51,21 +54,26 @@ func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
http.Error(writer, err.Error(), http.StatusInternalServerError) http.Error(writer, err.Error(), http.StatusInternalServerError)
return return
} }
var user string var idToken string
var ok bool var ok bool
if user, ok = session.Values[sessionKeyUsername].(string); ok { if idToken, ok = session.Values[sessionKeyIdToken].(string); ok {
}
if idToken, ok := session.Values[sessionKeyIdToken].(string); ok {
logoutUrl.RawQuery = url.Values{ logoutUrl.RawQuery = url.Values{
"id_token_hint": []string{idToken}, "id_token_hint": []string{idToken},
"post_logout_redirect_uri": []string{fmt.Sprintf("https://%s/after-logout", h.serverAddr)}, "post_logout_redirect_uri": []string{fmt.Sprintf("https://%s/after-logout", h.serverAddr)},
}.Encode() }.Encode()
} else {
return
}
oidcToken, err := ParseIdToken(idToken, h.keySet)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
} }
writer.Header().Add("Content-Type", "text/html") writer.Header().Add("Content-Type", "text/html")
err = page.Execute(writer, map[string]interface{}{ err = page.Execute(writer, map[string]interface{}{
"User": user, "User": oidcToken.Name(),
"LogoutURL": logoutUrl.String(), "LogoutURL": logoutUrl.String(),
}) })
if err != nil { if err != nil {
@ -74,6 +82,6 @@ func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
} }
} }
func NewIndexHandler(logoutUrl string, serverAddr string) *indexHandler { func NewIndexHandler(logoutUrl string, serverAddr string, keySet *jwk.Set) *indexHandler {
return &indexHandler{logoutUrl: logoutUrl, serverAddr: serverAddr} return &indexHandler{logoutUrl: logoutUrl, serverAddr: serverAddr, keySet: keySet}
} }

View file

@ -6,8 +6,7 @@ import (
"github.com/go-openapi/runtime/client" "github.com/go-openapi/runtime/client"
"github.com/lestrrat-go/jwx/jwk" "github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt" log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"git.cacert.org/oidc_login/app/services" "git.cacert.org/oidc_login/app/services"
@ -17,10 +16,6 @@ const (
sessionKeyAccessToken = iota sessionKeyAccessToken = iota
sessionKeyRefreshToken sessionKeyRefreshToken
sessionKeyIdToken sessionKeyIdToken
sessionKeyUserId
sessionKeyRoles
sessionKeyEmail
sessionKeyUsername
sessionRedirectTarget sessionRedirectTarget
) )
@ -29,17 +24,24 @@ type oidcCallbackHandler struct {
oauth2Config *oauth2.Config oauth2Config *oauth2.Config
} }
func (c *oidcCallbackHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { func (c *oidcCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if request.Method != http.MethodGet { if r.Method != http.MethodGet {
http.Error(writer, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return return
} }
if request.URL.Path != "/callback" { if r.URL.Path != "/callback" {
http.NotFound(writer, request) http.NotFound(w, r)
return return
} }
code := request.URL.Query().Get("code") errorText := r.URL.Query().Get("error")
errorDescription := r.URL.Query().Get("error_description")
if errorText != "" {
c.RenderErrorTemplate(w, r, errorText, errorDescription)
return
}
code := r.URL.Query().Get("code")
ctx := context.Background() ctx := context.Background()
httpClient, err := client.TLSClient(client.TLSClientOptions{InsecureSkipVerify: true}) httpClient, err := client.TLSClient(client.TLSClientOptions{InsecureSkipVerify: true})
@ -47,28 +49,29 @@ func (c *oidcCallbackHandler) ServeHTTP(writer http.ResponseWriter, request *htt
tok, err := c.oauth2Config.Exchange(ctx, code) tok, err := c.oauth2Config.Exchange(ctx, code)
if err != nil { if err != nil {
logrus.Error(err) log.Error(err)
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
session, err := services.GetSessionStore().Get(request, "resource_session") session, err := services.GetSessionStore().Get(r, "resource_session")
if err != nil { if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
session.Values[sessionKeyAccessToken] = tok.AccessToken session.Values[sessionKeyAccessToken] = tok.AccessToken
session.Values[sessionKeyRefreshToken] = tok.RefreshToken session.Values[sessionKeyRefreshToken] = tok.RefreshToken
session.Values[sessionKeyIdToken] = tok.Extra("id_token").(string)
idToken := tok.Extra("id_token") idToken := tok.Extra("id_token").(string)
if parsedIdToken, err := jwt.ParseString(idToken.(string), jwt.WithKeySet(c.keySet), jwt.WithOpenIDClaims()); err != nil { session.Values[sessionKeyIdToken] = idToken
logrus.Error(err)
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) if oidcToken, err := ParseIdToken(idToken, c.keySet); err != nil {
log.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} else { } else {
logrus.Infof(` log.Infof(`
ID Token ID Token
======== ========
@ -80,36 +83,33 @@ Not valid before: %s
Not valid after: %s Not valid after: %s
`, `,
parsedIdToken.Subject(), oidcToken.Subject(),
parsedIdToken.Audience(), oidcToken.Audience(),
parsedIdToken.IssuedAt(), oidcToken.IssuedAt(),
parsedIdToken.Issuer(), oidcToken.Issuer(),
parsedIdToken.NotBefore(), oidcToken.NotBefore(),
parsedIdToken.Expiration(), oidcToken.Expiration(),
) )
session.Values[sessionKeyUserId] = parsedIdToken.Subject()
if roles, ok := parsedIdToken.Get("Groups"); ok {
session.Values[sessionKeyRoles] = roles
}
if username, ok := parsedIdToken.Get("Username"); ok {
session.Values[sessionKeyUsername] = username
}
if email, ok := parsedIdToken.Get("Email"); ok {
session.Values[sessionKeyEmail] = email
}
} }
if err = session.Save(request, writer); err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError) if err = session.Save(r, w); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
} }
if redirectTarget, ok := session.Values[sessionRedirectTarget]; ok { if redirectTarget, ok := session.Values[sessionRedirectTarget]; ok {
writer.Header().Set("Location", redirectTarget.(string)) w.Header().Set("Location", redirectTarget.(string))
} else { } else {
writer.Header().Set("Location", "/") w.Header().Set("Location", "/")
} }
writer.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
}
func (c *oidcCallbackHandler) RenderErrorTemplate(w http.ResponseWriter, r *http.Request, errorText string, errorDescription string) {
if errorDescription != "" {
http.Error(w, errorDescription, http.StatusForbidden)
} else {
http.Error(w, errorText, http.StatusForbidden)
}
} }
func NewCallbackHandler(keySet *jwk.Set, oauth2Config *oauth2.Config) *oidcCallbackHandler { func NewCallbackHandler(keySet *jwk.Set, oauth2Config *oauth2.Config) *oidcCallbackHandler {

View file

@ -133,7 +133,7 @@ func main() {
authMiddleware := handlers.Authenticate(oauth2Config, config.MustString("oidc.client-id")) authMiddleware := handlers.Authenticate(oauth2Config, config.MustString("oidc.client-id"))
serverAddr := fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port")) serverAddr := fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port"))
indexHandler := handlers.NewIndexHandler(discoveryResponse.EndSessionEndpoint, serverAddr) indexHandler := handlers.NewIndexHandler(discoveryResponse.EndSessionEndpoint, serverAddr, keySet)
callbackHandler := handlers.NewCallbackHandler(keySet, oauth2Config) callbackHandler := handlers.NewCallbackHandler(keySet, oauth2Config)
afterLogoutHandler := handlers.NewAfterLogoutHandler(logger) afterLogoutHandler := handlers.NewAfterLogoutHandler(logger)

View file

@ -86,7 +86,10 @@ func main() {
if err != nil { if err != nil {
logger.Fatalf("error initializing login handler: %v", err) logger.Fatalf("error initializing login handler: %v", err)
} }
consentHandler := handlers.NewConsentHandler(logger, handlerContext) consentHandler, err := handlers.NewConsentHandler(logger, handlerContext)
if err != nil {
logger.Fatalf("error initializing consent handler: %v", err)
}
logoutHandler := handlers.NewLogoutHandler(logger, handlerContext) logoutHandler := handlers.NewLogoutHandler(logger, handlerContext)
logoutSuccessHandler := handlers.NewLogoutSuccessHandler() logoutSuccessHandler := handlers.NewLogoutSuccessHandler()
errorHandler := handlers.NewErrorHandler() errorHandler := handlers.NewErrorHandler()

View file

@ -2,40 +2,166 @@ package handlers
import ( import (
"context" "context"
"fmt"
"html/template"
"net/http" "net/http"
"time" "time"
"github.com/go-playground/form/v4"
"github.com/gorilla/csrf"
"github.com/lestrrat-go/jwx/jwt/openid"
"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"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"git.cacert.org/oidc_login/idp/services"
) )
type consentHandler struct { type consentHandler struct {
adminClient *admin.Client adminClient *admin.Client
logger *log.Logger bundle *i18n.Bundle
consentTemplate *template.Template
logger *log.Logger
messageCatalog *services.MessageCatalog
}
type ConsentInformation struct {
ConsentChecked bool `form:"consent"`
} }
func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
consentChallenge := r.URL.Query().Get("consent_challenge") challenge := r.URL.Query().Get("consent_challenge")
consentRequest, err := h.adminClient.AcceptConsentRequest( h.logger.Debugf("received consent challenge %s", challenge)
admin.NewAcceptConsentRequestParams().WithConsentChallenge(consentChallenge).WithBody( accept := r.Header.Get("Accept-Language")
&models.AcceptConsentRequest{ localizer := i18n.NewLocalizer(h.bundle, accept)
GrantAccessTokenAudience: nil,
GrantScope: []string{"openid", "offline"}, // retrieve consent information
HandledAt: models.NullTime(time.Now()), consentData, err := h.adminClient.GetConsentRequest(
Remember: true, admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
RememberFor: 86400,
}).WithTimeout(time.Second * 10))
if err != nil { if err != nil {
h.logger.Panic(err) h.logger.Error("error getting consent information: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
switch r.Method {
case http.MethodGet:
trans := h.messageCatalog.LookupMessage
// render consent form
client := consentData.GetPayload().Client
err = h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
"Title": trans("TitleRequestConsent", nil, localizer),
csrf.TemplateTag: csrf.TemplateField(r),
"errors": map[string]string{},
"client": client,
"requestedScope": h.mapRequestedScope(consentData.GetPayload().RequestedScope, localizer),
"LabelSubmit": trans("LabelSubmit", nil, localizer),
"LabelConsent": trans("LabelConsent", nil, localizer),
"IntroMoreInformation": template.HTML(trans("IntroConsentMoreInformation", map[string]interface{}{
"client": client.ClientName,
"clientLink": client.ClientURI,
}, localizer)),
"IntroConsentRequested": template.HTML(trans("IntroConsentRequested", map[string]interface{}{
"client": client.ClientName,
}, localizer)),
})
break
case http.MethodPost:
var consentInfo ConsentInformation
// validate input
decoder := form.NewDecoder()
if err := decoder.Decode(&consentInfo, r.Form); err != nil {
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
if consentInfo.ConsentChecked {
idTokenData := make(map[string]interface{}, 0)
for _, scope := range consentData.GetPayload().RequestedScope {
switch scope {
case "email":
idTokenData[openid.EmailKey] = "john@theripper.mil"
idTokenData[openid.EmailVerifiedKey] = true
break
case "profile":
idTokenData[openid.GivenNameKey] = "John"
idTokenData[openid.FamilyNameKey] = "The ripper"
idTokenData[openid.MiddleNameKey] = ""
idTokenData[openid.NameKey] = "John the Ripper"
idTokenData[openid.BirthdateKey] = "1970-01-01"
idTokenData[openid.ZoneinfoKey] = "Europe/London"
idTokenData[openid.LocaleKey] = "en_UK"
idTokenData["https://cacert.localhost/groups"] = []string{"admin", "user"}
break
}
}
sessionData := &models.ConsentRequestSession{
AccessToken: nil,
IDToken: idTokenData,
}
consentRequest, err := h.adminClient.AcceptConsentRequest(
admin.NewAcceptConsentRequestParams().WithConsentChallenge(challenge).WithBody(
&models.AcceptConsentRequest{
GrantAccessTokenAudience: nil,
GrantScope: consentData.GetPayload().RequestedScope,
HandledAt: models.NullTime(time.Now()),
Remember: true,
RememberFor: 86400,
Session: sessionData,
}).WithTimeout(time.Second * 10))
if err != nil {
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
} else {
consentRequest, err := h.adminClient.RejectConsentRequest(
admin.NewRejectConsentRequestParams().WithConsentChallenge(challenge).WithBody(
&models.RejectRequest{}))
if err != nil {
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
}
} }
w.Header().Add("Location", *consentRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
} }
func NewConsentHandler(logger *log.Logger, ctx context.Context) *consentHandler { type scopeWithLabel struct {
return &consentHandler{ Name string
logger: logger, Label string
adminClient: ctx.Value(CtxAdminClient).(*admin.Client), }
}
func (h *consentHandler) mapRequestedScope(scope models.StringSlicePipeDelimiter, localizer *i18n.Localizer) []*scopeWithLabel {
result := make([]*scopeWithLabel, 0)
for _, scopeName := range scope {
result = append(result, &scopeWithLabel{Name: scopeName, Label: h.messageCatalog.LookupMessage(
fmt.Sprintf("Scope-%s-Description", scopeName), nil, localizer)})
}
return result
}
func NewConsentHandler(logger *log.Logger, ctx context.Context) (*consentHandler, error) {
consentTemplate, err := template.ParseFiles("templates/base.gohtml", "templates/consent.gohtml")
if err != nil {
return nil, err
}
return &consentHandler{
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
bundle: ctx.Value(services.CtxI18nBundle).(*i18n.Bundle),
consentTemplate: consentTemplate,
logger: logger,
messageCatalog: ctx.Value(services.CtxI18nCatalog).(*services.MessageCatalog),
}, nil
} }

View file

@ -2,11 +2,11 @@ package handlers
import ( import (
"context" "context"
"fmt"
"html/template" "html/template"
"net/http" "net/http"
"time" "time"
"github.com/go-openapi/runtime"
"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"
@ -19,13 +19,25 @@ import (
) )
type loginHandler struct { type loginHandler struct {
loginTemplate *template.Template
bundle *i18n.Bundle
messageCatalog map[string]*i18n.Message
adminClient *admin.Client adminClient *admin.Client
bundle *i18n.Bundle
logger *log.Logger logger *log.Logger
loginTemplate *template.Template
messageCatalog *services.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"`
@ -34,13 +46,12 @@ type LoginInformation struct {
func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var err error var err error
challenge := r.URL.Query().Get("login_challenge") challenge := r.URL.Query().Get("login_challenge")
h.logger.Debugf("received challenge %s\n", challenge) h.logger.Debugf("received login challenge %s\n", challenge)
validate := validator.New() validate := validator.New()
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
// GET should render login form // render login form
err = h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{ err = h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{
"Title": "Title", "Title": "Title",
csrf.TemplateTag: csrf.TemplateField(r), csrf.TemplateTag: csrf.TemplateField(r),
@ -56,7 +67,6 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
break break
case http.MethodPost: case http.MethodPost:
// POST should perform the action
var loginInfo LoginInformation var loginInfo LoginInformation
// validate input // validate input
@ -72,7 +82,7 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
errors := make(map[string]string) errors := make(map[string]string)
for _, err := range err.(validator.ValidationErrors) { for _, err := range err.(validator.ValidationErrors) {
accept := r.Header.Get("Accept-Language") accept := r.Header.Get("Accept-Language")
errors[err.Field()] = h.lookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(h.bundle, accept)) errors[err.Field()] = h.messageCatalog.LookupErrorMessage(err.Tag(), err.Field(), err.Value(), i18n.NewLocalizer(h.bundle, accept))
} }
err = h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{ err = h.loginTemplate.Lookup("base").Execute(w, map[string]interface{}{
@ -98,14 +108,15 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
subject := "a-user-with-an-id" subject := "a-user-with-an-id"
loginRequest, err := h.adminClient.AcceptLoginRequest( loginRequest, err := h.adminClient.AcceptLoginRequest(
admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{ admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(&models.AcceptLoginRequest{
Acr: "no-creds", Acr: string(NoCredentials),
Remember: true, Remember: true,
RememberFor: 0, RememberFor: 0,
Subject: &subject, Subject: &subject,
}).WithTimeout(time.Second * 10)) }).WithTimeout(time.Second * 10))
if err != nil { if err != nil {
h.logger.Errorf("error getting logout requests: %v", err) h.logger.Errorf("error getting login request: %#v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, err.Error(), err.(*runtime.APIError).Code)
return
} }
w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo) w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound) w.WriteHeader(http.StatusFound)
@ -116,45 +127,16 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
} }
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 {
h.logger.Infof("no specific error message %s-%s", field, tag)
message, ok = h.messageCatalog[tag]
if !ok {
h.logger.Infof("no specific error message %s", tag)
message, ok = h.messageCatalog["unknown"]
if !ok {
h.logger.Error("no default translation found")
return tag
}
}
}
translation, err := l.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: map[string]interface{}{
"Value": value,
},
})
if err != nil {
h.logger.Error(err)
return tag
}
return translation
}
func NewLoginHandler(logger *log.Logger, ctx context.Context) (*loginHandler, error) { func NewLoginHandler(logger *log.Logger, ctx context.Context) (*loginHandler, error) {
loginTemplate, err := template.ParseFiles("templates/base.html", "templates/login.html") loginTemplate, err := template.ParseFiles("templates/base.gohtml", "templates/login.gohtml")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &loginHandler{ return &loginHandler{
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
bundle: ctx.Value(services.CtxI18nBundle).(*i18n.Bundle),
logger: logger, logger: logger,
loginTemplate: loginTemplate, loginTemplate: loginTemplate,
bundle: ctx.Value(services.CtxI18nBundle).(*i18n.Bundle), messageCatalog: ctx.Value(services.CtxI18nCatalog).(*services.MessageCatalog),
messageCatalog: ctx.Value(services.CtxI18nCatalog).(map[string]*i18n.Message),
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
}, nil }, nil
} }

View file

@ -2,6 +2,7 @@ package services
import ( import (
"context" "context"
"fmt"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
@ -16,6 +17,56 @@ const (
CtxI18nCatalog CtxI18nCatalog
) )
type MessageCatalog struct {
messages map[string]*i18n.Message
logger *log.Logger
}
func (m *MessageCatalog) LookupErrorMessage(tag string, field string, value interface{}, localizer *i18n.Localizer) string {
var message *i18n.Message
message, ok := m.messages[fmt.Sprintf("%s-%s", field, tag)]
if !ok {
m.logger.Infof("no specific error message %s-%s", field, tag)
message, ok = m.messages[tag]
if !ok {
m.logger.Infof("no specific error message %s", tag)
message, ok = m.messages["unknown"]
if !ok {
m.logger.Error("no default translation found")
return tag
}
}
}
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: map[string]interface{}{
"Value": value,
},
})
if err != nil {
m.logger.Error(err)
return tag
}
return translation
}
func (m *MessageCatalog) LookupMessage(id string, templateData map[string]interface{}, localizer *i18n.Localizer) string {
if message, ok := m.messages[id]; ok {
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
TemplateData: templateData,
})
if err != nil {
m.logger.Error(err)
return id
}
return translation
} else {
return id
}
}
func InitI18n(ctx context.Context, logger *log.Logger) context.Context { func InitI18n(ctx context.Context, logger *log.Logger) context.Context {
bundle := i18n.NewBundle(language.English) bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
@ -23,33 +74,69 @@ func InitI18n(ctx context.Context, logger *log.Logger) context.Context {
if err != nil { if err != nil {
logger.Warnln("message bundle de.toml not found") logger.Warnln("message bundle de.toml not found")
} }
catalog := initMessageCatalog() catalog := initMessageCatalog(logger)
ctx = context.WithValue(ctx, CtxI18nBundle, bundle) ctx = context.WithValue(ctx, CtxI18nBundle, bundle)
ctx = context.WithValue(ctx, CtxI18nCatalog, catalog) ctx = context.WithValue(ctx, CtxI18nCatalog, catalog)
return ctx return ctx
} }
func initMessageCatalog() map[string]*i18n.Message { func initMessageCatalog(logger *log.Logger) *MessageCatalog {
messageCatalog := make(map[string]*i18n.Message) messages := make(map[string]*i18n.Message)
messageCatalog["unknown"] = &i18n.Message{ messages["unknown"] = &i18n.Message{
ID: "ErrorUnknown", ID: "ErrorUnknown",
Other: "Unknown error", Other: "Unknown error",
} }
messageCatalog["email"] = &i18n.Message{ messages["email"] = &i18n.Message{
ID: "ErrorEmail", ID: "ErrorEmail",
Other: "Please enter a valid email address.", Other: "Please enter a valid email address.",
} }
messageCatalog["Email-required"] = &i18n.Message{ messages["Email-required"] = &i18n.Message{
ID: "ErrorEmailRequired", ID: "ErrorEmailRequired",
Other: "Please enter an email address.", Other: "Please enter an email address.",
} }
messageCatalog["required"] = &i18n.Message{ messages["required"] = &i18n.Message{
ID: "ErrorRequired", ID: "ErrorRequired",
Other: "Please enter a value", Other: "Please enter a value",
} }
messageCatalog["Password-required"] = &i18n.Message{ messages["Password-required"] = &i18n.Message{
ID: "ErrorPasswordRequired", ID: "ErrorPasswordRequired",
Other: "Please enter a password.", Other: "Please enter a password.",
} }
return messageCatalog messages["TitleRequestConsent"] = &i18n.Message{
ID: "TitleRequestConsent",
Other: "Application requests your consent",
}
messages["LabelSubmit"] = &i18n.Message{
ID: "LabelSubmit",
Other: "Submit",
}
messages["LabelConsent"] = &i18n.Message{
ID: "LabelConsent",
Other: "I hereby agree that the application may get the requested permissions.",
}
messages["IntroConsentRequested"] = &i18n.Message{
ID: "IntroConsentRequested",
Other: "The <strong>{{ .client }}</strong> application wants your consent for the requested set of permissions.",
}
messages["IntroConsentMoreInformation"] = &i18n.Message{
ID: "IntroConsentMoreInformation",
Other: "You can find more information about <strong>{{ .client }}</strong> at <a href=\"{{ .clientLink }}\">its description page</a>.",
}
messages["Scope-openid-Description"] = &i18n.Message{
ID: "Scope-openid-Description",
Other: "Request information about your identity.",
}
messages["Scope-offline_access-Description"] = &i18n.Message{
ID: "Scope-offline_access-Description",
Other: "Keep access to your information until you revoke the permission.",
}
messages["Scope-profile-Description"] = &i18n.Message{
ID: "Scope-profile-Description",
Other: "Access your user profile information including your name, birth date and locale.",
}
messages["Scope-email-Description"] = &i18n.Message{
ID: "Scope-email-Description",
Other: "Access your primary email address.",
}
return &MessageCatalog{messages: messages, logger: logger}
} }

10
templates/base.gohtml Normal file
View file

@ -0,0 +1,10 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head><title>{{ .Title }}</title></head>
<body>
<h1>{{ .Title }}</h1>
{{ template "content" . }}
</body>
</html>
{{ end }}

View file

@ -1,10 +0,0 @@
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head><title>{{ .Title }}</title></head>
<body>
<h1>{{ .Title }}</h1>
{{ template "content" . }}
</body>
</html>
{{ end }}

24
templates/consent.gohtml Normal file
View file

@ -0,0 +1,24 @@
{{ define "content" }}
<p>{{ .IntroConsentRequested }}</p>
{{ if .client.LogoURI }}
<p>
<img src="{{ .client.LogoURI }}" alt="{{ .client.ClientName }}"/>
</p>
{{ end }}
<p>{{ .IntroMoreInformation }}</p>
<form method="post">
{{ .csrfField }}
<ul>
{{ range .requestedScope }}
<li>{{ .Label }}</li>
{{ end }}
</ul>
<p>
<input type="checkbox" name="consent" id="consent" value="true"/> <label
for="consent">{{ .LabelConsent }}</label>
</p>
<button type="submit">{{ .LabelSubmit }}</button>
</form>
{{ end }}