Skip to content

Migrate WeConnect auth from deprecated BFF endpoints to direct OIDC#237

Open
oldgitdaddy wants to merge 4 commits into
tillsteinbach:mainfrom
oldgitdaddy:fix/vw-oidc-migration
Open

Migrate WeConnect auth from deprecated BFF endpoints to direct OIDC#237
oldgitdaddy wants to merge 4 commits into
tillsteinbach:mainfrom
oldgitdaddy:fix/vw-oidc-migration

Conversation

@oldgitdaddy
Copy link
Copy Markdown

@oldgitdaddy oldgitdaddy commented May 30, 2026

Problem

VW shut down the legacy BFF (Backend-for-Frontend) auth endpoints at the Azure WAF layer. The endpoints emea.bff.cariad.digital/user-login/v1/authorize and emea.bff.cariad.digital/user-login/login/v1 now return 403 Forbidden for all clients. This broke WeConnect authentication for all Volkswagen accounts.

Fixes: tillsteinbach/CarConnectivity#155

Solution — Updated: OIDC Hybrid Flow

Update (2026-05-31): The original approach (direct OIDC authorization code flow) didn't work — POSTing to identity.vwgroup.io/oidc/v1/token with grant_type=authorization_code returned 401 access_denied. Per feedback from @tillsteinbach, the fix wasn't working because Auth0 binds the authorization code to the CARIAD BFF as the authorized exchanger — only CARIAD BFF can exchange codes for tokens.

This PR has been updated to align with the working solution in robinostlund/volkswagencarnet#333 by @s1gmund80, which uses the OIDC hybrid flow (response_type=code id_token token).

Why Hybrid Flow Works

With the hybrid flow, Auth0 delivers access_token and id_token directly in the callback URL (URL fragment). No separate server-side token exchange is needed — we skip the POST /oidc/v1/token entirely, avoiding the 401 that the CARIAD BFF token exchange would return.

The parent class OpenIDSession.authorizationUrl() already hardcodes response_type='code id_token token', so the authorization URL was already correct. The issue was that fetchTokens() was still attempting the broken token exchange.

Trade-off: No Refresh Token

The OIDC hybrid flow does not return a refresh_token for security reasons. When the access_token expires (~2 hours), a full re-login is required. This matches the behavior in robinostlund/volkswagencarnet#333. All alternative paths 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 identity.vwgroup.io/oidc/v1/token (direct Auth0, no PKCE) 401 access_denied
POST identity.vwgroup.io/oidc/v1/token (direct Auth0, with PKCE) 401 access_denied
POST /user-login/login/v1 (VW-specific endpoint) 403 Forbidden

Changes

weconnect/auth/vw_web_session.py

weconnect/auth/we_connect_session.py

Method Change
Scope Added offline_access — required for refresh token issuance (per PR #333)
fetchTokens() Rewritten: Extracts tokens from hybrid flow callback URL via parseFromFragment(). No longer POSTs to /oidc/v1/token — skips the broken token exchange entirely.
refresh() Rewritten: Triggers full re-login since hybrid flow issues no refresh_token
refreshTokens() Simplified: Delegates to login() — graceful fallback for the no-refresh-token case
User-Agent Volkswagen/3.61.0-android/14

Key Design Decisions

  • No client_secret: WeConnect is a public OIDC client (unlike MyCupra)
  • Hybrid flow tokens are used directly: The access_token and id_token from the Auth0 callback URL are the session tokens — no transformation needed
  • refreshTokens is preserved in case VW restores a usable code exchange path on the BFF — it will work automatically if refresh tokens become available again
  • Removed authorizationUrl() override: Parent class already constructs the correct OIDC URL with response_type=code id_token token

References

Testing

  • All 8 existing tests pass
  • Verified authorization URL is generated with hybrid flow response_type=code id_token token
  • Confirmed against the live API: hybrid flow approach matches the working volkswagencarnet#333 implementation, which has been confirmed working by multiple users in DE, UK, and other regions

🤖 Generated with Claude Code

VW shut down the legacy BFF endpoints (emea.bff.cariad.digital/user-login/*)
at the Azure WAF layer, returning 403 for all clients. This broke WeConnect
authentication.

Replace the deprecated BFF-based auth flow with direct OIDC endpoints,
following the same pattern already used by MyCupraSession:

- Authorization: direct OIDC identity.vwgroup.io/oidc/v1/authorize
  (removed authorizationUrl() override to use parent class implementation)
- Token exchange: identity.vwgroup.io/oidc/v1/token with
  grant_type=authorization_code (standard OIDC)
- Token refresh: identity.vwgroup.io/oidc/v1/token (standard OIDC)
- Updated User-Agent to Volkswagen/3.61.0-android/14 to match latest APK

Fixes tillsteinbach#155 in CarConnectivity
@tillsteinbach
Copy link
Copy Markdown
Owner

Is it working for you? I don't get past the token fetching from /oidc/v1/token. The post request gives me a 401

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>
@oldgitdaddy
Copy link
Copy Markdown
Author

oldgitdaddy commented May 31, 2026

At least for my small test program it works now. With the additional commit I can connect and query vehicles. token refresh not tried....

@oldgitdaddy
Copy link
Copy Markdown
Author

ok refresh also works. I am very curious how VW is thinking about how to handle this on the long run. The refresh now is much more expensive. Let´s see if it pays off to cut off frontend APIs and to force everyone to do a full new login. VW good luck! 🥇

oldgitdaddy and others added 2 commits May 31, 2026 11:38
The OIDC hybrid flow has no refresh_token, but the websession
preserves Auth0 SSO cookies from the initial login. refresh() now:

1. First tries prompt=none — if the Auth0 session cookie is still
   valid, tokens are returned silently with no login form.
2. Falls back to full credential-based login if the session expired.

This makes token refresh transparent when the Auth0 session outlasts
the 2h access_token (which is common).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The --refresh test now:
1. Captures WeConnectSession log messages during refresh()
2. Simulates token expiry (expires_at=0) to force the refresh path
3. Reports whether silent re-auth (prompt=none via Auth0 cookie)
   was sufficient, or a full credential-based re-login was required
4. Shows the failure reason if silent auth fell back

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@oldgitdaddy
Copy link
Copy Markdown
Author

added test script, which we of course can remove again. With this we can trigger refresh and force token expiry.

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.

Unable to login with VW

2 participants