mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-02 00:10:45 +02:00
add global jwt_issuer_format option (#5508)
Add a corresponding global setting for the existing route-level jwt_issuer_format option. The route-level option will take precedence when set to a non-empty string.
This commit is contained in:
parent
b86c9931b1
commit
ad183873f4
11 changed files with 902 additions and 781 deletions
|
@ -149,6 +149,7 @@ func newPolicyEvaluator(
|
|||
evaluator.WithGoogleCloudServerlessAuthenticationServiceAccount(opts.GetGoogleCloudServerlessAuthenticationServiceAccount()),
|
||||
evaluator.WithJWTClaimsHeaders(opts.JWTClaimsHeaders),
|
||||
evaluator.WithJWTGroupsFilter(opts.JWTGroupsFilter),
|
||||
evaluator.WithDefaultJWTIssuerFormat(opts.JWTIssuerFormat),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ type evaluatorConfig struct {
|
|||
GoogleCloudServerlessAuthenticationServiceAccount string
|
||||
JWTClaimsHeaders config.JWTClaimHeaders
|
||||
JWTGroupsFilter config.JWTGroupsFilter
|
||||
DefaultJWTIssuerFormat config.JWTIssuerFormat
|
||||
}
|
||||
|
||||
// cacheKey() returns a hash over the configuration, except for the policies.
|
||||
|
@ -105,3 +106,10 @@ func WithJWTGroupsFilter(groups config.JWTGroupsFilter) Option {
|
|||
cfg.JWTGroupsFilter = groups
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultJWTIssuerFormat sets the default JWT issuer format in the config.
|
||||
func WithDefaultJWTIssuerFormat(format config.JWTIssuerFormat) Option {
|
||||
return func(cfg *evaluatorConfig) {
|
||||
cfg.DefaultJWTIssuerFormat = format
|
||||
}
|
||||
}
|
||||
|
|
|
@ -332,6 +332,7 @@ func updateStore(ctx context.Context, store *store.Store, cfg *evaluatorConfig)
|
|||
)
|
||||
store.UpdateJWTClaimHeaders(cfg.JWTClaimsHeaders)
|
||||
store.UpdateJWTGroupsFilter(cfg.JWTGroupsFilter)
|
||||
store.UpdateDefaultJWTIssuerFormat(cfg.DefaultJWTIssuerFormat)
|
||||
store.UpdateRoutePolicies(cfg.Policies)
|
||||
store.UpdateSigningKey(jwk)
|
||||
|
||||
|
|
|
@ -247,18 +247,16 @@ func (e *headersEvaluatorEvaluation) getGroupIDs(ctx context.Context) []string {
|
|||
return make([]string, 0)
|
||||
}
|
||||
|
||||
func (e *headersEvaluatorEvaluation) getJWTPayloadIss() (string, error) {
|
||||
var issuerFormat string
|
||||
if e.request.Policy != nil {
|
||||
func (e *headersEvaluatorEvaluation) getJWTPayloadIss() string {
|
||||
issuerFormat := e.evaluator.store.GetDefaultJWTIssuerFormat()
|
||||
if e.request.Policy != nil && e.request.Policy.JWTIssuerFormat != "" {
|
||||
issuerFormat = e.request.Policy.JWTIssuerFormat
|
||||
}
|
||||
switch issuerFormat {
|
||||
case "uri":
|
||||
return fmt.Sprintf("https://%s/", e.request.HTTP.Hostname), nil
|
||||
case "", "hostOnly":
|
||||
return e.request.HTTP.Hostname, nil
|
||||
case config.JWTIssuerFormatURI:
|
||||
return fmt.Sprintf("https://%s/", e.request.HTTP.Hostname)
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported JWT issuer format: %s", issuerFormat)
|
||||
return e.request.HTTP.Hostname
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -412,14 +410,9 @@ func (e *headersEvaluatorEvaluation) getJWTPayload(ctx context.Context) (map[str
|
|||
return e.cachedJWTPayload, nil
|
||||
}
|
||||
|
||||
iss, err := e.getJWTPayloadIss()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.gotJWTPayload = true
|
||||
e.cachedJWTPayload = map[string]any{
|
||||
"iss": iss,
|
||||
"iss": e.getJWTPayloadIss(),
|
||||
"aud": e.getJWTPayloadAud(),
|
||||
"jti": e.getJWTPayloadJTI(),
|
||||
"iat": e.getJWTPayloadIAT(),
|
||||
|
|
|
@ -437,34 +437,59 @@ func TestHeadersEvaluator(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
assert.Equal(t, "u1@example.com", output.Headers.Get("X-Pomerium-Claim-Email"))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("issuer format", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
func TestHeadersEvaluator_JWTIssuerFormat(t *testing.T) {
|
||||
privateJWK, _ := newJWK(t)
|
||||
|
||||
for _, tc := range []struct {
|
||||
format string
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{"", "example.com", "example.com"},
|
||||
{"hostOnly", "host-only.example.com", "host-only.example.com"},
|
||||
{"uri", "uri.example.com", "https://uri.example.com/"},
|
||||
} {
|
||||
store := store.New()
|
||||
store.UpdateSigningKey(privateJWK)
|
||||
|
||||
eval := func(_ *testing.T, input *Request) (*HeadersResponse, error) {
|
||||
ctx := context.Background()
|
||||
e := NewHeadersEvaluator(store)
|
||||
return e.Evaluate(ctx, input)
|
||||
}
|
||||
|
||||
hostname := "route.example.com"
|
||||
|
||||
cases := []struct {
|
||||
globalFormat config.JWTIssuerFormat
|
||||
routeFormat config.JWTIssuerFormat
|
||||
expected string
|
||||
}{
|
||||
{"", "", "route.example.com"},
|
||||
{"hostOnly", "", "route.example.com"},
|
||||
{"uri", "", "https://route.example.com/"},
|
||||
|
||||
{"", "hostOnly", "route.example.com"},
|
||||
{"hostOnly", "hostOnly", "route.example.com"},
|
||||
{"uri", "hostOnly", "route.example.com"},
|
||||
|
||||
{"", "uri", "https://route.example.com/"},
|
||||
{"hostOnly", "uri", "https://route.example.com/"},
|
||||
{"uri", "uri", "https://route.example.com/"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run("", func(t *testing.T) {
|
||||
store.UpdateDefaultJWTIssuerFormat(tc.globalFormat)
|
||||
output, err := eval(t,
|
||||
nil,
|
||||
&Request{
|
||||
HTTP: RequestHTTP{
|
||||
Hostname: tc.input,
|
||||
Hostname: hostname,
|
||||
},
|
||||
Policy: &config.Policy{
|
||||
JWTIssuerFormat: tc.format,
|
||||
JWTIssuerFormat: tc.routeFormat,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
m := decodeJWTAssertion(t, output.Headers)
|
||||
assert.Equal(t, tc.output, m["iss"], "unexpected issuer for format=%s", tc.format)
|
||||
}
|
||||
})
|
||||
assert.Equal(t, tc.expected, m["iss"],
|
||||
"unexpected issuer for global format=%q, route format=%q",
|
||||
tc.globalFormat, tc.routeFormat)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadersEvaluator_JWTGroupsFilter(t *testing.T) {
|
||||
|
|
|
@ -33,6 +33,7 @@ type Store struct {
|
|||
googleCloudServerlessAuthenticationServiceAccount atomic.Pointer[string]
|
||||
jwtClaimHeaders atomic.Pointer[map[string]string]
|
||||
jwtGroupsFilter atomic.Pointer[config.JWTGroupsFilter]
|
||||
defaultJWTIssuerFormat atomic.Pointer[config.JWTIssuerFormat]
|
||||
signingKey atomic.Pointer[jose.JSONWebKey]
|
||||
}
|
||||
|
||||
|
@ -66,6 +67,13 @@ func (s *Store) GetJWTGroupsFilter() config.JWTGroupsFilter {
|
|||
return config.JWTGroupsFilter{}
|
||||
}
|
||||
|
||||
func (s *Store) GetDefaultJWTIssuerFormat() config.JWTIssuerFormat {
|
||||
if f := s.defaultJWTIssuerFormat.Load(); f != nil {
|
||||
return *f
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Store) GetSigningKey() *jose.JSONWebKey {
|
||||
return s.signingKey.Load()
|
||||
}
|
||||
|
@ -89,6 +97,12 @@ func (s *Store) UpdateJWTGroupsFilter(groups config.JWTGroupsFilter) {
|
|||
s.jwtGroupsFilter.Store(&groups)
|
||||
}
|
||||
|
||||
// UpdateDefaultJWTIssuerFormat updates the JWT groups filter in the store.
|
||||
func (s *Store) UpdateDefaultJWTIssuerFormat(format config.JWTIssuerFormat) {
|
||||
// This isn't used by the Rego code, so we don't need to write it to the opastorage.Store instance.
|
||||
s.defaultJWTIssuerFormat.Store(&format)
|
||||
}
|
||||
|
||||
// UpdateRoutePolicies updates the route policies in the store.
|
||||
func (s *Store) UpdateRoutePolicies(routePolicies []*config.Policy) {
|
||||
s.write("/route_policies", routePolicies)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue