Initial implementation.
This commit is contained in:
parent
c0d24fa6db
commit
225210c0d3
|
@ -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
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"fileVersion": 1,
|
||||||
|
"versions": {
|
||||||
|
"openssl": "3.3.4",
|
||||||
|
"secured": "3.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
module jwt4d;
|
||||||
|
|
Loading…
Reference in New Issue