Refactor i18n, add templating for resource app
This commit is contained in:
parent
e4f17ca315
commit
161ea7fe0c
21 changed files with 432 additions and 152 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
|||
/*.toml
|
||||
/.idea/
|
||||
/certs/
|
||||
/hydra.yaml
|
||||
/idp.toml
|
||||
/resource_app.toml
|
||||
/sessions/
|
||||
|
|
30
README.md
30
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.<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
72
active.de.toml
Normal 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
21
active.en.toml
Normal 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"
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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,8 +65,13 @@ func (h *indexHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
|
|||
}
|
||||
|
||||
writer.Header().Add("Content-Type", "text/html")
|
||||
err = page.Execute(writer, map[string]interface{}{
|
||||
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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
31
app/services/i18n.go
Normal 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)
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
102
common/services/i18n.go
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
9
templates/app/base.gohtml
Normal file
9
templates/app/base.gohtml
Normal file
|
@ -0,0 +1,9 @@
|
|||
{{ define "base" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><title>{{ .Title }}</title></head>
|
||||
<body>
|
||||
{{ template "content" . }}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
5
templates/app/index.gohtml
Normal file
5
templates/app/index.gohtml
Normal 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
16
translate.de.toml
Normal 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"
|
Reference in a new issue