More improvements

This commit is contained in:
heromyth 2020-11-23 18:00:11 +08:00
parent 3b0c275fbc
commit 0b75b521b3
18 changed files with 1193 additions and 745 deletions

View file

@ -0,0 +1,20 @@
module hunt.jwt.Base64Codec;
import std.base64;
alias Base64URLNoPadding = Base64Impl!('-', '_', Base64.NoPadding);
/**
* Encode a string with URL-safe Base64.
*/
string urlsafeB64Encode(string inp) pure nothrow {
return Base64URLNoPadding.encode(cast(ubyte[])inp);
}
/**
* Decode a string with URL-safe Base64.
*/
string urlsafeB64Decode(string inp) pure {
return cast(string)Base64URLNoPadding.decode(inp);
}

267
source/hunt/jwt/Claims.d Normal file
View file

@ -0,0 +1,267 @@
module hunt.jwt.Claims;
import hunt.jwt.Base64Codec;
import hunt.jwt.Component;
import hunt.jwt.Exceptions;
import hunt.jwt.JwtAlgorithm;
import std.conv;
import std.datetime;
import std.json;
import std.string;
/**
* represents the claims component of a JWT
*/
class Claims : Component {
private JSONValue data;
private this(in JSONValue claims) {
this.data = claims;
}
this() {
this.data = JSONValue(["iat": JSONValue(Clock.currTime.toUnixTime())]);
}
void set(T)(string name, T data) {
static if(is(T == JSONValue)) {
this.data.object[name] = data;
} else {
this.data.object[name] = JSONValue(data);
}
}
/**
* Params:
* name = the name of the claim
* Returns: returns a string representation of the claim if it exists and is a string or an empty string if doesn't exist or is not a string
*/
string get(string name) {
try {
return this.data[name].str();
} catch (JSONException e) {
return string.init;
}
}
/**
* Params:
* name = the name of the claim
* Returns: an array of JSONValue
*/
JSONValue[] getArray(string name) {
try {
return this.data[name].array();
} catch (JSONException e) {
return JSONValue.Store.array.init;
}
}
/**
* Params:
* name = the name of the claim
* Returns: a JSONValue
*/
JSONValue[string] getObject(string name) {
try {
return this.data[name].object();
} catch (JSONException e) {
return JSONValue.Store.object.init;
}
}
/**
* Params:
* name = the name of the claim
* Returns: returns a long representation of the claim if it exists and is an
* integer or the initial value for long if doesn't exist or is not an integer
*/
long getInt(string name) {
try {
return this.data[name].integer();
} catch (JSONException e) {
return long.init;
}
}
/**
* Params:
* name = the name of the claim
* Returns: returns a double representation of the claim if it exists and is a
* double or the initial value for double if doesn't exist or is not a double
*/
double getDouble(string name) {
try {
return this.data[name].floating();
} catch (JSONException e) {
return double.init;
}
}
/**
* Params:
* name = the name of the claim
* Returns: returns a boolean representation of the claim if it exists and is a
* boolean or the initial value for bool if doesn't exist or is not a boolean
*/
bool getBool(string name) {
try {
return this.data[name].type == JSONType.true_;
} catch (JSONException e) {
return bool.init;
}
}
/**
* Params:
* name = the name of the claim
* Returns: returns a boolean value if the claim exists and is null or
* the initial value for bool it it doesn't exist or is not null
*/
bool isNull(string name) {
try {
return this.data[name].isNull();
} catch (JSONException) {
return bool.init;
}
}
@property void iss(string s) {
this.data.object["iss"] = s;
}
@property string iss() {
try {
return this.data["iss"].str();
} catch (JSONException e) {
return "";
}
}
@property void sub(string s) {
this.data.object["sub"] = s;
}
@property string sub() {
try {
return this.data["sub"].str();
} catch (JSONException e) {
return "";
}
}
@property void aud(string s) {
this.data.object["aud"] = s;
}
@property string aud() {
try {
return this.data["aud"].str();
} catch (JSONException e) {
return "";
}
}
@property void exp(long n) {
this.data.object["exp"] = n;
}
@property long exp() {
try {
return this.data["exp"].integer;
} catch (JSONException) {
return 0;
}
}
@property void nbf(long n) {
this.data.object["nbf"] = n;
}
@property long nbf() {
try {
return this.data["nbf"].integer;
} catch (JSONException) {
return 0;
}
}
@property void iat(long n) {
this.data.object["iat"] = n;
}
@property long iat() {
try {
return this.data["iat"].integer;
} catch (JSONException) {
return 0;
}
}
@property void jit(string s) {
this.data.object["jit"] = s;
}
@property string jit() {
try {
return this.data["jit"].str();
} catch(JSONException e) {
return "";
}
}
/**
* gives json encoded claims
* Returns: json encoded claims
*/
@property override string json() {
return this.data.toString();
}
}

View file

@ -0,0 +1,16 @@
module hunt.jwt.Component;
import hunt.jwt.Base64Codec;
/**
*
*/
class Component {
abstract @property string json();
@property string base64() {
string data = this.json();
return urlsafeB64Encode(data);
}
}

View file

@ -0,0 +1,31 @@
module hunt.jwt.Exceptions;
import std.exception;
class SignException : Exception {
this(string s) { super(s); }
}
class VerifyException : Exception {
this(string s) { super(s); }
}
/**
* thrown when the tokens is expired
*/
class ExpiredException : VerifyException {
this(string s) {
super(s);
}
}
/**
* thrown when the tokens will expire before it becomes valid
* usually when the nbf claim is greater than the exp claim
*/
class ExpiresBeforeValidException : Exception {
this(string s) {
super(s);
}
}

43
source/hunt/jwt/Header.d Normal file
View file

@ -0,0 +1,43 @@
module hunt.jwt.Header;
import hunt.jwt.Base64Codec;
import hunt.jwt.Component;
import hunt.jwt.JwtAlgorithm;
import std.conv;
import std.json;
import std.string;
/**
*
*/
class Header : Component {
JwtAlgorithm alg;
string typ;
this(in JwtAlgorithm alg, in string typ) {
this.alg = alg;
this.typ = typ;
}
this(in JSONValue headers) {
try {
this.alg = to!(JwtAlgorithm)(toUpper(headers["alg"].str()));
} catch (Exception e) {
throw new Exception(alg ~ " algorithm is not supported!");
}
this.typ = headers["typ"].str();
}
@property override string json() {
JSONValue headers = ["alg": cast(string)this.alg, "typ": this.typ];
return headers.toString();
}
}

114
source/hunt/jwt/Jwt.d Normal file
View file

@ -0,0 +1,114 @@
module hunt.jwt.Jwt;
import hunt.jwt.Base64Codec;
import hunt.jwt.Exceptions;
import hunt.jwt.JwtOpenSSL;
import hunt.jwt.JwtAlgorithm;
import std.json;
import std.base64;
import std.algorithm;
import std.array : split;
/**
simple version that accepts only strings as values for payload and header fields
*/
string encode(string[string] payload, string key, JwtAlgorithm algo = JwtAlgorithm.HS256,
string[string] header_fields = null) {
JSONValue jsonHeader = header_fields;
JSONValue jsonPayload = payload;
return encode(jsonPayload, key, algo, jsonHeader);
}
/**
full version that accepts JSONValue tree as payload and header fields
*/
string encode(ref JSONValue payload, string key, JwtAlgorithm algo = JwtAlgorithm.HS256,
JSONValue header_fields = null) {
return encode(cast(ubyte[])payload.toString(), key, algo, header_fields);
}
/**
full version that accepts ubyte[] as payload and JSONValue tree as header fields
*/
string encode(in ubyte[] payload, string key, JwtAlgorithm algo = JwtAlgorithm.HS256,
JSONValue header_fields = null) {
import std.functional : memoize;
auto getEncodedHeader(JwtAlgorithm algo, JSONValue fields) {
if(fields.type == JSONType.null_)
fields = (JSONValue[string]).init;
fields.object["alg"] = cast(string)algo;
fields.object["typ"] = "JWT";
return Base64URLNoPadding.encode(cast(ubyte[])fields.toString()).idup;
}
string encodedHeader = memoize!(getEncodedHeader, 64)(algo, header_fields);
string encodedPayload = Base64URLNoPadding.encode(payload);
string signingInput = encodedHeader ~ "." ~ encodedPayload;
string signature = Base64URLNoPadding.encode(cast(ubyte[])sign(signingInput, key, algo));
return signingInput ~ "." ~ signature;
}
/**
simple version that knows which key was used to encode the token
*/
JSONValue decode(string token, string key) {
return decode(token, (ref _) => key);
}
/**
full version where the key is provided after decoding the JOSE header
*/
JSONValue decode(string token, string delegate(ref JSONValue jose) lazyKey) {
import std.algorithm : count;
import std.conv : to;
import std.uni : toUpper;
if(count(token, ".") != 2)
throw new VerifyException("Token is incorrect.");
string[] tokenParts = split(token, ".");
JSONValue header;
try {
header = parseJSON(urlsafeB64Decode(tokenParts[0]));
} catch(Exception e) {
throw new VerifyException("Header is incorrect.");
}
JwtAlgorithm alg;
try {
// toUpper for none
alg = to!(JwtAlgorithm)(toUpper(header["alg"].str()));
} catch(Exception e) {
throw new VerifyException("Algorithm is incorrect.");
}
if (auto typ = ("typ" in header)) {
string typ_str = typ.str();
if(typ_str && typ_str != "JWT")
throw new VerifyException("Type is incorrect.");
}
const key = lazyKey(header);
if(!verifySignature(urlsafeB64Decode(tokenParts[2]), tokenParts[0]~"."~tokenParts[1], key, alg))
throw new VerifyException("Signature is incorrect.");
JSONValue payload;
try {
payload = parseJSON(urlsafeB64Decode(tokenParts[1]));
} catch(JSONException e) {
// Code coverage has to miss this line because the signature test above throws before this does
throw new VerifyException("Payload JSON is incorrect.");
}
return payload;
}

View file

@ -0,0 +1,18 @@
module hunt.jwt.JwtAlgorithm;
enum JwtAlgorithm : string {
NONE = "none",
HS256 = "HS256",
HS384 = "HS384",
HS512 = "HS512",
RS256 = "RS256",
RS384 = "RS384",
RS512 = "RS512",
ES256 = "ES256",
ES384 = "ES384",
ES512 = "ES512"
}
deprecated("Using JwtAlgorithm instead.")
alias JWTAlgorithm = JwtAlgorithm;

View file

@ -0,0 +1,334 @@
module hunt.jwt.JwtOpenSSL;
import deimos.openssl.ssl;
import deimos.openssl.pem;
import deimos.openssl.rsa;
import deimos.openssl.hmac;
import deimos.openssl.err;
import hunt.jwt.Exceptions;
import hunt.jwt.JwtAlgorithm;
EC_KEY* getESKeypair(uint curve_type, string key) {
EC_GROUP* curve;
EVP_PKEY* pktmp;
BIO* bpo;
EC_POINT* pub;
if(null == (curve = EC_GROUP_new_by_curve_name(curve_type)))
throw new Exception("Unsupported curve.");
scope(exit) EC_GROUP_free(curve);
bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1);
if(bpo is null) {
throw new Exception("Can't load the key.");
}
scope(exit) BIO_free(bpo);
pktmp = PEM_read_bio_PrivateKey(bpo, null, null, null);
if(pktmp is null) {
throw new Exception("Can't load the evp_pkey.");
}
scope(exit) EVP_PKEY_free(pktmp);
EC_KEY* eckey;
eckey = EVP_PKEY_get1_EC_KEY(pktmp);
if(eckey is null) {
throw new Exception("Can't convert evp_pkey to EC_KEY.");
}
scope(failure) EC_KEY_free(eckey);
if(1 != EC_KEY_set_group(eckey, curve)) {
throw new Exception("Can't associate group with the key.");
}
const BIGNUM *prv = EC_KEY_get0_private_key(eckey);
if(null == prv) {
throw new Exception("Can't get private key.");
}
pub = EC_POINT_new(curve);
if(null == pub) {
throw new Exception("Can't allocate EC point.");
}
scope(exit) EC_POINT_free(pub);
if (1 != EC_POINT_mul(curve, pub, prv, null, null, null)) {
throw new Exception("Can't calculate public key.");
}
if(1 != EC_KEY_set_public_key(eckey, pub)) {
throw new Exception("Can't set public key.");
}
return eckey;
}
EC_KEY* getESPrivateKey(uint curve_type, string key) {
EC_GROUP* curve;
EVP_PKEY* pktmp;
BIO* bpo;
if(null == (curve = EC_GROUP_new_by_curve_name(curve_type)))
throw new Exception("Unsupported curve.");
scope(exit) EC_GROUP_free(curve);
bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1);
if(bpo is null) {
throw new Exception("Can't load the key.");
}
scope(exit) BIO_free(bpo);
pktmp = PEM_read_bio_PrivateKey(bpo, null, null, null);
if(pktmp is null) {
throw new Exception("Can't load the evp_pkey.");
}
scope(exit) EVP_PKEY_free(pktmp);
EC_KEY * eckey;
eckey = EVP_PKEY_get1_EC_KEY(pktmp);
if(eckey is null) {
throw new Exception("Can't convert evp_pkey to EC_KEY.");
}
scope(failure) EC_KEY_free(eckey);
if(1 != EC_KEY_set_group(eckey, curve)) {
throw new Exception("Can't associate group with the key.");
}
return eckey;
}
EC_KEY* getESPublicKey(uint curve_type, string key) {
EC_GROUP* curve;
if(null == (curve = EC_GROUP_new_by_curve_name(curve_type)))
throw new Exception("Unsupported curve.");
scope(exit) EC_GROUP_free(curve);
EC_KEY* eckey;
BIO* bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1);
if(bpo is null) {
throw new Exception("Can't load the key.");
}
scope(exit) BIO_free(bpo);
eckey = PEM_read_bio_EC_PUBKEY(bpo, null, null, null);
scope(failure) EC_KEY_free(eckey);
if(1 != EC_KEY_set_group(eckey, curve)) {
throw new Exception("Can't associate group with the key.");
}
if(0 == EC_KEY_check_key(eckey))
throw new Exception("Public key is not valid.");
return eckey;
}
string sign(string msg, string key, JwtAlgorithm algo = JwtAlgorithm.HS256) {
ubyte[] sign;
void sign_hs(const(EVP_MD)* evp, uint signLen) {
sign = new ubyte[signLen];
HMAC_CTX ctx;
scope(exit) HMAC_CTX_reset(&ctx);
HMAC_CTX_reset(&ctx);
if(0 == HMAC_Init_ex(&ctx, key.ptr, cast(int)key.length, evp, null)) {
throw new Exception("Can't initialize HMAC context.");
}
if(0 == HMAC_Update(&ctx, cast(const(ubyte)*)msg.ptr, cast(ulong)msg.length)) {
throw new Exception("Can't update HMAC.");
}
if(0 == HMAC_Final(&ctx, cast(ubyte*)sign.ptr, &signLen)) {
throw new Exception("Can't finalize HMAC.");
}
}
void sign_rs(ubyte* hash, int type, uint len, uint signLen) {
sign = new ubyte[len];
RSA* rsa_private = RSA_new();
scope(exit) RSA_free(rsa_private);
BIO* bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1);
if(bpo is null)
throw new Exception("Can't load the key.");
scope(exit) BIO_free(bpo);
RSA* rsa = PEM_read_bio_RSAPrivateKey(bpo, &rsa_private, null, null);
if(rsa is null) {
throw new Exception("Can't create RSA key.");
}
if(0 == RSA_sign(type, hash, signLen, sign.ptr, &signLen, rsa_private)) {
throw new Exception("Can't sign RSA message digest.");
}
}
void sign_es(uint curve_type, ubyte* hash, int hashLen) {
EC_KEY* eckey = getESPrivateKey(curve_type, key);
scope(exit) EC_KEY_free(eckey);
ECDSA_SIG* sig = ECDSA_do_sign(hash, hashLen, eckey);
if(sig is null) {
throw new Exception("Digest sign failed.");
}
scope(exit) ECDSA_SIG_free(sig);
sign = new ubyte[ECDSA_size(eckey)];
ubyte* c = sign.ptr;
if(!i2d_ECDSA_SIG(sig, &c)) {
throw new Exception("Convert sign to DER format failed.");
}
}
switch(algo) {
case JwtAlgorithm.NONE: {
break;
}
case JwtAlgorithm.HS256: {
sign_hs(EVP_sha256(), SHA256_DIGEST_LENGTH);
break;
}
case JwtAlgorithm.HS384: {
sign_hs(EVP_sha384(), SHA384_DIGEST_LENGTH);
break;
}
case JwtAlgorithm.HS512: {
sign_hs(EVP_sha512(), SHA512_DIGEST_LENGTH);
break;
}
case JwtAlgorithm.RS256: {
ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH];
SHA256(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr);
sign_rs(hash.ptr, NID_sha256, 256, SHA256_DIGEST_LENGTH);
break;
}
case JwtAlgorithm.RS384: {
ubyte[] hash = new ubyte[SHA384_DIGEST_LENGTH];
SHA384(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr);
sign_rs(hash.ptr, NID_sha384, 384, SHA384_DIGEST_LENGTH);
break;
}
case JwtAlgorithm.RS512: {
ubyte[] hash = new ubyte[SHA512_DIGEST_LENGTH];
SHA512(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr);
sign_rs(hash.ptr, NID_sha512, 512, SHA512_DIGEST_LENGTH);
break;
}
case JwtAlgorithm.ES256: {
ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH];
SHA256(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr);
sign_es(NID_secp256k1, hash.ptr, SHA256_DIGEST_LENGTH);
break;
}
case JwtAlgorithm.ES384: {
ubyte[] hash = new ubyte[SHA384_DIGEST_LENGTH];
SHA384(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr);
sign_es(NID_secp384r1, hash.ptr, SHA384_DIGEST_LENGTH);
break;
}
case JwtAlgorithm.ES512: {
ubyte[] hash = new ubyte[SHA512_DIGEST_LENGTH];
SHA512(cast(const(ubyte)*)msg.ptr, msg.length, hash.ptr);
sign_es(NID_secp521r1, hash.ptr, SHA512_DIGEST_LENGTH);
break;
}
default:
throw new SignException("Wrong algorithm.");
}
return cast(string)sign;
}
bool verifySignature(string signature, string signing_input, string key, JwtAlgorithm algo = JwtAlgorithm.HS256) {
bool verify_rs(ubyte* hash, int type, uint len, uint signLen) {
RSA* rsa_public = RSA_new();
scope(exit) RSA_free(rsa_public);
BIO* bpo = BIO_new_mem_buf(cast(char*)key.ptr, -1);
if(bpo is null)
throw new Exception("Can't load key to the BIO.");
scope(exit) BIO_free(bpo);
RSA* rsa = PEM_read_bio_RSA_PUBKEY(bpo, &rsa_public, null, null);
if(rsa is null) {
throw new Exception("Can't create RSA key.");
}
ubyte[] sign = cast(ubyte[])signature;
int ret = RSA_verify(type, hash, signLen, sign.ptr, len, rsa_public);
return ret == 1;
}
bool verify_es(uint curve_type, ubyte* hash, int hashLen ) {
EC_KEY* eckey = getESPublicKey(curve_type, key);
scope(exit) EC_KEY_free(eckey);
ubyte* c = cast(ubyte*)signature.ptr;
ECDSA_SIG* sig = null;
sig = d2i_ECDSA_SIG(&sig, cast(const (ubyte)**)&c, cast(int) key.length);
if (sig is null) {
throw new Exception("Can't decode ECDSA signature.");
}
scope(exit) ECDSA_SIG_free(sig);
int ret = ECDSA_do_verify(hash, hashLen, sig, eckey);
return ret == 1;
}
switch(algo) {
case JwtAlgorithm.NONE: {
return key.length == 0;
}
case JwtAlgorithm.HS256:
case JwtAlgorithm.HS384:
case JwtAlgorithm.HS512: {
return signature == sign(signing_input, key, algo);
}
case JwtAlgorithm.RS256: {
ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH];
SHA256(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr);
return verify_rs(hash.ptr, NID_sha256, 256, SHA256_DIGEST_LENGTH);
}
case JwtAlgorithm.RS384: {
ubyte[] hash = new ubyte[SHA384_DIGEST_LENGTH];
SHA384(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr);
return verify_rs(hash.ptr, NID_sha384, 384, SHA384_DIGEST_LENGTH);
}
case JwtAlgorithm.RS512: {
ubyte[] hash = new ubyte[SHA512_DIGEST_LENGTH];
SHA512(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr);
return verify_rs(hash.ptr, NID_sha512, 512, SHA512_DIGEST_LENGTH);
}
case JwtAlgorithm.ES256:{
ubyte[] hash = new ubyte[SHA256_DIGEST_LENGTH];
SHA256(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr);
return verify_es(NID_secp256k1, hash.ptr, SHA256_DIGEST_LENGTH );
}
case JwtAlgorithm.ES384:{
ubyte[] hash = new ubyte[SHA384_DIGEST_LENGTH];
SHA384(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr);
return verify_es(NID_secp384r1, hash.ptr, SHA384_DIGEST_LENGTH );
}
case JwtAlgorithm.ES512: {
ubyte[] hash = new ubyte[SHA512_DIGEST_LENGTH];
SHA512(cast(const(ubyte)*)signing_input.ptr, signing_input.length, hash.ptr);
return verify_es(NID_secp521r1, hash.ptr, SHA512_DIGEST_LENGTH );
}
default:
throw new VerifyException("Wrong algorithm.");
}
}

View file

@ -0,0 +1,113 @@
module hunt.jwt.JwtRegisteredClaimNames;
//
// Summary:
// List of registered claims from different sources http://tools.ietf.org/html/rfc7519#section-4
// http://openid.net/specs/openid-connect-core-1_0.html#IDToken
struct JwtRegisteredClaimNames {
enum string Actort = "actort";
//
// Summary:
// http://tools.ietf.org/html/rfc7519#section-5
enum string Typ = "typ";
//
// Summary:
// http://tools.ietf.org/html/rfc7519#section-4
enum string Sub = "sub";
//
// Summary:
// http://openid.net/specs/openid-connect-frontchannel-1_0.html#OPLogout
enum string Sid = "sid";
enum string Prn = "prn";
//
// Summary:
// http://tools.ietf.org/html/rfc7519#section-4
enum string Nbf = "nbf";
//
// Summary:
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
enum string Nonce = "nonce";
enum string NameId = "nameid";
//
// Summary:
// http://tools.ietf.org/html/rfc7519#section-4
enum string Jti = "jti";
//
// Summary:
// http://tools.ietf.org/html/rfc7519#section-4
enum string Iss = "iss";
//
// Summary:
// http://tools.ietf.org/html/rfc7519#section-4
enum string Iat = "iat";
/**
* End-User's full name in displayable form including all name parts,
* possibly including titles and suffixes, ordered according to the End-User's locale and preferences.
*/
enum string Name = "name";
enum string Nickname = "nickname";
//
// Summary:
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
enum string GivenName = "given_name";
//
// Summary:
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
enum string FamilyName = "family_name";
enum string MiddleName = "middle_name";
//
// Summary:
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
enum string Gender = "gender";
//
// Summary:
// http://tools.ietf.org/html/rfc7519#section-4
enum string Exp = "exp";
//
// Summary:
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
enum string Email = "email";
//
// Summary:
// http://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken
enum string AtHash = "at_hash";
//
// Summary:
// https://openid.net/specs/openid-connect-core-1_0.html#HybridIDToken
enum string CHash = "c_hash";
//
// Summary:
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
enum string Birthdate = "birthdate";
//
// Summary:
// http://openid.net/specs/openid-connect-core-1_0.html#IDToken
enum string Azp = "azp";
//
// Summary:
// http://openid.net/specs/openid-connect-core-1_0.html#IDToken
enum string AuthTime = "auth_time";
//
// Summary:
// http://tools.ietf.org/html/rfc7519#section-4
enum string Aud = "aud";
//
// Summary:
// http://openid.net/specs/openid-connect-core-1_0.html#IDToken
enum string Amr = "amr";
//
// Summary:
// http://openid.net/specs/openid-connect-core-1_0.html#IDToken
enum string Acr = "acr";
enum string UniqueName = "unique_name";
enum string Website = "website";
enum string PhoneNumber = "phone_number";
enum string Address = "address";
}

206
source/hunt/jwt/JwtToken.d Normal file
View file

@ -0,0 +1,206 @@
module hunt.jwt.JwtToken;
import hunt.jwt.Base64Codec;
import hunt.jwt.Claims;
import hunt.jwt.Component;
import hunt.jwt.Exceptions;
import hunt.jwt.Header;
import hunt.jwt.Jwt;
import hunt.jwt.JwtAlgorithm;
import hunt.jwt.JwtOpenSSL;
import std.conv;
import std.datetime;
import std.json;
import std.string;
/**
* represents a token
*/
class JwtToken {
private {
Claims _claims;
Header _header;
this(Claims claims, Header header) {
this._claims = claims;
this._header = header;
}
@property string data() {
return this.header.base64 ~ "." ~ this.claims.base64;
}
}
this(in JwtAlgorithm alg, in string typ = "JWT") {
this._claims = new Claims();
this._header = new Header(alg, typ);
}
@property Claims claims() {
return this._claims;
}
@property Header header() {
return this._header;
}
/**
* used to get the signature of the token
* Parmas:
* secret = the secret key used to sign the token
* Returns: the signature of the token
*/
string signature(string secret) {
return Base64URLNoPadding.encode(cast(ubyte[])sign(this.data, secret, this.header.alg));
}
/**
* encodes the token
* Params:
* secret = the secret key used to sign the token
*Returns: base64 representation of the token including signature
*/
string encode(string secret) {
if ((this.claims.exp != ulong.init && this.claims.iat != ulong.init) && this.claims.exp < this.claims.iat) {
throw new ExpiredException("Token has already expired");
}
if ((this.claims.exp != ulong.init && this.claims.nbf != ulong.init) && this.claims.exp < this.claims.nbf) {
throw new ExpiresBeforeValidException("Token will expire before it becomes valid");
}
string token = this.data ~ "." ~ this.signature(secret);
version(HUNT_AUTH_DEBUG) {
tracef("secret: %s, token: %s", secret, token);
}
return token;
}
///
unittest {
JwtToken token = new JwtToken(JwtAlgorithm.HS512);
long now = Clock.currTime.toUnixTime();
string secret = "super_secret";
token.claims.exp = now - 3600;
assertThrown!ExpiredException(token.encode(secret));
token.claims.exp = now + 3600;
token.claims.nbf = now + 7200;
assertThrown!ExpiresBeforeValidException(token.encode(secret));
}
/**
* overload of the encode(string secret) function to simplify encoding of token without algorithm none
* Returns: base64 representation of the token
*/
string encode() {
assert(this.header.alg == JwtAlgorithm.NONE);
return this.encode("");
}
static JwtToken decode(string token, string delegate(ref JSONValue jose) lazyKey) {
import std.algorithm : count;
import std.conv : to;
import std.uni : toUpper;
version(HUNT_AUTH_DEBUG) {
tracef("token: %s", token);
}
if(count(token, ".") != 2)
throw new VerifyException("Token is incorrect.");
string[] tokenParts = split(token, ".");
JSONValue header;
try {
header = parseJSON(urlsafeB64Decode(tokenParts[0]));
} catch(Exception e) {
throw new VerifyException("Header is incorrect.");
}
JwtAlgorithm alg;
try {
// toUpper for none
alg = to!(JwtAlgorithm)(toUpper(header["alg"].str()));
} catch(Exception e) {
throw new VerifyException("Algorithm is incorrect.");
}
if (auto typ = ("typ" in header)) {
string typ_str = typ.str();
if(typ_str && typ_str != "JWT")
throw new VerifyException("Type is incorrect.");
}
const key = lazyKey(header);
if(!key.empty() && !verifySignature(urlsafeB64Decode(tokenParts[2]), tokenParts[0]~"."~tokenParts[1], key, alg))
throw new VerifyException("Signature is incorrect.");
JSONValue payload;
try {
payload = parseJSON(urlsafeB64Decode(tokenParts[1]));
} catch(JSONException e) {
// Code coverage has to miss this line because the signature test above throws before this does
throw new VerifyException("Payload JSON is incorrect.");
}
Header h = new Header(header);
Claims claims = new Claims(payload);
return new JwtToken(claims, h);
}
static JwtToken decode(string encodedToken, string key="") {
return decode(encodedToken, (ref _) => key);
}
static bool verify(string token, string key) {
import std.algorithm : count;
import std.conv : to;
import std.uni : toUpper;
if(count(token, ".") != 2)
throw new VerifyException("Token is incorrect.");
string[] tokenParts = split(token, ".");
string decHeader = urlsafeB64Decode(tokenParts[0]);
JSONValue header = parseJSON(decHeader);
JwtAlgorithm alg;
try {
// toUpper for none
alg = to!(JwtAlgorithm)(toUpper(header["alg"].str()));
} catch(Exception e) {
throw new VerifyException("Algorithm is incorrect.");
}
if (auto typ = ("typ" in header)) {
string typ_str = typ.str();
if(typ_str && typ_str != "JWT")
throw new VerifyException("Type is incorrect.");
}
return verifySignature(urlsafeB64Decode(tokenParts[2]), tokenParts[0]~"."~tokenParts[1], key, alg);
}
}
alias verify = JwtToken.verify;
alias decode = JwtToken.decode;
deprecated("Using JwtToken instead.")
alias Token = JwtToken;

12
source/hunt/jwt/package.d Normal file
View file

@ -0,0 +1,12 @@
module hunt.jwt;
public import hunt.jwt.Exceptions;
public import hunt.jwt.Jwt;
public import hunt.jwt.JwtAlgorithm;
public import hunt.jwt.Base64Codec;
public import hunt.jwt.Claims;
public import hunt.jwt.Component;
public import hunt.jwt.Header;
public import hunt.jwt.JwtOpenSSL;
public import hunt.jwt.JwtRegisteredClaimNames;
public import hunt.jwt.JwtToken;