More improvements
This commit is contained in:
parent
3b0c275fbc
commit
0b75b521b3
15
README.md
15
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"));
|
||||
|
||||
|
|
16
dub.json
16
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"
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
20
source/hunt/jwt/Base64Codec.d
Normal file
20
source/hunt/jwt/Base64Codec.d
Normal 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
267
source/hunt/jwt/Claims.d
Normal 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();
|
||||
|
||||
}
|
||||
}
|
16
source/hunt/jwt/Component.d
Normal file
16
source/hunt/jwt/Component.d
Normal 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);
|
||||
}
|
||||
}
|
||||
|
31
source/hunt/jwt/Exceptions.d
Normal file
31
source/hunt/jwt/Exceptions.d
Normal 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
43
source/hunt/jwt/Header.d
Normal 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
114
source/hunt/jwt/Jwt.d
Normal 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;
|
||||
}
|
18
source/hunt/jwt/JwtAlgorithm.d
Normal file
18
source/hunt/jwt/JwtAlgorithm.d
Normal 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;
|
334
source/hunt/jwt/JwtOpenSSL.d
Normal file
334
source/hunt/jwt/JwtOpenSSL.d
Normal 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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
module jwt.JwtRegisteredClaimNames;
|
||||
module hunt.jwt.JwtRegisteredClaimNames;
|
||||
|
||||
//
|
||||
// Summary:
|
206
source/hunt/jwt/JwtToken.d
Normal file
206
source/hunt/jwt/JwtToken.d
Normal 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
12
source/hunt/jwt/package.d
Normal 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;
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
571
source/jwt/jwt.d
571
source/jwt/jwt.d
|
@ -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));
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
module jwt;
|
||||
|
||||
public import jwt.JwtRegisteredClaimNames;
|
||||
public import jwt.algorithms;
|
||||
public import jwt.exceptions;
|
||||
public import jwt.jwt;
|
Loading…
Reference in a new issue