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
|
||||
webgoat-server/mongo-data/*
|
||||
webgoat-lessons/vulnerable-components/dependency-reduced-pom.xml
|
||||
**/.sts4-cache/*
|
||||
**/.vscode/*
|
||||
|
||||
/.sonatype
|
@ -2,7 +2,6 @@ package org.owasp.webgoat.lessons;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.owasp.webgoat.session.WebSession;
|
||||
|
||||
/**
|
||||
* <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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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.
|
||||
|
@ -9,4 +9,18 @@
|
||||
<version>v8.0.0.M14</version>
|
||||
</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>
|
||||
|
@ -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">
|
||||
|
||||
<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>
|
||||
<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">
|
||||
<!-- reuse this block for each 'page' of content -->
|
||||
<!-- 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="adoc-content" th:replace="doc:JWT_weak_keys"></div>
|
||||
|
||||
<div class="attack-container">
|
||||
<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 -->
|
||||
<!-- you can write your own custom forms, but standard form submission will take you to your endpoint and outside of the WebGoat framework -->
|
||||
<!-- of course, you can write your own ajax submission /handling in your own javascript if you like -->
|
||||
<form class="attack-form" accept-charset="UNKNOWN"
|
||||
method="POST" name="form"
|
||||
action="/WebGoat/HttpBasics/attack1"
|
||||
enctype="application/json;charset=UTF-8">
|
||||
<div id="lessonContent">
|
||||
<form accept-charset="UNKNOWN" method="POST" name="form"
|
||||
action="#attack/307/100" enctype="">
|
||||
Enter Your Name: <input name="person" value="" type="TEXT"/><input
|
||||
name="SUBMIT" value="Go!" type="SUBMIT"/>
|
||||
</form>
|
||||
<form class="attack-form" method="POST" name="form" action="/WebGoat/JWT/secret">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon"><i class="fa fa-flag-checkered" aria-hidden="true"
|
||||
style="font-size:20px"></i></div>
|
||||
<input type="text" class="form-control" id="flag" name="token"
|
||||
placeholder="XXX.YYY.ZZZ"/>
|
||||
</div>
|
||||
<div class="input-group" style="margin-top: 10px">
|
||||
<button type="submit" class="btn btn-primary">Submit token</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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-output"></div>
|
||||
<!-- ... of course, you can move them if you want to, but that will not look consistent to other lessons -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</html>
|
@ -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
|
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
|
||||
|
||||
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
|
||||
|
@ -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.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
|
||||
|
Loading…
x
Reference in New Issue
Block a user