feat: add JWE (JSON Web Encryption) decryption support#209
feat: add JWE (JSON Web Encryption) decryption support#209bvogel wants to merge 5 commits intoomniauth:masterfrom
Conversation
|
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 👍 |
Excellent point, added a section to the README |
|
We have this code in production for a customer integrating with It'sMe. |
0f1e4c4 to
c3c7835
Compare
|
|
||
| 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' |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
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)
78e8d74 to
fc600c2
Compare
…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
| 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? |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
What is NoMethodError here trying to prevent?
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:
id_token_encryption_alg'RSA-OAEP','RSA-OAEP-256', or'dir'id_token_encryption_key'dir'decode_id_tokenis extended to transparently detect and decrypt a JWE envelope before passing the inner JWS to the existing verification logic. Whenid_token_encryption_algis not configured the behavior is completely unchanged.Supported algorithms
RSA-OAEPRSA-OAEP-256dirError handling
All failure paths (missing key, malformed PEM, decryption failure, unsupported
enc, bad base64) are caught and re-raised asCallbackError, preserving the existing error-handling contract incallback_phase.Tests
New test file
test/lib/omniauth/strategies/openid_connect_jwe_test.rbwith 17 tests covering:jwe?detection (with/without alg configured, 3-segment vs 5-segment)decrypt_jwefor each alg: missing-key errors, delegation to json-jwt, round-trip decryptionDecryptionFailed,CipherError,ArgumentErrordecode_id_tokenintegration (decrypt called for JWE, skipped for JWS)All 53 tests pass (36 existing + 17 new).
References