Repository: avenga/couper
Pull request #616 (2022-11-04 11:33:27)

JWT signing allow setting typ header
allow setting `typ` header when signing JWT.

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 43563a9e..f952f088 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
   * Aligned the evaluation of [`beta_oauth2`](https://docs.couper.io/configuration/block/oauth2_ac)/[`oidc`](https://docs.couper.io/configuration/block/oidc) `redirect_uri` to `saml` `sp_acs_url` ([#589](https://github.com/avenga/couper/pull/589))
   * Proper handling of empty [`beta_oauth2`](https://docs.couper.io/configuration/block/oauth2_ac)/[`oidc`](https://docs.couper.io/configuration/block/oidc) `scope` ([#593](https://github.com/avenga/couper/pull/593))
   * Throwing [sequence errors](https://docs.couper.io/configuration/error-handling#endpoint-error-types) and selecting appropriate [error handlers](https://docs.couper.io/configuration/error-handling) ([#595](https://github.com/avenga/couper/pull/595))
+  * Allow setting of the `typ` JWT header in [`jwt_signing_profile`s](https://docs.couper.io/configuration/block/jwt_signing_profile) ([#616](https://github.com/avenga/couper/pull/616))
 
 ---
 
diff --git a/config/configload/load.go b/config/configload/load.go
index 180ffb28..0afe9d82 100644
--- a/config/configload/load.go
+++ b/config/configload/load.go
@@ -331,15 +331,8 @@ func LoadConfig(body *hclsyntax.Body, src [][]byte, environment string) (*config
 			expression, _ := profile.Headers.Value(nil)
 			headers := seetie.ValueToMap(expression)
 
-			var errorMessage string
 			if _, exists := headers["alg"]; exists {
-				errorMessage = `"alg" cannot be set via "headers"`
-			} else if _, exists = headers["typ"]; exists {
-				errorMessage = `"typ" cannot be set via "headers"`
-			}
-
-			if errorMessage != "" {
-				return nil, errors.Configuration.Label(profile.Name).With(fmt.Errorf(errorMessage))
+				return nil, errors.Configuration.Label(profile.Name).With(fmt.Errorf(`"alg" cannot be set via "headers"`))
 			}
 		}
 
diff --git a/config/jwt_signing_profile.go b/config/jwt_signing_profile.go
index ec3cb857..c373fed7 100644
--- a/config/jwt_signing_profile.go
+++ b/config/jwt_signing_profile.go
@@ -4,7 +4,7 @@ import "github.com/hashicorp/hcl/v2"
 
 type JWTSigningProfile struct {
 	Claims             Claims         `hcl:"claims,optional" docs:"claims for the JWT payload, claim values are evaluated per request"`
-	Headers            hcl.Expression `hcl:"headers,optional" docs:"additional header fields for the JWT, {alg} and {typ} cannot be set"`
+	Headers            hcl.Expression `hcl:"headers,optional" docs:"additional header fields for the JWT, {typ} has the default value {JWT}, {alg} cannot be set"`
 	Key                string         `hcl:"key,optional" docs:"private key (in PEM format) for {RS*} and {ES*} variants or the secret for {HS*} algorithms. Mutually exclusive with {key_file}."`
 	KeyFile            string         `hcl:"key_file,optional" docs:"reference to file containing signing key. Mutually exclusive with {key}. See {key} for more information."`
 	Name               string         `hcl:"name,label,optional"`
diff --git a/docs/website/content/2.configuration/4.block/jwt_signing_profile.md b/docs/website/content/2.configuration/4.block/jwt_signing_profile.md
index b925403e..e49c33a1 100644
--- a/docs/website/content/2.configuration/4.block/jwt_signing_profile.md
+++ b/docs/website/content/2.configuration/4.block/jwt_signing_profile.md
@@ -24,7 +24,7 @@ values: [
   },
   {
     "default": "",
-    "description": "additional header fields for the JWT, `alg` and `typ` cannot be set",
+    "description": "additional header fields for the JWT, `typ` has the default value `JWT`, `alg` cannot be set",
     "name": "headers",
     "type": "object"
   },
diff --git a/eval/lib/jwt.go b/eval/lib/jwt.go
index 7ad86328..959ab4cf 100644
--- a/eval/lib/jwt.go
+++ b/eval/lib/jwt.go
@@ -217,7 +217,9 @@ func CreateJWT(signatureAlgorithm string, key interface{}, mapClaims jwt.MapClai
 		headers = map[string]interface{}{}
 	}
 
-	headers["typ"] = "JWT"
+	if _, set := headers["typ"]; !set {
+		headers["typ"] = "JWT"
+	}
 	headers["alg"] = signingMethod.Alg()
 
 	// create token
diff --git a/oauth2/client.go b/oauth2/client.go
index a3c12031..b5162908 100644
--- a/oauth2/client.go
+++ b/oauth2/client.go
@@ -129,6 +129,9 @@ func NewClient(grantType string, asConfig config.OAuth2AS, clientConfig config.O
 					return nil, err
 				}
 				headers = seetie.ValueToMap(v)
+				if _, exists := headers["alg"]; exists {
+					return nil, fmt.Errorf(`"alg" cannot be set via "headers"`)
+				}
 			}
 
 			tokenEndpoint, err := asConfig.GetTokenEndpoint()


Test patch:
diff --git a/eval/lib/jwt_test.go b/eval/lib/jwt_test.go
index 28ab4c81..c777bed6 100644
--- a/eval/lib/jwt_test.go
+++ b/eval/lib/jwt_test.go
@@ -598,6 +598,7 @@ func TestJwtSignDynamic(t *testing.T) {
 					headers = {
 						kid = "key-id"
 						foo = [request.method, backend_responses.default.status]
+						typ = "at+jwt"
 					}
 					claims = {
 						x-method = "GET"
@@ -607,7 +608,7 @@ func TestJwtSignDynamic(t *testing.T) {
 			}
 			`,
 			"MyToken",
-			map[string]interface{}{"alg": "HS256", "typ": "JWT", "kid": "key-id", "foo": []interface{}{"GET", 200}},
+			map[string]interface{}{"alg": "HS256", "typ": "at+jwt", "kid": "key-id", "foo": []interface{}{"GET", 200}},
 			`{"sub": "12345"}`,
 			3600,
 			http.MethodGet,
@@ -834,26 +835,6 @@ func TestJwtSignConfigError(t *testing.T) {
 			`{"sub": "12345"}`,
 			`configuration error: MyToken: "alg" cannot be set via "headers"`,
 		},
-		{
-			"user-defined typ header",
-			`
-			server "test" {
-			}
-			definitions {
-				jwt_signing_profile "MyToken" {
-					signature_algorithm = "HS256"
-					key = "$3cRe4"
-					ttl = "1h"
-					headers = {
-						typ = "JET"
-					}
-				}
-			}
-			`,
-			"MyToken",
-			`{"sub": "12345"}`,
-			`configuration error: MyToken: "typ" cannot be set via "headers"`,
-		},
 	}
 
 	for _, tt := range tests {
diff --git a/server/http_oauth2_test.go b/server/http_oauth2_test.go
index 7305922e..26af5ba3 100644
--- a/server/http_oauth2_test.go
+++ b/server/http_oauth2_test.go
@@ -791,6 +791,30 @@ definitions {
 `,
 			"configuration error: be: client authentication key: read error: open ",
 		},
+		{
+			"alg header with client_secret_jwt",
+			`server {}
+definitions {
+  backend "be" {
+    oauth2 {
+      token_endpoint = "https://authorization.server/token"
+      client_id      = "my_client"
+      client_secret  = "my_client_secret"
+      grant_type     = "client_credentials"
+      token_endpoint_auth_method = "client_secret_jwt"
+      jwt_signing_profile {
+        signature_algorithm = "HS256"
+        ttl = "10s"
+        headers = {
+          alg = "some value"
+        }
+      }
+    }
+  }
+}
+`,
+			"configuration error: be: \"alg\" cannot be set via \"headers\"",
+		},
 	} {
 		var errMsg string
 		conf, err := configload.LoadBytes([]byte(tc.hcl), "couper.hcl")