mirror of
https://github.com/nais/wonderwall.git
synced 2026-02-14 17:49:54 +00:00
169 lines
5.3 KiB
Go
169 lines
5.3 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/nais/wonderwall/internal/o11y/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
httpinternal "github.com/nais/wonderwall/internal/http"
|
|
"github.com/nais/wonderwall/pkg/cookie"
|
|
mw "github.com/nais/wonderwall/pkg/middleware"
|
|
"github.com/nais/wonderwall/pkg/openid"
|
|
"github.com/nais/wonderwall/pkg/router/paths"
|
|
urlpkg "github.com/nais/wonderwall/pkg/url"
|
|
"github.com/nais/wonderwall/templates"
|
|
)
|
|
|
|
const (
|
|
// MaxAutoRetryAttempts is the maximum number of times to automatically redirect the user to retry their original request.
|
|
MaxAutoRetryAttempts = 3
|
|
)
|
|
|
|
func (s *Standalone) InternalError(w http.ResponseWriter, r *http.Request, cause error) {
|
|
span := trace.SpanFromContext(r.Context())
|
|
otel.AddErrorEvent(span, "errorHandler", "InternalError", cause)
|
|
s.respondError(w, r, http.StatusInternalServerError, cause, log.ErrorLevel)
|
|
}
|
|
|
|
func (s *Standalone) BadRequest(w http.ResponseWriter, r *http.Request, cause error) {
|
|
span := trace.SpanFromContext(r.Context())
|
|
otel.AddErrorEvent(span, "errorHandler", "BadRequest", cause)
|
|
s.respondError(w, r, http.StatusBadRequest, cause, log.ErrorLevel)
|
|
}
|
|
|
|
func (s *Standalone) Unauthorized(w http.ResponseWriter, r *http.Request, cause error) {
|
|
span := trace.SpanFromContext(r.Context())
|
|
otel.AddErrorEvent(span, "errorHandler", "Unauthorized", cause)
|
|
s.respondError(w, r, http.StatusUnauthorized, cause, log.WarnLevel)
|
|
}
|
|
|
|
func (s *Standalone) TooManyRequests(w http.ResponseWriter, r *http.Request, cause error) {
|
|
span := trace.SpanFromContext(r.Context())
|
|
otel.AddErrorEvent(span, "errorHandler", "TooManyRequests", cause)
|
|
s.respondError(w, r, http.StatusTooManyRequests, cause, log.WarnLevel)
|
|
}
|
|
|
|
// Retry returns a URI that should retry the desired route that failed.
|
|
// It only handles the routes exposed by Wonderwall, i.e. `/oauth2/*`. As these routes
|
|
// are related to the authentication flow, we default to redirecting back to the handled
|
|
// `/oauth2/login` endpoint unless the original request attempted to reach the logout-flow.
|
|
func (s *Standalone) Retry(r *http.Request, loginCookie *openid.LoginCookie) string {
|
|
requestPath := r.URL.Path
|
|
ingressPath := s.GetPath(r)
|
|
|
|
// redirect failed logout callbacks to logout
|
|
if strings.HasSuffix(requestPath, paths.OAuth2+paths.LogoutCallback) {
|
|
return ingressPath + paths.OAuth2 + paths.Logout
|
|
}
|
|
|
|
// redirect failed login callbacks to login with original referer as the redirect_uri
|
|
if strings.HasSuffix(requestPath, paths.OAuth2+paths.LoginCallback) {
|
|
redirect := s.Redirect.Canonical(r)
|
|
if loginCookie != nil && len(loginCookie.Referer) > 0 {
|
|
redirect = s.Redirect.Clean(r, loginCookie.Referer)
|
|
}
|
|
|
|
return urlpkg.LoginRelative(ingressPath, redirect)
|
|
}
|
|
|
|
u := *r.URL
|
|
u.Host = ""
|
|
u.Scheme = ""
|
|
return u.String()
|
|
}
|
|
|
|
func (s *Standalone) respondError(w http.ResponseWriter, r *http.Request, statusCode int, cause error, level log.Level) {
|
|
span := trace.SpanFromContext(r.Context())
|
|
logger := mw.LogEntryFrom(r).WithFields(httpinternal.Attributes(r))
|
|
msg := "error in route: %+v"
|
|
|
|
incrementRetryAttempt(w, r, s.GetCookieOptions(r))
|
|
|
|
attempts, ok := getRetryAttempts(r)
|
|
span.SetAttributes(attribute.Int("error.retry_count", attempts))
|
|
if (!ok || attempts < MaxAutoRetryAttempts) && (statusCode != http.StatusTooManyRequests) {
|
|
span.SetAttributes(attribute.Bool("error.retry", true))
|
|
loginCookie, err := openid.GetLoginCookie(r, s.Crypter)
|
|
if err != nil {
|
|
loginCookie = nil
|
|
}
|
|
|
|
retryUri := s.Retry(r, loginCookie)
|
|
logger.Infof(msg, cause)
|
|
logger.Infof("errorhandler: auto-retry (attempt %d/%d) redirecting to %q...", attempts+1, MaxAutoRetryAttempts, retryUri)
|
|
http.Redirect(w, r, retryUri, http.StatusTemporaryRedirect)
|
|
|
|
return
|
|
}
|
|
|
|
if level == log.WarnLevel || errors.Is(cause, context.Canceled) {
|
|
logger.Warnf(msg, cause)
|
|
} else {
|
|
logger.Errorf(msg, cause)
|
|
}
|
|
|
|
logger.Infof("errorhandler: maximum retry attempts exceeded; executing error template...")
|
|
span.SetAttributes(attribute.Bool("error.retries_exhausted", true))
|
|
s.defaultErrorResponse(w, r, statusCode)
|
|
}
|
|
|
|
func (s *Standalone) defaultErrorResponse(w http.ResponseWriter, r *http.Request, statusCode int) {
|
|
w.WriteHeader(statusCode)
|
|
|
|
loginCookie, err := openid.GetLoginCookie(r, s.Crypter)
|
|
if err != nil {
|
|
loginCookie = nil
|
|
}
|
|
|
|
defaultRedirect := s.Ingresses.Single().String()
|
|
if s.Config.SSO.IsServer() {
|
|
defaultRedirect = s.Config.SSO.ServerDefaultRedirectURL
|
|
}
|
|
|
|
err = templates.ExecError(w, templates.ErrorVariables{
|
|
CorrelationID: middleware.GetReqID(r.Context()),
|
|
CSS: templates.CSS,
|
|
DefaultRedirectURI: defaultRedirect,
|
|
HttpStatusCode: statusCode,
|
|
RetryURI: s.Retry(r, loginCookie),
|
|
})
|
|
if err != nil {
|
|
mw.LogEntryFrom(r).Errorf("errorhandler: executing error template: %+v", err)
|
|
}
|
|
}
|
|
|
|
func getRetryAttempts(r *http.Request) (int, bool) {
|
|
c, err := cookie.Get(r, cookie.Retry)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
|
|
val, err := strconv.Atoi(c.Value)
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
|
|
return val, true
|
|
}
|
|
|
|
func incrementRetryAttempt(w http.ResponseWriter, r *http.Request, opts cookie.Options) {
|
|
val := 1
|
|
|
|
prev, ok := getRetryAttempts(r)
|
|
if ok {
|
|
val = prev + 1
|
|
}
|
|
|
|
c := cookie.Make(cookie.Retry, strconv.Itoa(val), opts)
|
|
cookie.Set(w, c)
|
|
}
|