Skip to content

Commit 03f04b5

Browse files
author
Samuli Kärkkäinen
committed
Complete change of encode() API and implementation
1 parent 0bd0679 commit 03f04b5

File tree

11 files changed

+661
-279
lines changed

11 files changed

+661
-279
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ node_modules
33
# Ignore Eclipse stuff
44
.project
55
.settings
6+
.classpath
67

78
# Ignore Java and IntelliJ IDEA stuff
89
.idea

src/main/java/com/auth0/jwt/ClaimSet.java

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
package com.auth0.jwt;
2+
3+
import java.io.UnsupportedEncodingException;
4+
import java.net.URI;
5+
import java.net.URISyntaxException;
6+
import java.util.ArrayList;
7+
import java.util.Collection;
8+
import java.util.HashMap;
9+
import java.util.Iterator;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.UUID;
13+
14+
import javax.crypto.Mac;
15+
import javax.crypto.spec.SecretKeySpec;
16+
import javax.naming.OperationNotSupportedException;
17+
18+
import org.apache.commons.codec.binary.Base64;
19+
20+
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
22+
import com.fasterxml.jackson.databind.node.ObjectNode;
23+
24+
/**
25+
* JwtSigner implementation based on the Ruby implementation from http://jwt.io
26+
* No support for RSA encryption at present
27+
*/
28+
public class JWTSigner {
29+
private final String secret;
30+
31+
public JWTSigner(String secret) {
32+
this.secret = secret;
33+
}
34+
35+
/**
36+
* Generate a JSON Web Token.
37+
* using the default algorithm HMAC SHA-256 ("HS256")
38+
* and no claims automatically set.
39+
*
40+
* @param claims A map of the JWT claims that form the payload. Registered claims
41+
* must be of appropriate Java datatype as following:
42+
* <ul>
43+
* <li>iss, sub: String
44+
* <li>exp, nbf, iat, jti: numeric, eg. Long
45+
* <li>aud: String, or Collection&lt;String&gt;
46+
* </ul>
47+
* All claims with a null value are left out the JWT.
48+
* Any claims set automatically as specified in
49+
* the "options" parameter override claims in this map.
50+
*
51+
* @param secret Key to use in signing. Used as-is without Base64 encoding.
52+
*
53+
* @param options Allow choosing the signing algorithm, and automatic setting of some registered claims.
54+
*/
55+
public String sign(Map<String, Object> claims, Options options) {
56+
Algorithm algorithm = Algorithm.HS256;
57+
if (options != null && options.algorithm != null)
58+
algorithm = options.algorithm;
59+
60+
List<String> segments = new ArrayList<String>();
61+
try {
62+
segments.add(encodedHeader(algorithm));
63+
segments.add(encodedPayload(claims, options));
64+
segments.add(encodedSignature(join(segments, "."), algorithm));
65+
} catch (Exception e) {
66+
throw (e instanceof RuntimeException) ? (RuntimeException) e : new RuntimeException(e);
67+
}
68+
69+
return join(segments, ".");
70+
}
71+
72+
/**
73+
* Generate a JSON Web Token using the default algorithm HMAC SHA-256 ("HS256")
74+
* and no claims automatically set.
75+
*
76+
* @param secret Key to use in signing. Used as-is without Base64 encoding.
77+
*
78+
* For details, see the two parameter variant of this method.
79+
*/
80+
public String sign(Map<String, Object> claims) {
81+
return sign(claims, null);
82+
}
83+
84+
/**
85+
* Generate the header part of a JSON web token.
86+
*/
87+
private String encodedHeader(Algorithm algorithm) throws UnsupportedEncodingException {
88+
if (algorithm == null) { // default the algorithm if not specified
89+
algorithm = Algorithm.HS256;
90+
}
91+
92+
// create the header
93+
ObjectNode header = JsonNodeFactory.instance.objectNode();
94+
header.put("type", "JWT");
95+
header.put("alg", algorithm.name());
96+
97+
return base64UrlEncode(header.toString().getBytes("UTF-8"));
98+
}
99+
100+
/**
101+
* Generate the JSON web token payload string from the claims.
102+
* @param options
103+
*/
104+
private String encodedPayload(Map<String, Object> _claims, Options options) throws Exception {
105+
Map<String, Object> claims = new HashMap<String, Object>(_claims);
106+
enforceStringOrURI(claims, "iss");
107+
enforceStringOrURI(claims, "sub");
108+
enforceStringOrURICollection(claims, "aud");
109+
enforceIntDate(claims, "exp");
110+
enforceIntDate(claims, "nbf");
111+
enforceIntDate(claims, "iat");
112+
enforceString(claims, "jti");
113+
114+
if (options != null)
115+
processPayloadOptions(claims, options);
116+
117+
String payload = new ObjectMapper().writeValueAsString(claims);
118+
return base64UrlEncode(payload.getBytes("UTF-8"));
119+
}
120+
121+
private void processPayloadOptions(Map<String, Object> claims, Options options) {
122+
long now = System.currentTimeMillis() / 1000l;
123+
if (options.expirySeconds != null)
124+
claims.put("exp", now + options.expirySeconds);
125+
if (options.notValidBeforeLeeway != null)
126+
claims.put("nbf", now - options.notValidBeforeLeeway);
127+
if (options.isIssuedAt())
128+
claims.put("iat", now);
129+
if (options.isJwtId())
130+
claims.put("jti", UUID.randomUUID().toString());
131+
}
132+
133+
private void enforceIntDate(Map<String, Object> claims, String claimName) {
134+
Object value = handleNullValue(claims, claimName);
135+
if (value == null)
136+
return;
137+
if (!(value instanceof Number)) {
138+
throw new RuntimeException(String.format("Claim '%s' is invalid: must be an instance of Number", claimName));
139+
}
140+
long longValue = ((Number) value).longValue();
141+
if (longValue < 0)
142+
throw new RuntimeException(String.format("Claim '%s' is invalid: must be non-negative", claimName));
143+
claims.put(claimName, longValue);
144+
}
145+
146+
private void enforceStringOrURICollection(Map<String, Object> claims, String claimName) {
147+
Object values = handleNullValue(claims, claimName);
148+
if (values == null)
149+
return;
150+
if (values instanceof Collection) {
151+
@SuppressWarnings({ "unchecked" })
152+
Iterator<Object> iterator = ((Collection<Object>) values).iterator();
153+
while (iterator.hasNext()) {
154+
Object value = iterator.next();
155+
String error = checkStringOrURI(value);
156+
if (error != null)
157+
throw new RuntimeException(String.format("Claim 'aud' element is invalid: %s", error));
158+
}
159+
} else {
160+
enforceStringOrURI(claims, "aud");
161+
}
162+
}
163+
164+
private void enforceStringOrURI(Map<String, Object> claims, String claimName) {
165+
Object value = handleNullValue(claims, claimName);
166+
if (value == null)
167+
return;
168+
String error = checkStringOrURI(value);
169+
if (error != null)
170+
throw new RuntimeException(String.format("Claim '%s' is invalid: %s", claimName, error));
171+
}
172+
173+
private void enforceString(Map<String, Object> claims, String claimName) {
174+
Object value = handleNullValue(claims, claimName);
175+
if (value == null)
176+
return;
177+
if (!(value instanceof String))
178+
throw new RuntimeException(String.format("Claim '%s' is invalid: not a string", claimName));
179+
}
180+
181+
private Object handleNullValue(Map<String, Object> claims, String claimName) {
182+
if (! claims.containsKey(claimName))
183+
return null;
184+
Object value = claims.get(claimName);
185+
if (value == null) {
186+
claims.remove(claimName);
187+
return null;
188+
}
189+
return value;
190+
}
191+
192+
private String checkStringOrURI(Object value) {
193+
if (!(value instanceof String))
194+
return "not a string";
195+
String stringOrUri = (String) value;
196+
if (!stringOrUri.contains(":"))
197+
return null;
198+
try {
199+
new URI(stringOrUri);
200+
} catch (URISyntaxException e) {
201+
return "not a valid URI";
202+
}
203+
return null;
204+
}
205+
206+
/**
207+
* Sign the header and payload
208+
*/
209+
private String encodedSignature(String signingInput, Algorithm algorithm) throws Exception {
210+
byte[] signature = sign(algorithm, signingInput, secret);
211+
return base64UrlEncode(signature);
212+
}
213+
214+
/**
215+
* Safe URL encode a byte array to a String
216+
*/
217+
private String base64UrlEncode(byte[] str) {
218+
return new String(Base64.encodeBase64URLSafe(str));
219+
}
220+
221+
/**
222+
* Switch the signing algorithm based on input, RSA not supported
223+
*/
224+
private static byte[] sign(Algorithm algorithm, String msg, String secret) throws Exception {
225+
switch (algorithm) {
226+
case HS256:
227+
case HS384:
228+
case HS512:
229+
return signHmac(algorithm, msg, secret);
230+
case RS256:
231+
case RS384:
232+
case RS512:
233+
default:
234+
throw new OperationNotSupportedException("Unsupported signing method");
235+
}
236+
}
237+
238+
/**
239+
* Sign an input string using HMAC and return the encrypted bytes
240+
*/
241+
private static byte[] signHmac(Algorithm algorithm, String msg, String secret) throws Exception {
242+
Mac mac = Mac.getInstance(algorithm.getValue());
243+
mac.init(new SecretKeySpec(secret.getBytes(), algorithm.getValue()));
244+
return mac.doFinal(msg.getBytes());
245+
}
246+
247+
private String join(List<String> input, String on) {
248+
int size = input.size();
249+
int count = 1;
250+
StringBuilder joined = new StringBuilder();
251+
for (String string : input) {
252+
joined.append(string);
253+
if (count < size) {
254+
joined.append(on);
255+
}
256+
count++;
257+
}
258+
259+
return joined.toString();
260+
}
261+
262+
/**
263+
* An option object for JWT signing operation. Allow choosing the algorithm, and/or specifying
264+
* claims to be automatically set.
265+
*/
266+
public static class Options {
267+
private Algorithm algorithm;
268+
private Integer expirySeconds;
269+
private Integer notValidBeforeLeeway;
270+
private boolean issuedAt;
271+
private boolean jwtId;
272+
273+
public Algorithm getAlgorithm() {
274+
return algorithm;
275+
}
276+
/**
277+
* Algorithm to sign JWT with. Default is <code>HS256</code>.
278+
*/
279+
public Options setAlgorithm(Algorithm algorithm) {
280+
this.algorithm = algorithm;
281+
return this;
282+
}
283+
284+
285+
public Integer getExpirySeconds() {
286+
return expirySeconds;
287+
}
288+
/**
289+
* Set JWT claim "exp" to current timestamp plus this value.
290+
* Overrides content of <code>claims</code> in <code>sign()</code>.
291+
*/
292+
public Options setExpirySeconds(Integer expirySeconds) {
293+
this.expirySeconds = expirySeconds;
294+
return this;
295+
}
296+
297+
public Integer getNotValidBeforeLeeway() {
298+
return notValidBeforeLeeway;
299+
}
300+
/**
301+
* Set JWT claim "nbf" to current timestamp minus this value.
302+
* Overrides content of <code>claims</code> in <code>sign()</code>.
303+
*/
304+
public Options setNotValidBeforeLeeway(Integer notValidBeforeLeeway) {
305+
this.notValidBeforeLeeway = notValidBeforeLeeway;
306+
return this;
307+
}
308+
309+
public boolean isIssuedAt() {
310+
return issuedAt;
311+
}
312+
/**
313+
* Set JWT claim "iat" to current timestamp. Defaults to false.
314+
* Overrides content of <code>claims</code> in <code>sign()</code>.
315+
*/
316+
public Options setIssuedAt(boolean issuedAt) {
317+
this.issuedAt = issuedAt;
318+
return this;
319+
}
320+
321+
public boolean isJwtId() {
322+
return jwtId;
323+
}
324+
/**
325+
* Set JWT claim "jti" to a pseudo random unique value (type 4 UUID). Defaults to false.
326+
* Overrides content of <code>claims</code> in <code>sign()</code>.
327+
*/
328+
public Options setJwtId(boolean jwtId) {
329+
this.jwtId = jwtId;
330+
return this;
331+
}
332+
}
333+
}

src/main/java/com/auth0/jwt/JwtProxy.java

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)