Skip to content

fix: decode JWT segments as Base64url (RFC 7515) - #978

Merged
TheOneRing merged 4 commits into
opencloud-eu:mainfrom
willyp713:fix/jwt-base64url-decode
Jul 22, 2026
Merged

fix: decode JWT segments as Base64url (RFC 7515)#978
TheOneRing merged 4 commits into
opencloud-eu:mainfrom
willyp713:fix/jwt-base64url-decode

Conversation

@willyp713

@willyp713 willyp713 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #977

What

JWT::JWT() decoded token segments with QByteArray::fromBase64(part), which uses the standard Base64 alphabet. JWT segments are Base64url (RFC 7515 section 2). Qt's default IgnoreBase64DecodingErrors mode silently skips - and _ as invalid characters and corrupts every byte after them. The payload then fails JSON parsing, IdToken::aud() returns an empty string, and OIDC login fails with a misleading "The audience of the id_token did not contain …" error. See #977 for the full analysis, including why the failure depends on token content and looked IdP-specific.

Changes

Commit 1:

  • jwt.cpp: decode segments with Base64UrlEncoding | AbortOnBase64DecodingErrors via fromBase64Encoding. A header or payload segment that is not valid base64url now yields an empty object, and so an invalid JWT, instead of silently corrupted claims. serialize() already used Base64UrlEncoding; only parsing was wrong.
  • test/testjwt.cpp (new): regression tests. They cover a payload whose base64url encoding contains both - and _, a serialize/parse round-trip, structurally malformed tokens, and strict rejection of an invalid character inside an otherwise valid segment (the permissive decode wrongly accepted that case). Against the unpatched code, three of the four fail; with this change all pass.

Commit 2:

  • oauth.cpp: an unparseable id_token previously fell through to the audience check and produced the misleading audience error. The token exchange handler now retains the parsed JWT and reports "The id_token could not be parsed" before checking the audience.
  • test/testoauth/testoauth.cpp: new testUnparseableIdToken corrupts the header and payload segments in turn, and expects the parse error rather than a login or the audience message.

Notes

  • Strict Base64url decoding rejects tokens from IdPs that emit standard-alphabet + or / in JWT segments. RFC 7515 forbids those characters, but this is a behavior change for such non-compliant IdPs.
  • Tested on Linux with Qt 6.8.2. I compiled the testjwt target standalone and ran it against both the patched and unpatched decoder; I did not run the full suite build in my environment.

@dragotin

Copy link
Copy Markdown
Member

Thank you for the patch, looks like an interesting finding.

However, I needed pr_978.patch to make it compile over here, please double check and update the PR accordingly.

willyp713 pushed a commit to willyp713/desktop that referenced this pull request Jul 17, 2026
Both cleanups are from the review patch on opencloud-eu#978.

Co-authored-by: Klaas Freitag <1070214+dragotin@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@willyp713
willyp713 force-pushed the fix/jwt-base64url-decode branch from 82987cf to 08906e5 Compare July 17, 2026 01:58
@willyp713

Copy link
Copy Markdown
Contributor Author

Thanks for the patch; applied in full and force-pushed. The export macro and Q_SLOTS fixes are squashed into the first commit, the enum name fix into the second, and the two cleanups (duplicated word in the log message, redundant QtTest include) are a separate third commit.

The misses came from verifying only testjwt locally, on a Linux build with default symbol visibility and without QT_NO_KEYWORDS. I re-ran testjwt with QT_NO_KEYWORDS defined: 6/6 pass.

@dragotin dragotin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks now good to me, lets see what @TheOneRing thinks.

@dragotin
dragotin requested a review from TheOneRing July 17, 2026 13:10
Comment thread src/libsync/creds/jwt.cpp
auto parse = [](const QByteArray &part) {
const auto decoded = QByteArray::fromBase64Encoding(part, QByteArray::Base64UrlEncoding | QByteArray::AbortOnBase64DecodingErrors);
if (!decoded) {
return QJsonObject{};

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.

Please add a log line including the QByteArray::Base64DecodingStatus

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a qCWarning under a new sync.credentials.jwt category logging the Base64DecodingStatus on decode failure. Status only (no segment bytes) to keep token contents out of the logs. testjwt still 6/6.

willyp713 and others added 4 commits July 22, 2026 11:44
JWT::JWT() decoded segments with standard-alphabet Base64; Qt's default
permissive mode silently skips base64url's '-' and '_' characters,
corrupting every byte after them. A corrupted payload fails JSON
parsing and OIDC login aborts with a misleading audience error (opencloud-eu#977).

Decode with Base64UrlEncoding and abort on invalid characters, so a
header or payload segment that is not valid base64url yields an invalid
JWT instead of silently corrupted claims. serialize() already used
Base64UrlEncoding; only parsing was wrong.

Fixes: opencloud-eu#977

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An id_token with an undecodable header or payload segment previously
fell through to the audience check and failed with 'The audience of
the id_token did not contain ...'. That message blames the IdP client
configuration when the token never parsed. Check JWT validity first
and report a distinct parse error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both cleanups are from the review patch on opencloud-eu#978.

Co-authored-by: Klaas Freitag <1070214+dragotin@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a sync.credentials.jwt logging category so a rejected Base64url
segment is diagnosable instead of failing silently.
@TheOneRing
TheOneRing force-pushed the fix/jwt-base64url-decode branch from d53f81d to f243379 Compare July 22, 2026 09:44

@TheOneRing TheOneRing left a comment

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.

The crash in the unit test is unrelated.
Thank you for your contribution.

@TheOneRing
TheOneRing merged commit 8708f2e into opencloud-eu:main Jul 22, 2026
10 of 13 checks passed
TheOneRing pushed a commit that referenced this pull request Jul 22, 2026
Both cleanups are from the review patch on #978.

Co-authored-by: Klaas Freitag <1070214+dragotin@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@openclouders openclouders mentioned this pull request Jul 22, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OIDC login fails with "The audience of the id_token did not contain …" when the id_token payload's base64url encoding contains - or _

3 participants