From b185b59d9113791959cbca86889f18a54abf9390 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Tue, 28 Jun 2022 20:25:54 +0100 Subject: [PATCH] Handle errors from the Nintendo Account authorisation page --- src/app/main/ipc.ts | 10 ++-- src/app/main/menu.ts | 19 ++----- src/app/main/na-auth.ts | 116 +++++++++++++++++++++++++++++++--------- 3 files changed, 100 insertions(+), 45 deletions(-) diff --git a/src/app/main/ipc.ts b/src/app/main/ipc.ts index abd6eec..afbb51d 100644 --- a/src/app/main/ipc.ts +++ b/src/app/main/ipc.ts @@ -6,7 +6,7 @@ import openWebService, { WebServiceIpc } from './webservices.js'; import { createWindow, getWindowConfiguration } from './windows.js'; import { DiscordPresenceConfiguration, DiscordPresenceSource, WindowType } from '../common/types.js'; import { CurrentUser, Friend, Game, PresenceState, WebService } from '../../api/coral-types.js'; -import { addNsoAccount, addPctlAccount } from './na-auth.js'; +import { addNsoAccount, addPctlAccount, askAddNsoAccount, askAddPctlAccount, AuthoriseError } from './na-auth.js'; import { App } from './index.js'; import { NintendoAccountUser } from '../../api/na.js'; import { hrduration } from '../../util/misc.js'; @@ -45,8 +45,8 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) { }, 60 * 60 * 1000); ipcMain.handle('nxapi:accounts:list', () => storage.getItem('NintendoAccountIds')); - ipcMain.handle('nxapi:accounts:add-coral', () => addNsoAccount(store.storage).then(u => u.data.user.id)); - ipcMain.handle('nxapi:accounts:add-moon', () => addPctlAccount(store.storage).then(u => u.data.user.id)); + ipcMain.handle('nxapi:accounts:add-coral', () => askAddNsoAccount(store.storage).then(u => u?.data.user.id)); + ipcMain.handle('nxapi:accounts:add-moon', () => askAddPctlAccount(store.storage).then(u => u?.data.user.id)); ipcMain.handle('nxapi:nso:gettoken', (e, id: string) => storage.getItem('NintendoAccountToken.' + id)); ipcMain.handle('nxapi:nso:getcachedtoken', (e, token: string) => storage.getItem('NsoToken.' + token)); @@ -147,8 +147,8 @@ export function setupIpc(appinstance: App, ipcMain: IpcMain) { new MenuItem({label: 'Use the nxapi command to remove this user', enabled: false}), ]).popup({window: BrowserWindow.fromWebContents(e.sender)!}), undefined)); ipcMain.handle('nxapi:menu:add-user', e => (Menu.buildFromTemplate([ - new MenuItem({label: 'Add Nintendo Switch Online account', click: () => addNsoAccount(storage)}), - new MenuItem({label: 'Add Nintendo Switch Parental Controls account', click: () => addPctlAccount(storage)}), + new MenuItem({label: 'Add Nintendo Switch Online account', click: () => askAddNsoAccount(storage)}), + new MenuItem({label: 'Add Nintendo Switch Parental Controls account', click: () => askAddPctlAccount(storage)}), ]).popup({window: BrowserWindow.fromWebContents(e.sender)!}), undefined)); ipcMain.handle('nxapi:menu:friend-code', (e, fc: CurrentUser['links']['friendCode']) => (Menu.buildFromTemplate([ new MenuItem({label: 'SW-' + fc.id, enabled: false}), diff --git a/src/app/main/menu.ts b/src/app/main/menu.ts index 7e2b4d9..71e7840 100644 --- a/src/app/main/menu.ts +++ b/src/app/main/menu.ts @@ -1,6 +1,6 @@ import { app, dialog, Menu, Tray, nativeImage, MenuItem } from './electron.js'; import createDebug from 'debug'; -import { addNsoAccount, addPctlAccount } from './na-auth.js'; +import { askAddNsoAccount, askAddPctlAccount } from './na-auth.js'; import { App } from './index.js'; import { WebService } from '../../api/coral-types.js'; import openWebService from './webservices.js'; @@ -105,21 +105,8 @@ export default class MenuApp { this.tray.setContextMenu(menu); } - addNsoAccount = () => { - addNsoAccount(this.app.store.storage).catch(err => { - if (err.message === 'Canceled') return; - - dialog.showErrorBox('Error adding account', err.stack || err.message); - }); - }; - - addPctlAccount = () => { - addPctlAccount(this.app.store.storage).catch(err => { - if (err.message === 'Canceled') return; - - dialog.showErrorBox('Error adding account', err.stack || err.message); - }); - }; + addNsoAccount = () => askAddNsoAccount(this.app.store.storage); + addPctlAccount = () => askAddPctlAccount(this.app.store.storage); // Hardcode these temporarily until they are cached webservices: WebService[] | null = [ diff --git a/src/app/main/na-auth.ts b/src/app/main/na-auth.ts index 263bc30..34f7d8d 100644 --- a/src/app/main/na-auth.ts +++ b/src/app/main/na-auth.ts @@ -80,6 +80,23 @@ export interface NintendoAccountSessionTokenCode { window?: BrowserWindow; } +export class AuthoriseError extends Error { + constructor(readonly code: string, message?: string) { + super(message); + } + + static fromSearchParams(qs: URLSearchParams) { + const code = qs.get('error') ?? 'unknown_error'; + return new AuthoriseError(code, qs.get('error_description') ?? code); + } +} + +export class AuthoriseCancelError extends AuthoriseError { + constructor(message?: string) { + super('access_denied', message); + } +} + export function getSessionTokenCode(client_id: string, scope: string | string[], close_window: false): Promise export function getSessionTokenCode(client_id: string, scope: string | string[], close_window: true): @@ -91,34 +108,57 @@ export function getSessionTokenCode(client_id: string, scope: string | string[], const {url: authoriseurl, state, verifier, challenge} = getAuthUrl(client_id, scope); const window = createAuthWindow(); + const handleAuthUrl = (url: URL) => { + const authorisedparams = new URLSearchParams(url.hash.substr(1)); + debug('Redirect URL parameters', [...authorisedparams.entries()]); + + if (authorisedparams.get('state') !== state) { + rj(new Error('Invalid state')); + window.close(); + return; + } + + if (authorisedparams.has('error')) { + rj(AuthoriseError.fromSearchParams(authorisedparams)); + window.close(); + return; + } + + if (!authorisedparams.has('session_token_code')) { + rj(new Error('Response didn\'t include a session token code')); + window.close(); + return; + } + + const code = authorisedparams.get('session_token_code')!; + const [jwt, sig] = Jwt.decode(code); + + debug('code', code, jwt, sig); + + if (close_window) { + rs({ + code, + verifier, + }); + + window.close(); + } else { + rs({ + code, + verifier, + window, + }); + } + }; + window.webContents.on('will-navigate', (event, url_string) => { const url = new URL(url_string); debug('will navigate', url); if (url.protocol === 'npf' + client_id + ':' && url.host === 'auth') { - const authorisedparams = new URLSearchParams(url.hash.substr(1)); - debug('Redirect URL parameters', [...authorisedparams.entries()]); - - const code = authorisedparams.get('session_token_code')!; - const [jwt, sig] = Jwt.decode(code); - - debug('code', code, jwt, sig); - - if (close_window) { - rs({ - code, - verifier, - }); - - window.close(); - } else { - rs({ - code, - verifier, - window, - }); - } + handleAuthUrl(url); + event.preventDefault(); } else if (url.origin === 'https://accounts.nintendo.com') { // Ok } else { @@ -127,7 +167,7 @@ export function getSessionTokenCode(client_id: string, scope: string | string[], }); window.on('closed', () => { - rj(new Error('Canceled')); + rj(new AuthoriseCancelError('Canceled')); }); window.webContents.on('did-fail-load', e => rj(e)); @@ -135,8 +175,16 @@ export function getSessionTokenCode(client_id: string, scope: string | string[], window.webContents.userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 15_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.3 Mobile/15E148 Safari/604.1'; window.webContents.setWindowOpenHandler(details => { + const url = new URL(details.url); + debug('open', details); - shell.openExternal(details.url); + + if (url.protocol === 'npf' + client_id + ':' && url.host === 'auth') { + handleAuthUrl(url); + } else { + shell.openExternal(details.url); + } + return {action: 'deny'}; }); @@ -208,6 +256,16 @@ export async function addNsoAccount(storage: persist.LocalStorage) { } } +export async function askAddNsoAccount(storage: persist.LocalStorage) { + try { + return await addNsoAccount(storage); + } catch (err: any) { + if (err instanceof AuthoriseError && err.code === 'access_denied') return; + + dialog.showErrorBox('Error adding account', err.stack || err.message); + } +} + async function checkZncaApiUseAllowed(storage: persist.LocalStorage, window?: BrowserWindow, force = false) { if (!force) { if (await storage.getItem('ZncaApiConsent')) { @@ -330,3 +388,13 @@ export async function addPctlAccount(storage: persist.LocalStorage) { window?.close(); } } + +export async function askAddPctlAccount(storage: persist.LocalStorage) { + try { + return await addPctlAccount(storage); + } catch (err: any) { + if (err instanceof AuthoriseError && err.code === 'access_denied') return; + + dialog.showErrorBox('Error adding account', err.stack || err.message); + } +}