diff --git a/.gitignore b/.gitignore
index 917e56a8c..85137d053 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,4 +42,7 @@ webgoat-lessons/**/target
**/.DS_Store
webgoat-server/mongo-data/*
webgoat-lessons/vulnerable-components/dependency-reduced-pom.xml
+**/.sts4-cache/*
+**/.vscode/*
+
/.sonatype
\ No newline at end of file
diff --git a/webgoat-container/src/main/java/org/owasp/webgoat/lessons/LessonInfoModel.java b/webgoat-container/src/main/java/org/owasp/webgoat/lessons/LessonInfoModel.java
index 3a3d0f9f7..4a7bab3a7 100644
--- a/webgoat-container/src/main/java/org/owasp/webgoat/lessons/LessonInfoModel.java
+++ b/webgoat-container/src/main/java/org/owasp/webgoat/lessons/LessonInfoModel.java
@@ -2,7 +2,6 @@ package org.owasp.webgoat.lessons;
import lombok.AllArgsConstructor;
import lombok.Getter;
-import org.owasp.webgoat.session.WebSession;
/**
*
LessonInfoModel class.
diff --git a/webgoat-container/src/main/resources/application.properties b/webgoat-container/src/main/resources/application.properties
index 35b177ddd..431dbba99 100644
--- a/webgoat-container/src/main/resources/application.properties
+++ b/webgoat-container/src/main/resources/application.properties
@@ -11,8 +11,8 @@ spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.HSQLDialect
spring.datasource.driver-class-name=org.hsqldb.jdbc.JDBCDriver
-logging.level.org.springframework=WARN
-logging.level.org.springframework.boot.devtools=WARN
+logging.level.org.springframework=INFO
+logging.level.org.springframework.boot.devtools=INFO
logging.level.org.owasp=DEBUG
logging.level.org.owasp.webgoat=TRACE
diff --git a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFConfirmFlag1.java b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFConfirmFlag1.java
index 03ca8d239..5710a799e 100644
--- a/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFConfirmFlag1.java
+++ b/webgoat-lessons/csrf/src/main/java/org/owasp/webgoat/plugin/CSRFConfirmFlag1.java
@@ -6,12 +6,9 @@ import org.owasp.webgoat.assignments.AssignmentPath;
import org.owasp.webgoat.assignments.AttackResult;
import org.owasp.webgoat.session.UserSessionData;
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.ResponseBody;
-import javax.servlet.http.HttpServletRequest;
-
/**
* Created by jason on 9/29/17.
*/
diff --git a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_JSON.adoc b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_JSON.adoc
index bcf7be949..41e8e3d4c 100644
--- a/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_JSON.adoc
+++ b/webgoat-lessons/csrf/src/main/resources/lessonPlans/en/CSRF_JSON.adoc
@@ -3,7 +3,7 @@
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
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.
To make a long answer short: this is *not* a valid protection against CSRF.
diff --git a/webgoat-lessons/jwt/pom.xml b/webgoat-lessons/jwt/pom.xml
index e03c3385e..a2a5843c5 100644
--- a/webgoat-lessons/jwt/pom.xml
+++ b/webgoat-lessons/jwt/pom.xml
@@ -9,4 +9,18 @@
v8.0.0.M14
+
+
+ io.jsonwebtoken
+ jjwt
+ 0.7.0
+
+
+ org.springframework.security
+ spring-security-test
+ 4.1.3.RELEASE
+ test
+
+
+
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
new file mode 100644
index 000000000..ec99d43b5
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTSecretKeyEndpoint.java
@@ -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());
+ }
+ }
+}
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
new file mode 100644
index 000000000..48419e096
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/JWTVotesEndpoint.java
@@ -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 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 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());
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..4a790c979
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Views.java
@@ -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 {
+ }
+}
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
new file mode 100644
index 000000000..ef79d5cd3
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/java/org/owasp/webgoat/plugin/votes/Vote.java
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/webgoat-lessons/jwt/src/main/resources/css/jwt.css b/webgoat-lessons/jwt/src/main/resources/css/jwt.css
new file mode 100644
index 000000000..590e2a4b0
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/resources/css/jwt.css
@@ -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%;
+}
\ No newline at end of file
diff --git a/webgoat-lessons/jwt/src/main/resources/html/JWT.html b/webgoat-lessons/jwt/src/main/resources/html/JWT.html
index 242452f71..ff4c17f03 100644
--- a/webgoat-lessons/jwt/src/main/resources/html/JWT.html
+++ b/webgoat-lessons/jwt/src/main/resources/html/JWT.html
@@ -3,40 +3,102 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
-
-
-
-
\ No newline at end of file
diff --git a/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties b/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties
index 9b9f75e31..214fa1c5d 100644
--- a/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties
+++ b/webgoat-lessons/jwt/src/main/resources/i18n/WebGoatLabels.properties
@@ -1 +1,11 @@
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
\ No newline at end of file
diff --git a/webgoat-lessons/jwt/src/main/resources/images/jwt_diagram.png b/webgoat-lessons/jwt/src/main/resources/images/jwt_diagram.png
new file mode 100644
index 000000000..cb70a6b66
Binary files /dev/null and b/webgoat-lessons/jwt/src/main/resources/images/jwt_diagram.png differ
diff --git a/webgoat-lessons/jwt/src/main/resources/images/jwt_token.png b/webgoat-lessons/jwt/src/main/resources/images/jwt_token.png
new file mode 100644
index 000000000..43a07c4c8
Binary files /dev/null and b/webgoat-lessons/jwt/src/main/resources/images/jwt_token.png differ
diff --git a/webgoat-lessons/jwt/src/main/resources/js/jwt-signing.js b/webgoat-lessons/jwt/src/main/resources/js/jwt-signing.js
new file mode 100644
index 000000000..389145c4d
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/resources/js/jwt-signing.js
@@ -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 = '' +
+ '
' +
+ '' +
+ '';
+
+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();
+ }
+ )
+ }
+}
+
diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_content1.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_content1.adoc
deleted file mode 100644
index e192587b6..000000000
--- a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_content1.adoc
+++ /dev/null
@@ -1 +0,0 @@
-== Test
\ No newline at end of file
diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_login_to_token.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_login_to_token.adoc
new file mode 100644
index 000000000..0682b666a
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_login_to_token.adoc
@@ -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.
+
diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_plan.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_plan.adoc
index d6c375bb8..ae76a5876 100644
--- a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_plan.adoc
+++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_plan.adoc
@@ -7,14 +7,13 @@ This lesson teaches about using JSON Web Tokens (JWT) for authentication and the
== 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
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:
-
-------------------------------------------------------
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact
and self-contained way for securely transmitting
diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh.adoc
new file mode 100644
index 000000000..5068626ec
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_refresh.adoc
@@ -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
+
+
+
+
diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_signing.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_signing.adoc
new file mode 100644
index 000000000..16e48409e
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_signing.adoc
@@ -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.
+
+
+
+
diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_storing.adoc b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_storing.adoc
new file mode 100644
index 000000000..e1fb92adc
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_storing.adoc
@@ -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.
\ No newline at end of file
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
new file mode 100644
index 000000000..e44aa9079
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_structure.adoc
@@ -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).
+
+
diff --git a/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_weak_keys b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_weak_keys
new file mode 100644
index 000000000..b8da3bf02
--- /dev/null
+++ b/webgoat-lessons/jwt/src/main/resources/lessonPlans/en/JWT_weak_keys
@@ -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
+```
\ No newline at end of file
diff --git a/webwolf/src/main/resources/application.properties b/webwolf/src/main/resources/application.properties
index 2d8f6dded..421665f81 100644
--- a/webwolf/src/main/resources/application.properties
+++ b/webwolf/src/main/resources/application.properties
@@ -15,8 +15,6 @@ logging.level.org.springframework=INFO
logging.level.org.springframework.boot.devtools=WARN
logging.level.org.owasp=DEBUG
logging.level.org.owasp.webwolf=TRACE
-logging.level.org.apache.activemq=WARN
-
endpoints.trace.sensitive=false
management.trace.include=REQUEST_HEADERS,RESPONSE_HEADERS,COOKIES,ERRORS,TIME_TAKEN,PARAMETERS,QUERY_STRING