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:
Kenneth Jenkins 2025-03-11 14:11:50 -07:00 committed by GitHub
parent b86c9931b1
commit ad183873f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 902 additions and 781 deletions

View file

@ -149,6 +149,7 @@ func newPolicyEvaluator(
evaluator.WithGoogleCloudServerlessAuthenticationServiceAccount(opts.GetGoogleCloudServerlessAuthenticationServiceAccount()),
evaluator.WithJWTClaimsHeaders(opts.JWTClaimsHeaders),
evaluator.WithJWTGroupsFilter(opts.JWTGroupsFilter),
evaluator.WithDefaultJWTIssuerFormat(opts.JWTIssuerFormat),
)
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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(),

View file

@ -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) {

View file

@ -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)