From dda6f674a3e8a87c3c9df1983895d372d4221fea Mon Sep 17 00:00:00 2001 From: Nanne Baars Date: Tue, 22 May 2018 17:06:03 +0200 Subject: [PATCH] Last assignment for JWT tokens finished --- .../js/goatApp/view/LessonContentView.js | 3 + .../src/test/resources/logback-test.xml | 16 ++- .../webgoat/plugin/JWTRefreshEndpoint.java | 109 +++++++++++++++ .../jwt/src/main/resources/html/JWT.html | 130 +++++++++++++----- .../resources/i18n/WebGoatLabels.properties | 6 + .../jwt/src/main/resources/images/logs.txt | 10 +- .../jwt/src/main/resources/js/jwt-refresh.js | 42 ++++++ .../en/JWT_refresh_assignment.adoc | 2 +- .../plugin/JWTRefreshEndpointTest.java | 102 ++++++++++++++ .../org/owasp/webgoat/plugin/TokenTest.java | 19 +++ 10 files changed, 394 insertions(+), 45 deletions(-) create mode 100644 webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTRefreshEndpoint.java create mode 100644 webgoat-lessons/jwt/src/main/resources/js/jwt-refresh.js create mode 100644 webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTRefreshEndpointTest.java diff --git a/webgoat-container/src/main/resources/static/js/goatApp/view/LessonContentView.js b/webgoat-container/src/main/resources/static/js/goatApp/view/LessonContentView.js index 75ff968b2..117761108 100644 --- a/webgoat-container/src/main/resources/static/js/goatApp/view/LessonContentView.js +++ b/webgoat-container/src/main/resources/static/js/goatApp/view/LessonContentView.js @@ -90,6 +90,8 @@ define(['jquery', var prepareDataFunctionName = $(curForm).attr('prepareData'); var callbackFunctionName = $(curForm).attr('callback'); var submitData = (typeof webgoat.customjs[prepareDataFunctionName] === 'function') ? webgoat.customjs[prepareDataFunctionName]() : $(curForm).serialize(); + var additionalHeadersFunctionName = $(curForm).attr('additionalHeaders'); + var additionalHeaders = (typeof webgoat.customjs[additionalHeadersFunctionName] === 'function') ? webgoat.customjs[additionalHeadersFunctionName]() : function() {}; var successCallBackFunctionName = $(curForm).attr('successCallback'); var failureCallbackFunctionName = $(curForm).attr('failureCallback'); var callbackFunction = (typeof webgoat.customjs[callbackFunctionName] === 'function') ? webgoat.customjs[callbackFunctionName] : function() {}; @@ -104,6 +106,7 @@ define(['jquery', $.ajax({ //data:submitData, url:formUrl, + headers: additionalHeaders, method:formMethod, contentType:contentType, data: submitData, diff --git a/webgoat-container/src/test/resources/logback-test.xml b/webgoat-container/src/test/resources/logback-test.xml index adfa02c68..2ee84585a 100644 --- a/webgoat-container/src/test/resources/logback-test.xml +++ b/webgoat-container/src/test/resources/logback-test.xml @@ -1 +1,15 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTRefreshEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTRefreshEndpoint.java new file mode 100644 index 000000000..192a4bef7 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTRefreshEndpoint.java @@ -0,0 +1,109 @@ +package org.owasp.webgoat.plugin; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import io.jsonwebtoken.*; +import org.apache.commons.lang3.RandomStringUtils; +import org.owasp.webgoat.assignments.AssignmentEndpoint; +import org.owasp.webgoat.assignments.AssignmentHints; +import org.owasp.webgoat.assignments.AssignmentPath; +import org.owasp.webgoat.assignments.AttackResult; +import org.owasp.webgoat.session.WebSession; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @author nbaars + * @since 4/23/17. + */ +@AssignmentPath("/JWT/refresh/") +@AssignmentHints({"jwt-refresh-hint1", "jwt-refresh-hint2", "jwt-refresh-hint3", "jwt-refresh-hint4"}) +public class JWTRefreshEndpoint extends AssignmentEndpoint { + + public static final String PASSWORD = "bm5nhSkxCXZkKRy4"; + private static final String JWT_PASSWORD = "bm5n3SkxCX4kKRy4"; + private static final List validRefreshTokens = Lists.newArrayList(); + + @PostMapping(value = "login", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public @ResponseBody + ResponseEntity follow(@RequestBody Map json) { + String user = (String) json.get("user"); + String password = (String) json.get("password"); + + if ("Jerry".equals(user) && PASSWORD.equals(password)) { + return ResponseEntity.ok(createNewTokens(user)); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + private Map createNewTokens(String user) { + Map claims = Maps.newHashMap(); + claims.put("admin", "false"); + claims.put("user", user); + String token = Jwts.builder() + .setIssuedAt(new Date(System.currentTimeMillis() + TimeUnit.DAYS.toDays(10))) + .setClaims(claims) + .signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD) + .compact(); + Map tokenJson = Maps.newHashMap(); + String refreshToken = RandomStringUtils.randomAlphabetic(20); + validRefreshTokens.add(refreshToken); + tokenJson.put("access_token", token); + tokenJson.put("refresh_token", refreshToken); + return tokenJson; + } + + @PostMapping("checkout") + public @ResponseBody + AttackResult checkout(@RequestHeader("Authorization") String token) { + try { + Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", "")); + Claims claims = (Claims) jwt.getBody(); + String user = (String) claims.get("user"); + if ("Tom".equals(user)) { + return trackProgress(success().build()); + } + return trackProgress(failed().feedback("jwt-refresh-not-tom").feedbackArgs(user).build()); + } catch (ExpiredJwtException e) { + return trackProgress(failed().output(e.getMessage()).build()); + } catch (JwtException e) { + return trackProgress(failed().feedback("jwt-invalid-token").build()); + } + } + + @PostMapping("newToken") + public @ResponseBody + ResponseEntity newToken(@RequestHeader("Authorization") String token, @RequestBody Map json) { + String user; + String refreshToken; + try { + Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(token.replace("Bearer ", "")); + user = (String) jwt.getBody().get("user"); + refreshToken = (String) json.get("refresh_token"); + } catch (ExpiredJwtException e) { + user = (String) e.getClaims().get("user"); + refreshToken = (String) json.get("refresh_token"); + } + + if (user == null || refreshToken == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } else if (validRefreshTokens.contains(refreshToken)) { + validRefreshTokens.remove(refreshToken); + return ResponseEntity.ok(createNewTokens(user)); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + } + +} diff --git a/webgoat-lessons/jwt/src/main/resources/html/JWT.html b/webgoat-lessons/jwt/src/main/resources/html/JWT.html index 9eebc1f37..e81693e10 100644 --- a/webgoat-lessons/jwt/src/main/resources/html/JWT.html +++ b/webgoat-lessons/jwt/src/main/resources/html/JWT.html @@ -110,49 +110,101 @@ - +
-
-
- -
-
- -
-

Jerry

-
- Jerry is a small, brown, house mouse. -
-
- -
-
-
-
- -
-
- -
-

Tom

-
- Tom is a grey and white domestic short hair cat. -
-
- +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProductQuantityPriceTotal 
+ + + + $4.87$14.61 + +
+
+ +
+

Pentesting for professionals

+
by WebWolf Publishing
+ Status: Leaves warehouse in 2 - 3 weeks +
+
+
+ + $4.99$9.98 + +
     
Subtotal

Estimated shipping
+

Total

$24.59

$6.94
+

$31.53

      + + + +
@@ -209,7 +261,9 @@
diff --git a/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties index 62f53fd55..21f416bb2 100644 --- a/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties @@ -14,6 +14,12 @@ jwt-secret-hint1=Save the token and try to verify the token locally jwt-secret-hint2=Download a word list dictionary (https://github.com/first20hours/google-10000-english) jwt-secret-hint3=Write a small program or use HashCat for brute forcing the token according the word list +jwt-refresh-hint1=Look at the access log you will find a token there +jwt-refresh-hint2=The token from the access log is no longer valid, can you find a way to refresh it? +jwt-refresh-hint3=The endpoint for refreshing a token is 'jwt/refresh/newToken' +jwt-refresh-hint4=Use the found access token in the Authorization: Bearer header and use your refresh token +jwt-refresh-not-tom=User is not Tom but {0}, please try again + 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 diff --git a/webgoat-lessons/jwt/src/main/resources/images/logs.txt b/webgoat-lessons/jwt/src/main/resources/images/logs.txt index a4d1c7107..42146c185 100644 --- a/webgoat-lessons/jwt/src/main/resources/images/logs.txt +++ b/webgoat-lessons/jwt/src/main/resources/images/logs.txt @@ -1,6 +1,6 @@ -205.167.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /statuses/605086114483826688/favorite?authenticity_token=264c8bf11e0212227c001b77543c3519 HTTP/1.1" 404 242 "-" "Go-http-client/1.1" "-" -205.167.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /images/phocagallery/almhuette/thumbs/phoca_thumb_l_zimmer.jpg HTTP/1.1" 200 12783 "-" "Go-http-client/1.1" "-" -205.167.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /signup HTTP/1.1" 404 212 "-" "Go-http-client/1.1" "-" -205.167.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /vsmartseo/status/605086114483826688/actions HTTP/1.1" 404 249 "-" "Go-http-client/1.1" "-" -205.167.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /vsmartseo?p=s HTTP/1.1" 404 215 "-" "Go-http-client/1.1" "-" +194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/checkout?token=eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q HTTP/1.1" 401 242 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" +194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 200 12783 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" +194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/login HTTP/1.1" 200 212 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" +194.201.170.15 - - [28/Jan/2016:21:28:01 +0100] "GET /JWT/refresh/addItems HTTP/1.1" 404 249 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0" "-" +195.206.170.15 - - [28/Jan/2016:21:28:01 +0100] "POST /JWT/refresh/moveToCheckout HTTP/1.1" 404 215 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36" "-" diff --git a/webgoat-lessons/jwt/src/main/resources/js/jwt-refresh.js b/webgoat-lessons/jwt/src/main/resources/js/jwt-refresh.js new file mode 100644 index 000000000..e8f24a2a5 --- /dev/null +++ b/webgoat-lessons/jwt/src/main/resources/js/jwt-refresh.js @@ -0,0 +1,42 @@ +$(document).ready(function () { + login('Jerry'); +}) + +function login(user) { + $.ajax({ + type: 'POST', + url: 'JWT/refresh/login', + contentType: "application/json", + data: JSON.stringify({user: user, password: "bm5nhSkxCXZkKRy4"}) + }).success( + function (response) { + localStorage.setItem('access_token', response['access_token']); + localStorage.setItem('refresh_token', response['refresh_token']); + } + ) +} + +//Dev comment: Pass token as header as we had an issue with tokens ending up in the access_log +webgoat.customjs.addBearerToken = function () { + var headers_to_set = {}; + headers_to_set['Authorization'] = 'Bearer ' + localStorage.getItem('access_token'); + return headers_to_set; +} + +//Dev comment: Temporarily disabled from page we need to work out the refresh token flow but for now we can go live with the checkout page +function newToken() { + localStorage.getItem('refreshToken'); + $.ajax({ + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('access_token') + }, + type: 'POST', + url: 'JWT/refresh/newToken', + data: JSON.stringify({refreshToken: localStorage.getItem('refresh_token')}) + }).success( + function () { + localStorage.setItem('access_token', apiToken); + localStorage.setItem('refresh_token', refreshToken); + } + ) +} \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh_assignment.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh_assignment.adoc index 87f787a0f..7e3357d8d 100644 --- a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh_assignment.adoc +++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh_assignment.adoc @@ -2,7 +2,7 @@ === Assignment -Jerry really wants to follow Tom, can you help him to follow Jerry? From a breach of last year the following logfile is available link:images/logs.txt[here] +Can you find a way to order the books but let *Tom* pay for them? diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTRefreshEndpointTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTRefreshEndpointTest.java new file mode 100644 index 000000000..045601062 --- /dev/null +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTRefreshEndpointTest.java @@ -0,0 +1,102 @@ +package org.owasp.webgoat.plugin; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.Maps; +import org.hamcrest.CoreMatchers; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.owasp.webgoat.plugins.LessonTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Map; + +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.owasp.webgoat.plugin.JWTRefreshEndpoint.PASSWORD; +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 JWTRefreshEndpointTest extends LessonTest { + + @Before + public void setup() { + JWT jwt = new JWT(); + when(webSession.getCurrentLesson()).thenReturn(jwt); + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + when(webSession.getUserName()).thenReturn("unit-test"); + } + + @Test + public void solveAssignment() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + + //First login to obtain tokens for Jerry + Map loginJson = Maps.newHashMap(); + loginJson.put("user", "Jerry"); + loginJson.put("password", PASSWORD); + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginJson))) + .andExpect(status().isOk()) + .andReturn(); + Map tokens = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + String accessToken = tokens.get("access_token"); + String refreshToken = tokens.get("refresh_token"); + + //Now create a new refresh token for Tom based on Toms old access token and send the refresh token of Jerry + String accessTokenTom = "eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q"; + Map refreshJson = Maps.newHashMap(); + refreshJson.put("refresh_token", refreshToken); + result = mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/newToken") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + accessTokenTom) + .content(objectMapper.writeValueAsString(refreshJson))) + .andExpect(status().isOk()) + .andReturn(); + tokens = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + accessTokenTom = tokens.get("access_token"); + + //Now checkout with the new token from Tom + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/checkout") + .header("Authorization", "Bearer " + accessTokenTom)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void checkoutWithTomsTokenFromAccessLogShouldFail() throws Exception { + String accessTokenTom = "eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1MjYxMzE0MTEsImV4cCI6MTUyNjIxNzgxMSwiYWRtaW4iOiJmYWxzZSIsInVzZXIiOiJUb20ifQ.DCoaq9zQkyDH25EcVWKcdbyVfUL4c9D4jRvsqOqvi9iAd4QuqmKcchfbU8FNzeBNF9tLeFXHZLU4yRkq-bjm7Q"; + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/checkout") + .header("Authorization", "Bearer " + accessTokenTom)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.output", CoreMatchers.containsString("JWT expired at"))); + } + + @Test + public void flowForJerryAlwaysWorks() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + + Map loginJson = Maps.newHashMap(); + loginJson.put("user", "Jerry"); + loginJson.put("password", PASSWORD); + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginJson))) + .andExpect(status().isOk()) + .andReturn(); + Map tokens = objectMapper.readValue(result.getResponse().getContentAsString(), Map.class); + String accessToken = tokens.get("access_token"); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/refresh/checkout") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", is("User is not Tom but Jerry, please try again"))); + + } +} \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/TokenTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/TokenTest.java index 498c976de..ab922217e 100644 --- a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/TokenTest.java +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/TokenTest.java @@ -6,6 +6,10 @@ import io.jsonwebtoken.*; import io.jsonwebtoken.impl.TextCodec; import org.junit.Test; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.Period; import java.util.Date; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -34,4 +38,19 @@ public class TokenTest { }).parse(token); } + + @Test + public void testRefresh() { + Instant now = Instant.now(); //current date + Claims claims = Jwts.claims().setIssuedAt(Date.from(now.minus(Duration.ofDays(10)))); + claims.setExpiration(Date.from(now.minus(Duration.ofDays(9)))); + claims.put("admin", "false"); + claims.put("user", "Tom"); + String token = Jwts.builder().setClaims(claims) + .signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, "bm5n3SkxCX4kKRy4") + .compact(); + //Jws jws = Jwts.parser().setSigningKey("bm5n3SkxCX4kKRy4").parseClaimsJws(token); + //Jwts.parser().setSigningKey().parsePlaintextJws(token); + System.out.println(token); + } }