This commit is contained in:
Caleb Doxsey 2025-02-14 09:54:07 -07:00
parent f19dd8b371
commit b138b2ea12
6 changed files with 1104 additions and 773 deletions

View file

@ -0,0 +1,91 @@
package config
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/mitchellh/mapstructure"
configpb "github.com/pomerium/pomerium/pkg/grpc/config"
)
// BearerTokenFormat specifies how bearer tokens are interepreted by Pomerium.
type BearerTokenFormat string
// Bearer Token Formats
const (
BearerTokenFormatDefault BearerTokenFormat = "default"
BearerTokenFormatIDPAccessToken BearerTokenFormat = "idp_access_token"
BearerTokenFormatIDPIdentityToken BearerTokenFormat = "idp_identity_token"
)
// ParseBearerTokenFormat parses the BearerTokenFormat.
func ParseBearerTokenFormat(raw string) (BearerTokenFormat, error) {
switch BearerTokenFormat(strings.TrimSpace(strings.ToLower(raw))) {
case BearerTokenFormatDefault:
return BearerTokenFormatDefault, nil
case BearerTokenFormatIDPAccessToken:
return BearerTokenFormatIDPAccessToken, nil
case BearerTokenFormatIDPIdentityToken:
return BearerTokenFormatIDPIdentityToken, nil
}
return BearerTokenFormatDefault, fmt.Errorf("invalid bearer token format: %s", raw)
}
func BearerTokenFormatFromPB(pbBearerTokenFormat *configpb.BearerTokenFormat) *BearerTokenFormat {
if pbBearerTokenFormat == nil {
return nil
}
bearerTokenFormat := new(BearerTokenFormat)
*bearerTokenFormat = BearerTokenFormatDefault
switch *pbBearerTokenFormat {
case configpb.BearerTokenFormat_BEARER_TOKEN_FORMAT_DEFAULT:
*bearerTokenFormat = BearerTokenFormatDefault
case configpb.BearerTokenFormat_BEARER_TOKEN_FORMAT_IDP_ACCESS_TOKEN:
*bearerTokenFormat = BearerTokenFormatIDPAccessToken
case configpb.BearerTokenFormat_BEARER_TOKEN_FORMAT_IDP_IDENTITY_TOKEN:
*bearerTokenFormat = BearerTokenFormatIDPIdentityToken
}
return bearerTokenFormat
}
// ToEnvoy converts the bearer token format into a protobuf enum.
func (bearerTokenFormat *BearerTokenFormat) ToPB() *configpb.BearerTokenFormat {
if bearerTokenFormat == nil {
return nil
}
switch *bearerTokenFormat {
case BearerTokenFormatDefault:
return configpb.BearerTokenFormat_BEARER_TOKEN_FORMAT_DEFAULT.Enum()
case BearerTokenFormatIDPAccessToken:
return configpb.BearerTokenFormat_BEARER_TOKEN_FORMAT_IDP_ACCESS_TOKEN.Enum()
case BearerTokenFormatIDPIdentityToken:
return configpb.BearerTokenFormat_BEARER_TOKEN_FORMAT_IDP_IDENTITY_TOKEN.Enum()
default:
panic(fmt.Sprintf("unknown bearer token format: %s", bearerTokenFormat))
}
}
func decodeBearerTokenFormatHookFunc() mapstructure.DecodeHookFunc {
return func(_, t reflect.Type, data any) (any, error) {
if t != reflect.TypeOf(BearerTokenFormat("")) {
return data, nil
}
bs, err := json.Marshal(data)
if err != nil {
return nil, err
}
var raw string
err = json.Unmarshal(bs, &raw)
if err != nil {
return nil, err
}
return ParseBearerTokenFormat(raw)
}
}

View file

@ -196,6 +196,13 @@ type Options struct {
// List of JWT claims to insert as x-pomerium-claim-* headers on proxied requests
JWTClaimsHeaders JWTClaimHeaders `mapstructure:"jwt_claims_headers" yaml:"jwt_claims_headers,omitempty"`
// BearerTokenFormat indicates how authorization bearer tokens are interepreted. Possible values:
// - "default": Only Bearer tokens prefixed with Pomerium- will be interpreted by Pomerium.
// - "idp_access_token": The Bearer token will be interpreted as an IdP access token.
// - "idp_identity_token": The Bearer token will be interpreted as an IdP identity token.
// When unset "default" will be used.
BearerTokenFormat *BearerTokenFormat `mapstructure:"bearer_token_format" yaml:"bearer_token_format,omitempty"`
// Allowlist of group names/IDs to include in the Pomerium JWT.
JWTGroupsFilter JWTGroupsFilter
@ -1503,6 +1510,7 @@ func (o *Options) ApplySettings(ctx context.Context, certsIndex *cryptutil.Certi
set(&o.SigningKey, settings.SigningKey)
setMap(&o.SetResponseHeaders, settings.SetResponseHeaders)
setMap(&o.JWTClaimsHeaders, settings.JwtClaimsHeaders)
o.BearerTokenFormat = BearerTokenFormatFromPB(settings.BearerTokenFormat)
if len(settings.JwtGroupsFilter) > 0 {
o.JWTGroupsFilter = NewJWTGroupsFilter(settings.JwtGroupsFilter)
}
@ -1614,6 +1622,7 @@ func (o *Options) ToProto() *config.Config {
copySrcToOptionalDest(&settings.SigningKey, valueOrFromFileBase64(o.SigningKey, o.SigningKeyFile))
settings.SetResponseHeaders = o.SetResponseHeaders
settings.JwtClaimsHeaders = o.JWTClaimsHeaders
settings.BearerTokenFormat = o.BearerTokenFormat.ToPB()
settings.JwtGroupsFilter = o.JWTGroupsFilter.ToSlice()
copyOptionalDuration(&settings.DefaultUpstreamTimeout, o.DefaultUpstreamTimeout)
copySrcToOptionalDest(&settings.MetricsAddress, &o.MetricsAddr)

View file

@ -168,6 +168,12 @@ type Policy struct {
// - "hostOnly" (default): Issuer strings will be the hostname of the route, with no scheme or trailing slash.
// - "uri": Issuer strings will be a complete URI, including the scheme and ending with a trailing slash.
JWTIssuerFormat string `mapstructure:"jwt_issuer_format" yaml:"jwt_issuer_format,omitempty"`
// BearerTokenFormat indicates how authorization bearer tokens are interepreted. Possible values:
// - "default": Only Bearer tokens prefixed with Pomerium- will be interpreted by Pomerium
// - "idp_access_token": The Bearer token will be interpreted as an IdP access token.
// - "idp_identity_token": The Bearer token will be interpreted as an IdP identity token.
// When unset the global option will be used.
BearerTokenFormat *BearerTokenFormat `mapstructure:"bearer_token_format" yaml:"bearer_token_format,omitempty"`
// Allowlist of group names/IDs to include in the Pomerium JWT.
// This expands on any global allowlist set in the main Options.

View file

@ -1,8 +1,10 @@
package config
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/encoding/jws"
@ -11,6 +13,7 @@ import (
"github.com/pomerium/pomerium/internal/sessions/header"
"github.com/pomerium/pomerium/internal/sessions/queryparam"
"github.com/pomerium/pomerium/internal/urlutil"
"github.com/pomerium/pomerium/pkg/grpc/session"
)
// A SessionStore saves and loads sessions based on the options.
@ -112,3 +115,120 @@ func (store *SessionStore) LoadSessionStateAndCheckIDP(r *http.Request) (*sessio
func (store *SessionStore) SaveSession(w http.ResponseWriter, r *http.Request, v any) error {
return store.store.SaveSession(w, r, v)
}
// An IDPTokenSessionHandler handles incoming idp access and identity tokens.
type IDPTokenSessionHandler struct {
options *Options
getSession func(ctx context.Context, id string) (*session.Session, error)
putSession func(ctx context.Context, s *session.Session) error
}
// NewIDPTokenSessionHandler creates a new IDPTokenSessionHandler.
func NewIDPTokenSessionHandler(
options *Options,
getSession func(ctx context.Context, id string) (*session.Session, error),
putSession func(ctx context.Context, s *session.Session) error,
) *IDPTokenSessionHandler {
return &IDPTokenSessionHandler{
options: options,
getSession: getSession,
putSession: putSession,
}
}
// // CreateSessionForIncomingIDPToken creates a session from an incoming idp access or identity token.
// // If no such tokens are found or they are invalid ErrNoSessionFound will be returned.
// func (h *IDPTokenSessionHandler) CreateSessionForIncomingIDPToken(r *http.Request) (*session.Session, error) {
// idp, err := h.options.GetIdentityProviderForRequestURL(urlutil.GetAbsoluteURL(r).String())
// if err != nil {
// return nil, err
// }
// return nil, sessions.ErrNoSessionFound
// }
// func (h *IDPTokenSessionHandler) getIncomingIDPAccessToken(r *http.Request) (rawAccessToken string, ok bool) {
// if h.options.
// return "", false
// }
// func (h *IDPTokenSessionHandler) getIncomingIDPIdentityToken(r *http.Request) (rawIdentityToken string, ok bool) {
// return "", false
// }
// func CreateSessionForIncomingIDPToken(
// r *http.Request,
// options *Options,
// policy *Policy,
// getSession func(ctx context.Context, id string) (*session.Session, error),
// putSession func(ctx context.Context, s *session.Session) error)(*session.Session, error) {
// }
// GetIncomingIDPAccessTokenForPolicy returns the raw idp access token from a request if there is one.
func (options *Options) GetIncomingIDPAccessTokenForPolicy(policy *Policy, r *http.Request) (rawAccessToken string, ok bool) {
bearerTokenFormat := BearerTokenFormatDefault
if options != nil && options.BearerTokenFormat != nil {
bearerTokenFormat = *options.BearerTokenFormat
}
if policy != nil && policy.BearerTokenFormat != nil {
bearerTokenFormat = *policy.BearerTokenFormat
}
if token := r.Header.Get("X-Pomerium-IDP-Access-Token"); token != "" {
return token, true
}
if auth := r.Header.Get("Authorization"); auth != "" {
prefix := "Pomerium-IDP-Access-Token "
if strings.HasPrefix(strings.ToLower(auth), strings.ToLower(prefix)) {
return strings.TrimPrefix(auth, prefix), true
}
prefix = "Bearer Pomerium-IDP-Access-Token-"
if strings.HasPrefix(strings.ToLower(auth), strings.ToLower(prefix)) {
return strings.TrimPrefix(auth, prefix), true
}
prefix = "Bearer "
if strings.HasPrefix(strings.ToLower(auth), strings.ToLower(prefix)) && bearerTokenFormat == BearerTokenFormatIDPAccessToken {
return strings.TrimPrefix(auth, prefix), true
}
}
return "", false
}
// GetIncomingIDPAccessTokenForPolicy returns the raw idp identity token from a request if there is one.
func (options *Options) GetIncomingIDPIdentityTokenForPolicy(policy *Policy, r *http.Request) (rawIdentityToken string, ok bool) {
bearerTokenFormat := BearerTokenFormatDefault
if options != nil && options.BearerTokenFormat != nil {
bearerTokenFormat = *options.BearerTokenFormat
}
if policy != nil && policy.BearerTokenFormat != nil {
bearerTokenFormat = *policy.BearerTokenFormat
}
if token := r.Header.Get("X-Pomerium-IDP-Identity-Token"); token != "" {
return token, true
}
if auth := r.Header.Get("Authorization"); auth != "" {
prefix := "Pomerium-IDP-Identity-Token "
if strings.HasPrefix(strings.ToLower(auth), strings.ToLower(prefix)) {
return strings.TrimPrefix(auth, prefix), true
}
prefix = "Bearer Pomerium-IDP-Identity-Token-"
if strings.HasPrefix(strings.ToLower(auth), strings.ToLower(prefix)) {
return strings.TrimPrefix(auth, prefix), true
}
prefix = "Bearer "
if strings.HasPrefix(strings.ToLower(auth), strings.ToLower(prefix)) && bearerTokenFormat == BearerTokenFormatIDPIdentityToken {
return strings.TrimPrefix(auth, prefix), true
}
}
return "", false
}

File diff suppressed because it is too large Load diff

View file

@ -45,7 +45,14 @@ enum IssuerFormat {
IssuerURI = 1;
}
// Next ID: 70.
enum BearerTokenFormat {
BEARER_TOKEN_FORMAT_UNKNOWN = 0;
BEARER_TOKEN_FORMAT_DEFAULT = 1;
BEARER_TOKEN_FORMAT_IDP_ACCESS_TOKEN = 2;
BEARER_TOKEN_FORMAT_IDP_IDENTITY_TOKEN = 3;
}
// Next ID: 71.
message Route {
message StringList { repeated string values = 1; }
@ -118,6 +125,7 @@ message Route {
bool enable_google_cloud_serverless_authentication = 42;
IssuerFormat jwt_issuer_format = 65;
repeated string jwt_groups_filter = 66;
optional BearerTokenFormat bearer_token_format = 70;
envoy.config.cluster.v3.Cluster envoy_opts = 36;
@ -152,7 +160,7 @@ message Policy {
string remediation = 9;
}
// Next ID: 138.
// Next ID: 139.
message Settings {
message Certificate {
bytes cert_bytes = 3;
@ -207,6 +215,7 @@ message Settings {
// repeated string jwt_claims_headers = 37;
map<string, string> jwt_claims_headers = 63;
repeated string jwt_groups_filter = 119;
optional BearerTokenFormat bearer_token_format = 138;
optional google.protobuf.Duration default_upstream_timeout = 39;
optional string metrics_address = 40;
optional string metrics_basic_auth = 64;