88 "fmt"
99 "log/slog"
1010 "net/http"
11+ "net/url"
1112 "path/filepath"
1213 "slices"
1314 "strings"
3839 "/kas.AccessService/PublicKey" ,
3940 "/healthz" ,
4041 "/.well-known/opentdf-configuration" ,
42+ "/kas/kas_public_key" ,
4143 "/kas/v2/kas_public_key" ,
4244 }
4345 // only asymmetric algorithms and no 'none'
@@ -67,6 +69,8 @@ type Authentication struct {
6769 enforcer * Enforcer
6870 // Public Routes HTTP & gRPC
6971 publicRoutes []string
72+ // Allowed origins for the DPoP htu field
73+ allowedHosts []* url.URL
7074}
7175
7276// Creates new authN which is used to verify tokens for a set of given issuers
@@ -126,13 +130,33 @@ func NewAuthenticator(ctx context.Context, cfg Config, d *db.Client) (*Authentic
126130
127131 a .oidcConfigurations [cfg .Issuer ] = cfg .AuthNConfig
128132
133+ a .allowedHosts = make ([]* url.URL , len (cfg .AllowedHosts ))
134+ for i , h := range cfg .AllowedHosts {
135+ a .allowedHosts [i ], err = url .Parse (h )
136+ if err != nil {
137+ return nil , err
138+ }
139+ }
140+
129141 return a , nil
130142}
131143
132144type dpopInfo struct {
133- headers []string
134- path string
135- method string
145+ u []string
146+ m string
147+ }
148+
149+ func (a Authentication ) normalizeURL (u * url.URL ) []string {
150+ n := * u
151+ n .RawQuery = ""
152+ n .Fragment = ""
153+ us := make ([]string , len (a .allowedHosts ))
154+ for i , v := range a .allowedHosts {
155+ v2 := * v
156+ v2 .Path = u .Path
157+ us [i ] = v2 .String ()
158+ }
159+ return us
136160}
137161
138162// verifyTokenHandler is a http handler that verifies the token
@@ -150,10 +174,9 @@ func (a Authentication) MuxHandler(handler http.Handler) http.Handler {
150174 return
151175 }
152176 tok , newCtx , err := a .checkToken (r .Context (), header , dpopInfo {
153- headers : r .Header ["Dpop" ],
154- path : r .URL .Path ,
155- method : r .Method ,
156- })
177+ u : a .normalizeURL (r .URL ),
178+ m : r .Method ,
179+ }, r .Header ["Dpop" ])
157180
158181 if err != nil {
159182 slog .WarnContext (r .Context (), "failed to validate token" , slog .String ("error" , err .Error ()))
@@ -232,10 +255,10 @@ func (a Authentication) UnaryServerInterceptor(ctx context.Context, req any, inf
232255 ctx ,
233256 header ,
234257 dpopInfo {
235- headers : md ["dpop" ],
236- path : info .FullMethod ,
237- method : http .MethodPost ,
258+ u : []string {info .FullMethod },
259+ m : http .MethodPost ,
238260 },
261+ md ["dpop" ],
239262 )
240263 if err != nil {
241264 slog .Warn ("failed to validate token" , slog .String ("error" , err .Error ()))
@@ -258,7 +281,7 @@ func (a Authentication) UnaryServerInterceptor(ctx context.Context, req any, inf
258281}
259282
260283// checkToken is a helper function to verify the token.
261- func (a Authentication ) checkToken (ctx context.Context , authHeader []string , dpopInfo dpopInfo ) (jwt.Token , context.Context , error ) {
284+ func (a Authentication ) checkToken (ctx context.Context , authHeader []string , dpopInfo dpopInfo , dpopHeader [] string ) (jwt.Token , context.Context , error ) {
262285 var (
263286 tokenRaw string
264287 )
@@ -317,7 +340,7 @@ func (a Authentication) checkToken(ctx context.Context, authHeader []string, dpo
317340 // come from token introspection
318341 return accessToken , ctx , nil
319342 }
320- key , err := validateDPoP (accessToken , tokenRaw , dpopInfo )
343+ key , err := validateDPoP (accessToken , tokenRaw , dpopInfo , dpopHeader )
321344 if err != nil {
322345 return nil , nil , err
323346 }
@@ -340,11 +363,11 @@ func GetJWKFromContext(ctx context.Context) jwk.Key {
340363 panic ("got something that is not a jwk.Key from the JWK context" )
341364}
342365
343- func validateDPoP (accessToken jwt.Token , acessTokenRaw string , dpopInfo dpopInfo ) (jwk.Key , error ) {
344- if len (dpopInfo . headers ) != 1 {
345- return nil , fmt .Errorf ("got %d dpop headers, should have 1" , len (dpopInfo . headers ))
366+ func validateDPoP (accessToken jwt.Token , acessTokenRaw string , dpopInfo dpopInfo , headers [] string ) (jwk.Key , error ) {
367+ if len (headers ) != 1 {
368+ return nil , fmt .Errorf ("got %d dpop headers, should have 1" , len (headers ))
346369 }
347- dpopHeader := dpopInfo . headers [0 ]
370+ dpopHeader := headers [0 ]
348371
349372 cnf , ok := accessToken .Get ("cnf" )
350373 if ! ok {
@@ -405,8 +428,9 @@ func validateDPoP(accessToken jwt.Token, acessTokenRaw string, dpopInfo dpopInfo
405428 return nil , fmt .Errorf ("couldn't compute thumbprint for key in `jwk` in DPoP JWT" )
406429 }
407430
408- if base64 .URLEncoding .WithPadding (base64 .NoPadding ).EncodeToString (thumbprint ) != jkt {
409- return nil , fmt .Errorf ("the `jkt` from the DPoP JWT didn't match the thumbprint from the access token" )
431+ thumbprintStr := base64 .URLEncoding .WithPadding (base64 .NoPadding ).EncodeToString (thumbprint )
432+ if thumbprintStr != jkt {
433+ return nil , fmt .Errorf ("the `jkt` from the DPoP JWT didn't match the thumbprint from the access token; cnf.jkt=[%v], computed=[%v]" , jkt , thumbprintStr )
410434 }
411435
412436 // at this point we have the right key because its thumbprint matches the `jkt` claim
@@ -432,17 +456,17 @@ func validateDPoP(accessToken jwt.Token, acessTokenRaw string, dpopInfo dpopInfo
432456 return nil , fmt .Errorf ("`htm` claim missing in DPoP JWT" )
433457 }
434458
435- if htm != dpopInfo .method {
436- return nil , fmt .Errorf ("incorrect `htm` claim in DPoP JWT" )
459+ if htm != dpopInfo .m {
460+ return nil , fmt .Errorf ("incorrect `htm` claim in DPoP JWT; should match [%v]" , dpopInfo . m )
437461 }
438462
439463 htu , ok := dpopToken .Get ("htu" )
440464 if ! ok {
441465 return nil , fmt .Errorf ("`htu` claim missing in DPoP JWT" )
442466 }
443467
444- if htu != dpopInfo .path {
445- return nil , fmt .Errorf ("incorrect `htu` claim in DPoP JWT" )
468+ if ! slices . Contains ( dpopInfo .u , htu .( string )) {
469+ return nil , fmt .Errorf ("incorrect `htu` claim in DPoP JWT; should match %v" , dpopInfo . u )
446470 }
447471
448472 ath , ok := dpopToken .Get ("ath" )
0 commit comments