diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebWolfMacro.java b/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebWolfMacro.java index 7f2712d7f..d78d62765 100644 --- a/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebWolfMacro.java +++ b/webgoat-container/src/main/java/org/owasp/webgoat/asciidoc/WebWolfMacro.java @@ -41,7 +41,7 @@ public class WebWolfMacro extends InlineMacroProcessor { * The purpose is to make it possible to use the application behind a reverse proxy. For instance in the docker * compose/stack version with webgoat webwolf and nginx proxy. * You do not have to use the indicated hostname, but if you do, you should define two hosts aliases - * 127.0.0.1 www.webgoat.local www.webwolf.locaal + * 127.0.0.1 www.webgoat.local www.webwolf.local */ private String determineHost(String host, String port) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/jwt/JWTDecodeEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/jwt/JWTDecodeEndpoint.java new file mode 100644 index 000000000..63815d87a --- /dev/null +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/jwt/JWTDecodeEndpoint.java @@ -0,0 +1,22 @@ +package org.owasp.webgoat.jwt; + +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AttackResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class JWTDecodeEndpoint extends AssignmentEndpoint { + + @PostMapping("/JWT/decode") + @ResponseBody + public AttackResult decode(@RequestParam("jwt-encode-user") String user) { + if ("user".equals(user)) { + return success(this).build(); + } else { + return failed(this).build(); + } + } +} diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/jwt/JWTFinalEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/jwt/JWTFinalEndpoint.java index 44d19a1f2..ec44e9756 100644 --- a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/jwt/JWTFinalEndpoint.java +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/jwt/JWTFinalEndpoint.java @@ -84,62 +84,6 @@ public class JWTFinalEndpoint extends AssignmentEndpoint { private JWTFinalEndpoint(DataSource dataSource) { this.dataSource = dataSource; } - - @PostMapping(path="/JWT/encode",produces=MediaType.TEXT_HTML_VALUE) - @ResponseBody - public String encode(@RequestParam("jsonHeader") String jsonHeader, - @RequestParam("jsonPayload") String jsonPayload, - @RequestParam("jsonSecret") String jsonSecret) throws NoSuchAlgorithmException { - - //System.out.println(jsonHeader); - //System.out.println(jsonPayload); - String encodedHeader; - String encodedPayload; - String encodedSignature; - try { - encodedHeader = TextCodec.BASE64URL.encode(jsonHeader); - encodedPayload = TextCodec.BASE64URL.encode(jsonPayload); - if (jsonHeader.toLowerCase().contains("none")) { - encodedSignature=""; - } else { - encodedSignature = TextCodec.BASE64URL.encode(getJWTSignature(jsonHeader, encodedHeader, encodedPayload, jsonSecret)); - } - } catch (Exception e) { - encodedHeader=""; - encodedPayload="signature type not supported in this tool, try jwt.io"; - encodedSignature = ""; - } - String result = "{\"encodedHeader\":\""+encodedHeader+"\",\"encodedPayload\":\""+encodedPayload+"\",\"encodedSignature\":\""+encodedSignature+"\"}"; - //System.out.println(result); - return result; - } - - private byte[] getJWTSignature(String jsonHeader, String encodedHeader, String encodedPayload, String jsonSecret) throws NoSuchAlgorithmException, InvalidKeyException { - String message = encodedHeader+"."+encodedPayload; - String algorithm = "HmacSHA256"; - if (jsonHeader.equals("HS512")) { - algorithm = "HmacSHA512"; - } - Mac macInstance = Mac.getInstance(algorithm); - SecretKeySpec secret_key = new SecretKeySpec(TextCodec.BASE64.decode(jsonSecret), algorithm); - macInstance.init(secret_key); - - return macInstance.doFinal(message.getBytes(StandardCharsets.UTF_8)); - } - - @PostMapping(path="/JWT/decode",produces=MediaType.TEXT_HTML_VALUE) - @ResponseBody - public String decode(@RequestParam("jwtToken") String jwtToken) throws NoSuchAlgorithmException { - try { - String encodedHeader = jwtToken.substring(0, jwtToken.indexOf(".")); - String encodedPayload = jwtToken.substring(jwtToken.indexOf(".")+1, jwtToken.lastIndexOf(".")); - String jsonHeader = TextCodec.BASE64URL.decodeToString(encodedHeader); - String jsonPayload = TextCodec.BASE64URL.decodeToString(encodedPayload); - return "{\"jsonHeader\":\""+jsonHeader.replace("\"", "\\\"")+"\",\"jsonPayload\":\""+jsonPayload.replace("\"", "\\\"").replace("\t","").replace("\r", "").replace("\n", "")+"\"}"; - } catch (Exception e) { - return "{\"jsonHeader\":\"\",\"jsonPayload\":\"\"}"; - } - } @PostMapping("/JWT/final/follow/{user}") public @ResponseBody diff --git a/webgoat-lessons/jwt/src/main/resources/html/JWT.html b/webgoat-lessons/jwt/src/main/resources/html/JWT.html index 27bd0ac89..323832b61 100644 --- a/webgoat-lessons/jwt/src/main/resources/html/JWT.html +++ b/webgoat-lessons/jwt/src/main/resources/html/JWT.html @@ -1,61 +1,39 @@ -
- - - -
-
- - - - - - - -
JWT header:
JWT payload: - -
.
encryption key:
-
-
- - - - -
JWT token: - eyAgImFsZyI6IkhTMjU2IiwgICJ0eXAiOiJKV1QifQ - .//header
-
eyAgCQ0KCSJleHAiOiAxNDE2NDcxOTM0LCAgInVzZXJfbmFtZSI6ICJ1c2Vy - IiwgIA0KCSJzY29wZSI6IFsgICAgInJlYWQiLCAgICAid3JpdGUiICBdLCAg - DQoJImF1dGhvcml0aWVzIjogWyAgICAiUk9MRV9BRE1JTiIsICAgICJST0xF - X1VTRVIiICBdLCAgDQoJImp0aSI6ICI5YmM5MmE0NC0wYjFhLTRjNWUtYmU3 - MC1kYTUyMDc1YjlhODQiLCAgDQoJImNsaWVudF9pZCI6ICJteS1jbGllbnQt - d2l0aC1zZWNyZXQiDQp9DQoJCQk. //payload
- gc32RepP65NANPLQP31Aq7QPbpWKBOiaS9UXczYVZLE//signature
JWT token:
-
+ +
+
+
+ +
+
+
+
+
+ + + Username: + + + + +
+
+
+
+
+
+
+
+
@@ -63,7 +41,6 @@ $(document).ready(
-
@@ -125,7 +102,7 @@ $(document).ready(
-
+
@@ -225,9 +202,11 @@ $(document).ready( - $4.99 + $4.99 - $9.98 + $9.98
- \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_decode.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_decode.adoc new file mode 100644 index 000000000..69ac22310 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_decode.adoc @@ -0,0 +1,12 @@ +== Decoding a JWT token + +Let's try decoding a JWT token, for this you can use the webWolfLink:jwt[noLink] inside WebWolf. +Given the following token: + +[source] +---- +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDcwOTk2MDgsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiI5YmM5MmE0NC0wYjFhLTRjNWUtYmU3MC1kYTUyMDc1YjlhODQiLCJjbGllbnRfaWQiOiJteS1jbGllbnQtd2l0aC1zZWNyZXQifQ.N9TsIXpvMoICVeGI9mEOPVlYODjMOCis--yB-34BOOw +---- + +Copy and paste the following token and decode the token, can you find the user inside the token? + diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_structure.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_structure.adoc index 9edbdaea5..9cb187f48 100644 --- a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_structure.adoc +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_structure.adoc @@ -1,10 +1,17 @@ == Structure of a JWT token -Let's take a look at the structure of a JWT token. +Let's take a look at the structure of a JWT token: -The token is base64-url-encoded and consists of three parts `header.claims.signature`. +[role="lesson-image"] +image::images/jwt_token.png[JWT] + +The token is base64 encoded and consists of three parts: + + - header + - claims + - signature + +Both header and claims consist are respresented by a JSON object. The header describes the cryptographic operations applied to the JWT and optionally, additional properties of the JWT. +The claims represent a JSON object whose members are the claims conveyed by the JWT. -Based on the algorithm the signature will be added to the token. This way you can verify that someone did not modify the token (one change to the token will invalidate the signature). -You can use the below forms as a simple way to encode and decode JWT tokens. Or you can make use of the more extensive options at https://jwt.io[jwt.io,window=_blank]. -You can revisit this page for the assignments to come and use it as part of your attempts to solve it. diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/jwt/JWTFinalEndpointTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/jwt/JWTFinalEndpointTest.java index c79a1356e..379c402b5 100644 --- a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/jwt/JWTFinalEndpointTest.java +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/jwt/JWTFinalEndpointTest.java @@ -1,16 +1,6 @@ package org.owasp.webgoat.jwt; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertTrue; -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 java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - +import io.jsonwebtoken.Jwts; import org.hamcrest.CoreMatchers; import org.junit.Before; import org.junit.Test; @@ -21,9 +11,15 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; -import lombok.SneakyThrows; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +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; @RunWith(SpringJUnit4ClassRunner.class) public class JWTFinalEndpointTest extends LessonTest { @@ -77,22 +73,4 @@ public class JWTFinalEndpointTest extends LessonTest { .andExpect(status().isOk()) .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-invalid-token")))); } - - @Test - @SneakyThrows - public void testJWTTestTools() { - - //JWTFinalEndpoint jwtFinalEndpoint = new JWTFinalEndpoint(null); - String jsonHeader = "{\"alg\":\"HS256\"}"; - String jsonPayload = "{\"iss\":\"OWASP\"}"; - String jsonSecret = "secret"; - String jwtToken = jwtFinalEndpoint.encode(jsonHeader, jsonPayload, jsonSecret).replace(":", "") - .replace("encodedHeader", "").replace("encodedPayload", "").replace("encodedSignature", "") - .replace("{", "").replace("}", "").replace("\"", "").replace(",", "."); - - Jwt jwt = Jwts.parser().setSigningKey(jsonSecret).parse(jwtToken); - String revert = jwtFinalEndpoint.decode(jwtToken); - //System.out.println("revert: "+revert); - - } } \ No newline at end of file diff --git a/webwolf/pom.xml b/webwolf/pom.xml index 1a46f046f..fb1f5e4ee 100644 --- a/webwolf/pom.xml +++ b/webwolf/pom.xml @@ -37,9 +37,9 @@ commons-lang3 - io.jsonwebtoken - jjwt - 0.7.0 + org.bitbucket.b_c + jose4j + 0.7.2 org.springframework.boot diff --git a/webwolf/src/main/java/org/owasp/webwolf/jwt/JWTToken.java b/webwolf/src/main/java/org/owasp/webwolf/jwt/JWTToken.java index 41e79c51a..6d82ce3b0 100644 --- a/webwolf/src/main/java/org/owasp/webwolf/jwt/JWTToken.java +++ b/webwolf/src/main/java/org/owasp/webwolf/jwt/JWTToken.java @@ -1,20 +1,28 @@ package org.owasp.webwolf.jwt; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.*; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.springframework.util.Base64Utils; +import lombok.*; +import org.jose4j.jws.AlgorithmIdentifiers; +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwt.consumer.JwtConsumer; +import org.jose4j.jwt.consumer.JwtConsumerBuilder; +import org.jose4j.jwx.CompactSerializer; +import org.jose4j.keys.HmacKey; +import org.jose4j.lang.JoseException; import org.springframework.util.StringUtils; import java.util.Map; import java.util.regex.Pattern; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.springframework.util.Base64Utils.decodeFromUrlSafeString; + @NoArgsConstructor +@AllArgsConstructor @Getter @Setter +@Builder public class JWTToken { private static final Pattern jwtPattern = Pattern.compile("(.*)\\.(.*)\\.(.*)"); @@ -24,63 +32,70 @@ public class JWTToken { private String header; private String payload; private boolean signatureValid = true; - private boolean validToken = true; public void decode() { - validToken = parseToken(encoded); + parseToken(encoded.trim().replace(System.getProperty("line.separator"), "")); signatureValid = validateSignature(secretKey, encoded); } public void encode() { - var mapper = new ObjectMapper(); - try { - if (StringUtils.hasText(secretKey)) { - encoded = Jwts.builder() - .signWith(SignatureAlgorithm.HS256, Base64Utils.encodeToUrlSafeString(secretKey.getBytes())) - .setClaims(mapper.readValue(payload, Map.class)) - .setHeader(mapper.readValue(header, Map.class)) - .compact(); - } else { - encoded = Jwts.builder() - .setClaims(mapper.readValue(payload, Map.class)) - .setHeader(mapper.readValue(header, Map.class)) - .compact(); + JsonWebSignature jws = new JsonWebSignature(); + jws.setPayload(payload); + jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256); + jws.setDoKeyValidation(false); + if (StringUtils.hasText(secretKey)) { + jws.setKey(new HmacKey(secretKey.getBytes(UTF_8))); + try { + encoded = jws.getCompactSerialization(); + signatureValid = true; + } catch (JoseException e) { + header = ""; + payload = ""; } - validToken = true; - } catch (JsonProcessingException e) { - validToken = false; - signatureValid = false; + } else { + var encodedHeader = jws.getHeaders().getEncodedHeader(); + var encodedPayload = jws.getEncodedPayload(); + encoded = CompactSerializer.serialize(new String[]{encodedHeader, encodedPayload}); } } private boolean parseToken(String jwt) { var matcher = jwtPattern.matcher(jwt); - var mapper = new ObjectMapper().writerWithDefaultPrettyPrinter(); + var mapper = new ObjectMapper(); + if (matcher.matches()) { try { - Jwt headerClaimsJwt = Jwts.parser().parseClaimsJwt(matcher.group(1) + "." + matcher.group(2) + "."); - this.header = mapper.writeValueAsString(headerClaimsJwt.getHeader()); - this.payload = mapper.writeValueAsString(headerClaimsJwt.getBody()); + var prettyPrint = mapper.writerWithDefaultPrettyPrinter(); + this.header = prettyPrint.writeValueAsString(mapper.readValue(decodeFromUrlSafeString(matcher.group(1)), Map.class)); + this.payload = prettyPrint.writeValueAsString(mapper.readValue(decodeFromUrlSafeString(matcher.group(2)), Map.class)); + return true; } catch (Exception e) { - try { - this.header = mapper.writeValueAsString(new String(Base64Utils.decodeFromUrlSafeString(matcher.group(1)))); - this.payload = mapper.writeValueAsString(new String(Base64Utils.decodeFromUrlSafeString(matcher.group(2)))); - } catch (Exception ex) { - return false; - } + this.header = new String(decodeFromUrlSafeString(matcher.group(1))); + this.payload = new String(decodeFromUrlSafeString(matcher.group(2))); + return false; } - return true; + } else { + this.header = "error"; + this.payload = "error"; } return false; } private boolean validateSignature(String secretKey, String jwt) { - try { - Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt); - return true; - } catch (Exception e) { - return false; + if (StringUtils.hasText(secretKey)) { + JwtConsumer jwtConsumer = new JwtConsumerBuilder() + .setSkipAllValidators() + .setVerificationKey(new HmacKey(secretKey.getBytes(UTF_8))) + .setRelaxVerificationKeyValidation() + .build(); + try { + jwtConsumer.processToClaims(jwt); + return true; + } catch (InvalidJwtException e) { + return false; + } } + return false; } } diff --git a/webwolf/src/main/resources/static/js/jwt.js b/webwolf/src/main/resources/static/js/jwt.js index 7a175818f..28a858204 100644 --- a/webwolf/src/main/resources/static/js/jwt.js +++ b/webwolf/src/main/resources/static/js/jwt.js @@ -8,10 +8,8 @@ $(document).ready(() => { url: '/WebWolf/jwt/decode', data: JSON.stringify({encoded: token, secretKey: secretKey}), success: function (data) { - if (data.validToken) { - $('#tokenHeader').val(data.header); - $('#tokenPayload').val(data.payload); - } + $('#tokenHeader').val(data.header); + $('#tokenPayload').val(data.payload); updateSignature(data); }, contentType: "application/json", @@ -69,11 +67,10 @@ function parseJson(text) { return true; } - function updateSignature(data) { if (data.signatureValid) { - $('#signatureValid').val("Signature valid"); + $('#signatureValid').html("Signature valid"); } else { - $('#signatureValid').val("Signature invalid"); + $('#signatureValid').html("Signature invalid"); } } diff --git a/webwolf/src/main/resources/templates/login.html b/webwolf/src/main/resources/templates/login.html index f651a437f..23fe24e79 100644 --- a/webwolf/src/main/resources/templates/login.html +++ b/webwolf/src/main/resources/templates/login.html @@ -16,7 +16,6 @@

Sign in

- Use your WebGoat account.
@@ -31,11 +30,11 @@
+ placeholder="Username WebGoat" required="true" autofocus="true"/>
+ placeholder="Password WebGoat" required="true"/>
diff --git a/webwolf/src/test/java/org/owasp/webwolf/jwt/JWTTokenTest.java b/webwolf/src/test/java/org/owasp/webwolf/jwt/JWTTokenTest.java new file mode 100644 index 000000000..344714280 --- /dev/null +++ b/webwolf/src/test/java/org/owasp/webwolf/jwt/JWTTokenTest.java @@ -0,0 +1,81 @@ +package org.owasp.webwolf.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class JWTTokenTest { + + @Test + void encodeCorrectTokenWithoutSignature() { + var headers = Map.of("alg", "HS256", "typ", "JWT"); + var payload = Map.of("test", "test"); + var token = JWTToken.builder().header(toString(headers)).payload(toString(payload)).build(); + + token.encode(); + + assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoidGVzdCJ9"); + } + + @Test + void encodeCorrectTokenWithSignature() { + var headers = Map.of("alg", "HS256", "typ", "JWT"); + var payload = Map.of("test", "test"); + var token = JWTToken.builder() + .header(toString(headers)) + .payload(toString(payload)) + .secretKey("test") + .build(); + + token.encode(); + + assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoidGVzdCJ9.KOobRHDYyaesV_doOk11XXGKSONwzllraAaqqM4VFE4"); + } + + @Test + void encodeTokenWithNonJsonInput() { + var token = JWTToken.builder() + .header("aaa") + .payload("bbb") + .secretKey("test") + .build(); + + token.encode(); + + assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJIUzI1NiJ9.YmJi.VAcRegquayARuahZZ1ednXpbAyv7KEFnyjNJlxLNX0I"); + } + + @Test + void decodeValidSignedToken() { + var token = JWTToken.builder() + .encoded("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoidGVzdCJ9.KOobRHDYyaesV_doOk11XXGKSONwzllraAaqqM4VFE4") + .secretKey("test") + .build(); + + token.decode(); + + assertThat(token.getHeader()).contains("\"alg\" : \"HS256\""); + assertThat(token.isSignatureValid()).isTrue(); + } + + @Test + void decodeInvalidSignedToken() { + var token = JWTToken.builder().encoded("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXsdfdfsaasfddfasN0IjoidGVzdCJ9.KOobRHDYyaesV_doOk11XXGKSONwzllraAaqqM4VFE4").build(); + + token.decode(); + + assertThat(token.getHeader()).contains("\"alg\":\"HS256\""); + assertThat(token.getPayload()).contains("{\"te"); + } + + @SneakyThrows + private String toString(Map map) { + var mapper = new ObjectMapper(); + return mapper.writeValueAsString(map); + } + +} \ No newline at end of file