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

@ -1,9 +1,15 @@
package org.owasp.webwolf.jwt;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.util.MultiValueMap;
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 static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
@RestController
public class JWTController {
@ -12,16 +18,19 @@ public class JWTController {
return new ModelAndView("jwt");
}
@PostMapping(value = "/WebWolf/jwt/decode", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public JWTToken decode(@RequestBody JWTToken token) {
token.decode();
return token;
@PostMapping(value = "/WebWolf/jwt/decode", consumes = APPLICATION_FORM_URLENCODED_VALUE, produces = APPLICATION_JSON_VALUE)
public JWTToken decode(@RequestBody MultiValueMap<String, String> formData) {
var jwt = formData.getFirst("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)
public JWTToken encode(@RequestBody JWTToken token) {
token.encode();
return token;
@PostMapping(value = "/WebWolf/jwt/encode", consumes = APPLICATION_FORM_URLENCODED_VALUE, produces = APPLICATION_JSON_VALUE)
public JWTToken encode(@RequestBody MultiValueMap<String, String> formData) {
var header = formData.getFirst("header");
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;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.*;
import org.jose4j.jws.AlgorithmIdentifiers;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.consumer.InvalidJwtException;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.jwx.CompactSerializer;
import org.jose4j.keys.HmacKey;
import org.jose4j.lang.JoseException;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
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.StringUtils.hasText;
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
@Builder(toBuilder = true)
public class JWTToken {
private static final Pattern jwtPattern = Pattern.compile("(.*)\\.(.*)\\.(.*)");
@ -30,59 +35,92 @@ public class JWTToken {
private String encoded = "";
private String secretKey;
private String header;
private boolean validHeader;
private boolean validPayload;
private boolean validToken;
private String payload;
private boolean signatureValid = true;
public void decode() {
parseToken(encoded.trim().replace(System.getProperty("line.separator"), ""));
signatureValid = validateSignature(secretKey, encoded);
public static JWTToken decode(String jwt, String secretKey) {
var token = parseToken(jwt.trim().replace(System.getProperty("line.separator"), ""));
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();
jws.setPayload(payload);
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
jws.setDoKeyValidation(false);
if (StringUtils.hasText(secretKey)) {
jws.setPayload(payloadAsString);
headers.forEach((k, v) -> jws.setHeader(k, v));
if (!headers.isEmpty()) { //otherwise e30 meaning {} will be shown as header
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)));
try {
encoded = jws.getCompactSerialization();
signatureValid = true;
builder.encoded(jws.getCompactSerialization());
builder.signatureValid(true);
} catch (JoseException e) {
header = "";
payload = "";
//Do nothing
}
} 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 mapper = new ObjectMapper();
var builder = JWTToken.builder().encoded(jwt);
if (matcher.matches()) {
try {
var prettyPrint = mapper.writerWithDefaultPrettyPrinter();
this.header = prettyPrint.writeValueAsString(mapper.readValue(decodeFromUrlSafeString(matcher.group(1)), Map.class));
this.payload = prettyPrint.writeValueAsString(mapper.readValue(decodeFromUrlSafeString(matcher.group(2)), Map.class));
return true;
} catch (Exception e) {
this.header = new String(decodeFromUrlSafeString(matcher.group(1)));
this.payload = new String(decodeFromUrlSafeString(matcher.group(2)));
return false;
}
var header = new String(decodeFromUrlSafeString(matcher.group(1)), UTF_8);
var payloadAsString = new String(decodeFromUrlSafeString(matcher.group(2)), UTF_8);
var headers = parse(header);
var payload = parse(payloadAsString);
builder.header(write(header, headers));
builder.payload(write(payloadAsString, payload));
builder.validHeader(!headers.isEmpty());
builder.validPayload(!payload.isEmpty());
builder.validToken(!headers.isEmpty() && !payload.isEmpty());
} else {
this.header = "error";
this.payload = "error";
builder.validToken(false);
}
return false;
return builder.build();
}
private boolean validateSignature(String secretKey, String jwt) {
if (StringUtils.hasText(secretKey)) {
private static boolean validateSignature(String secretKey, String jwt) {
if (hasText(secretKey)) {
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setSkipAllValidators()
.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(() => {
$('#encodedToken').on('input', () => {
var token = $('#encodedToken').val();
var secretKey = $('#secretKey').val();
$('#payload').on('input', call(true));
$('#header').on('input', call(true));
$('#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({
type: 'POST',
url: '/WebWolf/jwt/decode',
data: JSON.stringify({encoded: token, secretKey: secretKey}),
url: url,
data: formData,
success: function (data) {
$('#tokenHeader').val(data.header);
$('#tokenPayload').val(data.payload);
updateSignature(data);
update(data)
},
contentType: "application/json",
contentType: "application/x-www-form-urlencoded",
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="form-group">
<label for="encodedToken">Encoded</label>
<form name="encodedTokenForm" id="encodedTokenForm" action="/WebWolf/jwt/encode" method="POST">
<textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="encodedToken" rows="4"
<label for="token">Encoded</label>
<form id="decodeForm">
<textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="token" name="token"
rows="4"
placeholder="Paste token here" spellcheck="false"></textarea>
</form>
</div>
<div class="form-group">
<label>Decoded</label>
<div class="row">
<div class="col-xs-6 col-md-5">Header</div>
<div class="col-xs-6 col-md-7">Payload</div>
</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>
<form id="encodeForm">
<div class="form-group">
<label>Decoded</label>
<div class="row">
<div class="col-xs-6 col-md-5">Header</div>
<div class="col-xs-6 col-md-7">Payload</div>
</div>
<div class="col-xs-6 col-md-7">
<textarea class="form-control" style="font-size: 14pt; font-family:monospace;" id="tokenPayload"
<div class="row">
<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>
</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>
</form>
<br/>
<div class="input-group">
<span class="input-group-addon" id="header">Secret key</span>
<input type="text" class="form-control" id="secretKey">
<span class="input-group-addon">Secret key</span>
<input type="text" value="webgoat" class="form-control" id="secretKey">
</div>
<div class="input-group">

View File

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