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

@ -9,6 +9,13 @@ A Simple D implementation of JSON Web Tokens. It's forked from https://github.co
- HS256
- HS384
- HS512
- RS256
- RS384
- RS512
- ES256
- ES384
- ES512
#### This library uses [semantic versioning 2.0.0][3]
@ -29,7 +36,7 @@ A Simple D implementation of JSON Web Tokens. It's forked from https://github.co
JSONValue user = ["id": JSONValue(60119), "uri": JSONValue("https://api.we.are/60119")];
Token token = new Token(JWTAlgorithm.HS512);
JwtToken token = new JwtToken(JwtAlgorithm.HS512);
token.claims.exp = Clock.currTime.toUnixTime();
@ -55,7 +62,7 @@ A Simple D implementation of JSON Web Tokens. It's forked from https://github.co
try {
Token token = verify(encodedToken, "supersecret", [JWTAlgorithm.HS512, JWTAlgorithm.HS256]);
JwtToken token = JwtToken.verify(encodedToken, "supersecret");
writeln(token.claims.getInt("id"));
@ -103,7 +110,7 @@ A Simple D implementation of JSON Web Tokens. It's forked from https://github.co
void main() {
Token token = new Token(JWTAlgorithm.NONE);
JwtToken token = new JwtToken(JwtAlgorithm.NONE);
token.claims.exp = Clock.currTime.toUnixTime();
@ -127,7 +134,7 @@ A Simple D implementation of JSON Web Tokens. It's forked from https://github.co
try {
Token token = verify(encodedToken);
JwtToken token = JwtToken.verify(encodedToken);
writeln(token.claims.getInt("id"));

View file

@ -1,9 +1,17 @@
{
"name": "hunt-jwt",
"authors": [
"Zelalem Mekonen"
"Oleh Havrys",
"Sergey Buth",
"Lionello Lunesu",
"Tomáš Chaloupka",
"Zelalem Mekonen",
"HuntLabs"
],
"description": "A Simple D implementation of JSON Web Tokens",
"copyright": "Copyright © 2016, Zelalem Mekonen",
"license": "MIT"
"description": "A D implementation of JSON Web Tokens",
"copyright": "Copyright © 2020, HuntLabs",
"license": "MIT",
"dependencies": {
"hunt-openssl": "~>1.0.1"
}
}

View file

@ -1,17 +1,122 @@
import std.stdio;
import jwt;
import hunt.jwt;
import hunt.logging.ConsoleLogger;
import std.datetime;
import std.exception;
import std.conv;
import std.json;
import std.format;
void main()
{
// testToken();
// testHS512();
testES256();
}
void testToken() {
string tokenString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYW1lIjoiYWxpY2UiLCJlbWFpbCI6ImFsaWNlQGdtYWlsLmNvbSIsInBob25lX251bWJlciI6IjE4ODAwMDAwMDAxIiwibmJmIjoxNTA5NDY0MzQwLCJleHAiOjE1MTAwNjkxNDAsImlhdCI6MTUwOTQ2NDM0MH0.nV7duR2gWHA3TB9xPhP1WWhDpXRn1GA_k8_zBBirT6g";
Token tk = decode(tokenString);
JwtToken tk = JwtToken.decode(tokenString);
writeln(tk.header.json());
writeln(tk.claims.json());
tk = verify(tokenString, "secret", []);
assert(JwtToken.verify(tokenString, "secret"));
}
// HS512
void testHS512() {
scope(failure) {
warning("failed");
}
scope(success) {
info("passed");
}
string hs_secret = "secret";
enum FinalToken = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MDYwMzI4MjQsImxhbmd1YWdlIjoiRCJ9.nIWh2aWLdjA64NWm5P1RO5vG66DKGg8nXAfJ7js3qEV1CoX-BNvXFKvhPJvNby7_ZQTrqHLpCNBWEdtrshxYFQ";
long iat = 1606032824;
JSONValue payload = parseJSON(`{"iat":1606032824,"language":"D"}`);
string hs512Token = encode(payload, hs_secret, JwtAlgorithm.HS512);
assert(hs512Token == FinalToken);
assert(JwtToken.verify(hs512Token, hs_secret));
warning(hs512Token);
JwtToken token = new JwtToken(JwtAlgorithm.HS512);
token.claims.set("language", "D");
token.claims.iat = iat;
string hs512Token2 = token.encode(hs_secret);
warning(hs512Token2);
assert(hs512Token == hs512Token2);
//
token = JwtToken.decode(FinalToken, hs_secret);
string language = token.claims.get("language");
assert(language == "D");
}
// ES256
void testES256() {
scope(failure) {
warning("failed");
}
scope(success) {
info("passed");
}
string es256_public = q"EOS
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEMuSnsWbiIPyfFAIAvlbliPOUnQlibb67
yE6JUqXVaevb8ZorK2HfxfFg9pGVhg3SGuBCbHcJ84WKOX3GSMEwcA==
-----END PUBLIC KEY-----
EOS";
string es256_private = q"EOS
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIB8cQPtLEF5hOJsom5oVU5dMpgDUR2QYuJTXdtvxezQloAcGBSuBBAAK
oUQDQgAEMuSnsWbiIPyfFAIAvlbliPOUnQlibb67yE6JUqXVaevb8ZorK2HfxfFg
9pGVhg3SGuBCbHcJ84WKOX3GSMEwcA==
-----END EC PRIVATE KEY-----
EOS";
long iat = 1606032824;
JSONValue payload = parseJSON(format(`{"iat":%d,"language":"D"}`, iat));
string es256Token = encode(payload, es256_private, JwtAlgorithm.ES256);
warning(es256Token);
// assert(es256Token == Es256FinalToken);
assert(JwtToken.verify(es256Token, es256_public));
string es256Token1 = encode(payload, es256_private, JwtAlgorithm.ES256);
trace(es256Token1);
assert(es256Token != es256Token1);
JwtToken token = new JwtToken(JwtAlgorithm.ES256);
token.claims.set("language", "D");
token.claims.iat = iat;
string es256Token2 = token.encode(es256_private);
warning(es256Token2);
assert(JwtToken.verify(es256Token2, es256_public));
//
token = JwtToken.decode(es256Token2, es256_public);
string language = token.claims.get("language");
assert(language == "D");
}

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

@ -1,4 +1,4 @@
module jwt.JwtRegisteredClaimNames;
module hunt.jwt.JwtRegisteredClaimNames;
//
// Summary:

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;

View file

@ -1,72 +0,0 @@
module jwt.algorithms;
import std.digest.hmac;
import std.digest.sha;
import std.string : representation;
import std.base64;
import std.stdio;
import jwt.exceptions;
/**
* string literal used to represent signing algorithm type
*/
enum JWTAlgorithm : string {
NONE = "none", // string representation of the none algorithm
HS256 = "HS256", // string representation of hmac algorithm with sha256
HS384 = "HS384", // string representation of hmac algorithm with sha348
HS512 = "HS512" //string representation of hmac algorithm with sha512
}
/**
* an alias for base64 encoding that is url safe and removes the '=' padding character
*/
alias URLSafeBase64 = Base64Impl!('-', '_', Base64.NoPadding);
/**
* signs the given data with the secret using the given algorithm
* Params:
* secret = the secret used to sign the data
* data = the data that is to be signed
* alg = the algorithm to be used to sign the data
* Returns: signature of the data
*/
string sign(string secret, string data, JWTAlgorithm alg) {
switch(alg) {
case JWTAlgorithm.HS256:
auto signature = HMAC!SHA256(secret.representation);
signature.put(data.representation);
return URLSafeBase64.encode(signature.finish());
case JWTAlgorithm.HS384:
auto signature = HMAC!SHA384(secret.representation);
signature.put(data.representation);
return URLSafeBase64.encode(signature.finish());
case JWTAlgorithm.HS512:
auto signature = HMAC!SHA512(secret.representation);
signature.put(data.representation);
return URLSafeBase64.encode(signature.finish());
case JWTAlgorithm.NONE:
return "";
default:
throw new UnsupportedAlgorithmException(alg ~ " algorithm is not supported!");
}
}
///
unittest {
string secret = "supersecret";
string data = "an unstoppable force crashes into an unmovable body";
string signature = sign(secret, data, JWTAlgorithm.HS512);
assert(signature.length > 0);
signature = sign(secret, data, JWTAlgorithm.NONE);
assert(signature.length == 0);
}

View file

@ -1,84 +0,0 @@
module jwt.exceptions;
/**
* thrown when there are issues with token verification
*/
class VerifyException : Exception {
this(string s) {
super(s);
}
}
/**
* thrown when attempting to encode or decode a token with an unsupported algorithm
*/
class UnsupportedAlgorithmException : Exception {
this(string s) {
super(s);
}
}
/**
* thrown when there are issues with the token
*/
class InvalidTokenException : VerifyException {
this(string s) {
super(s);
}
}
/**
* thrown when the tokens signature doesn't match the data signature
*/
class InvalidSignatureException : VerifyException {
this(string s) {
super(s);
}
}
/**
* thrown when the algorithm used to sign the token is invalid
*/
class InvalidAlgorithmException : VerifyException {
this(string s) {
super(s);
}
}
/**
* thrown when the tokens is expired
*/
class ExpiredException : VerifyException {
this(string s) {
super(s);
}
}
/**
* thrown when the token is not valid yet
* or in other words when the nbf claim time is before the current time
*/
class NotBeforeException : VerifyException {
this(string s) {
super(s);
}
}
/**
* thrown when the token has an incorrect format
*/
class MalformedToken : InvalidTokenException {
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);
}
}

View file

@ -1,571 +0,0 @@
module jwt.jwt;
import std.json;
import std.base64;
import std.stdio;
import std.conv;
import std.string;
import std.datetime;
import std.exception;
import std.array : split;
import std.algorithm : count;
import jwt.algorithms;
import jwt.exceptions;
private class Component {
abstract @property string json();
@property string base64() {
ubyte[] data = cast(ubyte[])this.json;
return URLSafeBase64.encode(data);
}
}
private class Header : Component {
public:
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 UnsupportedAlgorithmException(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();
}
}
/**
* represents the claims component of a JWT
*/
private class Claims : Component {
private:
JSONValue data;
this(in JSONValue claims) {
this.data = claims;
}
public:
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();
}
}
/**
* represents a token
*/
class Token {
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;
}
public:
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 sign(secret, this.data, 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");
}
return this.data ~ "." ~ this.signature(secret);
}
///
unittest {
Token token = new Token(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("");
}
}
Token decode(string encodedToken) {
string[] tokenParts = split(encodedToken, ".");
if(tokenParts.length != 3) {
throw new MalformedToken("Malformed Token");
}
string component = tokenParts[0];
string jsonComponent = cast(string)URLSafeBase64.decode(component);
JSONValue parsedComponent = parseJSON(jsonComponent);
Header header = new Header(parsedComponent);
component = tokenParts[1];
jsonComponent = cast(string)URLSafeBase64.decode(component);
parsedComponent = parseJSON(jsonComponent);
Claims claims = new Claims(parsedComponent);
return new Token(claims, header);
}
/**
* verifies the tokens is valid, using the algorithm given instead of the alg field in the claims
* Params:
* encodedToken = the encoded token
* secret = the secret key used to sign the token
* alg = the algorithm to be used to verify the token
* Returns: a decoded Token
*/
Token verify(string encodedToken, string secret, JWTAlgorithm[] algs) {
Token token = decode(encodedToken);
long now = Clock.currTime.toUnixTime();
bool algorithmAllowed = false;
foreach(index, alg; algs) {
if(token.header.alg == alg) {
algorithmAllowed = true;
}
}
if (!algorithmAllowed) {
throw new InvalidAlgorithmException("Algorithm " ~ token.header.alg ~ " is not in the allowed algorithms field");
}
string signature = split(encodedToken, ".")[2];
if (signature != token.signature(secret)) {
throw new InvalidSignatureException("Signature Match Failed");
}
if (token.header.alg == JWTAlgorithm.NONE) {
throw new VerifyException("Algorithm set to none while secret is provided");
}
if (token.claims.exp != ulong.init && token.claims.exp < now) {
throw new ExpiredException("Token has expired");
}
if (token.claims.nbf != ulong.init && token.claims.nbf > now) {
throw new NotBeforeException("Token is not valid yet");
}
return token;
}
unittest {
string secret = "super_secret";
long now = Clock.currTime.toUnixTime();
Token token = new Token(JWTAlgorithm.HS512);
token.claims.nbf = now + (60 * 60);
string encodedToken = token.encode(secret);
assertThrown!NotBeforeException(verify(encodedToken, secret, [JWTAlgorithm.HS512]));
token = new Token(JWTAlgorithm.HS512);
token.claims.iat = now - 3600;
token.claims.exp = now - 60;
encodedToken = token.encode(secret);
assertThrown!ExpiredException(verify(encodedToken, secret, [JWTAlgorithm.HS512]));
token = new Token(JWTAlgorithm.NONE);
encodedToken = token.encode(secret);
assertThrown!VerifyException(verify(encodedToken, secret, [JWTAlgorithm.HS512]));
token = new Token(JWTAlgorithm.HS512);
encodedToken = token.encode(secret) ~ "we_are";
assertThrown!InvalidSignatureException(verify(encodedToken, secret, [JWTAlgorithm.HS512]));
token = new Token(JWTAlgorithm.HS512);
encodedToken = token.encode(secret);
assertThrown!InvalidAlgorithmException(verify(encodedToken, secret, [JWTAlgorithm.HS256, JWTAlgorithm.HS384]));
}
/**
* verifies the tokens is valid, used in case the token was signed with "none" as algorithm
* Params:
* encodedToken = the encoded token
* Returns: a decoded Token
*/
Token verify(string encodedToken) {
Token token = decode(encodedToken);
long now = Clock.currTime.toUnixTime();
if (token.claims.exp != ulong.init && token.claims.exp < now) {
throw new ExpiredException("Token has expired");
}
if (token.claims.nbf != ulong.init && token.claims.nbf > now) {
throw new NotBeforeException("Token is not valid yet");
}
return token;
}
///
unittest {
long now = Clock.currTime.toUnixTime();
Token token = new Token(JWTAlgorithm.NONE);
token.claims.nbf = now + (60 * 60);
string encodedToken = token.encode();
assertThrown!NotBeforeException(verify(encodedToken));
token = new Token(JWTAlgorithm.NONE);
token.claims.iat = now - 3600;
token.claims.exp = now - 60;
encodedToken = token.encode();
assertThrown!ExpiredException(token = verify(encodedToken));
}

View file

@ -1,6 +0,0 @@
module jwt;
public import jwt.JwtRegisteredClaimNames;
public import jwt.algorithms;
public import jwt.exceptions;
public import jwt.jwt;