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

Welcome back,

+
+
+ +
+

Vote for your favorite

+
+
+ +
+
+
+
+
+ +
+
+
+
+
- - -
- +
+
- - - -
-
- - Enter Your Name: - +
+
+
+
+ +
+
+ +
+
- + +
-
-
\ 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 = '' + + '
' + + '
' + + 'placehold.it/350x250' + + '
' + + '
' + + '
' + + '

TITLE

' + + '

INFORMATION

' + + '
' + + '
' + + '

NO_VOTES' + + ' votes' + + '

' + + '' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '

Average AVERAGE /4

' + + '
' + + '
' + + '
'; + +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