Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
|
9cf62e4d36 |
32
README.md
32
README.md
|
@ -3,3 +3,35 @@
|
||||||
A 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.
|
Currently, only the "HS256" algorithm is supported.
|
||||||
|
|
||||||
|
## Example: Create a JWT
|
||||||
|
```d
|
||||||
|
import jwt4d;
|
||||||
|
import std.datetime; // To set expiration in duration.
|
||||||
|
import std.json; // To add a custom claim value.
|
||||||
|
import std.stdio;
|
||||||
|
|
||||||
|
const string MY_SECRET = "this is a secret!";
|
||||||
|
|
||||||
|
JwtClaims claims = JwtClaims()
|
||||||
|
.issuer("my.webpage.com")
|
||||||
|
.subject("user123")
|
||||||
|
.issuedAtNow()
|
||||||
|
.expiresIn(minutes(30))
|
||||||
|
.customClaim("role", JSONValue("admin"));
|
||||||
|
string token = writeJwt(claims, MY_SECRET);
|
||||||
|
writeln(token);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Read a JWT
|
||||||
|
```d
|
||||||
|
import jwt4d;
|
||||||
|
import std.stdio;
|
||||||
|
|
||||||
|
const string MY_SECRET = "this is a secret!";
|
||||||
|
|
||||||
|
string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTM5MjYzMjAsImlhdCI6MTc1MzkyNDUyMCwiaXNzIjoibXkud2VicGFnZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJzdWIiOiJ1c2VyMTIzIn0.n5X2giJ3S5T3wrW4C0qlZrShr2ZwPiWIu6FxUzQ3K9s";
|
||||||
|
|
||||||
|
JwtClaims claims = readJwt(token, MY_SECRET);
|
||||||
|
writeln(claims.toJson());
|
||||||
|
```
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module jwt4d.model;
|
module jwt4d.model;
|
||||||
|
|
||||||
import std.json;
|
import std.json;
|
||||||
import std.base64;
|
import std.datetime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enum defining the names of registered JWT claims, as per RFC 7519.
|
* Enum defining the names of registered JWT claims, as per RFC 7519.
|
||||||
|
@ -95,6 +95,12 @@ struct JwtClaims {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref expiresIn(Duration dur) {
|
||||||
|
long expirationUnixTime = (Clock.currTime() + dur).toUnixTime!long;
|
||||||
|
this.obj.object[JwtClaim.Expiration] = expirationUnixTime;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
long expiration() const {
|
long expiration() const {
|
||||||
if (JwtClaim.Expiration !in this.obj.object) return -1;
|
if (JwtClaim.Expiration !in this.obj.object) return -1;
|
||||||
return this.obj.object[JwtClaim.Expiration].integer;
|
return this.obj.object[JwtClaim.Expiration].integer;
|
||||||
|
@ -123,6 +129,17 @@ struct JwtClaims {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref issuedAtNow() {
|
||||||
|
long now = Clock.currTime().toUnixTime!long();
|
||||||
|
this.obj.object[JwtClaim.IssuedAt] = now;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
long issuedAt() const {
|
||||||
|
if (JwtClaim.IssuedAt !in this.obj.object) return -1;
|
||||||
|
return this.obj.object[JwtClaim.IssuedAt].integer;
|
||||||
|
}
|
||||||
|
|
||||||
ref jwtId(string jwtId) {
|
ref jwtId(string jwtId) {
|
||||||
if (jwtId is null) {
|
if (jwtId is null) {
|
||||||
this.obj.object.remove(JwtClaim.JwtId);
|
this.obj.object.remove(JwtClaim.JwtId);
|
||||||
|
@ -132,6 +149,21 @@ struct JwtClaims {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string jwtId() const {
|
||||||
|
if (JwtClaim.JwtId !in this.obj.object) return null;
|
||||||
|
return this.obj.object[JwtClaim.JwtId].str;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref customClaim(string name, JSONValue value) {
|
||||||
|
this.obj.object[name] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONValue customClaim(string name) const {
|
||||||
|
if (name !in this.obj.object) return JSONValue(null);
|
||||||
|
return this.obj.object[name];
|
||||||
|
}
|
||||||
|
|
||||||
string toJson() const {
|
string toJson() const {
|
||||||
return this.obj.toJSON();
|
return this.obj.toJSON();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Defines the `readJwt` function for reading a JWT from a string token, and
|
||||||
|
* associated exceptions which may be thrown for invalid tokens.
|
||||||
|
*/
|
||||||
module jwt4d.reader;
|
module jwt4d.reader;
|
||||||
|
|
||||||
import std.json;
|
import std.json;
|
||||||
|
@ -7,36 +11,50 @@ import secured;
|
||||||
|
|
||||||
import jwt4d.model;
|
import jwt4d.model;
|
||||||
|
|
||||||
|
/// Base type for any exception thrown while attempting to read a JWT.
|
||||||
class JwtException : Exception {
|
class JwtException : Exception {
|
||||||
this(string message) {
|
this(string message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Thrown if a token is malformed and cannot be read.
|
||||||
class JwtFormatException : JwtException {
|
class JwtFormatException : JwtException {
|
||||||
this(string message) {
|
this(string message) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Thrown if a token cannot be verified.
|
||||||
class JwtVerificationException : JwtException {
|
class JwtVerificationException : JwtException {
|
||||||
this() {
|
this() {
|
||||||
super("JWT could not be verified.");
|
super("JWT could not be verified.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Thrown if a token's "exp" claim is present and not in the future.
|
||||||
class JwtExpiredException : JwtException {
|
class JwtExpiredException : JwtException {
|
||||||
this() {
|
this() {
|
||||||
super("JWT has expired.");
|
super("JWT has expired.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Thrown if a token's "nbf" claim is present and not in the past.
|
||||||
class JwtNotBeforeException : JwtException {
|
class JwtNotBeforeException : JwtException {
|
||||||
this() {
|
this() {
|
||||||
super("JWT is not valid yet (not before time).");
|
super("JWT is not valid yet (not before time).");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Internal struct for passing around JWT data after parsing it.
|
||||||
|
private struct JwtComponents {
|
||||||
|
JSONValue headerObj;
|
||||||
|
JSONValue claimsObj;
|
||||||
|
string payloadForSigning;
|
||||||
|
string algorithm;
|
||||||
|
ubyte[] signature;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a token and parses the claims. If the token is valid and its signature
|
* 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"
|
* is verified, this method will also evaluate the "exp" (expiration) and "nbf"
|
||||||
|
@ -47,36 +65,54 @@ class JwtNotBeforeException : JwtException {
|
||||||
* Returns: The set of claims.
|
* Returns: The set of claims.
|
||||||
*/
|
*/
|
||||||
JwtClaims readJwt(string token, string secret) {
|
JwtClaims readJwt(string token, string secret) {
|
||||||
import std.algorithm : splitter;
|
JwtComponents components = extractComponents(token);
|
||||||
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(
|
bool verified = hmac_verify_ex(
|
||||||
signatureBytes,
|
components.signature,
|
||||||
cast(ubyte[]) secret,
|
cast(ubyte[]) secret,
|
||||||
cast(ubyte[]) (parts[0] ~ "." ~ parts[1]),
|
cast(ubyte[]) components.payloadForSigning,
|
||||||
HashAlgorithm.SHA2_256
|
HashAlgorithm.SHA2_256
|
||||||
);
|
);
|
||||||
if (!verified) {
|
if (!verified) {
|
||||||
throw new JwtVerificationException();
|
throw new JwtVerificationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
JwtClaims claims = JwtClaims(claimsObj);
|
JwtClaims claims = JwtClaims(components.claimsObj);
|
||||||
verifyStandardTimeClaims(claims);
|
verifyStandardTimeClaims(claims);
|
||||||
return claims;
|
return claims;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private JwtComponents extractComponents(string token) {
|
||||||
|
import std.algorithm : splitter;
|
||||||
|
import std.array : array;
|
||||||
|
|
||||||
|
auto parts = token.splitter('.').array;
|
||||||
|
if (parts.length != 3 || parts[0].length == 0 || parts[1].length == 0 || parts[2].length == 0) {
|
||||||
|
throw new JwtFormatException("Invalid token format. Couldn't parse header, payload, and signature parts.");
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONValue headerObj;
|
||||||
|
JSONValue claimsObj;
|
||||||
|
ubyte[] signature;
|
||||||
|
try {
|
||||||
|
headerObj = parseJSON(cast(string) Base64URLNoPadding.decode(parts[0]));
|
||||||
|
claimsObj = parseJSON(cast(string) Base64URLNoPadding.decode(parts[1]));
|
||||||
|
signature = Base64URLNoPadding.decode(parts[2]);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new JwtFormatException("Invalid JSON format.");
|
||||||
|
} catch (Base64Exception e) {
|
||||||
|
throw new JwtFormatException("Invalid Base64 encoding.");
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyHeaderFormat(headerObj);
|
||||||
|
string algorithm = headerObj.object["alg"].str;
|
||||||
|
if (algorithm != "HS256") {
|
||||||
|
throw new JwtFormatException("Unsupported algorithm: " ~ algorithm);
|
||||||
|
}
|
||||||
|
verifyClaimsFormat(claimsObj);
|
||||||
|
|
||||||
|
return JwtComponents(headerObj, claimsObj, parts[0] ~ "." ~ parts[1], algorithm, signature);
|
||||||
|
}
|
||||||
|
|
||||||
private void verifyHeaderFormat(in JSONValue j) {
|
private void verifyHeaderFormat(in JSONValue j) {
|
||||||
if (j.type != JSONType.OBJECT) {
|
if (j.type != JSONType.OBJECT) {
|
||||||
throw new JwtFormatException("Header must be a JSON object.");
|
throw new JwtFormatException("Header must be a JSON object.");
|
||||||
|
@ -116,4 +152,17 @@ unittest {
|
||||||
JwtClaims readClaims = readJwt(token, "test");
|
JwtClaims readClaims = readJwt(token, "test");
|
||||||
import std.stdio;
|
import std.stdio;
|
||||||
writeln(readClaims.toJson());
|
writeln(readClaims.toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// README example.
|
||||||
|
unittest {
|
||||||
|
import jwt4d;
|
||||||
|
import std.stdio;
|
||||||
|
|
||||||
|
const string MY_SECRET = "this is a secret!";
|
||||||
|
|
||||||
|
string token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTM5MjYzMjAsImlhdCI6MTc1MzkyNDUyMCwiaXNzIjoibXkud2VicGFnZS5jb20iLCJyb2xlIjoiYWRtaW4iLCJzdWIiOiJ1c2VyMTIzIn0.n5X2giJ3S5T3wrW4C0qlZrShr2ZwPiWIu6FxUzQ3K9s";
|
||||||
|
|
||||||
|
JwtClaims claims = readJwt(token, MY_SECRET);
|
||||||
|
writeln(claims.toJson());
|
||||||
|
}
|
||||||
|
|
|
@ -34,12 +34,31 @@ string writeJwt(in JwtClaims claims, string secret) {
|
||||||
}
|
}
|
||||||
|
|
||||||
unittest {
|
unittest {
|
||||||
JwtClaims claims;
|
JwtClaims claims = JwtClaims()
|
||||||
claims.issuer = "example.com";
|
.issuer("example.com")
|
||||||
claims.subject = "user123";
|
.subject("user123")
|
||||||
claims.expiration = 123;
|
.expiration(123);
|
||||||
|
|
||||||
string token = writeJwt(claims, "test");
|
string token = writeJwt(claims, "test");
|
||||||
import std.stdio;
|
import std.stdio;
|
||||||
writeln(token);
|
writeln(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// README example.
|
||||||
|
unittest {
|
||||||
|
import jwt4d;
|
||||||
|
import std.datetime; // To set expiration in duration.
|
||||||
|
import std.json; // To add a custom claim value.
|
||||||
|
import std.stdio;
|
||||||
|
|
||||||
|
const string MY_SECRET = "this is a secret!";
|
||||||
|
|
||||||
|
JwtClaims claims = JwtClaims()
|
||||||
|
.issuer("my.webpage.com")
|
||||||
|
.subject("user123")
|
||||||
|
.issuedAtNow()
|
||||||
|
.expiresIn(minutes(30))
|
||||||
|
.customClaim("role", JSONValue("admin"));
|
||||||
|
string token = writeJwt(claims, MY_SECRET);
|
||||||
|
writeln(token);
|
||||||
}
|
}
|
Loading…
Reference in New Issue