Add more docs, more cleanup.
This commit is contained in:
parent
4585845c95
commit
9cf62e4d36
32
README.md
32
README.md
|
@ -3,3 +3,35 @@
|
|||
A JSON Web Token library to generate, parse, sign, and verify JWTs.
|
||||
|
||||
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;
|
||||
|
||||
import std.json;
|
||||
import std.base64;
|
||||
import std.datetime;
|
||||
|
||||
/**
|
||||
* Enum defining the names of registered JWT claims, as per RFC 7519.
|
||||
|
@ -95,6 +95,12 @@ struct JwtClaims {
|
|||
return this;
|
||||
}
|
||||
|
||||
ref expiresIn(Duration dur) {
|
||||
long expirationUnixTime = (Clock.currTime() + dur).toUnixTime!long;
|
||||
this.obj.object[JwtClaim.Expiration] = expirationUnixTime;
|
||||
return this;
|
||||
}
|
||||
|
||||
long expiration() const {
|
||||
if (JwtClaim.Expiration !in this.obj.object) return -1;
|
||||
return this.obj.object[JwtClaim.Expiration].integer;
|
||||
|
@ -123,6 +129,17 @@ struct JwtClaims {
|
|||
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) {
|
||||
if (jwtId is null) {
|
||||
this.obj.object.remove(JwtClaim.JwtId);
|
||||
|
@ -132,6 +149,21 @@ struct JwtClaims {
|
|||
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 {
|
||||
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;
|
||||
|
||||
import std.json;
|
||||
|
@ -7,36 +11,50 @@ import secured;
|
|||
|
||||
import jwt4d.model;
|
||||
|
||||
/// Base type for any exception thrown while attempting to read a JWT.
|
||||
class JwtException : Exception {
|
||||
this(string message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown if a token is malformed and cannot be read.
|
||||
class JwtFormatException : JwtException {
|
||||
this(string message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown if a token cannot be verified.
|
||||
class JwtVerificationException : JwtException {
|
||||
this() {
|
||||
super("JWT could not be verified.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown if a token's "exp" claim is present and not in the future.
|
||||
class JwtExpiredException : JwtException {
|
||||
this() {
|
||||
super("JWT has expired.");
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown if a token's "nbf" claim is present and not in the past.
|
||||
class JwtNotBeforeException : JwtException {
|
||||
this() {
|
||||
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
|
||||
* is verified, this method will also evaluate the "exp" (expiration) and "nbf"
|
||||
|
@ -47,36 +65,54 @@ class JwtNotBeforeException : JwtException {
|
|||
* 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]);
|
||||
JwtComponents components = extractComponents(token);
|
||||
bool verified = hmac_verify_ex(
|
||||
signatureBytes,
|
||||
components.signature,
|
||||
cast(ubyte[]) secret,
|
||||
cast(ubyte[]) (parts[0] ~ "." ~ parts[1]),
|
||||
cast(ubyte[]) components.payloadForSigning,
|
||||
HashAlgorithm.SHA2_256
|
||||
);
|
||||
if (!verified) {
|
||||
throw new JwtVerificationException();
|
||||
}
|
||||
|
||||
JwtClaims claims = JwtClaims(claimsObj);
|
||||
JwtClaims claims = JwtClaims(components.claimsObj);
|
||||
verifyStandardTimeClaims(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) {
|
||||
if (j.type != JSONType.OBJECT) {
|
||||
throw new JwtFormatException("Header must be a JSON object.");
|
||||
|
@ -116,4 +152,17 @@ unittest {
|
|||
JwtClaims readClaims = readJwt(token, "test");
|
||||
import std.stdio;
|
||||
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 {
|
||||
JwtClaims claims;
|
||||
claims.issuer = "example.com";
|
||||
claims.subject = "user123";
|
||||
claims.expiration = 123;
|
||||
JwtClaims claims = JwtClaims()
|
||||
.issuer("example.com")
|
||||
.subject("user123")
|
||||
.expiration(123);
|
||||
|
||||
string token = writeJwt(claims, "test");
|
||||
import std.stdio;
|
||||
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