diff --git a/src/main/resources/lessons/jwt/i18n/WebGoatLabels.properties b/src/main/resources/lessons/jwt/i18n/WebGoatLabels.properties
index ed05f46b2..390c79eba 100644
--- a/src/main/resources/lessons/jwt/i18n/WebGoatLabels.properties
+++ b/src/main/resources/lessons/jwt/i18n/WebGoatLabels.properties
@@ -26,9 +26,15 @@ jwt-refresh-alg-none=Nicely found! You solved the assignment with 'alg: none' ca
jwt-final-jerry-account=Yikes, you are removing Jerry's account, try to delete the account of Tom
jwt-final-not-tom=Username is not Tom try to pass a token for Tom
-jwt-final-hint1=Take a look at the token and specifically and the header
-jwt-final-hint2=The 'kid' (key ID) header parameter is a hint indicating which key was used to secure the JWS
-jwt-final-hint3=The key can be located on the filesystem in memory or even reside in the database
-jwt-final-hint4=The key is stored in the database and loaded while verifying a token
-jwt-final-hint5=Using a SQL injection you might be able to manipulate the key to something you know and create a new token.
-jwt-final-hint6=Use: hacked' UNION select 'deletingTom' from INFORMATION_SCHEMA.SYSTEM_USERS -- as the kid in the header and change the contents of the token to Tom and hit the endpoint with the new token
+jwt-jku-hint1=Take a look at the token and specifically and the header
+jwt-jku-hint2=The 'jku' (key ID) header parameter is a hint indicating which key is used to verify the JWS
+jwt-jku-hint3=Could you use WebWolf to host the public key as a JWKS?
+jwt-jku-hint4=Create a key pair and sign the token with the private key
+jwt-jku-hint5=Change the JKU header claim and point it to a URL which hosts the public key in JWKS format.
+
+jwt-kid-hint1=Take a look at the token and specifically and the header
+jwt-kid-hint2=The 'kid' (key ID) header parameter is a hint indicating which key was used to secure the JWS
+jwt-kid-hint3=The key can be located on the filesystem in memory or even reside in the database
+jwt-kid-hint4=The key is stored in the database and loaded while verifying a token
+jwt-kid-hint5=Using a SQL injection you might be able to manipulate the key to something you know and create a new token.
+jwt-kid-hint6=Use: hacked' UNION select 'deletingTom' from INFORMATION_SCHEMA.SYSTEM_USERS -- as the kid in the header and change the contents of the token to Tom and hit the endpoint with the new token
diff --git a/src/test/java/org/owasp/webgoat/lessons/jwt/claimmisuse/JWTHeaderJKUEndpointTest.java b/src/test/java/org/owasp/webgoat/lessons/jwt/claimmisuse/JWTHeaderJKUEndpointTest.java
new file mode 100644
index 000000000..2afe6371e
--- /dev/null
+++ b/src/test/java/org/owasp/webgoat/lessons/jwt/claimmisuse/JWTHeaderJKUEndpointTest.java
@@ -0,0 +1,95 @@
+package org.owasp.webgoat.lessons.jwt.claimmisuse;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
+import static io.jsonwebtoken.SignatureAlgorithm.RS256;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import io.jsonwebtoken.Jwts;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.interfaces.RSAPublicKey;
+import java.util.HashMap;
+import java.util.Map;
+import org.jose4j.jwk.JsonWebKeySet;
+import org.jose4j.jwk.RsaJsonWebKey;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.owasp.webgoat.container.plugins.LessonTest;
+import org.owasp.webgoat.lessons.jwt.JWT;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+class JWTHeaderJKUEndpointTest extends LessonTest {
+ private KeyPair keyPair;
+ private WireMockServer webwolfServer;
+ private int port;
+
+ @BeforeEach
+ public void setup() throws Exception {
+ when(webSession.getCurrentLesson()).thenReturn(new JWT());
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
+
+ setupWebWolf();
+ this.keyPair = generateRsaKey();
+ }
+
+ private void setupWebWolf() {
+ this.webwolfServer = new WireMockServer(options().dynamicPort());
+ webwolfServer.start();
+ this.port = webwolfServer.port();
+ }
+
+ private KeyPair generateRsaKey() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ @Test
+ void solve() throws Exception {
+ setupJsonWebKeySetInWebWolf();
+ var token = createTokenAndSignIt();
+
+ mockMvc
+ .perform(MockMvcRequestBuilders.post("/JWT/jku/delete").param("token", token).content(""))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.lessonCompleted", is(true)));
+ }
+
+ @Test
+ @DisplayName("When JWKS is not present in WebWolf then the call should fail")
+ void shouldFailNotPresent() throws Exception {
+ var token = createTokenAndSignIt();
+
+ mockMvc
+ .perform(MockMvcRequestBuilders.post("/JWT/jku/delete").param("token", token).content(""))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.lessonCompleted", is(false)));
+ }
+
+ private String createTokenAndSignIt() {
+ Map claims = new HashMap<>();
+ claims.put("username", "Tom");
+ var token =
+ Jwts.builder()
+ .setHeaderParam("jku", "http://localhost:%d/files/jwks".formatted(port))
+ .setClaims(claims)
+ .signWith(RS256, this.keyPair.getPrivate())
+ .compact();
+ return token;
+ }
+
+ private void setupJsonWebKeySetInWebWolf() {
+ var jwks = new JsonWebKeySet(new RsaJsonWebKey((RSAPublicKey) keyPair.getPublic()));
+ webwolfServer.stubFor(
+ WireMock.get(WireMock.urlMatching("/files/jwks"))
+ .willReturn(aResponse().withStatus(200).withBody(jwks.toJson())));
+ }
+}
diff --git a/src/test/java/org/owasp/webgoat/lessons/jwt/JWTFinalEndpointTest.java b/src/test/java/org/owasp/webgoat/lessons/jwt/claimmisuse/JWTHeaderKIDEndpointTest.java
similarity index 85%
rename from src/test/java/org/owasp/webgoat/lessons/jwt/JWTFinalEndpointTest.java
rename to src/test/java/org/owasp/webgoat/lessons/jwt/claimmisuse/JWTHeaderKIDEndpointTest.java
index 1c903c351..6995388c9 100644
--- a/src/test/java/org/owasp/webgoat/lessons/jwt/JWTFinalEndpointTest.java
+++ b/src/test/java/org/owasp/webgoat/lessons/jwt/claimmisuse/JWTHeaderKIDEndpointTest.java
@@ -1,4 +1,4 @@
-package org.owasp.webgoat.lessons.jwt;
+package org.owasp.webgoat.lessons.jwt.claimmisuse;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when;
@@ -14,10 +14,11 @@ import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.owasp.webgoat.container.plugins.LessonTest;
+import org.owasp.webgoat.lessons.jwt.JWT;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-public class JWTFinalEndpointTest extends LessonTest {
+public class JWTHeaderKIDEndpointTest extends LessonTest {
private static final String TOKEN_JERRY =
"eyJraWQiOiJ3ZWJnb2F0X2tleSIsImFsZyI6IkhTNTEyIn0.eyJhdWQiOiJ3ZWJnb2F0Lm9yZyIsImVtYWlsIjoiamVycnlAd2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IkplcnJ5In0.xBc5FFwaOcuxjdr_VJ16n8Jb7vScuaZulNTl66F2MWF1aBe47QsUosvbjWGORNcMPiPNwnMu1Yb0WZVNrp2ZXA";
@@ -42,7 +43,7 @@ public class JWTFinalEndpointTest extends LessonTest {
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, key)
.compact();
mockMvc
- .perform(MockMvcRequestBuilders.post("/JWT/final/delete").param("token", token).content(""))
+ .perform(MockMvcRequestBuilders.post("/JWT/kid/delete").param("token", token).content(""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.lessonCompleted", is(true)));
}
@@ -51,9 +52,7 @@ public class JWTFinalEndpointTest extends LessonTest {
public void withJerrysKeyShouldNotSolveAssignment() throws Exception {
mockMvc
.perform(
- MockMvcRequestBuilders.post("/JWT/final/delete")
- .param("token", TOKEN_JERRY)
- .content(""))
+ MockMvcRequestBuilders.post("/JWT/kid/delete").param("token", TOKEN_JERRY).content(""))
.andExpect(status().isOk())
.andExpect(
jsonPath(
@@ -64,7 +63,7 @@ public class JWTFinalEndpointTest extends LessonTest {
public void shouldNotBeAbleToBypassWithSimpleToken() throws Exception {
mockMvc
.perform(
- MockMvcRequestBuilders.post("/JWT/final/delete")
+ MockMvcRequestBuilders.post("/JWT/kid/delete")
.param("token", ".eyJ1c2VybmFtZSI6IlRvbSJ9.")
.content(""))
.andExpect(status().isOk())