Initial version for JWT
This commit is contained in:
parent
63ca11a1bb
commit
ea9c1a453d
3
.gitignore
vendored
3
.gitignore
vendored
@ -42,4 +42,7 @@ webgoat-lessons/**/target
|
|||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
webgoat-server/mongo-data/*
|
webgoat-server/mongo-data/*
|
||||||
webgoat-lessons/vulnerable-components/dependency-reduced-pom.xml
|
webgoat-lessons/vulnerable-components/dependency-reduced-pom.xml
|
||||||
|
**/.sts4-cache/*
|
||||||
|
**/.vscode/*
|
||||||
|
|
||||||
/.sonatype
|
/.sonatype
|
@ -2,7 +2,6 @@ package org.owasp.webgoat.lessons;
|
|||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.owasp.webgoat.session.WebSession;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>LessonInfoModel class.</p>
|
* <p>LessonInfoModel class.</p>
|
||||||
|
@ -11,8 +11,8 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.HSQLDialect
|
|||||||
spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
|
spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
|
||||||
|
|
||||||
|
|
||||||
logging.level.org.springframework=WARN
|
logging.level.org.springframework=INFO
|
||||||
logging.level.org.springframework.boot.devtools=WARN
|
logging.level.org.springframework.boot.devtools=INFO
|
||||||
logging.level.org.owasp=DEBUG
|
logging.level.org.owasp=DEBUG
|
||||||
logging.level.org.owasp.webgoat=TRACE
|
logging.level.org.owasp.webgoat=TRACE
|
||||||
|
|
||||||
|
@ -6,12 +6,9 @@ import org.owasp.webgoat.assignments.AssignmentPath;
|
|||||||
import org.owasp.webgoat.assignments.AttackResult;
|
import org.owasp.webgoat.assignments.AttackResult;
|
||||||
import org.owasp.webgoat.session.UserSessionData;
|
import org.owasp.webgoat.session.UserSessionData;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by jason on 9/29/17.
|
* Created by jason on 9/29/17.
|
||||||
*/
|
*/
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
A lot of web applications implement no protection against CSRF they are somehow protected by the fact that
|
A lot of web applications implement no protection against CSRF they are somehow protected by the fact that
|
||||||
they only work with `application/json` as content type. The only way to make a request with this content-type from the
|
they only work with `application/json` as content type. The only way to make a request with this content-type from the
|
||||||
browser is with a XHR request. Before the browser can make such a request a preflight request will be made towards
|
browser is with a XHR request. Before the browser can make such a request a preflight request will be made towards
|
||||||
the server (remember the CSRF request will be cross origin). If the preflight response does not allow the cross origin
|
the server (remember the CSRF request will be cross origin). If the pre-flight response does not allow the cross origin
|
||||||
request the browser will not make the call.
|
request the browser will not make the call.
|
||||||
|
|
||||||
To make a long answer short: this is *not* a valid protection against CSRF.
|
To make a long answer short: this is *not* a valid protection against CSRF.
|
||||||
|
@ -9,4 +9,18 @@
|
|||||||
<version>v8.0.0.M14</version>
|
<version>v8.0.0.M14</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt</artifactId>
|
||||||
|
<version>0.7.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<version>4.1.3.RELEASE</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
package org.owasp.webgoat.plugin;
|
||||||
|
|
||||||
|
import org.owasp.webgoat.assignments.AssignmentEndpoint;
|
||||||
|
import org.owasp.webgoat.assignments.AssignmentHints;
|
||||||
|
import org.owasp.webgoat.assignments.AssignmentPath;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author nbaars
|
||||||
|
* @since 4/23/17.
|
||||||
|
*/
|
||||||
|
@AssignmentPath("/JWT/secret")
|
||||||
|
@AssignmentHints({"jwt-secret-hint1", "jwt-secret-hint2", "jwt-secret-hint3", "jwt-secret-hint4", "jwt-secret-hint5"})
|
||||||
|
public class JWTSecretKeyEndpoint extends AssignmentEndpoint {
|
||||||
|
|
||||||
|
private static final String JWT_SECRET = "victory";
|
||||||
|
private static final String WEBGOAT_USER = "WebGoat";
|
||||||
|
|
||||||
|
@PostMapping()
|
||||||
|
public void login(@RequestParam String token) {
|
||||||
|
try {
|
||||||
|
Jwt jwt = Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJwt(token);
|
||||||
|
Claims claims = (Claims) jwt.getBody();
|
||||||
|
String user = (String) claims.get("username");
|
||||||
|
|
||||||
|
if (WEBGOAT_USER.equalsIgnoreCase(user)) {
|
||||||
|
trackProgress(success().build());
|
||||||
|
} else {
|
||||||
|
trackProgress(failed().feedback("jwt-secret.not-correct").feedbackArgs(user).build());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
trackProgress(failed().feedback("jwt-invalid-token").output(e.getMessage()).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
package org.owasp.webgoat.plugin;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwt;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
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.plugin.votes.Views;
|
||||||
|
import org.owasp.webgoat.plugin.votes.Vote;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import javax.servlet.http.Cookie;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static java.util.Comparator.comparingLong;
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author nbaars
|
||||||
|
* @since 4/23/17.
|
||||||
|
*/
|
||||||
|
@AssignmentPath("/JWT/votings")
|
||||||
|
@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";
|
||||||
|
private static String validUsers = "TomJerrySylvester";
|
||||||
|
|
||||||
|
private static int totalVotes = 38929;
|
||||||
|
private Map<String, Vote> votes = Maps.newHashMap();
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initVotes() {
|
||||||
|
votes.put("Admin lost password", new Vote("Admin lost password",
|
||||||
|
"In this challenge you will need to help the admin and find the password in order to login",
|
||||||
|
"challenge1-small.png", "challenge1.png", 36000, totalVotes));
|
||||||
|
votes.put("Vote for your favourite",
|
||||||
|
new Vote("Vote for your favourite",
|
||||||
|
"In this challenge ...",
|
||||||
|
"challenge5-small.png", "challenge5.png", 30000, totalVotes));
|
||||||
|
votes.put("Get it for free",
|
||||||
|
new Vote("Get it for free",
|
||||||
|
"The objective for this challenge is to buy a Samsung phone for free.",
|
||||||
|
"challenge2-small.png", "challenge2.png", 20000, totalVotes));
|
||||||
|
votes.put("Photo comments",
|
||||||
|
new Vote("Photo comments",
|
||||||
|
"n this challenge you can comment on the photo you will need to find the flag somewhere.",
|
||||||
|
"challenge3-small.png", "challenge3.png", 10000, totalVotes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/login")
|
||||||
|
public void login(@RequestParam("user") String user, HttpServletResponse response) {
|
||||||
|
if (validUsers.contains(user)) {
|
||||||
|
Map<String, Object> 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();
|
||||||
|
Cookie cookie = new Cookie("access_token", token);
|
||||||
|
response.addCookie(cookie);
|
||||||
|
response.setStatus(HttpStatus.OK.value());
|
||||||
|
} else {
|
||||||
|
Cookie cookie = new Cookie("access_token", "");
|
||||||
|
response.addCookie(cookie);
|
||||||
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@ResponseBody
|
||||||
|
public MappingJacksonValue getVotes(@CookieValue(value = "access_token", required = false) String accessToken) {
|
||||||
|
MappingJacksonValue value = new MappingJacksonValue(votes.values().stream().sorted(comparingLong(Vote::getAverage).reversed()).collect(toList()));
|
||||||
|
if (StringUtils.isEmpty(accessToken)) {
|
||||||
|
value.setSerializationView(Views.GuestView.class);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
value.setSerializationView(isAdmin ? Views.AdminView.class : Views.UserView.class);
|
||||||
|
} catch (JwtException e) {
|
||||||
|
value.setSerializationView(Views.GuestView.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "{title}")
|
||||||
|
@ResponseBody
|
||||||
|
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||||
|
public ResponseEntity<?> vote(@PathVariable String title, @CookieValue(value = "access_token", required = false) String accessToken) {
|
||||||
|
if (StringUtils.isEmpty(accessToken)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
|
||||||
|
Claims claims = (Claims) jwt.getBody();
|
||||||
|
String user = (String) claims.get("user");
|
||||||
|
if (!validUsers.contains(user)) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
} else {
|
||||||
|
ofNullable(votes.get(title)).ifPresent(v -> v.incrementNumberOfVotes(totalVotes));
|
||||||
|
return ResponseEntity.accepted().build();
|
||||||
|
}
|
||||||
|
} catch (JwtException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("reset")
|
||||||
|
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 {
|
||||||
|
try {
|
||||||
|
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parse(accessToken);
|
||||||
|
Claims claims = (Claims) jwt.getBody();
|
||||||
|
boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
|
||||||
|
if (!isAdmin) {
|
||||||
|
votes.values().forEach(vote -> vote.reset());
|
||||||
|
return trackProgress(failed().feedback("jwt-only-admin").build());
|
||||||
|
} else {
|
||||||
|
votes.values().forEach(vote -> vote.reset());
|
||||||
|
return trackProgress(success().build());
|
||||||
|
}
|
||||||
|
} catch (JwtException e) {
|
||||||
|
return trackProgress(failed().feedback("jwt-invalid-token").output(e.toString()).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package org.owasp.webgoat.plugin.votes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author nbaars
|
||||||
|
* @since 4/30/17.
|
||||||
|
*/
|
||||||
|
public class Views {
|
||||||
|
public interface GuestView {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface UserView extends GuestView {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface AdminView extends UserView {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package org.owasp.webgoat.plugin.votes;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonView;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author nbaars
|
||||||
|
* @since 5/2/17.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class Vote {
|
||||||
|
@JsonView(Views.GuestView.class)
|
||||||
|
private final String title;
|
||||||
|
@JsonView(Views.GuestView.class)
|
||||||
|
private final String information;
|
||||||
|
@JsonView(Views.GuestView.class)
|
||||||
|
private final String imageSmall;
|
||||||
|
@JsonView(Views.GuestView.class)
|
||||||
|
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)
|
||||||
|
private long average = 0;
|
||||||
|
|
||||||
|
|
||||||
|
public Vote(String title, String information, String imageSmall, String imageBig, int numberOfVotes, int totalVotes) {
|
||||||
|
this.title = title;
|
||||||
|
this.information = information;
|
||||||
|
this.imageSmall = imageSmall;
|
||||||
|
this.imageBig = imageBig;
|
||||||
|
this.numberOfVotes = numberOfVotes;
|
||||||
|
this.average = calculateStars(totalVotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void incrementNumberOfVotes(int totalVotes) {
|
||||||
|
this.numberOfVotes = this.numberOfVotes + 1;
|
||||||
|
this.average = calculateStars(totalVotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
this.numberOfVotes = 1;
|
||||||
|
this.average = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long calculateStars(int totalVotes) {
|
||||||
|
return Math.round(((double) numberOfVotes / (double) totalVotes) * 4);
|
||||||
|
}
|
||||||
|
}
|
12
webgoat-lessons/jwt/src/main/resources/css/jwt.css
Normal file
12
webgoat-lessons/jwt/src/main/resources/css/jwt.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
a.list-group-item {
|
||||||
|
height:auto;
|
||||||
|
}
|
||||||
|
a.list-group-item.active small {
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
.stars {
|
||||||
|
margin:20px auto 1px;
|
||||||
|
}
|
||||||
|
.img-responsive {
|
||||||
|
min-width: 100%;
|
||||||
|
}
|
@ -3,40 +3,102 @@
|
|||||||
<html xmlns:th="http://www.thymeleaf.org">
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
|
||||||
<div class="lesson-page-wrapper">
|
<div class="lesson-page-wrapper">
|
||||||
<!-- reuse this lesson-page-wrapper block for each 'page' of content in your lesson -->
|
|
||||||
<!-- include content here, or can be placed in another location. Content will be presented via asciidocs files,
|
|
||||||
which you put in src/main/resources/lessonplans/{lang}/{fileName}.adoc -->
|
|
||||||
<div class="adoc-content" th:replace="doc:JWT_plan.adoc"></div>
|
<div class="adoc-content" th:replace="doc:JWT_plan.adoc"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="lesson-page-wrapper">
|
||||||
|
<div class="adoc-content" th:replace="doc:JWT_structure.adoc"></div>
|
||||||
|
</div>
|
||||||
|
<div class="lesson-page-wrapper">
|
||||||
|
<div class="adoc-content" th:replace="doc:JWT_login_to_token.adoc"></div>
|
||||||
|
</div>
|
||||||
|
<div class="lesson-page-wrapper">
|
||||||
|
<div class="adoc-content" th:replace="doc:JWT_signing.adoc"></div>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" th:href="@{/lesson_css/jwt.css}"/>
|
||||||
|
<script th:src="@{/lesson_js/bootstrap.min.js}" language="JavaScript"></script>
|
||||||
|
<script th:src="@{/lesson_js/jwt-signing.js}" language="JavaScript"></script>
|
||||||
|
<div class="attack-container">
|
||||||
|
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||||
|
<form class="attack-form" accept-charset="UNKNOWN"
|
||||||
|
method="POST"
|
||||||
|
successCallback="jwtSigningCallback"
|
||||||
|
action="/WebGoat/JWT/votings/reset"
|
||||||
|
enctype="application/json;charset=UTF-8">
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="well">
|
||||||
|
<div class="pull-right">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button type="button" data-toggle="dropdown" class="btn btn-default dropdown-toggle"
|
||||||
|
title="Change user">
|
||||||
|
<i class="fa fa-user"></i> <span class="caret"></span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-left">
|
||||||
|
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||||
|
onclick="javascript:login('Guest')"
|
||||||
|
th:text="Guest">current</a></li>
|
||||||
|
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||||
|
onclick="javascript:login('Tom')"
|
||||||
|
th:text="Tom">current</a></li>
|
||||||
|
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||||
|
onclick="javascript:login('Jerry')"
|
||||||
|
th:text="Jerry">current</a></li>
|
||||||
|
<li role="presentation"><a role="menuitem" tabindex="-1"
|
||||||
|
onclick="javascript:login('Sylvester')"
|
||||||
|
th:text="Sylvester">current</a></li>
|
||||||
|
</ul>
|
||||||
|
<button type="button" class="btn btn-default fa fa-refresh" title="Refresh votes"
|
||||||
|
onclick="javascript:getVotings()"/>
|
||||||
|
<button type="submit" class="btn btn-default fa fa-trash-o" title="Reset votes"/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-right">Welcome back, <b><span id="name"></span></b></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3>Vote for your favorite</h3>
|
||||||
|
</div>
|
||||||
|
<div id="votesList" class="list-group">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<div class="attack-feedback"></div>
|
||||||
|
<div class="attack-output"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="lesson-page-wrapper">
|
<div class="lesson-page-wrapper">
|
||||||
<!-- reuse this block for each 'page' of content -->
|
<div class="adoc-content" th:replace="doc:JWT_weak_keys"></div>
|
||||||
<!-- sample ascii doc content for second page -->
|
|
||||||
<div class="adoc-content" th:replace="doc:JWT_content1.adoc"></div>
|
|
||||||
<!-- if including attack, reuse this section, leave classes in place -->
|
|
||||||
<div class="attack-container">
|
<div class="attack-container">
|
||||||
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
|
||||||
<!-- using attack-form class on your form will allow your request to be ajaxified and stay within the display framework for webgoat -->
|
<form class="attack-form" method="POST" name="form" action="/WebGoat/JWT/secret">
|
||||||
<!-- you can write your own custom forms, but standard form submission will take you to your endpoint and outside of the WebGoat framework -->
|
<div class="form-group">
|
||||||
<!-- of course, you can write your own ajax submission /handling in your own javascript if you like -->
|
<div class="input-group">
|
||||||
<form class="attack-form" accept-charset="UNKNOWN"
|
<div class="input-group-addon"><i class="fa fa-flag-checkered" aria-hidden="true"
|
||||||
method="POST" name="form"
|
style="font-size:20px"></i></div>
|
||||||
action="/WebGoat/HttpBasics/attack1"
|
<input type="text" class="form-control" id="flag" name="token"
|
||||||
enctype="application/json;charset=UTF-8">
|
placeholder="XXX.YYY.ZZZ"/>
|
||||||
<div id="lessonContent">
|
</div>
|
||||||
<form accept-charset="UNKNOWN" method="POST" name="form"
|
<div class="input-group" style="margin-top: 10px">
|
||||||
action="#attack/307/100" enctype="">
|
<button type="submit" class="btn btn-primary">Submit token</button>
|
||||||
Enter Your Name: <input name="person" value="" type="TEXT"/><input
|
|
||||||
name="SUBMIT" value="Go!" type="SUBMIT"/>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
<!-- do not remove the two following div's, this is where your feedback/output will land -->
|
|
||||||
<div class="attack-feedback"></div>
|
|
||||||
<div class="attack-output"></div>
|
|
||||||
<!-- ... of course, you can move them if you want to, but that will not look consistent to other lessons -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<div class="attack-feedback"></div>
|
||||||
|
<div class="attack-output"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -1 +1,11 @@
|
|||||||
jwt.title=JWT tokens (Under development)
|
jwt.title=JWT tokens (Under development)
|
||||||
|
|
||||||
|
#Assignment changing tokens
|
||||||
|
jwt-user=You are logged in as {0}, but you are not an admin yet, please try again
|
||||||
|
jwt-invalid-token=Not a valid JWT token, please try again
|
||||||
|
jwt-only-admin=Only an admin user can reset the votes
|
||||||
|
jwt-change-token-hint1=Select a different user and look at the token you receive back, use the delete button to reset the votes count
|
||||||
|
jwt-change-token-hint2=Decode the token and look at the contents
|
||||||
|
jwt-change-token-hint3=Change the contents of the token and replace the cookie before sending the request for getting the votes
|
||||||
|
jwt-change-token-hint4=Change the admin field to true in the token
|
||||||
|
jwt-change-token-hint5=Submit the token by changing the algorithm to None and remove the signature
|
BIN
webgoat-lessons/jwt/src/main/resources/images/jwt_diagram.png
Normal file
BIN
webgoat-lessons/jwt/src/main/resources/images/jwt_diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
BIN
webgoat-lessons/jwt/src/main/resources/images/jwt_token.png
Normal file
BIN
webgoat-lessons/jwt/src/main/resources/images/jwt_token.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
87
webgoat-lessons/jwt/src/main/resources/js/jwt-signing.js
Normal file
87
webgoat-lessons/jwt/src/main/resources/js/jwt-signing.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
$(document).ready(function () {
|
||||||
|
login('Guest');
|
||||||
|
})
|
||||||
|
|
||||||
|
function login(user) {
|
||||||
|
$("#name").text(user);
|
||||||
|
$.ajax({
|
||||||
|
url: "JWT/votings/login?user=" + user,
|
||||||
|
complete: function (result, status) {
|
||||||
|
getVotings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<a href="#" class="list-group-item ACTIVE">' +
|
||||||
|
'<div class="media col-md-3">' +
|
||||||
|
'<figure> ' +
|
||||||
|
'<img class="media-object img-rounded" src="images/IMAGE_SMALL" alt="placehold.it/350x250"/>' +
|
||||||
|
'</figure>' +
|
||||||
|
'</div> ' +
|
||||||
|
'<div class="col-md-6">' +
|
||||||
|
'<h4 class="list-group-item-heading">TITLE</h4>' +
|
||||||
|
'<p class="list-group-item-text">INFORMATION</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="col-md-3 text-center">' +
|
||||||
|
'<h2 HIDDEN_VIEW_VOTES>NO_VOTES' +
|
||||||
|
'<small HIDDEN_VIEW_VOTES> votes</small>' +
|
||||||
|
'</h2>' +
|
||||||
|
'<button type="button" id="TITLE" class="btn BUTTON btn-lg btn-block" onclick="vote(this.id)">Vote Now!</button>' +
|
||||||
|
'<div style="visibility:HIDDEN_VIEW_RATING;" class="stars"> ' +
|
||||||
|
'<span class="glyphicon glyphicon-star"></span>' +
|
||||||
|
'<span class="glyphicon glyphicon-star"></span>' +
|
||||||
|
'<span class="glyphicon glyphicon-star"></span>' +
|
||||||
|
'<span class="glyphicon glyphicon-star-empty"></span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<p HIDDEN_VIEW_RATING>Average AVERAGE<small> /</small>4</p>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="clearfix"></div>' +
|
||||||
|
'</a>';
|
||||||
|
|
||||||
|
function getVotings() {
|
||||||
|
$("#votesList").empty();
|
||||||
|
$.get("JWT/votings", function (result, status) {
|
||||||
|
for (var i = 0; i < result.length; i++) {
|
||||||
|
var voteTemplate = html.replace('IMAGE_SMALL', result[i].imageSmall);
|
||||||
|
if (i === 0) {
|
||||||
|
voteTemplate = voteTemplate.replace('ACTIVE', 'active');
|
||||||
|
voteTemplate = voteTemplate.replace('BUTTON', 'btn-default');
|
||||||
|
} else {
|
||||||
|
voteTemplate = voteTemplate.replace('ACTIVE', '');
|
||||||
|
voteTemplate = voteTemplate.replace('BUTTON', 'btn-primary');
|
||||||
|
}
|
||||||
|
voteTemplate = voteTemplate.replace(/TITLE/g, result[i].title);
|
||||||
|
voteTemplate = voteTemplate.replace('INFORMATION', result[i].information || '');
|
||||||
|
voteTemplate = voteTemplate.replace('NO_VOTES', result[i].numberOfVotes || '');
|
||||||
|
voteTemplate = voteTemplate.replace('AVERAGE', result[i].average || '');
|
||||||
|
|
||||||
|
var hidden = (result[i].numberOfVotes === undefined ? 'hidden' : '');
|
||||||
|
voteTemplate = voteTemplate.replace(/HIDDEN_VIEW_VOTES/g, hidden);
|
||||||
|
hidden = (result[i].average === undefined ? 'hidden' : '');
|
||||||
|
voteTemplate = voteTemplate.replace(/HIDDEN_VIEW_RATING/g, hidden);
|
||||||
|
|
||||||
|
$("#votesList").append(voteTemplate);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
webgoat.customjs.jwtSigningCallback = function() {
|
||||||
|
getVotings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function vote(title) {
|
||||||
|
var user = $("#name").text();
|
||||||
|
if (user === 'Guest') {
|
||||||
|
alert("As a guest you are not allowed to vote, please login first.")
|
||||||
|
} else {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: 'JWT/votings/' + title
|
||||||
|
}).then(
|
||||||
|
function () {
|
||||||
|
getVotings();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +0,0 @@
|
|||||||
== Test
|
|
@ -0,0 +1,19 @@
|
|||||||
|
== Authentication and getting a JWT token
|
||||||
|
|
||||||
|
A basic sequence of getting a token is as follows:
|
||||||
|
|
||||||
|
image::images/jwt_diagram.png[style="lesson-image"]
|
||||||
|
|
||||||
|
{nbsp} +
|
||||||
|
|
||||||
|
In this flow you can see the user logs in with a username and password on a successful authentication the server
|
||||||
|
returns. The server creates a new token and returns this one to the client. When the client makes a successive
|
||||||
|
call toward the server it attaches the new token in the "Authorization" header.
|
||||||
|
The server reads the token and first validates the signature after a successful verification the server uses the
|
||||||
|
information in the token to identify the user.
|
||||||
|
|
||||||
|
=== Claims
|
||||||
|
|
||||||
|
The token contains claims to identify the user and all other information necessary for the server to fulfil the request.
|
||||||
|
Be aware not to store sensitive information in the token and always send them over a secure channel.
|
||||||
|
|
@ -7,14 +7,13 @@ This lesson teaches about using JSON Web Tokens (JWT) for authentication and the
|
|||||||
|
|
||||||
== Goals
|
== Goals
|
||||||
|
|
||||||
Teach how to securely implement the usage of tokens.
|
Teach how to securely implement the usage of tokens and validation of those tokens.
|
||||||
|
|
||||||
== Introduction
|
== Introduction
|
||||||
|
|
||||||
Many application use JSON Web Tokens (JWT) to allow the client to indicate is identity for further exchange after authentication.
|
Many application use JSON Web Tokens (JWT) to allow the client to indicate is identity for further exchange after authentication.
|
||||||
|
|
||||||
From https://jwt.io/introduction:
|
From https://jwt.io/introduction:
|
||||||
|
|
||||||
-------------------------------------------------------
|
-------------------------------------------------------
|
||||||
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact
|
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact
|
||||||
and self-contained way for securely transmitting
|
and self-contained way for securely transmitting
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
== Refreshing a token
|
||||||
|
|
||||||
|
=== Introduction
|
||||||
|
|
||||||
|
In this section we touch upon refreshing an access token. There are many solutions some might
|
||||||
|
|
||||||
|
=== Types of tokens
|
||||||
|
|
||||||
|
In general there are two type of tokens: access token and a refresh token. The access token is used for making API
|
||||||
|
calls towards the server. Access tokens have a limited life span, that's where the refresh token comes in. Once
|
||||||
|
the access token is no longer valid a request can me made towards the server to get a new access token by presenting
|
||||||
|
the refresh token. The refresh token can expire but their life span is much longer. This solves the problem of a user
|
||||||
|
having to authenticate again with their credentials. Whether you should use a refresh token and access token depends
|
||||||
|
below can find a couple of points to keep in mind while choosing which tokens to use.
|
||||||
|
|
||||||
|
So a normal flow can look like:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -X POST -H -d 'username=webgoat&password=webgoat' localhost:8080/WebGoat/login
|
||||||
|
```
|
||||||
|
|
||||||
|
The server returns:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"token_type":"bearer",
|
||||||
|
"access_token":"XXXX.YYYY.ZZZZ",
|
||||||
|
"expires_in":10,
|
||||||
|
"refresh_token":"4a9a0b1eac1a34201b3c5659944e8b7"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see the refresh token is a random string which the server can keep track of (in memory or store in a database)
|
||||||
|
With storing the information you can match the refresh token to the specific user to which the refresh token was
|
||||||
|
granted to. So in this case whenever the access token is still valid we can speak of a "stateless" session, there is
|
||||||
|
no burden on the server side to setup the user session, the token is self contained.
|
||||||
|
When the access token is no longer valid the server needs to query for the stored refresh token to make sure the token
|
||||||
|
is not blocked in any way.
|
||||||
|
|
||||||
|
Whenever the attacker gets a hold on an access token it is only valid for a certain amount of time (say 10 minutes). The
|
||||||
|
attacker then needs the refresh token to get a new access token. That is why the refresh token needs better protection.
|
||||||
|
|
||||||
|
It is also possible to make the refresh token stateless but this means it will become more difficult to see if
|
||||||
|
the user revoked the tokens.
|
||||||
|
|
||||||
|
After the server made all the validations it must return a new refresh token and a new access token to the client. The
|
||||||
|
client can use the new access token to make the API call.
|
||||||
|
|
||||||
|
|
||||||
|
=== What should you check for?
|
||||||
|
|
||||||
|
Regardless of the chosen solution you should store enough information on the server side to validate whether the user
|
||||||
|
is still trusted. You can think of many things, like store the ip address, keep track of how many times the refresh
|
||||||
|
token is used (using the refresh token multiple times in the valid time window of the access token might indicate strange
|
||||||
|
behavior, you can revoke all the tokens an let the user authenticate again).
|
||||||
|
|
||||||
|
It is also a good to keep track of which access token belonged to which refresh token. Otherwise an attacker might
|
||||||
|
be able to get a new access token for a different user with the refresh token of the attacker
|
||||||
|
(see https://emtunc.org/blog/11/2017/jwt-refresh-token-manipulation/ for a nice write up about how this attack works)
|
||||||
|
|
||||||
|
Also a good thing to check for is the ip address or geolocation of the user. If you need to give out a new token check
|
||||||
|
whether the location is still the same if not revoke all the tokens and let the user authenticate again.
|
||||||
|
|
||||||
|
=== Need for refresh tokens
|
||||||
|
|
||||||
|
Does it make sense to use a refresh token in a modern single page application (SPA)? As we have seen in the section
|
||||||
|
about storing tokens there are two option: web storage or a cookie which mean a refresh token is right beside an
|
||||||
|
access token, so if the access token is leaked changes are the refresh token will also be compromised. Most of the time
|
||||||
|
there is a difference of course, the access token is send when you make an API call, the refresh token is only send
|
||||||
|
when a new access token should be obtained, which in most cases is a different endpoint. If you end up on the same
|
||||||
|
server you can chose to only use the access token.
|
||||||
|
|
||||||
|
As stated above using an access token and a separate refresh token gives some leverage for the server not to check
|
||||||
|
the access token over and over. Only perform the check when the user needs a new access token.
|
||||||
|
|
||||||
|
It is certainly possible to only use an access token, at the server you store the exact same information you would
|
||||||
|
store for a refresh token, see previous paragraph. This way you need to check the token each time but this might
|
||||||
|
be suitable depending on the application.
|
||||||
|
|
||||||
|
In the case the refresh tokens are stored for validation it is important to protect these tokens as well (at least
|
||||||
|
use a hash function to store them in your database).
|
||||||
|
Another check is to make use there is only one access token
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
|||||||
|
== JWT signing
|
||||||
|
|
||||||
|
Each JWT token should at least be signed before sending it to a client, if a token is not signed the client application
|
||||||
|
would be able to change the contents of the token. The signing specifications are defined https://tools.ietf.org/html/rfc7515[here]
|
||||||
|
the specific algorithms you can use are described https://tools.ietf.org/html/rfc7518[here]
|
||||||
|
|
||||||
|
It basically comes down you use "HMAC with SHA-2 Functions" or "Digital Signature with RSASSA-PKCS1-v1_5/ECDSA/RSASSA-PSS" function
|
||||||
|
for signing the token.
|
||||||
|
|
||||||
|
=== Checking the signature
|
||||||
|
|
||||||
|
One important step is to *verify the signature* before performing any other action, let's try to see some things you need
|
||||||
|
to be aware of before validating the token.
|
||||||
|
|
||||||
|
== Assignment
|
||||||
|
|
||||||
|
Try to change the token you receive and become an admin user by changing the token.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
|||||||
|
== Storing JWT tokens
|
||||||
|
|
||||||
|
When receiving a JWT token you need to store it at the client side. There are basically two options:
|
||||||
|
|
||||||
|
- Store the token in a cookie
|
||||||
|
- Store the token in local/session storage
|
||||||
|
|
||||||
|
=== Cookies
|
||||||
|
|
||||||
|
Cookies is the most simplest form, every browser supports cookies for a long time. A best practise is to mark the
|
||||||
|
cookie with the `HttpOnly` to guarantee scripts cannot read the cookie and with `Secure` to make sure the cookie
|
||||||
|
is only sent over HTTPs.
|
||||||
|
|
||||||
|
Note: using a cookie does not mean you have maintain a state stored on the server, like the old session cookies worked
|
||||||
|
before. The JWT token is self contained and can/should contain all the information necessary to be completely stateless the
|
||||||
|
cookie is just used as the transport mechanism.
|
||||||
|
|
||||||
|
=== Web storage
|
||||||
|
|
||||||
|
In this case you store the token in on the client side in HTML5 Web Storage.
|
||||||
|
|
||||||
|
=== Choices, security risks
|
||||||
|
|
||||||
|
Web storage is accessible through JavaScript running on the same domain, so the script will have access to the
|
||||||
|
web storage. So if the site is vulnerable to a cross-site scripting attack the script is able to read the token
|
||||||
|
from the web storage. See XSS lesson for more about how this attack works.
|
||||||
|
|
||||||
|
On the other hand using cookies have a different problem namely they are vulnerable to a cross-site request forgery
|
||||||
|
attack. In this case the attacker tries to invoke an action on the website you have a token for. See CSRF lesson for more
|
||||||
|
information about how this attack works.
|
||||||
|
|
||||||
|
The best recommendation is to choose for the cookie based approach. In practise it is easier to defend against a CSRF
|
||||||
|
attack. On the other hand many JavaScript frameworks are protecting the user for a XSS attack by applying the right
|
||||||
|
encoding, this protection comes out of the box. A CSRF protection sometimes is not provided by default and requires work.
|
||||||
|
In the end take a look at what the framework is offering you, but most of the time a XSS attack gives the attacker more leverage.
|
@ -0,0 +1,39 @@
|
|||||||
|
== Structure of a JWT token
|
||||||
|
|
||||||
|
Let's take a look at the structure of a JWT token:
|
||||||
|
|
||||||
|
image::images/jwt_token.png[style="lesson-image"]
|
||||||
|
|
||||||
|
{nbsp} +
|
||||||
|
|
||||||
|
The token is base64 encoded and consists of three parts `header.claims.signature`. The decoded version of this token is:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"alg":"HS256",
|
||||||
|
"typ":"JWT"
|
||||||
|
}
|
||||||
|
.
|
||||||
|
{
|
||||||
|
"exp": 1416471934,
|
||||||
|
"user_name": "user",
|
||||||
|
"scope": [
|
||||||
|
"read",
|
||||||
|
"write"
|
||||||
|
],
|
||||||
|
"authorities": [
|
||||||
|
"ROLE_ADMIN",
|
||||||
|
"ROLE_USER"
|
||||||
|
],
|
||||||
|
"jti": "9bc92a44-0b1a-4c5e-be70-da52075b9a84",
|
||||||
|
"client_id": "my-client-with-secret"
|
||||||
|
}
|
||||||
|
.
|
||||||
|
qxNjYSPIKSURZEMqLQQPw1Zdk6Le2FdGHRYZG7SQnNk
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
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).
|
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
|||||||
|
== JWT cracking
|
||||||
|
|
||||||
|
With the HMAC with SHA-2 Functions you use a secret key to sign and verify the token. Once we figure out this key
|
||||||
|
we can create a new token and sign it. So it is very important the key is strong enough so a brute force or
|
||||||
|
dictionary attack is not feasible. Once you have a token you can start an offline brute force or dictionary attack.
|
||||||
|
|
||||||
|
=== Assignment
|
||||||
|
|
||||||
|
Given we have the following token try to find out secret key and submit a new key with the userId changed to WebGoat.
|
||||||
|
|
||||||
|
```
|
||||||
|
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJXZWJHb2F0IFRva2VuIEJ1aWxkZXIiLCJpYXQiOjE1MjQyMTA5MDQsImV4cCI6MTYxODkwNTMwNCwiYXVkIjoid2ViZ29hdC5vcmciLCJzdWIiOiJ0b21Ad2ViZ29hdC5jb20iLCJ1c2VybmFtZSI6IlRvbSIsIkVtYWlsIjoidG9tQHdlYmdvYXQuY29tIiwiUm9sZSI6WyJNYW5hZ2VyIiwiUHJvamVjdCBBZG1pbmlzdHJhdG9yIl19.m-jSyfYEsVzD3CBI6N39wZ7AcdKdp_GiO7F_Ym12u-0
|
||||||
|
```
|
@ -15,8 +15,6 @@ logging.level.org.springframework=INFO
|
|||||||
logging.level.org.springframework.boot.devtools=WARN
|
logging.level.org.springframework.boot.devtools=WARN
|
||||||
logging.level.org.owasp=DEBUG
|
logging.level.org.owasp=DEBUG
|
||||||
logging.level.org.owasp.webwolf=TRACE
|
logging.level.org.owasp.webwolf=TRACE
|
||||||
logging.level.org.apache.activemq=WARN
|
|
||||||
|
|
||||||
|
|
||||||
endpoints.trace.sensitive=false
|
endpoints.trace.sensitive=false
|
||||||
management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,COOKIES,ERRORS,TIME_TAKEN,PARAMETERS,QUERY_STRING
|
management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,COOKIES,ERRORS,TIME_TAKEN,PARAMETERS,QUERY_STRING
|
||||||
|
Loading…
x
Reference in New Issue
Block a user