Update JWT lesson
This commit is contained in:
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)))
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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();
|
||||
|
Reference in New Issue
Block a user