n8n-nodes-jygse-vw-weconnect 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -141,21 +141,43 @@ class VwWeConnect {
141
141
  }
142
142
  }
143
143
  exports.VwWeConnect = VwWeConnect;
144
- // VW OAuth2 Configuration
145
- const VW_CLIENT_ID = '9496332b-ea03-4091-a224-8c746b885068@apps_vw-dilab_com';
146
- const VW_SCOPE = 'openid profile mbb';
144
+ // VW OAuth2 Configuration (Updated January 2026)
145
+ const VW_CLIENT_ID = 'a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com';
146
+ const VW_SCOPE = 'openid profile badge cars dealers birthdate vin';
147
147
  const VW_REDIRECT_URI = 'weconnect://authenticated';
148
+ const VW_RESPONSE_TYPE = 'code id_token token';
149
+ // Generate PKCE code verifier and challenge
150
+ function generateCodeVerifier() {
151
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
152
+ let result = '';
153
+ for (let i = 0; i < 64; i++) {
154
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
155
+ }
156
+ return result;
157
+ }
158
+ async function generateCodeChallenge(verifier) {
159
+ // Simple base64url encoding of SHA256 hash
160
+ // Since we can't use crypto directly, we'll use a simplified approach
161
+ // For now, use plain verifier (S256 would be better but requires crypto)
162
+ return verifier;
163
+ }
148
164
  async function vwLogin(context, email, password) {
149
165
  try {
150
- // Step 1: Get authorization page and extract form data
166
+ // Generate PKCE values
167
+ const codeVerifier = generateCodeVerifier();
168
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
169
+ const stateParam = generateNonce();
170
+ // Step 1: Get authorization page - this now returns Auth0 login page
151
171
  const authorizeUrl = 'https://identity.vwgroup.io/oidc/v1/authorize';
152
172
  const authorizeParams = new URLSearchParams({
153
173
  client_id: VW_CLIENT_ID,
154
174
  scope: VW_SCOPE,
155
- response_type: 'code',
175
+ response_type: VW_RESPONSE_TYPE,
156
176
  redirect_uri: VW_REDIRECT_URI,
157
177
  nonce: generateNonce(),
158
- state: generateNonce(),
178
+ state: stateParam,
179
+ code_challenge: codeChallenge,
180
+ code_challenge_method: 'plain',
159
181
  });
160
182
  const authorizeResponse = await context.helpers.httpRequest({
161
183
  method: 'GET',
@@ -168,15 +190,19 @@ async function vwLogin(context, email, password) {
168
190
  returnFullResponse: true,
169
191
  ignoreHttpStatusErrors: true,
170
192
  });
171
- // Extract CSRF token and relay state from the response
172
- // Handle both string response and object with body property
193
+ // Extract response body and check for redirect
173
194
  let htmlContent;
195
+ let currentUrl = '';
174
196
  if (typeof authorizeResponse === 'string') {
175
197
  htmlContent = authorizeResponse;
176
198
  }
177
199
  else if (authorizeResponse && typeof authorizeResponse === 'object') {
178
200
  const respObj = authorizeResponse;
179
- // Try different possible response structures
201
+ const respHeaders = respObj.headers;
202
+ // Check for redirect in headers
203
+ if (respHeaders && respHeaders.location) {
204
+ currentUrl = respHeaders.location;
205
+ }
180
206
  if (respObj.body && typeof respObj.body === 'string') {
181
207
  htmlContent = respObj.body;
182
208
  }
@@ -184,94 +210,172 @@ async function vwLogin(context, email, password) {
184
210
  htmlContent = respObj.data;
185
211
  }
186
212
  else {
187
- // Serialize the object to see its structure in the error
188
213
  htmlContent = JSON.stringify(respObj);
189
214
  }
190
215
  }
191
216
  else {
192
217
  htmlContent = String(authorizeResponse);
193
218
  }
194
- const csrfMatch = htmlContent.match(/name="_csrf"\s+value="([^"]+)"/);
195
- const relayStateMatch = htmlContent.match(/name="relayState"\s+value="([^"]+)"/);
196
- const hmacMatch = htmlContent.match(/name="hmac"\s+value="([^"]+)"/);
197
- if (!csrfMatch) {
198
- // Show first 500 chars of response for debugging
199
- const preview = htmlContent.substring(0, 500);
200
- throw new Error(`Could not extract CSRF token. Response preview: ${preview}`);
201
- }
202
- const csrf = csrfMatch[1];
203
- const relayState = relayStateMatch ? relayStateMatch[1] : '';
204
- const hmac = hmacMatch ? hmacMatch[1] : '';
205
- // Step 2: Submit email
206
- const identifierResponse = await context.helpers.httpRequest({
207
- method: 'POST',
208
- url: 'https://identity.vwgroup.io/signin-service/v1/signin/login/identifier',
209
- headers: {
210
- 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
211
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
212
- 'Content-Type': 'application/x-www-form-urlencoded',
213
- },
214
- body: new URLSearchParams({
215
- _csrf: csrf,
216
- relayState: relayState,
217
- hmac: hmac,
218
- email: email,
219
- }).toString(),
220
- encoding: 'text',
221
- returnFullResponse: true,
222
- ignoreHttpStatusErrors: true,
223
- });
224
- // Extract new CSRF for password submission
225
- let identifierHtml;
226
- if (typeof identifierResponse === 'string') {
227
- identifierHtml = identifierResponse;
219
+ // Try to extract state token from Auth0 page (new flow)
220
+ // Look for state in URL or in hidden form field
221
+ let stateToken = '';
222
+ // Check if we were redirected to /u/login
223
+ const stateUrlMatch = currentUrl.match(/state=([^&]+)/);
224
+ if (stateUrlMatch) {
225
+ stateToken = decodeURIComponent(stateUrlMatch[1]);
228
226
  }
229
- else if (identifierResponse && typeof identifierResponse === 'object') {
230
- const respObj = identifierResponse;
231
- identifierHtml = respObj.body || JSON.stringify(respObj);
227
+ // Also try to find state in the HTML
228
+ if (!stateToken) {
229
+ const stateHtmlMatch = htmlContent.match(/name="state"\s+value="([^"]+)"/);
230
+ if (stateHtmlMatch) {
231
+ stateToken = stateHtmlMatch[1];
232
+ }
232
233
  }
233
- else {
234
- identifierHtml = String(identifierResponse);
234
+ // Try to find state in form action URL
235
+ if (!stateToken) {
236
+ const formActionMatch = htmlContent.match(/action="[^"]*\?state=([^"&]+)/);
237
+ if (formActionMatch) {
238
+ stateToken = decodeURIComponent(formActionMatch[1]);
239
+ }
235
240
  }
236
- const csrf2Match = identifierHtml.match(/name="_csrf"\s+value="([^"]+)"/);
237
- const relayState2Match = identifierHtml.match(/name="relayState"\s+value="([^"]+)"/);
238
- const hmac2Match = identifierHtml.match(/name="hmac"\s+value="([^"]+)"/);
239
- const csrf2 = csrf2Match ? csrf2Match[1] : csrf;
240
- const relayState2 = relayState2Match ? relayState2Match[1] : relayState;
241
- const hmac2 = hmac2Match ? hmac2Match[1] : hmac;
242
- // Step 3: Submit password
243
- const authResponse = await context.helpers.httpRequest({
244
- method: 'POST',
245
- url: 'https://identity.vwgroup.io/signin-service/v1/signin/login/authenticate',
246
- headers: {
247
- 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
248
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
249
- 'Content-Type': 'application/x-www-form-urlencoded',
250
- },
251
- body: new URLSearchParams({
252
- _csrf: csrf2,
253
- relayState: relayState2,
254
- hmac: hmac2,
255
- email: email,
256
- password: password,
257
- }).toString(),
258
- encoding: 'text',
259
- returnFullResponse: true,
260
- ignoreHttpStatusErrors: true,
261
- });
262
- // Get the redirect URL which contains the authorization code
241
+ // Try legacy CSRF-based flow first
242
+ const csrfMatch = htmlContent.match(/name="_csrf"\s+value="([^"]+)"/);
243
+ const relayStateMatch = htmlContent.match(/name="relayState"\s+value="([^"]+)"/);
244
+ const hmacMatch = htmlContent.match(/name="hmac"\s+value="([^"]+)"/);
263
245
  let redirectUrl = '';
264
246
  let authHtml = '';
265
- if (typeof authResponse === 'string') {
266
- authHtml = authResponse;
247
+ if (csrfMatch) {
248
+ // Legacy flow with CSRF token
249
+ const csrf = csrfMatch[1];
250
+ const relayState = relayStateMatch ? relayStateMatch[1] : '';
251
+ const hmac = hmacMatch ? hmacMatch[1] : '';
252
+ // Step 2: Submit email
253
+ const identifierResponse = await context.helpers.httpRequest({
254
+ method: 'POST',
255
+ url: 'https://identity.vwgroup.io/signin-service/v1/signin/login/identifier',
256
+ headers: {
257
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
258
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
259
+ 'Content-Type': 'application/x-www-form-urlencoded',
260
+ },
261
+ body: new URLSearchParams({
262
+ _csrf: csrf,
263
+ relayState: relayState,
264
+ hmac: hmac,
265
+ email: email,
266
+ }).toString(),
267
+ encoding: 'text',
268
+ returnFullResponse: true,
269
+ ignoreHttpStatusErrors: true,
270
+ });
271
+ let identifierHtml;
272
+ if (typeof identifierResponse === 'string') {
273
+ identifierHtml = identifierResponse;
274
+ }
275
+ else if (identifierResponse && typeof identifierResponse === 'object') {
276
+ const respObj = identifierResponse;
277
+ identifierHtml = respObj.body || JSON.stringify(respObj);
278
+ }
279
+ else {
280
+ identifierHtml = String(identifierResponse);
281
+ }
282
+ const csrf2Match = identifierHtml.match(/name="_csrf"\s+value="([^"]+)"/);
283
+ const relayState2Match = identifierHtml.match(/name="relayState"\s+value="([^"]+)"/);
284
+ const hmac2Match = identifierHtml.match(/name="hmac"\s+value="([^"]+)"/);
285
+ const csrf2 = csrf2Match ? csrf2Match[1] : csrf;
286
+ const relayState2 = relayState2Match ? relayState2Match[1] : relayState;
287
+ const hmac2 = hmac2Match ? hmac2Match[1] : hmac;
288
+ // Step 3: Submit password
289
+ const authResponse = await context.helpers.httpRequest({
290
+ method: 'POST',
291
+ url: 'https://identity.vwgroup.io/signin-service/v1/signin/login/authenticate',
292
+ headers: {
293
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
294
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
295
+ 'Content-Type': 'application/x-www-form-urlencoded',
296
+ },
297
+ body: new URLSearchParams({
298
+ _csrf: csrf2,
299
+ relayState: relayState2,
300
+ hmac: hmac2,
301
+ email: email,
302
+ password: password,
303
+ }).toString(),
304
+ encoding: 'text',
305
+ returnFullResponse: true,
306
+ ignoreHttpStatusErrors: true,
307
+ });
308
+ if (typeof authResponse === 'string') {
309
+ authHtml = authResponse;
310
+ }
311
+ else if (authResponse && typeof authResponse === 'object') {
312
+ const respObj = authResponse;
313
+ const respHeaders = respObj.headers;
314
+ if (respHeaders && respHeaders.location) {
315
+ redirectUrl = respHeaders.location;
316
+ }
317
+ authHtml = respObj.body || '';
318
+ }
267
319
  }
268
- else if (authResponse && typeof authResponse === 'object') {
269
- const respObj = authResponse;
270
- const respHeaders = respObj.headers;
271
- if (respHeaders && respHeaders.location) {
272
- redirectUrl = respHeaders.location;
320
+ else if (stateToken || htmlContent.includes('auth0.com') || htmlContent.includes('/u/login')) {
321
+ // New Auth0-based flow
322
+ // Extract state from the login page URL if not already found
323
+ if (!stateToken) {
324
+ // Try to get state from the response URL or form
325
+ const loginFormMatch = htmlContent.match(/action="([^"]*\/u\/login[^"]*)"/);
326
+ if (loginFormMatch) {
327
+ const formUrl = loginFormMatch[1];
328
+ const formStateMatch = formUrl.match(/state=([^&"]+)/);
329
+ if (formStateMatch) {
330
+ stateToken = decodeURIComponent(formStateMatch[1]);
331
+ }
332
+ }
333
+ }
334
+ if (!stateToken) {
335
+ // Last resort: extract from any state parameter in the HTML
336
+ const anyStateMatch = htmlContent.match(/state=([a-zA-Z0-9_-]+)/);
337
+ if (anyStateMatch) {
338
+ stateToken = anyStateMatch[1];
339
+ }
340
+ }
341
+ if (!stateToken) {
342
+ throw new Error(`Could not extract state token from Auth0 login page. Response preview: ${htmlContent.substring(0, 500)}`);
343
+ }
344
+ // Submit credentials to Auth0 /u/login endpoint
345
+ const loginResponse = await context.helpers.httpRequest({
346
+ method: 'POST',
347
+ url: `https://identity.vwgroup.io/u/login?state=${encodeURIComponent(stateToken)}`,
348
+ headers: {
349
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
350
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
351
+ 'Content-Type': 'application/x-www-form-urlencoded',
352
+ 'Origin': 'https://identity.vwgroup.io',
353
+ 'Referer': `https://identity.vwgroup.io/u/login?state=${encodeURIComponent(stateToken)}`,
354
+ },
355
+ body: new URLSearchParams({
356
+ state: stateToken,
357
+ username: email,
358
+ password: password,
359
+ action: 'default',
360
+ }).toString(),
361
+ encoding: 'text',
362
+ returnFullResponse: true,
363
+ ignoreHttpStatusErrors: true,
364
+ });
365
+ if (typeof loginResponse === 'string') {
366
+ authHtml = loginResponse;
273
367
  }
274
- authHtml = respObj.body || '';
368
+ else if (loginResponse && typeof loginResponse === 'object') {
369
+ const respObj = loginResponse;
370
+ const respHeaders = respObj.headers;
371
+ if (respHeaders && respHeaders.location) {
372
+ redirectUrl = respHeaders.location;
373
+ }
374
+ authHtml = respObj.body || '';
375
+ }
376
+ }
377
+ else {
378
+ throw new Error(`Unknown login page format. Response preview: ${htmlContent.substring(0, 500)}`);
275
379
  }
276
380
  if (!redirectUrl && authHtml) {
277
381
  // Check if we got a redirect in the response HTML
@@ -280,17 +384,35 @@ async function vwLogin(context, email, password) {
280
384
  redirectUrl = redirectMatch[1];
281
385
  }
282
386
  }
283
- // Follow redirects to get the auth code
387
+ // Follow redirects to get the auth code, id_token, and access_token
284
388
  let authCode = '';
389
+ let idToken = '';
390
+ let accessToken = '';
285
391
  let maxRedirects = 10;
286
392
  while (maxRedirects > 0 && redirectUrl && !authCode) {
393
+ // Extract tokens from URL fragment or query params
394
+ // The response_type 'code id_token token' returns all three
287
395
  if (redirectUrl.includes('code=')) {
288
- const codeMatch = redirectUrl.match(/code=([^&]+)/);
396
+ const codeMatch = redirectUrl.match(/code=([^&#]+)/);
289
397
  if (codeMatch) {
290
398
  authCode = codeMatch[1];
291
- break;
292
399
  }
293
400
  }
401
+ if (redirectUrl.includes('id_token=')) {
402
+ const idTokenMatch = redirectUrl.match(/id_token=([^&#]+)/);
403
+ if (idTokenMatch) {
404
+ idToken = idTokenMatch[1];
405
+ }
406
+ }
407
+ if (redirectUrl.includes('access_token=')) {
408
+ const accessTokenMatch = redirectUrl.match(/access_token=([^&#]+)/);
409
+ if (accessTokenMatch) {
410
+ accessToken = accessTokenMatch[1];
411
+ }
412
+ }
413
+ if (authCode) {
414
+ break;
415
+ }
294
416
  const followResponse = await context.helpers.httpRequest({
295
417
  method: 'GET',
296
418
  url: redirectUrl.startsWith('http') ? redirectUrl : `https://identity.vwgroup.io${redirectUrl}`,
@@ -320,21 +442,48 @@ async function vwLogin(context, email, password) {
320
442
  throw new Error('Could not obtain authorization code. Please check your credentials.');
321
443
  }
322
444
  // Step 4: Exchange auth code for tokens
323
- const tokenResponse = await context.helpers.httpRequest({
324
- method: 'POST',
325
- url: 'https://tokenrefreshservice.apps.emea.vwapps.io/exchangeAuthCode',
326
- headers: {
327
- 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
328
- 'Accept': 'application/json',
329
- 'Content-Type': 'application/json',
330
- 'X-Client-Id': VW_CLIENT_ID,
331
- },
332
- body: {
333
- auth_code: authCode,
334
- id_token: '',
335
- brand: 'vw',
336
- },
337
- });
445
+ // Try the new CARIAD login endpoint first
446
+ let tokenResponse;
447
+ try {
448
+ const cariadResponse = await context.helpers.httpRequest({
449
+ method: 'POST',
450
+ url: 'https://emea.bff.cariad.digital/user-login/login/v1',
451
+ headers: {
452
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
453
+ 'Accept': 'application/json',
454
+ 'Content-Type': 'application/json',
455
+ },
456
+ body: {
457
+ state: authorizeParams.get('state'),
458
+ id_token: idToken,
459
+ redirect_uri: VW_REDIRECT_URI,
460
+ region: 'emea',
461
+ access_token: accessToken,
462
+ authorizationCode: authCode,
463
+ code_verifier: codeVerifier,
464
+ },
465
+ });
466
+ tokenResponse = cariadResponse;
467
+ }
468
+ catch {
469
+ // Fallback to old token exchange endpoint
470
+ tokenResponse = await context.helpers.httpRequest({
471
+ method: 'POST',
472
+ url: 'https://tokenrefreshservice.apps.emea.vwapps.io/exchangeAuthCode',
473
+ headers: {
474
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36',
475
+ 'Accept': 'application/json',
476
+ 'Content-Type': 'application/json',
477
+ 'X-Client-Id': VW_CLIENT_ID,
478
+ },
479
+ body: {
480
+ auth_code: authCode,
481
+ id_token: idToken || '',
482
+ brand: 'vw',
483
+ code_verifier: codeVerifier,
484
+ },
485
+ });
486
+ }
338
487
  return {
339
488
  accessToken: tokenResponse.access_token,
340
489
  refreshToken: tokenResponse.refresh_token,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-jygse-vw-weconnect",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "n8n community node for VW We Connect - Control your Volkswagen T6.1 and other VW vehicles",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",