diff --git a/cmd/idp.go b/cmd/idp.go
index 8e9d4a0..5319b08 100644
--- a/cmd/idp.go
+++ b/cmd/idp.go
@@ -1,18 +1,18 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package main
@@ -23,7 +23,6 @@ import (
"crypto/x509"
"encoding/base64"
"fmt"
- "io/ioutil"
"net/http"
"net/url"
"os"
@@ -31,24 +30,33 @@ import (
"sync/atomic"
"time"
- "git.cacert.org/oidc_idp/ui"
"github.com/go-openapi/runtime/client"
"github.com/gorilla/csrf"
"github.com/knadh/koanf"
hydra "github.com/ory/hydra-client-go/client"
log "github.com/sirupsen/logrus"
+ "git.cacert.org/oidc_idp/ui"
+
"git.cacert.org/oidc_idp/handlers"
"git.cacert.org/oidc_idp/services"
)
+const (
+ TimeoutThirty = 30 * time.Second
+ TimeoutTwenty = 20 * time.Second
+ DefaultCSRFMaxAge = 600
+ DefaultServerPort = 3000
+)
+
func main() {
logger := log.New()
+
config, err := services.ConfigureApplication(
logger,
"IDP",
map[string]interface{}{
- "server.port": 3000,
+ "server.port": DefaultServerPort,
"server.name": "login.cacert.localhost",
"server.key": "certs/idp.cacert.localhost.key",
"server.certificate": "certs/idp.cacert.localhost.crt.pem",
@@ -61,23 +69,28 @@ func main() {
}
logger.Infoln("Server is starting")
- ctx := context.Background()
+ bundle, catalog := services.InitI18n(logger, config.Strings("i18n.languages"))
- ctx = services.InitI18n(ctx, logger, config.Strings("i18n.languages"))
- services.AddMessages(ctx)
+ if err = services.AddMessages(catalog); err != nil {
+ logger.Fatalf("could not add messages for i18n: %v", err)
+ }
adminURL, err := url.Parse(config.MustString("admin.url"))
if err != nil {
logger.Fatalf("error parsing admin URL: %v", err)
}
+
tlsClientConfig := &tls.Config{MinVersion: tls.VersionTLS12}
+
if config.Exists("api-client.rootCAs") {
rootCAFile := config.MustString("api-client.rootCAs")
caCertPool := x509.NewCertPool()
- pemBytes, err := ioutil.ReadFile(rootCAFile)
+
+ pemBytes, err := os.ReadFile(rootCAFile)
if err != nil {
log.Fatalf("could not read CA certificate file: %v", err)
}
+
caCertPool.AppendCertsFromPEM(pemBytes)
tlsClientConfig.RootCAs = caCertPool
}
@@ -92,16 +105,10 @@ func main() {
)
adminClient := hydra.New(clientTransport, nil)
- handlerContext := context.WithValue(ctx, handlers.CtxAdminClient, adminClient.Admin)
- loginHandler, err := handlers.NewLoginHandler(handlerContext, logger)
- if err != nil {
- logger.Fatalf("error initializing login handler: %v", err)
- }
- consentHandler, err := handlers.NewConsentHandler(handlerContext, logger)
- if err != nil {
- logger.Fatalf("error initializing consent handler: %v", err)
- }
- logoutHandler := handlers.NewLogoutHandler(handlerContext, logger)
+ loginHandler := handlers.NewLoginHandler(logger, bundle, catalog, adminClient.Admin)
+ consentHandler := handlers.NewConsentHandler(logger, bundle, catalog, adminClient.Admin)
+ logoutHandler := handlers.NewLogoutHandler(logger, adminClient.Admin)
+
logoutSuccessHandler := handlers.NewLogoutSuccessHandler()
errorHandler := handlers.NewErrorHandler()
staticFiles := http.FileServer(http.FS(ui.Static))
@@ -126,20 +133,21 @@ func main() {
logger.Fatalf("could not parse CSRF key bytes: %v", err)
}
- nextRequestId := func() string {
+ nextRequestID := func() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
- tracing := handlers.Tracing(nextRequestId)
+ tracing := handlers.Tracing(nextRequestID)
logging := handlers.Logging(logger)
hsts := handlers.EnableHSTS()
csrfProtect := csrf.Protect(
csrfKey,
csrf.Secure(true),
csrf.SameSite(csrf.SameSiteStrictMode),
- csrf.MaxAge(600))
+ csrf.MaxAge(DefaultCSRFMaxAge))
+
errorMiddleware, err := handlers.ErrorHandling(
- ctx,
+ context.Background(),
logger,
ui.Templates,
)
@@ -149,7 +157,7 @@ func main() {
handlerChain := tracing(logging(hsts(errorMiddleware(csrfProtect(router)))))
- startServer(ctx, handlerChain, logger, config)
+ startServer(context.Background(), handlerChain, logger, config)
}
func startServer(ctx context.Context, handlerChain http.Handler, logger *log.Logger, config *koanf.Koanf) {
@@ -158,10 +166,12 @@ func startServer(ctx context.Context, handlerChain http.Handler, logger *log.Log
serverPort := config.Int("server.port")
clientCertPool := x509.NewCertPool()
- pemBytes, err := ioutil.ReadFile(clientCertificateCAFile)
+
+ pemBytes, err := os.ReadFile(clientCertificateCAFile)
if err != nil {
logger.Fatalf("could not load client CA certificates: %v", err)
}
+
clientCertPool.AppendCertsFromPEM(pemBytes)
tlsConfig := &tls.Config{
@@ -173,9 +183,9 @@ func startServer(ctx context.Context, handlerChain http.Handler, logger *log.Log
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", serverName, serverPort),
Handler: handlerChain,
- ReadTimeout: 20 * time.Second,
- WriteTimeout: 20 * time.Second,
- IdleTimeout: 30 * time.Second,
+ ReadTimeout: TimeoutTwenty,
+ WriteTimeout: TimeoutTwenty,
+ IdleTimeout: TimeoutThirty,
TLSConfig: tlsConfig,
}
@@ -188,18 +198,21 @@ func startServer(ctx context.Context, handlerChain http.Handler, logger *log.Log
logger.Infoln("Server is shutting down...")
atomic.StoreInt32(&handlers.Healthy, 0)
- ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ ctx, cancel := context.WithTimeout(ctx, TimeoutThirty)
defer cancel()
server.SetKeepAlivesEnabled(false)
+
if err := server.Shutdown(ctx); err != nil {
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
}
+
close(done)
}()
logger.Infof("Server is ready to handle requests at https://%s/", server.Addr)
atomic.StoreInt32(&handlers.Healthy, 1)
+
if err := server.ListenAndServeTLS(
config.String("server.certificate"), config.String("server.key"),
); err != nil && err != http.ErrServerClosed {
diff --git a/handlers/common.go b/handlers/common.go
deleted file mode 100644
index aae852e..0000000
--- a/handlers/common.go
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- Copyright 2020, 2021 Jan Dittberner
-
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package handlers
-
-type handlerContextKey int
-
-const (
- CtxAdminClient handlerContextKey = iota
-)
diff --git a/handlers/consent.go b/handlers/consent.go
index 2182340..6c381ab 100644
--- a/handlers/consent.go
+++ b/handlers/consent.go
@@ -1,33 +1,32 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package handlers
import (
- "context"
"encoding/json"
+ "errors"
+ "fmt"
"html/template"
"net/http"
"net/url"
"strings"
"time"
- commonModels "git.cacert.org/oidc_idp/models"
- "git.cacert.org/oidc_idp/ui"
"github.com/go-playground/form/v4"
"github.com/gorilla/csrf"
"github.com/lestrrat-go/jwx/jwt/openid"
@@ -36,14 +35,16 @@ import (
"github.com/ory/hydra-client-go/models"
log "github.com/sirupsen/logrus"
+ commonModels "git.cacert.org/oidc_idp/models"
+ "git.cacert.org/oidc_idp/ui"
+
"git.cacert.org/oidc_idp/services"
)
-type consentHandler struct {
- adminClient *admin.Client
+type ConsentHandler struct {
+ adminClient admin.ClientService
bundle *i18n.Bundle
consentTemplate *template.Template
- context context.Context
logger *log.Logger
messageCatalog *services.MessageCatalog
}
@@ -70,6 +71,8 @@ const (
ScopeEmail = "email"
)
+const OneDayInSeconds = 86400
+
func init() {
supportedScopes = make(map[string]*i18n.Message)
supportedScopes[ScopeOpenID] = &i18n.Message{
@@ -107,9 +110,11 @@ func (i *UserInfo) GetFullName() string {
return i.CommonName
}
-func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (h *ConsentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
challenge := r.URL.Query().Get("consent_challenge")
+
h.logger.Debugf("received consent challenge %s", challenge)
+
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
@@ -122,8 +127,12 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
- h.renderConsentForm(w, r, consentData, requestedClaims, err, localizer)
- break
+ if err := h.renderConsentForm(w, r, consentData, requestedClaims, localizer); err != nil {
+ h.logger.Error(err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
+ return
+ }
case http.MethodPost:
var consentInfo ConsentInformation
@@ -131,11 +140,8 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
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,
- )
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
@@ -144,6 +150,7 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err != nil {
h.logger.Errorf("could not get session data: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
@@ -154,34 +161,38 @@ func (h *consentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
GrantScope: consentInfo.GrantedScopes,
HandledAt: models.NullTime(time.Now()),
Remember: true,
- RememberFor: 86400,
+ RememberFor: OneDayInSeconds,
Session: sessionData,
- }).WithTimeout(time.Second * 10))
+ }).WithTimeout(TimeoutTen))
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)
- return
- } 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)
+
+ return
}
+
+ 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)
}
}
-func (h *consentHandler) getRequestedConsentInformation(challenge string, r *http.Request) (
+func (h *ConsentHandler) getRequestedConsentInformation(challenge string, r *http.Request) (
*admin.GetConsentRequestOK,
*commonModels.OIDCClaimsRequest,
error,
@@ -190,20 +201,26 @@ func (h *consentHandler) getRequestedConsentInformation(challenge string, r *htt
admin.NewGetConsentRequestParams().WithConsentChallenge(challenge))
if err != nil {
h.logger.Errorf("error getting consent information: %v", err)
- var errorDetails *ErrorDetails
- errorDetails = &ErrorDetails{
- ErrorMessage: "could not get consent details",
- ErrorDetails: []string{http.StatusText(http.StatusInternalServerError)},
+
+ if errorBucket := GetErrorBucket(r); errorBucket != nil {
+ errorDetails := &ErrorDetails{
+ ErrorMessage: "could not get consent details",
+ ErrorDetails: []string{http.StatusText(http.StatusInternalServerError)},
+ }
+
+ errorBucket.AddError(errorDetails)
}
- GetErrorBucket(r).AddError(errorDetails)
- return nil, nil, err
+
+ return nil, nil, fmt.Errorf("error getting consent information: %w", err)
}
+
var requestedClaims commonModels.OIDCClaimsRequest
- requestUrl, err := url.Parse(consentData.Payload.RequestURL)
+
+ requestURL, err := url.Parse(consentData.Payload.RequestURL)
if err != nil {
h.logger.Warnf("could not parse original request URL %s: %v", consentData.Payload.RequestURL, err)
} else {
- claimsParameter := requestUrl.Query().Get("claims")
+ claimsParameter := requestURL.Query().Get("claims")
if claimsParameter != "" {
decoder := json.NewDecoder(strings.NewReader(claimsParameter))
err := decoder.Decode(&requestedClaims)
@@ -216,27 +233,28 @@ func (h *consentHandler) getRequestedConsentInformation(challenge string, r *htt
}
}
}
+
return consentData, &requestedClaims, nil
}
-func (h *consentHandler) renderConsentForm(
+func (h *ConsentHandler) renderConsentForm(
w http.ResponseWriter,
r *http.Request,
consentData *admin.GetConsentRequestOK,
claims *commonModels.OIDCClaimsRequest,
- err error,
localizer *i18n.Localizer,
-) {
+) error {
trans := func(id string, values ...map[string]interface{}) string {
if len(values) > 0 {
return h.messageCatalog.LookupMessage(id, values[0], localizer)
}
+
return h.messageCatalog.LookupMessage(id, nil, localizer)
}
// render consent form
client := consentData.GetPayload().Client
- err = h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
+ err := h.consentTemplate.Lookup("base").Execute(w, map[string]interface{}{
"Title": trans("TitleRequestConsent"),
csrf.TemplateTag: csrf.TemplateField(r),
"errors": map[string]string{},
@@ -245,15 +263,24 @@ func (h *consentHandler) renderConsentForm(
"requestedClaims": h.mapRequestedClaims(claims, localizer),
"LabelSubmit": trans("LabelSubmit"),
"LabelConsent": trans("LabelConsent"),
- "IntroMoreInformation": template.HTML(trans("IntroConsentMoreInformation", map[string]interface{}{
- "client": client.ClientName,
- "clientLink": client.ClientURI,
- })),
- "ClaimsInformation": template.HTML(trans("ClaimsInformation", nil)),
- "IntroConsentRequested": template.HTML(trans("IntroConsentRequested", map[string]interface{}{
- "client": client.ClientName,
- })),
+ "IntroMoreInformation": template.HTML( //nolint:gosec
+ trans("IntroConsentMoreInformation", map[string]interface{}{
+ "client": client.ClientName,
+ "clientLink": client.ClientURI,
+ })),
+ "ClaimsInformation": template.HTML( //nolint:gosec
+ trans("ClaimsInformation", nil)),
+ "IntroConsentRequested": template.HTML( //nolint:gosec
+ trans("IntroConsentRequested", map[string]interface{}{
+ "client": client.ClientName,
+ })),
})
+
+ if err != nil {
+ return fmt.Errorf("rendering failed: %w", err)
+ }
+
+ return nil
}
type scopeWithLabel struct {
@@ -261,13 +288,19 @@ type scopeWithLabel struct {
Label string
}
-func (h *consentHandler) mapRequestedScope(scope models.StringSlicePipeDelimiter, localizer *i18n.Localizer) []*scopeWithLabel {
+func (h *ConsentHandler) mapRequestedScope(
+ scope models.StringSlicePipeDelimiter,
+ localizer *i18n.Localizer,
+) []*scopeWithLabel {
result := make([]*scopeWithLabel, 0)
+
for _, scopeName := range scope {
if _, ok := supportedScopes[scopeName]; !ok {
h.logger.Warnf("unsupported scope %s ignored", scopeName)
+
continue
}
+
label, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: supportedScopes[scopeName],
})
@@ -275,8 +308,10 @@ func (h *consentHandler) mapRequestedScope(scope models.StringSlicePipeDelimiter
h.logger.Warnf("could not localize label for scope %s: %v", scopeName, err)
label = scopeName
}
+
result = append(result, &scopeWithLabel{Name: scopeName, Label: label})
}
+
return result
}
@@ -286,7 +321,10 @@ type claimWithLabel struct {
Essential bool
}
-func (h *consentHandler) mapRequestedClaims(claims *commonModels.OIDCClaimsRequest, localizer *i18n.Localizer) []*claimWithLabel {
+func (h *ConsentHandler) mapRequestedClaims(
+ claims *commonModels.OIDCClaimsRequest,
+ localizer *i18n.Localizer,
+) []*claimWithLabel {
result := make([]*claimWithLabel, 0)
known := make(map[string]bool)
@@ -295,8 +333,10 @@ func (h *consentHandler) mapRequestedClaims(claims *commonModels.OIDCClaimsReque
for k, v := range *claimElement {
if _, ok := supportedClaims[k]; !ok {
h.logger.Warnf("unsupported claim %s ignored", k)
+
continue
}
+
label, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: supportedClaims[k],
})
@@ -304,6 +344,7 @@ func (h *consentHandler) mapRequestedClaims(claims *commonModels.OIDCClaimsReque
h.logger.Warnf("could not localize label for claim %s: %v", k, err)
label = k
}
+
if !known[k] {
result = append(result, &claimWithLabel{
Name: k,
@@ -315,10 +356,11 @@ func (h *consentHandler) mapRequestedClaims(claims *commonModels.OIDCClaimsReque
}
}
}
+
return result
}
-func (h *consentHandler) getSessionData(
+func (h *ConsentHandler) getSessionData(
r *http.Request,
info ConsentInformation,
claims *commonModels.OIDCClaimsRequest,
@@ -329,32 +371,42 @@ func (h *consentHandler) getSessionData(
userInfo := h.GetUserInfoFromClientCertificate(r, payload.Subject)
- h.fillTokenData(accessTokenData, payload.RequestedScope, claims, info, userInfo)
- h.fillTokenData(idTokenData, payload.RequestedScope, claims, info, userInfo)
+ if err := h.fillTokenData(accessTokenData, payload.RequestedScope, claims, info, userInfo); err != nil {
+ return nil, err
+ }
+
+ if err := h.fillTokenData(idTokenData, payload.RequestedScope, claims, info, userInfo); err != nil {
+ return nil, err
+ }
+
return &models.ConsentRequestSession{
AccessToken: accessTokenData,
IDToken: idTokenData,
}, nil
}
-func (h *consentHandler) fillTokenData(
+func (h *ConsentHandler) fillTokenData(
m map[string]interface{},
requestedScope models.StringSlicePipeDelimiter,
claimsRequest *commonModels.OIDCClaimsRequest,
consentInformation ConsentInformation,
userInfo *UserInfo,
-) {
+) error {
for _, scope := range requestedScope {
granted := false
+
for _, k := range consentInformation.GrantedScopes {
if k == scope {
granted = true
+
break
}
}
+
if !granted {
continue
}
+
switch scope {
case ScopeEmail:
// email
@@ -362,7 +414,6 @@ func (h *consentHandler) fillTokenData(
// email_verified Claims.
m[openid.EmailKey] = userInfo.Email
m[openid.EmailVerifiedKey] = userInfo.EmailVerified
- break
case ScopeProfile:
// profile
// OPTIONAL. This scope value requests access to the
@@ -371,53 +422,88 @@ func (h *consentHandler) fillTokenData(
// preferred_username, profile, picture, website, gender,
// birthdate, zoneinfo, locale, and updated_at.
m[openid.NameKey] = userInfo.GetFullName()
- break
}
}
+
if userInfoClaims := claimsRequest.GetUserInfo(); userInfoClaims != nil {
- for claimName, claim := range *userInfoClaims {
- granted := false
- for _, k := range consentInformation.SelectedClaims {
- if k == claimName {
- granted = true
- break
- }
- }
- if !granted {
- continue
- }
- if claim.WantedValue() != nil {
- m[claimName] = *claim.WantedValue()
- continue
- }
- if claim.IsEssential() {
- h.logger.Warnf(
- "handling for essential claim name %s not implemented",
- claimName,
- )
- } else {
- h.logger.Warnf(
- "handling for claim name %s not implemented",
- claimName,
- )
- }
+ err := h.parseUserInfoClaims(m, userInfoClaims, consentInformation)
+ if err != nil {
+ return err
}
}
+
+ return nil
}
-func (h *consentHandler) GetUserInfoFromClientCertificate(r *http.Request, subject string) *UserInfo {
+func (h *ConsentHandler) parseUserInfoClaims(
+ m map[string]interface{},
+ userInfoClaims *commonModels.ClaimElement,
+ consentInformation ConsentInformation,
+) error {
+ for claimName, claim := range *userInfoClaims {
+ granted := false
+
+ for _, k := range consentInformation.SelectedClaims {
+ if k == claimName {
+ granted = true
+
+ break
+ }
+ }
+
+ if !granted {
+ continue
+ }
+
+ wantedValue, err := claim.WantedValue()
+ if err != nil {
+ if !errors.Is(err, commonModels.ErrNoValue) {
+ return fmt.Errorf("error handling claim: %w", err)
+ }
+ }
+
+ if wantedValue != "" {
+ m[claimName] = wantedValue
+
+ continue
+ }
+
+ if claim.IsEssential() {
+ h.logger.Warnf(
+ "handling for essential claim name %s not implemented",
+ claimName,
+ )
+ } else {
+ h.logger.Warnf(
+ "handling for claim name %s not implemented",
+ claimName,
+ )
+ }
+ }
+
+ return nil
+}
+
+func (h *ConsentHandler) GetUserInfoFromClientCertificate(r *http.Request, subject string) *UserInfo {
if r.TLS != nil && r.TLS.PeerCertificates != nil && len(r.TLS.PeerCertificates) > 0 {
firstCert := r.TLS.PeerCertificates[0]
+
var verified bool
+
for _, email := range firstCert.EmailAddresses {
h.logger.Infof("authenticated with a client certificate for email address %s", email)
+
if subject == email {
verified = true
}
}
if !verified {
- h.logger.Warnf("authentication attempt with a wrong certificate that did not contain the requested address %s", subject)
+ h.logger.Warnf(
+ "authentication attempt with a wrong certificate that did not contain the requested address %s",
+ subject,
+ )
+
return nil
}
@@ -427,10 +513,16 @@ func (h *consentHandler) GetUserInfoFromClientCertificate(r *http.Request, subje
CommonName: firstCert.Subject.CommonName,
}
}
+
return nil
}
-func NewConsentHandler(ctx context.Context, logger *log.Logger) (*consentHandler, error) {
+func NewConsentHandler(
+ logger *log.Logger,
+ bundle *i18n.Bundle,
+ messageCatalog *services.MessageCatalog,
+ adminClient admin.ClientService,
+) *ConsentHandler {
consentTemplate := template.Must(
template.ParseFS(
ui.Templates,
@@ -438,12 +530,11 @@ func NewConsentHandler(ctx context.Context, logger *log.Logger) (*consentHandler
"templates/consent.gohtml",
))
- return &consentHandler{
- adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
- bundle: services.GetI18nBundle(ctx),
+ return &ConsentHandler{
+ adminClient: adminClient,
+ bundle: bundle,
consentTemplate: consentTemplate,
- context: ctx,
logger: logger,
- messageCatalog: services.GetMessageCatalog(ctx),
- }, nil
+ messageCatalog: messageCatalog,
+ }
}
diff --git a/handlers/doc.go b/handlers/doc.go
new file mode 100644
index 0000000..0003bca
--- /dev/null
+++ b/handlers/doc.go
@@ -0,0 +1,19 @@
+/*
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package handlers provides request handlers.
+package handlers
diff --git a/handlers/error.go b/handlers/error.go
index 49a1243..1d003e0 100644
--- a/handlers/error.go
+++ b/handlers/error.go
@@ -1,18 +1,18 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package handlers
@@ -24,9 +24,10 @@ import (
"io/fs"
"net/http"
- "git.cacert.org/oidc_idp/services"
"github.com/nicksnyder/go-i18n/v2/i18n"
log "github.com/sirupsen/logrus"
+
+ "git.cacert.org/oidc_idp/services"
)
type errorKey int
@@ -62,6 +63,7 @@ func (b *ErrorBucket) serveHTTP(w http.ResponseWriter, r *http.Request) {
),
"details": b.errorDetails,
})
+
if err != nil {
log.Errorf("error rendering error template: %v", err)
http.Error(
@@ -74,92 +76,110 @@ func (b *ErrorBucket) serveHTTP(w http.ResponseWriter, r *http.Request) {
}
func GetErrorBucket(r *http.Request) *ErrorBucket {
- return r.Context().Value(errorBucketKey).(*ErrorBucket)
+ if bucket, ok := r.Context().Value(errorBucketKey).(*ErrorBucket); ok {
+ return bucket
+ }
+
+ return nil
}
-// call this from your application's handler
+// AddError can be called to add error details from your application's handler.
func (b *ErrorBucket) AddError(details *ErrorDetails) {
b.errorDetails = details
}
type errorResponseWriter struct {
http.ResponseWriter
- ctx context.Context
- statusCode int
+ errorBucket *ErrorBucket
+ statusCode int
}
func (w *errorResponseWriter) WriteHeader(code int) {
w.statusCode = code
- if code >= 400 {
+
+ if code >= http.StatusBadRequest {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- errorBucket := w.ctx.Value(errorBucketKey).(*ErrorBucket)
- if errorBucket != nil && errorBucket.errorDetails == nil {
- errorBucket.AddError(&ErrorDetails{
- ErrorMessage: http.StatusText(code),
- })
- }
+
+ w.errorBucket.AddError(&ErrorDetails{
+ ErrorMessage: http.StatusText(code),
+ })
}
+
w.ResponseWriter.WriteHeader(code)
}
func (w *errorResponseWriter) Write(content []byte) (int, error) {
- if w.statusCode > 400 {
- errorBucket := w.ctx.Value(errorBucketKey).(*ErrorBucket)
- if errorBucket != nil {
- if errorBucket.errorDetails.ErrorDetails == nil {
- errorBucket.errorDetails.ErrorDetails = make([]string, 0)
- }
- errorBucket.errorDetails.ErrorDetails = append(
- errorBucket.errorDetails.ErrorDetails, string(content),
- )
- return len(content), nil
+ if w.statusCode >= http.StatusBadRequest {
+ if w.errorBucket.errorDetails.ErrorDetails == nil {
+ w.errorBucket.errorDetails.ErrorDetails = make([]string, 0)
}
+
+ w.errorBucket.errorDetails.ErrorDetails = append(
+ w.errorBucket.errorDetails.ErrorDetails, string(content),
+ )
+
+ return len(content), nil
}
- return w.ResponseWriter.Write(content)
+
+ code, err := w.ResponseWriter.Write(content)
+ if err != nil {
+ return code, fmt.Errorf("error writing response: %w", err)
+ }
+
+ return code, nil
}
-func ErrorHandling(handlerContext context.Context, logger *log.Logger, templateFS fs.FS) (func(http.Handler) http.Handler, error) {
+func ErrorHandling(
+ handlerContext context.Context,
+ logger *log.Logger,
+ templateFS fs.FS,
+) (func(http.Handler) http.Handler, error) {
errorTemplates, err := template.ParseFS(
templateFS,
"templates/base.gohtml",
"templates/errors.gohtml",
)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("could not parse templates: %w", err)
}
+
+ bundle, err := services.GetI18nBundle(handlerContext)
+ if err != nil {
+ return nil, fmt.Errorf("could not get i18n bundle: %w", err)
+ }
+
+ messageCatalog, err := services.GetMessageCatalog(handlerContext)
+ if err != nil {
+ return nil, fmt.Errorf("could not get message catalog: %w", err)
+ }
+
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
errorBucket := &ErrorBucket{
templates: errorTemplates,
logger: logger,
- bundle: services.GetI18nBundle(handlerContext),
- messageCatalog: services.GetMessageCatalog(handlerContext),
- }
- ctx := context.WithValue(r.Context(), errorBucketKey, errorBucket)
- interCeptingResponseWriter := &errorResponseWriter{
- w,
- ctx,
- http.StatusOK,
+ bundle: bundle,
+ messageCatalog: messageCatalog,
}
next.ServeHTTP(
- interCeptingResponseWriter,
- r.WithContext(ctx),
+ &errorResponseWriter{w, errorBucket, http.StatusOK},
+ r.WithContext(context.WithValue(r.Context(), errorBucketKey, errorBucket)),
)
errorBucket.serveHTTP(w, r)
})
}, nil
}
-type errorHandler struct {
+type ErrorHandler struct {
}
-func (e *errorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (e *ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = fmt.Fprintf(w, `
didumm %#v
`, r.URL.Query())
}
-func NewErrorHandler() *errorHandler {
- return &errorHandler{}
+func NewErrorHandler() *ErrorHandler {
+ return &ErrorHandler{}
}
diff --git a/handlers/login.go b/handlers/login.go
index 0beb221..f6c2909 100644
--- a/handlers/login.go
+++ b/handlers/login.go
@@ -1,25 +1,25 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package handlers
import (
"bytes"
- "context"
+ "crypto/x509"
"errors"
"fmt"
"html/template"
@@ -27,13 +27,14 @@ import (
"strconv"
"time"
- "git.cacert.org/oidc_idp/ui"
"github.com/gorilla/csrf"
"github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/ory/hydra-client-go/client/admin"
"github.com/ory/hydra-client-go/models"
log "github.com/sirupsen/logrus"
+ "git.cacert.org/oidc_idp/ui"
+
"git.cacert.org/oidc_idp/services"
)
@@ -49,22 +50,30 @@ type templateName string
const (
CertificateLogin templateName = "cert"
- NoEmailsInClientCertificate = "no_emails"
+ NoEmailsInClientCertificate templateName = "no_emails"
)
-type loginHandler struct {
- adminClient *admin.Client
+const TimeoutTen = 10 * time.Second
+
+type LoginHandler struct {
+ adminClient admin.ClientService
bundle *i18n.Bundle
- context context.Context
logger *log.Logger
templates map[templateName]*template.Template
messageCatalog *services.MessageCatalog
}
-func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- var err error
+func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet && r.Method != http.MethodPost {
+ http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
+
+ return
+ }
+
challenge := r.URL.Query().Get("login_challenge")
+
h.logger.Debugf("received login challenge %s\n", challenge)
+
accept := r.Header.Get("Accept-Language")
localizer := i18n.NewLocalizer(h.bundle, accept)
@@ -72,110 +81,173 @@ func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if certEmails == nil {
h.renderNoEmailsInClientCertificate(w, localizer)
+
return
}
- switch r.Method {
- case http.MethodGet:
- loginRequest, err := h.adminClient.GetLoginRequest(admin.NewGetLoginRequestParams().WithLoginChallenge(challenge))
- if err != nil {
- h.logger.Warnf("could not get login request for challenge %s: %v", challenge, err)
-
- var e *admin.GetLoginRequestGone
- if errors.As(err, &e) {
- w.Header().Set("Location", *e.GetPayload().RedirectTo)
- w.WriteHeader(http.StatusGone)
- return
- }
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- h.renderRequestForClientCert(w, r, certEmails, localizer, loginRequest)
- break
- case http.MethodPost:
- if r.FormValue("use-identity") != "accept" {
- h.rejectLogin(w, challenge, localizer)
- return
- }
-
- var userId string
- // perform certificate auth
- h.logger.Infof("would perform certificate authentication with: %+v", certEmails)
- userId, err = h.performCertificateLogin(certEmails, r)
- if err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
-
- // finish login and redirect to target
- loginRequest, err := h.adminClient.AcceptLoginRequest(
- admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(
- &models.AcceptLoginRequest{
- Acr: string(ClientCertificate),
- Remember: true,
- RememberFor: 0,
- Subject: &userId,
- }).WithTimeout(time.Second * 10))
- if err != nil {
- h.logger.Errorf("error getting login request: %#v", err)
- var errorDetails *ErrorDetails
- switch v := err.(type) {
- case *admin.AcceptLoginRequestNotFound:
- payload := v.GetPayload()
- errorDetails = &ErrorDetails{
- ErrorMessage: payload.Error,
- ErrorDetails: []string{payload.ErrorDescription},
- }
- if v.Payload.StatusCode != 0 {
- errorDetails.ErrorCode = strconv.Itoa(int(payload.StatusCode))
- }
- break
- default:
- errorDetails = &ErrorDetails{
- ErrorMessage: "could not accept login",
- ErrorDetails: []string{err.Error()},
- }
- }
- GetErrorBucket(r).AddError(errorDetails)
- return
- }
- w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
- w.WriteHeader(http.StatusFound)
- default:
- http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
- return
+ if r.Method == http.MethodGet {
+ h.handleGet(w, r, challenge, certEmails, localizer)
+ } else {
+ h.handlePost(w, r, challenge, certEmails, localizer)
}
}
-func (h *loginHandler) rejectLogin(w http.ResponseWriter, challenge string, localizer *i18n.Localizer) {
- rejectLoginRequest, err := h.adminClient.RejectLoginRequest(admin.NewRejectLoginRequestParams().WithLoginChallenge(challenge).WithBody(
- &models.RejectRequest{
- ErrorDescription: h.messageCatalog.LookupMessage("LoginDeniedByUser", nil, localizer),
- ErrorHint: h.messageCatalog.LookupMessage("HintChooseAnIdentityForAuthentication", nil, localizer),
- StatusCode: http.StatusForbidden,
- },
- ).WithTimeout(time.Second * 10))
+func (h *LoginHandler) handleGet(
+ w http.ResponseWriter,
+ r *http.Request,
+ challenge string,
+ certEmails []string,
+ localizer *i18n.Localizer,
+) {
+ loginRequest, err := h.adminClient.GetLoginRequest(admin.NewGetLoginRequestParams().WithLoginChallenge(challenge))
+ if err != nil {
+ h.logger.Warnf("could not get login request for challenge %s: %v", challenge, err)
+
+ var e *admin.GetLoginRequestGone
+ if errors.As(err, &e) {
+ w.Header().Set("Location", *e.GetPayload().RedirectTo)
+ w.WriteHeader(http.StatusGone)
+
+ return
+ }
+
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
+ return
+ }
+
+ h.renderRequestForClientCert(w, r, certEmails, localizer, loginRequest)
+}
+
+func (h *LoginHandler) handlePost(
+ w http.ResponseWriter,
+ r *http.Request,
+ challenge string,
+ certEmails []string,
+ localizer *i18n.Localizer,
+) {
+ if r.FormValue("use-identity") != "accept" {
+ h.rejectLogin(w, challenge, localizer)
+
+ return
+ }
+
+ // perform certificate auth
+ h.logger.Infof("would perform certificate authentication with: %+v", certEmails)
+
+ userID, err := h.performCertificateLogin(certEmails, r)
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
+ return
+ }
+
+ // finish login and redirect to target
+ loginRequest, err := h.adminClient.AcceptLoginRequest(
+ admin.NewAcceptLoginRequestParams().WithLoginChallenge(challenge).WithBody(
+ &models.AcceptLoginRequest{
+ Acr: string(ClientCertificate),
+ Remember: true,
+ RememberFor: 0,
+ Subject: &userID,
+ }).WithTimeout(TimeoutTen))
+ if err != nil {
+ h.logger.Errorf("error getting login request: %#v", err)
+
+ h.fillAcceptLoginRequestErrorBucket(r, err)
+
+ return
+ }
+
+ w.Header().Add("Location", *loginRequest.GetPayload().RedirectTo)
+ w.WriteHeader(http.StatusFound)
+}
+
+func (h *LoginHandler) fillAcceptLoginRequestErrorBucket(r *http.Request, err error) {
+ if errorBucket := GetErrorBucket(r); errorBucket != nil {
+ var (
+ errorDetails *ErrorDetails
+ acceptLoginRequestNotFound *admin.AcceptLoginRequestNotFound
+ )
+
+ if errors.As(err, &acceptLoginRequestNotFound) {
+ payload := acceptLoginRequestNotFound.GetPayload()
+ errorDetails = &ErrorDetails{
+ ErrorMessage: payload.Error,
+ ErrorDetails: []string{payload.ErrorDescription},
+ }
+
+ if acceptLoginRequestNotFound.Payload.StatusCode != 0 {
+ errorDetails.ErrorCode = strconv.Itoa(int(payload.StatusCode))
+ }
+ } else {
+ errorDetails = &ErrorDetails{
+ ErrorMessage: "could not accept login",
+ ErrorDetails: []string{err.Error()},
+ }
+ }
+
+ errorBucket.AddError(errorDetails)
+ }
+}
+
+func (h *LoginHandler) rejectLogin(w http.ResponseWriter, challenge string, localizer *i18n.Localizer) {
+ const Ten = 10 * time.Second
+
+ rejectLoginRequest, err := h.adminClient.RejectLoginRequest(
+ admin.NewRejectLoginRequestParams().WithLoginChallenge(challenge).WithBody(
+ &models.RejectRequest{
+ ErrorDescription: h.messageCatalog.LookupMessage("LoginDeniedByUser", nil, localizer),
+ ErrorHint: h.messageCatalog.LookupMessage("HintChooseAnIdentityForAuthentication", nil, localizer),
+ StatusCode: http.StatusForbidden,
+ },
+ ).WithTimeout(Ten))
if err != nil {
h.logger.Errorf("error getting reject login request: %#v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
w.Header().Set("Location", *rejectLoginRequest.GetPayload().RedirectTo)
w.WriteHeader(http.StatusFound)
}
-func (h *loginHandler) getEmailAddressesFromClientCertificate(r *http.Request) []string {
+func (h *LoginHandler) getEmailAddressesFromClientCertificate(r *http.Request) []string {
if r.TLS != nil && r.TLS.PeerCertificates != nil && len(r.TLS.PeerCertificates) > 0 {
firstCert := r.TLS.PeerCertificates[0]
+
+ if !isClientCertificate(firstCert) {
+ return nil
+ }
+
for _, email := range firstCert.EmailAddresses {
h.logger.Infof("authenticated with a client certificate for email address %s", email)
}
+
return firstCert.EmailAddresses
}
+
return nil
}
-func (h *loginHandler) renderRequestForClientCert(w http.ResponseWriter, r *http.Request, emails []string, localizer *i18n.Localizer, loginRequest *admin.GetLoginRequestOK) {
+func isClientCertificate(cert *x509.Certificate) bool {
+ for _, ext := range cert.ExtKeyUsage {
+ if ext == x509.ExtKeyUsageClientAuth {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (h *LoginHandler) renderRequestForClientCert(
+ w http.ResponseWriter,
+ r *http.Request,
+ emails []string,
+ localizer *i18n.Localizer,
+ loginRequest *admin.GetLoginRequestOK,
+) {
trans := func(label string) string {
return h.messageCatalog.LookupMessage(label, nil, localizer)
}
@@ -184,7 +256,7 @@ func (h *loginHandler) renderRequestForClientCert(w http.ResponseWriter, r *http
err := h.templates[CertificateLogin].Lookup("base").Execute(rendered, map[string]interface{}{
"Title": trans("LoginTitle"),
csrf.TemplateTag: csrf.TemplateField(r),
- "IntroText": template.HTML(h.messageCatalog.LookupMessage(
+ "IntroText": template.HTML(h.messageCatalog.LookupMessage( //nolint:gosec
"CertLoginIntroText",
map[string]interface{}{"ClientName": loginRequest.GetPayload().Client.ClientName},
localizer,
@@ -195,27 +267,31 @@ func (h *loginHandler) renderRequestForClientCert(w http.ResponseWriter, r *http
"AcceptLabel": trans("LabelAcceptCertLogin"),
"RejectLabel": trans("LabelRejectCertLogin"),
})
+
if err != nil {
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
+
w.Header().Add("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
_, _ = w.Write(rendered.Bytes())
}
-func (h *loginHandler) performCertificateLogin(emails []string, r *http.Request) (string, error) {
+func (h *LoginHandler) performCertificateLogin(emails []string, r *http.Request) (string, error) {
requestedEmail := r.PostFormValue("email")
for _, email := range emails {
if email == requestedEmail {
return email, nil
}
}
+
return "", fmt.Errorf("no user found")
}
-func (h *loginHandler) renderNoEmailsInClientCertificate(w http.ResponseWriter, localizer *i18n.Localizer) {
+func (h *LoginHandler) renderNoEmailsInClientCertificate(w http.ResponseWriter, localizer *i18n.Localizer) {
trans := func(label string) string {
return h.messageCatalog.LookupMessage(label, nil, localizer)
}
@@ -227,15 +303,20 @@ func (h *loginHandler) renderNoEmailsInClientCertificate(w http.ResponseWriter,
if err != nil {
h.logger.Error(err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
}
-func NewLoginHandler(ctx context.Context, logger *log.Logger) (*loginHandler, error) {
- return &loginHandler{
- adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
- bundle: services.GetI18nBundle(ctx),
- context: ctx,
+func NewLoginHandler(
+ logger *log.Logger,
+ bundle *i18n.Bundle,
+ messageCatalog *services.MessageCatalog,
+ adminClient admin.ClientService,
+) *LoginHandler {
+ return &LoginHandler{
+ adminClient: adminClient,
+ bundle: bundle,
logger: logger,
templates: map[templateName]*template.Template{
CertificateLogin: template.Must(template.ParseFS(
@@ -249,6 +330,6 @@ func NewLoginHandler(ctx context.Context, logger *log.Logger) (*loginHandler, er
"templates/no_email_in_client_certificate.gohtml",
)),
},
- messageCatalog: services.GetMessageCatalog(ctx),
- }, nil
+ messageCatalog: messageCatalog,
+ }
}
diff --git a/handlers/logout.go b/handlers/logout.go
index fe092d6..e3fd7ae 100644
--- a/handlers/logout.go
+++ b/handlers/logout.go
@@ -1,24 +1,23 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package handlers
import (
- "context"
"net/http"
"time"
@@ -26,20 +25,23 @@ import (
log "github.com/sirupsen/logrus"
)
-type logoutHandler struct {
- adminClient *admin.Client
+type LogoutHandler struct {
+ adminClient admin.ClientService
logger *log.Logger
}
-func (h *logoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (h *LogoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ const Ten = 10 * time.Second
+
challenge := r.URL.Query().Get("logout_challenge")
h.logger.Debugf("received challenge %s\n", challenge)
logoutRequest, err := h.adminClient.GetLogoutRequest(
- admin.NewGetLogoutRequestParams().WithLogoutChallenge(challenge).WithTimeout(time.Second * 10))
+ admin.NewGetLogoutRequestParams().WithLogoutChallenge(challenge).WithTimeout(Ten))
if err != nil {
h.logger.Errorf("error getting logout requests: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+
return
}
@@ -56,20 +58,20 @@ func (h *logoutHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusFound)
}
-func NewLogoutHandler(ctx context.Context, logger *log.Logger) *logoutHandler {
- return &logoutHandler{
+func NewLogoutHandler(logger *log.Logger, adminClient admin.ClientService) *LogoutHandler {
+ return &LogoutHandler{
logger: logger,
- adminClient: ctx.Value(CtxAdminClient).(*admin.Client),
+ adminClient: adminClient,
}
}
-type logoutSuccessHandler struct {
+type LogoutSuccessHandler struct {
}
-func (l *logoutSuccessHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (l *LogoutSuccessHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {
panic("implement me")
}
-func NewLogoutSuccessHandler() *logoutSuccessHandler {
- return &logoutSuccessHandler{}
+func NewLogoutSuccessHandler() *LogoutSuccessHandler {
+ return &LogoutSuccessHandler{}
}
diff --git a/handlers/observability.go b/handlers/observability.go
index e5e6a9f..4bb1bf7 100644
--- a/handlers/observability.go
+++ b/handlers/observability.go
@@ -1,24 +1,25 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package handlers
import (
"context"
+ "fmt"
"net/http"
"sync/atomic"
@@ -28,7 +29,7 @@ import (
type key int
const (
- requestIdKey key = iota
+ requestIDKey key = iota
)
type statusCodeInterceptor struct {
@@ -45,7 +46,12 @@ func (sci *statusCodeInterceptor) WriteHeader(code int) {
func (sci *statusCodeInterceptor) Write(content []byte) (int, error) {
count, err := sci.ResponseWriter.Write(content)
sci.count += count
- return count, err
+
+ if err != nil {
+ return count, fmt.Errorf("could not write response: %w", err)
+ }
+
+ return count, nil
}
func Logging(logger *log.Logger) func(http.Handler) http.Handler {
@@ -53,13 +59,13 @@ func Logging(logger *log.Logger) func(http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
interceptor := &statusCodeInterceptor{w, http.StatusOK, 0}
defer func() {
- requestId, ok := r.Context().Value(requestIdKey).(string)
+ requestID, ok := r.Context().Value(requestIDKey).(string)
if !ok {
- requestId = "unknown"
+ requestID = "unknown"
}
logger.Infof(
"%s %s \"%s %s\" %d %d \"%s\"",
- requestId,
+ requestID,
r.RemoteAddr,
r.Method,
r.URL.Path,
@@ -73,15 +79,15 @@ func Logging(logger *log.Logger) func(http.Handler) http.Handler {
}
}
-func Tracing(nextRequestId func() string) func(http.Handler) http.Handler {
+func Tracing(nextRequestID func() string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- requestId := r.Header.Get("X-Request-Id")
- if requestId == "" {
- requestId = nextRequestId()
+ requestID := r.Header.Get("X-Request-Id")
+ if requestID == "" {
+ requestID = nextRequestID()
}
- ctx := context.WithValue(r.Context(), requestIdKey, requestId)
- w.Header().Set("X-Request-Id", requestId)
+ ctx := context.WithValue(r.Context(), requestIDKey, requestID)
+ w.Header().Set("X-Request-Id", requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
@@ -93,8 +99,10 @@ func NewHealthHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if atomic.LoadInt32(&Healthy) == 1 {
w.WriteHeader(http.StatusNoContent)
+
return
}
+
w.WriteHeader(http.StatusServiceUnavailable)
})
}
diff --git a/handlers/security.go b/handlers/security.go
index c0c99a2..afd9d62 100644
--- a/handlers/security.go
+++ b/handlers/security.go
@@ -1,18 +1,18 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package handlers
@@ -26,7 +26,8 @@ import (
func EnableHSTS() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", int((time.Hour*24*180).Seconds())))
+ const Days180 = 180
+ w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", int((time.Hour*24*Days180).Seconds())))
next.ServeHTTP(w, r)
})
}
diff --git a/models/oidc.go b/models/oidc.go
index 750ad28..02ee4e5 100644
--- a/models/oidc.go
+++ b/models/oidc.go
@@ -1,45 +1,47 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
-/*
-This package contains data models.
-*/
+// Package models contains data models
package models
-// An individual claim request.
+import "errors"
+
+var ErrNoValue = errors.New("value not found")
+
+// IndividualClaimsRequest represents an individual claim request.
//
-// Specification
+// # Specification
//
// https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
-type IndividualClaimRequest map[string]interface{}
+type IndividualClaimsRequest map[string]interface{}
// ClaimElement represents a claim element
-type ClaimElement map[string]*IndividualClaimRequest
+type ClaimElement map[string]*IndividualClaimsRequest
// OIDCClaimsRequest the claims request parameter sent with the authorization request.
//
-// Specification
+// # Specification
//
// https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
type OIDCClaimsRequest map[string]ClaimElement
// GetUserInfo extracts the userinfo claim element from the request.
//
-// Specification
+// # Specification
//
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
//
@@ -56,12 +58,13 @@ func (r OIDCClaimsRequest) GetUserInfo() *ClaimElement {
if userInfo, ok := r["userinfo"]; ok {
return &userInfo
}
+
return nil
}
// GetIDToken extracts the id_token claim element from the request.
//
-// Specification
+// # Specification
//
// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
//
@@ -75,12 +78,13 @@ func (r OIDCClaimsRequest) GetIDToken() *ClaimElement {
if idToken, ok := r["id_token"]; ok {
return &idToken
}
+
return nil
}
-// Checks whether the individual claim is an essential claim.
+// IsEssential checks whether the individual claim is an essential claim.
//
-// Specification
+// # Specification
//
// https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
//
@@ -88,7 +92,7 @@ func (r OIDCClaimsRequest) GetIDToken() *ClaimElement {
// value is true, this indicates that the Claim is an Essential Claim. For
// instance, the Claim request:
//
-// "auth_time": {"essential": true}
+// "auth_time": {"essential": true}
//
// can be used to specify that it is Essential to return an auth_time Claim
// Value. If the value is false, it indicates that it is a Voluntary Claim.
@@ -99,54 +103,59 @@ func (r OIDCClaimsRequest) GetIDToken() *ClaimElement {
// specific task requested by the End-User.
//
// Note that even if the Claims are not available because the End-User did not
-// authorize their release or they are not present, the Authorization Server
+// authorize their release, or they are not present, the Authorization Server
// MUST NOT generate an error when Claims are not returned, whether they are
// Essential or Voluntary, unless otherwise specified in the description of
// the specific claim.
-func (i IndividualClaimRequest) IsEssential() bool {
+func (i IndividualClaimsRequest) IsEssential() bool {
if essential, ok := i["essential"]; ok {
- return essential.(bool)
+ if e, ok := essential.(bool); ok {
+ return e
+ }
}
+
return false
}
-// Returns the wanted value for an individual claim request.
+// WantedValue returns the wanted value for an individual claim request.
//
-// Specification
+// # Specification
//
// https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
//
// Requests that the Claim be returned with a particular value. For instance
// the Claim request:
//
-// "sub": {"value": "248289761001"}
+// "sub": {"value": "248289761001"}
//
// can be used to specify that the request apply to the End-User with Subject
// Identifier 248289761001. The value of the value member MUST be a valid
// value for the Claim being requested. Definitions of individual Claims can
// include requirements on how and whether the value qualifier is to be used
// when requesting that Claim.
-func (i IndividualClaimRequest) WantedValue() *string {
+func (i IndividualClaimsRequest) WantedValue() (string, error) {
if value, ok := i["value"]; ok {
- valueString := value.(string)
- return &valueString
+ if valueString, ok := value.(string); ok {
+ return valueString, nil
+ }
}
- return nil
+
+ return "", ErrNoValue
}
-// Get the allowed values for an individual claim request that specifies
+// AllowedValues gets the allowed values for an individual claim request that specifies
// a values field.
//
-// Specification
+// # Specification
//
// https://openid.net/specs/openid-connect-core-1_0.html#IndividualClaimsRequests
//
// Requests that the Claim be returned with one of a set of values, with the
// values appearing in order of preference. For instance the Claim request:
//
-// "acr": {"essential": true,
-// "values": ["urn:mace:incommon:iap:silver",
-// "urn:mace:incommon:iap:bronze"]}
+// "acr": {"essential": true,
+// "values": ["urn:mace:incommon:iap:silver",
+// "urn:mace:incommon:iap:bronze"]}
//
// specifies that it is Essential that the acr Claim be returned with either
// the value urn:mace:incommon:iap:silver or urn:mace:incommon:iap:bronze.
@@ -154,17 +163,20 @@ func (i IndividualClaimRequest) WantedValue() *string {
// being requested. Definitions of individual Claims can include requirements
// on how and whether the values qualifier is to be used when requesting that
// Claim.
-func (i IndividualClaimRequest) AllowedValues() []string {
+func (i IndividualClaimsRequest) AllowedValues() []string {
if values, ok := i["values"]; ok {
- return values.([]string)
+ if v, ok := values.([]string); ok {
+ return v
+ }
}
+
return nil
}
// OpenIDConfiguration contains the parts of the OpenID discovery information
// that are relevant for us.
//
-// Specifications
+// # Specifications
//
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
//
@@ -174,7 +186,7 @@ type OpenIDConfiguration struct {
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
- JwksUri string `json:"jwks_uri"`
+ JwksURI string `json:"jwks_uri"`
RegistrationEndpoint string `json:"registration_endpoint"`
ScopesSupported []string `json:"scopes_supported"`
EndSessionEndpoint string `json:"end_session_endpoint"`
diff --git a/services/configuration.go b/services/configuration.go
index e61e555..5d63b67 100644
--- a/services/configuration.go
+++ b/services/configuration.go
@@ -1,18 +1,18 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package services
@@ -39,44 +39,48 @@ func ConfigureApplication(
) (*koanf.Koanf, error) {
f := pflag.NewFlagSet("config", pflag.ContinueOnError)
f.Usage = func() {
- fmt.Println(f.FlagUsages())
+ fmt.Println(f.FlagUsages()) //nolint:forbidigo
os.Exit(0)
}
+
f.StringSlice(
"conf",
[]string{fmt.Sprintf("%s.toml", strings.ToLower(appName))},
"path to one or more .toml files",
)
- var err error
- if err = f.Parse(os.Args[1:]); err != nil {
+ if err := f.Parse(os.Args[1:]); err != nil {
logger.Fatal(err)
}
config := koanf.New(".")
_ = config.Load(confmap.Provider(defaultConfig, "."), nil)
+
cFiles, _ := f.GetStringSlice("conf")
for _, c := range cFiles {
if err := config.Load(file.Provider(c), toml.Parser()); err != nil {
logger.Fatalf("error loading config file: %s", err)
}
}
+
if err := config.Load(posflag.Provider(f, ".", config), nil); err != nil {
logger.Fatalf("error loading configuration: %s", err)
}
+
if err := config.Load(
file.Provider("resource_app.toml"),
toml.Parser(),
); err != nil && !os.IsNotExist(err) {
- logrus.Fatalf("error loading config: %v", err)
+ logger.Fatalf("error loading config: %v", err)
}
+
prefix := fmt.Sprintf("%s_", strings.ToUpper(appName))
if err := config.Load(env.Provider(prefix, ".", func(s string) string {
- return strings.Replace(strings.ToLower(
- strings.TrimPrefix(s, prefix)), "_", ".", -1)
+ return strings.ReplaceAll(strings.ToLower(strings.TrimPrefix(s, prefix)), "_", ".")
}), nil); err != nil {
- logrus.Fatalf("error loading config: %v", err)
+ logger.Fatalf("error loading config: %v", err)
}
- return config, err
+
+ return config, nil
}
diff --git a/services/i18n.go b/services/i18n.go
index a551d00..2e88470 100644
--- a/services/i18n.go
+++ b/services/i18n.go
@@ -1,24 +1,25 @@
/*
- Copyright 2020, 2021 Jan Dittberner
+Copyright 2020-2023 CAcert Inc.
+SPDX-License-Identifier: Apache-2.0
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
*/
package services
import (
"context"
+ "errors"
"fmt"
log "github.com/sirupsen/logrus"
@@ -28,7 +29,7 @@ import (
"golang.org/x/text/language"
)
-func AddMessages(ctx context.Context) {
+func AddMessages(catalog *MessageCatalog) error {
messages := make(map[string]*i18n.Message)
messages["unknown"] = &i18n.Message{
ID: "ErrorUnknown",
@@ -47,12 +48,14 @@ func AddMessages(ctx context.Context) {
Other: "I hereby agree that the application may get the requested permissions.",
}
messages["IntroConsentRequested"] = &i18n.Message{
- ID: "IntroConsentRequested",
- Other: "The {{ .client }} application requested your consent for the following set of permissions:",
+ ID: "IntroConsentRequested",
+ Other: "The {{ .client }} application requested your consent for the following set of " +
+ "permissions:",
}
messages["IntroConsentMoreInformation"] = &i18n.Message{
- ID: "IntroConsentMoreInformation",
- Other: "You can find more information about {{ .client }} at its description page.",
+ ID: "IntroConsentMoreInformation",
+ Other: "You can find more information about {{ .client }} at " +
+ "its description page.",
}
messages["ClaimsInformation"] = &i18n.Message{
ID: "ClaimsInformation",
@@ -67,12 +70,13 @@ func AddMessages(ctx context.Context) {
Other: "The application {{ .ClientName }} requests a login.",
}
messages["EmailChoiceText"] = &i18n.Message{
- ID: "EmailChoiceText",
- One: "You have presented a valid client certificate for the following email address:",
- Other: "You have presented a valid client certificate for multiple email addresses. Please choose which one you want to present to the application:",
+ ID: "EmailChoiceText",
+ One: "You have presented a valid client certificate for the following email address:",
+ Other: "You have presented a valid client certificate for multiple email addresses. " +
+ "Please choose which one you want to present to the application:",
}
messages["LoginTitle"] = &i18n.Message{
- ID: "LoginTitle",
+ ID: "LoginTitle",
Other: "Authenticate with a client certificate",
}
messages["CertLoginRequestText"] = &i18n.Message{
@@ -97,7 +101,10 @@ func AddMessages(ctx context.Context) {
ID: "HintChooseAnIdentityForAuthentication",
Other: "Choose an identity for authentication.",
}
- GetMessageCatalog(ctx).AddMessages(messages)
+
+ catalog.AddMessages(messages)
+
+ return nil
}
type contextKey int
@@ -118,17 +125,21 @@ func (m *MessageCatalog) AddMessages(messages map[string]*i18n.Message) {
}
}
-func (m *MessageCatalog) LookupErrorMessage(tag string, field string, value interface{}, localizer *i18n.Localizer) string {
+func (m *MessageCatalog) LookupErrorMessage(tag, 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.Warnf("no default translation found")
+
return tag
}
}
@@ -142,38 +153,41 @@ func (m *MessageCatalog) LookupErrorMessage(tag string, field string, value inte
})
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 {
+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 {
- switch err.(type) {
- case *i18n.MessageNotFoundErr:
- m.logger.Warnf("message %s not found: %v", id, err)
- if translation != "" {
- return translation
- }
- break
- default:
- m.logger.Error(err)
- }
- return id
+ return m.handleLocalizeError(id, translation, err)
}
+
return translation
- } else {
- m.logger.Warnf("no translation found for %s", id)
- return id
}
+
+ m.logger.Warnf("no translation found for %s", id)
+
+ return id
}
-func (m *MessageCatalog) LookupMessagePlural(id string, templateData map[string]interface{}, localizer *i18n.Localizer, count int) string {
+func (m *MessageCatalog) LookupMessagePlural(
+ id string,
+ templateData map[string]interface{},
+ localizer *i18n.Localizer,
+ count int,
+) string {
if message, ok := m.messages[id]; ok {
translation, err := localizer.Localize(&i18n.LocalizeConfig{
DefaultMessage: message,
@@ -181,38 +195,47 @@ func (m *MessageCatalog) LookupMessagePlural(id string, templateData map[string]
PluralCount: count,
})
if err != nil {
- switch err.(type) {
- case *i18n.MessageNotFoundErr:
- m.logger.Warnf("message %s not found: %v", id, err)
- if translation != "" {
- return translation
- }
- break
- default:
- m.logger.Error(err)
- }
- return id
+ return m.handleLocalizeError(id, translation, err)
}
+
return translation
- } else {
- m.logger.Warnf("no translation found for %s", id)
- return id
}
+
+ m.logger.Warnf("no translation found for %s", id)
+
+ return id
}
-func InitI18n(ctx context.Context, logger *log.Logger, languages []string) context.Context {
+func (m *MessageCatalog) handleLocalizeError(id string, translation string, err error) string {
+ var messageNotFound *i18n.MessageNotFoundErr
+
+ if errors.As(err, &messageNotFound) {
+ m.logger.Warnf("message %s not found: %v", id, err)
+
+ if translation != "" {
+ return translation
+ }
+ } else {
+ m.logger.Error(err)
+ }
+
+ return id
+}
+
+func InitI18n(logger *log.Logger, languages []string) (*i18n.Bundle, *MessageCatalog) {
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")
+ logger.Warnf("message bundle %s.toml not found", lang)
}
}
+
catalog := initMessageCatalog(logger)
- ctx = context.WithValue(ctx, ctxI18nBundle, bundle)
- ctx = context.WithValue(ctx, ctxI18nCatalog, catalog)
- return ctx
+
+ return bundle, catalog
}
func initMessageCatalog(logger *log.Logger) *MessageCatalog {
@@ -221,13 +244,22 @@ func initMessageCatalog(logger *log.Logger) *MessageCatalog {
ID: "ErrorTitle",
Other: "An error has occurred",
}
+
return &MessageCatalog{messages: messages, logger: logger}
}
-func GetI18nBundle(ctx context.Context) *i18n.Bundle {
- return ctx.Value(ctxI18nBundle).(*i18n.Bundle)
+func GetI18nBundle(ctx context.Context) (*i18n.Bundle, error) {
+ if b, ok := ctx.Value(ctxI18nBundle).(*i18n.Bundle); ok {
+ return b, nil
+ }
+
+ return nil, errors.New("context value is not a Bundle")
}
-func GetMessageCatalog(ctx context.Context) *MessageCatalog {
- return ctx.Value(ctxI18nCatalog).(*MessageCatalog)
+func GetMessageCatalog(ctx context.Context) (*MessageCatalog, error) {
+ if c, ok := ctx.Value(ctxI18nCatalog).(*MessageCatalog); ok {
+ return c, nil
+ }
+
+ return nil, errors.New("context value is not a MessageCatalog")
}