Skip to content

feat: add JWE (JSON Web Encryption) decryption support#209

Open
bvogel wants to merge 5 commits intoomniauth:masterfrom
UnidyID:feat/jwe-decryption-support
Open

feat: add JWE (JSON Web Encryption) decryption support#209
bvogel wants to merge 5 commits intoomniauth:masterfrom
UnidyID:feat/jwe-decryption-support

Conversation

@bvogel
Copy link
Copy Markdown

@bvogel bvogel commented Mar 5, 2026

Motivation

Some OpenID Connect providers mandate that ID tokens are wrapped in a JWE (JSON Web Encryption) envelope before being returned to the relying party. A notable example is the Belgian It'sMe identity provider, which requires RSA-OAEP-256 encrypted ID tokens per the OIDC spec (RFC 7516).

There is currently no way to use such providers with this gem.

What this adds

Two new strategy options:

Option Description
id_token_encryption_alg The key-wrapping algorithm: 'RSA-OAEP', 'RSA-OAEP-256', or 'dir'
id_token_encryption_key PEM string for RSA algorithms; raw bytes/string for 'dir'

decode_id_token is extended to transparently detect and decrypt a JWE envelope before passing the inner JWS to the existing verification logic. When id_token_encryption_alg is not configured the behavior is completely unchanged.

Supported algorithms

Key-wrapping Content encryption Implementation
RSA-OAEP A128GCM, A256GCM, A128CBC-HS256, A256CBC-HS512 json-jwt (already a transitive dep)
RSA-OAEP-256 A128GCM, A256GCM, A128CBC-HS256, A256CBC-HS512 Native OpenSSL (requires OpenSSL >= 3.0)
dir A128GCM, A256GCM, A128CBC-HS256, A256CBC-HS512 json-jwt

Note on RSA-OAEP-256: json-jwt does not support SHA-256 for the OAEP hash and MGF1 mask, so this path uses native OpenSSL. OpenSSL 3.0 added the rsa_oaep_md / rsa_mgf1_md options required for this. OpenSSL >= 3.0 ships by default with Ruby 3.1+. If RSA-OAEP-256 is requested on an older OpenSSL, a descriptive CallbackError is raised.

Error handling

All failure paths (missing key, malformed PEM, decryption failure, unsupported enc, bad base64) are caught and re-raised as CallbackError, preserving the existing error-handling contract in callback_phase.

Tests

New test file test/lib/omniauth/strategies/openid_connect_jwe_test.rb with 17 tests covering:

  • jwe? detection (with/without alg configured, 3-segment vs 5-segment)
  • decrypt_jwe for each alg: missing-key errors, delegation to json-jwt, round-trip decryption
  • RSA-OAEP-256 round-trips with A128GCM and A128CBC-HS256 enc
  • Error wrapping for DecryptionFailed, CipherError, ArgumentError
  • decode_id_token integration (decrypt called for JWE, skipped for JWS)

All 53 tests pass (36 existing + 17 new).

References

@bruno-
Copy link
Copy Markdown

bruno- commented Mar 5, 2026

This looks good to me. Only minor thing I noticed: how about updating the README the options that were added?

Overall, the approach seems solid 👍

@bvogel
Copy link
Copy Markdown
Author

bvogel commented Mar 5, 2026

This looks good to me. Only minor thing I noticed: how about updating the README the options that were added?

Excellent point, added a section to the README

Comment thread lib/omniauth/strategies/openid_connect.rb
Copy link
Copy Markdown

@Sokre95 Sokre95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good !

@bvogel
Copy link
Copy Markdown
Author

bvogel commented Mar 10, 2026

We have this code in production for a customer integrating with It'sMe.

@bvogel bvogel force-pushed the feat/jwe-decryption-support branch from 0f1e4c4 to c3c7835 Compare March 10, 2026 15:00

option :logout_path, '/logout'
option :id_token_encryption_alg, nil # e.g. 'RSA-OAEP', 'RSA-OAEP-256', 'dir'
option :id_token_encryption_key, nil # PEM string for RSA algorithms; raw bytes/string for 'dir'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Raw bytes are a bit tricky to pass through as strings, especially if this config comes from JSON. Would it better to point this to a file?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, raw bytes are a pain, however our practical experience showed that in most cases the shared key is usually "just" a string, so it would be fine, but if the maintainers prefer we could opt for a base64 encoding or a file, but as said - our production experience works fine with this format.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend there be an option to use either a base64 version or a file. In my installations, we load OmniAuth configs from JSON, and raw bytes are not an option.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you @stanhu for you patience and for looking into this. I added now base64 encryption and the file option. Anything else you want me to add?

Comment thread lib/omniauth/strategies/openid_connect.rb
bvogel added 4 commits March 18, 2026 09:17
Adds support for providers that wrap the ID token in a JWE envelope
before returning it (e.g. the Belgian It'sMe identity provider mandates
RSA-OAEP-256 encrypted ID tokens). Previously there was no way to use
such providers with this gem.

New options:
- `id_token_encryption_alg` - the key-wrapping algorithm ('RSA-OAEP',
  'RSA-OAEP-256', or 'dir')
- `id_token_encryption_key` - PEM string for RSA algorithms; raw bytes
  for 'dir' (direct symmetric key agreement)

`decode_id_token` transparently decrypts the JWE envelope before
passing the inner JWS to the existing verification logic. When
`id_token_encryption_alg` is not set the behaviour is unchanged.

Supported algorithms:
- RSA-OAEP: delegated to the json-jwt gem (already a transitive dep)
- RSA-OAEP-256: custom OpenSSL path - requires OpenSSL >= 3.0
- dir: delegated to the json-jwt gem

Supported content encryption: A128GCM, A256GCM, A128CBC-HS256,
A256CBC-HS512.

All error paths (missing key, bad PEM, decryption failure, unknown enc)
are wrapped in CallbackError to preserve the existing error-handling
contract.

Ref: RFC 7516 (JSON Web Encryption)
@bvogel bvogel force-pushed the feat/jwe-decryption-support branch from 78e8d74 to fc600c2 Compare March 18, 2026 08:18
…y_file option

- id_token_encryption_key for dir alg is now expected to be base64url-encoded
  instead of raw bytes, making it safe to pass via JSON config or env vars
- add id_token_encryption_key_file option as an alternative to inline key values
  for both RSA (PEM file) and dir (base64url file) algorithms
- update tests to pass base64url-encoded keys for all dir round-trip cases
- add file-based key tests for both dir and RSA-OAEP
- update README options table and JWE support section accordingly
Comment thread lib/omniauth/strategies/openid_connect.rb
raise_missing_key(:id_token_encryption_key) if key.nil? || key.to_s.strip.empty?
decrypt_rsa_oaep_256(jwe_token, OpenSSL::PKey.read(key))
when 'dir'
raise_missing_key(:id_token_encryption_key) if key.nil? || key.to_s.empty?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why this line doesn't also call key.to_s.strip? Should this raise_missing_key just be called once?

userinfo = fetch_userinfo_attributes
# OIDC Core §5.3.2: the sub in the UserInfo response MUST exactly match the sub
# in the ID Token; if they differ, the UserInfo response MUST NOT be used.
if userinfo.present? && userinfo[:sub].to_s != decoded[:sub].to_s
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

present? also assumes activesupport is present. So it seems that we are now introducing a direct dependency.

end
rescue JSON::JWE::DecryptionFailed, JSON::JWE::InvalidFormat, JSON::JWE::UnexpectedAlgorithm,
OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError,
JSON::ParserError, ArgumentError, NoMethodError => e
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is NoMethodError here trying to prevent?

Comment thread lib/omniauth/strategies/openid_connect.rb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants