Initial implementation.

This commit is contained in:
Andrew Lalis 2025-07-29 20:29:19 -04:00
parent c0d24fa6db
commit 225210c0d3
5 changed files with 232 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
.dub
docs.json
__dummy.html
docs/
/jwt4d
jwt4d.so
jwt4d.dylib
jwt4d.dll
jwt4d.a
jwt4d.lib
jwt4d-test-*
*.exe
*.pdb
*.o
*.obj
*.lst
*.a

13
dub.json Normal file
View File

@ -0,0 +1,13 @@
{
"authors": [
"Andrew Lalis"
],
"copyright": "Copyright © 2025, Andrew Lalis",
"dependencies": {
"secured": "~>3.0.0"
},
"description": "JSON Web Token library to generate, parse, sign, and verify JWTs.",
"license": "CC0",
"name": "jwt4d",
"targetType": "library"
}

7
dub.selections.json Normal file
View File

@ -0,0 +1,7 @@
{
"fileVersion": 1,
"versions": {
"openssl": "3.3.4",
"secured": "3.0.0"
}
}

193
source/jwt4d/model.d Normal file
View File

@ -0,0 +1,193 @@
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.
*/
enum JwtClaim {
Issuer = "iss",
Subject = "sub",
Audience = "aud",
Expiration = "exp",
NotBefore = "nbf",
IssuedAt = "iat",
JwtId = "jti"
}
/**
* Data structure containing a JWT's claims.
*/
struct JwtClaims {
private JSONValue obj = JSONValue.emptyObject;
ref issuer(string issuer) {
if (issuer is null) {
this.obj.object.remove(JwtClaim.Issuer);
} else {
this.obj.object[JwtClaim.Issuer] = issuer;
}
return this;
}
string issuer() const {
if (JwtClaim.IssuedAt !in this.obj.object) return null;
return this.obj.object[JwtClaim.Issuer].str;
}
ref subject(string subject) {
if (subject is null) {
this.obj.object.remove(JwtClaim.Subject);
} else {
this.obj.object[JwtClaim.Subject] = subject;
}
return this;
}
string subject() const {
if (JwtClaim.Subject !in this.obj.object) return null;
return this.obj.object[JwtClaim.Subject].str;
}
ref audience(string audience) {
if (audience is null) {
this.obj.object.remove(JwtClaim.Audience);
} else {
this.obj.object[JwtClaim.Audience] = audience;
}
return this;
}
ref audience(string[] audiences) {
if (audiences is null || audiences.length == 0) {
this.obj.object.remove(JwtClaim.Audience);
} else {
this.obj.object[JwtClaim.Audience] = audiences;
}
return this;
}
string audience() const {
if (
JwtClaim.Audience !in this.obj.object ||
this.obj.object[JwtClaim.Audience].type != JSONType.STRING
) return null;
return this.obj.object[JwtClaim.Audience].str;
}
string[] audiences() const {
import std.algorithm : map;
import std.array : array;
if (
JwtClaim.Audience !in this.obj.object ||
this.obj.object[JwtClaim.Audience].type != JSONType.ARRAY
) return [];
return this.obj.object[JwtClaim.Audience].array.map!(v => v.str).array;
}
ref expiration(long expiration) {
if (expiration < 0) {
this.obj.object.remove(JwtClaim.Expiration);
} else {
this.obj.object[JwtClaim.Expiration] = expiration;
}
return this;
}
ref notBefore(long notBefore) {
if (notBefore < 0) {
this.obj.object.remove(JwtClaim.NotBefore);
} else {
this.obj.object[JwtClaim.NotBefore] = notBefore;
}
return this;
}
ref issuedAt(long issuedAt) {
if (issuedAt < 0) {
this.obj.object.remove(JwtClaim.IssuedAt);
} else {
this.obj.object[JwtClaim.IssuedAt] = issuedAt;
}
return this;
}
ref jwtId(string jwtId) {
if (jwtId is null) {
this.obj.object.remove(JwtClaim.JwtId);
} else {
this.obj.object[JwtClaim.JwtId] = jwtId;
}
return this;
}
string toJson() const {
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());
}

2
source/jwt4d/package.d Normal file
View File

@ -0,0 +1,2 @@
module jwt4d;