fix(auth): switch to OIDC hybrid flow after CARIAD BFF token exchange brake - #333
Conversation
|
Have you confirmed that this works together with the ha integration? 😊 i can release it but i dont have any possibilities to test it |
|
Wow, thx!, it is working for me , I have a custom component with the fix: https://github.com/tmenguy/homeassistant-volkswagencarnet |
Same I can test and let you know, i am in the UK if that matters. |
|
Can confirm in germany that the fix is working. Receiving data again. |
|
Will release it with some notes that it doesnt some sensors may be missing data |
|
Works, thank you !!! VW, Go Fk Y... |
|
UK fix is working. Much appreciate all the efforts people Sent from my iPhoneOn 30 May 2026, at 21:28, Mark Jarvis ***@***.***> wrote:jarvisms left a comment (robinostlund/volkswagencarnet#333)
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.png (view on web)
—Reply to this email directly, view it on GitHub, or unsubscribe.Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS and Android. Download it today!
You are receiving this because you commented.Message ID: ***@***.***>
|
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>
|
Note sure where to write. I have used a ported version of this: https://github.com/John6810/myaudi-api (i am using PHP). /login/v1/idk/openid-configuration → /auth/v1/idk/oidc/openid-configuration 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. |



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:A critical
TypeErrorin_login—_get_authorization_codereturned a(auth_code, code_verifier)tuple but_loginassigned it to a single variableand passed it unchanged to
_exchange_code_for_tokens. This caused aTypeErroron every login attempt, silently caught by the broad
except Exceptionhandler.The CARIAD BFF token endpoint now rejects the authorization code exchange —
POST /auth/v1/idk/oidc/tokenreturns{"error":"Bad Request"}regardless ofwhether a PKCE
code_verifieris included or not. All permutations tried(Bearer JWT, inner opaque code, JSON body, form body) failed. The endpoint
POST /user-login/login/v1used 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 aconfidential 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_tokenandid_tokendirectly in the callback URL (query string /fragment), alongside the
code. These Auth0-issued tokens are JWTs that the CARIADBFF 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.pyoffline_accesstoCLIENT_SCOPE— confirmed present in thescpclaimof tokens issued by the live Auth0 server; without it no refresh token is issued.
CLIENT_TOKEN_TYPES = "code"— no longer used; response type is nowhardcoded to
"code id_token token"where it matters.volkswagencarnet/vw_connection.pyget_authorization_pagecode_challengeparameter (PKCE remnant).try/exceptwrapper that only re-raised exceptions.response_typeto"code id_token token"(hybrid flow).nonceparameter (required by Auth0 hybrid flow; Auth0 embeds it in thereturned
id_token)._get_authorization_codeaction=defaultto the login form POST body — required by Auth0 UniversalLogin to distinguish a credential submission from other form actions (e.g.
"Forgot password"). Without it the login POST can be silently ignored.
extracting
code,id_token, andaccess_token.(auth_code, id_token, access_token)tuple._exchange_code_for_tokens→_build_session_tokensvariants — all returned 400/403 from the CARIAD BFF).
JWT decode for debug logging only.
_loginattempt to raise
TypeError.token_endpointfetching (no longer needed for the initial login)._build_session_tokens.handle_login_with_passwordpost_formwith no callers.logout_session_headersinstead of
_session_tokensfor 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 neverplaced in URLs for security reasons). All alternative paths to obtain one were
tested and failed:
POST /auth/v1/idk/oidc/token(inner opaque code, Bearer JWT)400 Bad RequestPOST /auth/v1/idk/oidc/token(inner code, no Bearer)400 Bad RequestPOST /auth/v1/idk/oidc/token(JWT ascode)400 invalid assertion headersPOST https://identity.vwgroup.io/oidc/v1/token(direct Auth0, no PKCE)401 access_deniedPOST https://identity.vwgroup.io/oidc/v1/token(direct Auth0, with PKCE)401 access_deniedPOST /user-login/login/v1(VW-specific endpoint)403 ForbiddenAuth0 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_tokenexpires (typically ~2 hours), a full re-login is required.The
refresh_tokensmethod is preserved in place — if VW restores a usable codeexchange path on the BFF, it will work automatically.
Testing
Verified with a real VW account:
fuel level, trip statistics, maintenance intervals)