mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-18 11:37:08 +02:00
Add configurable JWT claim headers (#596)
This commit is contained in:
parent
b08ecc624a
commit
789068e27a
6 changed files with 118 additions and 17 deletions
|
@ -135,6 +135,9 @@ type Options struct {
|
|||
HeadersEnv string `yaml:",omitempty"`
|
||||
Headers map[string]string `yaml:",omitempty"`
|
||||
|
||||
// List of JWT claims to insert as x-pomerium-claim-* headers on proxied requests
|
||||
JWTClaimsHeaders []string `mapstructure:"jwt_claims_headers" yaml:"jwt_claims_headers,omitempty"`
|
||||
|
||||
// RefreshCooldown limits the rate a user can refresh her session
|
||||
RefreshCooldown time.Duration `mapstructure:"refresh_cooldown" yaml:"refresh_cooldown,omitempty"`
|
||||
|
||||
|
|
|
@ -674,6 +674,17 @@ Refresh cooldown is the minimum amount of time between allowed manually refreshe
|
|||
|
||||
Default Upstream Timeout is the default timeout applied to a proxied route when no `timeout` key is specified by the policy.
|
||||
|
||||
### JWT Claim Headers
|
||||
- Environmental Variable: `JWT_CLAIMS_HEADERS`
|
||||
- Config File Key: `jwt_claims_headers`
|
||||
- Type: `string list`
|
||||
- Example: `email,groups`, `user`
|
||||
- Optional
|
||||
|
||||
Set this option for the pomerium proxy to copy JWT claim information into request headers with the name `x-pomerium-claim-*`. Any claim listed in the pomerium JWT can be placed into a corresponding header for downstream consumption. This claim information is sourced from your IDP and pomerium's own session metadata.
|
||||
|
||||
Use this option if you previously relied on `x-pomerium-authenticated-user-{email|user-id|groups}` for downstream authN/Z.
|
||||
|
||||
## Cache Service
|
||||
|
||||
The cache service is used for storing user session data.
|
||||
|
|
|
@ -13,6 +13,8 @@ description: >-
|
|||
|
||||
User detail headers ( `x-pomerium-authenticated-user-id` / `x-pomerium-authenticated-user-email` / `x-pomerium-authenticated-user-groups`) have been removed in favor of using the more secure, more data rich attestation jwt header (`x-pomerium-jwt-assertion`).
|
||||
|
||||
If you still rely on individual claim headers, please see the `jwt_claims_headers` option [here](https://www.pomerium.io/configuration/#jwt-claim-headers).
|
||||
|
||||
### Non-standard port users
|
||||
|
||||
Non-standard port users (e.g. those not using `443`/`80` where the port _would_ be part of the client's request) will have to clear their user's session before upgrading. Starting with version v0.7.0, audience (`aud`) and issuer (`iss`) claims will be port specific.
|
||||
|
|
|
@ -155,25 +155,50 @@ func SetResponseHeaders(headers map[string]string) func(next http.Handler) http.
|
|||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) userDetailsLoggerMiddleware(next http.Handler) http.Handler {
|
||||
func (p *Proxy) jwtClaimMiddleware(next http.Handler) http.Handler {
|
||||
return httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||
if jwt, err := sessions.FromContext(r.Context()); err == nil {
|
||||
var s sessions.State
|
||||
if err := p.encoder.Unmarshal([]byte(jwt), &s); err == nil {
|
||||
var jwtClaims map[string]interface{}
|
||||
if err := p.encoder.Unmarshal([]byte(jwt), &jwtClaims); err == nil {
|
||||
formattedJWTClaims := make(map[string]string)
|
||||
|
||||
// reformat claims into something resembling map[string]string
|
||||
for claim, value := range jwtClaims {
|
||||
var formattedClaim string
|
||||
if cv, ok := value.([]interface{}); ok {
|
||||
elements := make([]string, len(cv))
|
||||
|
||||
for i, v := range cv {
|
||||
elements[i] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
formattedClaim = strings.Join(elements, ",")
|
||||
} else {
|
||||
formattedClaim = fmt.Sprintf("%v", value)
|
||||
}
|
||||
formattedJWTClaims[claim] = formattedClaim
|
||||
}
|
||||
|
||||
// log group, email, user claims
|
||||
l := log.Ctx(r.Context())
|
||||
for _, claimName := range []string{"groups", "email", "user"} {
|
||||
|
||||
l.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Strs("groups", s.Groups)
|
||||
})
|
||||
l.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("email", s.Email)
|
||||
})
|
||||
l.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("user-id", s.User)
|
||||
return c.Str(claimName, fmt.Sprintf("%v", formattedJWTClaims[claimName]))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// set headers for any claims specified by config
|
||||
for _, claimName := range p.jwtClaimHeaders {
|
||||
if _, ok := formattedJWTClaims[claimName]; ok {
|
||||
|
||||
headerName := fmt.Sprintf("x-pomerium-claim-%s", claimName)
|
||||
r.Header.Set(headerName, formattedJWTClaims[claimName])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pomerium/pomerium/internal/encoding"
|
||||
"github.com/pomerium/pomerium/internal/encoding/jws"
|
||||
"github.com/pomerium/pomerium/internal/encoding/mock"
|
||||
"github.com/pomerium/pomerium/internal/grpc/authorize"
|
||||
"github.com/pomerium/pomerium/internal/grpc/authorize/client"
|
||||
|
@ -72,15 +73,72 @@ func TestProxy_AuthenticateSession(t *testing.T) {
|
|||
r = r.WithContext(ctx)
|
||||
r.Header.Set("Accept", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
got := a.userDetailsLoggerMiddleware(a.AuthenticateSession(fn))
|
||||
got := a.jwtClaimMiddleware(a.AuthenticateSession(fn))
|
||||
got.ServeHTTP(w, r)
|
||||
if status := w.Code; status != tt.wantStatus {
|
||||
t.Errorf("AuthenticateSession() error = %v, wantErr %v\n%v", w.Result().StatusCode, tt.wantStatus, w.Body.String())
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_jwtClaimMiddleware(t *testing.T) {
|
||||
email := "test@pomerium.example"
|
||||
groups := []string{"foo", "bar"}
|
||||
claimHeaders := []string{"email", "groups", "missing"}
|
||||
sharedKey := "80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="
|
||||
|
||||
session := &sessions.State{Email: email, Expiry: jwt.NewNumericDate(time.Now().Add(10 * time.Second)), Groups: groups}
|
||||
encoder, _ := jws.NewHS256Signer([]byte(sharedKey), "https://authenticate.pomerium.example")
|
||||
state, err := encoder.Marshal(session)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("failed to marshal state: %s", err)
|
||||
}
|
||||
|
||||
a := Proxy{
|
||||
SharedKey: sharedKey,
|
||||
cookieSecret: []byte("80ldlrU2d7w+wVpKNfevk6fmb8otEx6CqOfshj2LwhQ="),
|
||||
encoder: encoder,
|
||||
jwtClaimHeaders: claimHeaders,
|
||||
}
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
ctx := r.Context()
|
||||
ctx = sessions.NewContext(ctx, string(state), nil)
|
||||
r = r.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
proxyHandler := a.jwtClaimMiddleware(handler)
|
||||
proxyHandler.ServeHTTP(w, r)
|
||||
|
||||
t.Run("email claim", func(t *testing.T) {
|
||||
emailHeader := r.Header.Get("x-pomerium-claim-email")
|
||||
if emailHeader != email {
|
||||
t.Errorf("did not find claim email, want=%q, got=%q", email, emailHeader)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("groups claim", func(t *testing.T) {
|
||||
groupsHeader := r.Header.Get("x-pomerium-claim-groups")
|
||||
if groupsHeader != strings.Join(groups, ",") {
|
||||
t.Errorf("did not find claim groups, want=%q, got=%q", groups, groupsHeader)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing claim", func(t *testing.T) {
|
||||
absentHeader := r.Header.Get("x-pomerium-claim-missing")
|
||||
if absentHeader != "" {
|
||||
t.Errorf("found claim that should not exist, got=%q", absentHeader)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestProxy_AuthorizeSession(t *testing.T) {
|
||||
t.Parallel()
|
||||
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -90,6 +90,7 @@ type Proxy struct {
|
|||
sessionStore sessions.SessionStore
|
||||
sessionLoaders []sessions.SessionLoader
|
||||
templates *template.Template
|
||||
jwtClaimHeaders []string
|
||||
}
|
||||
|
||||
// New takes a Proxy service from options and a validation function.
|
||||
|
@ -136,6 +137,7 @@ func New(opts config.Options) (*Proxy, error) {
|
|||
header.NewStore(encoder, "Pomerium"),
|
||||
queryparam.NewStore(encoder, "pomerium_session")},
|
||||
templates: template.Must(frontend.NewTemplates()),
|
||||
jwtClaimHeaders: opts.JWTClaimsHeaders,
|
||||
}
|
||||
// errors checked in ValidateOptions
|
||||
p.authorizeURL, _ = urlutil.DeepCopy(opts.AuthorizeURL)
|
||||
|
@ -269,8 +271,8 @@ func (p *Proxy) reverseProxyHandler(r *mux.Router, policy config.Policy) *mux.Ro
|
|||
rp.Use(p.AuthorizeSession)
|
||||
// 7. Strip the user session cookie from the downstream request
|
||||
rp.Use(middleware.StripCookie(p.cookieOptions.Name))
|
||||
// 8 . Add user details to the request logger context
|
||||
rp.Use(p.userDetailsLoggerMiddleware)
|
||||
// 8 . Add claim details to the request logger context and headers
|
||||
rp.Use(p.jwtClaimMiddleware)
|
||||
|
||||
return r
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue