added signing and some unit tests, HMAC signing still doesn't quite work
parent
7a6af8c07d
commit
869a6ddeef
|
@ -0,0 +1,47 @@
|
|||
package org.mitre.jwt;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
|
||||
public class AbstractJwtSigner implements JwtSigner {
|
||||
|
||||
public static final String PLAINTEXT = "none";
|
||||
public static final String HS256 = "HS256";
|
||||
public static final String HS384 = "HS384";
|
||||
public static final String HS512 = "HS512";
|
||||
|
||||
private String algorithm;
|
||||
|
||||
public AbstractJwtSigner(String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the algorithm
|
||||
*/
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param algorithm the algorithm to set
|
||||
*/
|
||||
public void setAlgorithm(String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the 'alg' of the given JWT matches the {@link #algorithm} of this signer
|
||||
*/
|
||||
@Override
|
||||
public void sign(Jwt jwt) {
|
||||
if (!Objects.equal(algorithm, jwt.getHeader().getAlgorithm())) {
|
||||
// algorithm type doesn't match
|
||||
// TODO: should this be an error or should we just fix it in the incoming jwt?
|
||||
// for now, we fix the Jwt
|
||||
jwt.getHeader().setAlgorithm(algorithm);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package org.mitre.jwt;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
public class Hmac256Signer extends AbstractJwtSigner {
|
||||
|
||||
private Mac mac;
|
||||
|
||||
private byte[] passphrase;
|
||||
|
||||
public Hmac256Signer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public Hmac256Signer(byte[] passphrase) {
|
||||
super(HS256);
|
||||
setPassphrase(passphrase);
|
||||
|
||||
try {
|
||||
mac = Mac.getInstance("HMACSHA256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.mitre.jwt.AbstractJwtSigner#sign(org.mitre.jwt.Jwt)
|
||||
*/
|
||||
@Override
|
||||
public void sign(Jwt jwt) {
|
||||
super.sign(jwt);
|
||||
|
||||
if (passphrase == null) {
|
||||
return; // TODO: probably throw some kind of exception
|
||||
}
|
||||
|
||||
try {
|
||||
mac.init(new SecretKeySpec(getPassphrase(), mac.getAlgorithm()));
|
||||
} catch (InvalidKeyException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
mac.update(jwt.getSignatureBase().getBytes());
|
||||
|
||||
byte[] sigBytes = mac.doFinal();
|
||||
|
||||
String sig = new String(Base64.encodeBase64URLSafe(sigBytes));
|
||||
|
||||
// strip off any padding
|
||||
sig = sig.replace("=", "");
|
||||
|
||||
jwt.setSignature(sig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the passphrase
|
||||
*/
|
||||
public byte[] getPassphrase() {
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param passphrase the passphrase to set
|
||||
*/
|
||||
public void setPassphrase(byte[] passphrase) {
|
||||
this.passphrase = passphrase;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,26 +1,54 @@
|
|||
package org.mitre.jwt;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
public class Jwt {
|
||||
|
||||
private JwtHeader header = new JwtHeader();
|
||||
|
||||
private JwtClaims claims = new JwtClaims();
|
||||
private JwtHeader header;
|
||||
|
||||
private JwtClaims claims;
|
||||
|
||||
/**
|
||||
* Base64Url encoded signature string
|
||||
*/
|
||||
private String signature;
|
||||
|
||||
|
||||
|
||||
|
||||
public Jwt() {
|
||||
this.header = new JwtHeader();
|
||||
this.claims = new JwtClaims();
|
||||
this.signature = null; // unsigned by default
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a Jwt from existing components
|
||||
* @param header
|
||||
* @param claims
|
||||
* @param signature
|
||||
*/
|
||||
public Jwt(JwtHeader header, JwtClaims claims, String signature) {
|
||||
super();
|
||||
this.header = header;
|
||||
this.claims = claims;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return the header
|
||||
|
@ -78,15 +106,19 @@ public class Jwt {
|
|||
* Return the canonical encoded string of this JWT
|
||||
*/
|
||||
public String toString() {
|
||||
JsonObject h = header.getAsJsonObject();
|
||||
JsonObject o = claims.getAsJsonObject();
|
||||
|
||||
String h64 = new String(Base64.encodeBase64URLSafe(h.toString().getBytes()));
|
||||
String o64 = new String(Base64.encodeBase64(o.toString().getBytes()));
|
||||
|
||||
return h64 + "." + o64 + "." + Strings.nullToEmpty(this.signature);
|
||||
return getSignatureBase() + Strings.nullToEmpty(this.signature);
|
||||
}
|
||||
|
||||
public String getSignatureBase() {
|
||||
JsonObject h = header.getAsJsonObject();
|
||||
JsonObject c = claims.getAsJsonObject();
|
||||
|
||||
String h64 = new String(Base64.encodeBase64URLSafe(h.toString().getBytes()));
|
||||
String c64 = new String(Base64.encodeBase64URLSafe(c.toString().getBytes()));
|
||||
|
||||
return h64 + "." + c64 + ".";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse a wire-encoded JWT
|
||||
|
@ -101,15 +133,15 @@ public class Jwt {
|
|||
}
|
||||
|
||||
String h64 = parts.get(0);
|
||||
String o64 = parts.get(1);
|
||||
String c64 = parts.get(1);
|
||||
String s64 = parts.get(2);
|
||||
|
||||
JsonParser parser = new JsonParser();
|
||||
|
||||
|
||||
JsonObject hjo = parser.parse(new InputStreamReader(new ByteArrayInputStream(Base64.decodeBase64(h64)))).getAsJsonObject();
|
||||
JsonObject cjo = parser.parse(new InputStreamReader(new ByteArrayInputStream(Base64.decodeBase64(c64)))).getAsJsonObject();
|
||||
|
||||
// shuttle for return value
|
||||
Jwt jwt = new Jwt();
|
||||
Jwt jwt = new Jwt(new JwtHeader(hjo), new JwtClaims(cjo), s64);
|
||||
|
||||
return jwt;
|
||||
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
package org.mitre.jwt;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
public class JwtClaims {
|
||||
|
||||
/**
|
||||
* ISO8601 / RFC3339 Date Format
|
||||
*/
|
||||
public static DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-DD'T'HH:mm:ssz");
|
||||
public static DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
|
||||
|
||||
/*
|
||||
* TODO: Should we instead be using a generic claims map with well-named accessor methods?
|
||||
*/
|
||||
|
||||
private Date expiration;
|
||||
|
||||
private Date notBefore;
|
||||
|
@ -37,6 +45,42 @@ public class JwtClaims {
|
|||
|
||||
}
|
||||
|
||||
public JwtClaims(JsonObject json) {
|
||||
for (Entry<String, JsonElement> element : json.entrySet()) {
|
||||
if (element.getKey().equals("exp")) {
|
||||
expiration = new Date(element.getValue().getAsLong() * 1000L);
|
||||
} else if (element.getKey().equals("nbf")) {
|
||||
notBefore = new Date(element.getValue().getAsLong() * 1000L);
|
||||
} else if (element.getKey().equals("iat")) {
|
||||
issuedAt = new Date(element.getValue().getAsLong() * 1000L);
|
||||
} else if (element.getKey().equals("iss")) {
|
||||
issuer = element.getValue().getAsString();
|
||||
} else if (element.getKey().equals("aud")) {
|
||||
audience = element.getValue().getAsString();
|
||||
} else if (element.getKey().equals("prn")) {
|
||||
principal = element.getValue().getAsString();
|
||||
} else if (element.getKey().equals("jti")) {
|
||||
jwtId = element.getValue().getAsString();
|
||||
} else if (element.getKey().equals("typ")) {
|
||||
type = element.getValue().getAsString();
|
||||
} else if (element.getValue().isJsonPrimitive()){
|
||||
// we handle all primitives in here
|
||||
JsonPrimitive prim = element.getValue().getAsJsonPrimitive();
|
||||
|
||||
if (prim.isBoolean()) {
|
||||
claims.put(element.getKey(), prim.getAsBoolean());
|
||||
} else if (prim.isNumber()) {
|
||||
claims.put(element.getKey(), prim.getAsNumber());
|
||||
} else if (prim.isString()) {
|
||||
claims.put(element.getKey(), prim.getAsString());
|
||||
}
|
||||
} else {
|
||||
// everything else gets handled as a raw JsonElement
|
||||
claims.put(element.getKey(), element.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the expiration
|
||||
*/
|
||||
|
@ -179,15 +223,15 @@ public class JwtClaims {
|
|||
JsonObject o = new JsonObject();
|
||||
|
||||
if (this.expiration != null) {
|
||||
o.addProperty("exp", dateFormat.format(this.expiration));
|
||||
o.addProperty("exp", this.expiration.getTime() / 1000L);
|
||||
}
|
||||
|
||||
if (this.notBefore != null) {
|
||||
o.addProperty("nbf", dateFormat.format(this.notBefore));
|
||||
o.addProperty("nbf", this.notBefore.getTime() / 1000L);
|
||||
}
|
||||
|
||||
if (this.issuedAt != null) {
|
||||
o.addProperty("iat", dateFormat.format(this.issuedAt));
|
||||
o.addProperty("iat", this.issuedAt.getTime() / 1000L);
|
||||
}
|
||||
|
||||
if (this.issuer != null) {
|
||||
|
@ -212,7 +256,9 @@ public class JwtClaims {
|
|||
|
||||
if (this.claims != null) {
|
||||
for (Map.Entry<String, Object> claim : this.claims.entrySet()) {
|
||||
if (claim.getValue() instanceof String) {
|
||||
if (claim.getValue() instanceof JsonElement) {
|
||||
o.add(claim.getKey(), (JsonElement)claim.getValue());
|
||||
} else if (claim.getValue() instanceof String) {
|
||||
o.addProperty(claim.getKey(), (String)claim.getValue());
|
||||
} else if (claim.getValue() instanceof Number) {
|
||||
o.addProperty(claim.getKey(), (Number)claim.getValue());
|
||||
|
@ -233,4 +279,14 @@ public class JwtClaims {
|
|||
return o;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JwtClaims [expiration=" + expiration + ", notBefore=" + notBefore + ", issuedAt=" + issuedAt + ", issuer=" + issuer + ", audience=" + audience + ", principal=" + principal + ", jwtId=" + jwtId + ", type=" + type + ", claims=" + claims + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -2,11 +2,17 @@ package org.mitre.jwt;
|
|||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
public class JwtHeader {
|
||||
|
||||
/*
|
||||
* TODO: Should we instead be using a generic claims map with well-named accessor methods?
|
||||
*/
|
||||
|
||||
private String type;
|
||||
|
||||
private String algorithm;
|
||||
|
@ -16,10 +22,32 @@ public class JwtHeader {
|
|||
private Map<String, Object> claims = new HashMap<String, Object>();
|
||||
|
||||
|
||||
/**
|
||||
* Make an empty header
|
||||
*/
|
||||
public JwtHeader() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build a header from a JSON object
|
||||
* @param json
|
||||
*/
|
||||
public JwtHeader(JsonObject json) {
|
||||
|
||||
for (Entry<String, JsonElement> element : json.entrySet()) {
|
||||
if (element.getKey().equals("typ")) {
|
||||
this.type = json.get("typ").getAsString();
|
||||
} else if (element.getKey().equals("alg")) {
|
||||
this.algorithm = json.get("alg").getAsString();
|
||||
} else if (element.getKey().equals("enc")) {
|
||||
this.encryptionMethod = json.get("enc").getAsString();
|
||||
} else {
|
||||
// TODO: this assumes string encoding for extensions, probably not quite correct
|
||||
claims.put(element.getKey(), element.getValue().getAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the type
|
||||
|
@ -97,7 +125,9 @@ public class JwtHeader {
|
|||
public JsonObject getAsJsonObject() {
|
||||
JsonObject o = new JsonObject();
|
||||
|
||||
o.addProperty("typ", this.type);
|
||||
if (this.type != null) {
|
||||
o.addProperty("typ", this.type);
|
||||
}
|
||||
if (this.algorithm != null) {
|
||||
o.addProperty("alg", this.algorithm);
|
||||
}
|
||||
|
@ -129,5 +159,14 @@ public class JwtHeader {
|
|||
return o;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JwtHeader [type=" + type + ", algorithm=" + algorithm + ", encryptionMethod=" + encryptionMethod + ", claims=" + claims + "]";
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package org.mitre.jwt;
|
||||
|
||||
public class PlaintextSigner extends AbstractJwtSigner {
|
||||
|
||||
public PlaintextSigner() {
|
||||
super(PLAINTEXT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sign(Jwt jwt) {
|
||||
super.sign(jwt);
|
||||
|
||||
jwt.setSignature("");
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package org.mitre.jwt;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
public class JwtTest {
|
||||
|
||||
@Test
|
||||
public void testToStringPlaintext() {
|
||||
Jwt jwt = new Jwt();
|
||||
jwt.getHeader().setAlgorithm("none");
|
||||
jwt.getClaims().setIssuer("joe");
|
||||
jwt.getClaims().setExpiration(new Date(1300819380L * 1000L));
|
||||
jwt.getClaims().setClaim("http://example.com/is_root", Boolean.TRUE);
|
||||
|
||||
// sign it with a blank signature
|
||||
JwtSigner signer = new PlaintextSigner();
|
||||
signer.sign(jwt);
|
||||
|
||||
/*
|
||||
* Expected string based on the following structures, serialized exactly as folows and base64 encoded:
|
||||
*
|
||||
* header: {"alg":"none"}
|
||||
* claims: {"exp":1300819380,"iss":"joe","http://example.com/is_root":true}
|
||||
*/
|
||||
String expected = "eyJhbGciOiJub25lIn0.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.";
|
||||
|
||||
String actual = jwt.toString();
|
||||
|
||||
assertThat(actual, equalTo(expected));
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHmacSignature() {
|
||||
Jwt jwt = new Jwt();
|
||||
jwt.getHeader().setType("JWT");
|
||||
jwt.getHeader().setAlgorithm("HS256");
|
||||
jwt.getClaims().setIssuer("joe");
|
||||
jwt.getClaims().setExpiration(new Date(1300819380L * 1000L));
|
||||
jwt.getClaims().setClaim("http://example.com/is_root", Boolean.TRUE);
|
||||
|
||||
// sign it
|
||||
byte[] key = "secret".getBytes();
|
||||
|
||||
JwtSigner signer = new Hmac256Signer(key);
|
||||
|
||||
signer.sign(jwt);
|
||||
|
||||
String expected = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.vQqHHhblAtGiFs7q7nPt9Q";
|
||||
|
||||
String actual = jwt.toString();
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParse() {
|
||||
String source = "eyJhbGciOiJub25lIn0.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.";
|
||||
|
||||
|
||||
Jwt jwt = Jwt.parse(source);
|
||||
|
||||
assertThat(jwt.getHeader().getAlgorithm(), equalTo(AbstractJwtSigner.PLAINTEXT));
|
||||
assertThat(jwt.getClaims().getIssuer(), equalTo("joe"));
|
||||
assertThat(jwt.getClaims().getExpiration(), equalTo(new Date(1300819380L * 1000L)));
|
||||
assertThat((Boolean)jwt.getClaims().getClaim("http://example.com/is_root"), equalTo(Boolean.TRUE));
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue