Fix: ‘autoRedirect’ hardcode ‘https’ scheme

Signed-off-by: icefed <zlwangel@gmail.com>
This commit is contained in:
icefed 2024-03-05 20:50:09 +08:00
parent 51a72c2aef
commit 63eb22d74b
No known key found for this signature in database
GPG key ID: 84C3CBCCDE9ABC56
3 changed files with 157 additions and 29 deletions

View file

@ -591,7 +591,8 @@ security.
| `service` | yes | The service being authenticated. | | `service` | yes | The service being authenticated. |
| `issuer` | yes | The name of the token issuer. The issuer inserts this into the token so it must match the value configured for the issuer. | | `issuer` | yes | The name of the token issuer. The issuer inserts this into the token so it must match the value configured for the issuer. |
| `rootcertbundle` | yes | The absolute path to the root certificate bundle. This bundle contains the public part of the certificates used to sign authentication tokens. | | `rootcertbundle` | yes | The absolute path to the root certificate bundle. This bundle contains the public part of the certificates used to sign authentication tokens. |
| `autoredirect` | no | When set to `true`, `realm` will automatically be set using the Host header of the request as the domain and a path of `/auth/token/`| | `autoredirect` | no | When set to `true`, `realm` will automatically be set using the Host header of the request as the domain and a path of `/auth/token/`(or specified by `autoredirectpath`), the `realm` URL Scheme will use `X-Forwarded-Proto` header if set, otherwise it will be set to `https`. |
| `autoredirectpath` | no | The path to redirect to if `autoredirect` is set to `true`, default: `/auth/token/`. |
For more information about Token based authentication configuration, see the For more information about Token based authentication configuration, see the

View file

@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"os" "os"
"strings" "strings"
@ -83,11 +84,12 @@ var (
// authChallenge implements the auth.Challenge interface. // authChallenge implements the auth.Challenge interface.
type authChallenge struct { type authChallenge struct {
err error err error
realm string realm string
autoRedirect bool autoRedirect bool
service string autoRedirectPath string
accessSet accessSet service string
accessSet accessSet
} }
var _ auth.Challenge = authChallenge{} var _ auth.Challenge = authChallenge{}
@ -102,13 +104,28 @@ func (ac authChallenge) Status() int {
return http.StatusUnauthorized return http.StatusUnauthorized
} }
func buildAutoRedirectURL(r *http.Request, autoRedirectPath string) string {
scheme := "https"
if forwardedProto := r.Header.Get("X-Forwarded-Proto"); len(forwardedProto) > 0 {
scheme = forwardedProto
}
u := &url.URL{
Scheme: scheme,
Host: r.Host,
Path: autoRedirectPath,
}
return u.String()
}
// challengeParams constructs the value to be used in // challengeParams constructs the value to be used in
// the WWW-Authenticate response challenge header. // the WWW-Authenticate response challenge header.
// See https://tools.ietf.org/html/rfc6750#section-3 // See https://tools.ietf.org/html/rfc6750#section-3
func (ac authChallenge) challengeParams(r *http.Request) string { func (ac authChallenge) challengeParams(r *http.Request) string {
var realm string var realm string
if ac.autoRedirect { if ac.autoRedirect {
realm = fmt.Sprintf("https://%s/auth/token", r.Host) realm = buildAutoRedirectURL(r, ac.autoRedirectPath)
} else { } else {
realm = ac.realm realm = ac.realm
} }
@ -134,23 +151,29 @@ func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
// accessController implements the auth.AccessController interface. // accessController implements the auth.AccessController interface.
type accessController struct { type accessController struct {
realm string realm string
autoRedirect bool autoRedirect bool
issuer string autoRedirectPath string
service string issuer string
rootCerts *x509.CertPool service string
trustedKeys map[string]crypto.PublicKey rootCerts *x509.CertPool
trustedKeys map[string]crypto.PublicKey
} }
const (
defaultAutoRedirectPath = "/auth/token"
)
// tokenAccessOptions is a convenience type for handling // tokenAccessOptions is a convenience type for handling
// options to the contstructor of an accessController. // options to the contstructor of an accessController.
type tokenAccessOptions struct { type tokenAccessOptions struct {
realm string realm string
autoRedirect bool autoRedirect bool
issuer string autoRedirectPath string
service string issuer string
rootCertBundle string service string
jwks string rootCertBundle string
jwks string
} }
// checkOptions gathers the necessary options // checkOptions gathers the necessary options
@ -187,6 +210,19 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
} }
opts.autoRedirect = autoRedirect opts.autoRedirect = autoRedirect
} }
if opts.autoRedirect {
autoRedirectPathVal, ok := options["autoredirectpath"]
if ok {
autoRedirectPath, ok := autoRedirectPathVal.(string)
if !ok {
return opts, fmt.Errorf("token auth requires a valid option string: autoredirectpath")
}
opts.autoRedirectPath = autoRedirectPath
}
if opts.autoRedirectPath == "" {
opts.autoRedirectPath = defaultAutoRedirectPath
}
}
return opts, nil return opts, nil
} }
@ -287,12 +323,13 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
} }
return &accessController{ return &accessController{
realm: config.realm, realm: config.realm,
autoRedirect: config.autoRedirect, autoRedirect: config.autoRedirect,
issuer: config.issuer, autoRedirectPath: config.autoRedirectPath,
service: config.service, issuer: config.issuer,
rootCerts: rootPool, service: config.service,
trustedKeys: trustedKeys, rootCerts: rootPool,
trustedKeys: trustedKeys,
}, nil }, nil
} }
@ -300,10 +337,11 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
// for actions on resources described by the given access items. // for actions on resources described by the given access items.
func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Access) (*auth.Grant, error) { func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Access) (*auth.Grant, error) {
challenge := &authChallenge{ challenge := &authChallenge{
realm: ac.realm, realm: ac.realm,
autoRedirect: ac.autoRedirect, autoRedirect: ac.autoRedirect,
service: ac.service, autoRedirectPath: ac.autoRedirectPath,
accessSet: newAccessSet(accessItems...), service: ac.service,
accessSet: newAccessSet(accessItems...),
} }
prefix, rawToken, ok := strings.Cut(req.Header.Get("Authorization"), " ") prefix, rawToken, ok := strings.Cut(req.Header.Get("Authorization"), " ")

View file

@ -0,0 +1,89 @@
package token
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestBuildAutoRedirectURL(t *testing.T) {
cases := []struct {
name string
reqGetter func() *http.Request
autoRedirectPath string
expectedURL string
}{{
name: "http",
reqGetter: func() *http.Request {
req := httptest.NewRequest("GET", "http://example.com/", nil)
return req
},
autoRedirectPath: "/auth",
expectedURL: "https://example.com/auth",
}, {
name: "x-forwarded",
reqGetter: func() *http.Request {
req := httptest.NewRequest("GET", "http://example.com/", nil)
req.Header.Set("X-Forwarded-Proto", "http")
return req
},
autoRedirectPath: "/auth/token",
expectedURL: "http://example.com/auth/token",
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := tc.reqGetter()
result := buildAutoRedirectURL(req, tc.autoRedirectPath)
if result != tc.expectedURL {
t.Errorf("expected %s, got %s", tc.expectedURL, result)
}
})
}
}
func TestCheckOptions(t *testing.T) {
realm := "https://auth.example.com/token/"
issuer := "test-issuer.example.com"
service := "test-service.example.com"
options := map[string]interface{}{
"realm": realm,
"issuer": issuer,
"service": service,
"rootcertbundle": "",
"autoredirect": true,
"autoredirectpath": "/auth",
}
ta, err := checkOptions(options)
if err != nil {
t.Fatal(err)
}
if ta.autoRedirect != true {
t.Fatal("autoredirect should be true")
}
if ta.autoRedirectPath != "/auth" {
t.Fatal("autoredirectpath should be /auth")
}
options = map[string]interface{}{
"realm": realm,
"issuer": issuer,
"service": service,
"rootcertbundle": "",
"autoredirect": true,
"autoredirectforcetlsdisabled": true,
}
ta, err = checkOptions(options)
if err != nil {
t.Fatal(err)
}
if ta.autoRedirect != true {
t.Fatal("autoredirect should be true")
}
if ta.autoRedirectPath != "/auth/token" {
t.Fatal("autoredirectpath should be /auth/token")
}
}