From 0b75b521b378f0cff042611d7dd99b7ccd14f205 Mon Sep 17 00:00:00 2001 From: heromyth Date: Mon, 23 Nov 2020 18:00:11 +0800 Subject: [PATCH] More improvements --- README.md | 15 +- dub.json | 16 +- examples/simple/source/app.d | 111 +++- source/hunt/jwt/Base64Codec.d | 20 + source/hunt/jwt/Claims.d | 267 ++++++++ source/hunt/jwt/Component.d | 16 + source/hunt/jwt/Exceptions.d | 31 + source/hunt/jwt/Header.d | 43 ++ source/hunt/jwt/Jwt.d | 114 ++++ source/hunt/jwt/JwtAlgorithm.d | 18 + source/hunt/jwt/JwtOpenSSL.d | 334 ++++++++++ .../{ => hunt}/jwt/JwtRegisteredClaimNames.d | 2 +- source/hunt/jwt/JwtToken.d | 206 +++++++ source/hunt/jwt/package.d | 12 + source/jwt/algorithms.d | 72 --- source/jwt/exceptions.d | 84 --- source/jwt/jwt.d | 571 ------------------ source/jwt/package.d | 6 - 18 files changed, 1193 insertions(+), 745 deletions(-) create mode 100644 source/hunt/jwt/Base64Codec.d create mode 100644 source/hunt/jwt/Claims.d create mode 100644 source/hunt/jwt/Component.d create mode 100644 source/hunt/jwt/Exceptions.d create mode 100644 source/hunt/jwt/Header.d create mode 100644 source/hunt/jwt/Jwt.d create mode 100644 source/hunt/jwt/JwtAlgorithm.d create mode 100644 source/hunt/jwt/JwtOpenSSL.d rename source/{ => hunt}/jwt/JwtRegisteredClaimNames.d (98%) create mode 100644 source/hunt/jwt/JwtToken.d create mode 100644 source/hunt/jwt/package.d delete mode 100644 source/jwt/algorithms.d delete mode 100644 source/jwt/exceptions.d delete mode 100644 source/jwt/jwt.d delete mode 100644 source/jwt/package.d diff --git a/README.md b/README.md index 110bce5..9e06476 100644 --- a/README.md +++ b/README.md @@ -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")); diff --git a/dub.json b/dub.json index 85d2972..ca67ed5 100644 --- a/dub.json +++ b/dub.json @@ -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" + } } \ No newline at end of file diff --git a/examples/simple/source/app.d b/examples/simple/source/app.d index 46f935f..ce8f93f 100644 --- a/examples/simple/source/app.d +++ b/examples/simple/source/app.d @@ -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"); +} \ No newline at end of file diff --git a/source/hunt/jwt/Base64Codec.d b/source/hunt/jwt/Base64Codec.d new file mode 100644 index 0000000..769e02f --- /dev/null +++ b/source/hunt/jwt/Base64Codec.d @@ -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); +} diff --git a/source/hunt/jwt/Claims.d b/source/hunt/jwt/Claims.d new file mode 100644 index 0000000..ba37dbf --- /dev/null +++ b/source/hunt/jwt/Claims.d @@ -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(); + + } +} diff --git a/source/hunt/jwt/Component.d b/source/hunt/jwt/Component.d new file mode 100644 index 0000000..fb00ded --- /dev/null +++ b/source/hunt/jwt/Component.d @@ -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); + } +} + diff --git a/source/hunt/jwt/Exceptions.d b/source/hunt/jwt/Exceptions.d new file mode 100644 index 0000000..b78d90e --- /dev/null +++ b/source/hunt/jwt/Exceptions.d @@ -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); + } +} \ No newline at end of file diff --git a/source/hunt/jwt/Header.d b/source/hunt/jwt/Header.d new file mode 100644 index 0000000..d728a73 --- /dev/null +++ b/source/hunt/jwt/Header.d @@ -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(); + + } +} diff --git a/source/hunt/jwt/Jwt.d b/source/hunt/jwt/Jwt.d new file mode 100644 index 0000000..483fe36 --- /dev/null +++ b/source/hunt/jwt/Jwt.d @@ -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; +} diff --git a/source/hunt/jwt/JwtAlgorithm.d b/source/hunt/jwt/JwtAlgorithm.d new file mode 100644 index 0000000..c2d0d6a --- /dev/null +++ b/source/hunt/jwt/JwtAlgorithm.d @@ -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; \ No newline at end of file diff --git a/source/hunt/jwt/JwtOpenSSL.d b/source/hunt/jwt/JwtOpenSSL.d new file mode 100644 index 0000000..9ff4e89 --- /dev/null +++ b/source/hunt/jwt/JwtOpenSSL.d @@ -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."); + } +} + diff --git a/source/jwt/JwtRegisteredClaimNames.d b/source/hunt/jwt/JwtRegisteredClaimNames.d similarity index 98% rename from source/jwt/JwtRegisteredClaimNames.d rename to source/hunt/jwt/JwtRegisteredClaimNames.d index e841a52..222e57d 100644 --- a/source/jwt/JwtRegisteredClaimNames.d +++ b/source/hunt/jwt/JwtRegisteredClaimNames.d @@ -1,4 +1,4 @@ -module jwt.JwtRegisteredClaimNames; +module hunt.jwt.JwtRegisteredClaimNames; // // Summary: diff --git a/source/hunt/jwt/JwtToken.d b/source/hunt/jwt/JwtToken.d new file mode 100644 index 0000000..25247aa --- /dev/null +++ b/source/hunt/jwt/JwtToken.d @@ -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; \ No newline at end of file diff --git a/source/hunt/jwt/package.d b/source/hunt/jwt/package.d new file mode 100644 index 0000000..4feb04c --- /dev/null +++ b/source/hunt/jwt/package.d @@ -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; \ No newline at end of file diff --git a/source/jwt/algorithms.d b/source/jwt/algorithms.d deleted file mode 100644 index f603c0c..0000000 --- a/source/jwt/algorithms.d +++ /dev/null @@ -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); -} \ No newline at end of file diff --git a/source/jwt/exceptions.d b/source/jwt/exceptions.d deleted file mode 100644 index 8e4a1d4..0000000 --- a/source/jwt/exceptions.d +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/source/jwt/jwt.d b/source/jwt/jwt.d deleted file mode 100644 index d25de6e..0000000 --- a/source/jwt/jwt.d +++ /dev/null @@ -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)); -} \ No newline at end of file diff --git a/source/jwt/package.d b/source/jwt/package.d deleted file mode 100644 index bebb2e1..0000000 --- a/source/jwt/package.d +++ /dev/null @@ -1,6 +0,0 @@ -module jwt; - -public import jwt.JwtRegisteredClaimNames; -public import jwt.algorithms; -public import jwt.exceptions; -public import jwt.jwt; \ No newline at end of file