DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
jwt.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file jwt.cpp
6 * @brief JWT HS256 verification + claim extraction (base64url over base64).
7 */
8
9#include "services/jwt/jwt.h"
10
11#if DETWS_ENABLE_JWT
12
15#include <stdio.h>
16#include <string.h>
17
18// Constant-time equality over @p n bytes (no early-out timing oracle).
19static bool ct_eq(const char *a, const char *b, size_t n)
20{
21 uint8_t diff = 0;
22 for (size_t i = 0; i < n; i++)
23 diff |= (uint8_t)(a[i] ^ b[i]);
24 return diff == 0;
25}
26
27// base64url encode/decode are shared with OIDC in the base64 module
28// (base64url_encode / base64url_decode).
29
30// Split a compact JWT into header.payload (signing input) and the signature.
31// Requires exactly two '.' separators. Returns false on a malformed shape.
32static bool jwt_split(const char *token, size_t token_len, size_t *signing_len, const char **sig, size_t *sig_len)
33{
34 const char *d1 = (const char *)memchr(token, '.', token_len);
35 if (!d1)
36 return false;
37 size_t rem = token_len - (size_t)(d1 + 1 - token);
38 const char *d2 = (const char *)memchr(d1 + 1, '.', rem);
39 if (!d2)
40 return false;
41 size_t rem2 = token_len - (size_t)(d2 + 1 - token);
42 if (memchr(d2 + 1, '.', rem2)) // a third '.' is not a valid JWT
43 return false;
44 *signing_len = (size_t)(d2 - token);
45 *sig = d2 + 1;
46 *sig_len = rem2;
47 return true;
48}
49
50// RFC 7515 §5.2: the algorithm used MUST be the one named by the JWS header "alg".
51// Decode the header segment and require alg == "HS256" - this rejects "none",
52// RS256, HS384, and any other algorithm-substitution attempt before the HMAC check.
53static bool jwt_header_alg_is_hs256(const char *header, size_t hlen)
54{
55 uint8_t buf[96];
56 size_t n = base64url_decode(header, hlen, buf, sizeof(buf) - 1);
57 if (n == 0)
58 return false;
59 buf[n] = '\0';
60 const char *p = strstr((const char *)buf, "\"alg\"");
61 if (!p)
62 return false;
63 p += 5;
64 while (*p == ' ' || *p == ':' || *p == '\t')
65 p++;
66 if (*p != '"')
67 return false;
68 p++;
69 return strncmp(p, "HS256", 5) == 0 && p[5] == '"';
70}
71
72bool jwt_verify_hs256(const char *token, size_t token_len, const uint8_t *secret, size_t secret_len)
73{
74 if (!token || token_len < 5 || token_len > DETWS_JWT_MAX_LEN)
75 return false;
76
77 size_t signing_len;
78 size_t sig_len;
79 const char *sig;
80 if (!jwt_split(token, token_len, &signing_len, &sig, &sig_len))
81 return false;
82
83 // Validate the declared algorithm matches what we verify (RFC 7515 §5.2).
84 const char *d1 = (const char *)memchr(token, '.', token_len);
85 if (!jwt_header_alg_is_hs256(token, (size_t)(d1 - token)))
86 return false;
87
88 // HS256 -> 32-byte MAC -> 43 base64url chars (no padding).
89 if (sig_len != 43)
90 return false;
91
92 uint8_t mac[SSH_HMAC_SHA256_LEN];
93 ssh_hmac_sha256(secret, secret_len, (const uint8_t *)token, signing_len, mac);
94
95 char computed[48];
96 if (base64url_encode(mac, sizeof(mac), computed) != 43)
97 return false;
98 return ct_eq(computed, sig, 43);
99}
100
101bool jwt_bearer_valid(const char *auth_header, const uint8_t *secret, size_t secret_len)
102{
103 if (!auth_header || strncasecmp(auth_header, "Bearer ", 7) != 0)
104 return false;
105 const char *tok = auth_header + 7;
106 while (*tok == ' ')
107 tok++;
108 return jwt_verify_hs256(tok, strnlen(tok, DETWS_JWT_MAX_LEN + 1), secret, secret_len);
109}
110
111bool jwt_time_valid(const char *token, size_t token_len, long now_epoch, long leeway_s)
112{
113 if (!token || now_epoch <= 0)
114 return true; // no wall clock -> time claims cannot be evaluated (the signature is the gate)
115
116 // Subtraction form (not exp + leeway) so a far-future claim cannot overflow `long`.
117 long exp = 0;
118 if (jwt_claim_int(token, token_len, "exp", &exp) && now_epoch - exp > leeway_s)
119 return false; // expired (RFC 7519 §4.1.4)
120
121 long nbf = 0;
122 if (jwt_claim_int(token, token_len, "nbf", &nbf) && nbf - now_epoch > leeway_s)
123 return false; // not yet valid (RFC 7519 §4.1.5)
124
125 return true;
126}
127
128bool jwt_verify_hs256_at(const char *token, size_t token_len, const uint8_t *secret, size_t secret_len, long now_epoch,
129 long leeway_s)
130{
131 return jwt_verify_hs256(token, token_len, secret, secret_len) &&
132 jwt_time_valid(token, token_len, now_epoch, leeway_s);
133}
134
135bool jwt_bearer_valid_at(const char *auth_header, const uint8_t *secret, size_t secret_len, long now_epoch,
136 long leeway_s)
137{
138 if (!auth_header || strncasecmp(auth_header, "Bearer ", 7) != 0)
139 return false;
140 const char *tok = auth_header + 7;
141 while (*tok == ' ')
142 tok++;
143 return jwt_verify_hs256_at(tok, strnlen(tok, DETWS_JWT_MAX_LEN + 1), secret, secret_len, now_epoch, leeway_s);
144}
145
146bool jwt_claim_int(const char *token, size_t token_len, const char *name, long *out)
147{
148 if (!token || !name || !out)
149 return false;
150
151 const char *d1 = (const char *)memchr(token, '.', token_len);
152 if (!d1)
153 return false;
154 size_t rem = token_len - (size_t)(d1 + 1 - token);
155 const char *d2 = (const char *)memchr(d1 + 1, '.', rem);
156 if (!d2)
157 return false;
158 const char *payload = d1 + 1;
159 size_t payload_len = (size_t)(d2 - payload);
160
161 uint8_t buf[DETWS_JWT_MAX_LEN];
162 size_t n = base64url_decode(payload, payload_len, buf, sizeof(buf) - 1);
163 if (n == 0)
164 return false;
165 buf[n] = '\0';
166
167 char key[40];
168 int kn = snprintf(key, sizeof(key), "\"%s\"", name);
169 if (kn <= 0 || kn >= (int)sizeof(key))
170 return false;
171 const char *p = strstr((const char *)buf, key);
172 if (!p)
173 return false;
174 p += kn;
175 while (*p == ' ' || *p == ':' || *p == '\t')
176 p++;
177 bool neg = false;
178 if (*p == '-')
179 {
180 neg = true;
181 p++;
182 }
183 if (*p < '0' || *p > '9')
184 return false;
185 unsigned long v = 0; // accumulate unsigned: signed overflow (a huge claim value) is UB
186 while (*p >= '0' && *p <= '9')
187 v = v * 10UL + (unsigned long)(*p++ - '0');
188 *out = neg ? (long)(0UL - v) : (long)v; // two's-complement reinterpret, no negation UB
189 return true;
190}
191
192bool jwt_claim_str(const char *token, size_t token_len, const char *name, char *out, size_t out_cap)
193{
194 if (!token || !name || !out || out_cap == 0)
195 return false;
196 out[0] = '\0';
197
198 const char *d1 = (const char *)memchr(token, '.', token_len);
199 if (!d1)
200 return false;
201 size_t rem = token_len - (size_t)(d1 + 1 - token);
202 const char *d2 = (const char *)memchr(d1 + 1, '.', rem);
203 if (!d2)
204 return false;
205 const char *payload = d1 + 1;
206 size_t payload_len = (size_t)(d2 - payload);
207
208 uint8_t buf[DETWS_JWT_MAX_LEN];
209 size_t n = base64url_decode(payload, payload_len, buf, sizeof(buf) - 1);
210 if (n == 0)
211 return false;
212 buf[n] = '\0';
213
214 char key[40];
215 int kn = snprintf(key, sizeof(key), "\"%s\"", name);
216 if (kn <= 0 || kn >= (int)sizeof(key))
217 return false;
218 const char *p = strstr((const char *)buf, key);
219 if (!p)
220 return false;
221 p += kn;
222 while (*p == ' ' || *p == ':' || *p == '\t')
223 p++;
224 if (*p != '"') // not a string-valued claim
225 return false;
226 p++;
227 size_t i = 0;
228 while (*p && *p != '"' && i + 1 < out_cap)
229 {
230 if (*p == '\\' && p[1]) // minimal unescape: drop the backslash, copy the next char
231 p++;
232 out[i++] = *p++;
233 }
234 if (*p != '"') // unterminated string or value too long for out
235 {
236 out[0] = '\0';
237 return false;
238 }
239 out[i] = '\0';
240 return true;
241}
242
243bool jwt_scope_allows(const char *scope_claim, const char *required)
244{
245 if (!scope_claim || !required || !*required)
246 return false;
247 size_t rlen = strnlen(required, DETWS_JWT_MAX_LEN + 1);
248 const char *p = scope_claim;
249 while (*p)
250 {
251 while (*p == ' ')
252 p++;
253 const char *start = p;
254 while (*p && *p != ' ')
255 p++;
256 if ((size_t)(p - start) == rlen && memcmp(start, required, rlen) == 0)
257 return true;
258 }
259 return false;
260}
261
262#endif // DETWS_ENABLE_JWT
#define DETWS_JWT_MAX_LEN
Maximum accepted JWT length in bytes (header.payload.signature).
size_t base64url_decode(const char *src, size_t src_len, uint8_t *dst, size_t dst_cap)
Decode src_len characters of base64url (RFC 4648 section 5, '-'/'_' alphabet; an '=' ends the input).
Definition base64.cpp:200
size_t base64url_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes as base64url (RFC 4648 section 5): '-' / '_' in place of '+' / '/',...
Definition base64.cpp:163
Base64 encoder/decoder.
Zero-heap JWT (JSON Web Token) bearer-auth verification, HS256.
void ssh_hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t len, uint8_t mac[SSH_HMAC_SHA256_LEN])
Compute HMAC-SHA2-256 over a single contiguous buffer.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1).
#define SSH_HMAC_SHA256_LEN
HMAC-SHA2-256 output length in bytes.