blob: c1e7ecfb104680ab1483e09702ebe0070f6a64dd [file] [edit]
@page examples \emoji :bulb: Usage Examples
This page walks through common end-to-end tasks with LibJWT, starting from the
simplest possible case and gradually layering in more advanced concepts. Each
example introduces only one or two new ideas, so they are meant to be read in
order.
Almost everything in LibJWT is built around two pairs of objects:
Purpose | Create / sign | Consume / verify
:---------------------- | :-------------------- | :--------------------
Signed tokens (JWS) | @ref jwt_builder_t | @ref jwt_checker_t
Encrypted tokens (JWE) | @ref jwe_builder_t | @ref jwe_checker_t
The pattern is always the same: create the object, configure it (a key, some
claims, perhaps a callback), then call a single function to produce or consume a
token.
> [!NOTE]
> To keep them readable, the examples below check only the most important return
> values. Real applications should check every return code (using
> ``jwt_builder_error_msg()`` and friends to report failures) and are encouraged
> to use the ``jwt_builder_auto_t`` / ``jwt_checker_auto_t`` /
> ``jwe_builder_auto_t`` / ``jwe_checker_auto_t`` / ``jwk_set_auto_t`` cleanup
> types (on GCC and Clang) so objects are freed automatically when they go out of
> scope.
@tableofcontents
@section ex_jws \emoji :pencil2: Signing and Verifying (JWS)
These examples cover creating and consuming signed tokens with the
@ref jwt_builder_grp and @ref jwt_checker_grp.
@subsection ex_jws_sign Signing your first token
The simplest token is signed with ``HS256``, an HMAC over a shared secret. The
secret is just a JWK with ``"kty":"oct"``. Loading a key is always a two-step
process: read the JSON into a keyring (@ref jwk_set_t) with one of the
``jwks_create_*`` functions, then pull the individual key (@ref jwk_item_t) out
of it with @ref jwks_item_get.
@code{.c}
#include <stdio.h>
#include <stdlib.h>
#include <jwt.h>
int main(void)
{
jwt_builder_t *builder = NULL;
jwk_set_t *keys = NULL;
const jwk_item_t *key = NULL;
char *token = NULL;
/* Load the signing key (a JWK with "kty":"oct"). */
keys = jwks_create_fromfile("oct_key_256.json");
key = jwks_item_get(keys, 0);
/* Create the builder and give it the key + algorithm. */
builder = jwt_builder_new();
jwt_builder_setkey(builder, JWT_ALG_HS256, key);
/* Generate the token. The caller owns the returned string. */
token = jwt_builder_generate(builder);
if (token == NULL)
fprintf(stderr, "generate failed: %s\n", jwt_builder_error_msg(builder));
else
printf("%s\n", token);
free(token);
jwt_builder_free(builder);
jwks_free(keys);
return 0;
}
@endcode
By default the builder adds an ``iat`` (Issued At) claim. You can turn that off
with @ref jwt_builder_enable_iat.
@subsection ex_jws_verify Verifying a token
Verifying mirrors signing: load the same key, set it on a @ref jwt_checker_t with
the algorithm you expect, then call @ref jwt_checker_verify. A return of ``0``
means the signature (and any configured claim checks) passed.
@code{.c}
jwt_checker_t *checker = NULL;
jwk_set_t *keys = NULL;
const jwk_item_t *key = NULL;
keys = jwks_create_fromfile("oct_key_256.json");
key = jwks_item_get(keys, 0);
checker = jwt_checker_new();
jwt_checker_setkey(checker, JWT_ALG_HS256, key);
if (jwt_checker_verify(checker, token) == 0)
printf("token is valid\n");
else
printf("rejected: %s\n", jwt_checker_error_msg(checker));
jwt_checker_free(checker);
jwks_free(keys);
@endcode
@note Pinning the algorithm with @ref jwt_checker_setkey is a security feature:
the checker will reject a token whose ``alg`` header does not match the algorithm
you configured. This prevents algorithm-confusion attacks.
@subsection ex_jws_claims Adding and validating claims
Claims and headers are set through a small helper struct, @ref jwt_value_t. You
fill it in with one of the ``jwt_set_SET_*`` macros, then hand it to
@ref jwt_builder_claim_set. Anything you set on the builder is copied into every
token it generates.
@code{.c}
jwt_value_t jval;
/* "sub": "user@example.com" */
jwt_set_SET_STR(&jval, "sub", "user@example.com");
jwt_builder_claim_set(builder, &jval);
/* "iss": "myapp" */
jwt_set_SET_STR(&jval, "iss", "myapp");
jwt_builder_claim_set(builder, &jval);
/* A custom integer claim. */
jwt_set_SET_INT(&jval, "login_count", 42);
jwt_builder_claim_set(builder, &jval);
@endcode
On the verifying side, the checker can validate a handful of registered claims
for you with a simple comparison. Use @ref jwt_checker_claim_set with the claim
you care about; verification then fails if the token's value does not match.
@code{.c}
/* Only accept tokens issued by "myapp" for the "billing" audience. */
jwt_checker_claim_set(checker, JWT_CLAIM_ISS, "myapp");
jwt_checker_claim_set(checker, JWT_CLAIM_AUD, "billing");
if (jwt_checker_verify(checker, token) == 0)
printf("issuer and audience accepted\n");
@endcode
The checker validates ``iss``, ``aud``, and ``sub`` by string comparison, and
``exp``/``nbf`` by time (see below). For anything more involved — multiple
acceptable issuers, custom claims, business rules — use a callback
(@ref ex_jws_callback).
@subsection ex_jws_time Expiration and not-before
The ``exp`` (Expiration) and ``nbf`` (Not Before) claims are time-based, so
LibJWT manages them as offsets from "now". On the builder, set the offset in
seconds with @ref jwt_builder_time_offset.
@code{.c}
/* Token is valid for one hour. */
jwt_builder_time_offset(builder, JWT_CLAIM_EXP, 3600);
/* ...and not valid until 30 seconds from now. */
jwt_builder_time_offset(builder, JWT_CLAIM_NBF, 30);
@endcode
The checker validates ``exp`` and ``nbf`` automatically whenever they are
present. To tolerate small clock differences between machines, give it some
leeway (in seconds) with @ref jwt_checker_time_leeway.
@code{.c}
/* Allow 60 seconds of clock skew on both exp and nbf. */
jwt_checker_time_leeway(checker, JWT_CLAIM_EXP, 60);
jwt_checker_time_leeway(checker, JWT_CLAIM_NBF, 60);
@endcode
@subsection ex_jws_crit Critical headers
Sometimes a token carries a custom header that the recipient @em must
understand. @rfc{7515,4.1.11} handles this with the ``crit`` (Critical) header,
which lists header names that cannot be ignored. On the builder, register each
critical name with @ref jwt_builder_setcrit; the header itself is added in a
callback (see @ref ex_jws_callback).
@code{.c}
/* Mark our custom "myhdr" header as critical. */
jwt_builder_setcrit(builder, "myhdr");
@endcode
A checker rejects any token whose ``crit`` header lists a name it has not been
told to expect. Declare the names your application understands with
@ref jwt_checker_understands.
@code{.c}
/* We know how to process "myhdr"; without this, verify() would fail. */
jwt_checker_understands(checker, "myhdr");
@endcode
@subsection ex_jws_callback Dynamic tokens with a callback
Everything above is static — the same configuration applies to every token. When
something must change per token (a different ``sub`` for each user, a key chosen
at runtime, a unique ``jti``), use a callback. The builder invokes it during
@ref jwt_builder_generate, after the token object is created but before it is
signed, giving you a @ref jwt_t to modify and a @ref jwt_config_t to set the key
and algorithm.
@code{.c}
/* Set a per-token claim and choose the signing key at generation time. */
static int on_generate(jwt_t *jwt, jwt_config_t *config)
{
jwt_value_t jval;
const char *user = config->ctx; /* the ctx we registered below */
jwt_set_SET_STR(&jval, "sub", user);
jwt_claim_set(jwt, &jval);
config->alg = JWT_ALG_HS256;
config->key = my_lookup_key(user); /* application-provided */
return 0; /* non-zero aborts generation */
}
/* Register it, passing a context pointer through to the callback. */
jwt_builder_setcb(builder, on_generate, "user@example.com");
token = jwt_builder_generate(builder);
@endcode
The checker has the mirror-image hook, @ref jwt_checker_setcb — it runs during
verification so you can inspect the header and claims, or select the verification
key, before the signature is checked. (A checker callback should only read the
@ref jwt_t — changes to it do not affect verification.)
@note For the very common case of a unique token id, the builder offers a
dedicated @ref jwt_builder_setjti hook to generate the ``jti`` claim, paired with
@ref jwt_checker_setjti on the checker for replay protection.
@section ex_jwe \emoji :lock: Encrypting and Decrypting (JWE)
Encrypted tokens use the @ref jwe_builder_grp and @ref jwe_checker_grp. The main
difference from JWS is that JWE needs @em two algorithms: a key management
algorithm (the ``"alg"`` header, @ref jwe_key_alg_t) that establishes the Content
Encryption Key (CEK), and a content encryption algorithm (the ``"enc"`` header,
@ref jwe_enc_t) that actually encrypts the payload.
@subsection ex_jwe_encrypt Encrypting your first token
The simplest JWE uses ``dir`` (Direct Encryption): a shared symmetric key @em is
the CEK, so there is no key wrapping. Here it is paired with ``A256GCM`` content
encryption. The key is a JWK with ``"kty":"oct"``. Because the plaintext can be
arbitrary bytes, @ref jwe_builder_generate takes an explicit length.
@code{.c}
#include <stdlib.h>
#include <string.h>
#include <jwt.h>
jwe_builder_t *builder = NULL;
jwk_set_t *keys = NULL;
const jwk_item_t *key = NULL;
const char *message = "the launch code is 0000";
char *token = NULL;
keys = jwks_create_fromfile("oct_dir_256.json");
key = jwks_item_get(keys, 0);
builder = jwe_builder_new();
jwe_builder_setkey(builder, JWE_ALG_DIR, JWE_ENC_A256GCM, key);
token = jwe_builder_generate(builder, (const unsigned char *)message,
strlen(message));
if (token == NULL)
fprintf(stderr, "encrypt failed: %s\n", jwe_builder_error_msg(builder));
else
printf("%s\n", token); /* a five-part compact JWE */
free(token);
jwe_builder_free(builder);
jwks_free(keys);
@endcode
@subsection ex_jwe_decrypt Decrypting a token
To decrypt, configure a @ref jwe_checker_t with the same key and the algorithms
you expect, then call @ref jwe_checker_decrypt. It returns a newly allocated
buffer and, through its out-parameter, the true plaintext length. The buffer is
also nil-terminated for convenience.
@code{.c}
jwe_checker_t *checker = NULL;
unsigned char *plaintext = NULL;
size_t len = 0;
keys = jwks_create_fromfile("oct_dir_256.json");
key = jwks_item_get(keys, 0);
checker = jwe_checker_new();
jwe_checker_setkey(checker, JWE_ALG_DIR, JWE_ENC_A256GCM, key);
plaintext = jwe_checker_decrypt(checker, token, &len);
if (plaintext == NULL)
fprintf(stderr, "decrypt failed: %s\n", jwe_checker_error_msg(checker));
else
printf("recovered %zu bytes: %s\n", len, plaintext);
free(plaintext);
jwe_checker_free(checker);
jwks_free(keys);
@endcode
@note Decryption with an authenticated mode like ``A256GCM`` also @em verifies
the token. If the ciphertext, IV, or tag has been altered, decryption fails and
returns NULL — there is no separate "verify" step.
@subsection ex_jwe_rsa Encrypting to a recipient's public key
``dir`` requires both sides to share a secret in advance. More often you want to
encrypt to someone's @em public key so that only their private key can decrypt.
That is what the asymmetric key management algorithms are for. Here we use
``RSA-OAEP-256``: only the ``alg`` changes — the rest of the flow is identical.
@code{.c}
/* The sender needs only the recipient's public RSA JWK. */
keys = jwks_create_fromfile("rsa_key_2048_enc.json");
key = jwks_item_get(keys, 0);
builder = jwe_builder_new();
jwe_builder_setkey(builder, JWE_ALG_RSA_OAEP_256, JWE_ENC_A256GCM, key);
token = jwe_builder_generate(builder, (const unsigned char *)message,
strlen(message));
@endcode
The recipient decrypts exactly as in @ref ex_jwe_decrypt, but using their RSA
@em private key and the matching ``JWE_ALG_RSA_OAEP_256``.
@subsection ex_jwe_json JSON serialization and AAD
Every example so far produced the @rfc{7516,7.1} Compact Serialization — the
five-part ``a.b.c.d.e`` string. The @rfc{7516,7.2} JSON Serialization is a JSON
object instead, and it can carry things the compact form cannot, such as
Additional Authenticated Data (AAD): bytes that are authenticated (bound into the
tag) but not encrypted. Select the JSON form with @ref jwe_builder_set_format and
attach AAD with @ref jwe_builder_set_aad.
@code{.c}
static const unsigned char aad[] = "context-id:42";
builder = jwe_builder_new();
jwe_builder_setkey(builder, JWE_ALG_DIR, JWE_ENC_A256GCM, key);
/* Produce the Flattened JSON Serialization with AAD. */
jwe_builder_set_format(builder, JWE_FORMAT_JSON_FLAT);
jwe_builder_set_aad(builder, aad, sizeof(aad) - 1);
token = jwe_builder_generate(builder, (const unsigned char *)message,
strlen(message));
@endcode
A JSON token may be compact @em or JSON, so on the consuming side use
@ref jwe_checker_decrypt_all, which auto-detects the serialization. After a
successful decrypt you can recover the authenticated AAD with
@ref jwe_checker_get_aad.
@code{.c}
size_t aad_len = 0;
const unsigned char *got = NULL;
plaintext = jwe_checker_decrypt_all(checker, token, &len);
got = jwe_checker_get_aad(checker, &aad_len);
if (got != NULL)
printf("authenticated AAD: %.*s\n", (int)aad_len, got);
@endcode
@subsection ex_jwe_multi Multiple recipients
The General JSON Serialization can address @em several recipients at once: the
payload is encrypted a single time with one CEK, and that CEK is then wrapped
independently for each recipient. Any one recipient can decrypt the token with
their own key. Configure the first recipient with @ref jwe_builder_setkey as
usual, then add each additional one with @ref jwe_builder_add_recipient. Adding a
second recipient automatically switches output to @ref JWE_FORMAT_JSON_GENERAL.
@code{.c}
const jwk_item_t *rsa_key = NULL, *ec_key = NULL;
jwk_set_t *rsa_keys = NULL, *ec_keys = NULL;
rsa_keys = jwks_create_fromfile("rsa_key_2048_enc.json");
rsa_key = jwks_item_get(rsa_keys, 0);
ec_keys = jwks_create_fromfile("ec_key_prime256v1_enc.json");
ec_key = jwks_item_get(ec_keys, 0);
builder = jwe_builder_new();
/* First recipient: RSA. The shared "enc" is set here. */
jwe_builder_setkey(builder, JWE_ALG_RSA_OAEP_256, JWE_ENC_A256GCM, rsa_key);
/* Second recipient: an EC key via ECDH-ES + A128KW. */
jwe_builder_add_recipient(builder, JWE_ALG_ECDH_ES_A128KW, ec_key);
/* Produces the General JSON Serialization with a "recipients" array. */
token = jwe_builder_generate(builder, (const unsigned char *)message,
strlen(message));
@endcode
Each recipient decrypts with their own key. The checker is configured with that
recipient's key and algorithm, and @ref jwe_checker_decrypt_all finds the
matching entry in the ``recipients`` array.
@code{.c}
/* The EC recipient decrypts with its own key and algorithm. */
checker = jwe_checker_new();
jwe_checker_setkey(checker, JWE_ALG_ECDH_ES_A128KW, JWE_ENC_A256GCM, ec_key);
plaintext = jwe_checker_decrypt_all(checker, token, &len);
@endcode
@note ``dir`` and ECDH-ES Direct (@ref JWE_ALG_ECDH_ES) derive the CEK directly
from a single key, so they cannot be combined with other recipients.
@subsection ex_jwe_ecdh ECDH-ES and PartyInfo
ECDH-ES derives the CEK from an (ephemeral) Diffie-Hellman exchange against the
recipient's EC key. The derivation (a Concat KDF, @rfc{7518,4.6}) can optionally
mix in two application-supplied octet strings, PartyUInfo and PartyVInfo, which
are also emitted as the ``apu`` and ``apv`` headers. Set them with
@ref jwe_builder_set_partyinfo.
@code{.c}
keys = jwks_create_fromfile("ec_key_prime256v1_enc.json");
key = jwks_item_get(keys, 0);
builder = jwe_builder_new();
jwe_builder_setkey(builder, JWE_ALG_ECDH_ES, JWE_ENC_A256GCM, key);
/* Optional context bound into the key derivation. */
jwe_builder_set_partyinfo(builder,
(const unsigned char *)"Alice", 5, /* apu */
(const unsigned char *)"Bob", 3); /* apv */
token = jwe_builder_generate(builder, (const unsigned char *)message,
strlen(message));
@endcode
The decrypting side sets the same algorithms and uses its EC private key; the
``apu``/``apv`` values travel in the token, so the checker reproduces the same
derivation automatically.
@note For per-recipient PartyInfo or per-recipient headers in a General JSON
token, use the recipient handle returned by @ref jwe_builder_add_recipient with
@ref jwe_recipient_set_partyinfo and @ref jwe_recipient_add_header_json.