Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(java-sdk): support for pattern in jwt static claim #1926

Merged
merged 3 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions docs/src/modules/java/pages/using-jwts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,39 @@ When the values of specific claims are known in advance, Kalix can be configured

[source, java]
----
@JWT(validate = JWT.JwtMethodMode.BEARER_TOKEN,
bearerTokenIssuer = "my-issuer",
staticClaims = {
@JWT.StaticClaim(claim = "role", value = {"admin", "editor"}), // <1>
@JWT.StaticClaim(claim = "aud", value = "${ENV}.kalix.io")}) // <2>
@JWT(validate = JWT.JwtMethodMode.BEARER_TOKEN,
bearerTokenIssuer = "my-issuer",
staticClaims = {
@JWT.StaticClaim(claim = "role", value = {"admin", "editor"}), // <1>
@JWT.StaticClaim(claim = "aud", value = "${ENV}.kalix.io")}) // <2>
----
<1> When declaring multiple values for the same claim, **all** of them will be required when validating the request.
<2> The value of the claim can be dependent on an environment variable, which will be resolved at runtime.

NOTE: For specifying an issuer claim (i.e. "iss"), you should still use the provided issuer-specific fields and not static claims.

==== Configuring static claims with a pattern

Static claims can also be defined using a pattern. This is useful when the value of the claim is not completely known in advance, but it can still be validated against a regular expression. See some examples below:

[source, java]
----
@JWT(validate = JWT.JwtMethodMode.BEARER_TOKEN,
staticClaims = {
@JWT.StaticClaim(claim = "role", pattern = "^(admin|editor)$"), // <1>
@JWT.StaticClaim(claim = "sub", pattern = // <2>
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"),
@JWT.StaticClaim(claim = "name", pattern = "^\\S+$") // <3>
})
----
<1> Claim "role" must have one of 2 values: `admin` or `editor`.
<2> Claim "sub" must be a valid UUID.
<3> Claim "name" must be not empty.

When signing a JWT, static claim defined by a pattern will not be included. They are only used in validation.

NOTE: A static claim can be defined with a `value` or a `pattern`, but not both.

== Running locally with JWT support

When running locally using docker compose, by default, a dev key with id `dev` is configured for use. This key uses the JWT `none` signing algorithm, which means the JWT tokens produced do not contain a cryptographic signature, nor are they validated against a signature when validating. Therefore, when calling an endpoint with a bearer token, only its presence is validated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ enum JwtMethodMode {
String[] bearerTokenIssuer() default {};


/**
* A static claim is a claim that is required to be present on the token, and have a particular
* value. This can be used to ensure that the token has a particular role, for example.
* <p>
* If the claim is not present, or does not have the expected value, then the request will be
* rejected with a 403 Forbidden response.
* <p>
* If the claim is present, but does not have the expected value, then the request will be
* rejected with a 403 Forbidden response.
* <p>
* If the claim is present, and has the expected value, then the request will be allowed to
* proceed.
*
* Each static claim can be configured either with a 'value' or a 'pattern' that will
* be matched against the value of the claim, but not both.
*/
@interface StaticClaim {
/**
* The claim name needs to be a hardcoded literal (e.g. "role")
Expand All @@ -61,7 +77,20 @@ enum JwtMethodMode {
* or a combination of both (e.g. "${ENV_VAR}-admin").
* When declaring multiple values, ALL of those will be required when validating the claim.
*/
String[] value();
String[] value() default {};

/**
* This receives a regex expression (Java flavor) used to match on the incoming claim value.
* Cannot be used in conjunction with `value` field above. It's one or the other.
* <p>NOTE: when signing, a static claim defined with a pattern will not be included in the token.
*
* <p>Usage examples:
* <ul>
* <li>claim value is not empty: "\\S+"</li>
* <li>claim value has one of 2 possible values: "^(admin|manager)$"</li>
* </ul>
*/
String pattern() default "";
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ import scala.jdk.CollectionConverters.IterableHasAsJava

object JwtDescriptorFactory {

private def buildStaticClaimFromAnnotation(sc: JWT.StaticClaim): JwtStaticClaim =
JwtStaticClaim
.newBuilder()
.setClaim(sc.claim())
.addAllValue(sc.value().toList.asJava)
.setPattern(sc.pattern())
.build()

private def jwtMethodOptions(javaMethod: Method): JwtMethodOptions = {
val ann = javaMethod.getAnnotation(classOf[JWT])
val jwt = JwtMethodOptions.newBuilder()
Expand All @@ -38,8 +46,7 @@ object JwtDescriptorFactory {

ann
.staticClaims()
.foreach(sc =>
jwt.addStaticClaim(JwtStaticClaim.newBuilder().setClaim(sc.claim()).addAllValue(sc.value().toList.asJava)))
.foreach(sc => jwt.addStaticClaim(buildStaticClaimFromAnnotation(sc)))
jwt.build()
}

Expand Down Expand Up @@ -69,8 +76,7 @@ object JwtDescriptorFactory {
ann.bearerTokenIssuer().map(jwt.addBearerTokenIssuer)
ann
.staticClaims()
.foreach(sc =>
jwt.addStaticClaim(JwtStaticClaim.newBuilder().setClaim(sc.claim()).addAllValue(sc.value().toList.asJava)))
.foreach(sc => jwt.addStaticClaim(buildStaticClaimFromAnnotation(sc)))

val kalixServiceOptions = kalix.ServiceOptions.newBuilder()
kalixServiceOptions.setJwt(jwt.build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ public static class ActionWithMethodLevelJWT extends Action {
validate = JWT.JwtMethodMode.BEARER_TOKEN,
bearerTokenIssuer = {"a", "b"},
staticClaims = {
@JWT.StaticClaim(claim = "role", value = "admin"),
@JWT.StaticClaim(claim = "roles", value = {"viewer", "editor"}),
@JWT.StaticClaim(claim = "aud", value = "${ENV}.kalix.io"),
@JWT.StaticClaim(claim = "more-roles", value = {"viewer", "editor"})
@JWT.StaticClaim(claim = "sub", pattern = "^sub-\\S+$")
})
public Action.Effect<Message> message(@RequestBody Message msg) {
return effects().reply(msg);
Expand All @@ -120,9 +120,9 @@ public Action.Effect<Message> message(@RequestBody Message msg) {
validate = JWT.JwtMethodMode.BEARER_TOKEN,
bearerTokenIssuer = {"a", "b"},
staticClaims = {
@JWT.StaticClaim(claim = "role", value = "admin"),
@JWT.StaticClaim(claim = "roles", value = {"editor", "viewer"}),
@JWT.StaticClaim(claim = "aud", value = "${ENV}.kalix.io"),
@JWT.StaticClaim(claim = "more-roles", value = {"editor", "viewer"})
@JWT.StaticClaim(claim = "sub", pattern = "^\\S+$")
})
public static class ActionWithServiceLevelJWT extends Action {
@PostMapping("/message")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,12 @@ class ActionDescriptorFactorySpec extends AnyWordSpec with ComponentDescriptorSu
assertRequestFieldJavaType(method, "json_body", JavaType.MESSAGE)

val Seq(claim1, claim2, claim3) = jwtOption.getStaticClaimList.asScala.toSeq
claim1.getClaim shouldBe "role"
claim1.getValue(0) shouldBe "admin"
claim1.getClaim shouldBe "roles"
claim1.getValueList.asScala.toSeq shouldBe Seq("viewer", "editor")
claim2.getClaim shouldBe "aud"
claim2.getValue(0) shouldBe "${ENV}.kalix.io"
claim3.getClaim shouldBe "more-roles"
claim3.getValueList.asScala.toSeq shouldBe Seq("viewer", "editor")
claim3.getClaim shouldBe "sub"
claim3.getPattern shouldBe "^sub-\\S+$"
}
}

Expand All @@ -243,12 +243,12 @@ class ActionDescriptorFactorySpec extends AnyWordSpec with ComponentDescriptorSu
jwtOption.getValidate shouldBe JwtServiceMode.BEARER_TOKEN

val Seq(claim1, claim2, claim3) = jwtOption.getStaticClaimList.asScala.toSeq
claim1.getClaim shouldBe "role"
claim1.getValue(0) shouldBe "admin"
claim1.getClaim shouldBe "roles"
claim1.getValueList.asScala.toSeq shouldBe Seq("editor", "viewer")
claim2.getClaim shouldBe "aud"
claim2.getValue(0) shouldBe "${ENV}.kalix.io"
claim3.getClaim shouldBe "more-roles"
claim3.getValueList.asScala.toSeq shouldBe Seq("editor", "viewer")
claim3.getClaim shouldBe "sub"
claim3.getPattern shouldBe "^\\S+$"
}
}

Expand Down