Refactor i18n, add templating for resource app

This commit is contained in:
Jan Dittberner 2021-01-01 09:20:49 +01:00
parent e4f17ca315
commit 161ea7fe0c
21 changed files with 432 additions and 152 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
/*.toml
/.idea/
/certs/
/hydra.yaml
/idp.toml
/resource_app.toml
/sessions/

View File

@ -186,3 +186,33 @@ Now you can start Hydra, the IDP and the demo app in 3 terminal windows:
Visit https://app.cacert.localhost:4000/ in a Browser and you will be directed
through the OpenID connect authorization code flow.
## Translations
This application uses [go-i18n](https://github.com/nicksnyder/go-i18n/) for internationalization (i18n) support.
The translation workflow needs the `go18n` binary which can be installed via
```
go get -u github.com/nicksnyder/go-i18n/v2/goi18n
```
To extract new messages from the code run
```
goi18n extract .
```
Then use
```
goi18n merge active.*.toml
```
to create TOML files for translation as `translate.<locale>.toml`. After translating the messages run
```
goi18n merge active.*.toml translate.*.toml
```
to merge the messages back into the active translation files. To add a new language you need to add the language code
to the languages configuration option (default is defined in the configmap in cmd/idp/main.go and cmd/app/main.go).

72
active.de.toml Normal file
View File

@ -0,0 +1,72 @@
[ErrorEmail]
hash = "sha1-d2306dd8970ff616631a3501791297f31475e416"
other = "Bitte gib eine gültige E-Mailadresse ein."
[ErrorEmailRequired]
hash = "sha1-e61b6eae7b3294d71eed349d7f1509d20ce35162"
other = "Bitte gib eine E-Mailadresse ein."
[ErrorPasswordRequired]
hash = "sha1-13c9d732467bf266a77ddfd1bbbe09dde518f9b0"
other = "Bitte gib ein Passwort ein."
[ErrorRequired]
hash = "sha1-31632fcec9d22a8463757f459e51c7c0eccd1f28"
other = "Dieses Feld wird benötigt."
[ErrorUnknown]
hash = "sha1-e5fd9aa24c9417e7332e6f25936ae2a6ec8f1524"
other = "Unbekannter Fehler"
[IndexGreeting]
hash = "sha1-d4a13058e497fa24143ea96d50d82b818455ef61"
other = "Hallo {{ .User }}"
[IndexIntroductionText]
hash = "sha1-c2c530e263fc9c38482338ed290aafb496794179"
other = "Das ist eine zugriffsgeschützte Resource"
[IndexTitle]
hash = "sha1-eccb2b889c068d3f25496c1dad3fb0f88d021bd9"
other = "Willkommen in der Demo-Anwendung"
[IntroConsentMoreInformation]
hash = "sha1-f58b8378238bd433deef3c3e6b0b70d0fd0dd59e"
other = "Auf der <a href=\"{{ .clientLink }}\">Beschreibungsseite</a> findest du mehr Informationen zu <strong>{{ .client }}</strong>."
[IntroConsentRequested]
hash = "sha1-cb8efc74b5b726201321e0924747bf38d39629a1"
other = "Die Anwendung <strong>{{ .client }}</strong> benötigt deine Einwilligungung für die angefragten Berechtigungen."
[LabelConsent]
hash = "sha1-5e56a367cf99015bbe98488845541db00b7e04f6"
other = "Ich erteile hiermit meine Einwilligung, dass die Anwendung die angefragten Berechtigungen erhalten darf."
[LabelSubmit]
hash = "sha1-2dacf65959849884a011f36f76a04eebea94c5ea"
other = "Abschicken"
[LogoutLabel]
description = "A label on a logout button or link"
hash = "sha1-8acfdeb9a8286f00c8e5dd48471cfdc994807579"
other = "Ausloggen"
[Scope-email-Description]
hash = "sha1-e50e5ea384cad8fac6f918d698be373b1362b351"
other = "Zugriff auf deine primäre E-Mail-Adresse."
[Scope-offline_access-Description]
hash = "sha1-732881bf998daa62cbad8615b2e6feb7a053b123"
other = "Zugriff auf deine Informationen behalten, bis du diese Zustimmung widerrufst."
[Scope-openid-Description]
hash = "sha1-0ad714e7a22b97d8247b70254990256bffa2ef76"
other = "Informationen über deine Identität abfragen."
[Scope-profile-Description]
hash = "sha1-f12d1d8af26e41aa82a3c1d6a7b7f0dba0313be1"
other = "Deine Nutzerprofilinformationen inklusive Name, Geburtsdatum und Spracheinstellung abfragen."
[TitleRequestConsent]
hash = "sha1-14984a9e08cda5390677458a98408baa955f9f8b"
other = "Anwendung erbittet deine Zustimmung"

21
active.en.toml Normal file
View File

@ -0,0 +1,21 @@
ErrorEmail = "Please enter a valid email address."
ErrorEmailRequired = "Please enter an email address."
ErrorPasswordRequired = "Please enter a password."
ErrorRequired = "Please enter a value"
ErrorUnknown = "Unknown error"
IndexGreeting = "Hello {{ .User }}"
IndexIntroductionText = "This is an authorization protected resource"
IndexTitle = "Welcome to the Demo application"
IntroConsentMoreInformation = "You can find more information about <strong>{{ .client }}</strong> at <a href=\"{{ .clientLink }}\">its description page</a>."
IntroConsentRequested = "The <strong>{{ .client }}</strong> application wants your consent for the requested set of permissions."
LabelConsent = "I hereby agree that the application may get the requested permissions."
LabelSubmit = "Submit"
Scope-email-Description = "Access your primary email address."
Scope-offline_access-Description = "Keep access to your information until you revoke the permission."
Scope-openid-Description = "Request information about your identity."
Scope-profile-Description = "Access your user profile information including your name, birth date and locale."
TitleRequestConsent = "Application requests your consent"
[LogoutLabel]
description = "A label on a logout button or link"
other = "Logout"

View File

@ -1,6 +1,7 @@
package handlers
import (
"context"
"encoding/base64"
"net/http"
"net/url"
@ -8,7 +9,6 @@ import (
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
"github.com/lestrrat-go/jwx/jwt/openid"
"golang.org/x/oauth2"
"git.cacert.org/oidc_login/app/services"
commonServices "git.cacert.org/oidc_login/common/services"
@ -16,7 +16,7 @@ import (
const sessionName = "resource_session"
func Authenticate(oauth2Config *oauth2.Config, clientId string) func(http.Handler) http.Handler {
func Authenticate(ctx context.Context, clientId string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, err := services.GetSessionStore().Get(r, sessionName)
@ -34,7 +34,7 @@ func Authenticate(oauth2Config *oauth2.Config, clientId string) func(http.Handle
return
}
var authUrl *url.URL
if authUrl, err = url.Parse(oauth2Config.Endpoint.AuthURL); err != nil {
if authUrl, err = url.Parse(commonServices.GetOAuth2Config(ctx).Endpoint.AuthURL); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

View File

@ -1,20 +1,26 @@
package handlers
import (
"context"
"fmt"
"html/template"
"net/http"
"net/url"
"github.com/lestrrat-go/jwx/jwk"
"github.com/nicksnyder/go-i18n/v2/i18n"
"git.cacert.org/oidc_login/app/services"
commonServices "git.cacert.org/oidc_login/common/services"
)
type indexHandler struct {
logoutUrl string
serverAddr string
keySet *jwk.Set
bundle *i18n.Bundle
indexTemplate *template.Template
keySet *jwk.Set
logoutUrl string
messageCatalog *commonServices.MessageCatalog
serverAddr string
}
func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
@ -26,23 +32,10 @@ func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
http.NotFound(writer, request)
return
}
accept := request.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
writer.WriteHeader(http.StatusOK)
page, err := template.New("").Parse(`
<!DOCTYPE html>
<html lang="en">
<head><title>Auth test</title></head>
<body>
<h1>Hello {{ .User }}</h1>
<p>This is an authorization protected resource</p>
<a href="{{ .LogoutURL }}">Logout</a>
</body>
</html>
`)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
session, err := services.GetSessionStore().Get(request, sessionName)
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
@ -72,9 +65,14 @@ func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
}
writer.Header().Add("Content-Type", "text/html")
err = page.Execute(writer, map[string]interface{}{
"User": oidcToken.Name(),
"LogoutURL": logoutUrl.String(),
err = h.indexTemplate.Lookup("base").Execute(writer, map[string]interface{}{
"Title": h.messageCatalog.LookupMessage("IndexTitle", nil, localizer),
"Greeting": h.messageCatalog.LookupMessage("IndexGreeting", map[string]interface{}{
"User": oidcToken.Name(),
}, localizer),
"IntroductionText": h.messageCatalog.LookupMessage("IndexIntroductionText", nil, localizer),
"LogoutLabel": h.messageCatalog.LookupMessage("LogoutLabel", nil, localizer),
"LogoutURL": logoutUrl.String(),
})
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
@ -82,6 +80,18 @@ func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
}
}
func NewIndexHandler(logoutUrl string, serverAddr string, keySet *jwk.Set) *indexHandler {
return &indexHandler{logoutUrl: logoutUrl, serverAddr: serverAddr, keySet: keySet}
func NewIndexHandler(ctx context.Context, serverAddr string) (*indexHandler, error) {
indexTemplate, err := template.ParseFiles(
"templates/app/base.gohtml", "templates/app/index.gohtml")
if err != nil {
return nil, err
}
return &indexHandler{
bundle: commonServices.GetI18nBundle(ctx),
indexTemplate: indexTemplate,
keySet: commonServices.GetJwkSet(ctx),
logoutUrl: commonServices.GetOidcConfig(ctx).EndSessionEndpoint,
messageCatalog: commonServices.GetMessageCatalog(ctx),
serverAddr: serverAddr,
}, nil
}

View File

@ -10,6 +10,7 @@ import (
"golang.org/x/oauth2"
"git.cacert.org/oidc_login/app/services"
commonServices "git.cacert.org/oidc_login/common/services"
)
const (
@ -112,6 +113,9 @@ func (c *oidcCallbackHandler) RenderErrorTemplate(w http.ResponseWriter, r *http
}
}
func NewCallbackHandler(keySet *jwk.Set, oauth2Config *oauth2.Config) *oidcCallbackHandler {
return &oidcCallbackHandler{keySet: keySet, oauth2Config: oauth2Config}
func NewCallbackHandler(ctx context.Context) *oidcCallbackHandler {
return &oidcCallbackHandler{
keySet: commonServices.GetJwkSet(ctx),
oauth2Config: commonServices.GetOAuth2Config(ctx),
}
}

31
app/services/i18n.go Normal file
View File

@ -0,0 +1,31 @@
package services
import (
"context"
"github.com/nicksnyder/go-i18n/v2/i18n"
"git.cacert.org/oidc_login/common/services"
)
func AddMessages(ctx context.Context) {
messages := make(map[string]*i18n.Message)
messages["IndexGreeting"] = &i18n.Message{
ID: "IndexGreeting",
Other: "Hello {{ .User }}",
}
messages["IndexTitle"] = &i18n.Message{
ID: "IndexTitle",
Other: "Welcome to the Demo application",
}
messages["LogoutLabel"] = &i18n.Message{
ID: "LogoutLabel",
Description: "A label on a logout button or link",
Other: "Logout",
}
messages["IndexIntroductionText"] = &i18n.Message{
ID: "IndexIntroductionText",
Other: "This is an authorization protected resource",
}
services.GetMessageCatalog(ctx).AddMessages(messages)
}

View File

@ -18,10 +18,8 @@ import (
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
"github.com/lestrrat-go/jwx/jwk"
log "github.com/sirupsen/logrus"
flag "github.com/spf13/pflag"
"golang.org/x/oauth2"
"git.cacert.org/oidc_login/app/handlers"
"git.cacert.org/oidc_login/app/services"
@ -52,6 +50,7 @@ func main() {
"server.certificate": "certs/app.cacert.localhost.crt.pem",
"oidc.server": "https://auth.cacert.localhost:4444/",
"session.path": "sessions/app",
"i18n.languages": []string{"en", "de"},
}, "."), nil)
cFiles, _ := f.GetStringSlice("conf")
for _, c := range cFiles {
@ -77,6 +76,10 @@ func main() {
oidcClientId := config.MustString("oidc.client-id")
oidcClientSecret := config.MustString("oidc.client-secret")
ctx := context.Background()
ctx = commonServices.InitI18n(ctx, logger, config.Strings("i18n.languages"))
services.AddMessages(ctx)
sessionPath := config.MustString("session.path")
sessionAuthKey, err := base64.StdEncoding.DecodeString(config.String("session.auth-key"))
if err != nil {
@ -109,32 +112,26 @@ func main() {
log.Infof("put the following in your resource_app.toml:\n%s", string(tomlData))
}
var discoveryResponse *commonServices.OpenIDConfiguration
apiClient := &http.Client{}
if discoveryResponse, err = commonServices.DiscoverOIDC(logger, oidcServer, apiClient); err != nil {
if ctx, err = commonServices.DiscoverOIDC(ctx, logger, &commonServices.OidcParams{
OidcServer: oidcServer,
OidcClientId: oidcClientId,
OidcClientSecret: oidcClientSecret,
APIClient: &http.Client{},
}); err != nil {
log.Fatalf("OpenID Connect discovery failed: %s", err)
}
oauth2Config := &oauth2.Config{
ClientID: oidcClientId,
ClientSecret: oidcClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: discoveryResponse.AuthorizationEndpoint,
TokenURL: discoveryResponse.TokenEndpoint,
},
Scopes: []string{"openid", "offline"},
}
keySet, err := jwk.FetchHTTP(discoveryResponse.JwksUri, jwk.WithHTTPClient(apiClient))
if err != nil {
log.Fatalf("could not fetch JWKs: %s", err)
}
services.InitSessionStore(logger, sessionPath, sessionAuthKey, sessionEncKey)
authMiddleware := handlers.Authenticate(oauth2Config, config.MustString("oidc.client-id"))
authMiddleware := handlers.Authenticate(ctx, oidcClientId)
serverAddr := fmt.Sprintf("%s:%d", config.String("server.name"), config.Int("server.port"))
indexHandler := handlers.NewIndexHandler(discoveryResponse.EndSessionEndpoint, serverAddr, keySet)
callbackHandler := handlers.NewCallbackHandler(keySet, oauth2Config)
indexHandler, err := handlers.NewIndexHandler(ctx, serverAddr)
if err != nil {
logger.Fatalf("could not initialize index handler: %v", err)
}
callbackHandler := handlers.NewCallbackHandler(ctx)
afterLogoutHandler := handlers.NewAfterLogoutHandler(logger)
router := http.NewServeMux()
@ -172,7 +169,6 @@ func main() {
logger.Infoln("Server is shutting down...")
atomic.StoreInt32(&commonHandlers.Healthy, 0)
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

View File

@ -26,6 +26,7 @@ import (
flag "github.com/spf13/pflag"
commonHandlers "git.cacert.org/oidc_login/common/handlers"
commonServices "git.cacert.org/oidc_login/common/services"
"git.cacert.org/oidc_login/idp/handlers"
"git.cacert.org/oidc_login/idp/services"
)
@ -52,6 +53,7 @@ func main() {
"server.key": "certs/idp.cacert.localhost.key",
"server.certificate": "certs/idp.cacert.localhost.crt.pem",
"admin.url": "https://hydra.cacert.localhost:4445/",
"i18n.languages": []string{"en", "de"},
}, "."), nil)
cFiles, _ := f.GetStringSlice("conf")
for _, c := range cFiles {
@ -73,7 +75,9 @@ func main() {
logger.Infoln("Server is starting")
ctx := context.Background()
ctx = services.InitI18n(ctx, logger)
ctx = commonServices.InitI18n(ctx, logger, config.Strings("i18n.languages"))
services.AddMessages(ctx)
adminURL, err := url.Parse(config.MustString("admin.url"))
if err != nil {
logger.Fatalf("error parsing admin URL: %v", err)
@ -82,11 +86,11 @@ func main() {
adminClient := hydra.New(clientTransport, nil)
handlerContext := context.WithValue(ctx, handlers.CtxAdminClient, adminClient.Admin)
loginHandler, err := handlers.NewLoginHandler(logger, handlerContext)
loginHandler, err := handlers.NewLoginHandler(handlerContext, logger)
if err != nil {
logger.Fatalf("error initializing login handler: %v", err)
}
consentHandler, err := handlers.NewConsentHandler(logger, handlerContext)
consentHandler, err := handlers.NewConsentHandler(handlerContext, logger)
if err != nil {
logger.Fatalf("error initializing consent handler: %v", err)
}

102
common/services/i18n.go Normal file
View File

@ -0,0 +1,102 @@
package services
import (
"context"
"fmt"
"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
)
type MessageCatalog struct {
messages map[string]*i18n.Message
logger *log.Logger
}
func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
for key, value := range messages {
m.messages[key] = value
}
}
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, languages []string) context.Context {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
for _, lang := range languages {
_, err := bundle.LoadMessageFile(fmt.Sprintf("active.%s.toml", lang))
if err != nil {
logger.Warnln("message bundle de.toml not found")
}
}
catalog := initMessageCatalog(logger)
ctx = context.WithValue(ctx, ctxI18nBundle, bundle)
ctx = context.WithValue(ctx, ctxI18nCatalog, catalog)
return ctx
}
func initMessageCatalog(logger *log.Logger) *MessageCatalog {
messages := make(map[string]*i18n.Message)
return &MessageCatalog{messages: messages, logger: logger}
}
func GetI18nBundle(ctx context.Context) *i18n.Bundle {
return ctx.Value(ctxI18nBundle).(*i18n.Bundle)
}
func GetMessageCatalog(ctx context.Context) *MessageCatalog {
return ctx.Value(ctxI18nCatalog).(*MessageCatalog)
}

View File

@ -2,11 +2,22 @@ package services
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/url"
"github.com/lestrrat-go/jwx/jwk"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
)
type oidcContextKey int
const (
ctxOidcConfig oidcContextKey = iota
ctxOAuth2Config
ctxOidcJwks
)
type OpenIDConfiguration struct {
@ -16,11 +27,19 @@ type OpenIDConfiguration struct {
EndSessionEndpoint string `json:"end_session_endpoint"`
}
func DiscoverOIDC(logger *log.Logger, oidcServer string, apiClient *http.Client) (o *OpenIDConfiguration, err error) {
type OidcParams struct {
OidcServer string
OidcClientId string
OidcClientSecret string
APIClient *http.Client
}
func DiscoverOIDC(ctx context.Context, logger *log.Logger, params *OidcParams) (context.Context, error) {
var discoveryUrl *url.URL
if discoveryUrl, err = url.Parse(oidcServer); err != nil {
logger.Fatalf("could not parse oidc.server parameter value %s: %s", oidcServer, err)
discoveryUrl, err := url.Parse(params.OidcServer)
if err != nil {
logger.Fatalf("could not parse oidc.server parameter value %s: %s", params.OidcServer, err)
} else {
discoveryUrl.Path = "/.well-known/openid-configuration"
}
@ -29,23 +48,53 @@ func DiscoverOIDC(logger *log.Logger, oidcServer string, apiClient *http.Client)
var req *http.Request
req, err = http.NewRequest(http.MethodGet, discoveryUrl.String(), bytes.NewBuffer(body))
if err != nil {
return
return nil, err
}
req.Header = map[string][]string{
"Accept": {"application/json"},
}
resp, err := apiClient.Do(req)
resp, err := params.APIClient.Do(req)
if err != nil {
return
return nil, err
}
dec := json.NewDecoder(resp.Body)
o = &OpenIDConfiguration{}
err = dec.Decode(o)
discoveryResponse := &OpenIDConfiguration{}
err = dec.Decode(discoveryResponse)
if err != nil {
return
return nil, err
}
ctx = context.WithValue(ctx, ctxOidcConfig, discoveryResponse)
return
oauth2Config := &oauth2.Config{
ClientID: params.OidcClientId,
ClientSecret: params.OidcClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: discoveryResponse.AuthorizationEndpoint,
TokenURL: discoveryResponse.TokenEndpoint,
},
Scopes: []string{"openid", "offline"},
}
ctx = context.WithValue(ctx, ctxOAuth2Config, oauth2Config)
keySet, err := jwk.FetchHTTP(discoveryResponse.JwksUri, jwk.WithHTTPClient(params.APIClient))
if err != nil {
log.Fatalf("could not fetch JWKs: %s", err)
}
ctx = context.WithValue(ctx, ctxOidcJwks, keySet)
return ctx, nil
}
func GetOidcConfig(ctx context.Context) *OpenIDConfiguration {
return ctx.Value(ctxOidcConfig).(*OpenIDConfiguration)
}
func GetOAuth2Config(ctx context.Context) *oauth2.Config {
return ctx.Value(ctxOAuth2Config).(*oauth2.Config)
}
func GetJwkSet(ctx context.Context) *jwk.Set {
return ctx.Value(ctxOidcJwks).(*jwk.Set)
}

View File

@ -15,7 +15,7 @@ import (
"github.com/ory/hydra-client-go/models"
log "github.com/sirupsen/logrus"
"git.cacert.org/oidc_login/idp/services"
commonServices "git.cacert.org/oidc_login/common/services"
)
type consentHandler struct {
@ -23,7 +23,7 @@ type consentHandler struct {
bundle *i18n.Bundle
consentTemplate *template.Template
logger *log.Logger
messageCatalog *services.MessageCatalog
messageCatalog *commonServices.MessageCatalog
}
type ConsentInformation struct {
@ -151,17 +151,18 @@ func (h *consentHandler) mapRequestedScope(scope models.StringSlicePipeDelimiter
return result
}
func NewConsentHandler(logger *log.Logger, ctx context.Context) (*consentHandler, error) {
consentTemplate, err := template.ParseFiles("templates/base.gohtml", "templates/consent.gohtml")
func NewConsentHandler(ctx context.Context, logger *log.Logger) (*consentHandler, error) {
consentTemplate, err := template.ParseFiles(
"templates/idp/base.gohtml", "templates/idp/consent.gohtml")
if err != nil {
return nil, err
}
return &consentHandler{
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
bundle: ctx.Value(services.CtxI18nBundle).(*i18n.Bundle),
bundle: commonServices.GetI18nBundle(ctx),
consentTemplate: consentTemplate,
logger: logger,
messageCatalog: ctx.Value(services.CtxI18nCatalog).(*services.MessageCatalog),
messageCatalog: commonServices.GetMessageCatalog(ctx),
}, nil
}

View File

@ -15,7 +15,7 @@ import (
"github.com/ory/hydra-client-go/models"
log "github.com/sirupsen/logrus"
"git.cacert.org/oidc_login/idp/services"
commonServices "git.cacert.org/oidc_login/common/services"
)
type loginHandler struct {
@ -23,7 +23,7 @@ type loginHandler struct {
bundle *i18n.Bundle
logger *log.Logger
loginTemplate *template.Template
messageCatalog *services.MessageCatalog
messageCatalog *commonServices.MessageCatalog
}
type acrType string
@ -127,16 +127,17 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
func NewLoginHandler(logger *log.Logger, ctx context.Context) (*loginHandler, error) {
loginTemplate, err := template.ParseFiles("templates/base.gohtml", "templates/login.gohtml")
func NewLoginHandler(ctx context.Context, logger *log.Logger) (*loginHandler, error) {
loginTemplate, err := template.ParseFiles(
"templates/idp/base.gohtml", "templates/idp/login.gohtml")
if err != nil {
return nil, err
}
return &loginHandler{
adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
bundle: ctx.Value(services.CtxI18nBundle).(*i18n.Bundle),
bundle: commonServices.GetI18nBundle(ctx),
logger: logger,
loginTemplate: loginTemplate,
messageCatalog: ctx.Value(services.CtxI18nCatalog).(*services.MessageCatalog),
messageCatalog: commonServices.GetMessageCatalog(ctx),
}, nil
}

View File

@ -2,85 +2,13 @@ package services
import (
"context"
"fmt"
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
log "github.com/sirupsen/logrus"
"golang.org/x/text/language"
"git.cacert.org/oidc_login/common/services"
)
type contextKey int
const (
CtxI18nBundle contextKey = iota
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 {
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(logger)
ctx = context.WithValue(ctx, CtxI18nBundle, bundle)
ctx = context.WithValue(ctx, CtxI18nCatalog, catalog)
return ctx
}
func initMessageCatalog(logger *log.Logger) *MessageCatalog {
func AddMessages(ctx context.Context) {
messages := make(map[string]*i18n.Message)
messages["unknown"] = &i18n.Message{
ID: "ErrorUnknown",
@ -138,5 +66,5 @@ func initMessageCatalog(logger *log.Logger) *MessageCatalog {
ID: "Scope-email-Description",
Other: "Access your primary email address.",
}
return &MessageCatalog{messages: messages, logger: logger}
services.GetMessageCatalog(ctx).AddMessages(messages)
}

View File

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

View File

@ -0,0 +1,5 @@
{{ define "content" }}
<h1>{{ .Greeting }}</h1>
<p>{{ .IntroductionText }}</p>
<a href="{{ .LogoutURL }}">{{ .LogoutLabel }}</a>
{{ end }}

16
translate.de.toml Normal file
View File

@ -0,0 +1,16 @@
[IndexGreeting]
hash = "sha1-d4a13058e497fa24143ea96d50d82b818455ef61"
other = "Hallo {{ .User }}"
[IndexIntroductionText]
hash = "sha1-c2c530e263fc9c38482338ed290aafb496794179"
other = "Das ist eine zugriffsgeschützte Resource"
[IndexTitle]
hash = "sha1-eccb2b889c068d3f25496c1dad3fb0f88d021bd9"
other = "Willkommen in der Demo-Anwendung"
[LogoutLabel]
description = "A label on a logout button or link"
hash = "sha1-8acfdeb9a8286f00c8e5dd48471cfdc994807579"
other = "Ausloggen"