Skip to content

fix(auth): switch to OIDC hybrid flow after CARIAD BFF token exchange brake - #333

Merged
robinostlund merged 1 commit into
robinostlund:masterfrom
s1gmund80:develop
May 30, 2026
Merged

fix(auth): switch to OIDC hybrid flow after CARIAD BFF token exchange brake#333
robinostlund merged 1 commit into
robinostlund:masterfrom
s1gmund80:develop

Conversation

@s1gmund80

Copy link
Copy Markdown
Contributor

Fix VW auth flow: switch to OIDC hybrid flow (code id_token token)

Background

Around 27 May 2026, Volkswagen changed the CARIAD BFF backend in a way that broke
authentication. PR #331 migrated the OIDC endpoint paths from /login/v1/idk/ to
/auth/v1/idk/oidc/ (the correct paths), but login remained broken in two ways:

  1. A critical TypeError in _login_get_authorization_code returned a
    (auth_code, code_verifier) tuple but _login assigned it to a single variable
    and passed it unchanged to _exchange_code_for_tokens. This caused a TypeError
    on every login attempt, silently caught by the broad except Exception handler.

  2. The CARIAD BFF token endpoint now rejects the authorization code exchange
    POST /auth/v1/idk/oidc/token returns {"error":"Bad Request"} regardless of
    whether a PKCE code_verifier is included or not. All permutations tried
    (Bearer JWT, inner opaque code, JSON body, form body) failed. The endpoint
    POST /user-login/login/v1 used in the reverted PR Fix VW auth flow: use identity.vwgroup.io preamble to get Auth0 state #329 now returns 403.

Root cause analysis

The auth flow was captured via a MITM proxy on the official Volkswagen iOS app
(v18.7) to understand the exact requests made by a working client.

The proxy showed that identity.vwgroup.io (Auth0) performs the standard
Auth0 Universal Login flow and delivers a signed JWT as the authorization code
in the callback URL weconnect://authenticated?code=<JWT>&state=<state>.

After extensive testing of every viable token exchange approach against
/auth/v1/idk/oidc/token, the root cause became clear: the CARIAD BFF acts as a
confidential client to Auth0 and handles the Auth0-side token exchange
server-side using its own credentials. It does not expose a usable public-client
token exchange endpoint for authorization codes.

Solution

Switch to the OIDC hybrid flow (response_type=code id_token token).

When Auth0's authorization endpoint is called with this response type, it delivers
access_token and id_token directly in the callback URL (query string /
fragment), alongside the code. These Auth0-issued tokens are JWTs that the CARIAD
BFF validates via Auth0's public keys — no separate token exchange step is needed.

This approach was confirmed working against the live API with real credentials and
a real vehicle account (two VINs, full telemetry data retrieved).

Changes

volkswagencarnet/vw_const.py

  • Added offline_access to CLIENT_SCOPE — confirmed present in the scp claim
    of tokens issued by the live Auth0 server; without it no refresh token is issued.
  • Removed CLIENT_TOKEN_TYPES = "code" — no longer used; response type is now
    hardcoded to "code id_token token" where it matters.

volkswagencarnet/vw_connection.py

get_authorization_page

  • Removed unused code_challenge parameter (PKCE remnant).
  • Removed redundant try/except wrapper that only re-raised exceptions.
  • Changed response_type to "code id_token token" (hybrid flow).
  • Added nonce parameter (required by Auth0 hybrid flow; Auth0 embeds it in the
    returned id_token).
  • Improved error message when the authorization redirect is missing.

_get_authorization_code

  • Added action=default to the login form POST body — required by Auth0 Universal
    Login to distinguish a credential submission from other form actions (e.g.
    "Forgot password"). Without it the login POST can be silently ignored.
  • Updated to parse both the query string and the URL fragment from the callback,
    extracting code, id_token, and access_token.
  • Returns a (auth_code, id_token, access_token) tuple.

_exchange_code_for_tokens_build_session_tokens

  • Renamed to reflect that no server-side token exchange occurs.
  • Removed all token exchange logic (form POST, JSON POST, Bearer JWT header
    variants — all returned 400/403 from the CARIAD BFF).
  • Now simply packages the hybrid flow tokens into the session dict; retains the
    JWT decode for debug logging only.

_login

handle_login_with_password

  • Removed — was a thin one-liner wrapper around post_form with no callers.

logout

  • Fixed pre-existing bug: refresh token revocation checked _session_headers
    instead of _session_tokens for the token lookup, so revocation never fired.

Known limitations

No refresh token — re-login required on expiry.

The hybrid flow does not return a refresh_token (long-lived tokens are never
placed in URLs for security reasons). All alternative paths to obtain one were
tested and failed:

Approach Result
POST /auth/v1/idk/oidc/token (inner opaque code, Bearer JWT) 400 Bad Request
POST /auth/v1/idk/oidc/token (inner code, no Bearer) 400 Bad Request
POST /auth/v1/idk/oidc/token (JWT as code) 400 invalid assertion headers
POST https://identity.vwgroup.io/oidc/v1/token (direct Auth0, no PKCE) 401 access_denied
POST https://identity.vwgroup.io/oidc/v1/token (direct Auth0, with PKCE) 401 access_denied
POST /user-login/login/v1 (VW-specific endpoint) 403 Forbidden

Auth0 binds the authorization code to the CARIAD BFF as the authorised exchanger;
direct exchanges from the public client are rejected. The CARIAD BFF's own token
exchange endpoint returns 400 for all attempted parameter combinations.

When the access_token expires (typically ~2 hours), a full re-login is required.
The refresh_tokens method is preserved in place — if VW restores a usable code
exchange path on the BFF, it will work automatically.

Testing

Verified with a real VW account:

  • Login succeeds end-to-end
  • All registered VINs are discovered
  • Full vehicle telemetry is returned (battery, charging, climate, doors, windows,
    fuel level, trip statistics, maintenance intervals)
  • Logout completes without error

@robinostlund

Copy link
Copy Markdown
Owner

Have you confirmed that this works together with the ha integration? 😊 i can release it but i dont have any possibilities to test it

@Yogibaer75

Yogibaer75 commented May 30, 2026

Copy link
Copy Markdown

I can give a positive feedback - patched my python libraries inside ha and got data again for my vehicle.
At the moment no information about how stable it is, only first sync is working and only 3 minutes old. 😄

  • Update
    Data and status update works.
image

Some entities are gone missing
image
I don't had time to investigate why they are missing now. Possible reason would be the "offline_access" client scope. But i need to check first.

@tmenguy

tmenguy commented May 30, 2026

Copy link
Copy Markdown

Wow, thx!, it is working for me , I have a custom component with the fix: https://github.com/tmenguy/homeassistant-volkswagencarnet

@holly1889

Copy link
Copy Markdown

Have you confirmed that this works together with the ha integration? 😊 i can release it but i dont have any possibilities to test it

Same I can test and let you know, i am in the UK if that matters.

@ohkaja

ohkaja commented May 30, 2026

Copy link
Copy Markdown

Can confirm in germany that the fix is working. Receiving data again.

@robinostlund

Copy link
Copy Markdown
Owner

Will release it with some notes that it doesnt some sensors may be missing data

@jarvisms

Copy link
Copy Markdown

Works for me in the UK with my ID.3 and my dad's ID.4. All existing entities woke up, including GPS location and I have tried pre-condition and this worked as expected. Looks like we're winning!

image

@robinostlund robinostlund added the bug Something isn't working label May 30, 2026
@shaarkys

Copy link
Copy Markdown

Works, thank you !!!

VW, Go Fk Y...

@robinostlund
robinostlund merged commit 861cb86 into robinostlund:master May 30, 2026
7 of 9 checks passed
@holly1889

holly1889 commented May 30, 2026 via email

Copy link
Copy Markdown

oldgitdaddy added a commit to oldgitdaddy/WeConnect-python that referenced this pull request May 31, 2026
The direct POST to identity.vwgroup.io/oidc/v1/token with
grant_type=authorization_code returns 401 access_denied because
Auth0 binds the authorization code to the CARIAD BFF as the
authorized exchanger.

Switch to the OIDC hybrid flow (response_type=code id_token token)
where access_token and id_token are delivered directly in the
callback URL — no server-side token exchange is needed. The parent
class OpenIDSession.authorizationUrl() already uses this response
type so the authorization URL was already correct.

Key changes:
- fetchTokens(): extract tokens from callback directly, skip the
  broken token exchange POST to /oidc/v1/token
- refresh(): trigger full re-login since hybrid flow issues no
  refresh_token (~2h access token lifetime)
- refreshTokens(): simplified to delegate to login() for graceful
  fallback
- _handle_new_auth_flow(): add action=default to login form POST
  (required by Auth0 Universal Login, per PR #333 finding)
- Scope: add offline_access
- Remove unused imports (requests, InsecureTransportError, etc.)

Aligned with robinostlund/volkswagencarnet#333.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@henrikedegrd

Copy link
Copy Markdown

Note sure where to write.

I have used a ported version of this: https://github.com/John6810/myaudi-api (i am using PHP).
And since this problem came up, the only thing i changed was:

/login/v1/idk/openid-configuration → /auth/v1/idk/oidc/openid-configuration
/login/v1/idk/token → /auth/v1/idk/oidc/token

Then everything still works. Getting all sensors, refresh of token every hour.

You could maybe get some details from that implementation in order to fix the token-refresh?

Sadly Python isnt my thing, so cant really contribute any more than point towards that.
If my working php-parts are of interest to look at, then just say so and I can probably make it public to look at.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants