Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/services/skoda/skodaAuth.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,16 @@ function parseIdk(html) {
}

function parseFragment(location) {
const hash = location.split('#')[1] || '';
return Object.fromEntries(new URLSearchParams(hash));
// response_type=code returns the auth code as a QUERY param on the
// myskoda:// redirect; older hybrid flows used the #fragment. Read both,
// query first (the real Skoda behaviour), fragment as fallback.
const params = {};
const afterScheme = location.split('://')[1] || location;
const query = (afterScheme.split('?')[1] || '').split('#')[0];
const fragment = location.split('#')[1] || '';
for (const [k, v] of new URLSearchParams(fragment)) params[k] = v;
for (const [k, v] of new URLSearchParams(query)) params[k] = v;
return params;
}

function formBody(fields) { return new URLSearchParams(fields).toString(); }
Expand All @@ -62,10 +70,11 @@ async function login(email, password, { fetchImpl = fetch } = {}) {
client_id: CLIENT_ID,
nonce,
redirect_uri: REDIRECT_URI,
response_type: 'code id_token',
response_type: 'code',
scope: SCOPES,
code_challenge: challenge,
code_challenge_method: 'S256',
code_challenge_method: 's256',
prompt: 'login',
}).toString();

const start = await followRedirects(jar, authorizeUrl, { method: 'GET', headers: { accept: 'text/html' } }, { fetchImpl });
Expand Down
16 changes: 14 additions & 2 deletions tests/skoda_auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,18 @@ test('parseIdk throws SKODA_AUTH_FLOW_CHANGED on unexpected html', () => {
assert.throws(() => auth.parseIdk('<html>maintenance</html>'), (e) => e.code === 'SKODA_AUTH_FLOW_CHANGED');
});

test('parseFragment reads code from myskoda redirect', () => {
test('parseFragment reads code from myskoda redirect (fragment)', () => {
const params = auth.parseFragment('myskoda://redirect/login/#code=THECODE&token_type=bearer&id_token=IDT');
assert.equal(params.code, 'THECODE');
assert.equal(params.id_token, 'IDT');
});

test('parseFragment reads code from query (response_type=code)', () => {
// Real Skoda behaviour: response_type=code returns the code as a query param.
const params = auth.parseFragment('myskoda://redirect/login/?state=x&code=QCODE');
assert.equal(params.code, 'QCODE');
});

function htmlRes(body) {
return { status: 200, headers: new Headers({ 'content-type': 'text/html' }), text: async () => body, json: async () => ({}) };
}
Expand All @@ -49,14 +55,20 @@ test('login walks the full flow and exchanges the code', async () => {
if (url.startsWith(auth.IDENT_BASE + '/oidc/v1/authorize')) return htmlRes(emailPage);
if (url.includes('/login/identifier')) return htmlRes(passwordPage);
if (url.includes('/login/authenticate')) return redirectRes(auth.IDENT_BASE + '/oidc/v1/oauth/sso?x=1', ['SESSION=s1']);
if (url.includes('/oidc/v1/oauth/sso')) return redirectRes('myskoda://redirect/login/#code=THECODE&id_token=IDT');
// response_type=code → code comes back as a QUERY param on the redirect
if (url.includes('/oidc/v1/oauth/sso')) return redirectRes('myskoda://redirect/login/?code=THECODE');
if (url.startsWith(auth.API_BASE + '/api/v1/authentication/exchange-authorization-code')) {
return jsonRes({ accessToken: 'AT', refreshToken: 'RT', idToken: 'IDT' });
}
throw new Error('unexpected url ' + url);
};
const tokens = await auth.login('a@b.c', 'pw', { fetchImpl });
assert.deepEqual(tokens, { accessToken: 'AT', refreshToken: 'RT', idToken: 'IDT' });
// authorize request must use the real Skoda params
const authorize = seen.find((s) => s.url.includes('/oidc/v1/authorize'));
assert.match(authorize.url, /response_type=code(&|$)/);
assert.match(authorize.url, /code_challenge_method=s256/);
assert.match(authorize.url, /prompt=login/);
const exchange = seen.find((s) => s.url.includes('exchange-authorization-code'));
const body = JSON.parse(exchange.body);
assert.equal(body.code, 'THECODE');
Expand Down
Loading