Add a command to get a session token

This commit is contained in:
Samuel Elliott 2022-03-16 20:14:33 +00:00
parent 5610cf8630
commit 5aba992899
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
4 changed files with 145 additions and 19 deletions

View File

@ -4,6 +4,35 @@ import { ErrorResponse } from './util.js';
const debug = createDebug('api:na');
export async function getNintendoAccountSessionToken(code: string, verifier: string, client_id: string) {
debug('Getting Nintendo Account session token');
const response = await fetch('https://accounts.nintendo.com/connect/1.0.0/api/session_token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Platform': 'Android',
'X-ProductVersion': '2.0.0',
'User-Agent': 'NASDKAPI; Android',
},
body: new URLSearchParams({
client_id,
session_token_code: code,
session_token_code_verifier: verifier,
}).toString(),
});
const token = await response.json() as NintendoAccountSessionToken | NintendoAccountError;
if ('errorCode' in token) {
throw new ErrorResponse('[na] + ' + token.detail, response, token);
}
debug('Got Nintendo Account session token', token);
return token;
}
export async function getNintendoAccountToken(token: string) {
debug('Getting Nintendo Account token');
@ -56,6 +85,11 @@ export async function getNintendoAccountUser(token: NintendoAccountToken) {
return user;
}
export interface NintendoAccountSessionToken {
session_token: string;
code: string;
}
export interface NintendoAccountToken {
scope: ['openid', 'user', 'user.birthday', 'user.mii', 'user.screenName'];
token_type: 'Bearer';

99
src/cli/auth.ts Normal file
View File

@ -0,0 +1,99 @@
import * as util from 'util';
import createDebug from 'debug';
import * as crypto from 'crypto';
import type { Arguments as ParentArguments } from '../cli.js';
import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../util.js';
import { getNintendoAccountSessionToken } from '../api/na.js';
const debug = createDebug('cli:auth');
export const command = 'auth';
export const desc = 'Generate a link to login to a Nintendo Account';
export function builder(yargs: Argv<ParentArguments>) {
return yargs.option('auth', {
describe: 'Authenticate immediately',
type: 'boolean',
default: true,
}).option('select', {
describe: 'Set as default user (default: true if only user)',
type: 'boolean',
});
}
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 params = {
state,
redirect_uri: 'npf71b963c1b7b6d119://auth',
client_id: '71b963c1b7b6d119',
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);
console.log('1. Open this URL and login to your Nintendo Account:');
console.log('');
console.log(authoriseurl);
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".');
console.log('');
const read = await import('read');
// @ts-expect-error
const prompt = util.promisify(read.default as typeof read);
const applink = await prompt({
prompt: `Paste the link: `,
output: process.stderr,
});
console.log('');
const authorisedurl = new URL(applink);
const authorisedparams = new URLSearchParams(authorisedurl.hash.substr(1));
debug('Redirect URL parameters', [...authorisedparams.entries()]);
const token = await getNintendoAccountSessionToken(
authorisedparams.get('session_token_code')!, verifier, '71b963c1b7b6d119');
console.log('Session token', token);
if (argv.auth) {
const storage = await initStorage(argv.dataPath);
const {nso, data} = await getToken(storage, token.session_token, argv.zncProxyUrl);
console.log('Authenticated as Nintendo Account %s (NA %s, NSO %s)',
data.user.screenName, data.user.nickname, data.nsoAccount.user.name);
await storage.setItem('NintendoAccountToken.' + data.user.id, token.session_token);
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
users.add(data.user.id);
await storage.setItem('NintendoAccountIds', [...users]);
if ('select' in argv ? argv.select : users.size === 1) {
await storage.setItem('SelectedUser', data.user.id);
console.log('Set as default user');
}
}
}

View File

@ -1,4 +1,5 @@
export * as token from './token.js';
export * as auth from './auth.js';
export * as users from './users.js';
export * as user from './user.js';
export * as announcements from './announcements.js';

View File

@ -12,10 +12,6 @@ export function builder(yargs: Argv<ParentArguments>) {
return yargs.positional('token', {
describe: 'Nintendo Account session token (it is recommended this is not set and you enter it interactively)',
type: 'string',
}).option('auth', {
describe: 'Authenticate immediately',
type: 'boolean',
default: true,
}).option('select', {
describe: 'Set as default user (default: true if only user)',
type: 'boolean',
@ -39,26 +35,22 @@ export async function handler(argv: ArgumentsCamelCase<Arguments>) {
});
}
await storage.setItem('SessionToken', argv.token);
const {nso, data} = await getToken(storage, argv.token, argv.zncProxyUrl);
if (argv.auth) {
const {nso, data} = await getToken(storage, argv.token, argv.zncProxyUrl);
console.warn('Authenticated as Nintendo Account %s (NA %s, NSO %s)',
data.user.screenName, data.user.nickname, data.nsoAccount.user.name);
console.log('Authenticated as Nintendo Account %s (NA %s, NSO %s)',
data.user.screenName, data.user.nickname, data.nsoAccount.user.name);
await storage.setItem('NintendoAccountToken.' + data.user.id, argv.token);
await storage.setItem('NintendoAccountToken.' + data.user.id, argv.token);
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
users.add(data.user.id);
await storage.setItem('NintendoAccountIds', [...users]);
const users = new Set(await storage.getItem('NintendoAccountIds') ?? []);
users.add(data.user.id);
await storage.setItem('NintendoAccountIds', [...users]);
console.log('Saved token');
if ('select' in argv ? argv.select : users.size === 1) {
await storage.setItem('SelectedUser', data.user.id);
if ('select' in argv ? argv.select : users.size === 1) {
await storage.setItem('SelectedUser', data.user.id);
console.log('Set as default user');
}
} else {
console.log('Saved token');
console.log('Set as default user');
}
}