Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Traefik decision api support #904

Merged
merged 19 commits into from
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions api/decision.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ import (

const (
DecisionPath = "/decisions"

xForwardedMethod = "X-Forwarded-Method"
xForwardedProto = "X-Forwarded-Proto"
xForwardedHost = "X-Forwarded-Host"
xForwardedUri = "X-Forwarded-Uri"
)

type decisionHandlerRegistry interface {
Expand All @@ -53,12 +58,29 @@ func NewJudgeHandler(r decisionHandlerRegistry) *DecisionHandler {

func (h *DecisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if len(r.URL.Path) >= len(DecisionPath) && r.URL.Path[:len(DecisionPath)] == DecisionPath {
r.URL.Scheme = "http"
r.URL.Host = r.Host
if r.TLS != nil || strings.EqualFold(r.Header.Get("X-Forwarded-Proto"), "https") {
r.URL.Scheme = "https"
var method, scheme, host, path string

if method = r.Header.Get(xForwardedMethod); method == "" {
method = r.Method
}
if scheme = r.Header.Get(xForwardedProto); scheme == "" {
if r.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
if host = r.Header.Get(xForwardedHost); host == "" {
host = r.Host
}
r.URL.Path = r.URL.Path[len(DecisionPath):]
if path = r.Header.Get(xForwardedUri); path == "" {
path = r.URL.Path[len(DecisionPath):]
}

r.Method = method
r.URL.Scheme = scheme
r.URL.Host = host
r.URL.Path = path

h.decisions(w, r)
} else {
Expand Down Expand Up @@ -112,7 +134,6 @@ func (h *DecisionHandler) decisions(w http.ResponseWriter, r *http.Request) {
WithFields(fields).
WithField("granted", false).
Info("Access request denied")

h.r.ProxyRequestHandler().HandleError(w, r, rl, err)
return
}
Expand Down
14 changes: 8 additions & 6 deletions credentials/verifier_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"crypto/rsa"
"fmt"
"strings"

"github.com/golang-jwt/jwt/v4"
"github.com/pkg/errors"
Expand Down Expand Up @@ -42,7 +43,7 @@ func (v *VerifierDefault) Verify(

kid, ok := token.Header["kid"].(string)
if !ok || kid == "" {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("The JSON Web Token must contain a kid header value but did not."))
return nil, errors.WithStack(herodot.ErrBadRequest.WithReason("The JSON Web Token must contain a kid header value but did not."))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing all of these is kind of a breaking change (although not an terrible one). Given that our service configuration returns something incorrect here, why would we report to the client calling this endpoint that it's a bad request?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to my understanding of the code, this is actually not about our service configuration, it is about the data sent by the client. If the client sent a malformed request (here the an access token not containing the expected data), it should imho be answered accordingly.

Another reason was logging. Internal server error is about unrecoverable errors, which imply some kind of a bug, which should be addressed. Here, we don't have things like this. So I wanted avoiding the be filled with error log statements, which are no errors.

Do you agree, or do you still want to have these changes reverted?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, in case that we verify credentials, Unauthorized error would also be acceptable. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main idea is not polluting the logs with error messages, which are actually not errors. I consider this as best practice.
Ah, if I remember correctly, to have the error handler working with traefik (to redirect to the login page), I had to change some of the responses to Unauthorized (which relate to authentication cases).

Back to your proposal. So you would rather like to see all changed responses to be Unauthorized? Fine for me ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please let me know if this is what you would like to have. I'll then update the PR accordingly.

}

key, err := v.r.CredentialsFetcher().ResolveKey(ctx, r.KeyURLs, kid, "sig")
Expand Down Expand Up @@ -74,10 +75,10 @@ func (v *VerifierDefault) Verify(
return k, nil
}
default:
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`This request object uses unsupported signing algorithm "%s".`, token.Header["alg"]))
return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`This request object uses unsupported signing algorithm "%s".`, token.Header["alg"]))
}

return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`The signing key algorithm does not match the algorithm from the token header.`))
return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`The signing key algorithm does not match the algorithm from the token header.`))
})
if err != nil {
if e, ok := errors.Cause(err).(*jwt.ValidationError); ok {
Expand All @@ -100,13 +101,14 @@ func (v *VerifierDefault) Verify(
parsedClaims := jwtx.ParseMapStringInterfaceClaims(claims)
for _, audience := range r.Audiences {
if !stringslice.Has(parsedClaims.Audience, audience) {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Token audience %v is not intended for target audience %s.", parsedClaims.Audience, audience))
return nil, herodot.ErrUnauthorized.WithReasonf("Token audience %v is not intended for target audience %s.", parsedClaims.Audience, audience)
}
}

if len(r.Issuers) > 0 {
if !stringslice.Has(r.Issuers, parsedClaims.Issuer) {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Token issuer does not match any trusted issuer."))
return nil, herodot.ErrUnauthorized.WithReasonf("Token issuer does not match any trusted issuer %s.", parsedClaims.Issuer).
WithDetail("received issuers", strings.Join(r.Issuers, ", "))
}
}

Expand All @@ -117,7 +119,7 @@ func (v *VerifierDefault) Verify(
if r.ScopeStrategy != nil {
for _, sc := range r.Scope {
if !r.ScopeStrategy(s, sc) {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`JSON Web Token is missing required scope "%s".`, sc))
return nil, herodot.ErrUnauthorized.WithReasonf(`JSON Web Token is missing required scope "%s".`, sc)
}
}
} else {
Expand Down
30 changes: 27 additions & 3 deletions pipeline/errors/error_redirect.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package errors

import (
"encoding/json"
"fmt"
"net/http"
"net/url"

Expand Down Expand Up @@ -40,7 +41,29 @@ func (a *ErrorRedirect) Handle(w http.ResponseWriter, r *http.Request, config js
return err
}

http.Redirect(w, r, a.RedirectURL(r, c), c.Code)
var scheme, host, requestUri string
if scheme = r.Header.Get("X-Forwarded-Proto"); scheme == "" {
scheme = r.URL.Scheme
}
if host = r.Header.Get("X-Forwarded-Host"); host == "" {
host = r.URL.Host
}
if requestUri = r.Header.Get("X-Forwarded-Uri"); requestUri == "" {
requestUri = r.URL.RequestURI()
}

var uri *url.URL
if scheme == "" || host == "" {
// FIXME: I don't think this is applicable for real requests. It is however used by tests.
uri, err = url.Parse(fmt.Sprintf("%s", requestUri))
} else {
uri, err = url.Parse(fmt.Sprintf("%s://%s%s", scheme, host, requestUri))
}
if err != nil {
return err
}

http.Redirect(w, r, a.RedirectURL(uri, c), c.Code)
return nil
}

Expand Down Expand Up @@ -69,7 +92,7 @@ func (a *ErrorRedirect) GetID() string {
return "redirect"
}

func (a *ErrorRedirect) RedirectURL(r *http.Request, c *ErrorRedirectConfig) string {
func (a *ErrorRedirect) RedirectURL(uri *url.URL, c *ErrorRedirectConfig) string {
if c.ReturnToQueryParam == "" {
return c.To
}
Expand All @@ -78,8 +101,9 @@ func (a *ErrorRedirect) RedirectURL(r *http.Request, c *ErrorRedirectConfig) str
if err != nil {
return c.To
}

q := u.Query()
q.Set(c.ReturnToQueryParam, r.URL.String())
q.Set(c.ReturnToQueryParam, uri.String())
u.RawQuery = q.Encode()
return u.String()
}