Validate session tokens and znca id_tokens

This commit is contained in:
Samuel Elliott
2022-03-26 21:12:46 +00:00
parent f6fe7b57ba
commit e801487010
7 changed files with 314 additions and 11 deletions

View File

@@ -7,7 +7,7 @@ import { DailySummaries, Devices, MonthlySummaries, MonthlySummary, MoonError, S
const debug = createDebug('api:moon');
const MOON_URL = 'https://api-lp1.pctl.srv.nintendo.net/moon';
export const MOON_CLIENT_ID = '54789befb391a838';
export const ZNMA_CLIENT_ID = '54789befb391a838';
export default class MoonApi {
constructor(
@@ -95,7 +95,7 @@ export default class MoonApi {
static async loginWithSessionToken(token: string) {
// Nintendo Account token
const nintendoAccountToken = await getNintendoAccountToken(token, MOON_CLIENT_ID);
const nintendoAccountToken = await getNintendoAccountToken(token, ZNMA_CLIENT_ID);
// Nintendo Account user data
const user = await getNintendoAccountUser(nintendoAccountToken);

View File

@@ -1,6 +1,6 @@
import fetch from 'node-fetch';
import createDebug from 'debug';
import { ErrorResponse } from './util.js';
import { ErrorResponse, JwtPayload } from './util.js';
const debug = createDebug('api:na');
@@ -89,6 +89,18 @@ export interface NintendoAccountSessionToken {
session_token: string;
code: string;
}
export interface NintendoAccountSessionTokenJwtPayload extends JwtPayload {
jti: string;
typ: 'session_token';
iss: 'https://accounts.nintendo.com';
'st:scp': number[];
/** Subject (Nintendo Account ID) */
sub: string;
exp: number;
/** Audience (client ID) */
aud: string;
iat: number;
}
export interface NintendoAccountToken {
scope: string[];
@@ -97,6 +109,32 @@ export interface NintendoAccountToken {
access_token?: string;
expires_in: 900;
}
export interface NintendoAccountIdTokenJwtPayload extends JwtPayload {
/** Subject (Nintendo Account ID) */
sub: string;
iat: number;
exp: number;
/** Audience (client ID) */
aud: string;
iss: 'https://accounts.nintendo.com';
jti: string;
at_hash: string; // ??
typ: 'id_token';
country: string;
}
export interface NintendoAccountAccessTokenJwtPayload extends JwtPayload {
iss: 'https://accounts.nintendo.com';
jti: string;
typ: 'token';
/** Subject (Nintendo Account ID) */
sub: string;
iat: number;
'ac:grt': number; // ??
'ac:scp': number[]; // ??
exp: number;
/** Audience (client ID) */
aud: string;
}
export interface NintendoAccountUser {
emailOptedIn: boolean;

View File

@@ -1,3 +1,4 @@
import * as crypto from 'crypto';
import { Response } from 'node-fetch';
export class ErrorResponse<T = unknown> extends Error {
@@ -9,3 +10,111 @@ export class ErrorResponse<T = unknown> extends Error {
super(message);
}
}
export interface JwtHeader {
typ?: 'JWT';
alg: JwtAlgorithm;
/** Key ID */
kid?: string;
/** JSON Web Key Set URL */
jku?: string;
}
export enum JwtAlgorithm {
RS256 = 'RS256',
}
export interface JwtPayload {
/** Audience */
aud: string;
/** Expiration timestamp (seconds) */
exp: number;
/** Issue timestamp (seconds) */
iat: number;
/** Issuer */
iss: string;
/** Token ID */
jti: string;
/** Subject */
sub: string | number;
/** Token type */
typ: string;
}
type JwtVerifier = (data: Buffer, signature: Buffer, key: string) => boolean;
export class Jwt<T = JwtPayload, H extends JwtHeader = JwtHeader> {
constructor(
readonly header: H,
readonly payload: T
) {}
static decode<T = JwtPayload, H extends JwtHeader = JwtHeader>(token: string) {
const [header_str, payload_str, signature_str] = token.split('.', 3);
const header = JSON.parse(Buffer.from(header_str, 'base64url').toString());
const payload = JSON.parse(Buffer.from(payload_str, 'base64url').toString());
const signature = Buffer.from(signature_str, 'base64url');
if ('typ' in header && header.typ !== 'JWT') {
throw new Error('Invalid JWT');
}
const jwt = new this<T, H>(header, payload);
return [jwt, signature] as const;
}
verify(signature: Buffer, key: string, verifier?: JwtVerifier) {
const header_str = Buffer.from(JSON.stringify(this.header)).toString('base64url');
const payload_str = Buffer.from(JSON.stringify(this.payload)).toString('base64url');
const sign_data = header_str + '.' + payload_str;
if (!verifier) {
if (!(this.header.alg in Jwt.verifiers) || !Jwt.verifiers[this.header.alg]) {
throw new Error('Unknown algorithm');
}
verifier = Jwt.verifiers[this.header.alg];
}
return verifier.call(null, Buffer.from(sign_data), signature, key);
}
static verifiers: Record<JwtAlgorithm, JwtVerifier> = {
[JwtAlgorithm.RS256]: (data, signature, key) => {
const verify = crypto.createVerify('RSA-SHA256');
verify.end(data);
return verify.verify(key, signature);
},
};
}
export interface Jwks {
keys: Jwk[];
}
export interface Jwk {
/** Key type */
kty: string;
use?: JwkUse | string;
key_ops?: JwkKeyOperation | string;
alg?: JwtAlgorithm | string;
/** Key ID */
kid?: string;
x5u?: string[];
x5c?: string[];
x5t?: string;
'x5t#S256'?: string;
}
export enum JwkUse {
SIGNATURE = 'sig',
ENCRYPTION = 'enc',
}
export enum JwkKeyOperation {
SIGN = 'sign',
VERIFY = 'verify',
ENCRYPT = 'encrypt',
DECRYPT = 'decrypt',
WRAP_KEY = 'wrapKey',
UNWRAP_KEY = 'unwrapKey',
DERIVE_KEY = 'deriveKey',
DERIVE_BITS = 'deriveBits',
}

View File

@@ -4,7 +4,7 @@ import createDebug from 'debug';
import { flapg, FlapgIid, genfc } from './f.js';
import { AccountLogin, ActiveEvent, Announcement, CurrentUser, Friends, WebService, WebServiceToken, ZncResponse } from './znc-types.js';
import { getNintendoAccountToken, getNintendoAccountUser } from './na.js';
import { ErrorResponse } from './util.js';
import { ErrorResponse, JwtPayload } from './util.js';
const debug = createDebug('api:znc');
@@ -169,3 +169,37 @@ export default class ZncApi {
};
}
}
export interface ZncJwtPayload extends JwtPayload {
isChildRestricted: boolean;
membership: {
active: boolean;
};
aud: string;
exp: number;
iat: number;
iss: 'api-lp1.znc.srv.nintendo.net';
/** User ID (CurrentUser.id, not CurrentUser.nsaID) */
sub: number;
typ: 'id_token';
}
export interface ZncWebServiceJwtPayload extends JwtPayload {
isChildRestricted: boolean;
aud: string;
exp: number;
iat: number;
iss: 'api-lp1.znc.srv.nintendo.net';
jti: string;
/** User ID (CurrentUser.id, not CurrentUser.nsaID) */
sub: number;
links: {
networkServiceAccount: {
/** NSA ID (CurrentUser.nsaID) */
id: string;
};
};
typ: 'id_token';
membership: {
active: boolean;
};
}