From 9ada098b75629a8ee6148b2b9b5775ee829b0fca Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Thu, 15 Dec 2011 16:31:25 -0500 Subject: [PATCH 1/9] cleaned up project, added in some model components, added in shells for repositories --- .classpath | 1 + .settings/org.eclipse.wst.common.component | 1 + pom.xml | 15 --------------- .../openid/connect/model/OAuth2AccessToken.java | 8 -------- .../mitre/openid/connect/model/OAuth2Client.java | 8 -------- .../org/mitre/openid/connect/model/UserInfo.java | 2 ++ .../org/mitre/openid/connect/repository/empty.txt | 0 .../openid/connect/web/AuthorizationEndpoint.java | 11 +++++++++++ .../mitre/openid/connect/web/TokenEndpoint.java | 4 ++++ .../WEB-INF/spring/appServlet/servlet-context.xml | 3 --- 10 files changed, 19 insertions(+), 34 deletions(-) delete mode 100644 src/main/java/org/mitre/openid/connect/model/OAuth2AccessToken.java delete mode 100644 src/main/java/org/mitre/openid/connect/model/OAuth2Client.java delete mode 100644 src/main/java/org/mitre/openid/connect/repository/empty.txt diff --git a/.classpath b/.classpath index 4cea5050d..08dd16939 100644 --- a/.classpath +++ b/.classpath @@ -9,6 +9,7 @@ + diff --git a/.settings/org.eclipse.wst.common.component b/.settings/org.eclipse.wst.common.component index 4f15f0bfa..b5bffc576 100644 --- a/.settings/org.eclipse.wst.common.component +++ b/.settings/org.eclipse.wst.common.component @@ -9,6 +9,7 @@ + diff --git a/pom.xml b/pom.xml index 4ae8fa1da..1a0d6dc47 100644 --- a/pom.xml +++ b/pom.xml @@ -226,21 +226,6 @@ mysql-connector-java 5.1.16 - - com.google.code.maven-replacer-plugin - maven-replacer-plugin - 1.3.8 - - - org.codehaus.jackson - jackson-core-asl - 1.6.4 - - - org.codehaus.jackson - jackson-mapper-asl - 1.6.4 - commons-codec commons-codec diff --git a/src/main/java/org/mitre/openid/connect/model/OAuth2AccessToken.java b/src/main/java/org/mitre/openid/connect/model/OAuth2AccessToken.java deleted file mode 100644 index 4bf89f595..000000000 --- a/src/main/java/org/mitre/openid/connect/model/OAuth2AccessToken.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.mitre.openid.connect.model; - -import javax.persistence.Entity; - -@Entity -public class OAuth2AccessToken { - -} diff --git a/src/main/java/org/mitre/openid/connect/model/OAuth2Client.java b/src/main/java/org/mitre/openid/connect/model/OAuth2Client.java deleted file mode 100644 index 53232df48..000000000 --- a/src/main/java/org/mitre/openid/connect/model/OAuth2Client.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.mitre.openid.connect.model; - -import javax.persistence.Entity; - -@Entity -public class OAuth2Client { - -} diff --git a/src/main/java/org/mitre/openid/connect/model/UserInfo.java b/src/main/java/org/mitre/openid/connect/model/UserInfo.java index e230d4758..67c8a2687 100644 --- a/src/main/java/org/mitre/openid/connect/model/UserInfo.java +++ b/src/main/java/org/mitre/openid/connect/model/UserInfo.java @@ -5,6 +5,8 @@ import javax.persistence.Entity; @Entity public class UserInfo { + // TODO: underbars are awkward in java, should we switch all this to camel case and put in underbars in the serialization view? + private String user_id; private String name; private String given_name; diff --git a/src/main/java/org/mitre/openid/connect/repository/empty.txt b/src/main/java/org/mitre/openid/connect/repository/empty.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/java/org/mitre/openid/connect/web/AuthorizationEndpoint.java b/src/main/java/org/mitre/openid/connect/web/AuthorizationEndpoint.java index a663fb2c6..10ccb36b0 100644 --- a/src/main/java/org/mitre/openid/connect/web/AuthorizationEndpoint.java +++ b/src/main/java/org/mitre/openid/connect/web/AuthorizationEndpoint.java @@ -7,4 +7,15 @@ import org.springframework.web.bind.annotation.RequestMapping; @RequestMapping("/authorize") public class AuthorizationEndpoint { + /* + * handle "code" flow + * + */ + + + /* + * handle "idtoken token" flow + * + */ + } diff --git a/src/main/java/org/mitre/openid/connect/web/TokenEndpoint.java b/src/main/java/org/mitre/openid/connect/web/TokenEndpoint.java index 6da95a73f..ac1cbc2c0 100644 --- a/src/main/java/org/mitre/openid/connect/web/TokenEndpoint.java +++ b/src/main/java/org/mitre/openid/connect/web/TokenEndpoint.java @@ -9,4 +9,8 @@ public class TokenEndpoint { //Corresponds to spring security Authentication Filter class + // handle sending back a token and an id token for a code + + // fall through to SSOA code if no id token? + } diff --git a/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml b/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml index 78a3800b9..ca62af15a 100644 --- a/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml +++ b/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml @@ -29,9 +29,6 @@ - - - From 9ab65b84a3e23fd1cd971ab4f896edd35a19495c Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Thu, 15 Dec 2011 16:34:38 -0500 Subject: [PATCH 2/9] added basic repository and model components --- .../openid/connect/model/ApprovedSite.java | 155 ++++++++++++++++++ .../org/mitre/openid/connect/model/Event.java | 23 +++ .../openid/connect/model/WhitelistedSite.java | 34 ++++ .../repository/ApprovedSiteRepository.java | 11 ++ .../connect/repository/IdTokenRepository.java | 5 + .../repository/UserInfoRepository.java | 9 + .../repository/WhitelistedSiteRepository.java | 5 + 7 files changed, 242 insertions(+) create mode 100644 src/main/java/org/mitre/openid/connect/model/ApprovedSite.java create mode 100644 src/main/java/org/mitre/openid/connect/model/Event.java create mode 100644 src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java create mode 100644 src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java create mode 100644 src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java create mode 100644 src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java create mode 100644 src/main/java/org/mitre/openid/connect/repository/WhitelistedSiteRepository.java diff --git a/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java b/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java new file mode 100644 index 000000000..3f16c4d03 --- /dev/null +++ b/src/main/java/org/mitre/openid/connect/model/ApprovedSite.java @@ -0,0 +1,155 @@ +package org.mitre.openid.connect.model; + +import java.util.Collection; +import java.util.Date; + +import javax.persistence.Basic; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Temporal; + +import org.springframework.security.oauth2.provider.ClientDetails; + +@Entity +public class ApprovedSite { + + // unique id + private Long id; + + // which user made the approval + private UserInfo userInfo; + + // which OAuth2 client is this tied to + private ClientDetails clientDetails; + + // when was this first approved? + private Date creationDate; + + // when was this last accessed? + private Date accessDate; + + // if this is a time-limited access, when does it run out? + private Date timeoutDate; + + // what scopes have been allowed + // this should include all information for what data to access + private Collection allowedScopes; + + // TODO: should we store the OAuth2 tokens and IdTokens here? + + + public ApprovedSite() { + + } + + /** + * @return the id + */ + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + public Long getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(Long id) { + this.id = id; + } + + /** + * @return the userInfo + */ + public UserInfo getUserInfo() { + return userInfo; + } + + /** + * @param userInfo the userInfo to set + */ + public void setUserInfo(UserInfo userInfo) { + this.userInfo = userInfo; + } + + /** + * @return the clientDetails + */ + public ClientDetails getClientDetails() { + return clientDetails; + } + + /** + * @param clientDetails the clientDetails to set + */ + public void setClientDetails(ClientDetails clientDetails) { + this.clientDetails = clientDetails; + } + + /** + * @return the creationDate + */ + @Basic + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + public Date getCreationDate() { + return creationDate; + } + + /** + * @param creationDate the creationDate to set + */ + public void setCreationDate(Date creationDate) { + this.creationDate = creationDate; + } + + /** + * @return the accessDate + */ + @Basic + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + public Date getAccessDate() { + return accessDate; + } + + /** + * @param accessDate the accessDate to set + */ + public void setAccessDate(Date accessDate) { + this.accessDate = accessDate; + } + + /** + * @return the allowedScopes + */ + public Collection getAllowedScopes() { + return allowedScopes; + } + + /** + * @param allowedScopes the allowedScopes to set + */ + public void setAllowedScopes(Collection allowedScopes) { + this.allowedScopes = allowedScopes; + } + + /** + * @return the timeoutDate + */ + @Basic + @Temporal(javax.persistence.TemporalType.TIMESTAMP) + public Date getTimeoutDate() { + return timeoutDate; + } + + /** + * @param timeoutDate the timeoutDate to set + */ + public void setTimeoutDate(Date timeoutDate) { + this.timeoutDate = timeoutDate; + } + + + +} diff --git a/src/main/java/org/mitre/openid/connect/model/Event.java b/src/main/java/org/mitre/openid/connect/model/Event.java new file mode 100644 index 000000000..d287ad996 --- /dev/null +++ b/src/main/java/org/mitre/openid/connect/model/Event.java @@ -0,0 +1,23 @@ +package org.mitre.openid.connect.model; + +import java.util.Date; + +import javax.persistence.Entity; + +/** + * Class to contain a logged event in the system. + * + * @author jricher + * + */ + +@Entity +public class Event { + + public static enum EventType { LOGIN, AUTHORIZATION, ACCESS } + + private Long id; + private EventType type; + private Date timestamp; + +} diff --git a/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java b/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java new file mode 100644 index 000000000..ca5b4987d --- /dev/null +++ b/src/main/java/org/mitre/openid/connect/model/WhitelistedSite.java @@ -0,0 +1,34 @@ +package org.mitre.openid.connect.model; + +import java.util.Collection; +import java.util.Date; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import org.springframework.security.oauth2.provider.ClientDetails; + +/** + * Indicator that login to a site should be automatically granted + * without user interaction. + * @author jricher + * + */ +public class WhitelistedSite { + + // unique id + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + // who added this site to the whitelist (should be an admin) + private UserInfo userInfo; + + // which OAuth2 client is this tied to + private ClientDetails clientDetails; + + // what scopes be allowed by default + // this should include all information for what data to access + private Collection allowedScopes; +} diff --git a/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java b/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java new file mode 100644 index 000000000..db7b06574 --- /dev/null +++ b/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java @@ -0,0 +1,11 @@ +package org.mitre.openid.connect.repository; + +import org.mitre.openid.connect.model.ApprovedSite; + +public interface ApprovedSiteRepository { + + public ApprovedSite getById(Long id); + + public ApprovedSite getByUrl(String url); + +} diff --git a/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java b/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java new file mode 100644 index 000000000..5e9183093 --- /dev/null +++ b/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java @@ -0,0 +1,5 @@ +package org.mitre.openid.connect.repository; + +public interface IdTokenRepository { + +} diff --git a/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java b/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java new file mode 100644 index 000000000..9da679568 --- /dev/null +++ b/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java @@ -0,0 +1,9 @@ +package org.mitre.openid.connect.repository; + +import org.mitre.openid.connect.model.UserInfo; + +public interface UserInfoRepository { + + public UserInfo getByUserId(String user_id); + +} diff --git a/src/main/java/org/mitre/openid/connect/repository/WhitelistedSiteRepository.java b/src/main/java/org/mitre/openid/connect/repository/WhitelistedSiteRepository.java new file mode 100644 index 000000000..a2478efce --- /dev/null +++ b/src/main/java/org/mitre/openid/connect/repository/WhitelistedSiteRepository.java @@ -0,0 +1,5 @@ +package org.mitre.openid.connect.repository; + +public interface WhitelistedSiteRepository { + +} From a8d39b905f85b416c0d8d97381259f7d5d845816 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Fri, 16 Dec 2011 13:28:59 -0500 Subject: [PATCH 3/9] updated some repositories with basic get/save functions --- mitre-local | 2 +- spring-security-oauth | 2 +- .../connect/repository/ApprovedSiteRepository.java | 10 +++++++++- .../openid/connect/repository/IdTokenRepository.java | 6 ++++++ .../openid/connect/repository/UserInfoRepository.java | 2 ++ .../openid/connect/web/AuthorizationEndpoint.java | 4 ++++ 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/mitre-local b/mitre-local index b3dbc687b..f1f301046 160000 --- a/mitre-local +++ b/mitre-local @@ -1 +1 @@ -Subproject commit b3dbc687b2922a78cb3879ca16c0ce1bc9a4f657 +Subproject commit f1f301046b6c6f397bc4d40a25dd407ca6f2b757 diff --git a/spring-security-oauth b/spring-security-oauth index 674ebf1fa..5784e6bf5 160000 --- a/spring-security-oauth +++ b/spring-security-oauth @@ -1 +1 @@ -Subproject commit 674ebf1fa71f4886216c01d731c6acdcd4e37d72 +Subproject commit 5784e6bf534e65627d4efb94be90ab7ba4df9069 diff --git a/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java b/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java index db7b06574..3e06f3a12 100644 --- a/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java +++ b/src/main/java/org/mitre/openid/connect/repository/ApprovedSiteRepository.java @@ -1,11 +1,19 @@ package org.mitre.openid.connect.repository; +import java.util.Collection; +import java.util.Collections; + import org.mitre.openid.connect.model.ApprovedSite; +import org.mitre.openid.connect.model.UserInfo; public interface ApprovedSiteRepository { public ApprovedSite getById(Long id); - public ApprovedSite getByUrl(String url); + public Collection getAllForUser(UserInfo user); + + public Collection getAllExpired(); + + public ApprovedSite save(ApprovedSite site); } diff --git a/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java b/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java index 5e9183093..98676bb66 100644 --- a/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java +++ b/src/main/java/org/mitre/openid/connect/repository/IdTokenRepository.java @@ -1,5 +1,11 @@ package org.mitre.openid.connect.repository; +import org.mitre.openid.connect.model.IdToken; + public interface IdTokenRepository { + public IdToken getById(Long id); + + public IdToken save(IdToken idToken); + } diff --git a/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java b/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java index 9da679568..0f239660c 100644 --- a/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java +++ b/src/main/java/org/mitre/openid/connect/repository/UserInfoRepository.java @@ -6,4 +6,6 @@ public interface UserInfoRepository { public UserInfo getByUserId(String user_id); + public UserInfo save(UserInfo user); + } diff --git a/src/main/java/org/mitre/openid/connect/web/AuthorizationEndpoint.java b/src/main/java/org/mitre/openid/connect/web/AuthorizationEndpoint.java index 10ccb36b0..77767d69a 100644 --- a/src/main/java/org/mitre/openid/connect/web/AuthorizationEndpoint.java +++ b/src/main/java/org/mitre/openid/connect/web/AuthorizationEndpoint.java @@ -17,5 +17,9 @@ public class AuthorizationEndpoint { * handle "idtoken token" flow * */ + + /* + * Other flows get handled outside of our endpoints by SSOA + */ } From 7a6af8c07dc867a168731b04bf16f939949c6564 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Wed, 21 Dec 2011 11:47:40 -0500 Subject: [PATCH 4/9] started a JWT library --- mitre-local | 2 +- pom.xml | 11 +- src/main/java/org/mitre/jwt/Jwt.java | 118 +++++++++ src/main/java/org/mitre/jwt/JwtClaims.java | 236 ++++++++++++++++++ src/main/java/org/mitre/jwt/JwtHeader.java | 133 ++++++++++ src/main/java/org/mitre/jwt/JwtSigner.java | 7 + .../mitre/openid/connect/model/IdToken.java | 4 +- .../spring/appServlet/servlet-context.xml | 4 +- 8 files changed, 508 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/mitre/jwt/Jwt.java create mode 100644 src/main/java/org/mitre/jwt/JwtClaims.java create mode 100644 src/main/java/org/mitre/jwt/JwtHeader.java create mode 100644 src/main/java/org/mitre/jwt/JwtSigner.java diff --git a/mitre-local b/mitre-local index f1f301046..b81dd3091 160000 --- a/mitre-local +++ b/mitre-local @@ -1 +1 @@ -Subproject commit f1f301046b6c6f397bc4d40a25dd407ca6f2b757 +Subproject commit b81dd3091e53be285bbfd9f0886b5a88b2cab19c diff --git a/pom.xml b/pom.xml index 1a0d6dc47..0de433283 100644 --- a/pom.xml +++ b/pom.xml @@ -194,7 +194,7 @@ com.google.code.gson gson - 1.7.1 + 2.0 org.eclipse.persistence @@ -214,7 +214,7 @@ com.google.guava guava - r09 + 10.0.1 org.apache.httpcomponents @@ -229,7 +229,12 @@ commons-codec commons-codec - 1.3 + 1.6 + + + org.bouncycastle + bcprov-jdk16 + 1.46 diff --git a/src/main/java/org/mitre/jwt/Jwt.java b/src/main/java/org/mitre/jwt/Jwt.java new file mode 100644 index 000000000..fa47a2605 --- /dev/null +++ b/src/main/java/org/mitre/jwt/Jwt.java @@ -0,0 +1,118 @@ +package org.mitre.jwt; + +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.JsonObject; +import com.google.gson.JsonParser; + +public class Jwt { + + private JwtHeader header = new JwtHeader(); + + private JwtClaims claims = new JwtClaims(); + + private String signature; + + + + + /** + * @return the header + */ + public JwtHeader getHeader() { + return header; + } + + + + /** + * @param header the header to set + */ + public void setHeader(JwtHeader header) { + this.header = header; + } + + + + /** + * @return the claims + */ + public JwtClaims getClaims() { + return claims; + } + + + + /** + * @param claims the claims to set + */ + public void setClaims(JwtClaims claims) { + this.claims = claims; + } + + + + /** + * @return the signature + */ + public String getSignature() { + return signature; + } + + + + /** + * @param signature the signature to set + */ + public void setSignature(String signature) { + this.signature = signature; + } + + /** + * 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); + } + + + /** + * Parse a wire-encoded JWT + */ + public static Jwt parse(String s) { + + // split on the dots + List parts = Lists.newArrayList(Splitter.on(".").split(s)); + + if (parts.size() != 3) { + throw new IllegalArgumentException("Invalid JWT format."); + } + + String h64 = parts.get(0); + String o64 = parts.get(1); + String s64 = parts.get(2); + + JsonParser parser = new JsonParser(); + + + + // shuttle for return value + Jwt jwt = new Jwt(); + + return jwt; + + } + +} diff --git a/src/main/java/org/mitre/jwt/JwtClaims.java b/src/main/java/org/mitre/jwt/JwtClaims.java new file mode 100644 index 000000000..dd4124db4 --- /dev/null +++ b/src/main/java/org/mitre/jwt/JwtClaims.java @@ -0,0 +1,236 @@ +package org.mitre.jwt; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +public class JwtClaims { + + /** + * ISO8601 / RFC3339 Date Format + */ + public static DateFormat dateFormat = new SimpleDateFormat("YYYY-MM-DD'T'HH:mm:ssz"); + + private Date expiration; + + private Date notBefore; + + private Date issuedAt; + + private String issuer; + + private String audience; + + private String principal; + + private String jwtId; + + private String type; + + private Map claims = new HashMap(); + + public JwtClaims() { + + } + + /** + * @return the expiration + */ + public Date getExpiration() { + return expiration; + } + + /** + * @param expiration the expiration to set + */ + public void setExpiration(Date expiration) { + this.expiration = expiration; + } + + /** + * @return the notBefore + */ + public Date getNotBefore() { + return notBefore; + } + + /** + * @param notBefore the notBefore to set + */ + public void setNotBefore(Date notBefore) { + this.notBefore = notBefore; + } + + /** + * @return the issuedAt + */ + public Date getIssuedAt() { + return issuedAt; + } + + /** + * @param issuedAt the issuedAt to set + */ + public void setIssuedAt(Date issuedAt) { + this.issuedAt = issuedAt; + } + + /** + * @return the issuer + */ + public String getIssuer() { + return issuer; + } + + /** + * @param issuer the issuer to set + */ + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + /** + * @return the audience + */ + public String getAudience() { + return audience; + } + + /** + * @param audience the audience to set + */ + public void setAudience(String audience) { + this.audience = audience; + } + + /** + * @return the principal + */ + public String getPrincipal() { + return principal; + } + + /** + * @param principal the principal to set + */ + public void setPrincipal(String principal) { + this.principal = principal; + } + + /** + * @return the jwtId + */ + public String getJwtId() { + return jwtId; + } + + /** + * @param jwtId the jwtId to set + */ + public void setJwtId(String jwtId) { + this.jwtId = jwtId; + } + + /** + * @return the type + */ + public String getType() { + return type; + } + + /** + * @param type the type to set + */ + public void setType(String type) { + this.type = type; + } + + /** + * Get an extension claim + */ + public Object getClaim(String key) { + return claims.get(key); + } + + /** + * Set an extension claim + */ + public void setClaim(String key, Object value) { + claims.put(key, value); + } + + /** + * Remove an extension claim + */ + public Object removeClaim(String key) { + return claims.remove(key); + } + + /** + * Get a copy of this header as a JsonObject. The JsonObject is not + * backed by a live copy of this JwtHeader. + * @return a copy of the data in this header in a JsonObject + */ + public JsonObject getAsJsonObject() { + JsonObject o = new JsonObject(); + + if (this.expiration != null) { + o.addProperty("exp", dateFormat.format(this.expiration)); + } + + if (this.notBefore != null) { + o.addProperty("nbf", dateFormat.format(this.notBefore)); + } + + if (this.issuedAt != null) { + o.addProperty("iat", dateFormat.format(this.issuedAt)); + } + + if (this.issuer != null) { + o.addProperty("iss", this.issuer); + } + + if (this.audience != null) { + o.addProperty("aud", this.audience); + } + + if (this.principal != null) { + o.addProperty("prn", this.principal); + } + + if (this.jwtId != null) { + o.addProperty("jti", this.jwtId); + } + + if (this.type != null) { + o.addProperty("typ", this.type); + } + + if (this.claims != null) { + for (Map.Entry claim : this.claims.entrySet()) { + 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()); + } else if (claim.getValue() instanceof Boolean) { + o.addProperty(claim.getKey(), (Boolean)claim.getValue()); + } else if (claim.getValue() instanceof Character) { + o.addProperty(claim.getKey(), (Character)claim.getValue()); + } else if (claim.getValue() != null) { + // try to put it in as a string + o.addProperty(claim.getKey(), claim.getValue().toString()); + } else { + // otherwise add in as a null + o.add(claim.getKey(), null); + } + } + } + + return o; + } + +} diff --git a/src/main/java/org/mitre/jwt/JwtHeader.java b/src/main/java/org/mitre/jwt/JwtHeader.java new file mode 100644 index 000000000..fd7560769 --- /dev/null +++ b/src/main/java/org/mitre/jwt/JwtHeader.java @@ -0,0 +1,133 @@ +package org.mitre.jwt; + +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.JsonObject; + +public class JwtHeader { + + private String type; + + private String algorithm; + + private String encryptionMethod; + + private Map claims = new HashMap(); + + + public JwtHeader() { + + } + + + /** + * @return the type + */ + public String getType() { + return type; + } + + + /** + * @param type the type to set + */ + public void setType(String type) { + this.type = type; + } + + + /** + * @return the algorithm + */ + public String getAlgorithm() { + return algorithm; + } + + + /** + * @param algorithm the algorithm to set + */ + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + + /** + * @return the encryptionMethod + */ + public String getEncryptionMethod() { + return encryptionMethod; + } + + + /** + * @param encryptionMethod the encryptionMethod to set + */ + public void setEncryptionMethod(String encryptionMethod) { + this.encryptionMethod = encryptionMethod; + } + + /** + * Get an extension claim + */ + public Object getClaim(String key) { + return claims.get(key); + } + + /** + * Set an extension claim + */ + public void setClaim(String key, Object value) { + claims.put(key, value); + } + + /** + * Remove an extension claim + */ + public Object removeClaim(String key) { + return claims.remove(key); + } + + /** + * Get a copy of this header as a JsonObject. The JsonObject is not + * backed by a live copy of this JwtHeader. + * @return a copy of the data in this header in a JsonObject + */ + public JsonObject getAsJsonObject() { + JsonObject o = new JsonObject(); + + o.addProperty("typ", this.type); + if (this.algorithm != null) { + o.addProperty("alg", this.algorithm); + } + + if (this.encryptionMethod != null) { + o.addProperty("enc", this.encryptionMethod); + } + + if (this.claims != null) { + for (Map.Entry claim : this.claims.entrySet()) { + 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()); + } else if (claim.getValue() instanceof Boolean) { + o.addProperty(claim.getKey(), (Boolean)claim.getValue()); + } else if (claim.getValue() instanceof Character) { + o.addProperty(claim.getKey(), (Character)claim.getValue()); + } else if (claim.getValue() != null) { + // try to put it in as a string + o.addProperty(claim.getKey(), claim.getValue().toString()); + } else { + // otherwise add in as a null + o.add(claim.getKey(), null); + } + } + } + + return o; + } + + +} diff --git a/src/main/java/org/mitre/jwt/JwtSigner.java b/src/main/java/org/mitre/jwt/JwtSigner.java new file mode 100644 index 000000000..3d2f3c797 --- /dev/null +++ b/src/main/java/org/mitre/jwt/JwtSigner.java @@ -0,0 +1,7 @@ +package org.mitre.jwt; + +public interface JwtSigner { + + public void sign(Jwt jwt); + +} diff --git a/src/main/java/org/mitre/openid/connect/model/IdToken.java b/src/main/java/org/mitre/openid/connect/model/IdToken.java index 734f6bd9e..7e55800b9 100644 --- a/src/main/java/org/mitre/openid/connect/model/IdToken.java +++ b/src/main/java/org/mitre/openid/connect/model/IdToken.java @@ -2,11 +2,13 @@ package org.mitre.openid.connect.model; import javax.persistence.Entity; +import org.mitre.jwt.Jwt; + /* * TODO: This class needs to be encoded as a JWT */ @Entity -public class IdToken { +public class IdToken extends Jwt { private String iss; private String user_id; diff --git a/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml b/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml index ca62af15a..0ee38fc01 100644 --- a/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml +++ b/src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml @@ -30,7 +30,7 @@ - - + + From 869a6ddeefeb2499a21056e4e184611cc3cce56c Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Wed, 21 Dec 2011 15:57:34 -0500 Subject: [PATCH 5/9] added signing and some unit tests, HMAC signing still doesn't quite work --- .../java/org/mitre/jwt/AbstractJwtSigner.java | 47 +++++++++++ .../java/org/mitre/jwt/Hmac256Signer.java | 81 +++++++++++++++++++ src/main/java/org/mitre/jwt/Jwt.java | 62 ++++++++++---- src/main/java/org/mitre/jwt/JwtClaims.java | 66 +++++++++++++-- src/main/java/org/mitre/jwt/JwtHeader.java | 43 +++++++++- .../java/org/mitre/jwt/PlaintextSigner.java | 17 ++++ src/test/java/org/mitre/jwt/JwtTest.java | 76 +++++++++++++++++ 7 files changed, 370 insertions(+), 22 deletions(-) create mode 100644 src/main/java/org/mitre/jwt/AbstractJwtSigner.java create mode 100644 src/main/java/org/mitre/jwt/Hmac256Signer.java create mode 100644 src/main/java/org/mitre/jwt/PlaintextSigner.java create mode 100644 src/test/java/org/mitre/jwt/JwtTest.java diff --git a/src/main/java/org/mitre/jwt/AbstractJwtSigner.java b/src/main/java/org/mitre/jwt/AbstractJwtSigner.java new file mode 100644 index 000000000..aa17755ef --- /dev/null +++ b/src/main/java/org/mitre/jwt/AbstractJwtSigner.java @@ -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); + } + + } + + +} \ No newline at end of file diff --git a/src/main/java/org/mitre/jwt/Hmac256Signer.java b/src/main/java/org/mitre/jwt/Hmac256Signer.java new file mode 100644 index 000000000..ea4338bbc --- /dev/null +++ b/src/main/java/org/mitre/jwt/Hmac256Signer.java @@ -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; + } + + + +} diff --git a/src/main/java/org/mitre/jwt/Jwt.java b/src/main/java/org/mitre/jwt/Jwt.java index fa47a2605..84a1bf44f 100644 --- a/src/main/java/org/mitre/jwt/Jwt.java +++ b/src/main/java/org/mitre/jwt/Jwt.java @@ -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; diff --git a/src/main/java/org/mitre/jwt/JwtClaims.java b/src/main/java/org/mitre/jwt/JwtClaims.java index dd4124db4..0f5e6e182 100644 --- a/src/main/java/org/mitre/jwt/JwtClaims.java +++ b/src/main/java/org/mitre/jwt/JwtClaims.java @@ -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 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 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 + "]"; + } + + + } diff --git a/src/main/java/org/mitre/jwt/JwtHeader.java b/src/main/java/org/mitre/jwt/JwtHeader.java index fd7560769..0c002103f 100644 --- a/src/main/java/org/mitre/jwt/JwtHeader.java +++ b/src/main/java/org/mitre/jwt/JwtHeader.java @@ -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 claims = new HashMap(); + /** + * Make an empty header + */ public JwtHeader() { } - + + /** + * Build a header from a JSON object + * @param json + */ + public JwtHeader(JsonObject json) { + + for (Entry 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 + "]"; + } + + } diff --git a/src/main/java/org/mitre/jwt/PlaintextSigner.java b/src/main/java/org/mitre/jwt/PlaintextSigner.java new file mode 100644 index 000000000..8c1058b1a --- /dev/null +++ b/src/main/java/org/mitre/jwt/PlaintextSigner.java @@ -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(""); + + } + +} diff --git a/src/test/java/org/mitre/jwt/JwtTest.java b/src/test/java/org/mitre/jwt/JwtTest.java new file mode 100644 index 000000000..0b18d6455 --- /dev/null +++ b/src/test/java/org/mitre/jwt/JwtTest.java @@ -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)); + + } + +} From f44f22cf801685907b9467e27f179d0cd0492840 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Thu, 22 Dec 2011 12:52:56 -0500 Subject: [PATCH 6/9] fixed HMAC signer, added test for it --- .../java/org/mitre/jwt/Hmac256Signer.java | 12 +++++++- src/test/java/org/mitre/jwt/JwtTest.java | 30 +++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/mitre/jwt/Hmac256Signer.java b/src/main/java/org/mitre/jwt/Hmac256Signer.java index ea4338bbc..e9d1ed2dd 100644 --- a/src/main/java/org/mitre/jwt/Hmac256Signer.java +++ b/src/main/java/org/mitre/jwt/Hmac256Signer.java @@ -1,5 +1,7 @@ package org.mitre.jwt; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -50,7 +52,15 @@ public class Hmac256Signer extends AbstractJwtSigner { e.printStackTrace(); } - mac.update(jwt.getSignatureBase().getBytes()); + try { + mac.update(jwt.getSignatureBase().getBytes("UTF-8")); + } catch (IllegalStateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } byte[] sigBytes = mac.doFinal(); diff --git a/src/test/java/org/mitre/jwt/JwtTest.java b/src/test/java/org/mitre/jwt/JwtTest.java index 0b18d6455..5c09702f8 100644 --- a/src/test/java/org/mitre/jwt/JwtTest.java +++ b/src/test/java/org/mitre/jwt/JwtTest.java @@ -3,6 +3,7 @@ package org.mitre.jwt; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; +import java.io.UnsupportedEncodingException; import java.util.Date; import org.junit.Test; @@ -24,7 +25,7 @@ public class JwtTest { signer.sign(jwt); /* - * Expected string based on the following structures, serialized exactly as folows and base64 encoded: + * Expected string based on the following structures, serialized exactly as follows and base64 encoded: * * header: {"alg":"none"} * claims: {"exp":1300819380,"iss":"joe","http://example.com/is_root":true} @@ -47,16 +48,35 @@ public class JwtTest { jwt.getClaims().setClaim("http://example.com/is_root", Boolean.TRUE); // sign it - byte[] key = "secret".getBytes(); + byte[] key = null; + try { + key = "secret".getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } JwtSigner signer = new Hmac256Signer(key); signer.sign(jwt); - - String expected = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.vQqHHhblAtGiFs7q7nPt9Q"; + + /* + * Expected string based on the following strucutres, serialized exactly as follows and base64 encoded: + * + * header: {"typ":"JWT","alg":"HS256"} + * claims: {"exp":1300819380,"iss":"joe","http://example.com/is_root":true} + * + * Expected signature: iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E + * + */ + String signature = "iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E"; + String expected = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E"; String actual = jwt.toString(); - + + assertThat(actual, equalTo(expected)); + assertThat(jwt.getSignature(), equalTo(signature)); + } @Test From 8998d2236907ca6e4e7a4124ab17ca05bc546e47 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Thu, 22 Dec 2011 13:15:56 -0500 Subject: [PATCH 7/9] added signature validator --- .../java/org/mitre/jwt/AbstractJwtSigner.java | 39 +++++++++++++++++-- .../java/org/mitre/jwt/Hmac256Signer.java | 19 ++++----- src/main/java/org/mitre/jwt/JwtSigner.java | 2 + .../java/org/mitre/jwt/PlaintextSigner.java | 9 ++--- src/test/java/org/mitre/jwt/JwtTest.java | 23 ++++++++++- 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/mitre/jwt/AbstractJwtSigner.java b/src/main/java/org/mitre/jwt/AbstractJwtSigner.java index aa17755ef..016211b7b 100644 --- a/src/main/java/org/mitre/jwt/AbstractJwtSigner.java +++ b/src/main/java/org/mitre/jwt/AbstractJwtSigner.java @@ -1,8 +1,13 @@ package org.mitre.jwt; -import com.google.common.base.Objects; +import java.util.List; -public class AbstractJwtSigner implements JwtSigner { +import com.google.common.base.Objects; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.Lists; + +public abstract class AbstractJwtSigner implements JwtSigner { public static final String PLAINTEXT = "none"; public static final String HS256 = "HS256"; @@ -40,8 +45,36 @@ public class AbstractJwtSigner implements JwtSigner { // for now, we fix the Jwt jwt.getHeader().setAlgorithm(algorithm); } - + + String sig = generateSignature(jwt.getSignatureBase()); + + jwt.setSignature(sig); } + /* (non-Javadoc) + * @see org.mitre.jwt.JwtSigner#verify(java.lang.String) + */ + @Override + public boolean verify(String jwtString) { + // split on the dots + List parts = Lists.newArrayList(Splitter.on(".").split(jwtString)); + + if (parts.size() != 3) { + throw new IllegalArgumentException("Invalid JWT format."); + } + + String h64 = parts.get(0); + String c64 = parts.get(1); + String s64 = parts.get(2); + + String expectedSignature = generateSignature(h64 + "." + c64 + "."); + + return Strings.nullToEmpty(s64).equals(Strings.nullToEmpty(expectedSignature)); + + } + + + protected abstract String generateSignature(String signatureBase); + } \ No newline at end of file diff --git a/src/main/java/org/mitre/jwt/Hmac256Signer.java b/src/main/java/org/mitre/jwt/Hmac256Signer.java index e9d1ed2dd..81f98d8a7 100644 --- a/src/main/java/org/mitre/jwt/Hmac256Signer.java +++ b/src/main/java/org/mitre/jwt/Hmac256Signer.java @@ -34,15 +34,10 @@ public class Hmac256Signer extends AbstractJwtSigner { } - /* (non-Javadoc) - * @see org.mitre.jwt.AbstractJwtSigner#sign(org.mitre.jwt.Jwt) - */ - @Override - public void sign(Jwt jwt) { - super.sign(jwt); - + @Override + protected String generateSignature(String signatureBase) { if (passphrase == null) { - return; // TODO: probably throw some kind of exception + return null; // TODO: probably throw some kind of exception } try { @@ -53,7 +48,7 @@ public class Hmac256Signer extends AbstractJwtSigner { } try { - mac.update(jwt.getSignatureBase().getBytes("UTF-8")); + mac.update(signatureBase.getBytes("UTF-8")); } catch (IllegalStateException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -68,8 +63,7 @@ public class Hmac256Signer extends AbstractJwtSigner { // strip off any padding sig = sig.replace("=", ""); - - jwt.setSignature(sig); + return sig; } /** @@ -85,7 +79,8 @@ public class Hmac256Signer extends AbstractJwtSigner { public void setPassphrase(byte[] passphrase) { this.passphrase = passphrase; } - + + } diff --git a/src/main/java/org/mitre/jwt/JwtSigner.java b/src/main/java/org/mitre/jwt/JwtSigner.java index 3d2f3c797..ffe7f8cb8 100644 --- a/src/main/java/org/mitre/jwt/JwtSigner.java +++ b/src/main/java/org/mitre/jwt/JwtSigner.java @@ -4,4 +4,6 @@ public interface JwtSigner { public void sign(Jwt jwt); + public boolean verify(String jwtString); + } diff --git a/src/main/java/org/mitre/jwt/PlaintextSigner.java b/src/main/java/org/mitre/jwt/PlaintextSigner.java index 8c1058b1a..5e197da7b 100644 --- a/src/main/java/org/mitre/jwt/PlaintextSigner.java +++ b/src/main/java/org/mitre/jwt/PlaintextSigner.java @@ -7,11 +7,8 @@ public class PlaintextSigner extends AbstractJwtSigner { } @Override - public void sign(Jwt jwt) { - super.sign(jwt); - - jwt.setSignature(""); - - } + protected String generateSignature(String signatureBase) { + return null; + } } diff --git a/src/test/java/org/mitre/jwt/JwtTest.java b/src/test/java/org/mitre/jwt/JwtTest.java index 5c09702f8..1fe9aa739 100644 --- a/src/test/java/org/mitre/jwt/JwtTest.java +++ b/src/test/java/org/mitre/jwt/JwtTest.java @@ -39,7 +39,7 @@ public class JwtTest { } @Test - public void testHmacSignature() { + public void testGenerateHmacSignature() { Jwt jwt = new Jwt(); jwt.getHeader().setType("JWT"); jwt.getHeader().setAlgorithm("HS256"); @@ -79,6 +79,27 @@ public class JwtTest { } + @Test + public void testValidateHmacSignature() { + // sign it + byte[] key = null; + try { + key = "secret".getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + JwtSigner signer = new Hmac256Signer(key); + + String jwtString = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E"; + + boolean valid = signer.verify(jwtString); + + assertThat(valid, equalTo(Boolean.TRUE)); + + } + @Test public void testParse() { String source = "eyJhbGciOiJub25lIn0.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ."; From a2c5c99b638b6a2685dd32caa0e7410a7ff08f1f Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Thu, 22 Dec 2011 16:40:25 -0500 Subject: [PATCH 8/9] comments! --- src/test/java/org/mitre/jwt/JwtTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/org/mitre/jwt/JwtTest.java b/src/test/java/org/mitre/jwt/JwtTest.java index 1fe9aa739..7dcba425b 100644 --- a/src/test/java/org/mitre/jwt/JwtTest.java +++ b/src/test/java/org/mitre/jwt/JwtTest.java @@ -92,6 +92,15 @@ public class JwtTest { JwtSigner signer = new Hmac256Signer(key); + /* + * Token string based on the following strucutres, serialized exactly as follows and base64 encoded: + * + * header: {"typ":"JWT","alg":"HS256"} + * claims: {"exp":1300819380,"iss":"joe","http://example.com/is_root":true} + * + * Expected signature: iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E + * + */ String jwtString = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E"; boolean valid = signer.verify(jwtString); From ee0ef8d563cbfcf5ff20fdd9f9782af348a9b428 Mon Sep 17 00:00:00 2001 From: Justin Richer Date: Fri, 23 Dec 2011 10:56:43 -0500 Subject: [PATCH 9/9] fixed Jwt signature base --- src/main/java/org/mitre/jwt/Hmac256Signer.java | 4 ++++ src/main/java/org/mitre/jwt/Jwt.java | 11 ++++++++--- src/test/java/org/mitre/jwt/JwtTest.java | 4 ++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/mitre/jwt/Hmac256Signer.java b/src/main/java/org/mitre/jwt/Hmac256Signer.java index 81f98d8a7..a5968f97a 100644 --- a/src/main/java/org/mitre/jwt/Hmac256Signer.java +++ b/src/main/java/org/mitre/jwt/Hmac256Signer.java @@ -20,8 +20,12 @@ public class Hmac256Signer extends AbstractJwtSigner { this(null); } + public Hmac256Signer(byte[] passphrase) { super(HS256); + + //TODO: set up a factory for other signature methods + setPassphrase(passphrase); try { diff --git a/src/main/java/org/mitre/jwt/Jwt.java b/src/main/java/org/mitre/jwt/Jwt.java index 84a1bf44f..52e148a5b 100644 --- a/src/main/java/org/mitre/jwt/Jwt.java +++ b/src/main/java/org/mitre/jwt/Jwt.java @@ -103,12 +103,15 @@ public class Jwt { } /** - * Return the canonical encoded string of this JWT + * Return the canonical encoded string of this JWT, the header in Base64, a period ".", the claims in Base64, a period ".", and the signature in Base64. */ public String toString() { - return getSignatureBase() + Strings.nullToEmpty(this.signature); + return getSignatureBase() + "." + Strings.nullToEmpty(this.signature); } + /** + * The signature base of a JWT is the header in Base64, a period ".", and the claims in Base64. + */ public String getSignatureBase() { JsonObject h = header.getAsJsonObject(); JsonObject c = claims.getAsJsonObject(); @@ -116,7 +119,7 @@ public class Jwt { String h64 = new String(Base64.encodeBase64URLSafe(h.toString().getBytes())); String c64 = new String(Base64.encodeBase64URLSafe(c.toString().getBytes())); - return h64 + "." + c64 + "."; + return h64 + "." + c64; } @@ -143,6 +146,8 @@ public class Jwt { // shuttle for return value Jwt jwt = new Jwt(new JwtHeader(hjo), new JwtClaims(cjo), s64); + // TODO: save the wire-encoded string in the Jwt object itself? + return jwt; } diff --git a/src/test/java/org/mitre/jwt/JwtTest.java b/src/test/java/org/mitre/jwt/JwtTest.java index 7dcba425b..d52f9bf77 100644 --- a/src/test/java/org/mitre/jwt/JwtTest.java +++ b/src/test/java/org/mitre/jwt/JwtTest.java @@ -69,8 +69,8 @@ public class JwtTest { * Expected signature: iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E * */ - String signature = "iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E"; - String expected = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.iGBPJj47S5q_HAhSoQqAdcS6A_1CFj3zrLaImqNbt9E"; + String signature = "p-63Jzz7mgi3H4hvW6MFB7lmPRZjhsL666MYkmpX33Y"; + String expected = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjEzMDA4MTkzODAsImlzcyI6ImpvZSIsImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ." + signature; String actual = jwt.toString();