//// //// import gleam/bit_array import gleam/dynamic/decode.{type Decoder} import gleam/javascript/promise.{type Promise, await, resolve} import gleam/json.{type Json} import gleam/time/timestamp import ywt/algorithm.{type Algorithm} import ywt/claim.{type Claim} import ywt/internal/core import ywt/internal/jwt import ywt/sign_key.{type SignKey} import ywt/verify_key.{type VerifyKey} /// Detailed error information for JWT parsing failures. /// /// This provides specific information about why JWT validation failed, enabling /// better error handling, logging, and debugging. Different error types allow /// you to respond appropriately - for example, expired tokens might trigger /// token refresh, while signature failures indicate potential attacks. /// /// ## Error Categories /// - **Format Errors**: Malformed JWT structure or encoding issues /// - **Signature Errors**: Cryptographic validation failures /// - **Claim Errors**: Business logic validation failures /// - **Key Errors**: Issues with cryptographic keys /// /// ## Usage Example /// ```gleam /// use decoded <- promise.await(ywt.decode(jwt, using: decoder, claims: claims, keys: keys)) /// case decoded { /// Ok(payload) -> { /// // Success - use the payload /// next(payload) /// } /// Error(TokenExpired(expired_at)) -> { /// // Handle expired token - maybe refresh /// log.info("Token expired at: " <> timestamp.to_string(expired_at)) /// redirect_to_refresh() /// } /// Error(InvalidSignature) -> { /// // Potential security issue - log and reject /// log.warning("Invalid JWT signature detected") /// unauthorized() /// } /// Error(InvalidIssuer(expected, actual)) -> { /// // Wrong issuer - reject but log for debugging /// log.error("Wrong issuer - expected: " <> expected <> ", got: " <> actual) /// forbidden() /// } /// Error(_) -> { /// // Generic handling for other errors /// bad_request() /// } /// } /// ``` pub type ParseError { // Format and structure errors /// JWT doesn't contain exactly 3 parts separated by dots MalformedToken /// Base64 decoding failed for header InvalidHeaderEncoding /// Base64 decoding failed for payload InvalidPayloadEncoding /// Base64 decoding failed for signature InvalidSignatureEncoding /// JSON parsing or decoding failed for header InvalidHeaderJson(json.DecodeError) /// JSON parsing failed for payload - note that this does not include /// failures for your payload decoder. See `PayloadDecodingError`. InvalidPayloadJson(json.DecodeError) // Cryptographic errors /// No suitable key found to verify the signature NoMatchingKey /// Signature verification failed (potential tampering) InvalidSignature // Key has invalid parameters or format //InvalidKey(details: String) // // Claim validation errors /// Token has expired TokenExpired(expired_at: timestamp.Timestamp) /// Token is not yet valid (nbf claim) TokenNotYetValid(not_before: timestamp.Timestamp) /// Wrong issuer InvalidIssuer(expected: List(String), actual: String) /// Wrong audience InvalidAudience(expected: List(String), actual: String) /// Wrong audience InvalidSubject(expected: List(String), actual: String) // Wroing JWT ID InvalidId(expected: List(String), actual: String) /// Required claim is missing MissingClaim(claim_name: String) /// Claim Decoding Error ClaimDecodingError(claim_name: String, error: List(decode.DecodeError)) /// InvalidCustomClaim(claim_name: String) // Payload decoding errors /// Payload structure doesn't match expected decoder PayloadDecodingError(List(decode.DecodeError)) } fn from_core_error(error: core.ParseError) -> ParseError { case error { core.ClaimDecodingError(claim_name:, error:) -> ClaimDecodingError(claim_name:, error:) core.InvalidAudience(expected:, actual:) -> InvalidAudience(expected:, actual:) core.InvalidCustomClaim(claim_name:) -> InvalidCustomClaim(claim_name:) core.InvalidHeaderEncoding -> InvalidHeaderEncoding core.InvalidHeaderJson(error) -> InvalidHeaderJson(error) core.InvalidId(expected:, actual:) -> InvalidId(expected:, actual:) core.InvalidIssuer(expected:, actual:) -> InvalidIssuer(expected:, actual:) core.InvalidPayloadEncoding -> InvalidPayloadEncoding core.InvalidPayloadJson(error) -> InvalidPayloadJson(error) core.InvalidSignature -> InvalidSignature core.InvalidSignatureEncoding -> InvalidSignatureEncoding core.InvalidSubject(expected:, actual:) -> InvalidSubject(expected:, actual:) core.MalformedToken -> MalformedToken core.MissingClaim(claim_name:) -> MissingClaim(claim_name:) core.NoMatchingKey -> NoMatchingKey core.PayloadDecodingError(error) -> PayloadDecodingError(error) core.TokenExpired(expired_at:) -> TokenExpired(expired_at:) core.TokenNotYetValid(not_before:) -> TokenNotYetValid(not_before:) } } /// Generates a new cryptographically secure signing key for the specified algorithm. /// /// This function creates fresh signing keys with appropriate parameters for each /// algorithm type. All keys are generated using cryptographically secure random /// number generators and follow current security best practices. /// /// The generated keys will have a random id set that can be used to identify the key /// during key rotation. /// /// ## Usage /// Use this for key generation in development, testing, or when implementing /// key rotation systems. For production use, consider generating keys offline /// and storing them in secure key management systems. /// /// ## Key Parameters /// - **HMAC**: Full-entropy random secrets of appropriate length (at least 32/48/64 bytes) /// - **RSA**: 4096-bit modulus with secure random prime generation /// - **ECDSA**: Secure random private scalars on specified curves. The digest type matches the selected curve. pub fn generate_key(algorithm: Algorithm) -> Promise(SignKey) { use key <- await(algorithm.generate_key( algorithm, generate_hmac, generate_ecdsa, generate_rsa, )) resolve(sign_key.with_random_id(key)) } @external(javascript, "./ywt_ffi.mjs", "generate_ecdsa") fn generate_ecdsa( curve: core.NamedCurve, digest: core.DigestType, ) -> Promise(SignKey) @external(javascript, "./ywt_ffi.mjs", "generate_hmac") fn generate_hmac(digest: core.DigestType) -> Promise(SignKey) @external(javascript, "./ywt_ffi.mjs", "generate_rsa") fn generate_rsa( digest: core.DigestType, key_length: Int, padding: core.Padding, ) -> Promise(SignKey) /// Creates a cryptographic signature for the given message using the specified signing key. /// /// This is a low-level function that performs the actual cryptographic signing operation. /// /// ## Usage /// This function is primarily used internally by higher-level JWT functions. Use this /// directly only when you need raw signature operations outside of JWT context. /// /// ## Security Considerations /// - Never sign untrusted or unvalidated input data /// - Ensure signing keys are stored securely and accessed only by authorized code /// /// ## Example /// ```gleam /// let message = <<"Hello, world!":utf8>> /// let signing_key = load_secure_signing_key() /// /// use signature <- promise.await(ywt.sign_bits(message, signing_key)) /// // signature is raw bytes that can be verified with corresponding verify key /// ``` /// /// ⚠️ **Warning:** This function performs raw cryptographic operations. Most applications /// should use the higher-level JWT functions instead. @external(javascript, "./ywt_ffi.mjs", "sign") pub fn sign_bits(message: BitArray, key: SignKey) -> Promise(BitArray) /// Creates a base64url-encoded signature for a UTF-8 string message. /// /// This is a convenience wrapper around `sign_bits` that handles string encoding /// and base64url encoding of the signature, commonly used for signing JWT components. /// /// ## Usage /// Use this when you need to sign string data and want the signature in base64url /// format for use in web contexts like JWTs or HTTP headers. /// /// ## Security Considerations /// - Same security considerations as `sign_bits` apply /// - The string is encoded as UTF-8 before signing /// - Verify signatures using the corresponding `verify_string` function /// /// ## Example /// ```gleam /// let jwt_payload = "eyJzdWIiOiIxMjM0NTY3ODkwIn0" /// let signing_key = load_jwt_signing_key() /// /// use signature <- promise.await(sign_string(jwt_payload, signing_key))) /// // signature is base64url-encoded string ready for JWT use /// ``` /// /// 💡 **Note:** The returned signature is base64url-encoded (URL-safe, no padding) /// as required by JWT and other web standards. pub fn sign_string(message: String, key: SignKey) -> Promise(String) { use bits <- await(sign_bits(<>, key)) resolve(bit_array.base64_url_encode(bits, False)) } /// Verifies a cryptographic signature against a message using the specified verification key. /// /// This is a low-level function that performs the actual cryptographic verification. /// Returns `True` if the signature is valid for the given message and key, `False` otherwise. /// /// ## Usage /// Use this for raw signature verification operations. The verification algorithm /// is determined by the key type and must match the algorithm used for signing. /// /// ## Example /// ```gleam /// let message = <<"Hello, world!":utf8>> /// let signature = // ... received signature bytes /// let verify_key = load_verification_key() /// /// use is_valid <- promise.await(verify_bits(message, signature, verify_key)) /// case is_valid { /// True -> process_verified_message(message) /// False -> reject_invalid_signature() /// } /// ``` /// /// 🔒 **Critical:** Never trust data with invalid signatures. A `False` result /// indicates potential tampering or use of wrong keys. @external(javascript, "./ywt_ffi.mjs", "verify") pub fn verify_bits( message: BitArray, signature: BitArray, key: VerifyKey, ) -> Promise(Bool) /// Verifies a base64url-encoded signature against a UTF-8 string message. /// /// This is a convenience wrapper around `verify_bits` that handles string encoding /// and base64url decoding of the signature. Commonly used for verifying JWT signatures. /// /// ## Usage /// Use this when verifying signatures that are base64url-encoded, such as those /// from JWTs or other web-based cryptographic protocols. /// /// ## Security Considerations /// - Same security considerations as `verify_bits` apply /// - Invalid base64url encoding in the signature automatically returns `False` /// /// ## Example /// ```gleam /// let payload = "eyJzdWIiOiIxMjM0NTY3ODkwIn0" /// let signature = "base64url-encoded-signature-string" /// let verify_key = load_jwt_verification_key() /// /// use is_valid <- promise.await(verify_string(payload, signature, verify_key)) /// case is_valid { /// True -> { /// // Signature is valid, payload can be trusted /// process_authenticated_request(payload) /// } /// False -> { /// // Signature invalid - reject the request /// return_authentication_error() /// } /// } /// ``` /// /// ⚠️ **Important:** Malformed base64url signatures return `False` rather than /// causing errors, ensuring consistent handling of invalid input. pub fn verify_string( message: String, signature: String, key: VerifyKey, ) -> Promise(Bool) { case bit_array.base64_url_decode(signature) { Ok(signature) -> verify_bits(<>, signature, key) Error(_) -> resolve(False) } } /// Extracts payload from a JWT without verifying its signature or claims. /// /// 🚨 **USE WITH EXTREME CAUTION** Only use this when you need to inspect tokens /// from trusted sources where signature verification is handled elsewhere. /// /// ## Security Considerations /// - **NEVER use this in production for authentication** /// - Tokens could be forged or tampered with /// - No expiration or claim validation is performed /// - Only use when signature validation happens at a different layer /// - Consider this as dangerous as accepting any user input pub fn decode_unsafely_without_validation( jwt: String, payload_decoder: Decoder(payload), ) -> Result(payload, Nil) { jwt.decode_unsafely_without_validation(jwt, payload_decoder) } /// Verifies a JWT signature and validates all claims, returning the decoded payload if successful. /// /// Use this to validate incoming JWTs from clients, ensuring they're authentic and /// haven't been tampered with. /// /// ## Security Considerations /// - Implement appropriate claims validation (expiration, issuer, audience) /// - Handle verification failures securely (don't leak information) /// - Consider rate limiting to prevent brute force attacks /// /// ## Example /// ```gleam /// // Define expected claims for validation /// let claims = [ /// claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)), /// claim.issuer("my-app", []), /// ] /// /// // Parse and validate the JWT /// use decoded <- promise.await( /// ywt.decode(jwt_token, using: payload_decoder, claims:, keys: [verify_key]) /// ) /// case decoded { /// Ok(payload) -> { /// // JWT is valid, use the payload /// let user_id = // extract user ID from payload /// authorize_request(user_id) /// } /// Error(_) -> { /// // JWT is invalid - reject the request /// unauthorized_response() /// } /// } /// ``` /// /// 💡 **Best Practice:** Always validate JWTs on every request and never trust /// client-provided tokens without verification. pub fn decode( jwt jwt: String, using decoder: Decoder(payload), claims claims: List(Claim), keys keys: List(VerifyKey), ) -> Promise(Result(payload, ParseError)) { let verify = fn(message, signature, key, next) { use result <- await(verify_bits(message, signature, key)) next(result) } let resolve = fn(result) { case result { Ok(result) -> resolve(Ok(result)) Error(error) -> resolve(Error(from_core_error(error))) } } jwt.decode(jwt:, using: decoder, claims:, keys:, verify:, resolve:) } /// Creates a signed JWT containing the specified payload and claims. /// /// Signed JWTs prevent actors without access to the SignKey from modifying it. /// This can be verified using the corresponding VerifyKey. /// /// ## Security Considerations /// - JWTs are signed, not encrypted - all data is publicly readable /// - Never include sensitive data like passwords or personal information /// - Keep payloads small to avoid large tokens /// - Include appropriate expiration times in your claims /// - Use strong signing keys and rotate them regularly /// /// If a field is present in claims as well as the payload, the payload takes /// precedence. /// /// ## Example /// ```gleam /// // Create a user session token /// let payload = [ /// #("sub", json.string("user_12345")), /// #("role", json.string("developer")), /// #("permissions", json.array([json.string("read"), json.string("write")])) /// ] /// /// let claims = [ /// claim.issued_at(), /// claim.expires_at(max_age: duration.hours(1), leeway: duration.minutes(5)), /// claim.issuer("my-app", []), /// ] /// /// use jwt <- promise.await(ywt.encode(payload: payload, claims: claims, key: sign_key)) /// // Returns: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ..." /// ``` /// /// ⚠️ **Remember:** Anyone can decode and read JWT contents. Only include data /// you're comfortable being public. pub fn encode( payload payload: List(#(String, Json)), claims claims: List(Claim), key key: SignKey, ) -> Promise(String) { let sign = fn(message, key, next) { use signature <- await(sign_bits(message, key)) resolve(next(signature)) } jwt.encode(payload:, claims:, key:, sign:) }