Packages

The ywt package for JWT targeting erlang.

Current section

Files

Jump to
ywt_erlang src ywt.erl
Raw

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([parse_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.
-type parse_error() :: malformed_token |
invalid_header_encoding |
invalid_payload_encoding |
invalid_signature_encoding |
{invalid_header_json, gleam@json:decode_error()} |
{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()), 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", 110).
-spec from_core_error(ywt@internal@core:parse_error()) -> parse_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};
invalid_header_encoding ->
invalid_header_encoding;
{invalid_header_json, Error@2} ->
{invalid_header_json, Error@2};
{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_payload_encoding ->
invalid_payload_encoding;
{invalid_payload_json, Error@3} ->
{invalid_payload_json, Error@3};
invalid_signature ->
invalid_signature;
invalid_signature_encoding ->
invalid_signature_encoding;
{invalid_subject, Expected@3, Actual@3} ->
{invalid_subject, Expected@3, Actual@3};
malformed_token ->
malformed_token;
{missing_claim, Claim_name@2} ->
{missing_claim, Claim_name@2};
no_matching_key ->
no_matching_key;
{payload_decoding_error, Error@4} ->
{payload_decoding_error, Error@4};
{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", 160).
?DOC(
" Generates a new cryptographically secure signing key for the specified algorithm.\n"
"\n"
" This function creates fresh signing keys with appropriate parameters for each\n"
" algorithm type. All keys are generated using cryptographically secure random\n"
" number generators and follow current security best practices.\n"
"\n"
" The generated keys will have a random id set that can be used to identify the key\n"
" during key rotation.\n"
"\n"
" ## Usage\n"
" Use this for key generation in development, testing, or when implementing\n"
" key rotation systems. For production use, consider generating keys offline\n"
" and storing them in secure key management systems.\n"
"\n"
" ## Key Parameters\n"
" - **HMAC**: Full-entropy random secrets of appropriate length (at least 32/48/64 bytes)\n"
" - **RSA**: 4096-bit modulus with secure random prime generation\n"
" - **ECDSA**: Secure random private scalars on specified curves. The digest type matches the selected curve.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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", 215).
?DOC(
" Creates a cryptographic signature for the given message using the specified signing key.\n"
"\n"
" This is a low-level function that performs the actual cryptographic signing operation.\n"
"\n"
" ## Usage\n"
" This function is primarily used internally by higher-level JWT functions. Use this\n"
" directly only when you need raw signature operations outside of JWT context.\n"
"\n"
" ## Security Considerations\n"
" - Never sign untrusted or unvalidated input data\n"
" - Ensure signing keys are stored securely and accessed only by authorized code\n"
"\n"
" ## Example\n"
" ```gleam\n"
" let message = <<\"Hello, world!\":utf8>>\n"
" let signing_key = load_secure_signing_key()\n"
"\n"
" let signature = ywt.sign_bits(message, signing_key)\n"
" // signature is raw bytes that can be verified with corresponding verify key\n"
" ```\n"
"\n"
" ⚠️ **Warning:** This function performs raw cryptographic operations. Most applications\n"
" should use the higher-level JWT functions instead.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\n"
).
-spec sign_bits(bitstring(), ywt@sign_key:sign_key()) -> bitstring().
sign_bits(Message, Key) ->
ywt_ffi:sign(Message, Key).
-file("src/ywt.gleam", 248).
?DOC(
" Creates a base64url-encoded signature for a UTF-8 string message.\n"
"\n"
" This is a convenience wrapper around `sign_bits` that handles string encoding\n"
" and base64url encoding of the signature, commonly used for signing JWT components.\n"
"\n"
" ## Usage\n"
" Use this when you need to sign string data and want the signature in base64url\n"
" format for use in web contexts like JWTs or HTTP headers.\n"
"\n"
" ## Security Considerations\n"
" - Same security considerations as `sign_bits` apply\n"
" - The string is encoded as UTF-8 before signing\n"
" - Verify signatures using the corresponding `verify_string` function\n"
"\n"
" ## Example\n"
" ```gleam\n"
" let jwt_payload = \"eyJzdWIiOiIxMjM0NTY3ODkwIn0\"\n"
" let signing_key = load_jwt_signing_key()\n"
"\n"
" let signature = sign_string(jwt_payload, signing_key))\n"
" // signature is base64url-encoded string ready for JWT use\n"
" ```\n"
"\n"
" 💡 **Note:** The returned signature is base64url-encoded (URL-safe, no padding)\n"
" as required by JWT and other web standards.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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", 284).
?DOC(
" Verifies a cryptographic signature against a message using the specified verification key.\n"
"\n"
" This is a low-level function that performs the actual cryptographic verification.\n"
" Returns `True` if the signature is valid for the given message and key, `False` otherwise.\n"
"\n"
" ## Usage\n"
" Use this for raw signature verification operations. The verification algorithm\n"
" is determined by the key type and must match the algorithm used for signing.\n"
"\n"
" ## Example\n"
" ```gleam\n"
" let message = <<\"Hello, world!\":utf8>>\n"
" let signature = // ... received signature bytes\n"
" let verify_key = load_verification_key()\n"
"\n"
" let is_valid = verify_bits(message, signature, verify_key)\n"
" case is_valid {\n"
" True -> process_verified_message(message)\n"
" False -> reject_invalid_signature()\n"
" }\n"
" ```\n"
"\n"
" 🔒 **Critical:** Never trust data with invalid signatures. A `False` result\n"
" indicates potential tampering or use of wrong keys.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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", 330).
?DOC(
" Verifies a base64url-encoded signature against a UTF-8 string message.\n"
"\n"
" This is a convenience wrapper around `verify_bits` that handles string encoding\n"
" and base64url decoding of the signature. Commonly used for verifying JWT signatures.\n"
"\n"
" ## Usage\n"
" Use this when verifying signatures that are base64url-encoded, such as those\n"
" from JWTs or other web-based cryptographic protocols.\n"
"\n"
" ## Security Considerations\n"
" - Same security considerations as `verify_bits` apply\n"
" - Invalid base64url encoding in the signature automatically returns `False`\n"
"\n"
" ## Example\n"
" ```gleam\n"
" let payload = \"eyJzdWIiOiIxMjM0NTY3ODkwIn0\"\n"
" let signature = \"base64url-encoded-signature-string\"\n"
" let verify_key = load_jwt_verification_key()\n"
"\n"
" let is_valid = verify_string(payload, signature, verify_key)\n"
" case is_valid {\n"
" True -> {\n"
" // Signature is valid, payload can be trusted\n"
" process_authenticated_request(payload)\n"
" }\n"
" False -> {\n"
" // Signature invalid - reject the request\n"
" return_authentication_error()\n"
" }\n"
" }\n"
" ```\n"
"\n"
" ⚠️ **Important:** Malformed base64url signatures return `False` rather than\n"
" causing errors, ensuring consistent handling of invalid input.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\n"
).
-spec verify_string(binary(), binary(), ywt@verify_key:verify_key()) -> boolean().
verify_string(Message, Signature, Key) ->
case gleam@bit_array:base64_url_decode(Signature) of
{ok, Signature@1} ->
ywt_ffi:verify(<<Message/binary>>, Signature@1, Key);
{error, _} ->
false
end.
-file("src/ywt.gleam", 354).
?DOC(
" Extracts payload from a JWT without verifying its signature or claims.\n"
"\n"
" 🚨 **USE WITH EXTREME CAUTION** Only use this when you need to inspect tokens\n"
" from trusted sources where signature verification is handled elsewhere.\n"
"\n"
" ## Security Considerations\n"
" - **NEVER use this in production for authentication**\n"
" - Tokens could be forged or tampered with\n"
" - No expiration or claim validation is performed\n"
" - Only use when signature validation happens at a different layer\n"
" - Consider this as dangerous as accepting any user input\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\n"
).
-spec decode_unsafely_without_validation(
binary(),
gleam@dynamic@decode:decoder(GGT)
) -> {ok, GGT} | {error, nil}.
decode_unsafely_without_validation(Jwt, Payload_decoder) ->
ywt@internal@jwt:decode_unsafely_without_validation(Jwt, Payload_decoder).
-file("src/ywt.gleam", 401).
?DOC(
" Verifies a JWT signature and validates all claims, returning the decoded payload if successful.\n"
"\n"
" Use this to validate incoming JWTs from clients, ensuring they're authentic and\n"
" haven't been tampered with.\n"
"\n"
" ## Security Considerations\n"
" - Implement appropriate claims validation (expiration, issuer, audience)\n"
" - Handle verification failures securely (don't leak information)\n"
" - Consider rate limiting to prevent brute force attacks\n"
"\n"
" ## Example\n"
" ```gleam\n"
" // Define expected claims for validation\n"
" let claims = [\n"
" claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),\n"
" claim.issuer(\"my-app\", []),\n"
" ]\n"
"\n"
" // Parse and validate the JWT\n"
" case ywt.decode(jwt_token, using: payload_decoder, claims:, keys: [verify_key]) {\n"
" Ok(payload) -> {\n"
" // JWT is valid, use the payload\n"
" let user_id = // extract user ID from payload\n"
" authorize_request(user_id)\n"
" }\n"
" Error(_) -> {\n"
" // JWT is invalid - reject the request\n"
" unauthorized_response()\n"
" }\n"
" }\n"
" ```\n"
"\n"
" 💡 **Best Practice:** Always validate JWTs on every request and never trust\n"
" client-provided tokens without verification.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\n"
).
-spec decode(
binary(),
gleam@dynamic@decode:decoder(GGX),
list(ywt@claim:claim()),
list(ywt@verify_key:verify_key())
) -> {ok, GGX} | {error, parse_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", 456).
?DOC(
" Creates a signed JWT containing the specified payload and claims.\n"
"\n"
" Signed JWTs prevent actors without access to the SignKey from modifying it.\n"
" This can be verified using the corresponding VerifyKey.\n"
"\n"
" ## Security Considerations\n"
" - JWTs are signed, not encrypted - all data is publicly readable\n"
" - Never include sensitive data like passwords or personal information\n"
" - Keep payloads small to avoid large tokens\n"
" - Include appropriate expiration times in your claims\n"
" - Use strong signing keys and rotate them regularly\n"
"\n"
" If a field is present in claims as well as the payload, the payload takes\n"
" precedence.\n"
"\n"
" ## Example\n"
" ```gleam\n"
" // Create a user session token\n"
" let payload = [\n"
" #(\"sub\", json.string(\"user_12345\")),\n"
" #(\"role\", json.string(\"developer\")),\n"
" #(\"permissions\", json.array([json.string(\"read\"), json.string(\"write\")]))\n"
" ]\n"
"\n"
" let claims = [\n"
" claim.issued_at(),\n"
" claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)),\n"
" claim.issuer(\"my-app\", []),\n"
" ]\n"
"\n"
" ywt.encode(payload: payload, claims: claims, key: sign_key)\n"
" // Returns: \"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ...\"\n"
" ```\n"
"\n"
" ⚠️ **Remember:** Anyone can decode and read JWT contents. Only include data\n"
" you're comfortable being public.\n"
"\n"
" <div style=\"text-align: right;\">\n"
" <a href=\"#\">\n"
" <small>Back to top ↑</small>\n"
" </a>\n"
" </div>\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).