Current section
Files
Jump to
Current section
Files
src/ywt.erl
-module(ywt).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/ywt.gleam").
-export([generate_key/1, sign_bits/2, sign_string/2, verify_bits/3, verify_string/3, decode_unsafely_without_validation/2, decode/4, encode/3]).
-export_type([error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" <style>\n"
" .goto-top {\n"
" display: block;\n"
" font-size: 0.85em;\n"
" text-align: right;\n"
" }\n"
" </style>\n"
" <script>\n"
" (callback => document.readyState !== 'loading' ? callback() : document.addEventListener('DOMContentLoaded', callback, { once: true }))(() => {\n"
" const goToTop = document.createElement('a')\n"
" goToTop.classList.add('goto-top')\n"
" goToTop.setAttribute('href', '#')\n"
" goToTop.textContent = 'Back to top ↑'\n"
" for (const member of document.querySelectorAll('.member')) {\n"
" member.insertBefore(goToTop.cloneNode(true), null)\n"
" }\n"
" })\n"
" </script>\n"
).
-type error() :: malformed_token |
invalid_header_encoding |
invalid_payload_encoding |
invalid_signature_encoding |
{invalid_header_json, gleam@json:decode_error()} |
{header_decoding_error, list(gleam@dynamic@decode:decode_error())} |
unsupported_critical_header |
unsupported_unencoded_payload |
{invalid_payload_json, gleam@json:decode_error()} |
no_matching_key |
invalid_signature |
{token_expired, gleam@time@timestamp:timestamp()} |
{token_not_yet_valid, gleam@time@timestamp:timestamp()} |
{invalid_issuer, list(binary()), binary()} |
{invalid_audience, list(binary()), list(binary())} |
{invalid_type, binary(), binary()} |
{invalid_subject, list(binary()), binary()} |
{invalid_id, list(binary()), binary()} |
{missing_claim, binary()} |
{claim_decoding_error, binary(), list(gleam@dynamic@decode:decode_error())} |
{invalid_custom_claim, binary()} |
{payload_decoding_error, list(gleam@dynamic@decode:decode_error())}.
-file("src/ywt.gleam", 96).
-spec from_core_error(ywt@internal@core:error()) -> error().
from_core_error(Error) ->
case Error of
{claim_decoding_error, Claim_name, Error@1} ->
{claim_decoding_error, Claim_name, Error@1};
{invalid_audience, Expected, Actual} ->
{invalid_audience, Expected, Actual};
{invalid_custom_claim, Claim_name@1} ->
{invalid_custom_claim, Claim_name@1};
{header_decoding_error, Error@2} ->
{header_decoding_error, Error@2};
invalid_header_encoding ->
invalid_header_encoding;
{invalid_header_json, Error@3} ->
{invalid_header_json, Error@3};
{invalid_id, Expected@1, Actual@1} ->
{invalid_id, Expected@1, Actual@1};
{invalid_issuer, Expected@2, Actual@2} ->
{invalid_issuer, Expected@2, Actual@2};
{invalid_type, Expected@3, Actual@3} ->
{invalid_type, Expected@3, Actual@3};
invalid_payload_encoding ->
invalid_payload_encoding;
{invalid_payload_json, Error@4} ->
{invalid_payload_json, Error@4};
invalid_signature ->
invalid_signature;
invalid_signature_encoding ->
invalid_signature_encoding;
unsupported_critical_header ->
unsupported_critical_header;
unsupported_unencoded_payload ->
unsupported_unencoded_payload;
{invalid_subject, Expected@4, Actual@4} ->
{invalid_subject, Expected@4, Actual@4};
malformed_token ->
malformed_token;
{missing_claim, Claim_name@2} ->
{missing_claim, Claim_name@2};
no_matching_key ->
no_matching_key;
{payload_decoding_error, Error@5} ->
{payload_decoding_error, Error@5};
{token_expired, Expired_at} ->
{token_expired, Expired_at};
{token_not_yet_valid, Not_before} ->
{token_not_yet_valid, Not_before}
end.
-file("src/ywt.gleam", 135).
?DOC(
" Generates a signing key for an algorithm.\n"
"\n"
" Generated keys include a random `kid` for key rotation. Store signing keys as\n"
" secrets; anyone with one can mint trusted tokens.\n"
"\n"
" ```gleam\n"
" let signing_key = ywt.generate_key(algorithm.es384)\n"
" let verify_key = verify_key.derived(signing_key)\n"
" ```\n"
).
-spec generate_key(ywt@algorithm:algorithm()) -> ywt@sign_key:sign_key().
generate_key(Algorithm) ->
Key = ywt@algorithm:generate_key(
Algorithm,
fun ywt_ffi:generate_hmac/1,
fun ywt_ffi:generate_ecdsa/2,
fun ywt_ffi:generate_rsa/3
),
ywt@sign_key:with_random_id(Key).
-file("src/ywt.gleam", 168).
?DOC(
" Signs bytes with a signing key.\n"
"\n"
" This is a low-level function used internally by ywt.\n"
"\n"
" This is a low-level primitive. Most applications should use `encode` so the\n"
" JWT header, payload, and claims are constructed consistently. Raw signatures\n"
" do not carry claim or audience validation.\n"
).
-spec sign_bits(bitstring(), ywt@sign_key:sign_key()) -> bitstring().
sign_bits(Message, Key) ->
ywt_ffi:sign(Message, Key).
-file("src/ywt.gleam", 178).
?DOC(
" Signs a UTF-8 string and returns an unpadded base64url signature.\n"
"\n"
" This is a low-level helper for JWT-style signing input. Prefer `encode` for\n"
" complete JWTs.\n"
"\n"
" ```gleam\n"
" ywt.sign_string(\"header.payload\", signing_key)\n"
" ```\n"
).
-spec sign_string(binary(), ywt@sign_key:sign_key()) -> binary().
sign_string(Message, Key) ->
Bits = ywt_ffi:sign(<<Message/binary>>, Key),
gleam@bit_array:base64_url_encode(Bits, false).
-file("src/ywt.gleam", 193).
?DOC(
" Verifies a byte signature with a verification key.\n"
"\n"
" This is a low-level function used internally by ywt.\n"
"\n"
" This only verifies the signature over the exact bytes you pass. It does not\n"
" parse JWTs or validate claims.\n"
"\n"
" RSA JWKs decoded without `alg` cannot be used for raw verification and return\n"
" `False`; use `decode` when the signed JWT header should choose the algorithm.\n"
).
-spec verify_bits(bitstring(), bitstring(), ywt@verify_key:verify_key()) -> boolean().
verify_bits(Message, Signature, Key) ->
ywt_ffi:verify(Message, Signature, Key).
-file("src/ywt.gleam", 203).
?DOC(
" Verifies an unpadded base64url signature for a UTF-8 string.\n"
"\n"
" Invalid base64url returns `False`. This only verifies the signature over the\n"
" exact string you pass; use `decode` for complete JWT verification.\n"
).
-spec verify_string(binary(), binary(), ywt@verify_key:verify_key()) -> boolean().
verify_string(Message, Signature, Key) ->
case ywt@internal@base64url:decode(Signature) of
{ok, Signature@1} ->
ywt_ffi:verify(<<Message/binary>>, Signature@1, Key);
{error, _} ->
false
end.
-file("src/ywt.gleam", 219).
?DOC(
" Decodes a JWT payload without verifying the signature or claims.\n"
"\n"
" Do not use this for authentication or authorization. The token may be forged,\n"
" expired, or intended for another audience. No `exp`, `nbf`, or `aud` checks\n"
" are performed.\n"
).
-spec decode_unsafely_without_validation(
binary(),
gleam@dynamic@decode:decoder(FTV)
) -> {ok, FTV} | {error, error()}.
decode_unsafely_without_validation(Jwt, Payload_decoder) ->
_pipe = ywt@internal@jwt:decode_unsafely_without_validation(
Jwt,
Payload_decoder
),
gleam@result:map_error(_pipe, fun from_core_error/1).
-file("src/ywt.gleam", 247).
?DOC(
" Verifies a JWT and decodes its payload.\n"
"\n"
" Tokens with `exp` and `nbf` are checked by default with zero leeway. Tokens\n"
" with `aud` are rejected by default unless you pass an `audience` claim.\n"
" Audience validation accepts string or array values.\n"
"\n"
" Unknown non-critical JWT header fields are ignored by ywt. Tokens with a\n"
" `crit` header parameter or `b64: false` are rejected. Unknown payload fields\n"
" are accepted or rejected by your payload decoder.\n"
"\n"
" ```gleam\n"
" let claims = [\n"
" claim.expires_at(max_age: duration.minutes(15), leeway: duration.minutes(1)),\n"
" claim.issuer(\"https://auth.example.com\", []),\n"
" claim.audience(\"https://api.example.com\", []),\n"
" ]\n"
"\n"
" let decoder = decode.field(\"sub\", decode.string)\n"
" let result = ywt.decode(jwt, using: decoder, claims:, keys: [verify_key])\n"
" ```\n"
).
-spec decode(
binary(),
gleam@dynamic@decode:decoder(FTZ),
list(ywt@claim:claim()),
list(ywt@verify_key:verify_key())
) -> {ok, FTZ} | {error, error()}.
decode(Jwt, Decoder, Claims, Keys) ->
Verify = fun(Message, Signature, Key, Next) ->
Next(ywt_ffi:verify(Message, Signature, Key))
end,
Resolve = fun(_capture) ->
gleam@result:map_error(_capture, fun from_core_error/1)
end,
ywt@internal@jwt:decode(Jwt, Decoder, Claims, Keys, Verify, Resolve).
-file("src/ywt.gleam", 280).
?DOC(
" Creates a signed JWT from payload data and claims.\n"
"\n"
" JWTs are signed, not encrypted, so anyone with the token can read the\n"
" payload. Claims take precedence when a field appears in both `payload` and\n"
" `claims`; put security fields such as `exp`, `iss`, and `aud` in claims.\n"
"\n"
" ```gleam\n"
" let payload = [\n"
" #(\"sub\", json.string(\"user_123\")),\n"
" #(\"role\", json.string(\"admin\")),\n"
" ]\n"
"\n"
" let claims = [\n"
" claim.expires_at(max_age: duration.minutes(15), leeway: duration.minutes(1)),\n"
" claim.issuer(\"https://auth.example.com\", []),\n"
" claim.audience(\"https://api.example.com\", []),\n"
" ]\n"
"\n"
" let jwt = ywt.encode(payload, claims, signing_key)\n"
" ```\n"
).
-spec encode(
list({binary(), gleam@json:json()}),
list(ywt@claim:claim()),
ywt@sign_key:sign_key()
) -> binary().
encode(Payload, Claims, Key) ->
Sign = fun(Message, Key@1, Next) -> Next(ywt_ffi:sign(Message, Key@1)) end,
ywt@internal@jwt:encode(Payload, Claims, Key, Sign).