Merge pull request #3642 from cesanta/jwt

Add JWT Bearer Authentication support and tutorial
This commit is contained in:
Sergio R. Caprile authored and GitHub committed 2026-07-24 11:49:46 -03:00
commit f78cb1b02f
21 files changed
+888 -81

No files matched your search

+49 -3
View File
@@ -12,16 +12,16 @@ static int mg_base64_encode_single(int c) {
}
}
static int mg_base64_decode_single(int c) {
static int base64_decode_single(int c, char plus, char slash) {
if (c >= 'A' && c <= 'Z') {
return c - 'A';
} else if (c >= 'a' && c <= 'z') {
return c + 26 - 'a';
} else if (c >= '0' && c <= '9') {
return c + 52 - '0';
} else if (c == '+') {
} else if (c == plus) {
return 62;
} else if (c == '/') {
} else if (c == slash) {
return 63;
} else if (c == '=') {
return 64;
@@ -30,6 +30,14 @@ static int mg_base64_decode_single(int c) {
}
}
static int mg_base64_decode_single(int c) {
return base64_decode_single(c, '+', '/');
}
static int mg_base64url_decode_single(int c) {
return base64_decode_single(c, '-', '_');
}
size_t mg_base64_update(unsigned char ch, char *to, size_t n) {
unsigned long rem = (n & 3) % 3;
if (rem == 0) {
@@ -91,3 +99,41 @@ fail:
if (dl > 0) dst[0] = '\0';
return 0;
}
size_t mg_base64url_encode(const unsigned char *p, size_t n, char *to,
size_t dl) {
size_t i, len = mg_base64_encode(p, n, to, dl);
if (len == 0) return 0;
for (i = 0; i < len; i++) {
if (to[i] == '+') {
to[i] = '-';
} else if (to[i] == '/') {
to[i] = '_';
}
}
while (len > 0 && to[len - 1] == '=') to[--len] = '\0';
return len;
}
size_t mg_base64url_decode(const char *src, size_t n, char *dst, size_t dl) {
size_t i, len = 0;
unsigned int bits = 0, v = 0;
if (dl == 0 || (n & 3) == 1) goto fail;
for (i = 0; src != NULL && i < n; i++) {
int c = mg_base64url_decode_single(src[i]);
if (c == 64) break;
if (c < 0) goto fail;
v = (v << 6) | (unsigned int) c;
bits += 6;
if (bits >= 8) {
bits -= 8;
if (len + 1 >= dl) goto fail;
dst[len++] = (char) ((v >> bits) & 255U);
}
}
dst[len] = '\0';
return len;
fail:
if (dl > 0) dst[0] = '\0';
return 0;
}
+3
View File
@@ -4,3 +4,6 @@ size_t mg_base64_update(unsigned char input_byte, char *buf, size_t len);
size_t mg_base64_final(char *buf, size_t len);
size_t mg_base64_encode(const unsigned char *p, size_t n, char *buf, size_t);
size_t mg_base64_decode(const char *src, size_t n, char *dst, size_t);
size_t mg_base64url_encode(const unsigned char *p, size_t n, char *buf,
size_t);
size_t mg_base64url_decode(const char *src, size_t n, char *dst, size_t);
+164
View File
@@ -0,0 +1,164 @@
#include "jwt.h"
#include "base64.h"
#include "json.h"
#include "log.h"
#include "printf.h"
#include "sha256.h"
#include "util.h"
#if MG_TLS == MG_TLS_BUILTIN
#include "tls_uecc.h"
#endif
static size_t jwt_split(struct mg_str jwt, struct mg_str *h, struct mg_str *p,
struct mg_str *s) {
size_t i, a = jwt.len, b = jwt.len;
for (i = 0; i < jwt.len; i++) {
if (jwt.buf[i] == '.') {
if (a == jwt.len) a = i;
if (a != i) b = i;
if (b != jwt.len) break;
}
}
if (a == jwt.len || b == jwt.len || b + 1 >= jwt.len) return 0;
*h = mg_str_n(jwt.buf, a);
*p = mg_str_n(jwt.buf + a + 1, b - a - 1);
*s = mg_str_n(jwt.buf + b + 1, jwt.len - b - 1);
return b;
}
static bool jwt_alg(struct mg_str h, const char *alg) {
char got[16];
size_t len = h.len * 3 / 4 + 2;
char *buf = (char *) mg_calloc(1, len);
bool ok;
if (buf == NULL) return false;
ok = mg_base64url_decode(h.buf, h.len, buf, len) > 0 &&
mg_json_unescape(mg_str(buf), "$.alg", got, sizeof(got)) > 0 &&
strcmp(got, alg) == 0;
mg_free(buf);
return ok;
}
static size_t jwt_header_payload(const char *alg, const struct mg_jwt_opts *opts,
char *buf, size_t len) {
char *header, *kid = NULL;
size_t n, m;
if (opts->kid.len > 0) {
kid = mg_mprintf(",%m:%m", MG_ESC("kid"), mg_print_esc,
(int) opts->kid.len, opts->kid.buf);
if (kid == NULL) return 0;
}
header = mg_mprintf("{%m:%m,%m:%m%.*s%.*s%.*s}", MG_ESC("alg"),
MG_ESC(alg), MG_ESC("typ"), MG_ESC("JWT"),
kid == NULL ? 0 : (int) strlen(kid),
kid == NULL ? "" : kid,
opts->header.len > 0 ? 1 : 0, ",",
(int) opts->header.len, opts->header.buf);
mg_free(kid);
if (header == NULL) return 0;
n = mg_base64url_encode((uint8_t *) header, strlen(header), buf, len);
mg_free(header);
if (n == 0 || n + 1 >= len) return 0;
buf[n++] = '.';
m = mg_base64url_encode((uint8_t *) opts->claims.buf, opts->claims.len,
buf + n, len - n);
return m == 0 ? 0 : n + m;
}
size_t mg_jwt_sign_hs256(const struct mg_jwt_opts *opts, char *buf,
size_t len) {
uint8_t sig[32];
size_t n = jwt_header_payload("HS256", opts, buf, len);
size_t m;
if (n == 0 || n + 1 >= len) return 0;
mg_hmac_sha256(sig, (uint8_t *) opts->secret.buf, opts->secret.len,
(uint8_t *) buf, n);
buf[n++] = '.';
m = mg_base64url_encode(sig, sizeof(sig), buf + n, len - n);
return m == 0 ? 0 : n + m;
}
size_t mg_jwt_verify_hs256(struct mg_str jwt, const struct mg_jwt_opts *opts,
char *buf, size_t len) {
struct mg_str h, p, s;
uint8_t sig[33], digest[32];
size_t n = jwt_split(jwt, &h, &p, &s);
if (n == 0 || !jwt_alg(h, "HS256")) return 0;
if (mg_base64url_decode(s.buf, s.len, (char *) sig, sizeof(sig)) !=
sizeof(digest))
return 0;
mg_hmac_sha256(digest, (uint8_t *) opts->secret.buf, opts->secret.len,
(uint8_t *) jwt.buf, n);
if (!mg_memeq(sig, digest, sizeof(digest))) return 0;
return mg_base64url_decode(p.buf, p.len, buf, len);
}
#if MG_TLS == MG_TLS_BUILTIN
typedef struct {
MG_UECC_HashContext uECC;
mg_sha256_ctx sha256;
} jwt_hash_ctx;
static void jwt_hash_init(const MG_UECC_HashContext *ctx) {
jwt_hash_ctx *c = (jwt_hash_ctx *) ctx;
mg_sha256_init(&c->sha256);
}
static void jwt_hash_update(const MG_UECC_HashContext *ctx, const uint8_t *msg,
unsigned len) {
jwt_hash_ctx *c = (jwt_hash_ctx *) ctx;
mg_sha256_update(&c->sha256, msg, len);
}
static void jwt_hash_finish(const MG_UECC_HashContext *ctx, uint8_t *hash) {
jwt_hash_ctx *c = (jwt_hash_ctx *) ctx;
mg_sha256_final(hash, &c->sha256);
}
size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf,
size_t len) {
uint8_t hash[32], sig[64], tmp[2 * 32 + 64];
jwt_hash_ctx ctx = {
{jwt_hash_init, jwt_hash_update, jwt_hash_finish, 64, 32, tmp},
{{0}, 0, 0, {0}}};
size_t n = jwt_header_payload("ES256", opts, buf, len);
size_t m;
if (n == 0 || n + 1 >= len) return 0;
mg_sha256(hash, (uint8_t *) buf, n);
if (mg_uecc_sign_deterministic(opts->private_key, hash, sizeof(hash),
&ctx.uECC, sig, mg_uecc_secp256r1()) != 1)
return 0;
buf[n++] = '.';
m = mg_base64url_encode(sig, sizeof(sig), buf + n, len - n);
return m == 0 ? 0 : n + m;
}
size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts,
char *buf, size_t len) {
struct mg_str h, p, s;
uint8_t sig[65], hash[32];
size_t n = jwt_split(jwt, &h, &p, &s);
if (n == 0 || !jwt_alg(h, "ES256")) return 0;
if (mg_base64url_decode(s.buf, s.len, (char *) sig, sizeof(sig)) != 64)
return 0;
mg_sha256(hash, (uint8_t *) jwt.buf, n);
if (mg_uecc_verify(opts->public_key, hash, sizeof(hash), sig,
mg_uecc_secp256r1()) != 1)
return 0;
return mg_base64url_decode(p.buf, p.len, buf, len);
}
#else
size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf,
size_t len) {
(void) opts, (void) buf, (void) len;
MG_ERROR(("JWT ES256 requires built-in TLS"));
return 0;
}
size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts,
char *buf, size_t len) {
(void) jwt, (void) opts, (void) buf, (void) len;
return mg_jwt_sign_es256(NULL, NULL, 0);
}
#endif
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "arch.h"
#include "str.h"
#include "tls.h"
struct mg_jwt_opts {
struct mg_str claims; // JSON payload
struct mg_str header; // Extra header members, no braces; alg/typ are set
struct mg_str kid; // Key ID protected header
struct mg_str secret; // HS256 secret
const uint8_t *private_key; // ES256 private key (MG_TLS_BUILTIN only)
const uint8_t *public_key; // ES256 public key (MG_TLS_BUILTIN only)
};
// Signs JSON claims as a compact JWT. Returns JWT length, or 0 on error.
size_t mg_jwt_sign_hs256(const struct mg_jwt_opts *opts, char *buf,
size_t len);
// Verifies JWT and decodes JSON claims into buf. Returns claims length, or 0.
size_t mg_jwt_verify_hs256(struct mg_str jwt, const struct mg_jwt_opts *opts,
char *buf, size_t len);
size_t mg_jwt_sign_es256(const struct mg_jwt_opts *opts, char *buf,
size_t len);
size_t mg_jwt_verify_es256(struct mg_str jwt, const struct mg_jwt_opts *opts,
char *buf, size_t len);
+6
View File
@@ -87,6 +87,12 @@ void mg_tls_ctx_init(struct mg_mgr *);
void mg_tls_ctx_free(struct mg_mgr *);
#define MG_IS_DER(buf) (((uint8_t *) (buf))[0] == 0x30) // DER begins with 0x30
#if MG_TLS == MG_TLS_BUILTIN
// Extracts a 32-byte P-256 private scalar from EC PRIVATE KEY or PKCS#8 PRIVATE
// KEY input. Accepts PEM or DER. Returns key size, or 0 on error.
size_t mg_uecc_parse_private_key(struct mg_str key, uint8_t *buf, size_t len);
#endif
// Low-level IO primitives used by TLS layer
enum { MG_IO_ERR = -1, MG_IO_WAIT = -2 };
long mg_io_send(struct mg_connection *c, const void *buf, size_t len);
+2 -5
View File
@@ -22,6 +22,7 @@
#include "arch.h"
#include "tls.h"
#include "util.h"
#if MG_TLS == MG_TLS_BUILTIN
/******************************************************************************/
#define AES_DECRYPTION 1 // whether AES decryption is supported
@@ -1115,8 +1116,6 @@ int mg_aes_gcm_decrypt(unsigned char *output, const unsigned char *input,
int ret = 0; // our return value
gcm_context ctx; // includes the AES context structure
unsigned char computed_tag[16];
size_t i;
int diff = 0;
if (tag_len > sizeof(computed_tag)) return -1;
@@ -1127,9 +1126,7 @@ int mg_aes_gcm_decrypt(unsigned char *output, const unsigned char *input,
gcm_zero_ctx(&ctx);
// compare tags
for (i = 0; i < tag_len; i++) diff |= computed_tag[i] ^ tag[i];
if (diff != 0) ret = -1;
if (!mg_memeq(computed_tag, tag, tag_len)) ret = -1;
return (ret);
}
+21 -4
View File
@@ -2409,7 +2409,7 @@ static int mg_rsa_parse_key(const uint8_t *der, size_t dersz,
// parameters ANY OPTIONAL
// }
static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz,
struct tls_data *tls) {
struct mg_rsa_key *rsa, uint8_t *ec_key) {
struct mg_der_tlv root, version, alg_id, private_key_octets;
struct mg_der_tlv alg_oid, alg_params;
@@ -2442,17 +2442,19 @@ static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz,
if (alg_oid.len == sizeof(mg_rsa_oid) &&
memcmp(alg_oid.value, mg_rsa_oid, sizeof(mg_rsa_oid)) == 0) {
struct mg_rsa_key rsa_key;
if (rsa == NULL) return -1;
if (mg_rsa_parse_key(private_key_octets.value, private_key_octets.len,
&rsa_key) < 0) {
MG_ERROR(("PKCS#8: failed to parse inner RSA key"));
return -1;
}
tls->rsa = rsa_key;
*rsa = rsa_key;
return 0;
} else if (alg_oid.len == sizeof(mg_ec_public_key_oid) &&
memcmp(alg_oid.value, mg_ec_public_key_oid,
sizeof(mg_ec_public_key_oid)) == 0) {
if (ec_key == NULL) return -1;
if (mg_der_next(&alg_id, &alg_params) < 0 || alg_params.type != 0x06) {
MG_ERROR(("PKCS#8: invalid EC parameters OID"));
return -1;
@@ -2466,7 +2468,7 @@ static int mg_parse_pkcs8_key(const uint8_t *der, size_t dersz,
}
return mg_parse_ec_private_key(private_key_octets.value,
private_key_octets.len, tls->ec_key);
private_key_octets.len, ec_key);
} else {
MG_ERROR(("PKCS#8: unsupported algorithm"));
@@ -2571,6 +2573,20 @@ static int mg_parse_pem_certs(const struct mg_str pem, struct mg_str **ders) {
return count;
}
size_t mg_uecc_parse_private_key(struct mg_str key, uint8_t *buf, size_t len) {
struct mg_str der = mg_str_n(NULL, 0);
size_t n = 0;
if (buf == NULL || len < 32) return 0;
// current mg_parse_ec_private_key() only handles 32-byte keys
if (mg_parse_pem(key, mg_str_s("EC PRIVATE KEY"), &der) == 0) {
if (mg_parse_ec_private_key((uint8_t *) der.buf, der.len, buf) == 0) n = 32;
} else if (mg_parse_pem(key, mg_str_s("PRIVATE KEY"), &der) == 0) {
if (mg_parse_pkcs8_key((uint8_t *) der.buf, der.len, NULL, buf) == 0) n = 32;
}
mg_free((void *) der.buf);
return n;
}
static void sync_time_cancel(struct mg_connection *c) {
c->fn = NULL;
c->is_closing = 1;
@@ -2713,7 +2729,8 @@ void mg_tls_init(struct mg_connection *c, const struct mg_tls_opts *opts) {
// Copy parsed RSA key components to tls->rsa for signing operations
tls->rsa = rsa_key;
} else if (mg_parse_pem(opts->key, mg_str_s("PRIVATE KEY"), &key) == 0) {
if (mg_parse_pkcs8_key((const uint8_t *) key.buf, key.len, tls) == 0) {
if (mg_parse_pkcs8_key((const uint8_t *) key.buf, key.len, &tls->rsa,
tls->ec_key) == 0) {
if (tls->rsa.n.len > 0) {
tls->rsa_key_der = key;
MG_INFO(("Parsed PKCS#8 RSA private key: %d bytes", (int) key.len));
+3 -6
View File
@@ -6,6 +6,7 @@
#include "tls_chacha20.h"
#include "tls.h"
#include "util.h"
#if MG_TLS == MG_TLS_BUILTIN && MG_ENABLE_CHACHA20
// ******* BEGIN: chacha-portable/chacha-portable.h ********
@@ -1336,8 +1337,6 @@ PORTABLE_8439_DECL size_t mg_chacha20_poly1305_decrypt(
// first we calculate the mac and see if it lines up, only then do we decrypt
size_t actual_size = cipher_text_size - RFC_8439_TAG_SIZE;
uint8_t computed_mac[RFC_8439_TAG_SIZE];
int diff = 0;
size_t i;
if (MG_OVERLAPPING(plain_text, actual_size, cipher_text, cipher_text_size)) {
return (size_t) -1;
}
@@ -1345,10 +1344,8 @@ PORTABLE_8439_DECL size_t mg_chacha20_poly1305_decrypt(
poly1305_calculate_mac(computed_mac, cipher_text, actual_size, key, nonce, ad,
ad_size);
// compare tags
for (i = 0; i < RFC_8439_TAG_SIZE; i++)
diff |= computed_mac[i] ^ cipher_text[actual_size + i];
if (diff != 0) return (size_t) -1;
if (!mg_memeq(computed_mac, cipher_text + actual_size, RFC_8439_TAG_SIZE))
return (size_t) -1;
chacha20_xor_stream(plain_text, cipher_text, actual_size, key, nonce, 1);
return actual_size;
+4 -12
View File
@@ -222,11 +222,7 @@ static cmpresult_t mg_uecc_vli_cmp_unsafe(const mg_uecc_word_t *left,
#define asm_mmod_fast_secp256r1 0
#endif
#if defined(default_RNG_defined) && default_RNG_defined
static MG_UECC_RNG_Function g_rng_function = &default_RNG;
#else
static MG_UECC_RNG_Function g_rng_function = 0;
#endif
static MG_UECC_RNG_Function g_rng_function = NULL;
void mg_uecc_set_rng(MG_UECC_RNG_Function rng_function) {
g_rng_function = rng_function;
@@ -2560,15 +2556,11 @@ MG_UECC_VLI_API int mg_uecc_generate_random_int(mg_uecc_word_t *random,
mg_uecc_word_t tries;
bitcount_t num_bits = mg_uecc_vli_numBits(top, num_words);
if (!g_rng_function) {
return 0;
}
for (tries = 0; tries < MG_UECC_RNG_MAX_TRIES; ++tries) {
if (!g_rng_function((uint8_t *) random,
(unsigned int) (num_words * MG_UECC_WORD_SIZE))) {
unsigned int len = (unsigned int) (num_words * MG_UECC_WORD_SIZE);
if (!(g_rng_function == NULL ? mg_random((uint8_t *) random, len)
: g_rng_function((uint8_t *) random, len)))
return 0;
}
random[num_words - 1] &=
mask >> ((bitcount_t) (num_words * MG_UECC_WORD_SIZE * 8 - num_bits));
if (!mg_uecc_vli_isZero(random, num_words) &&
+10 -10
View File
@@ -113,28 +113,28 @@ if 'dest' was filled with random data, or 0 if the random data could not be
generated. The filled-in values should be either truly random, or from a
cryptographically-secure PRNG.
A correctly functioning RNG function must be set (using mg_uecc_set_rng())
before calling mg_uecc_make_key() or mg_uecc_sign().
A correctly functioning RNG is required before calling mg_uecc_make_key() or
mg_uecc_sign().
Setting a correctly functioning RNG function improves the resistance to
side-channel attacks for mg_uecc_shared_secret() and
mg_uecc_sign_deterministic().
A correct RNG function is set by default when building for Windows, Linux, or OS
X. If you are building on another POSIX-compliant system that supports
/dev/random or /dev/urandom, you can define MG_UECC_POSIX to use the predefined
RNG. For embedded platforms there is no predefined RNG function; you must
provide your own.
*/
typedef int (*MG_UECC_RNG_Function)(uint8_t *dest, unsigned size);
// Mongoose: mg_uecc_make_key() and mg_uecc_sign() use mg_random() when no
// explicit uECC RNG is installed. If mg_random() cannot provide a strong source,
// those operations fail. Deterministic signing works without RNG, but only an
// explicit uECC RNG enables its optional side-channel blinding.
/* mg_uecc_set_rng() function.
Set the function that will be used to generate random bytes. The RNG function
should return 1 if the random data was generated, or 0 if the random data could
not be generated.
On platforms where there is no predefined RNG function (eg embedded platforms),
this must be called before mg_uecc_make_key() or mg_uecc_sign() are used.
Mongoose: this overrides the default mg_random() fallback used by
mg_uecc_make_key() and mg_uecc_sign(). Do not call mg_uecc_set_rng() unless the
application must provide its own cryptographic RNG.
Inputs:
rng_function - The function that will be used to generate random bytes.
+8
View File
@@ -25,6 +25,14 @@ uint64_t mg_timegm(unsigned int year, unsigned int month, unsigned int day,
day_secs * ((ly + 299) / 400);
}
bool mg_memeq(const void *a, const void *b, size_t n) {
const uint8_t *p = (const uint8_t *) a, *q = (const uint8_t *) b;
uint8_t r = 0;
size_t i;
for (i = 0; i < n; i++) r = (uint8_t) (r | (uint8_t) (p[i] ^ q[i]));
return r == 0;
}
#if MG_ENABLE_CUSTOM_RANDOM
#else
bool mg_random(void *buf, size_t len) {
+3
View File
@@ -22,6 +22,9 @@ void mg_free(void *ptr);
// sensitive data (keys, passwords).
void mg_bzero(volatile unsigned char *buf, size_t len);
// Fixed-length constant-time byte equality. Use for MACs, tags, signatures.
bool mg_memeq(const void *a, const void *b, size_t n);
// Fills buf with len cryptographically random bytes. Uses the best available
// hardware or OS source (hardware RNG, /dev/urandom, CryptGenRandom, etc.).
// Falls back to rand() with an error log if no strong source is available.