1 module hunt.jwt.Jwt; 2 3 import hunt.jwt.Base64Codec; 4 import hunt.jwt.Exceptions; 5 import hunt.jwt.JwtOpenSSL; 6 import hunt.jwt.JwtAlgorithm; 7 8 import std.json; 9 import std.base64; 10 import std.algorithm; 11 import std.array : split; 12 13 14 import hunt.logging; 15 16 17 /** 18 simple version that accepts only strings as values for payload and header fields 19 */ 20 string encode(string[string] payload, string key, JwtAlgorithm algo = JwtAlgorithm.HS256, 21 string[string] header_fields = null) { 22 JSONValue jsonHeader = header_fields; 23 JSONValue jsonPayload = payload; 24 25 return encode(jsonPayload, key, algo, jsonHeader); 26 } 27 28 /** 29 full version that accepts JSONValue tree as payload and header fields 30 */ 31 string encode(ref JSONValue payload, string key, JwtAlgorithm algo = JwtAlgorithm.HS256, 32 JSONValue header_fields = null) { 33 return encode(cast(ubyte[])payload.toString(), key, algo, header_fields); 34 } 35 36 /** 37 full version that accepts ubyte[] as payload and JSONValue tree as header fields 38 */ 39 string encode(in ubyte[] payload, string key, JwtAlgorithm algo = JwtAlgorithm.HS256, 40 JSONValue header_fields = null) { 41 import std.functional : memoize; 42 43 auto getEncodedHeader(JwtAlgorithm algo, JSONValue fields) { 44 if(fields.type == JSONType.null_) 45 fields = (JSONValue[string]).init; 46 fields.object["alg"] = cast(string)algo; 47 fields.object["typ"] = "JWT"; 48 49 return Base64URLNoPadding.encode(cast(ubyte[])fields.toString()).idup; 50 } 51 52 string encodedHeader = memoize!(getEncodedHeader, 64)(algo, header_fields); 53 string encodedPayload = Base64URLNoPadding.encode(payload); 54 55 string signingInput = encodedHeader ~ "." ~ encodedPayload; 56 string signValue = sign(signingInput, key, algo); 57 58 version(HUNT_JWT_DEBUG) { 59 tracef("sign: %(%02X %)", cast(ubyte[])signValue); 60 } 61 62 string signature = Base64URLNoPadding.encode(cast(ubyte[])sign(signingInput, key, algo)); 63 return signingInput ~ "." ~ signature; 64 } 65 66 // string fromDER(string data) { 67 68 // } 69 70 string encode(string payload, string key, JwtAlgorithm algo, 71 string header_fields) { 72 73 string encodedHeader = Base64URLNoPadding.encode(cast(ubyte[])header_fields); // memoize!(getEncodedHeader, 64)(algo, header_fields); 74 string encodedPayload = Base64URLNoPadding.encode(cast(ubyte[])payload); 75 76 string signingInput = encodedHeader ~ "." ~ encodedPayload; 77 string signature = Base64URLNoPadding.encode(cast(ubyte[])sign(signingInput, key, algo)); 78 79 return signingInput ~ "." ~ signature; 80 } 81 82 83 /** 84 simple version that knows which key was used to encode the token 85 */ 86 JSONValue decode(string token, string key) { 87 return decode(token, (ref _) => key); 88 } 89 90 /** 91 full version where the key is provided after decoding the JOSE header 92 */ 93 JSONValue decode(string token, string delegate(ref JSONValue jose) lazyKey) { 94 import std.algorithm : count; 95 import std.conv : to; 96 import std.uni : toUpper; 97 98 if(count(token, ".") != 2) 99 throw new VerifyException("Token is incorrect."); 100 101 string[] tokenParts = split(token, "."); 102 103 JSONValue header; 104 try { 105 header = parseJSON(cast(string)urlsafeB64Decode(tokenParts[0])); 106 } catch(Exception e) { 107 throw new VerifyException("Header is incorrect."); 108 } 109 110 JwtAlgorithm alg; 111 try { 112 // toUpper for none 113 alg = to!(JwtAlgorithm)(toUpper(header["alg"].str())); 114 } catch(Exception e) { 115 throw new VerifyException("Algorithm is incorrect."); 116 } 117 118 if (auto typ = ("typ" in header)) { 119 string typ_str = typ.str(); 120 if(typ_str && typ_str != "JWT") 121 throw new VerifyException("Type is incorrect."); 122 } 123 124 const key = lazyKey(header); 125 if(!verifySignature(tokenParts[0]~"."~tokenParts[1], tokenParts[2], key, alg)) 126 throw new VerifyException("Signature is incorrect."); 127 128 JSONValue payload; 129 130 try { 131 payload = parseJSON(cast(string)urlsafeB64Decode(tokenParts[1])); 132 } catch(JSONException e) { 133 // Code coverage has to miss this line because the signature test above throws before this does 134 throw new VerifyException("Payload JSON is incorrect."); 135 } 136 137 return payload; 138 }