diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpoint.java index 1ce27e416..5748681f5 100644 --- a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpoint.java +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpoint.java @@ -1,14 +1,19 @@ package org.owasp.webgoat.plugin; +import com.google.common.collect.Lists; 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.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.List; /** * @author nbaars @@ -18,23 +23,29 @@ import io.jsonwebtoken.Jwts; @AssignmentHints({"jwt-secret-hint1", "jwt-secret-hint2", "jwt-secret-hint3"}) public class JWTSecretKeyEndpoint extends AssignmentEndpoint { - private static final String JWT_SECRET = "victory"; + public static final String JWT_SECRET = "victory"; private static final String WEBGOAT_USER = "WebGoat"; + private static final List expectedClaims = Lists.newArrayList("iss", "iat", "exp", "aud", "sub", "username", "Email", "Role"); - @PostMapping() - public void login(@RequestParam String token) { + @PostMapping + @ResponseBody + public AttackResult login(@RequestParam String token) { try { - Jwt jwt = Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJwt(token); + Jwt jwt = Jwts.parser().setSigningKey(JWT_SECRET).parse(token); Claims claims = (Claims) jwt.getBody(); - String user = (String) claims.get("username"); - - if (WEBGOAT_USER.equalsIgnoreCase(user)) { - trackProgress(success().build()); + if (!claims.keySet().containsAll(expectedClaims)) { + return trackProgress(failed().feedback("jwt-secret-claims-missing").build()); } else { - trackProgress(failed().feedback("jwt-secret.not-correct").feedbackArgs(user).build()); + String user = (String) claims.get("username"); + + if (WEBGOAT_USER.equalsIgnoreCase(user)) { + return trackProgress(success().build()); + } else { + return trackProgress(failed().feedback("jwt-secret-incorrect-user").feedbackArgs(user).build()); + } } } catch (Exception e) { - trackProgress(failed().feedback("jwt-invalid-token").output(e.getMessage()).build()); + return trackProgress(failed().feedback("jwt-invalid-token").output(e.getMessage()).build()); } } } diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java index cf1e512db..94f829191 100644 --- a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java @@ -21,6 +21,8 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.PostConstruct; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; +import java.time.Duration; +import java.time.Instant; import java.util.Date; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -37,7 +39,7 @@ import static java.util.stream.Collectors.toList; @AssignmentHints({"jwt-change-token-hint1", "jwt-change-token-hint2", "jwt-change-token-hint3", "jwt-change-token-hint4", "jwt-change-token-hint5"}) public class JWTVotesEndpoint extends AssignmentEndpoint { - private static final String JWT_PASSWORD = "victory"; + public static final String JWT_PASSWORD = "victory"; private static String validUsers = "TomJerrySylvester"; private static int totalVotes = 38929; @@ -65,11 +67,10 @@ public class JWTVotesEndpoint extends AssignmentEndpoint { @GetMapping("/login") public void login(@RequestParam("user") String user, HttpServletResponse response) { if (validUsers.contains(user)) { - Map claims = Maps.newHashMap(); + Claims claims = Jwts.claims().setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(10)))); 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(); @@ -96,11 +97,11 @@ public class JWTVotesEndpoint extends AssignmentEndpoint { Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken); Claims claims = (Claims) jwt.getBody(); String user = (String) claims.get("user"); - boolean isAdmin = Boolean.valueOf((String) claims.get("admin")); if ("Guest".equals(user) || !validUsers.contains(user)) { value.setSerializationView(Views.GuestView.class); + } else { + value.setSerializationView(Views.UserView.class); } - value.setSerializationView(isAdmin ? Views.AdminView.class : Views.UserView.class); } catch (JwtException e) { value.setSerializationView(Views.GuestView.class); } @@ -132,7 +133,8 @@ public class JWTVotesEndpoint extends AssignmentEndpoint { } @PostMapping("reset") - public @ResponseBody AttackResult resetVotes(@CookieValue(value = "access_token", required = false) String accessToken) { + public @ResponseBody + AttackResult resetVotes(@CookieValue(value = "access_token", required = false) String accessToken) { if (StringUtils.isEmpty(accessToken)) { return trackProgress(failed().feedback("jwt-invalid-token").build()); } else { diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/refresh/RefreshEndpoint.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/refresh/RefreshEndpoint.java deleted file mode 100644 index d04cf8ef7..000000000 --- a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/refresh/RefreshEndpoint.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.owasp.webgoat.plugin.refresh; - -public class RefreshEndpoint { - - private static final String TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0OTUyODU3NDUsImV4cCI6MTQ5NTI4NTc0NSwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5vcmciLCJuYW1lIjoiVG9tIiwiZW1haWwiOiJ0b21Ad2ViZ29hdC5jb20iLCJyb2xlIjoiQ2F0In0.NTd3E17JZlVKZk52Wq_AWZ0RDafV5KDRMuJner_zUn4"; - private static final String JWT_KEY = "qwertyuiopasdfghjklzxcvbnm123456"; - - -} diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Views.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Views.java index 4a790c979..591769c5c 100644 --- a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Views.java +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Views.java @@ -10,7 +10,4 @@ public class Views { public interface UserView extends GuestView { } - - public interface AdminView extends UserView { - } } diff --git a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java index ef79d5cd3..d9217d402 100644 --- a/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java +++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java @@ -20,9 +20,6 @@ public class Vote { private final String imageBig; @JsonView(Views.UserView.class) private int numberOfVotes; - @JsonView(Views.AdminView.class) - @Setter - private String flag; @JsonView(Views.UserView.class) private boolean votingAllowed = true; @JsonView(Views.UserView.class) diff --git a/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties index 939de63ae..92cad6316 100644 --- a/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties +++ b/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties @@ -13,11 +13,13 @@ jwt-change-token-hint5=Submit the token by changing the algorithm to None and re 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-secret-claims-missing=You are missing some claims, you should keep all the claims in the token +jwt-secret-incorrect-user=The user is {0}, you need to change it to WebGoat 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-hint4=Use the found access token in the Authorization: Bearer header and use your own 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 diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpointTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpointTest.java new file mode 100644 index 000000000..421857307 --- /dev/null +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpointTest.java @@ -0,0 +1,110 @@ +package org.owasp.webgoat.plugin; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.hamcrest.CoreMatchers; +import org.joda.time.Days; +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.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.Duration; +import java.time.Instant; +import java.util.Date; + +import static io.jsonwebtoken.SignatureAlgorithm.*; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; +import static org.mockito.Mockito.when; +import static org.owasp.webgoat.plugin.JWTSecretKeyEndpoint.JWT_SECRET; +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 JWTSecretKeyEndpointTest 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"); + } + + private Claims createClaims(String username) { + Claims claims = Jwts.claims(); + claims.put("admin", "true"); + claims.put("user", "Tom"); + claims.setExpiration(Date.from(Instant.now().plus(Duration.ofDays(1)))); + claims.setIssuedAt(Date.from(Instant.now().plus(Duration.ofDays(1)))); + claims.setIssuer("iss"); + claims.setAudience("aud"); + claims.setSubject("sub"); + claims.put("username", username); + claims.put("Email", "webgoat@webgoat.io"); + claims.put("Role", new String[]{"user"}); + return claims; + } + + @Test + public void solveAssignment() throws Exception { + Claims claims = createClaims("WebGoat"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, JWT_SECRET).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void solveAssignmentWithLowercase() throws Exception { + Claims claims = createClaims("webgoat"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, JWT_SECRET).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void oneOfClaimIsMissingShouldNotSolveAssignment() throws Exception { + Claims claims = createClaims("WebGoat"); + claims.remove("aud"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, JWT_SECRET).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-secret-claims-missing")))); + } + + @Test + public void incorrectUser() throws Exception { + Claims claims = createClaims("Tom"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, JWT_SECRET).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-secret-incorrect-user", "default", "Tom")))); + } + + @Test + public void incorrectToken() throws Exception { + Claims claims = createClaims("Tom"); + String token = Jwts.builder().setClaims(claims).signWith(HS512, "wrong_password").compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/secret") + .param("token", token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-invalid-token")))); + } +} \ No newline at end of file diff --git a/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTVotesEndpointTest.java b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTVotesEndpointTest.java new file mode 100644 index 000000000..9c90c1678 --- /dev/null +++ b/webgoat-lessons/jwt/src/test/java/org/owasp/webgoat/plugin/JWTVotesEndpointTest.java @@ -0,0 +1,187 @@ +package org.owasp.webgoat.plugin; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +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 javax.servlet.http.Cookie; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.owasp.webgoat.plugin.JWTVotesEndpoint.JWT_PASSWORD; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie; +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 JWTVotesEndpointTest 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 { + //Create new token and set alg to none and do not sign it + Claims claims = Jwts.claims(); + claims.put("admin", "true"); + claims.put("user", "Tom"); + String token = Jwts.builder().setClaims(claims).setHeaderParam("alg", "none").compact(); + + //Call the reset endpoint + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/reset") + .contentType(MediaType.APPLICATION_JSON) + .cookie(new Cookie("access_token", token))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.lessonCompleted", is(true))); + } + + @Test + public void resetWithoutTokenShouldNotWork() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/reset") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.feedback", CoreMatchers.is(messages.getMessage("jwt-invalid-token")))); + } + + @Test + public void guestShouldNotGetAToken() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/login") + .contentType(MediaType.APPLICATION_JSON) + .param("user", "Guest")) + .andExpect(status().isUnauthorized()).andExpect(cookie().value("access_token", "")); + } + + @Test + public void tomShouldGetAToken() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/login") + .contentType(MediaType.APPLICATION_JSON) + .param("user", "Tom")) + .andExpect(status().isOk()).andExpect(cookie().value("access_token", containsString("eyJhbGciOiJIUzUxMiJ9."))); + } + + @Test + public void guestShouldNotSeeNumberOfVotes() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(new Cookie("access_token", ""))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].numberOfVotes").doesNotExist()) + .andExpect(jsonPath("$[0].votingAllowed").doesNotExist()) + .andExpect(jsonPath("$[0].average").doesNotExist()); + } + + @Test + public void tomShouldSeeNumberOfVotes() throws Exception { + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/login") + .contentType(MediaType.APPLICATION_JSON) + .param("user", "Tom")) + .andExpect(status().isOk()).andReturn(); + + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(result.getResponse().getCookies()[0])) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].numberOfVotes").exists()) + .andExpect(jsonPath("$[0].votingAllowed").exists()) + .andExpect(jsonPath("$[0].average").exists()); + } + + @Test + public void invalidTokenShouldSeeGuestView() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(new Cookie("access_token", "abcd.efgh.ijkl"))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].numberOfVotes").doesNotExist()) + .andExpect(jsonPath("$[0].votingAllowed").doesNotExist()) + .andExpect(jsonPath("$[0].average").doesNotExist()); + } + + @Test + public void tomShouldBeAbleToVote() throws Exception { + MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/login") + .contentType(MediaType.APPLICATION_JSON) + .param("user", "Tom")) + .andExpect(status().isOk()).andReturn(); + Cookie cookie = result.getResponse().getCookie("access_token"); + + result = mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(cookie)) + .andExpect(status().isOk()).andDo(print()).andReturn(); + Object[] nodes = new ObjectMapper().readValue(result.getResponse().getContentAsString(), Object[].class); + int currentNumberOfVotes = (int) findNodeByTitle(nodes, "Admin lost password").get("numberOfVotes"); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/Admin lost password") + .cookie(cookie)) + .andExpect(status().isAccepted()); + result = mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings") + .cookie(cookie)) + .andExpect(status().isOk()).andReturn(); + nodes = new ObjectMapper().readValue(result.getResponse().getContentAsString(), Object[].class); + int numberOfVotes = (int) findNodeByTitle(nodes, "Admin lost password").get("numberOfVotes"); + assertThat(numberOfVotes).isEqualTo(currentNumberOfVotes + 1); + } + + private Map findNodeByTitle(Object[] nodes, String title) { + for (Object n : nodes) { + Map node = (Map) n; + if (node.get("title").equals(title)) { + return node; + } + } + return null; + } + + @Test + public void guestShouldNotBeAbleToVote() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/Admin lost password") + .cookie(new Cookie("access_token", ""))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void unknownUserWithValidTokenShouldNotBeAbleToVote() throws Exception { + Claims claims = Jwts.claims(); + claims.put("admin", "true"); + claims.put("user", "Intruder"); + String token = Jwts.builder().signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD).setClaims(claims).compact(); + + mockMvc.perform(MockMvcRequestBuilders.post("/JWT/votings/Admin lost password") + .cookie(new Cookie("access_token", token))) + .andExpect(status().isUnauthorized()); + } + + @Test + public void unknownUserShouldSeeGuestView() throws Exception { + Claims claims = Jwts.claims(); + claims.put("admin", "true"); + claims.put("user", "Intruder"); + String token = Jwts.builder().signWith(io.jsonwebtoken.SignatureAlgorithm.HS512, JWT_PASSWORD).setClaims(claims).compact(); + + mockMvc.perform(MockMvcRequestBuilders.get("/JWT/votings/") + .cookie(new Cookie("access_token", token))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].numberOfVotes").doesNotExist()) + .andExpect(jsonPath("$[0].votingAllowed").doesNotExist()) + .andExpect(jsonPath("$[0].average").doesNotExist()); + } + + +} \ No newline at end of file