Initial version for JWT

This commit is contained in:
Nanne Baars 2018-04-23 11:09:30 +02:00
parent 63ca11a1bb
commit ea9c1a453d
25 changed files with 690 additions and 35 deletions

3
.gitignore vendored
View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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.
*/ */

View File

@ -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.

View File

@ -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>

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}
}

View File

@ -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 {
}
}

View File

@ -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);
}
}

View 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%;
}

View File

@ -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 </div>
name="SUBMIT" value="Go!" type="SUBMIT"/>
</form>
</div> </div>
</form> </form>
<!-- do not remove the two following div's, this is where your feedback/output will land -->
<br/>
<div class="attack-feedback"></div> <div class="attack-feedback"></div>
<div class="attack-output"></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>
</div> </div>
</html> </html>

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View 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();
}
)
}
}

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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).

View File

@ -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
```

View File

@ -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