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
algheader 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
-
.decode(value) ⇒ Ruby::Rego::Value
:reek:NilCheck – nil is the segment-decode-failure sentinel mapped to OPA’s undefined.
-
.decode_verify(jwt_value, constraints_value) ⇒ Ruby::Rego::Value
:reek:NilCheck.
-
.encode_sign(headers_value, payload_value, key_value) ⇒ Ruby::Rego::StringValue
-
.encode_sign_raw(headers_value, payload_value, key_value) ⇒ Ruby::Rego::StringValue
-
.register! ⇒ Ruby::Rego::Builtins::BuiltinRegistry
-
.register_decode_verify! ⇒ Ruby::Rego::Builtins::BuiltinRegistry
-
.register_encoders! ⇒ Ruby::Rego::Builtins::BuiltinRegistry
-
.register_verifications! ⇒ Ruby::Rego::Builtins::BuiltinRegistry
-
.verify(jwt_value, key_value, context) ⇒ Ruby::Rego::BooleanValue
:reek:NilCheck – a nil scheme_result is the undefined sentinel (empty/unparseable key).
Methods included from RegistryHelpers
Class Method Details
.decode(value) ⇒ Ruby::Rego::Value
:reek:NilCheck – nil is the segment-decode-failure sentinel mapped to OPA’s undefined.
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
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
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
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).
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 |