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 }