Skip to content

Commit

Permalink
JWT generation and parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
david-guzman committed Jan 26, 2017
1 parent aa3f35a commit c582f38
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 18 deletions.
5 changes: 5 additions & 0 deletions eidpsam/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
<artifactId>mockito-core</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package uk.ac.ucl.eidp.auth.jaspic;

import uk.ac.ucl.eidp.auth.jwt.CallerClaims;
import uk.ac.ucl.eidp.auth.jwt.Jwt;

import java.io.IOException;
import java.nio.charset.Charset;
import java.security.Key;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
Expand All @@ -13,13 +19,17 @@
import javax.security.auth.message.AuthStatus;
import javax.security.auth.message.MessageInfo;
import javax.security.auth.message.MessagePolicy;
import javax.security.auth.message.callback.CallerPrincipalCallback;
import javax.security.auth.message.callback.PasswordValidationCallback;
import javax.security.auth.message.module.ServerAuthModule;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;



public class HeaderBasicAuthModule implements ServerAuthModule {

private MessagePolicy requestPolicy;
private CallbackHandler handler;
private Map<String, String> options;

Expand All @@ -31,12 +41,21 @@ public class HeaderBasicAuthModule implements ServerAuthModule {
private boolean allowNotSecure = false;
// Initially set to UTF-8
private String credentialsEncoding = "UTF-8";
private String apiKeySecret;
private String jwtIssuer;
private long validFor = 0L;
private Key jwtKey;
private static final String BASIC = "Basic";
private static final String BEARER = "Bearer";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String ALLOW_NOTSECURE_NAME = "allow.notsecure";
private static final String CREDENTIALS_ENCODING_NAME = "credentials.encoding";
private static final String APIKEY_SECRET_NAME = "jwt.secret";
private static final String JWT_ISSUER_NAME = "jwt.issuer";
private static final String JWT_VALID_FOR = "jwt.validfor";
private static final String DEF_ENC = "UTF-8";
private static final String DEF_NOT_SEC = "false";
private static final String DEF_ISS = "eidp";

// private static final String MESSAGES = "META-INF/Messages";

Expand All @@ -51,6 +70,7 @@ public void initialize(
CallbackHandler handler,
Map options
) throws AuthException {
this.requestPolicy = requestPolicy;
this.handler = handler;
if (null != options) {
this.options = options;
Expand All @@ -59,7 +79,14 @@ public void initialize(
(String) options.getOrDefault(ALLOW_NOTSECURE_NAME, DEF_NOT_SEC)
);
credentialsEncoding = (String) options.getOrDefault(CREDENTIALS_ENCODING_NAME, DEF_ENC);
apiKeySecret = (String) options.get(APIKEY_SECRET_NAME);
jwtIssuer = (String) options.getOrDefault(JWT_ISSUER_NAME, DEF_ISS);
validFor = Long.parseLong((String) options.getOrDefault(JWT_VALID_FOR, "0"));
}
if (null == jwtKey) {
jwtKey = initJwtKey();
}

}
}

Expand All @@ -76,18 +103,31 @@ public AuthStatus validateRequest(
) throws AuthException {
final HttpServletRequest req = (HttpServletRequest) messageInfo.getRequestMessage();

// Evaluate whether not secure connections are allowed
// Evaluate whether not secure connections are allowed against policy
if (!req.isSecure() && !allowNotSecure) {
return AuthStatus.SEND_FAILURE;
}

final String userName = readAuthenticationHeader(messageInfo, clientSubject);
// Token authorisation mandatory for all resources except for authentication
if (!requestPolicy.isMandatory()) {

if (null == userName) {
return AuthStatus.FAILURE;
}
final String userName = readAuthenticationHeader(messageInfo, clientSubject);

return AuthStatus.SUCCESS;
if (null == userName) {
return AuthStatus.FAILURE;
}

return AuthStatus.SUCCESS;

// Otherwise process JWT
} else {
final boolean validJwt = readBearerHeader(messageInfo, clientSubject);
if (validJwt) {
return AuthStatus.SUCCESS;
} else {
return AuthStatus.FAILURE;
}
}

}

Expand All @@ -109,9 +149,7 @@ protected void setPasswordValidationCallback(PasswordValidationCallback pwdValid

private String readAuthenticationHeader(MessageInfo msgInfo, Subject subj) throws AuthException {

HttpServletRequest request = (HttpServletRequest) msgInfo.getRequestMessage();

String header = request.getHeader(AUTHORIZATION_HEADER);
String header = getAuthorisationHeader(msgInfo);

if (!isEmpty(header) && header.startsWith(BASIC + " ")) {

Expand Down Expand Up @@ -151,7 +189,62 @@ private String readAuthenticationHeader(MessageInfo msgInfo, Subject subj) throw
return null;
}

private boolean readBearerHeader(MessageInfo msgInfo, Subject subj) throws AuthException {
String header = getAuthorisationHeader(msgInfo);

if (isEmpty(header) || !header.startsWith(BEARER + " ")) {
throw new AuthException("JWT: Header missing or not valid");
}

header = header.substring(7).trim();
CallerClaims callerClaims = Jwt.toCallerClaims(header, jwtKey);
if (null == callerClaims) {
throw new AuthException("JWT: Signature not valid");
}

if (validFor > 0) {
if (null == callerClaims.getExp()) {
throw new AuthException("JWT: Claim missing");
}
Date date = new Date(System.currentTimeMillis());
if (callerClaims.getExp().before(date)) {
return false;
}
}

if (!jwtIssuer.equalsIgnoreCase(callerClaims.getIss())) {
return false;
}

Callback callerCallback = new CallerPrincipalCallback(subj, callerClaims.getSub());
Callback[] callbacks = new Callback[]{callerCallback};

try {
handler.handle(callbacks);
} catch (IOException | UnsupportedCallbackException ex) {
AuthException ae = new AuthException();
ae.initCause(ex);
throw ae;
}

return true;

}

private boolean isEmpty(String text) {
return text == null || text.isEmpty();
}

private String getAuthorisationHeader(MessageInfo msgInfo) {
HttpServletRequest request = (HttpServletRequest) msgInfo.getRequestMessage();
return request.getHeader(AUTHORIZATION_HEADER);
}

private Key initJwtKey() throws AuthException {
if (null == apiKeySecret) {
throw new AuthException("Authorisation parameter missing");
}
return new SecretKeySpec(apiKeySecret.getBytes(Charset.forName(credentialsEncoding)),"AES");
}

}
41 changes: 41 additions & 0 deletions eidpsam/src/main/java/uk/ac/ucl/eidp/auth/jwt/CallerClaims.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package uk.ac.ucl.eidp.auth.jwt;

import java.util.Date;
import java.util.UUID;

/**
* JWT Claims for processing in JASPIC modules.
* @author David Guzman {@literal d.guzman at ucl.ac.uk}
*/
public class CallerClaims {

private final UUID uid = UUID.randomUUID();
private final String sub;
private final String iss;
private Date exp;

public CallerClaims(String sub, String iss) {
this.sub = sub;
this.iss = iss;
}

public String getSub() {
return sub;
}

public String getIss() {
return iss;
}

public String getJti() {
return uid.toString();
}

public Date getExp() {
return exp;
}

public void setExp(Date exp) {
this.exp = exp;
}
}
61 changes: 61 additions & 0 deletions eidpsam/src/main/java/uk/ac/ucl/eidp/auth/jwt/Jwt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package uk.ac.ucl.eidp.auth.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;

import java.security.Key;
import java.util.Date;


/**
* Utility class for the generation and validation of JWT tokens.
* @author David Guzman {@literal d.guzman at ucl.ac.uk}
*/
public class Jwt {

/**
* Generates a JSON Web Token using HS256 signature algorithm.
* @param callerClaims The JWT claims.
* @param apiKey The API key.
* @param validForMs Validity in milliseconds of the token to be generated.
* @return The JSON Web Token.
*/
public static String createJwtToken(CallerClaims callerClaims, Key apiKey, long validForMs) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

long nowMs = System.currentTimeMillis();
Date now = new Date(nowMs);

JwtBuilder builder = Jwts.builder().setId(callerClaims.getJti())
.setIssuedAt(now)
.setSubject(callerClaims.getSub())
.setIssuer(callerClaims.getIss())
.signWith(signatureAlgorithm, apiKey);

if (validForMs > 0) {
builder.setExpiration(new Date(nowMs + validForMs));
}

return builder.compact();
}

/**
* Extract the subject information from the JWT.
* @param jwt the JSON web token
* @param apiKey the API key
* @return a Subject with JWT claims added as Principals
*/
public static CallerClaims toCallerClaims(String jwt, Key apiKey) {
try {
Claims claim = Jwts.parser().setSigningKey(apiKey).parseClaimsJws(jwt).getBody();
CallerClaims callerClaims = new CallerClaims(claim.getSubject(), claim.getIssuer());
callerClaims.setExp(claim.getExpiration());
return callerClaims;
} catch (SignatureException ex) {
return (null);
}
}
}
Loading

0 comments on commit c582f38

Please sign in to comment.