Skip to content

Commit e4876d8

Browse files
authored
Merge pull request auth0#85 from auth0/feat-hs-verify
Add HS and None Algorithm Verification
2 parents 535af31 + 29ee30b commit e4876d8

17 files changed

Lines changed: 393 additions & 85 deletions

lib/src/main/java/com/auth0/jwtdecodejava/JWTDecoder.java

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package com.auth0.jwtdecodejava;
22

3+
import com.auth0.jwtdecodejava.enums.Algorithm;
34
import com.auth0.jwtdecodejava.exceptions.JWTException;
45
import com.auth0.jwtdecodejava.impl.JWTParser;
56
import com.auth0.jwtdecodejava.interfaces.Claim;
67
import com.auth0.jwtdecodejava.interfaces.Header;
78
import com.auth0.jwtdecodejava.interfaces.JWT;
89
import com.auth0.jwtdecodejava.interfaces.Payload;
9-
import com.sun.istack.internal.NotNull;
1010

1111
import java.util.Date;
1212

@@ -27,23 +27,15 @@ public static JWT decode(String jwt) {
2727
}
2828

2929
private void parseToken(String token) throws JWTException {
30-
final String[] parts = splitToken(token);
30+
final String[] parts = Utils.splitToken(token);
3131
final JWTParser converter = new JWTParser();
3232
header = converter.parseHeader(base64Decode(parts[0]));
3333
payload = converter.parsePayload(base64Decode(parts[1]));
3434
signature = parts[2];
3535
}
3636

37-
private String[] splitToken(String token) {
38-
String[] parts = token.split("\\.");
39-
if (parts.length != 3) {
40-
throw new JWTException(String.format("The token was expected to have 3 parts, but got %s.", parts.length));
41-
}
42-
return parts;
43-
}
44-
4537
@Override
46-
public String getAlgorithm() {
38+
public Algorithm getAlgorithm() {
4739
return header.getAlgorithm();
4840
}
4941

@@ -93,7 +85,7 @@ public String getId() {
9385
}
9486

9587
@Override
96-
public Claim getClaim(@NotNull String name) {
88+
public Claim getClaim(String name) {
9789
return payload.getClaim(name);
9890
}
9991

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
package com.auth0.jwtdecodejava;
22

3+
import com.auth0.jwtdecodejava.enums.Algorithm;
34
import com.auth0.jwtdecodejava.exceptions.JWTException;
4-
import com.fasterxml.jackson.databind.ObjectMapper;
5-
import com.sun.istack.internal.Nullable;
65
import org.apache.commons.codec.binary.Base64;
76
import org.apache.commons.codec.binary.StringUtils;
87

9-
import java.io.IOException;
8+
import javax.crypto.Mac;
9+
import javax.crypto.spec.SecretKeySpec;
10+
import java.security.InvalidKeyException;
11+
import java.security.MessageDigest;
12+
import java.security.NoSuchAlgorithmException;
1013

11-
class Utils {
14+
public class Utils {
1215

13-
@Nullable
14-
static String base64Decode(String string) throws JWTException {
16+
public static String base64Decode(String string) throws JWTException {
1517
String decoded;
1618
try {
1719
decoded = StringUtils.newStringUtf8(Base64.decodeBase64(string));
@@ -21,8 +23,7 @@ static String base64Decode(String string) throws JWTException {
2123
return decoded;
2224
}
2325

24-
@Nullable
25-
static String base64Encode(String string) throws JWTException {
26+
public static String base64Encode(String string) throws JWTException {
2627
String encoded;
2728
try {
2829
encoded = StringUtils.newStringUtf8(Base64.encodeBase64(string.getBytes(), false, true));
@@ -31,4 +32,30 @@ static String base64Encode(String string) throws JWTException {
3132
}
3233
return encoded;
3334
}
35+
36+
public static boolean verifyHS(String[] jwtParts, String secret, Algorithm algorithm) throws NoSuchAlgorithmException, InvalidKeyException {
37+
if (secret == null) {
38+
throw new IllegalArgumentException("The Secret cannot be null");
39+
}
40+
if (algorithm != Algorithm.HS256 && algorithm != Algorithm.HS384 && algorithm != Algorithm.HS512) {
41+
throw new IllegalArgumentException("The Algorithm must be one of HS256, HS384, or HS512.");
42+
}
43+
Mac mac = Mac.getInstance(algorithm.toString());
44+
mac.init(new SecretKeySpec(secret.getBytes(), algorithm.toString()));
45+
String message = String.format("%s.%s", jwtParts[0], jwtParts[1]);
46+
byte[] result = mac.doFinal(message.getBytes());
47+
return MessageDigest.isEqual(result, Base64.decodeBase64(jwtParts[2]));
48+
}
49+
50+
public static String[] splitToken(String token) {
51+
String[] parts = token.split("\\.");
52+
if (parts.length == 2 && token.endsWith(".")) {
53+
//Tokens with alg='none' have empty String as Signature.
54+
parts = new String[]{parts[0], parts[1], ""};
55+
}
56+
if (parts.length != 3) {
57+
throw new JWTException(String.format("The token was expected to have 3 parts, but got %s.", parts.length));
58+
}
59+
return parts;
60+
}
3461
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.auth0.jwtdecodejava.enums;
2+
3+
import org.apache.commons.codec.digest.HmacAlgorithms;
4+
5+
public enum Algorithm {
6+
none(null),
7+
HS256(HmacAlgorithms.HMAC_SHA_256.toString()),
8+
HS384(HmacAlgorithms.HMAC_SHA_384.toString()),
9+
HS512(HmacAlgorithms.HMAC_SHA_512.toString());
10+
11+
private final String description;
12+
13+
Algorithm(String description) {
14+
this.description = description;
15+
}
16+
17+
@Override
18+
public String toString() {
19+
return description;
20+
}
21+
22+
public static Algorithm parseFrom(String algorithmName) {
23+
try {
24+
return Algorithm.valueOf(algorithmName);
25+
} catch (IllegalArgumentException ignored) {
26+
return null;
27+
}
28+
}
29+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.auth0.jwtdecodejava.exceptions;
2+
3+
public class AlgorithmMismatchException extends JWTException{
4+
public AlgorithmMismatchException(String message) {
5+
super(message);
6+
}
7+
}

lib/src/main/java/com/auth0/jwtdecodejava/exceptions/InvalidClaimException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
public class InvalidClaimException extends JWTException {
5-
public InvalidClaimException(String description) {
6-
super(description);
5+
public InvalidClaimException(String message) {
6+
super(message);
77
}
88
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.auth0.jwtdecodejava.exceptions;
2+
3+
import com.auth0.jwtdecodejava.enums.Algorithm;
4+
5+
public class SignatureVerificationException extends JWTException {
6+
public SignatureVerificationException(Algorithm algorithm) {
7+
this(algorithm, null);
8+
}
9+
10+
public SignatureVerificationException(Algorithm algorithm, Throwable cause) {
11+
super("The Token's Signature resulted invalid when verified using the Algorithm: " + algorithm.name(), cause);
12+
}
13+
}

lib/src/main/java/com/auth0/jwtdecodejava/impl/ClaimImpl.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.fasterxml.jackson.core.JsonProcessingException;
66
import com.fasterxml.jackson.databind.JsonNode;
77
import com.fasterxml.jackson.databind.ObjectMapper;
8-
import com.sun.istack.internal.NotNull;
98

109
import java.lang.reflect.Array;
1110
import java.util.ArrayList;
@@ -17,7 +16,7 @@ class ClaimImpl extends BaseClaim {
1716

1817
private final JsonNode data;
1918

20-
private ClaimImpl(@NotNull JsonNode node) {
19+
private ClaimImpl(JsonNode node) {
2120
this.data = node;
2221
}
2322

@@ -87,12 +86,11 @@ public <T> List<T> asList(Class<T> tClazz) throws JWTException {
8786
return list;
8887
}
8988

90-
public static Claim extractClaim(@NotNull String claimName, @NotNull Map<String, JsonNode> tree) {
89+
public static Claim extractClaim(String claimName, Map<String, JsonNode> tree) {
9190
JsonNode node = tree.get(claimName);
9291
return claimFromNode(node);
9392
}
9493

95-
@NotNull
9694
public static Claim claimFromNode(JsonNode node) {
9795
if (node == null || node.isNull() || node.isMissingNode()) {
9896
return new BaseClaim();

lib/src/main/java/com/auth0/jwtdecodejava/impl/HeaderImpl.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.auth0.jwtdecodejava.impl;
22

3+
import com.auth0.jwtdecodejava.enums.Algorithm;
34
import com.auth0.jwtdecodejava.interfaces.Header;
45
import com.fasterxml.jackson.databind.JsonNode;
56

@@ -16,8 +17,9 @@ class HeaderImpl implements Header {
1617
}
1718

1819
@Override
19-
public String getAlgorithm() {
20-
return extractClaim(ALGORITHM, tree).asString();
20+
public Algorithm getAlgorithm() {
21+
String alg = extractClaim(ALGORITHM, tree).asString();
22+
return Algorithm.parseFrom(alg);
2123
}
2224

2325
@Override

lib/src/main/java/com/auth0/jwtdecodejava/impl/JWTVerifier.java

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,55 @@
11
package com.auth0.jwtdecodejava.impl;
22

33
import com.auth0.jwtdecodejava.JWTDecoder;
4+
import com.auth0.jwtdecodejava.Utils;
5+
import com.auth0.jwtdecodejava.enums.Algorithm;
6+
import com.auth0.jwtdecodejava.exceptions.AlgorithmMismatchException;
47
import com.auth0.jwtdecodejava.exceptions.InvalidClaimException;
58
import com.auth0.jwtdecodejava.exceptions.JWTException;
9+
import com.auth0.jwtdecodejava.exceptions.SignatureVerificationException;
610
import com.auth0.jwtdecodejava.interfaces.JWT;
711

12+
import java.security.InvalidKeyException;
13+
import java.security.NoSuchAlgorithmException;
814
import java.util.Arrays;
915
import java.util.Date;
1016
import java.util.HashMap;
1117
import java.util.Map;
1218

1319
public class JWTVerifier {
20+
private final Algorithm algorithm;
21+
private final String secret;
1422
private final Map<String, Object> claims;
1523

16-
private JWTVerifier() {
24+
private JWTVerifier(Algorithm algorithm, String secret) {
25+
this.algorithm = algorithm;
26+
this.secret = secret;
1727
this.claims = new HashMap<>();
1828
}
1929

20-
public static JWTVerifier init(){
21-
return new JWTVerifier();
30+
public static JWTVerifier init() throws IllegalArgumentException {
31+
return init(Algorithm.none, null);
32+
}
33+
34+
public static JWTVerifier init(Algorithm algorithm, String secret) throws IllegalArgumentException {
35+
if (algorithm == null) {
36+
throw new IllegalArgumentException("The Algorithm cannot be null.");
37+
}
38+
switch (algorithm) {
39+
case HS256:
40+
case HS384:
41+
case HS512:
42+
if (secret == null) {
43+
throw new IllegalArgumentException(String.format("You can't use the %s algorithm without providing a valid Secret.", algorithm.name()));
44+
}
45+
break;
46+
case none:
47+
if (secret != null) {
48+
throw new IllegalArgumentException("You can't use the Algorithm 'none' with a non-null Secret.");
49+
}
50+
default:
51+
}
52+
return new JWTVerifier(algorithm, secret);
2253
}
2354

2455
public JWTVerifier withIssuer(String issuer) {
@@ -56,10 +87,37 @@ public JWTVerifier withJWTId(String jwtId) {
5687
return this;
5788
}
5889

59-
public JWT verify(String jwt) throws JWTException{
60-
JWT decode = JWTDecoder.decode(jwt);
61-
verifyClaims(decode, claims);
62-
return decode;
90+
public JWT verify(String token) throws JWTException {
91+
JWT jwt = JWTDecoder.decode(token);
92+
verifyClaims(jwt, claims);
93+
verifyAlgorithm(jwt, algorithm);
94+
verifySignature(Utils.splitToken(token));
95+
return jwt;
96+
}
97+
98+
private void verifySignature(String[] parts) {
99+
switch (algorithm) {
100+
case HS256:
101+
case HS384:
102+
case HS512:
103+
try {
104+
Utils.verifyHS(parts, secret, algorithm);
105+
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
106+
throw new SignatureVerificationException(algorithm, e);
107+
}
108+
break;
109+
case none:
110+
if (!parts[2].isEmpty()){
111+
throw new SignatureVerificationException(algorithm);
112+
}
113+
default:
114+
}
115+
}
116+
117+
private void verifyAlgorithm(JWT jwt, Algorithm expectedAlgorithm) throws AlgorithmMismatchException, IllegalArgumentException {
118+
if (!expectedAlgorithm.equals(jwt.getAlgorithm())) {
119+
throw new AlgorithmMismatchException("The provided Algorithm doesn't match the one defined in the JWT's Header.");
120+
}
63121
}
64122

65123
private void verifyClaims(JWT jwt, Map<String, Object> claims) {

lib/src/main/java/com/auth0/jwtdecodejava/impl/PayloadImpl.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.auth0.jwtdecodejava.interfaces.Claim;
44
import com.auth0.jwtdecodejava.interfaces.Payload;
55
import com.fasterxml.jackson.databind.JsonNode;
6-
import com.sun.istack.internal.NotNull;
76

87
import java.util.Date;
98
import java.util.Map;
@@ -67,7 +66,7 @@ public String getId() {
6766
}
6867

6968
@Override
70-
public Claim getClaim(@NotNull String name) {
69+
public Claim getClaim(String name) {
7170
return extractClaim(name, tree);
7271
}
7372

0 commit comments

Comments
 (0)