Current section
Files
Jump to
Current section
Files
src/gose@jwt.erl
-module(gose@jwt).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gose/jwt.gleam").
-export([gose_error_to_malformed_token_error/1, default_validation/0, with_jti_validator/2, with_max_token_age/2, verifier/3, claims/0, with_audience/2, with_audiences/2, with_expiration/2, with_issued_at/2, with_issuer/2, with_jwt_id/2, with_not_before/2, with_subject/2, alg/1, kid/1, select_keys_by_policy/3, serialize/1, sign/3, claims_to_json_string/1, dangerously_decode_unverified/2, decode/2, parse_claims_bits/1, verify_and_dangerously_skip_validation/2, parse/1, validate_claims/3, verify_and_validate/3, with_claim/3]).
-export_type([jwt_error/0, unverified/0, verified/0, claims/0, jwt/1, kid_policy/0, jwt_validation_options/0, verifier/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(
" JSON Web Token (JWT) - [RFC 7519](https://www.rfc-editor.org/rfc/rfc7519.html)\n"
"\n"
" This module provides JWT functionality built on top of JWS for signing\n"
" and verification. JWTs are a compact, URL-safe means of representing\n"
" claims to be transferred between two parties.\n"
"\n"
" ## Phantom Types\n"
"\n"
" JWT uses phantom types to enforce compile-time safety:\n"
" - `Jwt(Unverified)` - A JWT that has been parsed but not yet verified\n"
" - `Jwt(Verified)` - A JWT with verified signature, safe to trust claims\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import gleam/dynamic/decode\n"
" import gleam/time/duration\n"
" import gleam/time/timestamp\n"
" import gose/jwa\n"
" import gose/jwk\n"
" import gose/jwt\n"
"\n"
" let key = jwk.generate_hmac_key(jwa.HmacSha256)\n"
" let now = timestamp.system_time()\n"
"\n"
" // Create claims and sign\n"
" let claims = jwt.claims()\n"
" |> jwt.with_subject(\"user123\")\n"
" |> jwt.with_issuer(\"my-app\")\n"
" |> jwt.with_expiration(timestamp.add(now, duration.hours(1)))\n"
"\n"
" let assert Ok(signed) = jwt.sign(jwa.JwsHmac(jwa.HmacSha256), claims, key)\n"
" let token = jwt.serialize(signed)\n"
"\n"
" // Verify and validate using Verifier (enforces algorithm pinning)\n"
" let assert Ok(verifier) = jwt.verifier(jwa.JwsHmac(jwa.HmacSha256), [key], jwt.default_validation())\n"
" let assert Ok(verified) = jwt.verify_and_validate(verifier, token, now)\n"
"\n"
" // Decode verified claims\n"
" let decoder = {\n"
" use sub <- decode.field(\"sub\", decode.string)\n"
" decode.success(sub)\n"
" }\n"
" let assert Ok(subject) = jwt.decode(verified, decoder)\n"
" ```\n"
).
-type jwt_error() :: invalid_signature |
{decryption_failed, binary()} |
{token_expired, gleam@time@timestamp:timestamp()} |
{token_not_yet_valid, gleam@time@timestamp:timestamp()} |
missing_expiration |
missing_issued_at |
{issued_in_future, gleam@time@timestamp:timestamp()} |
{token_too_old, gleam@time@timestamp:timestamp(), integer()} |
{invalid_jti, binary()} |
{issuer_mismatch, binary(), gleam@option:option(binary())} |
{audience_mismatch, binary(), gleam@option:option(list(binary()))} |
{jws_algorithm_mismatch, gose@jwa:jws_alg(), gose@jwa:jws_alg()} |
{jwe_algorithm_mismatch,
gose@jwa:jwe_alg(),
gose@jwa:enc(),
gose@jwa:jwe_alg(),
gose@jwa:enc()} |
missing_kid |
{unknown_kid, binary()} |
{malformed_token, binary()} |
{claim_decoding_failed, binary()} |
{insecure_unprotected_header, binary()} |
{invalid_claim, binary()} |
{jose_error, gose:gose_error()}.
-type unverified() :: any().
-type verified() :: any().
-opaque claims() :: {claims,
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(list(binary())),
gleam@option:option(integer()),
gleam@option:option(integer()),
gleam@option:option(integer()),
gleam@option:option(binary()),
gleam@dict:dict(binary(), gleam@json:json())}.
-opaque jwt(AIVR) :: {jwt,
gose@jwa:jws_alg(),
gleam@option:option(binary()),
claims(),
binary()} |
{gleam_phantom, AIVR}.
-type kid_policy() :: no_kid_requirement | require_kid | require_kid_match.
-type jwt_validation_options() :: {jwt_validation_options,
gleam@option:option(binary()),
gleam@option:option(binary()),
integer(),
boolean(),
gleam@option:option(integer()),
gleam@option:option(fun((binary()) -> boolean())),
kid_policy()}.
-opaque verifier() :: {verifier,
gose@jwa:jws_alg(),
list(gose@jwk:jwk()),
jwt_validation_options()}.
-file("src/gose/jwt.gleam", 135).
?DOC(false).
-spec gose_error_to_malformed_token_error(gose:gose_error()) -> jwt_error().
gose_error_to_malformed_token_error(Err) ->
{malformed_token, gose:error_message(Err)}.
-file("src/gose/jwt.gleam", 229).
?DOC(
" Create default validation options.\n"
"\n"
" Default settings:\n"
" - No issuer validation\n"
" - No audience validation\n"
" - 60 seconds clock skew tolerance\n"
" - Expiration claim required\n"
" - No max token age\n"
" - No JWT ID validator\n"
" - No kid requirement (prioritizes matching keys but tries all)\n"
"\n"
" When an `iat` claim is present, it is always checked to ensure it is not\n"
" in the future (beyond clock skew), regardless of whether `max_token_age`\n"
" is configured.\n"
).
-spec default_validation() -> jwt_validation_options().
default_validation() ->
{jwt_validation_options,
none,
none,
60,
true,
none,
none,
no_kid_requirement}.
-file("src/gose/jwt.gleam", 262).
?DOC(
" Set a custom JWT ID (jti) validator.\n"
"\n"
" The validator function receives the `jti` claim value and should return\n"
" `True` if the ID is valid, `False` if it should be rejected.\n"
"\n"
" Common use cases:\n"
" - Check against a revocation list\n"
" - Verify the ID hasn't been seen before (replay prevention)\n"
" - Validate format/structure of the ID\n"
"\n"
" If the token has no `jti` claim, the validator is not called.\n"
"\n"
" ## Parameters\n"
"\n"
" - `options` - The validation options to update.\n"
" - `validator` - A function that receives the `jti` value and returns\n"
" `True` if valid.\n"
"\n"
" ## Returns\n"
"\n"
" The updated `JwtValidationOptions` with the custom jti validator.\n"
).
-spec with_jti_validator(jwt_validation_options(), fun((binary()) -> boolean())) -> jwt_validation_options().
with_jti_validator(Options, Validator) ->
{jwt_validation_options,
erlang:element(2, Options),
erlang:element(3, Options),
erlang:element(4, Options),
erlang:element(5, Options),
erlang:element(6, Options),
{some, Validator},
erlang:element(8, Options)}.
-file("src/gose/jwt.gleam", 283).
?DOC(
" Set the maximum token age in seconds.\n"
"\n"
" If set, tokens with an `iat` claim older than `now - max_age_seconds` will\n"
" be rejected with `TokenTooOld`. Requires the `iat` claim to be present.\n"
" Tokens without `iat` are rejected with `MissingIssuedAt`.\n"
"\n"
" ## Parameters\n"
"\n"
" - `options` - The validation options to update.\n"
" - `max_age_seconds` - The maximum allowed token age in seconds.\n"
"\n"
" ## Returns\n"
"\n"
" The updated `JwtValidationOptions` with the max token age set.\n"
).
-spec with_max_token_age(jwt_validation_options(), integer()) -> jwt_validation_options().
with_max_token_age(Options, Max_age_seconds) ->
{jwt_validation_options,
erlang:element(2, Options),
erlang:element(3, Options),
erlang:element(4, Options),
erlang:element(5, Options),
{some, Max_age_seconds},
erlang:element(7, Options),
erlang:element(8, Options)}.
-file("src/gose/jwt.gleam", 290).
-spec build_verifier(
gose@jwa:jws_alg(),
list(gose@jwk:jwk()),
jwt_validation_options()
) -> {ok, verifier()} | {error, gose:gose_error()}.
build_verifier(Alg, Keys, Options) ->
gose@internal@key_helpers:require_non_empty_keys(
Keys,
fun() ->
gleam@result:'try'(
gleam@list:try_each(
Keys,
fun(_capture) ->
gose@internal@key_helpers:validate_key_for_jws_verification(
Alg,
_capture
)
end
),
fun(_) -> {ok, {verifier, Alg, Keys, Options}} end
)
end
).
-file("src/gose/jwt.gleam", 352).
?DOC(
" Create a verifier for JWT signature verification and claim validation.\n"
"\n"
" Each verifier is pinned to a single algorithm. This prevents algorithm\n"
" confusion attacks where an attacker changes the `alg` header to trick\n"
" the verifier into using the wrong algorithm (see RFC 8725 Section 3.1).\n"
" For multi-algorithm scenarios (e.g., algorithm migration), create one\n"
" verifier per algorithm and try each in sequence:\n"
"\n"
" ```gleam\n"
" let assert Ok(rs_verifier) = jwt.verifier(\n"
" jwa.JwsRsaPkcs1(jwa.RsaPkcs1Sha256),\n"
" keys: rsa_keys,\n"
" options: jwt.default_validation(),\n"
" )\n"
" let assert Ok(ec_verifier) = jwt.verifier(\n"
" jwa.JwsEcdsa(jwa.EcdsaP256),\n"
" keys: ec_keys,\n"
" options: jwt.default_validation(),\n"
" )\n"
"\n"
" let result = case jwt.verify_and_validate(rs_verifier, token, now) {\n"
" Ok(verified) -> Ok(verified)\n"
" _ -> jwt.verify_and_validate(ec_verifier, token, now)\n"
" }\n"
" ```\n"
"\n"
" Accepts one or more keys for key rotation scenarios.\n"
"\n"
" Key selection during verification:\n"
" 1. If token has `kid` header, prioritize keys with matching kid\n"
" 2. Try keys in order until one succeeds\n"
" 3. Fail if no key verifies the signature\n"
"\n"
" Returns an error if:\n"
" - The key list is empty\n"
" - Any algorithm is incompatible with any key type\n"
" - Any key's `use` field is set but not `Signing`\n"
" - Any key's `key_ops` field is set but doesn't include `Verify`\n"
"\n"
" ## Parameters\n"
"\n"
" - `alg` - The expected JWS algorithm; tokens using a different algorithm are rejected.\n"
" - `keys` - One or more keys to try during verification (supports key rotation).\n"
" - `options` - Validation options controlling claim checks (expiration, issuer, etc.).\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(Verifier)` ready for use with `verify_and_validate`, or\n"
" `Error(JwtError)` if any key is incompatible with the algorithm or has\n"
" incorrect usage constraints.\n"
).
-spec verifier(
gose@jwa:jws_alg(),
list(gose@jwk:jwk()),
jwt_validation_options()
) -> {ok, verifier()} | {error, jwt_error()}.
verifier(Alg, Keys, Options) ->
_pipe = build_verifier(Alg, Keys, Options),
gleam@result:map_error(_pipe, fun(Field@0) -> {jose_error, Field@0} end).
-file("src/gose/jwt.gleam", 367).
?DOC(
" Create an empty claims set with no registered or custom claims.\n"
" Use the `with_*` functions to populate claims before signing.\n"
"\n"
" ## Returns\n"
"\n"
" An empty `Claims` value with no registered or custom claims set.\n"
).
-spec claims() -> claims().
claims() ->
{claims, none, none, none, none, none, none, none, maps:new()}.
-file("src/gose/jwt.gleam", 390).
?DOC(
" Set a single audience (aud) claim.\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to update.\n"
" - `aud` - The audience string.\n"
"\n"
" ## Returns\n"
"\n"
" The `Claims` with the `aud` claim set.\n"
).
-spec with_audience(claims(), binary()) -> claims().
with_audience(Claims, Aud) ->
{claims,
erlang:element(2, Claims),
erlang:element(3, Claims),
{some, [Aud]},
erlang:element(5, Claims),
erlang:element(6, Claims),
erlang:element(7, Claims),
erlang:element(8, Claims),
erlang:element(9, Claims)}.
-file("src/gose/jwt.gleam", 407).
?DOC(
" Set multiple audiences (aud) claim.\n"
"\n"
" Returns an error if the audience list is empty.\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to update.\n"
" - `aud` - The list of audience strings.\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(Claims)` with the audience list set, or `Error(JwtError)` if the\n"
" audience list is empty.\n"
).
-spec with_audiences(claims(), list(binary())) -> {ok, claims()} |
{error, jwt_error()}.
with_audiences(Claims, Aud) ->
case Aud of
[] ->
{error, {invalid_claim, <<"audience list cannot be empty"/utf8>>}};
_ ->
{ok,
{claims,
erlang:element(2, Claims),
erlang:element(3, Claims),
{some, Aud},
erlang:element(5, Claims),
erlang:element(6, Claims),
erlang:element(7, Claims),
erlang:element(8, Claims),
erlang:element(9, Claims)}}
end.
-file("src/gose/jwt.gleam", 454).
?DOC(
" Set the expiration time (exp) claim.\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to update.\n"
" - `exp` - The expiration timestamp.\n"
"\n"
" ## Returns\n"
"\n"
" The `Claims` with the `exp` claim set.\n"
).
-spec with_expiration(claims(), gleam@time@timestamp:timestamp()) -> claims().
with_expiration(Claims, Exp) ->
{Seconds, _} = gleam@time@timestamp:to_unix_seconds_and_nanoseconds(Exp),
{claims,
erlang:element(2, Claims),
erlang:element(3, Claims),
erlang:element(4, Claims),
{some, Seconds},
erlang:element(6, Claims),
erlang:element(7, Claims),
erlang:element(8, Claims),
erlang:element(9, Claims)}.
-file("src/gose/jwt.gleam", 469).
?DOC(
" Set the issued at time (iat) claim.\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to update.\n"
" - `iat` - The issued-at timestamp.\n"
"\n"
" ## Returns\n"
"\n"
" The `Claims` with the `iat` claim set.\n"
).
-spec with_issued_at(claims(), gleam@time@timestamp:timestamp()) -> claims().
with_issued_at(Claims, Iat) ->
{Seconds, _} = gleam@time@timestamp:to_unix_seconds_and_nanoseconds(Iat),
{claims,
erlang:element(2, Claims),
erlang:element(3, Claims),
erlang:element(4, Claims),
erlang:element(5, Claims),
erlang:element(6, Claims),
{some, Seconds},
erlang:element(8, Claims),
erlang:element(9, Claims)}.
-file("src/gose/jwt.gleam", 484).
?DOC(
" Set the issuer (iss) claim.\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to update.\n"
" - `iss` - The issuer string.\n"
"\n"
" ## Returns\n"
"\n"
" The `Claims` with the `iss` claim set.\n"
).
-spec with_issuer(claims(), binary()) -> claims().
with_issuer(Claims, Iss) ->
{claims,
{some, Iss},
erlang:element(3, Claims),
erlang:element(4, Claims),
erlang:element(5, Claims),
erlang:element(6, Claims),
erlang:element(7, Claims),
erlang:element(8, Claims),
erlang:element(9, Claims)}.
-file("src/gose/jwt.gleam", 498).
?DOC(
" Set the JWT ID (jti) claim.\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to update.\n"
" - `jti` - The unique token identifier.\n"
"\n"
" ## Returns\n"
"\n"
" The `Claims` with the `jti` claim set.\n"
).
-spec with_jwt_id(claims(), binary()) -> claims().
with_jwt_id(Claims, Jti) ->
{claims,
erlang:element(2, Claims),
erlang:element(3, Claims),
erlang:element(4, Claims),
erlang:element(5, Claims),
erlang:element(6, Claims),
erlang:element(7, Claims),
{some, Jti},
erlang:element(9, Claims)}.
-file("src/gose/jwt.gleam", 512).
?DOC(
" Set the not before time (nbf) claim.\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to update.\n"
" - `nbf` - The not-before timestamp.\n"
"\n"
" ## Returns\n"
"\n"
" The `Claims` with the `nbf` claim set.\n"
).
-spec with_not_before(claims(), gleam@time@timestamp:timestamp()) -> claims().
with_not_before(Claims, Nbf) ->
{Seconds, _} = gleam@time@timestamp:to_unix_seconds_and_nanoseconds(Nbf),
{claims,
erlang:element(2, Claims),
erlang:element(3, Claims),
erlang:element(4, Claims),
erlang:element(5, Claims),
{some, Seconds},
erlang:element(7, Claims),
erlang:element(8, Claims),
erlang:element(9, Claims)}.
-file("src/gose/jwt.gleam", 527).
?DOC(
" Set the subject (sub) claim.\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to update.\n"
" - `sub` - The subject string.\n"
"\n"
" ## Returns\n"
"\n"
" The `Claims` with the `sub` claim set.\n"
).
-spec with_subject(claims(), binary()) -> claims().
with_subject(Claims, Sub) ->
{claims,
erlang:element(2, Claims),
{some, Sub},
erlang:element(4, Claims),
erlang:element(5, Claims),
erlang:element(6, Claims),
erlang:element(7, Claims),
erlang:element(8, Claims),
erlang:element(9, Claims)}.
-file("src/gose/jwt.gleam", 540).
?DOC(
" Get the algorithm (`alg`) from a JWT.\n"
"\n"
" ## Parameters\n"
"\n"
" - `jwt` - The JWT to read the algorithm from.\n"
"\n"
" ## Returns\n"
"\n"
" The `JwsAlg` used to sign the token.\n"
).
-spec alg(jwt(any())) -> gose@jwa:jws_alg().
alg(Jwt) ->
{jwt, Alg, _, _, _} = Jwt,
Alg.
-file("src/gose/jwt.gleam", 558).
?DOC(
" Get the key ID (kid) from a JWT header.\n"
"\n"
" **Security Warning:** The `kid` value comes from the token and is untrusted\n"
" input. If you use it to look up keys (from a database, filesystem, or key\n"
" store), you must sanitize it first to prevent injection attacks.\n"
"\n"
" ## Parameters\n"
"\n"
" - `jwt` - The JWT to read the key ID from.\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(String)` with the key ID, or `Error(Nil)` if no `kid` is set.\n"
).
-spec kid(jwt(any())) -> {ok, binary()} | {error, nil}.
kid(Jwt) ->
{jwt, _, Kid, _, _} = Jwt,
gleam@option:to_result(Kid, nil).
-file("src/gose/jwt.gleam", 610).
-spec do_sign(
gose@jws:jws(gose@jws:unsigned(), gose@jws:built()),
gose@jwk:jwk(),
bitstring(),
gose@jwa:jws_alg(),
gleam@option:option(binary()),
claims()
) -> {ok, jwt(verified())} | {error, gose:gose_error()}.
do_sign(Unsigned, Key, Payload_bits, Alg, Kid, Claims) ->
gleam@result:'try'(
gose@jws:sign(Unsigned, Key, Payload_bits),
fun(Signed) -> _pipe = gose@jws:serialize_compact(Signed),
gleam@result:map(
_pipe,
fun(Token) -> {jwt, Alg, Kid, Claims, Token} end
) end
).
-file("src/gose/jwt.gleam", 623).
-spec apply_optional_kid(
gose@jws:jws(gose@jws:unsigned(), gose@jws:built()),
gleam@option:option(binary())
) -> gose@jws:jws(gose@jws:unsigned(), gose@jws:built()).
apply_optional_kid(Unsigned, Kid) ->
case Kid of
{some, K} ->
gose@jws:with_kid(Unsigned, K);
none ->
Unsigned
end.
-file("src/gose/jwt.gleam", 686).
-spec parse_jws(binary()) -> {ok,
gose@jws:jws(gose@jws:signed(), gose@jws:parsed())} |
{error, jwt_error()}.
parse_jws(Token) ->
_pipe = gose@jws:parse_compact(Token),
gleam@result:map_error(_pipe, fun gose_error_to_malformed_token_error/1).
-file("src/gose/jwt.gleam", 732).
?DOC(false).
-spec select_keys_by_policy(
list(gose@jwk:jwk()),
gleam@option:option(binary()),
kid_policy()
) -> {ok, list(gose@jwk:jwk())} | {error, jwt_error()}.
select_keys_by_policy(Keys, Token_kid, Kid_policy) ->
case {Token_kid, Kid_policy} of
{none, no_kid_requirement} ->
{ok, Keys};
{none, require_kid} ->
{error, missing_kid};
{none, require_kid_match} ->
{error, missing_kid};
{{some, Target}, require_kid_match} ->
Matching = gleam@list:filter(
Keys,
fun(Key) -> gose@jwk:kid(Key) =:= {ok, Target} end
),
case Matching of
[] ->
{error, {unknown_kid, Target}};
_ ->
{ok, Matching}
end;
{{some, Target@1}, require_kid} ->
{Matching@1, Others} = gleam@list:partition(
Keys,
fun(Key@1) -> gose@jwk:kid(Key@1) =:= {ok, Target@1} end
),
{ok, lists:append(Matching@1, Others)};
{{some, Target@1}, no_kid_requirement} ->
{Matching@1, Others} = gleam@list:partition(
Keys,
fun(Key@1) -> gose@jwk:kid(Key@1) =:= {ok, Target@1} end
),
{ok, lists:append(Matching@1, Others)}
end.
-file("src/gose/jwt.gleam", 757).
-spec try_verify_with_keys(
gose@jws:jws(gose@jws:signed(), gose@jws:parsed()),
gose@jwa:jws_alg(),
list(gose@jwk:jwk())
) -> {ok, nil} | {error, jwt_error()}.
try_verify_with_keys(Signed_jws, Expected_alg, Keys) ->
gleam@result:'try'(
begin
_pipe = gose@jws:verifier(Expected_alg, Keys),
gleam@result:map_error(
_pipe,
fun(Field@0) -> {jose_error, Field@0} end
)
end,
fun(Verifier) -> case gose@jws:verify(Verifier, Signed_jws) of
{ok, true} ->
{ok, nil};
{ok, false} ->
{error, invalid_signature};
{error, {crypto_error, _}} ->
{error, invalid_signature};
{error, {parse_error, Reason}} ->
{error, {malformed_token, Reason}};
{error, Err} ->
{error, {jose_error, Err}}
end end
).
-file("src/gose/jwt.gleam", 775).
-spec require_matching_algorithm(gose@jwa:jws_alg(), gose@jwa:jws_alg()) -> {ok,
nil} |
{error, jwt_error()}.
require_matching_algorithm(Expected, Actual) ->
case Expected =:= Actual of
true ->
{ok, nil};
false ->
{error, {jws_algorithm_mismatch, Expected, Actual}}
end.
-file("src/gose/jwt.gleam", 873).
?DOC(
" Serialize a verified JWT to compact format.\n"
"\n"
" ## Parameters\n"
"\n"
" - `jwt` - The verified JWT to serialize.\n"
"\n"
" ## Returns\n"
"\n"
" The JWT in compact format (`header.payload.signature`).\n"
).
-spec serialize(jwt(verified())) -> binary().
serialize(Jwt) ->
erlang:element(5, Jwt).
-file("src/gose/jwt.gleam", 887).
-spec claims_to_json(claims()) -> gleam@json:json().
claims_to_json(Claims) ->
Registered_fields = gleam@list:filter_map(
[gleam@option:map(
erlang:element(2, Claims),
fun(V) -> {<<"iss"/utf8>>, gleam@json:string(V)} end
),
gleam@option:map(
erlang:element(3, Claims),
fun(V@1) -> {<<"sub"/utf8>>, gleam@json:string(V@1)} end
),
gleam@option:map(
erlang:element(4, Claims),
fun(Auds) -> case Auds of
[Single] ->
{<<"aud"/utf8>>, gleam@json:string(Single)};
Multiple ->
{<<"aud"/utf8>>,
gleam@json:array(
Multiple,
fun gleam@json:string/1
)}
end end
),
gleam@option:map(
erlang:element(5, Claims),
fun(V@2) -> {<<"exp"/utf8>>, gleam@json:int(V@2)} end
),
gleam@option:map(
erlang:element(6, Claims),
fun(V@3) -> {<<"nbf"/utf8>>, gleam@json:int(V@3)} end
),
gleam@option:map(
erlang:element(7, Claims),
fun(V@4) -> {<<"iat"/utf8>>, gleam@json:int(V@4)} end
),
gleam@option:map(
erlang:element(8, Claims),
fun(V@5) -> {<<"jti"/utf8>>, gleam@json:string(V@5)} end
)],
fun(_capture) -> gleam@option:to_result(_capture, nil) end
),
Custom_fields = maps:to_list(erlang:element(9, Claims)),
gleam@json:object(lists:append(Registered_fields, Custom_fields)).
-file("src/gose/jwt.gleam", 590).
?DOC(
" Sign a JWT with the provided key.\n"
"\n"
" Automatically sets `typ: \"JWT\"` in the header.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let claims = jwt.claims()\n"
" |> jwt.with_subject(\"user123\")\n"
" |> jwt.with_expiration(exp)\n"
"\n"
" let assert Ok(signed) = jwt.sign(jwa.JwsHmac(jwa.HmacSha256), claims, key)\n"
" let token = jwt.serialize(signed)\n"
" ```\n"
"\n"
" ## Parameters\n"
"\n"
" - `alg` - The JWS algorithm to use for signing.\n"
" - `claims` - The claims set to include in the JWT payload.\n"
" - `key` - The signing key (must be compatible with the algorithm).\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(Jwt(Verified))` with the signed JWT ready to serialize with\n"
" `serialize()`, or `Error(JwtError)` if signing fails due to key\n"
" incompatibility or a crypto error. The token is marked `Verified`\n"
" because locally-signed tokens are implicitly trusted.\n"
).
-spec sign(gose@jwa:jws_alg(), claims(), gose@jwk:jwk()) -> {ok,
jwt(verified())} |
{error, jwt_error()}.
sign(Alg, Claims, Key) ->
Kid = gleam@option:from_result(gose@jwk:kid(Key)),
Payload = claims_to_json(Claims),
Payload_bits = begin
_pipe = gleam@json:to_string(Payload),
gleam_stdlib:identity(_pipe)
end,
Unsigned = begin
_pipe@1 = gose@jws:new(Alg),
_pipe@2 = gose@jws:with_typ(_pipe@1, <<"JWT"/utf8>>),
apply_optional_kid(_pipe@2, Kid)
end,
_pipe@3 = do_sign(Unsigned, Key, Payload_bits, Alg, Kid, Claims),
gleam@result:map_error(_pipe@3, fun(Field@0) -> {jose_error, Field@0} end).
-file("src/gose/jwt.gleam", 882).
?DOC(false).
-spec claims_to_json_string(claims()) -> binary().
claims_to_json_string(Claims) ->
_pipe = claims_to_json(Claims),
gleam@json:to_string(_pipe).
-file("src/gose/jwt.gleam", 1009).
-spec extract_payload_bits(binary()) -> {ok, bitstring()} | {error, jwt_error()}.
extract_payload_bits(Token) ->
_pipe = parse_jws(Token),
gleam@result:map(_pipe, fun gose@jws:payload/1).
-file("src/gose/jwt.gleam", 937).
?DOC(
" Decode an unverified JWT's claims using a custom decoder.\n"
"\n"
" **Warning:** These claims have not been verified. Do not trust them\n"
" until the JWT has been verified with `verify_and_validate`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let assert Ok(parsed) = jwt.parse(token)\n"
" let decoder = {\n"
" use iss <- decode.field(\"iss\", decode.string)\n"
" decode.success(iss)\n"
" }\n"
" let assert Ok(issuer) = jwt.dangerously_decode_unverified(parsed, decoder)\n"
" // issuer is untrusted - only use for routing/lookup, not authorization\n"
" ```\n"
"\n"
" ## Parameters\n"
"\n"
" - `jwt` - An unverified JWT obtained from `parse()`.\n"
" - `decoder` - A `gleam/dynamic/decode` decoder for extracting claims from the raw JSON payload.\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(a)` with the decoded value from the unverified claims, or\n"
" `Error(JwtError)` if the payload cannot be decoded.\n"
).
-spec dangerously_decode_unverified(
jwt(unverified()),
gleam@dynamic@decode:decoder(AIYI)
) -> {ok, AIYI} | {error, jwt_error()}.
dangerously_decode_unverified(Jwt, Decoder) ->
{jwt, _, _, _, Token} = Jwt,
gleam@result:'try'(
extract_payload_bits(Token),
fun(Payload_bits) ->
_pipe = gleam@json:parse_bits(Payload_bits, Decoder),
gleam@result:replace_error(
_pipe,
{claim_decoding_failed, <<"failed to decode claims"/utf8>>}
)
end
).
-file("src/gose/jwt.gleam", 973).
?DOC(
" Decode a verified JWT's claims using a custom decoder.\n"
"\n"
" This allows extracting claims directly into your own types using\n"
" `gleam/dynamic/decode`. The decoder receives the raw claims JSON.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let decoder = {\n"
" use sub <- decode.field(\"sub\", decode.string)\n"
" use role <- decode.field(\"role\", decode.string)\n"
" decode.success(User(sub:, role:))\n"
" }\n"
" let assert Ok(user) = jwt.decode(verified_jwt, decoder)\n"
" ```\n"
"\n"
" ## Parameters\n"
"\n"
" - `jwt` - A verified JWT obtained from `verify_and_validate` or `sign`.\n"
" - `decoder` - A `gleam/dynamic/decode` decoder for extracting claims from the raw JSON payload.\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(a)` with the decoded value from the verified claims, or\n"
" `Error(JwtError)` if the claims cannot be decoded with the provided\n"
" decoder.\n"
).
-spec decode(jwt(verified()), gleam@dynamic@decode:decoder(AIYN)) -> {ok, AIYN} |
{error, jwt_error()}.
decode(Jwt, Decoder) ->
gleam@result:'try'(
extract_payload_bits(erlang:element(5, Jwt)),
fun(Payload_bits) ->
_pipe = gleam@json:parse_bits(Payload_bits, Decoder),
gleam@result:replace_error(
_pipe,
{claim_decoding_failed, <<"failed to decode claims"/utf8>>}
)
end
).
-file("src/gose/jwt.gleam", 708).
-spec has_unprotected_alg(gose@jws:jws(gose@jws:signed(), gose@jws:parsed())) -> boolean().
has_unprotected_alg(Signed_jws) ->
gleam@bool:guard(
not gose@jws:has_unprotected_header(Signed_jws),
false,
fun() ->
Alg_decoder = begin
gleam@dynamic@decode:optional_field(
<<"alg"/utf8>>,
none,
gleam@dynamic@decode:optional(
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
),
fun(Alg) -> gleam@dynamic@decode:success(Alg) end
)
end,
case gose@jws:decode_unprotected_header(Signed_jws, Alg_decoder) of
{ok, {some, _}} ->
true;
_ ->
false
end
end
).
-file("src/gose/jwt.gleam", 693).
?DOC(
" Validate that a signed JWS is compatible with JWT requirements.\n"
" JWTs do not support detached payloads or unencoded payloads (b64=false).\n"
).
-spec require_jwt_compatible_jws(
gose@jws:jws(gose@jws:signed(), gose@jws:parsed())
) -> {ok, nil} | {error, jwt_error()}.
require_jwt_compatible_jws(Signed_jws) ->
gleam@bool:guard(
gose@jws:is_detached(Signed_jws),
{error,
{malformed_token, <<"JWTs do not support detached payloads"/utf8>>}},
fun() ->
case {gose@jws:has_unencoded_payload(Signed_jws),
has_unprotected_alg(Signed_jws)} of
{true, _} ->
{error,
{malformed_token,
<<"JWTs do not support unencoded payloads (b64=false)"/utf8>>}};
{_, true} ->
{error, {insecure_unprotected_header, <<"alg"/utf8>>}};
{_, _} ->
{ok, nil}
end
end
).
-file("src/gose/jwt.gleam", 1025).
-spec extract_optional_audience(
gleam@dict:dict(binary(), gleam@dynamic:dynamic_())
) -> {ok, gleam@option:option(list(binary()))} | {error, jwt_error()}.
extract_optional_audience(Fields) ->
case gleam_stdlib:map_get(Fields, <<"aud"/utf8>>) of
{ok, Value} ->
Audience_decoder = gleam@dynamic@decode:one_of(
gleam@dynamic@decode:list(
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
[gleam@dynamic@decode:map(
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun gleam@list:wrap/1
)]
),
case gleam@dynamic@decode:run(Value, Audience_decoder) of
{ok, []} ->
{error,
{malformed_token,
<<"aud claim cannot be an empty array"/utf8>>}};
{ok, Audiences} ->
{ok, {some, Audiences}};
{error, _} ->
{error,
{malformed_token,
<<"aud claim must be a string or array of strings"/utf8>>}}
end;
{error, _} ->
{ok, none}
end.
-file("src/gose/jwt.gleam", 1045).
-spec extract_optional_numeric_date(
gleam@dict:dict(binary(), gleam@dynamic:dynamic_()),
binary()
) -> {ok, gleam@option:option(integer())} | {error, jwt_error()}.
extract_optional_numeric_date(Fields, Key) ->
case gleam_stdlib:map_get(Fields, Key) of
{ok, Value} ->
Numeric_decoder = gleam@dynamic@decode:one_of(
{decoder, fun gleam@dynamic@decode:decode_int/1},
[gleam@dynamic@decode:map(
{decoder, fun gleam@dynamic@decode:decode_float/1},
fun erlang:trunc/1
)]
),
_pipe = gleam@dynamic@decode:run(Value, Numeric_decoder),
_pipe@1 = gleam@result:map(
_pipe,
fun(Field@0) -> {some, Field@0} end
),
gleam@result:replace_error(
_pipe@1,
{malformed_token,
<<Key/binary, " claim must be a numeric value"/utf8>>}
);
{error, _} ->
{ok, none}
end.
-file("src/gose/jwt.gleam", 1063).
-spec extract_optional_string(
gleam@dict:dict(binary(), gleam@dynamic:dynamic_()),
binary()
) -> {ok, gleam@option:option(binary())} | {error, jwt_error()}.
extract_optional_string(Fields, Key) ->
case gleam_stdlib:map_get(Fields, Key) of
{ok, Value} ->
_pipe = gleam@dynamic@decode:run(
Value,
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
_pipe@1 = gleam@result:map(
_pipe,
fun(Field@0) -> {some, Field@0} end
),
gleam@result:replace_error(
_pipe@1,
{malformed_token,
<<Key/binary, " claim must be a string"/utf8>>}
);
{error, _} ->
{ok, none}
end.
-file("src/gose/jwt.gleam", 1076).
-spec parse_claims_from_fields(
gleam@dict:dict(binary(), gleam@dynamic:dynamic_())
) -> {ok, claims()} | {error, jwt_error()}.
parse_claims_from_fields(All_fields) ->
gleam@result:'try'(
extract_optional_string(All_fields, <<"iss"/utf8>>),
fun(Iss) ->
gleam@result:'try'(
extract_optional_string(All_fields, <<"sub"/utf8>>),
fun(Sub) ->
gleam@result:'try'(
extract_optional_audience(All_fields),
fun(Aud) ->
gleam@result:'try'(
extract_optional_numeric_date(
All_fields,
<<"exp"/utf8>>
),
fun(Exp) ->
gleam@result:'try'(
extract_optional_numeric_date(
All_fields,
<<"nbf"/utf8>>
),
fun(Nbf) ->
gleam@result:'try'(
extract_optional_numeric_date(
All_fields,
<<"iat"/utf8>>
),
fun(Iat) ->
gleam@result:'try'(
extract_optional_string(
All_fields,
<<"jti"/utf8>>
),
fun(Jti) ->
{ok,
{claims,
Iss,
Sub,
Aud,
Exp,
Nbf,
Iat,
Jti,
maps:new()}}
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/gose/jwt.gleam", 1016).
?DOC(false).
-spec parse_claims_bits(bitstring()) -> {ok, claims()} | {error, jwt_error()}.
parse_claims_bits(Payload) ->
gleam@result:'try'(
begin
_pipe = gleam@json:parse_bits(
Payload,
gleam@dynamic@decode:dict(
{decoder, fun gleam@dynamic@decode:decode_string/1},
{decoder, fun gleam@dynamic@decode:decode_dynamic/1}
)
),
gleam@result:replace_error(
_pipe,
{malformed_token, <<"invalid claims JSON"/utf8>>}
)
end,
fun(All_fields) -> parse_claims_from_fields(All_fields) end
).
-file("src/gose/jwt.gleam", 831).
-spec build_verified_jwt(
gose@jws:jws(gose@jws:signed(), gose@jws:parsed()),
binary()
) -> {ok, jwt(verified())} | {error, jwt_error()}.
build_verified_jwt(Signed_jws, Token) ->
Payload = gose@jws:payload(Signed_jws),
gleam@result:'try'(
parse_claims_bits(Payload),
fun(Claims) ->
Alg = gose@jws:alg(Signed_jws),
Kid = gleam@option:from_result(gose@jws:kid(Signed_jws)),
{ok, {jwt, Alg, Kid, Claims, Token}}
end
).
-file("src/gose/jwt.gleam", 804).
?DOC(
" Verify a JWT's signature only, skipping all claim validation.\n"
"\n"
" **Warning:** This skips expiration, not-before, issuer, and audience checks.\n"
" Use only when you have a legitimate reason to bypass validation, such as\n"
" inspecting claims before deciding on validation policy.\n"
"\n"
" Still enforces algorithm pinning and `kid_policy` for security.\n"
" When multiple keys are configured, keys with matching `kid` are tried first.\n"
"\n"
" ## Parameters\n"
"\n"
" - `verifier` - A verifier created with `verifier()` that pins the algorithm and keys.\n"
" - `token` - The JWT compact-serialized string to verify.\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(Jwt(Verified))` with the verified JWT and unchecked claims, or\n"
" `Error(JwtError)` if signature verification fails or the algorithm\n"
" doesn't match.\n"
).
-spec verify_and_dangerously_skip_validation(verifier(), binary()) -> {ok,
jwt(verified())} |
{error, jwt_error()}.
verify_and_dangerously_skip_validation(Verifier, Token) ->
{verifier, Expected_alg, Keys, Options} = Verifier,
gleam@result:'try'(
parse_jws(Token),
fun(Signed_jws) ->
gleam@result:'try'(
require_jwt_compatible_jws(Signed_jws),
fun(_) ->
gleam@result:'try'(
require_matching_algorithm(
Expected_alg,
gose@jws:alg(Signed_jws)
),
fun(_) ->
Token_kid = gleam@option:from_result(
gose@jws:kid(Signed_jws)
),
gleam@result:'try'(
select_keys_by_policy(
Keys,
Token_kid,
erlang:element(8, Options)
),
fun(Verification_keys) ->
gleam@result:'try'(
try_verify_with_keys(
Signed_jws,
Expected_alg,
Verification_keys
),
fun(_) ->
build_verified_jwt(
Signed_jws,
Token
)
end
)
end
)
end
)
end
)
end
).
-file("src/gose/jwt.gleam", 996).
?DOC(
" Parse a JWT from compact format.\n"
"\n"
" Returns an unverified JWT that needs to be verified with\n"
" `verify_and_validate` or `verify_and_dangerously_skip_validation`.\n"
"\n"
" ## Parameters\n"
"\n"
" - `token` - The JWT compact-serialized string to parse.\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(Jwt(Unverified))` with the parsed but unverified JWT, or\n"
" `Error(JwtError)` if the token is malformed or uses unsupported JWT\n"
" features.\n"
).
-spec parse(binary()) -> {ok, jwt(unverified())} | {error, jwt_error()}.
parse(Token) ->
gleam@result:'try'(
parse_jws(Token),
fun(Signed) ->
gleam@result:'try'(
require_jwt_compatible_jws(Signed),
fun(_) ->
Payload = gose@jws:payload(Signed),
gleam@result:'try'(
parse_claims_bits(Payload),
fun(Claims) ->
Alg = gose@jws:alg(Signed),
Kid = gleam@option:from_result(gose@jws:kid(Signed)),
{ok, {jwt, Alg, Kid, Claims, Token}}
end
)
end
)
end
).
-file("src/gose/jwt.gleam", 1090).
-spec validate_audience(claims(), jwt_validation_options()) -> {ok, nil} |
{error, jwt_error()}.
validate_audience(Claims, Options) ->
case {erlang:element(3, Options), erlang:element(4, Claims)} of
{none, _} ->
{ok, nil};
{{some, Expected}, {some, Audiences}} ->
case gleam@list:contains(Audiences, Expected) of
true ->
{ok, nil};
false ->
{error, {audience_mismatch, Expected, {some, Audiences}}}
end;
{{some, Expected@1}, none} ->
{error, {audience_mismatch, Expected@1, none}}
end.
-file("src/gose/jwt.gleam", 1105).
-spec validate_exp(claims(), integer(), jwt_validation_options()) -> {ok, nil} |
{error, jwt_error()}.
validate_exp(Claims, Now_seconds, Options) ->
case {erlang:element(5, Claims), erlang:element(5, Options)} of
{none, true} ->
{error, missing_expiration};
{none, false} ->
{ok, nil};
{{some, Exp}, _} ->
Adjusted_now = Now_seconds - erlang:element(4, Options),
gleam@bool:guard(
Adjusted_now >= Exp,
{error,
{token_expired, gleam@time@timestamp:from_unix_seconds(Exp)}},
fun() -> {ok, nil} end
)
end.
-file("src/gose/jwt.gleam", 1139).
-spec validate_issuer(claims(), jwt_validation_options()) -> {ok, nil} |
{error, jwt_error()}.
validate_issuer(Claims, Options) ->
case {erlang:element(2, Options), erlang:element(2, Claims)} of
{none, _} ->
{ok, nil};
{{some, Expected}, {some, Actual}} when Expected =:= Actual ->
{ok, nil};
{{some, Expected@1}, Actual@1} ->
{error, {issuer_mismatch, Expected@1, Actual@1}}
end.
-file("src/gose/jwt.gleam", 1150).
-spec validate_jti(claims(), jwt_validation_options()) -> {ok, nil} |
{error, jwt_error()}.
validate_jti(Claims, Options) ->
case {erlang:element(7, Options), erlang:element(8, Claims)} of
{none, _} ->
{ok, nil};
{{some, _}, none} ->
{ok, nil};
{{some, Validator}, {some, Jti}} ->
gleam@bool:guard(
not Validator(Jti),
{error, {invalid_jti, Jti}},
fun() -> {ok, nil} end
)
end.
-file("src/gose/jwt.gleam", 1164).
-spec validate_nbf(claims(), integer(), jwt_validation_options()) -> {ok, nil} |
{error, jwt_error()}.
validate_nbf(Claims, Now_seconds, Options) ->
case erlang:element(6, Claims) of
none ->
{ok, nil};
{some, Nbf} ->
Adjusted_now = Now_seconds + erlang:element(4, Options),
gleam@bool:guard(
Adjusted_now < Nbf,
{error,
{token_not_yet_valid,
gleam@time@timestamp:from_unix_seconds(Nbf)}},
fun() -> {ok, nil} end
)
end.
-file("src/gose/jwt.gleam", 1182).
-spec validate_iat_not_future(integer(), integer(), jwt_validation_options()) -> {ok,
nil} |
{error, jwt_error()}.
validate_iat_not_future(Iat, Now_seconds, Options) ->
gleam@bool:guard(
Iat > (Now_seconds + erlang:element(4, Options)),
{error, {issued_in_future, gleam@time@timestamp:from_unix_seconds(Iat)}},
fun() -> {ok, nil} end
).
-file("src/gose/jwt.gleam", 1194).
-spec validate_token_age(integer(), integer(), jwt_validation_options()) -> {ok,
nil} |
{error, jwt_error()}.
validate_token_age(Iat, Now_seconds, Options) ->
case erlang:element(6, Options) of
none ->
{ok, nil};
{some, Max_age} ->
Token_age = Now_seconds - Iat,
gleam@bool:guard(
Token_age > Max_age,
{error,
{token_too_old,
gleam@time@timestamp:from_unix_seconds(Iat),
Max_age}},
fun() -> {ok, nil} end
)
end.
-file("src/gose/jwt.gleam", 1124).
-spec validate_iat(claims(), integer(), jwt_validation_options()) -> {ok, nil} |
{error, jwt_error()}.
validate_iat(Claims, Now_seconds, Options) ->
case {erlang:element(7, Claims), erlang:element(6, Options)} of
{none, {some, _}} ->
{error, missing_issued_at};
{none, none} ->
{ok, nil};
{{some, Iat}, _} ->
gleam@result:'try'(
validate_iat_not_future(Iat, Now_seconds, Options),
fun(_) -> validate_token_age(Iat, Now_seconds, Options) end
)
end.
-file("src/gose/jwt.gleam", 849).
?DOC(false).
-spec validate_claims(
claims(),
gleam@time@timestamp:timestamp(),
jwt_validation_options()
) -> {ok, nil} | {error, jwt_error()}.
validate_claims(Claims, Now, Options) ->
{Now_seconds, _} = gleam@time@timestamp:to_unix_seconds_and_nanoseconds(Now),
gleam@result:'try'(
validate_exp(Claims, Now_seconds, Options),
fun(_) ->
gleam@result:'try'(
validate_nbf(Claims, Now_seconds, Options),
fun(_) ->
gleam@result:'try'(
validate_issuer(Claims, Options),
fun(_) ->
gleam@result:'try'(
validate_audience(Claims, Options),
fun(_) ->
gleam@result:'try'(
validate_iat(
Claims,
Now_seconds,
Options
),
fun(_) ->
validate_jti(Claims, Options)
end
)
end
)
end
)
end
)
end
).
-file("src/gose/jwt.gleam", 656).
?DOC(
" Verify a JWT's signature and validate its claims using a Verifier.\n"
"\n"
" Checks:\n"
" 1. Token's `alg` header matches the verifier's expected algorithm\n"
" 2. Signature is valid for one of the verifier's keys\n"
" 3. Claims pass validation (exp, nbf, iss, aud per options)\n"
"\n"
" When multiple keys are configured:\n"
" - Keys with matching `kid` are tried first (if token has `kid` header)\n"
" - `kid_policy` controls kid header enforcement (see `KidPolicy` type)\n"
" - With `NoKidRequirement`, all keys are tried with matching keys prioritized\n"
"\n"
" ## Parameters\n"
"\n"
" - `verifier` - A verifier created with `verifier()` that pins the algorithm and keys.\n"
" - `token` - The JWT compact-serialized string to verify.\n"
" - `now` - The current timestamp, used for time-based claim validation.\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(Jwt(Verified))` with the verified JWT whose claims can be safely\n"
" trusted, or `Error(JwtError)` if signature verification or claim\n"
" validation fails.\n"
).
-spec verify_and_validate(
verifier(),
binary(),
gleam@time@timestamp:timestamp()
) -> {ok, jwt(verified())} | {error, jwt_error()}.
verify_and_validate(Verifier, Token, Now) ->
{verifier, Expected_alg, Keys, Options} = Verifier,
gleam@result:'try'(
parse_jws(Token),
fun(Signed_jws) ->
gleam@result:'try'(
require_jwt_compatible_jws(Signed_jws),
fun(_) ->
gleam@result:'try'(
require_matching_algorithm(
Expected_alg,
gose@jws:alg(Signed_jws)
),
fun(_) ->
Token_kid = gleam@option:from_result(
gose@jws:kid(Signed_jws)
),
gleam@result:'try'(
select_keys_by_policy(
Keys,
Token_kid,
erlang:element(8, Options)
),
fun(Verification_keys) ->
gleam@result:'try'(
try_verify_with_keys(
Signed_jws,
Expected_alg,
Verification_keys
),
fun(_) ->
gleam@result:'try'(
build_verified_jwt(
Signed_jws,
Token
),
fun(Jwt) ->
gleam@result:'try'(
validate_claims(
erlang:element(
4,
Jwt
),
Now,
Options
),
fun(_) -> {ok, Jwt} end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/gose/jwt.gleam", 432).
?DOC(
" Set a custom claim.\n"
"\n"
" Returns an error if the key is a reserved claim name. Use the dedicated\n"
" setters for registered claims (e.g., `with_issuer`, `with_subject`).\n"
"\n"
" ## Parameters\n"
"\n"
" - `claims` - The claims set to add the custom claim to.\n"
" - `key` - The claim name (must not be a reserved claim like \"iss\", \"sub\", etc.).\n"
" - `value` - The JSON value for the claim.\n"
"\n"
" ## Returns\n"
"\n"
" `Ok(Claims)` with the custom claim added, or `Error(JwtError)` if the\n"
" key is a reserved claim name.\n"
).
-spec with_claim(claims(), binary(), gleam@json:json()) -> {ok, claims()} |
{error, jwt_error()}.
with_claim(Claims, Key, Value) ->
case gleam@list:contains(
[<<"iss"/utf8>>,
<<"sub"/utf8>>,
<<"aud"/utf8>>,
<<"exp"/utf8>>,
<<"nbf"/utf8>>,
<<"iat"/utf8>>,
<<"jti"/utf8>>],
Key
) of
true ->
{error,
{invalid_claim,
<<<<"use dedicated setter for "/utf8, Key/binary>>/binary,
" claim"/utf8>>}};
false ->
{ok,
{claims,
erlang:element(2, Claims),
erlang:element(3, Claims),
erlang:element(4, Claims),
erlang:element(5, Claims),
erlang:element(6, Claims),
erlang:element(7, Claims),
erlang:element(8, Claims),
gleam@dict:insert(erlang:element(9, Claims), Key, Value)}}
end.