/*
* oxAuth is available under the MIT License (2008). See http://opensource.org/licenses/MIT for full text.
*
* Copyright (c) 2014, Gluu
*/
package org.xdi.oxauth.client.model;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.xdi.oxauth.model.crypto.AbstractCryptoProvider;
import org.xdi.oxauth.model.crypto.encryption.BlockEncryptionAlgorithm;
import org.xdi.oxauth.model.crypto.encryption.KeyEncryptionAlgorithm;
import org.xdi.oxauth.model.crypto.signature.SignatureAlgorithm;
import org.xdi.oxauth.model.exception.InvalidJwtException;
import org.xdi.oxauth.model.jwe.JweEncrypterImpl;
import org.xdi.oxauth.model.jwt.JwtHeader;
import org.xdi.oxauth.model.jwt.JwtType;
import org.xdi.oxauth.model.util.Base64Util;
import org.xdi.oxauth.model.util.Pair;
import org.xdi.oxauth.model.util.Util;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.PublicKey;
import java.security.SecureRandom;
import static org.xdi.oxauth.model.jwt.JwtStateClaimName.*;
/**
* @author Javier Rojas Blum
* @version May 3, 2017
*/
public class JwtState {
private static final Logger LOG = Logger.getLogger(JwtState.class);
// Header
private JwtType type;
private SignatureAlgorithm signatureAlgorithm;
private KeyEncryptionAlgorithm keyEncryptionAlgorithm;
private BlockEncryptionAlgorithm blockEncryptionAlgorithm;
private String keyId;
// Payload
private String rfp;
private String iat;
private String exp;
private String iss;
private String aud;
private String targetLinkUri;
private String as;
private String jti;
private String atHash;
private String cHash;
private JSONObject additionalClaims;
// Signature/Encryption Keys
private String sharedKey;
private AbstractCryptoProvider cryptoProvider;
public JwtState(SignatureAlgorithm signatureAlgorithm, AbstractCryptoProvider cryptoProvider) {
this(signatureAlgorithm, cryptoProvider, null, null, null);
}
public JwtState(SignatureAlgorithm signatureAlgorithm,
String sharedKey, AbstractCryptoProvider cryptoProvider) {
this(signatureAlgorithm, cryptoProvider, null, null, sharedKey);
}
public JwtState(KeyEncryptionAlgorithm keyEncryptionAlgorithm,
BlockEncryptionAlgorithm blockEncryptionAlgorithm, AbstractCryptoProvider cryptoProvider) {
this(null, cryptoProvider, keyEncryptionAlgorithm, blockEncryptionAlgorithm, null);
}
public JwtState(KeyEncryptionAlgorithm keyEncryptionAlgorithm,
BlockEncryptionAlgorithm blockEncryptionAlgorithm, String sharedKey) {
this(null, null, keyEncryptionAlgorithm, blockEncryptionAlgorithm, sharedKey);
}
private JwtState(SignatureAlgorithm signatureAlgorithm,
AbstractCryptoProvider cryptoProvider, KeyEncryptionAlgorithm keyEncryptionAlgorithm,
BlockEncryptionAlgorithm blockEncryptionAlgorithm, String sharedKey) {
this.type = JwtType.JWT;
this.signatureAlgorithm = signatureAlgorithm;
this.cryptoProvider = cryptoProvider;
this.keyEncryptionAlgorithm = keyEncryptionAlgorithm;
this.blockEncryptionAlgorithm = blockEncryptionAlgorithm;
this.sharedKey = sharedKey;
}
public JwtType getType() {
return type;
}
public void setType(JwtType type) {
this.type = type;
}
public SignatureAlgorithm getSignatureAlgorithm() {
return signatureAlgorithm;
}
public void setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) {
this.signatureAlgorithm = signatureAlgorithm;
}
public KeyEncryptionAlgorithm getKeyEncryptionAlgorithm() {
return keyEncryptionAlgorithm;
}
public void setKeyEncryptionAlgorithm(KeyEncryptionAlgorithm keyEncryptionAlgorithm) {
this.keyEncryptionAlgorithm = keyEncryptionAlgorithm;
}
public BlockEncryptionAlgorithm getBlockEncryptionAlgorithm() {
return blockEncryptionAlgorithm;
}
public void setBlockEncryptionAlgorithm(BlockEncryptionAlgorithm blockEncryptionAlgorithm) {
this.blockEncryptionAlgorithm = blockEncryptionAlgorithm;
}
/**
* Identifier of the key used to sign this state token at the issuer.
* Identifier of the key used to encrypt this JWT state token at the issuer.
*
* @return The key identifier
*/
public String getKeyId() {
return keyId;
}
/**
* Identifier of the key used to sign this state token at the issuer.
* Identifier of the key used to encrypt this JWT state token at the issuer.
*
* @param keyId The key identifier
*/
public void setKeyId(String keyId) {
this.keyId = keyId;
}
/**
* String containing a verifiable identifier for the browser session,
* that cannot be guessed by a third party.
* The verification of this element by the client protects it from
* accepting authorization responses generated in response to forged
* requests generated by third parties.
*
* @return The Request Forgery Protection value
*/
public String getRfp() {
return rfp;
}
/**
* String containing a verifiable identifier for the browser session,
* that cannot be guessed by a third party.
* The verification of this element by the client protects it from
* accepting authorization responses generated in response to forged
* requests generated by third parties.
*
* @param rfp The Request Forgery Protection value
*/
public void setRfp(String rfp) {
this.rfp = rfp;
}
/**
* Timestamp of when this Authorization Request was issued.
*
* @return The Issued at value
*/
public String getIat() {
return iat;
}
/**
* Timestamp of when this Authorization Request was issued.
*
* @param iat The Issued at value
*/
public void setIat(String iat) {
this.iat = iat;
}
/**
* The expiration time claim identifies the expiration time on or after which
* the JWT MUST NOT be accepted for processing.
* The processing of the "exp" claim requires that the current date/time MUST
* be before the expiration date/time listed in the "exp" claim.
* Implementers MAY provide for some small leeway, usually no more than a
* few minutes, to account for clock skew.
* Its value MUST be a number containing an IntDate value.
*
* @return The expiration time value
*/
public String getExp() {
return exp;
}
/**
* The expiration time claim identifies the expiration time on or after which
* the JWT MUST NOT be accepted for processing.
* The processing of the "exp" claim requires that the current date/time MUST
* be before the expiration date/time listed in the "exp" claim.
* Implementers MAY provide for some small leeway, usually no more than a
* few minutes, to account for clock skew.
* Its value MUST be a number containing an IntDate value.
*
* @param exp The expiration time value
*/
public void setExp(String exp) {
this.exp = exp;
}
/**
* String identifying the party that issued this state value.
*
* @return The issuer value
*/
public String getIss() {
return iss;
}
/**
* String identifying the party that issued this state value.
*
* @param iss The issuer value
*/
public void setIss(String iss) {
this.iss = iss;
}
/**
* String identifying the client that this state value is intended for.
*
* @return The audience
*/
public String getAud() {
return aud;
}
/**
* String identifying the client that this state value is intended for.
*
* @param aud The audience
*/
public void setAud(String aud) {
this.aud = aud;
}
/**
* URI containing the location the user agent is to be redirected to after authorization.
*
* @return The target link URI
*/
public String getTargetLinkUri() {
return targetLinkUri;
}
/**
* URI containing the location the user agent is to be redirected to after authorization.
*
* @param targetLinkUri The target link URI
*/
public void setTargetLinkUri(String targetLinkUri) {
this.targetLinkUri = targetLinkUri;
}
/**
* String identifying the authorization server that this request was sent to.
*
* @return The authorization server
*/
public String getAs() {
return as;
}
/**
* String identifying the authorization server that this request was sent to.
*
* @param as The authorization server
*/
public void setAs(String as) {
this.as = as;
}
/**
* The "jti" (JWT ID) claim provides a unique identifier for the JWT.
* The identifier value MUST be assigned in a manner that ensures that
* there is a negligible probability that the same value will be
* accidentally assigned to a different data object.
* The "jti" claim can be used to prevent the JWT from being replayed.
* The "jti" value is a case-sensitive string.
*
* @return The JWT ID
*/
public String getJti() {
return jti;
}
/**
* The "jti" (JWT ID) claim provides a unique identifier for the JWT.
* The identifier value MUST be assigned in a manner that ensures that
* there is a negligible probability that the same value will be
* accidentally assigned to a different data object.
* The "jti" claim can be used to prevent the JWT from being replayed.
* The "jti" value is a case-sensitive string.
*
* @param jti The JWT ID
*/
public void setJti(String jti) {
this.jti = jti;
}
/**
* Access Token hash value. Its value is the base64url encoding of the left-most half
* of the hash of the octets of the ASCII representation of the "access_token" value,
* where the hash algorithm used is the hash algorithm used in the "alg" parameter of
* the State Token's JWS header.
* For instance, if the "alg" is "RS256", hash the "access_token" value with SHA-256,
* then take the left-most 128 bits and base64url encode them.
* The "at_hash" value is a case sensitive string.
* This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and
* issued with a "access_token" in the authorization response.
*
* @return The access token hash value
*/
public String getAtHash() {
return atHash;
}
/**
* Access Token hash value. Its value is the base64url encoding of the left-most half
* of the hash of the octets of the ASCII representation of the "access_token" value,
* where the hash algorithm used is the hash algorithm used in the "alg" parameter of
* the State Token's JWS header.
* For instance, if the "alg" is "RS256", hash the "access_token" value with SHA-256,
* then take the left-most 128 bits and base64url encode them.
* The "at_hash" value is a case sensitive string.
* This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and
* issued with a "access_token" in the authorization response.
*
* @param atHash The access token hash value
*/
public void setAtHash(String atHash) {
this.atHash = atHash;
}
/**
* Code hash value. Its value is the base64url encoding of the left-most half of the
* hash of the octets of the ASCII representation of the "code" value, where the hash
* algorithm used is the hash algorithm used in the "alg" header parameter of the
* State Token's JWS [RFC7515] header.
* For instance, if the "alg" is "HS512", hash the "code" value with SHA-512, then
* take the left-most 256 bits and base64url encode them.
* The "c_hash" value is a case sensitive string.
* This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and
* issued with a "code" in the authorization response.
*
* @return The code hash value
*/
public String getcHash() {
return cHash;
}
/**
* Code hash value. Its value is the base64url encoding of the left-most half of the
* hash of the octets of the ASCII representation of the "code" value, where the hash
* algorithm used is the hash algorithm used in the "alg" header parameter of the
* State Token's JWS [RFC7515] header.
* For instance, if the "alg" is "HS512", hash the "code" value with SHA-512, then
* take the left-most 256 bits and base64url encode them.
* The "c_hash" value is a case sensitive string.
* This is REQUIRED if the JWT [RFC7519] state token is being produced by the AS and
* issued with a "code" in the authorization response.
*
* @param cHash The code hash value
*/
public void setcHash(String cHash) {
this.cHash = cHash;
}
public JSONObject getAdditionalClaims() {
return additionalClaims;
}
public void setAdditionalClaims(JSONObject additionalClaims) {
this.additionalClaims = additionalClaims;
}
public String getEncodedJwt(JSONObject jwks) throws Exception {
String encodedJwt = null;
if (keyEncryptionAlgorithm != null && blockEncryptionAlgorithm != null) {
JweEncrypterImpl jweEncrypter;
if (cryptoProvider != null && jwks != null) {
PublicKey publicKey = cryptoProvider.getPublicKey(keyId, jwks);
jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, blockEncryptionAlgorithm, publicKey);
} else {
jweEncrypter = new JweEncrypterImpl(keyEncryptionAlgorithm, blockEncryptionAlgorithm, sharedKey.getBytes(Util.UTF8_STRING_ENCODING));
}
String header = headerToJSONObject().toString();
String encodedHeader = Base64Util.base64urlencode(header.getBytes(Util.UTF8_STRING_ENCODING));
String claims = payloadToJSONObject().toString();
String encodedClaims = Base64Util.base64urlencode(claims.getBytes(Util.UTF8_STRING_ENCODING));
byte[] contentMasterKey = new byte[blockEncryptionAlgorithm.getCmkLength() / 8];
SecureRandom random = new SecureRandom();
random.nextBytes(contentMasterKey);
String encodedEncryptedKey = jweEncrypter.generateEncryptedKey(contentMasterKey);
byte[] initializationVector = new byte[blockEncryptionAlgorithm.getInitVectorLength() / 8];
random.nextBytes(initializationVector);
String encodedInitializationVector = Base64Util.base64urlencode(initializationVector);
String additionalAuthenticatedData = encodedHeader + "."
+ encodedEncryptedKey + "."
+ encodedInitializationVector;
Pair<String, String> result = jweEncrypter.generateCipherTextAndIntegrityValue(contentMasterKey, initializationVector,
additionalAuthenticatedData.getBytes(Util.UTF8_STRING_ENCODING),
encodedClaims.getBytes(Util.UTF8_STRING_ENCODING));
String encodedCipherText = result.getFirst();
String encodedIntegrityValue = result.getSecond();
encodedJwt = encodedHeader + "."
+ encodedEncryptedKey + "."
+ encodedInitializationVector + "."
+ encodedCipherText + "."
+ encodedIntegrityValue;
} else {
if (cryptoProvider == null) {
throw new Exception("The Crypto Provider cannot be null.");
}
JSONObject headerJsonObject = headerToJSONObject();
JSONObject payloadJsonObject = payloadToJSONObject();
String headerString = headerJsonObject.toString();
String payloadString = payloadJsonObject.toString();
String encodedHeader = Base64Util.base64urlencode(headerString.getBytes(Util.UTF8_STRING_ENCODING));
String encodedPayload = Base64Util.base64urlencode(payloadString.getBytes(Util.UTF8_STRING_ENCODING));
String signingInput = encodedHeader + "." + encodedPayload;
String encodedSignature = cryptoProvider.sign(signingInput, keyId, sharedKey, signatureAlgorithm);
encodedJwt = encodedHeader + "." + encodedPayload + "." + encodedSignature;
}
return encodedJwt;
}
public String getEncodedJwt() throws Exception {
return getEncodedJwt(null);
}
protected JSONObject headerToJSONObject() throws InvalidJwtException {
JwtHeader jwtHeader = new JwtHeader();
jwtHeader.setType(type);
if (keyEncryptionAlgorithm != null && blockEncryptionAlgorithm != null) {
jwtHeader.setAlgorithm(keyEncryptionAlgorithm);
jwtHeader.setEncryptionMethod(blockEncryptionAlgorithm);
} else {
jwtHeader.setAlgorithm(signatureAlgorithm);
}
jwtHeader.setKeyId(keyId);
return jwtHeader.toJsonObject();
}
protected JSONObject payloadToJSONObject() throws JSONException {
JSONObject obj = new JSONObject();
try {
if (StringUtils.isNotBlank(rfp)) {
obj.put(RFP, rfp);
}
if (StringUtils.isNotBlank(keyId)) {
obj.put(KID, keyId);
}
if (StringUtils.isNotBlank(iat)) {
obj.put(IAT, iat);
}
if (StringUtils.isNotBlank(exp)) {
obj.put(EXP, exp);
}
if (StringUtils.isNotBlank(iss)) {
obj.put(ISS, iss);
}
if (StringUtils.isNotBlank(aud)) {
obj.put(AUD, aud);
}
if (StringUtils.isNotBlank(targetLinkUri)) {
obj.put(TARGET_LINK_URI, URLEncoder.encode(targetLinkUri, "UTF-8"));
}
if (StringUtils.isNotBlank(as)) {
obj.put(AS, as);
}
if (StringUtils.isNotBlank(jti)) {
obj.put(JTI, jti);
}
if (StringUtils.isNotBlank(atHash)) {
obj.put(AT_HASH, atHash);
}
if (StringUtils.isNotBlank(cHash)) {
obj.put(C_HASH, cHash);
}
if (additionalClaims != null) {
obj.put(ADDITIONAL_CLAIMS, additionalClaims);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return obj;
}
}