Skip to content

Commit 228f900

Browse files
authored
Merge pull request auth0#86 from auth0/feat-rs-verify
Add RS Algorithm Verification
2 parents e4876d8 + 7338cbe commit 228f900

39 files changed

Lines changed: 970 additions & 505 deletions

lib/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies {
88
compile fileTree(dir: 'libs', include: ['*.jar'])
99
compile 'com.fasterxml.jackson.core:jackson-databind:2.8.4'
1010
compile 'commons-codec:commons-codec:1.10'
11+
compile 'org.bouncycastle:bcprov-jdk15on:1.55'
1112
testCompile 'junit:junit:4.12'
1213
testCompile 'org.hamcrest:hamcrest-library:1.3'
1314
testCompile 'org.mockito:mockito-core:2.2.8'

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

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

3-
import com.auth0.jwtdecodejava.enums.Algorithm;
4-
import com.auth0.jwtdecodejava.exceptions.JWTException;
3+
import com.auth0.jwtdecodejava.algorithms.Algorithm;
4+
import com.auth0.jwtdecodejava.exceptions.JWTDecodeException;
55
import com.auth0.jwtdecodejava.impl.JWTParser;
66
import com.auth0.jwtdecodejava.interfaces.Claim;
77
import com.auth0.jwtdecodejava.interfaces.Header;
@@ -10,27 +10,44 @@
1010

1111
import java.util.Date;
1212

13-
import static com.auth0.jwtdecodejava.Utils.base64Decode;
14-
13+
/**
14+
* The JWTDecoder class holds the decode method to parse a given Token into it's JWT representation.
15+
*/
1516
public final class JWTDecoder implements JWT {
1617

1718
private Header header;
1819
private Payload payload;
1920
private String signature;
2021

21-
private JWTDecoder(String jwt) {
22+
private JWTDecoder(String jwt) throws JWTDecodeException {
2223
parseToken(jwt);
2324
}
2425

25-
public static JWT decode(String jwt) {
26-
return new JWTDecoder(jwt);
26+
/**
27+
* Decode a given Token into a JWT instance.
28+
* Note that this method doesn't verify the JWT's signature! Use it only if you trust the issuer of the Token.
29+
*
30+
* @param token the String representation of the JWT.
31+
* @return a decoded JWT.
32+
* @throws JWTDecodeException if any part of the Token contained an invalid JWT or JSON format.
33+
*/
34+
public static JWT decode(String token) throws JWTDecodeException {
35+
return new JWTDecoder(token);
2736
}
2837

29-
private void parseToken(String token) throws JWTException {
30-
final String[] parts = Utils.splitToken(token);
38+
private void parseToken(String token) throws JWTDecodeException {
39+
final String[] parts = SignUtils.splitToken(token);
3140
final JWTParser converter = new JWTParser();
32-
header = converter.parseHeader(base64Decode(parts[0]));
33-
payload = converter.parsePayload(base64Decode(parts[1]));
41+
String headerJson;
42+
String payloadJson;
43+
try {
44+
headerJson = SignUtils.base64Decode(parts[0]);
45+
payloadJson = SignUtils.base64Decode(parts[1]);
46+
} catch (NullPointerException e) {
47+
throw new JWTDecodeException("The UTF-8 Charset isn't initialized.", e);
48+
}
49+
header = converter.parseHeader(headerJson);
50+
payload = converter.parsePayload(payloadJson);
3451
signature = parts[2];
3552
}
3653

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package com.auth0.jwtdecodejava;
2+
3+
import com.auth0.jwtdecodejava.algorithms.Algorithm;
4+
import com.auth0.jwtdecodejava.algorithms.HSAlgorithm;
5+
import com.auth0.jwtdecodejava.algorithms.NoneAlgorithm;
6+
import com.auth0.jwtdecodejava.algorithms.RSAlgorithm;
7+
import com.auth0.jwtdecodejava.exceptions.AlgorithmMismatchException;
8+
import com.auth0.jwtdecodejava.exceptions.InvalidClaimException;
9+
import com.auth0.jwtdecodejava.exceptions.JWTVerificationException;
10+
import com.auth0.jwtdecodejava.exceptions.SignatureVerificationException;
11+
import com.auth0.jwtdecodejava.impl.PublicClaims;
12+
import com.auth0.jwtdecodejava.interfaces.JWT;
13+
14+
import java.security.InvalidKeyException;
15+
import java.security.NoSuchAlgorithmException;
16+
import java.security.PublicKey;
17+
import java.security.SignatureException;
18+
import java.util.Arrays;
19+
import java.util.Date;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import static com.auth0.jwtdecodejava.algorithms.NoneAlgorithm.none;
24+
25+
/**
26+
* The JWTVerifier class holds the verify method to assert that a given Token has not only a proper JWT format, but also it's signature matches.
27+
*/
28+
public class JWTVerifier {
29+
private final Algorithm algorithm;
30+
private final String secret;
31+
private final PublicKey key;
32+
private final Map<String, Object> claims;
33+
34+
private JWTVerifier(Algorithm algorithm, String secret, PublicKey key) {
35+
this.algorithm = algorithm;
36+
this.key = key;
37+
this.secret = secret;
38+
this.claims = new HashMap<>();
39+
}
40+
41+
/**
42+
* Initialize a JWTVerifier instance using the Algorithm "none".
43+
*
44+
* @return a JWTVerifier instance to configure.
45+
*/
46+
public static JWTVerifier init() {
47+
return init(none, null, null);
48+
}
49+
50+
/**
51+
* Initialize a JWTVerifier instance using a HS Algorithm.
52+
*
53+
* @param algorithm a HSAlgorithm. Valid values are HS256, HS384, HS512.
54+
* @param secret to use when verifying the signature.
55+
* @return a JWTVerifier instance to configure.
56+
* @throws IllegalArgumentException if the provided algorithm is null or if the secret is null.
57+
*/
58+
public static JWTVerifier init(HSAlgorithm algorithm, String secret) throws IllegalArgumentException {
59+
return init(algorithm, null, secret);
60+
}
61+
62+
/**
63+
* Initialize a JWTVerifier instance using a RS Algorithm.
64+
*
65+
* @param algorithm a RSAlgorithm. Valid values are RS256, RS384, RS512.
66+
* @param publicKey to use when verifying the signature.
67+
* @return a JWTVerifier instance to configure.
68+
* @throws IllegalArgumentException if the provided algorithm is null or if the publicKey is null.
69+
*/
70+
public static JWTVerifier init(RSAlgorithm algorithm, PublicKey publicKey) throws IllegalArgumentException {
71+
return init(algorithm, publicKey, null);
72+
}
73+
74+
private static JWTVerifier init(Algorithm algorithm, PublicKey publicKey, String secret) throws IllegalArgumentException {
75+
if (algorithm == null) {
76+
throw new IllegalArgumentException("The Algorithm cannot be null.");
77+
}
78+
if (algorithm instanceof HSAlgorithm && secret == null) {
79+
throw new IllegalArgumentException(String.format("You can't use the %s algorithm without providing a valid Secret.", algorithm.name()));
80+
}
81+
if (algorithm instanceof RSAlgorithm && publicKey == null) {
82+
throw new IllegalArgumentException(String.format("You can't use the %s algorithm without providing a valid PublicKey.", algorithm.name()));
83+
}
84+
return new JWTVerifier(algorithm, secret, publicKey);
85+
}
86+
87+
/**
88+
* Require a specific Issuer ("iss") claim.
89+
*
90+
* @return this same JWTVerifier instance.
91+
*/
92+
public JWTVerifier withIssuer(String issuer) {
93+
requireClaim(PublicClaims.ISSUER, issuer);
94+
return this;
95+
}
96+
97+
/**
98+
* Require a specific Subject ("sub") claim.
99+
*
100+
* @return this same JWTVerifier instance.
101+
*/
102+
public JWTVerifier withSubject(String subject) {
103+
requireClaim(PublicClaims.SUBJECT, subject);
104+
return this;
105+
}
106+
107+
/**
108+
* Require a specific Audience ("aud") claim.
109+
*
110+
* @return this same JWTVerifier instance.
111+
*/
112+
public JWTVerifier withAudience(String[] audience) {
113+
requireClaim(PublicClaims.AUDIENCE, audience);
114+
return this;
115+
}
116+
117+
/**
118+
* Require a specific Expires At ("exp") claim.
119+
*
120+
* @return this same JWTVerifier instance.
121+
*/
122+
public JWTVerifier withExpiresAt(Date expiresAt) {
123+
requireClaim(PublicClaims.EXPIRES_AT, expiresAt);
124+
return this;
125+
}
126+
127+
/**
128+
* Require a specific Not Before ("nbf") claim.
129+
*
130+
* @return this same JWTVerifier instance.
131+
*/
132+
public JWTVerifier withNotBefore(Date notBefore) {
133+
requireClaim(PublicClaims.NOT_BEFORE, notBefore);
134+
return this;
135+
}
136+
137+
/**
138+
* Require a specific Issued At ("iat") claim.
139+
*
140+
* @return this same JWTVerifier instance.
141+
*/
142+
public JWTVerifier withIssuedAt(Date issuedAt) {
143+
requireClaim(PublicClaims.ISSUED_AT, issuedAt);
144+
return this;
145+
}
146+
147+
/**
148+
* Require a specific JWT Id ("jti") claim.
149+
*
150+
* @return this same JWTVerifier instance.
151+
*/
152+
public JWTVerifier withJWTId(String jwtId) {
153+
requireClaim(PublicClaims.JWT_ID, jwtId);
154+
return this;
155+
}
156+
157+
/**
158+
* Perform the verification against the given Token, using any previous configured options.
159+
*
160+
* @param token the String representation of the JWT.
161+
* @return a verified JWT.
162+
* @throws JWTVerificationException if any of the required contents inside the JWT is invalid.
163+
*/
164+
public JWT verify(String token) throws JWTVerificationException {
165+
JWT jwt = JWTDecoder.decode(token);
166+
verifyAlgorithm(jwt, algorithm);
167+
verifySignature(SignUtils.splitToken(token));
168+
verifyClaims(jwt, claims);
169+
return jwt;
170+
}
171+
172+
private void verifySignature(String[] parts) throws SignatureVerificationException {
173+
if (algorithm instanceof HSAlgorithm) {
174+
try {
175+
SignUtils.verifyHS((HSAlgorithm) algorithm, parts, secret);
176+
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
177+
throw new SignatureVerificationException(algorithm, e);
178+
}
179+
} else if (algorithm instanceof RSAlgorithm) {
180+
try {
181+
SignUtils.verifyRS((RSAlgorithm) algorithm, parts, key);
182+
} catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) {
183+
throw new SignatureVerificationException(algorithm, e);
184+
}
185+
} else if (algorithm instanceof NoneAlgorithm && !parts[2].isEmpty()) {
186+
throw new SignatureVerificationException(algorithm);
187+
}
188+
}
189+
190+
private void verifyAlgorithm(JWT jwt, Algorithm expectedAlgorithm) throws AlgorithmMismatchException {
191+
if (!expectedAlgorithm.equals(jwt.getAlgorithm())) {
192+
throw new AlgorithmMismatchException("The provided Algorithm doesn't match the one defined in the JWT's Header.");
193+
}
194+
}
195+
196+
private void verifyClaims(JWT jwt, Map<String, Object> claims) {
197+
for (Map.Entry<String, Object> entry : claims.entrySet()) {
198+
assertValidClaim(jwt, entry.getKey(), entry.getValue());
199+
}
200+
}
201+
202+
private void assertValidClaim(JWT jwt, String claimName, Object expectedValue) throws InvalidClaimException {
203+
boolean isValid;
204+
if (PublicClaims.AUDIENCE.equals(claimName)) {
205+
isValid = Arrays.equals(jwt.getAudience(), (String[]) expectedValue);
206+
} else if (PublicClaims.NOT_BEFORE.equals(claimName) || PublicClaims.EXPIRES_AT.equals(claimName) || PublicClaims.ISSUED_AT.equals(claimName)) {
207+
Date dateValue = (Date) expectedValue;
208+
isValid = dateValue.equals(jwt.getClaim(claimName).asDate());
209+
} else {
210+
String stringValue = (String) expectedValue;
211+
isValid = stringValue.equals(jwt.getClaim(claimName).asString());
212+
}
213+
214+
if (!isValid) {
215+
throw new InvalidClaimException(String.format("The Claim '%s' value doesn't match the required one.", claimName));
216+
}
217+
}
218+
219+
private void requireClaim(String name, Object value) {
220+
if (value == null) {
221+
claims.remove(name);
222+
return;
223+
}
224+
claims.put(name, value);
225+
}
226+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.auth0.jwtdecodejava;
2+
3+
import com.auth0.jwtdecodejava.algorithms.HSAlgorithm;
4+
import com.auth0.jwtdecodejava.algorithms.RSAlgorithm;
5+
import com.auth0.jwtdecodejava.exceptions.JWTDecodeException;
6+
import org.apache.commons.codec.binary.Base64;
7+
import org.apache.commons.codec.binary.StringUtils;
8+
9+
import javax.crypto.Mac;
10+
import javax.crypto.spec.SecretKeySpec;
11+
import java.security.*;
12+
13+
class SignUtils {
14+
15+
/**
16+
* Decodes a given String from it's Base64 string representation into a UTF-8 String.
17+
*
18+
* @param source the source of the decode process.
19+
* @return a UTF-8 String representing the Base64 decoded source.
20+
* @throws NullPointerException if the UTF-8 Charset isn't initialized.
21+
*/
22+
static String base64Decode(String source) throws NullPointerException {
23+
return StringUtils.newStringUtf8(Base64.decodeBase64(source));
24+
}
25+
26+
/**
27+
* Encodes a given String into it's Base64 string representation.
28+
*
29+
* @param source the source of the decode process.
30+
* @return a UTF-8 String encoded into it's Base64 representation.
31+
* @throws NullPointerException if the UTF-8 Charset isn't initialized.
32+
* @throws IllegalArgumentException if the source string is too long.
33+
*/
34+
static String base64Encode(String source) throws NullPointerException, IllegalArgumentException {
35+
return StringUtils.newStringUtf8(Base64.encodeBase64(source.getBytes(), false, true));
36+
}
37+
38+
/**
39+
* Splits the given token on the "." chars into a String array with 3 parts.
40+
*
41+
* @param token the string to split.
42+
* @return the array representing the 3 parts of the token.
43+
* @throws JWTDecodeException if the Token doesn't have 3 parts.
44+
*/
45+
static String[] splitToken(String token) throws JWTDecodeException {
46+
String[] parts = token.split("\\.");
47+
if (parts.length == 2 && token.endsWith(".")) {
48+
//Tokens with alg='none' have empty String as Signature.
49+
parts = new String[]{parts[0], parts[1], ""};
50+
}
51+
if (parts.length != 3) {
52+
throw new JWTDecodeException(String.format("The token was expected to have 3 parts, but got %s.", parts.length));
53+
}
54+
return parts;
55+
}
56+
57+
/**
58+
* Verify the given JWT parts using a specific HS Algorithm and a given secret.
59+
*
60+
* @param algorithm the HSAlgorithm to use. Must be one of HS256, HS384, or HS512.
61+
* @param jwtParts a valid array of size 3 representing the JWT parts.
62+
* @param secret the secret used when signing the token's content.
63+
* @return whether the Token's signature is valid or not.
64+
* @throws NoSuchAlgorithmException if the chosen algorithm isn't present.
65+
* @throws InvalidKeyException
66+
*/
67+
static boolean verifyHS(HSAlgorithm algorithm, String[] jwtParts, String secret) throws NoSuchAlgorithmException, InvalidKeyException {
68+
if (secret == null) {
69+
throw new IllegalArgumentException("The Secret cannot be null");
70+
}
71+
if (algorithm == null) {
72+
throw new IllegalArgumentException("The Algorithm must be one of HS256, HS384, or HS512.");
73+
}
74+
75+
Mac mac = Mac.getInstance(algorithm.describe());
76+
mac.init(new SecretKeySpec(secret.getBytes(), algorithm.describe()));
77+
String message = String.format("%s.%s", jwtParts[0], jwtParts[1]);
78+
byte[] result = mac.doFinal(message.getBytes());
79+
return MessageDigest.isEqual(result, Base64.decodeBase64(jwtParts[2]));
80+
}
81+
82+
static boolean verifyRS(RSAlgorithm algorithm, String[] jwtParts, PublicKey publicKey) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException {
83+
if (publicKey == null) {
84+
throw new IllegalArgumentException("The PublicKey cannot be null");
85+
}
86+
if (algorithm == null) {
87+
throw new IllegalArgumentException("The Algorithm must be one of RS256, RS384, or RS512.");
88+
}
89+
90+
final String content = String.format("%s.%s", jwtParts[0], jwtParts[1]);
91+
Signature s = Signature.getInstance(algorithm.describe());
92+
s.initVerify(publicKey);
93+
s.update(content.getBytes());
94+
return s.verify(Base64.decodeBase64(jwtParts[2]));
95+
}
96+
}

0 commit comments

Comments
 (0)