1 module hunt.jwt.JwtToken;
2 
3 import hunt.jwt.Base64Codec;
4 import hunt.jwt.Claims;
5 import hunt.jwt.Component;
6 import hunt.jwt.Exceptions;
7 import hunt.jwt.Header;
8 import hunt.jwt.Jwt;
9 import hunt.jwt.JwtAlgorithm;
10 import hunt.jwt.JwtOpenSSL;
11 
12 import std.conv;
13 import std.datetime;
14 import std.json;
15 import std.string;
16 
17 import hunt.logging;
18 
19 
20 /**
21 * represents a token
22 */
23 class JwtToken {
24 
25 private {
26     Claims _claims;
27     Header _header;
28 
29     this(Claims claims, Header header) {
30         this._claims = claims;
31         this._header = header;
32     }
33 
34     @property string data() {
35         return this.header.base64 ~ "." ~ this.claims.base64;
36     }
37 }
38 
39     this(in JwtAlgorithm alg, in string typ = "JWT") {
40         this._claims = new Claims();
41         this._header = new Header(alg, typ);
42     }
43 
44     @property Claims claims() {
45         return this._claims;
46     }
47 
48     @property Header header() {
49         return this._header;
50     }
51 
52     /**
53     * used to get the signature of the token
54     * Parmas:
55     *       secret = the secret key used to sign the token
56     * Returns: the signature of the token
57     */
58     string signature(string secret) {
59         return Base64URLNoPadding.encode(cast(ubyte[])sign(this.data, secret, this.header.alg));
60 
61     }
62 
63     /**
64     * encodes the token
65     * Params:
66     *       secret = the secret key used to sign the token
67     *Returns: base64 representation of the token including signature
68     */
69     string encode(string secret) {
70         if ((this.claims.exp != ulong.init && this.claims.iat != ulong.init) && this.claims.exp < this.claims.iat) {
71             throw new ExpiredException("Token has already expired");
72         }
73 
74         if ((this.claims.exp != ulong.init && this.claims.nbf != ulong.init) && this.claims.exp < this.claims.nbf) {
75             throw new ExpiresBeforeValidException("Token will expire before it becomes valid");
76         }
77 
78         string token = this.data ~ "." ~ this.signature(secret);
79 
80         version(HUNT_AUTH_DEBUG) {
81             import std.stdio;
82             writeln("secret: %s, token: %s", secret, token);
83         }
84 
85         return token;
86 
87     }
88     ///
89     unittest {
90         JwtToken token = new JwtToken(JwtAlgorithm.HS512);
91 
92         long now = Clock.currTime.toUnixTime();
93 
94         string secret = "super_secret";
95         token.claims.exp = now - 3600;
96 
97         assertThrown!ExpiredException(token.encode(secret));
98 
99         token.claims.exp = now + 3600;
100         token.claims.nbf = now + 7200;
101 
102         assertThrown!ExpiresBeforeValidException(token.encode(secret));
103     }
104 
105     /**
106     * overload of the encode(string secret) function to simplify encoding of token without algorithm none
107     * Returns: base64 representation of the token
108     */
109     string encode() {
110         assert(this.header.alg == JwtAlgorithm.NONE);
111         return this.encode("");
112     }
113 
114 
115     static JwtToken decode(string token, string delegate(ref JSONValue jose) lazyKey) {
116         import std.algorithm : count;
117         import std.conv : to;
118         import std.uni : toUpper;
119 
120         version(HUNT_JWT_DEBUG) {
121             tracef("token: %s", token);
122         }
123 
124         if(count(token, ".") != 2)
125             throw new VerifyException("Token is incorrect.");
126 
127         string[] tokenParts = split(token, ".");
128 
129         JSONValue header;
130         try {
131             header = parseJSON(cast(string)urlsafeB64Decode(tokenParts[0]));
132         } catch(Exception e) {
133             throw new VerifyException("Header is incorrect.");
134         }
135 
136         JwtAlgorithm alg;
137         try {
138             // toUpper for none
139             alg = to!(JwtAlgorithm)(toUpper(header["alg"].str()));
140         } catch(Exception e) {
141             throw new VerifyException("Algorithm is incorrect.");
142         }
143 
144         if (auto typ = ("typ" in header)) {
145             string typ_str = typ.str();
146             if(typ_str && typ_str != "JWT")
147                 throw new VerifyException("Type is incorrect.");
148         }
149 
150         const key = lazyKey(header);
151         if(!key.empty() && !verifySignature(tokenParts[0]~"."~tokenParts[1], tokenParts[2], key, alg))
152             throw new VerifyException("Signature is incorrect.");
153 
154         JSONValue payload;
155 
156         try {
157             payload = parseJSON(cast(string)urlsafeB64Decode(tokenParts[1]));
158         } catch(JSONException e) {
159             // Code coverage has to miss this line because the signature test above throws before this does
160             throw new VerifyException("Payload JSON is incorrect.");
161         }
162 
163         
164         Header h = new Header(header);
165         Claims claims = new Claims(payload);
166 
167         return new JwtToken(claims, h);
168     }
169 
170     static JwtToken decode(string encodedToken, string key="") {
171         return decode(encodedToken, (ref _) => key);	
172     }
173 
174     static bool verify(string token, string key) {
175         import std.algorithm : count;
176         import std.conv : to;
177         import std.uni : toUpper;
178 
179         if(count(token, ".") != 2)
180             throw new VerifyException("Token is incorrect.");
181 
182         string[] tokenParts = split(token, ".");
183 
184         string decHeader = cast(string)urlsafeB64Decode(tokenParts[0]);
185         JSONValue header = parseJSON(decHeader);
186 
187         JwtAlgorithm alg;
188         try {
189             // toUpper for none
190             alg = to!(JwtAlgorithm)(toUpper(header["alg"].str()));
191         } catch(Exception e) {
192             throw new VerifyException("Algorithm is incorrect.");
193         }
194 
195         if (auto typ = ("typ" in header)) {
196             string typ_str = typ.str();
197             if(typ_str && typ_str != "JWT")
198                 throw new VerifyException("Type is incorrect.");
199         }
200 
201         return verifySignature(tokenParts[0]~"."~tokenParts[1], tokenParts[2], key, alg);
202     }
203 }
204 
205 
206 alias verify = JwtToken.verify;
207 alias decode = JwtToken.decode;
208 
209 deprecated("Using JwtToken instead.")
210 alias Token = JwtToken;