Update JWT lesson

This commit is contained in:
Nanne Baars 2021-01-10 15:00:35 +01:00 committed by Nanne Baars
parent ead1d6fffb
commit f2ab5c1968
19 changed files with 571 additions and 251 deletions

View File

@ -22,27 +22,6 @@
package org.owasp.webgoat.jwt; package org.owasp.webgoat.jwt;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.sql.ResultSet;
import java.sql.SQLException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
import org.owasp.webgoat.assignments.AssignmentEndpoint;
import org.owasp.webgoat.assignments.AssignmentHints;
import org.owasp.webgoat.assignments.AttackResult;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwt;
@ -50,6 +29,19 @@ import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SigningKeyResolverAdapter; import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.impl.TextCodec; import io.jsonwebtoken.impl.TextCodec;
import org.apache.commons.lang3.StringUtils;
import org.owasp.webgoat.assignments.AssignmentEndpoint;
import org.owasp.webgoat.assignments.AssignmentHints;
import org.owasp.webgoat.assignments.AttackResult;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
/** /**
* <pre> * <pre>

View File

@ -0,0 +1,52 @@
package org.owasp.webgoat.jwt;
import org.owasp.webgoat.assignments.AssignmentEndpoint;
import org.owasp.webgoat.assignments.AttackResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
public class JWTQuiz extends AssignmentEndpoint {
private final String[] solutions = {"Solution 1", "Solution 2"};
private final boolean[] guesses = new boolean[solutions.length];
@PostMapping("/JWT/quiz")
@ResponseBody
public AttackResult completed(@RequestParam String[] question_0_solution, @RequestParam String[] question_1_solution) {
int correctAnswers = 0;
String[] givenAnswers = {question_0_solution[0], question_1_solution[0]};
for (int i = 0; i < solutions.length; i++) {
if (givenAnswers[i].contains(solutions[i])) {
// answer correct
correctAnswers++;
guesses[i] = true;
} else {
// answer incorrect
guesses[i] = false;
}
}
if (correctAnswers == solutions.length) {
return success(this).build();
} else {
return failed(this).build();
}
}
@GetMapping("/JWT/quiz")
@ResponseBody
public boolean[] getResults() {
return this.guesses;
}
}

View File

@ -62,7 +62,7 @@
<i class="fa fa-user"></i> <span class="caret"></span> <i class="fa fa-user"></i> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu dropdown-menu-left"> <ul class="dropdown-menu dropdown-menu-left">
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a data-toggle="dropdown" role="menuitem" tabindex="-1"
onclick="javascript:loginVotes('Guest')" onclick="javascript:loginVotes('Guest')"
th:text="Guest">current</a></li> th:text="Guest">current</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" <li role="presentation"><a role="menuitem" tabindex="-1"
@ -100,6 +100,38 @@
</div> </div>
</div> </div>
<div class="lesson-page-wrapper">
<div class="lesson-page-solution">
<div class="adoc-content" th:replace="doc:JWT_signing_solution.adoc"></div>
</div>
</div>
<div class="lesson-page-wrapper">
<span id="quiz_id" data-quiz_id="jwt"></span>
<link rel="stylesheet" type="text/css" th:href="@{/css/quiz.css}"/>
<script th:src="@{/js/quiz.js}" language="JavaScript"></script>
<link rel="import" type="application/json" th:href="@{/lesson_js/questions_jwt.json}"/>
<div class="adoc-content" th:replace="doc:JWT_libraries_assignment.adoc"></div>
<div class="attack-container">
<div class="attack-feedback"></div>
<div class="attack-output"></div>
<div class="assignment-success"><i class="fa fa-2 fa-check hidden" aria-hidden="true"></i></div>
<div class="container-fluid">
<form id="quiz-form" class="attack-form" accept-charset="UNKNOWN"
method="POST" name="form"
action="/WebGoat/JWT/quiz"
role="form">
<div id="q_container"></div>
<br/>
<input name="Quiz_solutions" value="Submit answers" type="SUBMIT"/>
</form>
</div>
<div class="attack-feedback"></div>
<div class="attack-output"></div>
</div>
</div>
<div class="lesson-page-wrapper"> <div class="lesson-page-wrapper">
<div class="adoc-content" th:replace="doc:JWT_weak_keys"></div> <div class="adoc-content" th:replace="doc:JWT_weak_keys"></div>
<div id="secrettoken"></div> <div id="secrettoken"></div>
@ -312,6 +344,10 @@
<div class="attack-output"></div> <div class="attack-output"></div>
</div> </div>
</div> </div>
<div class="lesson-page-wrapper">
<div class="adoc-content" th:replace="doc:JWT_mitigation.adoc"></div>
</div>
</body> </body>
</html> </html>

View File

@ -0,0 +1,20 @@
{
"questions": [
{
"text": "What is the result of the first code snippet?",
"solutions": {
"1": "Throws an exception in line 13",
"2": "Invoked the method removeAllUsers at line 8",
"3": "Logs an error in line 10"
}
},
{
"text": "What is the result of the first code snippet?",
"solutions": {
"1": "Throws an exception in line 13",
"2": "Invoked the method removeAllUsers at line 8",
"3": "Logs an error in line 10"
}
}
]
}

View File

@ -5,7 +5,7 @@ Given the following token:
[source] [source]
---- ----
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDcwOTk2MDgsInVzZXJfbmFtZSI6InVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfVVNFUiJdLCJqdGkiOiI5YmM5MmE0NC0wYjFhLTRjNWUtYmU3MC1kYTUyMDc1YjlhODQiLCJjbGllbnRfaWQiOiJteS1jbGllbnQtd2l0aC1zZWNyZXQifQ.N9TsIXpvMoICVeGI9mEOPVlYODjMOCis--yB-34BOOw eyJhbGciOiJIUzI1NiJ9.ew0KICAiYXV0aG9yaXRpZXMiIDogWyAiUk9MRV9BRE1JTiIsICJST0xFX1VTRVIiIF0sDQogICJjbGllbnRfaWQiIDogIm15LWNsaWVudC13aXRoLXNlY3JldCIsDQogICJleHAiIDogMTYwNzA5OTYwOCwNCiAgImp0aSIgOiAiOWJjOTJhNDQtMGIxYS00YzVlLWJlNzAtZGE1MjA3NWI5YTg0IiwNCiAgInNjb3BlIiA6IFsgInJlYWQiLCAid3JpdGUiIF0sDQogICJ1c2VyX25hbWUiIDogInVzZXIiDQp9.9lYaULTuoIDJ86-zKDSntJQyHPpJ2mZAbnWRfel99iI
---- ----
Copy and paste the following token and decode the token, can you find the user inside the token? Copy and paste the following token and decode the token, can you find the user inside the token?

View File

@ -1,5 +1,5 @@
== Final challenges == Final challenge
Below you see two account, one of Jerry and one of Tom. Jerry wants to remove Toms account from Twitter, but his token Below you see two accounts, one of Jerry and one of Tom. Jerry wants to remove Tom''s account from Twitter, but his token
can only delete his own account. Can you try to help him and delete Toms account? can only delete his account. Can you try to help him and delete Toms account?

View File

@ -0,0 +1,68 @@
== JWT libraries
There are a number of JWT libraries available in the Java ecosystem. Let's look at one of them:
The contents of our token is:
[source]
----
header:
{
"alg": "HS256",
"typ": "JWT"
}
claims:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
----
[source]
----
var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.NFvYpuwbF6YWbPyaNAGEPw9wbhiQSovvSrD89B8K7Ng";
Jwts.parser().setSigningKey("test").parseClaimsJws(token);
----
will work!
Let's change the header to `{"alg":"none","typ":"JWT"}`
Using the same source as above gives:
[source]
----
var token = " eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.NFvYpuwbF6YWbPyaNAGEPw9wbhiQSovvSrD89B8K7Ng";
Jwts.parser().setSigningKey("test").parseClaimsJws(token);
----
will result in:
[souce]
----
io.jsonwebtoken.MalformedJwtException: JWT string has a digest/signature, but the header does not reference a valid signature algorithm.
----
removing the signature completely (leaving the last `.`)
[source]
----
var token = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.";
Jwts.parser().setSigningKey("test").parseClaimsJws(token);
----
will result in:
[source]
----
io.jsonwebtoken.UnsupportedJwtException: Unsigned Claims JWTs are not supported.
----
This is what you would expect from the library!

View File

@ -0,0 +1,45 @@
== Code review
Now let's look at a code review and try to think on an attack with the `alg: none`, so we use the following token:
[source]
----
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
----
[source%linenums, java]
----
try {
Jwt jwt = Jwts.parser().setSigningKey(JWT_PASSWORD).parseClaimsJws(accessToken);
Claims claims = (Claims) jwt.getBody();
String user = (String) claims.get("user");
boolean isAdmin = Boolean.valueOf((String) claims.get("admin"));
if (isAdmin) {
removeAllUsers();
} else {
log.error("You are not an admin user");
}
} catch (JwtException e) {
throw new InvalidTokenException(e);
}
----
[source%linenums, java]
----
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 (isAdmin) {
removeAllUsers();
} else {
log.error("You are not an admin user");
}
} catch (JwtException e) {
throw new InvalidTokenException(e);
}
----
Can you spot the weakness?

View File

@ -6,7 +6,7 @@ image::images/jwt_diagram.png[style="lesson-image"]
{nbsp} + {nbsp} +
In this flow you can see the user logs in with a username and password on a successful authentication the server In this flow, you can see the user logs in with a username and password on successful authentication the server
returns. The server creates a new token and returns this one to the client. When the client makes a successive 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. 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 The server reads the token and first validates the signature after a successful verification the server uses the
@ -14,6 +14,6 @@ information in the token to identify the user.
=== Claims === Claims
The token contains claims to identify the user and all other information necessary for the server to fulfil the request. The token contains claims to identify the user and all other information necessary for the server to fulfill the request.
Be aware not to store sensitive information in the token and always send them over a secure channel. Be aware not to store sensitive information in the token and always send it over a secure channel.

View File

@ -0,0 +1,9 @@
=== Best practices
Some best practices when working with JWT:
- Fix the algorithm, do not allow a client to switch the algorithm.
- Make sure you use an appropriate key length when using a symmetric key for signing the token.
- Make sure the claims added to the token do not contain personal information. If you need to add more information opt for encrypting the token as well.
- Add sufficient test cases to your project to verify invalid tokens actually do not work. Integration with a third party to check your token does not mean you do not have test your application at all.
- Take a look at the best practices mentioned in https://tools.ietf.org/html/rfc8725#section-2

View File

@ -0,0 +1,91 @@
=== Solution
The idea behind this assignment is that you can manipulate the token which might cause the server to interpret the token differently. In the beginning when JWT libraries appeared they implemented the specification to the letter meaning that the library took the algorithm specified inside the header and tried to work with it.
[quote, https://tools.ietf.org/html/rfc8725#section-2.1]
____
Signed JSON Web Tokens carry an explicit indication of the signing
algorithm, in the form of the "alg" Header Parameter, to facilitate
cryptographic agility. This, in conjunction with design flaws in
some libraries and applications, has led to several attacks:
* The algorithm can be changed to "none" by an attacker, and some
libraries would trust this value and "validate" the JWT without
checking any signature.
* An "RS256" (RSA, 2048 bit) parameter value can be changed into
"HS256" (HMAC, SHA-256), and some libraries would try to validate
the signature using HMAC-SHA256 and using the RSA public key as
the HMAC shared secret (see [McLean] and [CVE-2015-9235]).
For mitigations, see Sections 3.1 and 3.2.
____
What basically happened was that libraries just parsed the token as it was given to them without validating what cryptographic operation was used during the creation of the token.
==== Solution
First note that we are logged in as `Guest` so first select a different user for example: Tom.
User Tom is allowed to vote as you can see, but he is unable to reset the votes. Looking at the request this will return an `access_token` in the response:
[source]
----
GET http://localhost:8080/WebGoat/JWT/votings/login?user=Tom HTTP/1.1
access_token=eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2MDgxMjg1NjYsImFkbWluIjoiZmFsc2UiLCJ1c2VyIjoiVG9tIn0.rTSX6PSXqUoGUvQQDBiqX0re2BSt7s2-X6FPf34Qly9SMpqIUSP8jykedJbjOBNlM3_CTjgk1SvUv48Pz8zIzA
----
Decoding the token gives:
[source]
----
{
"alg": "HS512"
}
{
"iat": 1608128566,
"admin": "false",
"user": "Tom"
}
----
We can change the `admin` claim to `false` but then signature will become invalid. How do we end up with a valid signature?
Looking at the https://tools.ietf.org/html/rfc7519#section-6.1[RFC specification] `alg: none` is a valid choice and gives an unsecured JWT.
Let's change our token:
[source]
----
headers:
{
"alg": "none"
}
claims:
{
"iat": 1608128566,
"admin": "true",
"user": "Tom"
}
----
If we use WebWolf to create our token we get:
[source]
----
eyJhbGciOiJub25lIn0.ew0KICAiYWRtaW4iIDogInRydWUiLA0KICAiaWF0IiA6IDE2MDgxMjg1NjYsDQogICJ1c2VyIiA6ICJUb20iDQp9
----
Now we can replace the token in the cookie and perform the reset again. One thing to watch out for is to add a `.` at the end otherwise the token is not valid.
== References
For more information take a look at the following video:
video::wt3UixCiPfo[youtube, height=480, width=100%]

View File

@ -0,0 +1,46 @@
package org.owasp.webgoat.jwt;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.owasp.webgoat.plugins.LessonTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringJUnit4ClassRunner.class)
public class JWTDecodeEndpointTest extends LessonTest {
@Autowired
private JWT jwt;
@Before
public void setup() {
when(webSession.getCurrentLesson()).thenReturn(jwt);
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
public void solveAssignment() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/JWT/decode")
.param("jwt-encode-user", "user")
.content(""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.lessonCompleted", is(true)));
}
@Test
public void wrongUserShouldNotSolveAssignment() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/JWT/decode")
.param("jwt-encode-user", "wrong")
.content(""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.lessonCompleted", is(false)));
}
}

View File

@ -1,59 +0,0 @@
/**
This is the basic javascript that can be used for a quiz assignment. It is made for single choice quizzes (tho a multiple choice extension should be easy to make).
Basic steps for implementing a quiz:
1. HTML: include this js script file for the assignment, build a basic form, where you include a #q_container div element, create a submit button with "Quiz_solutions" as name attribute
2. JSON: Create a JSON-file with the name questions_lesson_name.json, include a span element #quiz_id with lesson_name as the data-quiz_id attribute. Build a JSON file like the one in sql-injection -> resources -> js
3. Java: Create a normal assignment that has a String[] where the correct solutions are contained in the form of "Solution [i]", replace [i] with the position of the solution beginning at 1.
The request parameters will contain the answer in full text with "Solution [i]" in front of the text. Use them to check the answers validity.
4. CSS: include the css/quiz.css file for styling.
**/
$(function () {
var json = "";
var client = new XMLHttpRequest();
var quiz_id = document.getElementById("quiz_id").getAttribute("data-quiz_id");
client.open('GET', '/WebGoat/lesson_js/questions_' + quiz_id + '.json');
client.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
json += client.responseText;
console.log("entry");
let questionsJson = json;
var questionsObj = JSON.parse(questionsJson);
let html = "";
jQuery.each(questionsObj, function(i, obj) {
jQuery.each(obj, function(j, quest) {
html += "<div id='question_" + j + "' class='quiz_question' name='question'><p>" + (j+1) + ".&nbsp;" + quest.text + "</p>";
html += "<fieldset>";
jQuery.each(quest.solutions, function(k, solution) {
solution = "Solution " + k + ": " + solution;
html += '<input id="question_' + j + '_' + k + '_input" type="radio" name="question_' + j +'_solution" value="' + solution + '" required><label for="question_' + j + '_' + k + '_input">' + solution + '</label><br>';
});
html += "</fieldset></div>";
});
});
document.getElementById("q_container").innerHTML = html;
}
}
client.send();
});
$(document).ready( () => {
$("#q_container").closest(".attack-container").addClass("quiz");
$("#q_container").closest("form").on("submit", function(e) {
setTimeout(getFeedback, 200, this);
}); // end listener
}); // end ready
function getFeedback(context) {
$.ajax({
url: $(context).attr("action")
}).done( (result) => {
if (!result) return;
for(let i=0; i<result.length; i++) {
if (result[i] === true)
$("#q_container .quiz_question:nth-of-type(" + (i+1) + ")").removeClass("incorrect").addClass("correct");
else if (result[i] === false)
$("#q_container .quiz_question:nth-of-type(" + (i+1) + ")").removeClass("correct").addClass("incorrect");
}
}); // end ajax-done
} // end getFeedback

View File

@ -33,7 +33,7 @@ JSON parse error: Unexpected character '{' (code 123) in prolog; expected
Unexpected character '{' (code 123) in prolog; expected '<'\n at [row,col {unknown-source}]: [1,1]“ Unexpected character '{' (code 123) in prolog; expected '<'\n at [row,col {unknown-source}]: [1,1]“
---- ----
This error message appears because we are still sending a json message towards the endpoint, so if we intercept and change change the json message to a xml message: This error message appears because we are still sending a json message towards the endpoint, so if we intercept and change the json message to a xml message:
[source] [source]
---- ----

View File

@ -1,9 +1,15 @@
package org.owasp.webwolf.jwt; package org.owasp.webwolf.jwt;
import org.springframework.http.MediaType; import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RestController @RestController
public class JWTController { public class JWTController {
@ -12,16 +18,19 @@ public class JWTController {
return new ModelAndView("jwt"); return new ModelAndView("jwt");
} }
@PostMapping(value = "/WebWolf/jwt/decode", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/WebWolf/jwt/decode", consumes = APPLICATION_FORM_URLENCODED_VALUE, produces = APPLICATION_JSON_VALUE)
public JWTToken decode(@RequestBody JWTToken token) { public JWTToken decode(@RequestBody MultiValueMap<String, String> formData) {
token.decode(); var jwt = formData.getFirst("token");
return token; var secretKey = formData.getFirst("secretKey");
return JWTToken.decode(jwt, secretKey);
} }
@PostMapping(value = "/WebWolf/jwt/encode", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(value = "/WebWolf/jwt/encode", consumes = APPLICATION_FORM_URLENCODED_VALUE, produces = APPLICATION_JSON_VALUE)
public JWTToken encode(@RequestBody JWTToken token) { public JWTToken encode(@RequestBody MultiValueMap<String, String> formData) {
token.encode(); var header = formData.getFirst("header");
return token; var payload = formData.getFirst("payload");
var secretKey = formData.getFirst("secretKey");
return JWTToken.encode(header, payload, secretKey);
} }
} }

View File

@ -1,28 +1,33 @@
package org.owasp.webwolf.jwt; package org.owasp.webwolf.jwt;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.*; import lombok.AllArgsConstructor;
import org.jose4j.jws.AlgorithmIdentifiers; import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jose4j.jws.JsonWebSignature; import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.consumer.InvalidJwtException; import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer; import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder; import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.jwx.CompactSerializer;
import org.jose4j.keys.HmacKey; import org.jose4j.keys.HmacKey;
import org.jose4j.lang.JoseException; import org.jose4j.lang.JoseException;
import org.springframework.util.StringUtils;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.jose4j.jwx.CompactSerializer.serialize;
import static org.springframework.util.Base64Utils.decodeFromUrlSafeString; import static org.springframework.util.Base64Utils.decodeFromUrlSafeString;
import static org.springframework.util.StringUtils.hasText;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
@Setter @Setter
@Builder @Builder(toBuilder = true)
public class JWTToken { public class JWTToken {
private static final Pattern jwtPattern = Pattern.compile("(.*)\\.(.*)\\.(.*)"); private static final Pattern jwtPattern = Pattern.compile("(.*)\\.(.*)\\.(.*)");
@ -30,59 +35,92 @@ public class JWTToken {
private String encoded = ""; private String encoded = "";
private String secretKey; private String secretKey;
private String header; private String header;
private boolean validHeader;
private boolean validPayload;
private boolean validToken;
private String payload; private String payload;
private boolean signatureValid = true; private boolean signatureValid = true;
public void decode() { public static JWTToken decode(String jwt, String secretKey) {
parseToken(encoded.trim().replace(System.getProperty("line.separator"), "")); var token = parseToken(jwt.trim().replace(System.getProperty("line.separator"), ""));
signatureValid = validateSignature(secretKey, encoded); return token.toBuilder().signatureValid(validateSignature(secretKey, jwt)).build();
} }
public void encode() { private static Map<String, Object> parse(String header) {
var reader = new ObjectMapper();
try {
return reader.readValue(header, TreeMap.class);
} catch (JsonProcessingException e) {
return Map.of();
}
}
private static String write(String originalValue, Map<String, Object> data) {
var writer = new ObjectMapper().writerWithDefaultPrettyPrinter();
try {
if (data.isEmpty()) {
return originalValue;
}
return writer.writeValueAsString(data);
} catch (JsonProcessingException e) {
return originalValue;
}
}
public static JWTToken encode(String header, String payloadAsString, String secretKey) {
var headers = parse(header);
var payload = parse(payloadAsString);
var builder = JWTToken.builder()
.header(write(header, headers))
.payload(write(payloadAsString, payload))
.validHeader(!hasText(header) || !headers.isEmpty())
.validToken(true)
.validPayload(!hasText(payloadAsString) || !payload.isEmpty());
JsonWebSignature jws = new JsonWebSignature(); JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(payload); jws.setPayload(payloadAsString);
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256); headers.forEach((k, v) -> jws.setHeader(k, v));
jws.setDoKeyValidation(false); if (!headers.isEmpty()) { //otherwise e30 meaning {} will be shown as header
if (StringUtils.hasText(secretKey)) { builder.encoded(serialize(new String[]{jws.getHeaders().getEncodedHeader(), jws.getEncodedPayload()}));
}
//Only sign when valid header and payload
if (!headers.isEmpty() && !payload.isEmpty() && hasText(secretKey)) {
jws.setDoKeyValidation(false);
jws.setKey(new HmacKey(secretKey.getBytes(UTF_8))); jws.setKey(new HmacKey(secretKey.getBytes(UTF_8)));
try { try {
encoded = jws.getCompactSerialization(); builder.encoded(jws.getCompactSerialization());
signatureValid = true; builder.signatureValid(true);
} catch (JoseException e) { } catch (JoseException e) {
header = ""; //Do nothing
payload = "";
} }
} else {
var encodedHeader = jws.getHeaders().getEncodedHeader();
var encodedPayload = jws.getEncodedPayload();
encoded = CompactSerializer.serialize(new String[]{encodedHeader, encodedPayload});
} }
return builder.build();
} }
private boolean parseToken(String jwt) { private static JWTToken parseToken(String jwt) {
var matcher = jwtPattern.matcher(jwt); var matcher = jwtPattern.matcher(jwt);
var mapper = new ObjectMapper(); var builder = JWTToken.builder().encoded(jwt);
if (matcher.matches()) { if (matcher.matches()) {
try { var header = new String(decodeFromUrlSafeString(matcher.group(1)), UTF_8);
var prettyPrint = mapper.writerWithDefaultPrettyPrinter(); var payloadAsString = new String(decodeFromUrlSafeString(matcher.group(2)), UTF_8);
this.header = prettyPrint.writeValueAsString(mapper.readValue(decodeFromUrlSafeString(matcher.group(1)), Map.class)); var headers = parse(header);
this.payload = prettyPrint.writeValueAsString(mapper.readValue(decodeFromUrlSafeString(matcher.group(2)), Map.class)); var payload = parse(payloadAsString);
return true; builder.header(write(header, headers));
} catch (Exception e) { builder.payload(write(payloadAsString, payload));
this.header = new String(decodeFromUrlSafeString(matcher.group(1))); builder.validHeader(!headers.isEmpty());
this.payload = new String(decodeFromUrlSafeString(matcher.group(2))); builder.validPayload(!payload.isEmpty());
return false; builder.validToken(!headers.isEmpty() && !payload.isEmpty());
}
} else { } else {
this.header = "error"; builder.validToken(false);
this.payload = "error";
} }
return false; return builder.build();
} }
private boolean validateSignature(String secretKey, String jwt) { private static boolean validateSignature(String secretKey, String jwt) {
if (StringUtils.hasText(secretKey)) { if (hasText(secretKey)) {
JwtConsumer jwtConsumer = new JwtConsumerBuilder() JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setSkipAllValidators() .setSkipAllValidators()
.setVerificationKey(new HmacKey(secretKey.getBytes(UTF_8))) .setVerificationKey(new HmacKey(secretKey.getBytes(UTF_8)))

View File

@ -1,76 +1,47 @@
(function ($) {
$.fn.getFormData = function () {
var data = {};
var dataArray = $(this).serializeArray();
for (var i = 0; i < dataArray.length; i++) {
data[dataArray[i].name] = dataArray[i].value;
}
return data;
}
})(jQuery);
$(document).ready(() => { $(document).ready(() => {
$('#encodedToken').on('input', () => { $('#payload').on('input', call(true));
var token = $('#encodedToken').val(); $('#header').on('input', call(true));
var secretKey = $('#secretKey').val(); $('#secretKey').on('input', call(true));
$('#token').on('input', call(false));
});
function call(encode) {
return () => {
var url = encode ? '/WebWolf/jwt/encode' : '/WebWolf/jwt/decode';
var formData = encode ? $('#encodeForm').getFormData() : $('#decodeForm').getFormData();
formData["secretKey"] = $('#secretKey').val();
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: '/WebWolf/jwt/decode', url: url,
data: JSON.stringify({encoded: token, secretKey: secretKey}), data: formData,
success: function (data) { success: function (data) {
$('#tokenHeader').val(data.header); update(data)
$('#tokenPayload').val(data.payload);
updateSignature(data);
}, },
contentType: "application/json", contentType: "application/x-www-form-urlencoded",
dataType: 'json' dataType: 'json'
}); });
});
});
function encode() {
return () => {
var header = $('#tokenHeader').val();
var payload = $('#tokenPayload').val();
var secretKey = $('#secretKey').val();
var token = {header: header, payload: payload, secretKey: secretKey};
if (!parseJson(header)) {
$('#encodedToken').val("");
$('#tokenHeader').css('background-color', 'lightcoral');
} else if (!parseJson(payload)) {
$('#encodedToken').val("");
$('#tokenPayload').css('background-color', 'lightcoral');
} else {
$.ajax({
type: 'POST',
url: '/WebWolf/jwt/encode',
data: JSON.stringify(token),
success: function (data) {
$('#encodedToken').val(data.encoded);
$('#tokenPayload').css('background-color', '#FFFFFF');
$('#encodedToken').css('background-color', '#FFFFFF');
$('#tokenHeader').css('background-color', '#FFFFFF');
updateSignature(data);
},
contentType: "application/json",
dataType: 'json'
});
}
};
}
$(document).ready(() => {
$('#tokenPayload').on('input', encode());
$('#tokenHeader').on('input', encode());
$('#secretKey').on('input', encode());
});
function parseJson(text) {
try {
if (text) {
JSON.parse(text);
}
} catch (e) {
return false;
}
return true;
}
function updateSignature(data) {
if (data.signatureValid) {
$('#signatureValid').html("Signature valid");
} else {
$('#signatureValid').html("Signature invalid");
} }
} }
function update(token) {
$('#token').val(token.encoded);
$('#payload').val(token.payload);
$('#header').val(token.header);
$('#token').css('background-color', token.validToken ? '#FFFFFF' : 'lightcoral');
$('#header').css('background-color', token.validHeader ? '#FFFFFF' : 'lightcoral');
$('#payload').css('background-color', token.validPayload ? '#FFFFFF' : 'lightcoral');
$('#signatureValid').html(token.signatureValid ? "Signature valid" : "Signature invalid");
}

View File

@ -22,34 +22,39 @@
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true"> <div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="form-group"> <div class="form-group">
<label for="encodedToken">Encoded</label> <label for="token">Encoded</label>
<form name="encodedTokenForm" id="encodedTokenForm" action="/WebWolf/jwt/encode" method="POST"> <form id="decodeForm">
<textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="encodedToken" rows="4" <textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="token" name="token"
rows="4"
placeholder="Paste token here" spellcheck="false"></textarea> placeholder="Paste token here" spellcheck="false"></textarea>
</form> </form>
</div> </div>
<div class="form-group"> <form id="encodeForm">
<label>Decoded</label> <div class="form-group">
<div class="row"> <label>Decoded</label>
<div class="col-xs-6 col-md-5">Header</div> <div class="row">
<div class="col-xs-6 col-md-7">Payload</div> <div class="col-xs-6 col-md-5">Header</div>
</div> <div class="col-xs-6 col-md-7">Payload</div>
<div class="row">
<div class="col-xs-6 col-md-5">
<textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="tokenHeader"
rows="12"></textarea>
</div> </div>
<div class="col-xs-6 col-md-7"> <div class="row">
<textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="tokenPayload" <div class="col-xs-6 col-md-5">
<textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="header"
name="header"
rows="12"></textarea> rows="12"></textarea>
</div>
<div class="col-xs-6 col-md-7">
<textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="payload"
name="payload"
rows="12"></textarea>
</div>
</div> </div>
</div> </div>
</div> </form>
<br/> <br/>
<div class="input-group"> <div class="input-group">
<span class="input-group-addon" id="header">Secret key</span> <span class="input-group-addon">Secret key</span>
<input type="text" class="form-control" id="secretKey"> <input type="text" value="webgoat" class="form-control" id="secretKey">
</div> </div>
<div class="input-group"> <div class="input-group">

View File

@ -14,49 +14,30 @@ class JWTTokenTest {
void encodeCorrectTokenWithoutSignature() { void encodeCorrectTokenWithoutSignature() {
var headers = Map.of("alg", "HS256", "typ", "JWT"); var headers = Map.of("alg", "HS256", "typ", "JWT");
var payload = Map.of("test", "test"); var payload = Map.of("test", "test");
var token = JWTToken.builder().header(toString(headers)).payload(toString(payload)).build(); var token = JWTToken.encode(toString(headers), toString(payload), "");
token.encode(); assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCJ9");
assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoidGVzdCJ9");
} }
@Test @Test
void encodeCorrectTokenWithSignature() { void encodeCorrectTokenWithSignature() {
var headers = Map.of("alg", "HS256", "typ", "JWT"); var headers = Map.of("alg", "HS256", "typ", "JWT");
var payload = Map.of("test", "test"); var payload = Map.of("test", "test");
var token = JWTToken.builder() var token = JWTToken.encode(toString(headers), toString(payload), "webgoat");
.header(toString(headers))
.payload(toString(payload))
.secretKey("test")
.build();
token.encode(); assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCJ9.axNp9BkswwK_YRF2URJ5P1UejQNYZbK4qYcMnkusg6I");
assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoidGVzdCJ9.KOobRHDYyaesV_doOk11XXGKSONwzllraAaqqM4VFE4");
} }
@Test @Test
void encodeTokenWithNonJsonInput() { void encodeTokenWithNonJsonInput() {
var token = JWTToken.builder() var token = JWTToken.encode("aaa", "bbb", "test");
.header("aaa")
.payload("bbb")
.secretKey("test")
.build();
token.encode(); assertThat(token.getEncoded()).isNullOrEmpty();
assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJIUzI1NiJ9.YmJi.VAcRegquayARuahZZ1ednXpbAyv7KEFnyjNJlxLNX0I");
} }
@Test @Test
void decodeValidSignedToken() { void decodeValidSignedToken() {
var token = JWTToken.builder() var token = JWTToken.decode("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoidGVzdCJ9.KOobRHDYyaesV_doOk11XXGKSONwzllraAaqqM4VFE4", "test");
.encoded("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoidGVzdCJ9.KOobRHDYyaesV_doOk11XXGKSONwzllraAaqqM4VFE4")
.secretKey("test")
.build();
token.decode();
assertThat(token.getHeader()).contains("\"alg\" : \"HS256\""); assertThat(token.getHeader()).contains("\"alg\" : \"HS256\"");
assertThat(token.isSignatureValid()).isTrue(); assertThat(token.isSignatureValid()).isTrue();
@ -64,14 +45,30 @@ class JWTTokenTest {
@Test @Test
void decodeInvalidSignedToken() { void decodeInvalidSignedToken() {
var token = JWTToken.builder().encoded("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXsdfdfsaasfddfasN0IjoidGVzdCJ9.KOobRHDYyaesV_doOk11XXGKSONwzllraAaqqM4VFE4").build(); var token = JWTToken.decode("eyJhbGciOiJIUzI1NiJ9.eyJ0ZXsdfdfsaasfddfasN0IjoidGVzdCJ9.KOobRHDYyaesV_doOk11XXGKSONwzllraAaqqM4VFE4", "");
token.decode(); assertThat(token.getHeader()).contains("{\n" +
" \"alg\" : \"HS256\"\n" +
assertThat(token.getHeader()).contains("\"alg\":\"HS256\""); "}");
assertThat(token.getPayload()).contains("{\"te"); assertThat(token.getPayload()).contains("{\"te");
} }
@Test
void onlyEncodeWhenHeaderOrPayloadIsPresent() {
var token = JWTToken.encode("", "", "");
assertThat(token.getEncoded()).isNullOrEmpty();
}
@Test
void encodeAlgNone() {
var headers = Map.of("alg", "none");
var payload = Map.of("test", "test");
var token = JWTToken.encode(toString(headers), toString(payload), "test");
assertThat(token.getEncoded()).isEqualTo("eyJhbGciOiJub25lIn0.eyJ0ZXN0IjoidGVzdCJ9");
}
@SneakyThrows @SneakyThrows
private String toString(Map<String, String> map) { private String toString(Map<String, String> map) {
var mapper = new ObjectMapper(); var mapper = new ObjectMapper();