Fix linter warnings, modernize code

pull/1/head
Jan Dittberner 1 year ago
parent e828b30b21
commit 2c82ccb324

@ -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)
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
err := h.parseUserInfoClaims(m, userInfoClaims, consentInformation)
if err != nil {
return err
}
}
return nil
}
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 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,
)
}
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 {
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,
}
}

@ -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
}
code, err := w.ResponseWriter.Write(content)
if err != nil {
return code, fmt.Errorf("error writing response: %w", err)
}
return w.ResponseWriter.Write(content)
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)
if r.Method == http.MethodGet {
h.handleGet(w, r, challenge, certEmails, localizer)
} else {
h.handlePost(w, r, challenge, certEmails, localizer)
}
}
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
}
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)
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()},
}
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()},
}
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
errorBucket.AddError(errorDetails)
}
}
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) 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
}

@ -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
}
m.logger.Warnf("no translation found for %s", id)
return id
}
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.Warnf("no translation found for %s", id)
return id
m.logger.Error(err)
}
return id
}
func InitI18n(ctx context.Context, logger *log.Logger, languages []string) context.Context {
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…
Cancel
Save