diff --git a/README.md b/README.md index 487c06d..c341369 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # JWTs for Java + This is not an officially supported Google product [![CircleCI](https://img.shields.io/circleci/project/github/auth0/java-jwt.svg?style=flat-square)](https://circleci.com/gh/auth0/java-jwt/tree/master) diff --git a/lib/build.gradle b/lib/build.gradle index 2a7694c..1a53fd7 100644 --- a/lib/build.gradle +++ b/lib/build.gradle @@ -37,6 +37,7 @@ dependencies { compile 'com.fasterxml.jackson.core:jackson-databind:2.9.2' compile 'commons-codec:commons-codec:1.11' compile 'com.google.code.gson:gson:2.8.2' + compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5' testCompile 'org.bouncycastle:bcprov-jdk15on:1.58' testCompile 'junit:junit:4.12' testCompile 'net.jodah:concurrentunit:0.4.3' diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/JWEHeader.java b/lib/src/main/java/com/auth0/jwt/oicmsg/JWEHeader.java new file mode 100644 index 0000000..6606f59 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/JWEHeader.java @@ -0,0 +1,4 @@ +package com.auth0.jwt.oicmsg; + +public class JWEHeader { +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/JWSHeader.java b/lib/src/main/java/com/auth0/jwt/oicmsg/JWSHeader.java new file mode 100644 index 0000000..306515c --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/JWSHeader.java @@ -0,0 +1,4 @@ +package com.auth0.jwt.oicmsg; + +public class JWSHeader { +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/Message.java b/lib/src/main/java/com/auth0/jwt/oicmsg/Message.java new file mode 100644 index 0000000..cade6a3 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/Message.java @@ -0,0 +1,280 @@ +package com.auth0.jwt.oicmsg; + +import com.auth0.jwt.jwts.JWT; +import com.auth0.jwt.oicmsg.exceptions.MessageException; +import com.auth0.jwt.oicmsg.exceptions.MissingRequiredAttribute; +import com.auth0.jwt.oicmsg.exceptions.OicMsgError; +import com.auth0.jwt.oicmsg.exceptions.TooManyValues; +import com.google.common.base.Joiner; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.omg.CORBA.NameValuePair; + +public class Message { + + private Map dict; + private JWT jwt; + private JWSHeader jwsHeader; + private JWEHeader jweHeader; + private boolean lax; + private boolean shouldVerifySSL; + public Map claims; + private Map cDefault; + public Map cAllowedValues; + public static Tuple5 SINGLE_REQUIRED_STRING = new Tuple5(String.class, true, null, null, false); + public static Tuple5 SINGLE_OPTIONAL_STRING = new Tuple5(String.class, false, null, null, false); + public static Tuple5 REQUIRED_LIST_OF_STRINGS = new Tuple5(Arrays.asList(String.class), true, listSerializer, listDeserializer, false); + public static Tuple5 OPTIONAL_LIST_OF_STRINGS = new Tuple5(Arrays.asList(String.class), false, listSerializer, listDeserializer, false); + public static Tuple5 SINGLE_OPTIONAL_BOOLEAN = new Tuple5(Arrays.asList(Boolean.class), false, null, null, false); + public static Tuple5 OPTIONAL_ADDRESS = new Tuple5(Message.class, false, msgSer, addressDer, false); + public static Tuple5 OPTIONAL_MESSAGE = new Tuple5(Message.class, false, msgSer, msgDer, false); + public static Tuple5 SINGLE_OPTIONAL_INT = new Tuple5(Integer.class, false, null, null, false); + public static Tuple5 SINGLE_REQUIRED_INT = new Tuple5(Integer.class, true, null, null, false); + public static Tuple5 OPTIONAL_LIST_OF_SP_SEP_STRINGS = new Tuple5(Arrays.asList(String.class), false, spSepListSerializer, spSepListDeserializer, false); + + public Message(Map kwargs) { + this.dict = new HashMap<>(cDefault); + this.lax = false; + this.jwt = null; + this.jwsHeader = null; + this.jweHeader = null; + this.fromDict(kwargs); + this.shouldVerifySSL = true; + } + + public Message() { + } + + public String toUrlEncoded(int lev) throws MissingRequiredAttribute, UnsupportedEncodingException { + if(!this.lax) { + Tuple5 tuple5; + for(String key : this.claims.keySet()) { + tuple5 = claims.get(key); + if(tuple5.getVRequired() && !this.cDefault.containsKey(key)) { + throw new MissingRequiredAttribute("missing attribute: " + key); + } + } + } + + List params = new ArrayList<>(); + String[] splitArgs; + Tuple5 tuple5 = new Tuple5(); + for(String key : this.cDefault.keySet()) { + if(!cDefault.containsKey(key)) { + if(key.contains("#")) { + splitArgs = key.split("#"); + tuple5 = claims.get(splitArgs[0]); + } else if(key.contains("*")){ + tuple5 = claims.get("*"); + } else { + tuple5.setVSer(null); + tuple5.setVNullAllowed(false); + } + } + + Object value = cDefault.get(key); + if(value == null && !tuple5.getVNullAllowed()) { + continue; + } else if(value instanceof String) { + params.add(new Tuple(key, (String) value)); + } else if(value instanceof List) { + if(tuple5.getVSer() != null) { + params.add(new Tuple(key, ser(value, "urlencoded", lev))); + } else { + List list = (List) value; + for(Object item : list) { + params.add(new Tuple(key, (String) item)); + } + } + } else if(value instanceof Message) { + value = json.dumps(ser(value, "dict", lev+1)); + params.add(new Tuple(key, value)); + } else if(value == null) { + params.add(new Tuple(key, value)); + } else { + params.add(new Tuple(key, ser(value, lev))); + } + } + return urlencode(params); + } + + public Message fromUrlEncoded(List urlEncoded) throws URISyntaxException, TooManyValues { + String urlEncodedString = urlEncoded.get(0); + + List params = URLEncodedUtils.parse(new URI(urlEncodedString), "UTF-8"); + + Tuple5 tuple5; + String[] splitArgs; + String key; + for(NameValuePair param : params) { + key = param.id; + tuple5 = this.claims.get(key); + if(tuple5 == null) { + if(key.contains("#")) { + splitArgs = key.split("#"); + tuple5 = claims.get(splitArgs[0]); + } else if(key.contains("*")){ + tuple5 = claims.get("*"); + } else { + if(((List) param.value).size() == 1) { + cDefault.put(key, ((List) param.value).get(0)); + continue; + } + } + + } + + if(tuple5.getvType() != null && tuple5.getvType() == List.class) { + if(tuple5.getVDSer() != null) { + this.cDefault.put(key, tuple5.getVDSer(((List) param.value).get(0), "urlencoded")); + } else { + this.cDefault.put(key, ((List) param.value)); + } + } else { + Object value = ((List) param.value).get(0); + if(((List) param.value).size() == 1) { + if(tuple5.getVDSer() != null) { + this.cDefault.put(key, tuple5.getVDSer(value, "urlencoded")); + } else if(value instanceof tuple5.getvType()) { + this.cDefault.put(key, value); + } + } else { + throw new TooManyValues(key); + } + } + } + + return this; + } + + public Object msgSer(Object inst, String sFormat, int lev) throws MessageException, OicMsgError { + List sFormats = Arrays.asList("urlencoded", "json"); + Object res; + if(sFormats.contains(sFormat)) { + if(inst instanceof Map) { + if(sFormat.equals("json")) { + res = json.dumps(inst); + } else { + Map map = (Map) inst; + for(String key : map.keySet()) { + map.put(URLEncoder.encode(key), URLEncoder.encode(map.get(key))); + } + res = map; + } + } else if(inst instanceof Message) { + res = ((Message) inst).serialize(sFormat, lev); + } else { + res = inst; + } + } else if(sFormat.equals("dict")) { + if(inst instanceof Message) { + res = ((Message) inst).serialize(sFormat, lev); + } else if(inst instanceof Map || inst instanceof String) { + res = inst; + } else { + throw new MessageException("Wrong type: " + inst.getClass()); + } + } else { + throw new OicMsgError("Unknown sFormat: " + inst); + } + + return res; + } + + private String urlencode(List params) throws UnsupportedEncodingException { + StringBuilder sb = new StringBuilder(); + for(Tuple param : params){ + if(sb.length() > 0){ + sb.append('&'); + } + sb.append(URLEncoder.encode((String) param.getA(), "UTF-8")).append('=').append(URLEncoder.encode((String) param.getB(), "UTF-8")); + } + + return sb.toString(); + } + + public Collection getValues() { + return this.dict.values(); + } + + public Map serialize(String sFormat, int lev) { + return null; + } + + public Map getClaims() { + return claims; + } + + public void updateClaims(Map claims) { + for(String key : claims.keySet()) { + this.claims.put(key, claims.get(key)); + } + } + + public void setClaims(Map claims) { + this.claims = claims; + } + + public Map getcAllowedValues() { + return cAllowedValues; + } + + public void setcAllowedValues(Map cAllowedValues) { + this.cAllowedValues = cAllowedValues; + } + + public void updatecAllowedValues(Map claims) { + for(String key : claims.keySet()) { + this.cAllowedValues.put(key, claims.get(key)); + } + } + + private void fromDict(Map kwargs) { + } + + public Map getDict() { + return dict; + } + + public void addDict(Map claims) { + for(String key : claims.keySet()) { + this.dict.put(key, claims.get(key)); + } + } + + public void setDict(Map claims) { + this.dict = claims; + } + + public static JWT toJWT(Key key, String algorithm, int lev) { + } + + public boolean verify(Map kwargs) throws Exception { + + } + + public String spSepListSerializer(List vals) { + if(vals != null && vals.size() == 1) { + return vals.get(0); + } else { + Joiner joiner = Joiner.on(" ").skipNulls(); + return joiner.join(vals); + } + } + + public List spSepListDeserializer(List vals) { + if(vals != null && vals.size() == 1) { + return Arrays.asList(vals.get(0).split(" ")); + } else { + return vals; + } + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/Tuple.java b/lib/src/main/java/com/auth0/jwt/oicmsg/Tuple.java new file mode 100644 index 0000000..39ed502 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/Tuple.java @@ -0,0 +1,20 @@ +package com.auth0.jwt.oicmsg; + +public class Tuple { + + private Object a; + private Object b; + + public Tuple(Object a, Object b) { + this.a = a; + this.b = b; + } + + public Object getA() { + return a; + } + + public Object getB() { + return b; + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/Tuple5.java b/lib/src/main/java/com/auth0/jwt/oicmsg/Tuple5.java new file mode 100644 index 0000000..0a72f4a --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/Tuple5.java @@ -0,0 +1,75 @@ +package com.auth0.jwt.oicmsg; + +import java.util.Arrays; +import java.util.List; + +public class Tuple5 { + + private List> vTypeList; + private Class vType; + private boolean vRequired; + private Object vSer; + private Object vDSer; + private boolean vNullAllowed; + + public Tuple5(List> vTypeList, boolean vRequired, Object vSer, Object vDSer, boolean vNullAllowed) { + this.vTypeList = vTypeList; + this.vRequired = vRequired; + this.vSer = vSer; + this.vDSer = vDSer; + this.vNullAllowed = vNullAllowed; + } + + public Tuple5(Class vType, boolean vRequired, Object vSer, Object vDSer, boolean vNullAllowed) { + this(Arrays.asList((Class) null), vRequired, vSer, vDSer, vNullAllowed); + this.vType = vType; + } + + public List> getvTypeList() { + return vTypeList; + } + + public void setvTypeList(List> vTypeList) { + this.vTypeList = vTypeList; + } + + public Class getvType() { + return vType; + } + + public void setvType(Class vType) { + this.vType = vType; + } + + public boolean getVRequired() { + return vRequired; + } + + public void setVRequired(boolean vRequired) { + this.vRequired = vRequired; + } + + public Object getVSer() { + return vSer; + } + + public void setVSer(Object vSer) { + this.vSer = vSer; + } + + public Object getVDSer() { + return vDSer; + } + + public void setVDSer(Object vDSer) { + this.vDSer = vDSer; + } + + public boolean getVNullAllowed() { + return vNullAllowed; + } + + public void setVNullAllowed(boolean vNullAllowed) { + this.vNullAllowed = vNullAllowed; + } +} \ No newline at end of file diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/AtHashError.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/AtHashError.java new file mode 100644 index 0000000..a223d01 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/AtHashError.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class AtHashError extends Exception { + public AtHashError(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/CHashError.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/CHashError.java new file mode 100644 index 0000000..da20062 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/CHashError.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class CHashError extends Exception { + public CHashError(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/EXPError.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/EXPError.java new file mode 100644 index 0000000..210e822 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/EXPError.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class EXPError extends Exception { + public EXPError(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/IATError.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/IATError.java new file mode 100644 index 0000000..5fa20f1 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/IATError.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class IATError extends Exception { + public IATError(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/IssuerMismatch.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/IssuerMismatch.java new file mode 100644 index 0000000..668a545 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/IssuerMismatch.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class IssuerMismatch extends Exception { + public IssuerMismatch(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/MessageException.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/MessageException.java new file mode 100644 index 0000000..8e1f996 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/MessageException.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class MessageException extends Exception { + public MessageException(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/MissingRequiredAttribute.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/MissingRequiredAttribute.java new file mode 100644 index 0000000..f54e3f7 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/MissingRequiredAttribute.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class MissingRequiredAttribute extends Exception{ + public MissingRequiredAttribute(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/NotForMe.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/NotForMe.java new file mode 100644 index 0000000..54913dd --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/NotForMe.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class NotForMe extends Exception { + public NotForMe(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/OicMsgError.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/OicMsgError.java new file mode 100644 index 0000000..1dd3776 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/OicMsgError.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class OicMsgError extends Exception { + public OicMsgError(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/TooManyValues.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/TooManyValues.java new file mode 100644 index 0000000..ba9b785 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/TooManyValues.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class TooManyValues extends Exception { + public TooManyValues(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/ValueError.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/ValueError.java new file mode 100644 index 0000000..e760928 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/ValueError.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class ValueError extends Exception { + public ValueError(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/VerificationError.java b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/VerificationError.java new file mode 100644 index 0000000..c7954fe --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/exceptions/VerificationError.java @@ -0,0 +1,7 @@ +package com.auth0.jwt.oicmsg.exceptions; + +public class VerificationError extends Exception { + public VerificationError(String message) { + super(message); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AccessTokenResponse.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AccessTokenResponse.java new file mode 100644 index 0000000..bb7e4b8 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AccessTokenResponse.java @@ -0,0 +1,18 @@ +package com.auth0.jwt.oicmsg.oauth2; + +import com.auth0.jwt.oicmsg.Message; +import com.auth0.jwt.oicmsg.Tuple5; +import java.util.Map; + +public class AccessTokenResponse extends Message{ + public AccessTokenResponse() { + Map claims = getClaims(); + claims.put("accessToken", SINGLE_REQUIRED_STRING); + claims.put("tokenType", SINGLE_REQUIRED_STRING); + claims.put("expiresIn", SINGLE_OPTIONAL_INT); + claims.put("refreshToken", SINGLE_OPTIONAL_STRING); + claims.put("scope", OPTIONAL_LIST_OF_SP_SEP_STRINGS); + claims.put("state", SINGLE_OPTIONAL_STRING); + setClaims(claims); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AuthorizationErrorResponse.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AuthorizationErrorResponse.java new file mode 100644 index 0000000..d874d5e --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AuthorizationErrorResponse.java @@ -0,0 +1,19 @@ +package com.auth0.jwt.oicmsg.oauth2; + +import com.auth0.jwt.oicmsg.Tuple5; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class AuthorizationErrorResponse extends ErrorResponse{ + + public AuthorizationErrorResponse() { + Map claims = getClaims(); + claims.put("state", SINGLE_OPTIONAL_STRING); + updateClaims(claims); + Map cAllowedValueHashMap = getcAllowedValues(); + cAllowedValueHashMap.put("error", Arrays.asList("invalidRequest", "unauthorizedClient", "accessDenied", + "unsupportedResponseType", "invalidScope", "serverError", "temporarilyUnavailable")); + updatecAllowedValues(cAllowedValueHashMap); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AuthorizationResponse.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AuthorizationResponse.java new file mode 100644 index 0000000..34187f0 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/AuthorizationResponse.java @@ -0,0 +1,50 @@ +package com.auth0.jwt.oicmsg.oauth2; + +import com.auth0.jwt.oicmsg.Message; +import com.auth0.jwt.oicmsg.Tuple5; +import com.auth0.jwt.oicmsg.exceptions.ValueError; +import com.auth0.jwt.oicmsg.exceptions.VerificationError; +import java.util.Map; + +public class AuthorizationResponse extends Message{ + + public AuthorizationResponse(Map kwargs) { + super(kwargs); + Map claims = getClaims(); + claims.put("code", SINGLE_REQUIRED_STRING); + claims.put("state", SINGLE_OPTIONAL_STRING); + claims.put("issuer", SINGLE_OPTIONAL_STRING); + claims.put("clientId", SINGLE_OPTIONAL_STRING); + + setClaims(claims); + } + + public AuthorizationResponse() { + } + + public boolean verify(Map kwargs) throws Exception { + super.verify(kwargs); + Map claims = getDict(); + if(claims.containsKey("clientId")) { + if (kwargs.containsKey("clientId")) { + if(kwargs.get("clientId") instanceof String && !claims.get("clientId").equals((String) kwargs.get("clientId"))) { + throw new VerificationError("clientId mismatch"); + } + } else { + throw new ValueError("No clientId to verify against"); + } + } + + if(claims.containsKey("issuer")) { + if(kwargs.containsKey("issuer")) { + if (claims.get("issuer") != null && !claims.get("issuer").equals(kwargs.get("issuer"))) { + throw new VerificationError("Issuer mismatch"); + } + } else { + throw new ValueError("No issuer set in the Client config"); + } + } + + return true; + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/ErrorResponse.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/ErrorResponse.java new file mode 100644 index 0000000..b0b6f71 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oauth2/ErrorResponse.java @@ -0,0 +1,17 @@ +package com.auth0.jwt.oicmsg.oauth2; + +import com.auth0.jwt.oicmsg.Message; +import com.auth0.jwt.oicmsg.Tuple5; +import java.util.HashMap; +import java.util.Map; + +public class ErrorResponse extends Message{ + public ErrorResponse() { + Map claims = new HashMap() {{ + put("error", SINGLE_REQUIRED_STRING); + put("errorDescription", SINGLE_OPTIONAL_STRING); + put("errorUri", SINGLE_OPTIONAL_STRING); + }}; + setClaims(claims); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oic/AuthorizationErrorResponse.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/AuthorizationErrorResponse.java new file mode 100644 index 0000000..d0fbbaf --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/AuthorizationErrorResponse.java @@ -0,0 +1,24 @@ +package com.auth0.jwt.oicmsg.oic; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class AuthorizationErrorResponse extends com.auth0.jwt.oicmsg.oauth2.AuthorizationErrorResponse{ + + public AuthorizationErrorResponse() { + Map cAllowedValues = getcAllowedValues(); + List list = cAllowedValues.get("error"); + list.addAll(Arrays.asList("interactionRequired", + "loginRequired", + "sessionSelectionRequired", + "consentRequired", + "invalidRequestUri", + "invalidRequestObject", + "registrationNotSupported", + "requestNotSupported", + "requestUriNotSupported")); + cAllowedValues.put("error", list); + updatecAllowedValues(cAllowedValues); + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oic/AuthorizationResponse.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/AuthorizationResponse.java new file mode 100644 index 0000000..f172266 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/AuthorizationResponse.java @@ -0,0 +1,92 @@ +package com.auth0.jwt.oicmsg.oic; + +import com.auth0.jwt.oicmsg.Message; +import com.auth0.jwt.oicmsg.Tuple5; +import com.auth0.jwt.oicmsg.exceptions.AtHashError; +import com.auth0.jwt.oicmsg.exceptions.CHashError; +import com.auth0.jwt.oicmsg.exceptions.MissingRequiredAttribute; +import com.auth0.jwt.oicmsg.exceptions.VerificationError; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AuthorizationResponse extends com.auth0.jwt.oicmsg.oauth2.AuthorizationResponse{ + + public AuthorizationResponse() { + Map cParamAuthorizationResponse = new HashMap<>(getClaims()); + //c_param.update(oauth2.AccessTokenResponse.c_param) HOW TO DEAL WITH MULTIPLE INHERITANCE? + cParamAuthorizationResponse.put("code", SINGLE_OPTIONAL_STRING); + cParamAuthorizationResponse.put("accessToken", SINGLE_OPTIONAL_STRING); + cParamAuthorizationResponse.put("tokenType", SINGLE_OPTIONAL_STRING); + cParamAuthorizationResponse.put("idToken", new Tuple5(Arrays.asList(Message.class), false, msgSer, null, false)); + + updateClaims(cParamAuthorizationResponse); + } + + public boolean verify(Map kwargs) throws Exception { + super.verify(kwargs); + Map claims = getDict(); + if(claims.containsKey("audience")) { + if (kwargs.containsKey("clientId")) { + if(claims.get("audience") instanceof List + && ((List) claims.get("audience")).contains(kwargs.get("clientId"))) { + return false; + } + } + } + + if(claims.containsKey("idToken")) { + Map args = new HashMap<>(); + List argsTemp = Arrays.asList("key", "keyjar", "algs", "sender"); + for(String arg : argsTemp) { + args.put(arg, kwargs.get(arg)); + } + Object idToken = claims.get("idToken"); + if(!(idToken instanceof String)) { + throw new IllegalArgumentException("idToken should be of type String"); + } + String idTokenString = (String) idToken; + IdToken idt = new IdToken().fromJWT(idTokenString, args); + if(!idt.verify(kwargs)) { + throw new VerificationError("Could not verify idToken " + idt); + } + + String algorithm = idt.getJwsHeader("alg"); + + String hashFunction = "HS" + algorithm.substring(algorithm.length()-3); + + if(claims.containsKey("accessToken")) { + if(idt.getAtHash() == null) { + throw new MissingRequiredAttribute("Missing atHash property " + idt); + } + Object accessToken = claims.get("accessToken"); + if(!(accessToken instanceof String)) { + throw new IllegalArgumentException("accessToken should be of type String"); + } + String accessTokenString = (String) accessToken; + if(!idt.getAtHash().equals(JWS.leftHash(accessTokenString, hashFunction))) { + throw new AtHashError("Failed to verify accessToken hash " + idt); + } + } + + if(claims.containsKey("code")) { + if(idt.getCHash() == null) { + throw new MissingRequiredAttribute("Missing cHash property " + idt); + } + Object code = claims.get("code"); + if(!(code instanceof String)) { + throw new IllegalArgumentException("code should be of type String"); + } + String codeString = (String) code; + if(!idt.getCHash().equals(JWS.leftHash(codeString, hashFunction))) { + throw new CHashError("Failed to verify code hash " + idt); + } + } + + claims.put("verifiedIdToken", idt); + addDict(claims); + } + return true; + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oic/IdToken.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/IdToken.java new file mode 100644 index 0000000..ce3d491 --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/IdToken.java @@ -0,0 +1,219 @@ +package com.auth0.jwt.oicmsg.oic; + +import com.auth0.jwt.jwts.JWT; +import com.auth0.jwt.oicmsg.JWSHeader; +import com.auth0.jwt.oicmsg.Message; +import com.auth0.jwt.oicmsg.Tuple5; +import com.auth0.jwt.oicmsg.exceptions.EXPError; +import com.auth0.jwt.oicmsg.exceptions.IATError; +import com.auth0.jwt.oicmsg.exceptions.IssuerMismatch; +import com.auth0.jwt.oicmsg.exceptions.MissingRequiredAttribute; +import com.auth0.jwt.oicmsg.exceptions.NotForMe; +import com.auth0.jwt.oicmsg.exceptions.ValueError; +import com.auth0.jwt.oicmsg.exceptions.VerificationError; +import com.google.common.base.Strings; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class IdToken extends OpenIdSchema{ + + private String atHash; + private String cHash; + private String jti; + private String issuer; + private String azp; + private long iat; + private List audience; + private Map hashTable; + private static final int NONCE_STORAGE_TIME = 4 * 3600; + + public IdToken() { + Map claims = getClaims(); + claims.put("iss", SINGLE_REQUIRED_STRING); + claims.put("sub", SINGLE_REQUIRED_STRING); + claims.put("aud", REQUIRED_LIST_OF_STRINGS); + claims.put("exp", SINGLE_REQUIRED_INT); + claims.put("iat", SINGLE_REQUIRED_INT); + claims.put("authTime", SINGLE_OPTIONAL_INT); + claims.put("nonce", SINGLE_OPTIONAL_STRING); + claims.put("atHash", SINGLE_OPTIONAL_STRING); + claims.put("cHash", SINGLE_OPTIONAL_STRING); + claims.put("acr", SINGLE_OPTIONAL_STRING); + claims.put("amr", OPTIONAL_LIST_OF_STRINGS); + claims.put("azp", SINGLE_OPTIONAL_STRING); + claims.put("subJwk", SINGLE_OPTIONAL_STRING); + updateClaims(claims); + + hashTable = new HashMap() {{ + put("accessToken", "atHash"); + put("code", "cHash"); + }}; + } + + public void valHash(String algorithm) throws Exception { + String hashAlgorithm = "HS" + algorithm.substring(algorithm.length()-3); + String param; + for(String attribute : hashTable.keySet()) { + param = hashTable.get(attribute); + this.getClass().getField(param).set(param, JWS.leftHash(this.getClass().getField(attribute).toString(), hashAlgorithm)); + } + } + + public void packInit(int lifetime) throws Exception{ + this.getClass().getField("iat").set("iat", System.currentTimeMillis()); + if(lifetime != 0) { + this.getClass().getField("exp").set("exp", this.getClass().getField("iat").getInt(this) + lifetime); + } + } + + public void packInit() throws Exception{ + packInit(0); + } + + public void pack(String algorithm, Map args) throws Exception{ + this.valHash(algorithm); + if(args.containsKey("lifetime")) { + this.packInit((Integer) args.get("lifetime")); + } else { + this.packInit(); + } + + String jti; + if(this.cParam.containsKey("jti")) { + if(args.containsKey("jti")) { + if(args.get("jti") instanceof String) { + jti = (String) args.get("jti"); + } else { + throw new ValueError("Jti should be a string"); + } + } else { + jti = getRandomHexString(15); + } + + this.setJti(jti); + } + } + + public boolean verify(Map args) throws Exception { + new IdToken().verify(args); + + if(args.get("issuer") instanceof String && !Strings.isNullOrEmpty((String) args.get("issuer")) && !args.get("issuer").equals(this.getIssuer())) { + throw new IssuerMismatch(args.get("issuer") + " != " + this.getIssuer()); + } + + if(this.getAudience() != null && !this.getAudience().isEmpty()) { + if(args.containsKey("clientId")) { + if(!this.getAudience().contains(args.get("clientId"))) { + throw new NotForMe(args.get("clientId") + " not in aud: " + this.getAudience()); + } + } + + if(this.getAudience().size() > 1) { + if(!Strings.isNullOrEmpty(this.getAzp())) { + if(!this.getAudience().contains(this.getAzp())) { + throw new VerificationError("Mismatch between azp and aud claims"); + } + } else { + throw new VerificationError("azp missing"); + } + } + } + + if(!Strings.isNullOrEmpty(this.getAzp())) { + if(args.containsKey("clientId")) { + if(args.get("clientId") instanceof String && !Strings.isNullOrEmpty((String) args.get("clientId")) && !args.get("clientId").equals(this.getAzp())) { + throw new NotForMe(args.get("clientId") + " != azp: " + this.getAzp()); + } + } + } + + long now = System.currentTimeMillis(); + + Integer skewInteger = (Integer) args.get("skew"); + int skew; + if(skewInteger != null) { + skew = skewInteger.intValue(); + } else { + skew = 0; + } + + Integer expInteger = (Integer) args.get("exp"); + int exp; + if(expInteger != null) { + exp = expInteger.intValue(); + } else { + throw new MissingRequiredAttribute("exp"); + } + + if(now - skew > exp) { + throw new EXPError("Invalid expiration time"); + } + + Integer nonceStorageTimeInteger = (Integer) args.get("nonceStorageTime"); + int nonceStorageTime; + if(nonceStorageTimeInteger != null) { + nonceStorageTime = nonceStorageTimeInteger.intValue(); + } else { + nonceStorageTime = NONCE_STORAGE_TIME; + } + + long iat = this.getIat(); + if(iat + nonceStorageTime < (now - skew)) { + throw new IATError("Issued too long ago"); + } + + return true; + } + + public JWT toJWT(Key key, String algorithm, int lev, int lifetime) { + this.pack(algorithm, lifetime); + return Message.toJWT(key, algorithm,lev); + } + + private String getRandomHexString(int numchars){ + Random r = new Random(); + StringBuffer sb = new StringBuffer(); + while(sb.length() < numchars){ + sb.append(Integer.toHexString(r.nextInt())); + } + + return sb.toString().substring(0, numchars); + } + + public IdToken fromJWT(String idToken, Map args) { + } + + public String getJwsHeader(String alg) { + + } + + public String getAtHash() { + return atHash; + } + + public String getCHash() { + return cHash; + } + + public void setJti(String jti) { + this.jti = jti; + } + + public String getIssuer() { + return issuer; + } + + public List getAudience() { + return audience; + } + + public String getAzp() { + return azp; + } + + public long getIat() { + return iat; + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oic/JWS.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/JWS.java new file mode 100644 index 0000000..e424e7c --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/JWS.java @@ -0,0 +1,39 @@ +package com.auth0.jwt.oicmsg.oic; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import org.bouncycastle.util.encoders.Base64; + +public class JWS { + + private static final String HS_256 = "HS256"; + private static final String HS_384 = "HS384"; + private static final String HS_512 = "HS512"; + private static final String SHA_256 = "SHA-256"; + private static final String SHA_384 = "SHA-384"; + private static final String SHA_512 = "SHA-512"; + private static final String UTF_8 = "UTF-8"; + + public static byte[] leftHash(String message, String hashFunction) throws NoSuchAlgorithmException, UnsupportedEncodingException { + + if(hashFunction.equals(HS_256)) { + MessageDigest digest = MessageDigest.getInstance(SHA_256); + String byteToString = new String(digest.digest(message.getBytes())); + byteToString = byteToString.substring(0,16); + return new String(Base64.encode(byteToString.getBytes())).getBytes(UTF_8); + } else if(hashFunction.equals(HS_384)) { + MessageDigest digest = MessageDigest.getInstance(SHA_384); + String byteToString = new String(digest.digest(message.getBytes())); + byteToString = byteToString.substring(0,24); + return new String(Base64.encode(byteToString.getBytes())).getBytes(UTF_8); + } else if(hashFunction.equals(HS_512)) { + MessageDigest digest = MessageDigest.getInstance(SHA_512); + String byteToString = new String(digest.digest(message.getBytes())); + byteToString = byteToString.substring(0,32); + return new String(Base64.encode(byteToString.getBytes())).getBytes(UTF_8); + } else { + throw new IllegalArgumentException("Not a proper hash function"); + } + } +} diff --git a/lib/src/main/java/com/auth0/jwt/oicmsg/oic/OpenIdSchema.java b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/OpenIdSchema.java new file mode 100644 index 0000000..dd222da --- /dev/null +++ b/lib/src/main/java/com/auth0/jwt/oicmsg/oic/OpenIdSchema.java @@ -0,0 +1,79 @@ +package com.auth0.jwt.oicmsg.oic; + +import com.auth0.jwt.oicmsg.Message; +import com.auth0.jwt.oicmsg.Tuple5; +import com.auth0.jwt.oicmsg.exceptions.VerificationError; +import com.google.common.base.Strings; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Map; + +public class OpenIdSchema extends Message{ + + private String birthDate; + + public OpenIdSchema() { + Map cParam = new HashMap() {{ + put("subject", SINGLE_REQUIRED_STRING); + put("name", SINGLE_OPTIONAL_STRING); + put("givenName", SINGLE_OPTIONAL_STRING); + put("familyName", SINGLE_OPTIONAL_STRING); + put("middleName", SINGLE_OPTIONAL_STRING); + put("nickname", SINGLE_OPTIONAL_STRING); + put("preferredUsername", SINGLE_OPTIONAL_STRING); + put("profile", SINGLE_OPTIONAL_STRING); + put("picture", SINGLE_OPTIONAL_STRING); + put("website", SINGLE_OPTIONAL_STRING); + put("email", SINGLE_OPTIONAL_STRING); + put("emailVerified", SINGLE_OPTIONAL_BOOLEAN); + put("gender", SINGLE_OPTIONAL_STRING); + put("birthdate", SINGLE_OPTIONAL_STRING); + put("zoneInfo", SINGLE_OPTIONAL_STRING); + put("locale", SINGLE_OPTIONAL_STRING); + put("phoneNumber", SINGLE_OPTIONAL_STRING); + put("phoneNumberVerified", SINGLE_OPTIONAL_BOOLEAN); + put("address", OPTIONAL_ADDRESS); + put("updatedAt", SINGLE_OPTIONAL_INT); + put("claimNames", OPTIONAL_MESSAGE); + put("claimSources", OPTIONAL_MESSAGE); + }}; + setcParam(cParam); + } + + public boolean verify(Map kwargs) throws Exception { + new OpenIdSchema().verify(kwargs); + String birthDate = this.getBirthDate(); + if(!Strings.isNullOrEmpty(birthDate)) { + DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); + try { + formatter.parse(birthDate); + } catch (ParseException e) { + formatter = new SimpleDateFormat("yyyy"); + try { + formatter.parse(birthDate); + } catch (ParseException e1) { + formatter = new SimpleDateFormat("0000-MM-dd"); + try { + formatter.parse(birthDate); + } catch (ParseException e2) { + throw new VerificationError("Birthdate format error" + this); + } + } + } + } + + for(Object object : this.getValues()) { + if(object == null) { + return false; + } + } + + return true; + } + + public String getBirthDate() { + return birthDate; + } +}