|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module JWT |
| 4 | + # Provides functionality for creating and decoding Nested JWTs |
| 5 | + # as defined in RFC 7519 Section 5.2, Section 7.1 Step 5, and Appendix A.2. |
| 6 | + # |
| 7 | + # A Nested JWT is a JWT that is used as the payload of another JWT, |
| 8 | + # allowing for multiple layers of signing or encryption. |
| 9 | + # |
| 10 | + # @example Creating a Nested JWT |
| 11 | + # inner_jwt = JWT.encode({ user_id: 123 }, 'inner_secret', 'HS256') |
| 12 | + # nested_jwt = JWT::NestedToken.sign( |
| 13 | + # inner_jwt, |
| 14 | + # algorithm: 'RS256', |
| 15 | + # key: rsa_private_key |
| 16 | + # ) |
| 17 | + # |
| 18 | + # @example Decoding a Nested JWT |
| 19 | + # tokens = JWT::NestedToken.decode( |
| 20 | + # nested_jwt, |
| 21 | + # keys: [ |
| 22 | + # { algorithm: 'RS256', key: rsa_public_key }, |
| 23 | + # { algorithm: 'HS256', key: 'inner_secret' } |
| 24 | + # ] |
| 25 | + # ) |
| 26 | + # inner_payload = tokens.last.payload |
| 27 | + # |
| 28 | + # @see https://datatracker.ietf.org/doc/html/rfc7519#section-5.2 RFC 7519 Section 5.2 |
| 29 | + class NestedToken |
| 30 | + # The content type header value for nested JWTs as per RFC 7519 |
| 31 | + CTY_JWT = 'JWT' |
| 32 | + |
| 33 | + class << self |
| 34 | + # Wraps an inner JWT with an outer JWS, creating a Nested JWT. |
| 35 | + # Automatically sets the `cty` (content type) header to "JWT" as required by RFC 7519. |
| 36 | + # |
| 37 | + # @param inner_jwt [String] the inner JWT string to wrap |
| 38 | + # @param algorithm [String] the signing algorithm for the outer JWS (e.g., 'RS256', 'HS256') |
| 39 | + # @param key [Object] the signing key for the outer JWS |
| 40 | + # @param header [Hash] additional header fields to include (cty is automatically set) |
| 41 | + # @return [String] the Nested JWT string |
| 42 | + # |
| 43 | + # @raise [JWT::EncodeError] if signing fails |
| 44 | + # |
| 45 | + # @example Basic usage with HS256 |
| 46 | + # inner_jwt = JWT.encode({ sub: 'user' }, 'secret', 'HS256') |
| 47 | + # nested = JWT::NestedToken.sign(inner_jwt, algorithm: 'HS256', key: 'outer_secret') |
| 48 | + # |
| 49 | + # @example With RSA and custom headers |
| 50 | + # nested = JWT::NestedToken.sign( |
| 51 | + # inner_jwt, |
| 52 | + # algorithm: 'RS256', |
| 53 | + # key: rsa_private_key, |
| 54 | + # header: { kid: 'my-key-id' } |
| 55 | + # ) |
| 56 | + def sign(inner_jwt, algorithm:, key:, header: {}) |
| 57 | + outer_header = header.merge('cty' => CTY_JWT) |
| 58 | + token = Token.new(payload: inner_jwt, header: outer_header) |
| 59 | + token.sign!(algorithm: algorithm, key: key) |
| 60 | + token.jwt |
| 61 | + end |
| 62 | + |
| 63 | + # Decodes and verifies a Nested JWT, unwrapping all nesting levels. |
| 64 | + # Each level's signature is verified using the corresponding key configuration. |
| 65 | + # |
| 66 | + # @param token [String] the Nested JWT string to decode |
| 67 | + # @param keys [Array<Hash>] an array of key configurations for each nesting level, |
| 68 | + # ordered from outermost to innermost. Each hash should contain: |
| 69 | + # - `:algorithm` [String] the expected algorithm |
| 70 | + # - `:key` [Object] the verification key |
| 71 | + # @return [Array<JWT::EncodedToken>] array of tokens from outermost to innermost |
| 72 | + # |
| 73 | + # @raise [JWT::DecodeError] if decoding fails at any level |
| 74 | + # @raise [JWT::VerificationError] if signature verification fails at any level |
| 75 | + # |
| 76 | + # @example Decoding a two-level nested JWT |
| 77 | + # tokens = JWT::NestedToken.decode( |
| 78 | + # nested_jwt, |
| 79 | + # keys: [ |
| 80 | + # { algorithm: 'RS256', key: rsa_public_key }, |
| 81 | + # { algorithm: 'HS256', key: 'inner_secret' } |
| 82 | + # ] |
| 83 | + # ) |
| 84 | + # inner_token = tokens.last |
| 85 | + # inner_token.payload # => { 'user_id' => 123 } |
| 86 | + def decode(token, keys:) |
| 87 | + tokens = [] |
| 88 | + current_token = token |
| 89 | + |
| 90 | + keys.each_with_index do |key_config, index| |
| 91 | + encoded_token = EncodedToken.new(current_token) |
| 92 | + encoded_token.verify_signature!( |
| 93 | + algorithm: key_config[:algorithm], |
| 94 | + key: key_config[:key] |
| 95 | + ) |
| 96 | + |
| 97 | + tokens << encoded_token |
| 98 | + |
| 99 | + if encoded_token.nested? |
| 100 | + current_token = encoded_token.unverified_payload |
| 101 | + elsif index < keys.length - 1 |
| 102 | + raise JWT::DecodeError, 'Token is not nested but more keys were provided' |
| 103 | + end |
| 104 | + end |
| 105 | + |
| 106 | + tokens.each(&:verify_claims!) |
| 107 | + tokens |
| 108 | + end |
| 109 | + end |
| 110 | + end |
| 111 | +end |
0 commit comments