Split up reader and writer.
This commit is contained in:
parent
225210c0d3
commit
4585845c95
|
@ -1,3 +1,5 @@
|
||||||
# jwt4d
|
# JWT4D
|
||||||
|
|
||||||
JSON Web Token library to generate, parse, sign, and verify JWTs.
|
A JSON Web Token library to generate, parse, sign, and verify JWTs.
|
||||||
|
|
||||||
|
Currently, only the "HS256" algorithm is supported.
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
module jwt4d.model;
|
module jwt4d.model;
|
||||||
|
|
||||||
import std.json;
|
import std.json;
|
||||||
import std.typecons : Nullable;
|
|
||||||
import std.base64;
|
import std.base64;
|
||||||
import secured;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum defining the names of registered JWT claims, as per RFC 7519.
|
* Enum defining the names of registered JWT claims, as per RFC 7519.
|
||||||
|
@ -97,6 +95,11 @@ struct JwtClaims {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long expiration() const {
|
||||||
|
if (JwtClaim.Expiration !in this.obj.object) return -1;
|
||||||
|
return this.obj.object[JwtClaim.Expiration].integer;
|
||||||
|
}
|
||||||
|
|
||||||
ref notBefore(long notBefore) {
|
ref notBefore(long notBefore) {
|
||||||
if (notBefore < 0) {
|
if (notBefore < 0) {
|
||||||
this.obj.object.remove(JwtClaim.NotBefore);
|
this.obj.object.remove(JwtClaim.NotBefore);
|
||||||
|
@ -106,6 +109,11 @@ struct JwtClaims {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long notBefore() const {
|
||||||
|
if (JwtClaim.NotBefore !in this.obj.object) return -1;
|
||||||
|
return this.obj.object[JwtClaim.NotBefore].integer;
|
||||||
|
}
|
||||||
|
|
||||||
ref issuedAt(long issuedAt) {
|
ref issuedAt(long issuedAt) {
|
||||||
if (issuedAt < 0) {
|
if (issuedAt < 0) {
|
||||||
this.obj.object.remove(JwtClaim.IssuedAt);
|
this.obj.object.remove(JwtClaim.IssuedAt);
|
||||||
|
@ -128,66 +136,3 @@ struct JwtClaims {
|
||||||
return this.obj.toJSON();
|
return this.obj.toJSON();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string writeJwt(in JwtClaims claims, string secret) {
|
|
||||||
JSONValue headerObj = JSONValue.emptyObject;
|
|
||||||
headerObj.object["typ"] = "JWT";
|
|
||||||
headerObj.object["alg"] = "HS256";
|
|
||||||
string headerBase64 = Base64URLNoPadding.encode(cast(ubyte[]) headerObj.toJSON());
|
|
||||||
|
|
||||||
string claimsBase64 = Base64URLNoPadding.encode(cast(ubyte[]) claims.toJson());
|
|
||||||
|
|
||||||
string prefix = headerBase64 ~ "." ~ claimsBase64;
|
|
||||||
|
|
||||||
ubyte[] signatureBytes = hmac_ex(
|
|
||||||
cast(ubyte[]) secret,
|
|
||||||
cast(ubyte[]) (prefix),
|
|
||||||
HashAlgorithm.SHA2_256
|
|
||||||
);
|
|
||||||
string signatureBase64 = Base64URLNoPadding.encode(signatureBytes);
|
|
||||||
|
|
||||||
return prefix ~ "." ~ signatureBase64;
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
JwtClaims claims;
|
|
||||||
claims.issuer = "example.com";
|
|
||||||
claims.subject = "user123";
|
|
||||||
claims.expiration = 123;
|
|
||||||
|
|
||||||
string token = writeJwt(claims, "test");
|
|
||||||
import std.stdio;
|
|
||||||
writeln(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
JwtClaims readJwt(string token, string secret) {
|
|
||||||
import std.algorithm : splitter;
|
|
||||||
import std.array : array;
|
|
||||||
|
|
||||||
auto parts = token.splitter('.').array;
|
|
||||||
JSONValue headerObj = parseJSON(cast(string) Base64URLNoPadding.decode(parts[0]));
|
|
||||||
// TODO: Verify header object and algorithm.
|
|
||||||
JSONValue claimsObj = parseJSON(cast(string) Base64URLNoPadding.decode(parts[1]));
|
|
||||||
// TODO: Verify claims object structure.
|
|
||||||
ubyte[] signatureBytes = Base64URLNoPadding.decode(parts[2]);
|
|
||||||
bool verified = hmac_verify_ex(
|
|
||||||
signatureBytes,
|
|
||||||
cast(ubyte[]) secret,
|
|
||||||
cast(ubyte[]) (parts[0] ~ "." ~ parts[1]),
|
|
||||||
HashAlgorithm.SHA2_256
|
|
||||||
);
|
|
||||||
if (!verified) {
|
|
||||||
throw new Exception("Verification failed!");
|
|
||||||
// TODO: Custom error handling.
|
|
||||||
}
|
|
||||||
return JwtClaims(claimsObj);
|
|
||||||
}
|
|
||||||
|
|
||||||
unittest {
|
|
||||||
JwtClaims claims;
|
|
||||||
claims.issuer = "example.com";
|
|
||||||
string token = writeJwt(claims, "test");
|
|
||||||
JwtClaims readClaims = readJwt(token, "test");
|
|
||||||
import std.stdio;
|
|
||||||
writeln(readClaims.toJson());
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
module jwt4d;
|
module jwt4d;
|
||||||
|
|
||||||
|
public import jwt4d.model;
|
||||||
|
public import jwt4d.reader;
|
||||||
|
public import jwt4d.writer;
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
module jwt4d.reader;
|
||||||
|
|
||||||
|
import std.json;
|
||||||
|
import std.base64;
|
||||||
|
import std.datetime;
|
||||||
|
import secured;
|
||||||
|
|
||||||
|
import jwt4d.model;
|
||||||
|
|
||||||
|
class JwtException : Exception {
|
||||||
|
this(string message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JwtFormatException : JwtException {
|
||||||
|
this(string message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JwtVerificationException : JwtException {
|
||||||
|
this() {
|
||||||
|
super("JWT could not be verified.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JwtExpiredException : JwtException {
|
||||||
|
this() {
|
||||||
|
super("JWT has expired.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JwtNotBeforeException : JwtException {
|
||||||
|
this() {
|
||||||
|
super("JWT is not valid yet (not before time).");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a token and parses the claims. If the token is valid and its signature
|
||||||
|
* is verified, this method will also evaluate the "exp" (expiration) and "nbf"
|
||||||
|
* (not before) claims, and throw an exception if the token is not valid.
|
||||||
|
* Params:
|
||||||
|
* token = The token to read.
|
||||||
|
* secret = The secret used to sign the token, to verify it.
|
||||||
|
* Returns: The set of claims.
|
||||||
|
*/
|
||||||
|
JwtClaims readJwt(string token, string secret) {
|
||||||
|
import std.algorithm : splitter;
|
||||||
|
import std.array : array;
|
||||||
|
|
||||||
|
auto parts = token.splitter('.').array;
|
||||||
|
JSONValue headerObj = parseJSON(cast(string) Base64URLNoPadding.decode(parts[0]));
|
||||||
|
verifyHeaderFormat(headerObj);
|
||||||
|
string algorithm = headerObj.object["alg"].str;
|
||||||
|
if (algorithm != "HS256") {
|
||||||
|
throw new JwtFormatException("Unsupported algorithm: " ~ algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONValue claimsObj = parseJSON(cast(string) Base64URLNoPadding.decode(parts[1]));
|
||||||
|
verifyClaimsFormat(claimsObj);
|
||||||
|
|
||||||
|
ubyte[] signatureBytes = Base64URLNoPadding.decode(parts[2]);
|
||||||
|
bool verified = hmac_verify_ex(
|
||||||
|
signatureBytes,
|
||||||
|
cast(ubyte[]) secret,
|
||||||
|
cast(ubyte[]) (parts[0] ~ "." ~ parts[1]),
|
||||||
|
HashAlgorithm.SHA2_256
|
||||||
|
);
|
||||||
|
if (!verified) {
|
||||||
|
throw new JwtVerificationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
JwtClaims claims = JwtClaims(claimsObj);
|
||||||
|
verifyStandardTimeClaims(claims);
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyHeaderFormat(in JSONValue j) {
|
||||||
|
if (j.type != JSONType.OBJECT) {
|
||||||
|
throw new JwtFormatException("Header must be a JSON object.");
|
||||||
|
}
|
||||||
|
if ("typ" !in j.object || j.object["typ"].type != JSONType.STRING) {
|
||||||
|
throw new JwtFormatException("Header is missing the required 'typ' string property.");
|
||||||
|
}
|
||||||
|
if (j.object["typ"].str != "JWT") {
|
||||||
|
throw new JwtFormatException("Header 'typ' property must be 'JWT'.");
|
||||||
|
}
|
||||||
|
if ("alg" !in j.object || j.object["alg"].type != JSONType.STRING) {
|
||||||
|
throw new JwtFormatException("Header is missing the required 'alg' string property.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyClaimsFormat(in JSONValue j) {
|
||||||
|
if (j.type != JSONType.OBJECT) {
|
||||||
|
throw new JwtFormatException("Claims must be a JSON object.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyStandardTimeClaims(in JwtClaims claims) {
|
||||||
|
long currentTimestamp = Clock.currTime(UTC()).toUnixTime!long;
|
||||||
|
if (claims.expiration > 0 && claims.expiration <= currentTimestamp) {
|
||||||
|
throw new JwtExpiredException();
|
||||||
|
}
|
||||||
|
if (claims.notBefore > 0 && claims.notBefore > currentTimestamp) {
|
||||||
|
throw new JwtNotBeforeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
import jwt4d.writer;
|
||||||
|
JwtClaims claims;
|
||||||
|
claims.issuer = "example.com";
|
||||||
|
string token = writeJwt(claims, "test");
|
||||||
|
JwtClaims readClaims = readJwt(token, "test");
|
||||||
|
import std.stdio;
|
||||||
|
writeln(readClaims.toJson());
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
module jwt4d.writer;
|
||||||
|
|
||||||
|
import std.json;
|
||||||
|
import std.base64;
|
||||||
|
import secured;
|
||||||
|
|
||||||
|
import jwt4d.model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the given claims to a JWT, signed with the given secret.
|
||||||
|
* Params:
|
||||||
|
* claims = The claims to encode.
|
||||||
|
* secret = The secret to use when signing the JWT.
|
||||||
|
* Returns: The token.
|
||||||
|
*/
|
||||||
|
string writeJwt(in JwtClaims claims, string secret) {
|
||||||
|
JSONValue headerObj = JSONValue.emptyObject;
|
||||||
|
headerObj.object["typ"] = "JWT";
|
||||||
|
headerObj.object["alg"] = "HS256";
|
||||||
|
string headerBase64 = Base64URLNoPadding.encode(cast(ubyte[]) headerObj.toJSON());
|
||||||
|
|
||||||
|
string claimsBase64 = Base64URLNoPadding.encode(cast(ubyte[]) claims.toJson());
|
||||||
|
|
||||||
|
string prefix = headerBase64 ~ "." ~ claimsBase64;
|
||||||
|
|
||||||
|
ubyte[] signatureBytes = hmac_ex(
|
||||||
|
cast(ubyte[]) secret,
|
||||||
|
cast(ubyte[]) (prefix),
|
||||||
|
HashAlgorithm.SHA2_256
|
||||||
|
);
|
||||||
|
string signatureBase64 = Base64URLNoPadding.encode(signatureBytes);
|
||||||
|
|
||||||
|
return prefix ~ "." ~ signatureBase64;
|
||||||
|
}
|
||||||
|
|
||||||
|
unittest {
|
||||||
|
JwtClaims claims;
|
||||||
|
claims.issuer = "example.com";
|
||||||
|
claims.subject = "user123";
|
||||||
|
claims.expiration = 123;
|
||||||
|
|
||||||
|
string token = writeJwt(claims, "test");
|
||||||
|
import std.stdio;
|
||||||
|
writeln(token);
|
||||||
|
}
|
Loading…
Reference in New Issue