Module: Ruby::Rego::Builtins::Jwt

Extended by:
RegistryHelpers
Defined in:
lib/ruby/rego/builtins/jwt.rb,
lib/ruby/rego/builtins/jwt/jwk.rb,
lib/ruby/rego/builtins/jwt/sign.rb,
lib/ruby/rego/builtins/jwt/verify.rb,
lib/ruby/rego/builtins/jwt/decode_verify.rb

Overview

io.jwt.decode_verify(jwt, constraints): verify a compact JWS against a key AND a set of standard claim constraints, returning [valid, header, payload] (mirroring OPA’s builtinJWTDecodeVerify). valid is true only when the signature verifies and every applicable constraint holds; otherwise the result is [false, {}, {}]. The outcome is THREE-way, like verify_: * undefined — the constraints object or the token is structurally unusable: a token that is not three base64url segments whose header/payload are JSON objects and whose signature is base64url (a payload exp/nbf that is not a number is also undefined); a constraints value that is not an object, carries an unknown key, lacks exactly one of secret/cert, has a non-string secret/cert/alg/iss/aud, an empty or unparseable cert, or a time that is not a number >= 0. * [false, {}, {}] — the token and constraints are well-formed but a check fails: signature does not verify, the header alg is unsupported/”none” or does not match a constraint alg, the scheme’s key is absent (HMAC needs secret, asymmetric needs cert), exp/nbf/iss/aud do not hold. * [true, header, payload] — verified and all constraints satisfied. exp/nbf are seconds (JWT NumericDate); the time constraint is nanoseconds (default: now), and OPA compares time_ns against the claim * 1e9 — valid while time < exp1e9 and time >= nbf1e9. An aud claim requires a matching aud constraint and vice versa (a string aud matches by equality, an array aud by membership). The signature path reuses verify_’s scheme machinery keyed by the header alg (JWS_ALGORITHMS), and the token/key parsing reuses io.jwt.decode / Jwt::Jwk.

Divergence (upstream bug, see opa-builtin-upstream-bugs): OPA panics on a non-string iss claim (number or boolean) when an iss constraint is given (ast type assertion vs ast.String); the gem stays total and returns false (a non-string iss never equals a string constraint).

Defined Under Namespace

Modules: Jwk

Constant Summary collapse

JWT_FUNCTIONS =
{
  "io.jwt.decode" => { arity: 1, handler: :decode }
}.freeze
JWS_ALGORITHMS =

JWS alg header value -> { digest, scheme, kty }. Keyed by the on-the-wire alg name (distinct from VERIFY_ALGORITHMS’s builtin-name keys, same underlying data).

{
  "HS256" => { digest: "SHA256", scheme: :hmac, kty: "oct" },
  "HS384" => { digest: "SHA384", scheme: :hmac, kty: "oct" },
  "HS512" => { digest: "SHA512", scheme: :hmac, kty: "oct" },
  "RS256" => { digest: "SHA256", scheme: :rsa, kty: "RSA" },
  "RS384" => { digest: "SHA384", scheme: :rsa, kty: "RSA" },
  "RS512" => { digest: "SHA512", scheme: :rsa, kty: "RSA" },
  "PS256" => { digest: "SHA256", scheme: :pss, kty: "RSA" },
  "PS384" => { digest: "SHA384", scheme: :pss, kty: "RSA" },
  "PS512" => { digest: "SHA512", scheme: :pss, kty: "RSA" },
  "ES256" => { digest: "SHA256", scheme: :ecdsa, kty: "EC" },
  "ES384" => { digest: "SHA384", scheme: :ecdsa, kty: "EC" },
  "ES512" => { digest: "SHA512", scheme: :ecdsa, kty: "EC" },
  "EdDSA" => { digest: nil, scheme: :eddsa, kty: "OKP" }
}.freeze
ENCODE_FUNCTIONS =
{
  "io.jwt.encode_sign" => { arity: 3, handler: :encode_sign },
  "io.jwt.encode_sign_raw" => { arity: 3, handler: :encode_sign_raw }
}.freeze
SIGNERS =

scheme -> the signer for one asymmetric private key.

{ rsa: :rsa_sign, pss: :pss_sign, ecdsa: :ecdsa_sign, eddsa: :eddsa_sign }.freeze
ENCODE_CONTEXT =

The context for an undefined raised in the shared encode/sign path (front-ends use their own builtin name for their input validation); the value only feeds the internal error message.

"io.jwt.encode_sign"
JSON_STRING_TOKEN =

One JSON string literal: a quote, any run of escapes (\.) or non-quote/non-backslash bytes, a quote. Shared by every JSON scan below — first_alg’s value reader, the container skipper, and the comment gate — so each consumes a string token whole and never mistakes its contents for syntax.

/"(?:\\.|[^"\\])*"/
DUP_KEY_SUPPORTED =

json >= 2.14 added the allow_duplicate_key keyword; the gemspec allows json ~> 2.0, where the keyword is unknown and JSON.parse would raise ArgumentError (escaping the totality boundary). Probe once so the validity gate can keep a repeated key valid on json that supports it (OPA accepts dup keys; first_alg selects, and json 3.0 would otherwise raise) and fall back to the plain parse on older json, whose default already accepts duplicate keys.

begin
  JSON.parse("{}", allow_duplicate_key: true)
  true
rescue ::ArgumentError
  false
end
VERIFY_ALGORITHMS =
{
  "io.jwt.verify_hs256" => { digest: "SHA256", scheme: :hmac },
  "io.jwt.verify_hs384" => { digest: "SHA384", scheme: :hmac },
  "io.jwt.verify_hs512" => { digest: "SHA512", scheme: :hmac },
  "io.jwt.verify_rs256" => { digest: "SHA256", scheme: :rsa },
  "io.jwt.verify_rs384" => { digest: "SHA384", scheme: :rsa },
  "io.jwt.verify_rs512" => { digest: "SHA512", scheme: :rsa },
  "io.jwt.verify_ps256" => { digest: "SHA256", scheme: :pss },
  "io.jwt.verify_ps384" => { digest: "SHA384", scheme: :pss },
  "io.jwt.verify_ps512" => { digest: "SHA512", scheme: :pss },
  "io.jwt.verify_es256" => { digest: "SHA256", scheme: :ecdsa },
  "io.jwt.verify_es384" => { digest: "SHA384", scheme: :ecdsa },
  "io.jwt.verify_es512" => { digest: "SHA512", scheme: :ecdsa },
  "io.jwt.verify_eddsa" => { digest: nil, scheme: :eddsa }
}.freeze
VERIFY_FUNCTIONS =
VERIFY_ALGORITHMS.keys.to_h do |name|
  [name, { arity: 2, handler: name.split(".").last.to_sym }]
end.freeze
VERIFIERS =

scheme -> the verifier method for one asymmetric public key.

{ rsa: :rsa_verified?, pss: :pss_verified?, ecdsa: :ecdsa_verified?, eddsa: :eddsa_verified? }
.freeze
PEM_BEGIN =

The first PEM BEGIN marker, capturing the type label. A plain char class with no backreference, so matching is linear (the original block-spanning regex backtracked O(n^2) on many BEGIN lines).

/-----BEGIN ([A-Z0-9 ]+)-----/
PEM_TRAILER =

What may follow the END marker for the block to be the sole content, mirroring Go’s pem.Decode: the END line’s own trailing spaces/tabs and at most one line terminator (\n or \r\n) — a bare \r, a second newline, or any other text is trailing data and makes the key undefined.

/\A[ \t]*(?:\r\n|\n)?\z/
NIST_EC_CURVES =

OpenSSL curve names Go’s x509.ParsePKIXPublicKey accepts (the NIST P-curves); secp256k1 and the brainpool curves are rejected by OPA.

%w[secp224r1 prime256v1 secp384r1 secp521r1].freeze
ED25519_OID =

The OpenSSL oid of an Ed25519 key (a bare OpenSSL::PKey::PKey, distinguishing it from a similarly-bare RSASSA-PSS or post-quantum key, which OPA’s x509 parser rejects).

"ED25519"
DECODE_VERIFY_FUNCTIONS =
{
  "io.jwt.decode_verify" => { arity: 2, handler: :decode_verify }
}.freeze
DECODE_VERIFY_CONTEXT =
"io.jwt.decode_verify"
CONSTRAINT_KEYS =

The closed set of constraint keys OPA accepts; any other key makes the constraints undefined.

%w[cert secret alg iss aud time].freeze
NS_PER_SECOND =
1_000_000_000

Class Method Summary collapse

Methods included from RegistryHelpers

register_configured_functions

Class Method Details

.decode(value) ⇒ Ruby::Rego::Value

:reek:NilCheck – nil is the segment-decode-failure sentinel mapped to OPA’s undefined.

Parameters:

Returns:



59
60
61
62
63
64
65
# File 'lib/ruby/rego/builtins/jwt.rb', line 59

def self.decode(value)
  token = string_value(value, "io.jwt.decode")
  return UndefinedValue.new unless token.encoding.ascii_compatible? && token.valid_encoding?

  parts = decode_parts(token.split(".", -1))
  parts.nil? ? UndefinedValue.new : Value.from_ruby(parts)
end

.decode_verify(jwt_value, constraints_value) ⇒ Ruby::Rego::Value

:reek:NilCheck

Parameters:

Returns:



56
57
58
59
60
61
62
# File 'lib/ruby/rego/builtins/jwt/decode_verify.rb', line 56

def self.decode_verify(jwt_value, constraints_value)
  constraints = parse_constraints(constraints_value)
  token = parse_token(string_value(jwt_value, DECODE_VERIFY_CONTEXT))
  undefined!(DECODE_VERIFY_CONTEXT) if constraints.nil? || token.nil?

  verified?(constraints, token) ? verified_result(token) : invalid_result
end

.encode_sign(headers_value, payload_value, key_value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



83
84
85
# File 'lib/ruby/rego/builtins/jwt/sign.rb', line 83

def self.encode_sign(headers_value, payload_value, key_value)
  encode(Codecs.canonical_json(headers_value), Codecs.canonical_json(payload_value), key_value.to_ruby)
end

.encode_sign_raw(headers_value, payload_value, key_value) ⇒ Ruby::Rego::StringValue

Parameters:

Returns:



91
92
93
94
95
# File 'lib/ruby/rego/builtins/jwt/sign.rb', line 91

def self.encode_sign_raw(headers_value, payload_value, key_value)
  context = "io.jwt.encode_sign_raw"
  encode(json_string(headers_value, context), json_string(payload_value, context),
         parse_jwk(key_value, context))
end

.register!Ruby::Rego::Builtins::BuiltinRegistry



48
49
50
51
52
# File 'lib/ruby/rego/builtins/jwt.rb', line 48

def self.register!
  registry = BuiltinRegistry.instance
  register_configured_functions(registry, JWT_FUNCTIONS)
  registry
end

.register_decode_verify!Ruby::Rego::Builtins::BuiltinRegistry



46
47
48
49
50
# File 'lib/ruby/rego/builtins/jwt/decode_verify.rb', line 46

def self.register_decode_verify!
  registry = BuiltinRegistry.instance
  register_configured_functions(registry, DECODE_VERIFY_FUNCTIONS)
  registry
end

.register_encoders!Ruby::Rego::Builtins::BuiltinRegistry



73
74
75
76
77
# File 'lib/ruby/rego/builtins/jwt/sign.rb', line 73

def self.register_encoders!
  registry = BuiltinRegistry.instance
  register_configured_functions(registry, ENCODE_FUNCTIONS)
  registry
end

.register_verifications!Ruby::Rego::Builtins::BuiltinRegistry



75
76
77
78
79
# File 'lib/ruby/rego/builtins/jwt/verify.rb', line 75

def self.register_verifications!
  registry = BuiltinRegistry.instance
  register_configured_functions(registry, VERIFY_FUNCTIONS)
  registry
end

.verify(jwt_value, key_value, context) ⇒ Ruby::Rego::BooleanValue

:reek:NilCheck – a nil scheme_result is the undefined sentinel (empty/unparseable key).

Parameters:

Returns:



86
87
88
89
90
91
# File 'lib/ruby/rego/builtins/jwt/verify.rb', line 86

def self.verify(jwt_value, key_value, context)
  config = VERIFY_ALGORITHMS.fetch(context)
  signed = split_signed(string_value(jwt_value, context), context)
  result = scheme_result(config, string_value(key_value, context), signed)
  result.nil? ? undefined!(context) : BooleanValue.new(result)
end