Fix linter warnings, modernize code
This commit is contained in:
parent
e828b30b21
commit
2c82ccb324
12 changed files with 763 additions and 504 deletions
89
cmd/idp.go
89
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 {
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
19
handlers/doc.go
Normal file
19
handlers/doc.go
Normal file
|
@ -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
|
|
@ -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{}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
154
services/i18n.go
154
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 <strong>{{ .client }}</strong> application requested your consent for the following set of permissions:",
|
||||
ID: "IntroConsentRequested",
|
||||
Other: "The <strong>{{ .client }}</strong> application requested your consent for the following set of " +
|
||||
"permissions:",
|
||||
}
|
||||
messages["IntroConsentMoreInformation"] = &i18n.Message{
|
||||
ID: "IntroConsentMoreInformation",
|
||||
Other: "You can find more information about <strong>{{ .client }}</strong> at <a href=\"{{ .clientLink }}\">its description page</a>.",
|
||||
ID: "IntroConsentMoreInformation",
|
||||
Other: "You can find more information about <strong>{{ .client }}</strong> at " +
|
||||
"<a href=\"{{ .clientLink }}\">its description page</a>.",
|
||||
}
|
||||
messages["ClaimsInformation"] = &i18n.Message{
|
||||
ID: "ClaimsInformation",
|
||||
|
@ -67,12 +70,13 @@ func AddMessages(ctx context.Context) {
|
|||
Other: "The application <strong>{{ .ClientName }}</strong> 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")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue