Handle errors from the Nintendo Account authorisation page

This commit is contained in:
Samuel Elliott
2022-06-28 20:25:54 +01:00
parent 5e25570d2d
commit b185b59d91
3 changed files with 100 additions and 45 deletions

View File

@@ -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}),

View File

@@ -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 = [

View File

@@ -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<NintendoAccountSessionTokenCode & {window: BrowserWindow}>
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);
}
}