diff --git a/.gitignore b/.gitignore index 8057c27..b0cee37 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -/*.toml /.idea/ /certs/ /hydra.yaml +/idp.toml +/resource_app.toml /sessions/ diff --git a/README.md b/README.md index 7ac79b4..d144051 100644 --- a/README.md +++ b/README.md @@ -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..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). diff --git a/active.de.toml b/active.de.toml new file mode 100644 index 0000000..cf0df15 --- /dev/null +++ b/active.de.toml @@ -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 Beschreibungsseite findest du mehr Informationen zu {{ .client }}." + +[IntroConsentRequested] +hash = "sha1-cb8efc74b5b726201321e0924747bf38d39629a1" +other = "Die Anwendung {{ .client }} 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" diff --git a/active.en.toml b/active.en.toml new file mode 100644 index 0000000..1a45941 --- /dev/null +++ b/active.en.toml @@ -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 {{ .client }} at its description page." +IntroConsentRequested = "The {{ .client }} 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" diff --git a/app/handlers/common.go b/app/handlers/common.go index 6809567..4affddb 100644 --- a/app/handlers/common.go +++ b/app/handlers/common.go @@ -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 } diff --git a/app/handlers/index.go b/app/handlers/index.go index 5dbc595..fbc6a77 100644 --- a/app/handlers/index.go +++ b/app/handlers/index.go @@ -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(` - - -Auth test - -

Hello {{ .User }}

-

This is an authorization protected resource

-Logout - - -`) - 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 } diff --git a/app/handlers/oidc_callback.go b/app/handlers/oidc_callback.go index d017f16..82db0a6 100644 --- a/app/handlers/oidc_callback.go +++ b/app/handlers/oidc_callback.go @@ -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), + } } diff --git a/app/services/i18n.go b/app/services/i18n.go new file mode 100644 index 0000000..d78b278 --- /dev/null +++ b/app/services/i18n.go @@ -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) +} diff --git a/cmd/app/main.go b/cmd/app/main.go index 9fd99d3..3961eee 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -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() diff --git a/cmd/idp/main.go b/cmd/idp/main.go index 120a076..b3585b2 100644 --- a/cmd/idp/main.go +++ b/cmd/idp/main.go @@ -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) } diff --git a/common/services/i18n.go b/common/services/i18n.go new file mode 100644 index 0000000..f7cfee5 --- /dev/null +++ b/common/services/i18n.go @@ -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) +} diff --git a/common/services/oidc.go b/common/services/oidc.go index e2db15d..31d1d9d 100644 --- a/common/services/oidc.go +++ b/common/services/oidc.go @@ -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) } diff --git a/idp/handlers/consent.go b/idp/handlers/consent.go index fd6f1e2..9dabe29 100644 --- a/idp/handlers/consent.go +++ b/idp/handlers/consent.go @@ -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 } diff --git a/idp/handlers/login.go b/idp/handlers/login.go index e913d2f..5ba6568 100644 --- a/idp/handlers/login.go +++ b/idp/handlers/login.go @@ -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 } diff --git a/idp/services/i18n.go b/idp/services/i18n.go index 894b6a4..bac9f3c 100644 --- a/idp/services/i18n.go +++ b/idp/services/i18n.go @@ -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) } diff --git a/templates/app/base.gohtml b/templates/app/base.gohtml new file mode 100644 index 0000000..545279a --- /dev/null +++ b/templates/app/base.gohtml @@ -0,0 +1,9 @@ +{{ define "base" }} + + + {{ .Title }} + + {{ template "content" . }} + + +{{ end }} \ No newline at end of file diff --git a/templates/app/index.gohtml b/templates/app/index.gohtml new file mode 100644 index 0000000..79c581e --- /dev/null +++ b/templates/app/index.gohtml @@ -0,0 +1,5 @@ +{{ define "content" }} +

{{ .Greeting }}

+

{{ .IntroductionText }}

+ {{ .LogoutLabel }} +{{ end }} \ No newline at end of file diff --git a/templates/base.gohtml b/templates/idp/base.gohtml similarity index 100% rename from templates/base.gohtml rename to templates/idp/base.gohtml diff --git a/templates/consent.gohtml b/templates/idp/consent.gohtml similarity index 100% rename from templates/consent.gohtml rename to templates/idp/consent.gohtml diff --git a/templates/login.gohtml b/templates/idp/login.gohtml similarity index 100% rename from templates/login.gohtml rename to templates/idp/login.gohtml diff --git a/translate.de.toml b/translate.de.toml new file mode 100644 index 0000000..1ac1197 --- /dev/null +++ b/translate.de.toml @@ -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"