mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-04-24 06:56:54 -05:00
Add Nintendo Account authorisation helpers
This commit is contained in:
parent
62d1982193
commit
0af48a18ae
|
|
@ -2,7 +2,7 @@ import fetch, { Response } from 'node-fetch';
|
|||
import { v4 as uuidgen } from 'uuid';
|
||||
import { f, FResult, HashMethod } from './f.js';
|
||||
import { AccountLogin, AccountToken, Announcements, CurrentUser, CurrentUserPermissions, Event, Friends, GetActiveEventResult, PresencePermissions, User, WebServices, WebServiceToken, CoralErrorResponse, CoralResponse, CoralStatus, CoralSuccessResponse, FriendCodeUser, FriendCodeUrl, AccountTokenParameter, AccountLoginParameter, WebServiceTokenParameter } from './coral-types.js';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
|
||||
import { generateAuthData, getNintendoAccountToken, getNintendoAccountUser, NintendoAccountSessionAuthorisation, NintendoAccountToken, NintendoAccountUser } from './na.js';
|
||||
import { ErrorResponse, ResponseSymbol } from './util.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { JwtPayload } from '../util/jwt.js';
|
||||
|
|
@ -412,6 +412,31 @@ export default class CoralApi {
|
|||
}
|
||||
}
|
||||
|
||||
const na_client_settings = {
|
||||
client_id: ZNCA_CLIENT_ID,
|
||||
scope: 'openid user user.birthday user.mii user.screenName',
|
||||
};
|
||||
|
||||
export class NintendoAccountSessionAuthorisationCoral extends NintendoAccountSessionAuthorisation {
|
||||
protected constructor(
|
||||
authorise_url: string,
|
||||
state: string,
|
||||
verifier: string,
|
||||
redirect_uri?: string,
|
||||
) {
|
||||
const { client_id, scope } = na_client_settings;
|
||||
|
||||
super(client_id, scope, authorise_url, state, verifier, redirect_uri);
|
||||
}
|
||||
|
||||
static create(/** @internal */ redirect_uri?: string) {
|
||||
const { client_id, scope } = na_client_settings;
|
||||
const auth_data = generateAuthData(client_id, scope, redirect_uri);
|
||||
|
||||
return new this(auth_data.url, auth_data.state, auth_data.verifier, redirect_uri);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CoralAuthData {
|
||||
nintendoAccountToken: NintendoAccountToken;
|
||||
user: NintendoAccountUser;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import fetch, { Response } from 'node-fetch';
|
||||
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
|
||||
import { generateAuthData, getNintendoAccountToken, getNintendoAccountUser, NintendoAccountSessionAuthorisation, NintendoAccountToken, NintendoAccountUser } from './na.js';
|
||||
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
|
||||
import { DailySummaries, Devices, MonthlySummaries, MonthlySummary, MoonError, ParentalControlSettingState, SmartDevices, User } from './moon-types.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
|
|
@ -177,6 +177,45 @@ export default class MoonApi {
|
|||
}
|
||||
}
|
||||
|
||||
const na_client_settings = {
|
||||
client_id: ZNMA_CLIENT_ID,
|
||||
scope: [
|
||||
'openid',
|
||||
'user',
|
||||
'user.mii',
|
||||
'moonUser:administration',
|
||||
'moonDevice:create',
|
||||
'moonOwnedDevice:administration',
|
||||
'moonParentalControlSetting',
|
||||
'moonParentalControlSetting:update',
|
||||
'moonParentalControlSettingState',
|
||||
'moonPairingState',
|
||||
'moonSmartDevice:administration',
|
||||
'moonDailySummary',
|
||||
'moonMonthlySummary',
|
||||
].join(' '),
|
||||
};
|
||||
|
||||
export class NintendoAccountSessionAuthorisationMoon extends NintendoAccountSessionAuthorisation {
|
||||
protected constructor(
|
||||
authorise_url: string,
|
||||
state: string,
|
||||
verifier: string,
|
||||
redirect_uri?: string,
|
||||
) {
|
||||
const { client_id, scope } = na_client_settings;
|
||||
|
||||
super(client_id, scope, authorise_url, state, verifier, redirect_uri);
|
||||
}
|
||||
|
||||
static create(/** @internal */ redirect_uri?: string) {
|
||||
const { client_id, scope } = na_client_settings;
|
||||
const auth_data = generateAuthData(client_id, scope, redirect_uri);
|
||||
|
||||
return new this(auth_data.url, auth_data.state, auth_data.verifier, redirect_uri);
|
||||
}
|
||||
}
|
||||
|
||||
export interface MoonAuthData {
|
||||
nintendoAccountToken: NintendoAccountToken;
|
||||
user: NintendoAccountUser;
|
||||
|
|
|
|||
100
src/api/na.ts
100
src/api/na.ts
|
|
@ -1,11 +1,107 @@
|
|||
import fetch from 'node-fetch';
|
||||
import { defineResponse, ErrorResponse } from './util.js';
|
||||
import * as crypto from 'node:crypto';
|
||||
import fetch, { Response } from 'node-fetch';
|
||||
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
|
||||
import createDebug from '../util/debug.js';
|
||||
import { JwtPayload } from '../util/jwt.js';
|
||||
import { timeoutSignal } from '../util/misc.js';
|
||||
|
||||
const debug = createDebug('nxapi:api:na');
|
||||
|
||||
export class NintendoAccountSessionAuthorisation {
|
||||
readonly scope: string;
|
||||
|
||||
protected constructor(
|
||||
readonly client_id: string,
|
||||
scope: string | string[],
|
||||
readonly authorise_url: string,
|
||||
readonly state: string,
|
||||
readonly verifier: string,
|
||||
readonly redirect_uri = 'npf' + client_id + '://auth',
|
||||
) {
|
||||
this.scope = typeof scope === 'string' ? scope : scope.join(' ');
|
||||
}
|
||||
|
||||
async getSessionToken(code: string, state?: string): Promise<HasResponse<NintendoAccountSessionToken, Response>>
|
||||
async getSessionToken(params: URLSearchParams): Promise<HasResponse<NintendoAccountSessionToken, Response>>
|
||||
async getSessionToken(code: string | URLSearchParams | null, state?: string | null) {
|
||||
if (code instanceof URLSearchParams) {
|
||||
if (code.has('error')) {
|
||||
throw NintendoAccountSessionAuthorisationError.fromSearchParams(code);
|
||||
}
|
||||
|
||||
state = code.get('state') || null;
|
||||
code = code.get('session_token_code');
|
||||
}
|
||||
|
||||
if (typeof state !== 'undefined' && state !== this.state) {
|
||||
throw new TypeError('Invalid state');
|
||||
}
|
||||
|
||||
if (typeof code !== 'string' || !code) {
|
||||
throw new TypeError('Invalid code');
|
||||
}
|
||||
|
||||
return getNintendoAccountSessionToken(code, this.verifier, this.client_id);
|
||||
}
|
||||
|
||||
static create(
|
||||
client_id: string,
|
||||
scope: string | string[],
|
||||
/** @internal */ redirect_uri = 'npf' + client_id + '://auth',
|
||||
) {
|
||||
if (typeof scope !== 'string') scope = scope.join(' ');
|
||||
|
||||
const auth_data = generateAuthData(client_id, scope, redirect_uri);
|
||||
|
||||
return new NintendoAccountSessionAuthorisation(client_id, scope,
|
||||
auth_data.url, auth_data.state, auth_data.verifier, redirect_uri);
|
||||
}
|
||||
}
|
||||
|
||||
export class NintendoAccountSessionAuthorisationError extends Error {
|
||||
constructor(readonly code: string, message?: string) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
static fromSearchParams(qs: URLSearchParams) {
|
||||
const code = qs.get('error') ?? 'unknown_error';
|
||||
const message = qs.get('error_description') ?? code;
|
||||
|
||||
return new NintendoAccountSessionAuthorisationError(code, message);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateAuthData(
|
||||
client_id: string,
|
||||
scope: string | string[],
|
||||
redirect_uri = 'npf' + client_id + '://auth',
|
||||
) {
|
||||
const state = crypto.randomBytes(36).toString('base64url');
|
||||
const verifier = crypto.randomBytes(32).toString('base64url');
|
||||
const challenge = crypto.createHash('sha256').update(verifier).digest().toString('base64url');
|
||||
|
||||
const params = {
|
||||
state,
|
||||
redirect_uri,
|
||||
client_id,
|
||||
scope: typeof scope === 'string' ? scope : scope.join(' '),
|
||||
response_type: 'session_token_code',
|
||||
session_token_code_challenge: challenge,
|
||||
session_token_code_challenge_method: 'S256',
|
||||
theme: 'login_form',
|
||||
};
|
||||
|
||||
const url = 'https://accounts.nintendo.com/connect/1.0.0/authorize?' +
|
||||
new URLSearchParams(params).toString();
|
||||
|
||||
return {
|
||||
url,
|
||||
state,
|
||||
verifier,
|
||||
challenge,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getNintendoAccountSessionToken(code: string, verifier: string, client_id: string) {
|
||||
debug('Getting Nintendo Account session token');
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import { protocol_registration_options } from './index.js';
|
|||
import { createModalWindow, createWindow } from './windows.js';
|
||||
import { tryGetNativeImageFromUrl } from './util.js';
|
||||
import { WindowType } from '../common/types.js';
|
||||
import { getNintendoAccountSessionToken, NintendoAccountAuthError, NintendoAccountSessionToken } from '../../api/na.js';
|
||||
import { ZNCA_CLIENT_ID } from '../../api/coral.js';
|
||||
import { ZNMA_CLIENT_ID } from '../../api/moon.js';
|
||||
import { getNintendoAccountSessionToken, NintendoAccountAuthError, NintendoAccountSessionAuthorisation, NintendoAccountSessionAuthorisationError, NintendoAccountSessionToken } from '../../api/na.js';
|
||||
import { NintendoAccountSessionAuthorisationCoral, ZNCA_CLIENT_ID } from '../../api/coral.js';
|
||||
import { NintendoAccountSessionAuthorisationMoon, ZNMA_CLIENT_ID } from '../../api/moon.js';
|
||||
import { ErrorResponse } from '../../api/util.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
|
|
@ -20,33 +20,6 @@ const debug = createDebug('app:main:na-auth');
|
|||
|
||||
export type NintendoAccountAuthResult = NintendoAccountSessionToken;
|
||||
|
||||
export function getAuthUrl(client_id: string, scope: string | string[]) {
|
||||
const state = crypto.randomBytes(36).toString('base64url');
|
||||
const verifier = crypto.randomBytes(32).toString('base64url');
|
||||
const challenge = crypto.createHash('sha256').update(verifier).digest().toString('base64url');
|
||||
|
||||
const params = {
|
||||
state,
|
||||
redirect_uri: 'npf' + client_id + '://auth',
|
||||
client_id,
|
||||
scope: typeof scope === 'string' ? scope : scope.join(' '),
|
||||
response_type: 'session_token_code',
|
||||
session_token_code_challenge: challenge,
|
||||
session_token_code_challenge_method: 'S256',
|
||||
theme: 'login_form',
|
||||
};
|
||||
|
||||
const url = 'https://accounts.nintendo.com/connect/1.0.0/authorize?' +
|
||||
new URLSearchParams(params).toString();
|
||||
|
||||
return {
|
||||
url,
|
||||
state,
|
||||
verifier,
|
||||
challenge,
|
||||
};
|
||||
}
|
||||
|
||||
const css = `
|
||||
html {
|
||||
overflow-x: hidden;
|
||||
|
|
@ -79,51 +52,45 @@ export function createAuthWindow() {
|
|||
}
|
||||
|
||||
export interface NintendoAccountSessionTokenCode {
|
||||
authenticator: NintendoAccountSessionAuthorisation;
|
||||
code: string;
|
||||
verifier: string;
|
||||
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 {
|
||||
export class AuthoriseCancelError extends NintendoAccountSessionAuthorisationError {
|
||||
constructor(message?: string) {
|
||||
super('access_denied', message);
|
||||
}
|
||||
}
|
||||
|
||||
export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: string | string[], close_window: false):
|
||||
Promise<NintendoAccountSessionTokenCode & {window: BrowserWindow}>
|
||||
export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: string | string[], close_window: true):
|
||||
Promise<NintendoAccountSessionTokenCode & {window?: never}>
|
||||
export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: string | string[], close_window?: boolean):
|
||||
Promise<NintendoAccountSessionTokenCode & {window?: BrowserWindow}>
|
||||
export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: string | string[], close_window = true) {
|
||||
export function getSessionTokenCodeByInAppBrowser(
|
||||
authenticator: NintendoAccountSessionAuthorisation, close_window: false,
|
||||
): Promise<NintendoAccountSessionTokenCode & {window: BrowserWindow}>
|
||||
export function getSessionTokenCodeByInAppBrowser(
|
||||
authenticator: NintendoAccountSessionAuthorisation, close_window: true,
|
||||
): Promise<NintendoAccountSessionTokenCode & {window?: never}>
|
||||
export function getSessionTokenCodeByInAppBrowser(
|
||||
authenticator: NintendoAccountSessionAuthorisation, close_window?: boolean,
|
||||
): Promise<NintendoAccountSessionTokenCode & {window?: BrowserWindow}>
|
||||
|
||||
export function getSessionTokenCodeByInAppBrowser(
|
||||
authenticator: NintendoAccountSessionAuthorisation, close_window = true,
|
||||
) {
|
||||
return new Promise<NintendoAccountSessionTokenCode>((rs, rj) => {
|
||||
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) {
|
||||
if (authorisedparams.get('state') !== authenticator.state) {
|
||||
rj(new Error('Invalid state'));
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (authorisedparams.has('error')) {
|
||||
rj(AuthoriseError.fromSearchParams(authorisedparams));
|
||||
rj(NintendoAccountSessionAuthorisationError.fromSearchParams(authorisedparams));
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
|
@ -141,15 +108,15 @@ export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: stri
|
|||
|
||||
if (close_window) {
|
||||
rs({
|
||||
authenticator,
|
||||
code,
|
||||
verifier,
|
||||
});
|
||||
|
||||
window.close();
|
||||
} else {
|
||||
rs({
|
||||
authenticator,
|
||||
code,
|
||||
verifier,
|
||||
window,
|
||||
});
|
||||
}
|
||||
|
|
@ -160,7 +127,7 @@ export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: stri
|
|||
|
||||
debug('will navigate', url);
|
||||
|
||||
if (url.protocol === 'npf' + client_id + ':' && url.host === 'auth') {
|
||||
if (url.protocol === 'npf' + authenticator.client_id + ':' && url.host === 'auth') {
|
||||
handleAuthUrl(url);
|
||||
event.preventDefault();
|
||||
} else if (url.origin === 'https://accounts.nintendo.com') {
|
||||
|
|
@ -183,7 +150,7 @@ export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: stri
|
|||
|
||||
debug('open', details);
|
||||
|
||||
if (url.protocol === 'npf' + client_id + ':' && url.host === 'auth') {
|
||||
if (url.protocol === 'npf' + authenticator.client_id + ':' && url.host === 'auth') {
|
||||
handleAuthUrl(url);
|
||||
} else {
|
||||
shell.openExternal(details.url);
|
||||
|
|
@ -192,40 +159,34 @@ export function getSessionTokenCodeByInAppBrowser(client_id: string, scope: stri
|
|||
return {action: 'deny'};
|
||||
});
|
||||
|
||||
debug('Loading Nintendo Account authorisation', {
|
||||
authoriseurl,
|
||||
state,
|
||||
verifier,
|
||||
challenge,
|
||||
});
|
||||
debug('Loading Nintendo Account authorisation', authenticator);
|
||||
|
||||
window.loadURL(authoriseurl);
|
||||
window.loadURL(authenticator.authorise_url);
|
||||
});
|
||||
}
|
||||
|
||||
const FORCE_MANUAL_AUTH_URI_ENTRY = process.env.NXAPI_FORCE_MANUAL_AUTH === '1';
|
||||
|
||||
export function getSessionTokenCodeByDefaultBrowser(
|
||||
client_id: string, scope: string | string[],
|
||||
authenticator: NintendoAccountSessionAuthorisation,
|
||||
close_window = true,
|
||||
force_manual = FORCE_MANUAL_AUTH_URI_ENTRY
|
||||
) {
|
||||
return new Promise<NintendoAccountSessionTokenCode>((rs, rj) => {
|
||||
const {url: authoriseurl, state, verifier, challenge} = getAuthUrl(client_id, scope);
|
||||
let window: BrowserWindow | undefined = undefined;
|
||||
|
||||
const handleAuthUrl = (url: URL) => {
|
||||
const authorisedparams = new URLSearchParams(url.hash.substr(1));
|
||||
debug('Redirect URL parameters', [...authorisedparams.entries()]);
|
||||
|
||||
if (authorisedparams.get('state') !== state) {
|
||||
if (authorisedparams.get('state') !== authenticator.state) {
|
||||
rj(new Error('Invalid state'));
|
||||
window?.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (authorisedparams.has('error')) {
|
||||
rj(AuthoriseError.fromSearchParams(authorisedparams));
|
||||
rj(NintendoAccountSessionAuthorisationError.fromSearchParams(authorisedparams));
|
||||
window?.close();
|
||||
return;
|
||||
}
|
||||
|
|
@ -242,28 +203,23 @@ export function getSessionTokenCodeByDefaultBrowser(
|
|||
debug('code', code, jwt, sig);
|
||||
|
||||
if (window && close_window) window.close();
|
||||
else if (window) rs({code, verifier, window});
|
||||
else rs({code, verifier});
|
||||
else if (window) rs({authenticator, code, window});
|
||||
else rs({authenticator, code});
|
||||
};
|
||||
|
||||
debug('Prompting user for Nintendo Account authorisation', {
|
||||
authoriseurl,
|
||||
state,
|
||||
verifier,
|
||||
challenge,
|
||||
});
|
||||
debug('Prompting user for Nintendo Account authorisation', authenticator);
|
||||
|
||||
const protocol = 'npf' + client_id;
|
||||
const protocol = 'npf' + authenticator.client_id;
|
||||
|
||||
if (force_manual) {
|
||||
debug('Manual entry forced, prompting for redirect URI');
|
||||
window = askUserForRedirectUri(authoriseurl, client_id, handleAuthUrl, rj);
|
||||
window = askUserForRedirectUri(authenticator.authorise_url, authenticator.client_id, handleAuthUrl, rj);
|
||||
} else if (app.isDefaultProtocolClient(protocol,
|
||||
protocol_registration_options?.path, protocol_registration_options?.argv
|
||||
)) {
|
||||
debug('App is already default protocol handler, opening browser');
|
||||
auth_state.set(state, [handleAuthUrl, rj, protocol]);
|
||||
shell.openExternal(authoriseurl);
|
||||
auth_state.set(authenticator.state, [handleAuthUrl, rj, protocol]);
|
||||
shell.openExternal(authenticator.authorise_url);
|
||||
} else {
|
||||
const registered_app = app.getApplicationNameForProtocol(protocol);
|
||||
|
||||
|
|
@ -271,11 +227,11 @@ export function getSessionTokenCodeByDefaultBrowser(
|
|||
protocol_registration_options?.path, protocol_registration_options?.argv
|
||||
)) {
|
||||
debug('Another app is using the auth protocol or registration failed, prompting for redirect URI');
|
||||
window = askUserForRedirectUri(authoriseurl, client_id, handleAuthUrl, rj);
|
||||
window = askUserForRedirectUri(authenticator.authorise_url, authenticator.client_id, handleAuthUrl, rj);
|
||||
} else {
|
||||
debug('App is now default protocol handler, opening browser');
|
||||
auth_state.set(state, [handleAuthUrl, rj, protocol]);
|
||||
shell.openExternal(authoriseurl);
|
||||
auth_state.set(authenticator.state, [handleAuthUrl, rj, protocol]);
|
||||
shell.openExternal(authenticator.authorise_url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -346,9 +302,10 @@ const NSO_SCOPE = [
|
|||
];
|
||||
|
||||
export async function addNsoAccount(storage: persist.LocalStorage, use_in_app_browser = true) {
|
||||
const {code, verifier, window} = use_in_app_browser ?
|
||||
await getSessionTokenCodeByInAppBrowser(ZNCA_CLIENT_ID, NSO_SCOPE, false) :
|
||||
await getSessionTokenCodeByDefaultBrowser(ZNCA_CLIENT_ID, NSO_SCOPE, false);
|
||||
const authenticator = NintendoAccountSessionAuthorisationCoral.create();
|
||||
const {code, window} = use_in_app_browser ?
|
||||
await getSessionTokenCodeByInAppBrowser(authenticator, false) :
|
||||
await getSessionTokenCodeByDefaultBrowser(authenticator, false);
|
||||
|
||||
window?.setFocusable(false);
|
||||
window?.blurWebView();
|
||||
|
|
@ -378,7 +335,7 @@ export async function addNsoAccount(storage: persist.LocalStorage, use_in_app_br
|
|||
|
||||
if (data.error === 'invalid_grant') {
|
||||
// The session token has expired/was revoked
|
||||
return authenticateCoralSessionToken(storage, code, verifier, true);
|
||||
return authenticateCoralSessionToken(storage, authenticator, code, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -388,7 +345,7 @@ export async function addNsoAccount(storage: persist.LocalStorage, use_in_app_br
|
|||
|
||||
await checkZncaApiUseAllowed(storage, window);
|
||||
|
||||
return authenticateCoralSessionToken(storage, code, verifier);
|
||||
return authenticateCoralSessionToken(storage, authenticator, code);
|
||||
} finally {
|
||||
window?.close();
|
||||
}
|
||||
|
|
@ -396,10 +353,10 @@ export async function addNsoAccount(storage: persist.LocalStorage, use_in_app_br
|
|||
|
||||
async function authenticateCoralSessionToken(
|
||||
storage: persist.LocalStorage,
|
||||
code: string, verifier: string,
|
||||
authenticator: NintendoAccountSessionAuthorisation, code: string,
|
||||
reauthenticate = false,
|
||||
) {
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNCA_CLIENT_ID);
|
||||
const token = await authenticator.getSessionToken(code);
|
||||
|
||||
debug('session token', token);
|
||||
|
||||
|
|
@ -426,7 +383,7 @@ export async function askAddNsoAccount(storage: persist.LocalStorage, iab = true
|
|||
try {
|
||||
return await addNsoAccount(storage, iab);
|
||||
} catch (err: any) {
|
||||
if (err instanceof AuthoriseError && err.code === 'access_denied') return;
|
||||
if (err instanceof NintendoAccountSessionAuthorisationError && err.code === 'access_denied') return;
|
||||
|
||||
dialog.showErrorBox('Error adding account', err.stack || err.message);
|
||||
}
|
||||
|
|
@ -512,9 +469,10 @@ const MOON_SCOPE = [
|
|||
];
|
||||
|
||||
export async function addPctlAccount(storage: persist.LocalStorage, use_in_app_browser = true) {
|
||||
const {code, verifier, window} = use_in_app_browser ?
|
||||
await getSessionTokenCodeByInAppBrowser(ZNMA_CLIENT_ID, MOON_SCOPE, false) :
|
||||
await getSessionTokenCodeByDefaultBrowser(ZNMA_CLIENT_ID, MOON_SCOPE, false);
|
||||
const authenticator = NintendoAccountSessionAuthorisationMoon.create();
|
||||
const {code, window} = use_in_app_browser ?
|
||||
await getSessionTokenCodeByInAppBrowser(authenticator, false) :
|
||||
await getSessionTokenCodeByDefaultBrowser(authenticator, false);
|
||||
|
||||
window?.setFocusable(false);
|
||||
window?.blurWebView();
|
||||
|
|
@ -542,7 +500,7 @@ export async function addPctlAccount(storage: persist.LocalStorage, use_in_app_b
|
|||
|
||||
if (data.error === 'invalid_grant') {
|
||||
// The session token has expired/was revoked
|
||||
return authenticateMoonSessionToken(storage, code, verifier, true);
|
||||
return authenticateMoonSessionToken(storage, authenticator, code, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -550,7 +508,7 @@ export async function addPctlAccount(storage: persist.LocalStorage, use_in_app_b
|
|||
}
|
||||
}
|
||||
|
||||
return authenticateMoonSessionToken(storage, code, verifier);
|
||||
return authenticateMoonSessionToken(storage, authenticator, code);
|
||||
} finally {
|
||||
window?.close();
|
||||
}
|
||||
|
|
@ -558,11 +516,10 @@ export async function addPctlAccount(storage: persist.LocalStorage, use_in_app_b
|
|||
|
||||
async function authenticateMoonSessionToken(
|
||||
storage: persist.LocalStorage,
|
||||
code: string, verifier: string,
|
||||
authenticator: NintendoAccountSessionAuthorisation, code: string,
|
||||
reauthenticate = false,
|
||||
) {
|
||||
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNMA_CLIENT_ID);
|
||||
const token = await authenticator.getSessionToken(code);
|
||||
|
||||
debug('session token', token);
|
||||
|
||||
|
|
@ -586,7 +543,7 @@ export async function askAddPctlAccount(storage: persist.LocalStorage, iab = tru
|
|||
try {
|
||||
return await addPctlAccount(storage, iab);
|
||||
} catch (err: any) {
|
||||
if (err instanceof AuthoriseError && err.code === 'access_denied') return;
|
||||
if (err instanceof NintendoAccountSessionAuthorisationError && err.code === 'access_denied') return;
|
||||
|
||||
dialog.showErrorBox('Error adding account', err.stack || err.message);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import * as crypto from 'node:crypto';
|
||||
import type { Arguments as ParentArguments } from '../nso.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getToken } from '../../common/auth/coral.js';
|
||||
import { getNintendoAccountSessionToken } from '../../api/na.js';
|
||||
import { ZNCA_CLIENT_ID } from '../../api/coral.js';
|
||||
import { NintendoAccountSessionAuthorisationCoral } from '../../api/coral.js';
|
||||
import prompt from '../util/prompt.js';
|
||||
|
||||
const debug = createDebug('cli:nso:auth');
|
||||
|
|
@ -27,33 +25,13 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const state = crypto.randomBytes(36).toString('base64url');
|
||||
const verifier = crypto.randomBytes(32).toString('base64url');
|
||||
const challenge = crypto.createHash('sha256').update(verifier).digest().toString('base64url');
|
||||
const authenticator = NintendoAccountSessionAuthorisationCoral.create();
|
||||
|
||||
const params = {
|
||||
state,
|
||||
redirect_uri: 'npf71b963c1b7b6d119://auth',
|
||||
client_id: ZNCA_CLIENT_ID,
|
||||
scope: 'openid user user.birthday user.mii user.screenName',
|
||||
response_type: 'session_token_code',
|
||||
session_token_code_challenge: challenge,
|
||||
session_token_code_challenge_method: 'S256',
|
||||
theme: 'login_form',
|
||||
};
|
||||
|
||||
const authoriseurl = 'https://accounts.nintendo.com/connect/1.0.0/authorize?' +
|
||||
new URLSearchParams(params).toString();
|
||||
|
||||
debug('Authentication parameters', {
|
||||
state,
|
||||
verifier,
|
||||
challenge,
|
||||
}, params);
|
||||
debug('Authentication parameters', authenticator);
|
||||
|
||||
console.log('1. Open this URL and login to your Nintendo Account:');
|
||||
console.log('');
|
||||
console.log(authoriseurl);
|
||||
console.log(authenticator.authorise_url);
|
||||
console.log('');
|
||||
|
||||
console.log('2. On the "Linking an External Account" page, right click "Select this person" and copy the link. It should start with "npf71b963c1b7b6d119://auth".');
|
||||
|
|
@ -69,8 +47,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
const authorisedparams = new URLSearchParams(authorisedurl.hash.substr(1));
|
||||
debug('Redirect URL parameters', [...authorisedparams.entries()]);
|
||||
|
||||
const code = authorisedparams.get('session_token_code')!;
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNCA_CLIENT_ID);
|
||||
const token = await authenticator.getSessionToken(authorisedparams);
|
||||
|
||||
console.log('Session token', token);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import * as crypto from 'node:crypto';
|
||||
import type { Arguments as ParentArguments } from '../pctl.js';
|
||||
import createDebug from '../../util/debug.js';
|
||||
import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js';
|
||||
import { initStorage } from '../../util/storage.js';
|
||||
import { getPctlToken } from '../../common/auth/moon.js';
|
||||
import { getNintendoAccountSessionToken } from '../../api/na.js';
|
||||
import { ZNMA_CLIENT_ID } from '../../api/moon.js';
|
||||
import { NintendoAccountSessionAuthorisationMoon } from '../../api/moon.js';
|
||||
import prompt from '../util/prompt.js';
|
||||
|
||||
const debug = createDebug('cli:pctl:auth');
|
||||
|
|
@ -27,46 +25,13 @@ export function builder(yargs: Argv<ParentArguments>) {
|
|||
type Arguments = YargsArguments<ReturnType<typeof builder>>;
|
||||
|
||||
export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
||||
const state = crypto.randomBytes(36).toString('base64url');
|
||||
const verifier = crypto.randomBytes(32).toString('base64url');
|
||||
const challenge = crypto.createHash('sha256').update(verifier).digest().toString('base64url');
|
||||
const authenticator = NintendoAccountSessionAuthorisationMoon.create();
|
||||
|
||||
const params = {
|
||||
state,
|
||||
redirect_uri: 'npf54789befb391a838://auth',
|
||||
client_id: ZNMA_CLIENT_ID,
|
||||
scope: [
|
||||
'openid',
|
||||
'user',
|
||||
'user.mii',
|
||||
'moonUser:administration',
|
||||
'moonDevice:create',
|
||||
'moonOwnedDevice:administration',
|
||||
'moonParentalControlSetting',
|
||||
'moonParentalControlSetting:update',
|
||||
'moonParentalControlSettingState',
|
||||
'moonPairingState',
|
||||
'moonSmartDevice:administration',
|
||||
'moonDailySummary',
|
||||
'moonMonthlySummary',
|
||||
].join(' '),
|
||||
response_type: 'session_token_code',
|
||||
session_token_code_challenge: challenge,
|
||||
session_token_code_challenge_method: 'S256',
|
||||
};
|
||||
|
||||
const authoriseurl = 'https://accounts.nintendo.com/connect/1.0.0/authorize?' +
|
||||
new URLSearchParams(params).toString();
|
||||
|
||||
debug('Authentication parameters', {
|
||||
state,
|
||||
verifier,
|
||||
challenge,
|
||||
}, params);
|
||||
debug('Authentication parameters', authenticator);
|
||||
|
||||
console.log('1. Open this URL and login to your Nintendo Account:');
|
||||
console.log('');
|
||||
console.log(authoriseurl);
|
||||
console.log(authenticator.authorise_url);
|
||||
console.log('');
|
||||
|
||||
console.log('2. On the "Linking an External Account" page, right click "Select this person" and copy the link. It should start with "npf54789befb391a838://auth".');
|
||||
|
|
@ -82,8 +47,7 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
|
|||
const authorisedparams = new URLSearchParams(authorisedurl.hash.substr(1));
|
||||
debug('Redirect URL parameters', [...authorisedparams.entries()]);
|
||||
|
||||
const code = authorisedparams.get('session_token_code')!;
|
||||
const token = await getNintendoAccountSessionToken(code, verifier, ZNMA_CLIENT_ID);
|
||||
const token = await authenticator.getSessionToken(authorisedparams);
|
||||
|
||||
console.log('Session token', token);
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ export {
|
|||
|
||||
ResponseDataSymbol,
|
||||
CorrelationIdSymbol,
|
||||
|
||||
NintendoAccountSessionAuthorisationCoral,
|
||||
} from '../api/coral.js';
|
||||
|
||||
export * from '../api/coral-types.js';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export { getTitleIdFromEcUrl } from '../util/misc.js';
|
|||
export { ErrorResponse, ResponseSymbol } from '../api/util.js';
|
||||
export { addUserAgent, addUserAgentFromPackageJson } from '../util/useragent.js';
|
||||
|
||||
export { version } from '../util/product.js';
|
||||
export { version, product } from '../util/product.js';
|
||||
|
||||
export {
|
||||
default as Users,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ export {
|
|||
default,
|
||||
MoonAuthData,
|
||||
PartialMoonAuthData,
|
||||
|
||||
NintendoAccountSessionAuthorisationMoon,
|
||||
} from '../api/moon.js';
|
||||
|
||||
export * from '../api/moon-types.js';
|
||||
|
|
|
|||
12
src/exports/nintendo-account.ts
Normal file
12
src/exports/nintendo-account.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export {
|
||||
NintendoAccountSessionAuthorisation,
|
||||
NintendoAccountSessionAuthorisationError,
|
||||
|
||||
NintendoAccountSessionToken,
|
||||
NintendoAccountToken,
|
||||
NintendoAccountAuthError,
|
||||
|
||||
NintendoAccountUser,
|
||||
Mii,
|
||||
NintendoAccountError,
|
||||
} from '../api/na.js';
|
||||
Loading…
Reference in New Issue
Block a user