Split up reader and writer.

This commit is contained in:
Andrew Lalis 2025-07-30 10:23:13 -04:00
parent 225210c0d3
commit 4585845c95
5 changed files with 181 additions and 67 deletions

View File

@ -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.

View File

@ -1,9 +1,7 @@
module jwt4d.model;
import std.json;
import std.typecons : Nullable;
import std.base64;
import secured;
/**
* Enum defining the names of registered JWT claims, as per RFC 7519.
@ -97,6 +95,11 @@ struct JwtClaims {
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) {
if (notBefore < 0) {
this.obj.object.remove(JwtClaim.NotBefore);
@ -106,6 +109,11 @@ struct JwtClaims {
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) {
if (issuedAt < 0) {
this.obj.object.remove(JwtClaim.IssuedAt);
@ -128,66 +136,3 @@ struct JwtClaims {
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());
}

View File

@ -1,2 +1,5 @@
module jwt4d;
public import jwt4d.model;
public import jwt4d.reader;
public import jwt4d.writer;

119
source/jwt4d/reader.d Normal file
View File

@ -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());
}

45
source/jwt4d/writer.d Normal file
View File

@ -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);
}