Skip to content

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

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

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

Conversation

@bvogel
Copy link

@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

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)
@bruno-
Copy link

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
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

end

def jwe?(token)
options.id_token_encryption_alg.to_s != '' && token.to_s.count('.') + 1 == JWE_SEGMENT_COUNT
Copy link

Choose a reason for hiding this comment

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

so it's always the same number of segments ?

Copy link
Author

Choose a reason for hiding this comment

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

for encrypted? yes. Its always 3 for unencrypted and 5 for encrypted

Copy link

@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
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
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.

3 participants