From b6ce5f903fa2285cb381d73aedf02cc5e2712478 Mon Sep 17 00:00:00 2001 From: Luan Date: Tue, 21 May 2024 18:47:31 -0300 Subject: [PATCH] refactor(OAuth2)!: Rewrite auth module (#661) This is a rewrite of the OAuth2 module to address some bugs and inconsistencies. And since it changes the structure of the credentials, I'm marking this as a breaking change. Note that you will have to update your existing credentials, that is if you wish to continue using them. Otherwise, simply delete them and sign in again. --- examples/auth/README.md | 19 +- examples/auth/custom-oauth2-creds/index.ts | 8 +- examples/auth/yttv-oauth2.js | 6 +- src/core/OAuth.ts | 303 ------------------ src/core/OAuth2.ts | 338 +++++++++++++++++++++ src/core/Player.ts | 5 +- src/core/Session.ts | 52 ++-- src/core/index.ts | 4 +- src/utils/Constants.ts | 15 +- src/utils/HTTPClient.ts | 14 +- src/utils/Utils.ts | 2 +- 11 files changed, 390 insertions(+), 376 deletions(-) delete mode 100644 src/core/OAuth.ts create mode 100644 src/core/OAuth2.ts diff --git a/examples/auth/README.md b/examples/auth/README.md index 440cb66f..387f3820 100644 --- a/examples/auth/README.md +++ b/examples/auth/README.md @@ -8,22 +8,21 @@ Just like the official Data API, YouTube.js supports using your own OAuth2 crede The library supports signing in using YouTube TV's client id. This is the recommended way to sign in as it doesn't require you to create your own OAuth2 credentials. ```js -// 'auth-pending' is fired with the info needed to sign in via OAuth. + +// Fired when waiting for the user to authorize the sign in attempt. yt.session.on('auth-pending', (data) => { - // data.verification_url contains the URL to visit to authenticate. - // data.user_code contains the code to enter on the website. + // data.verification_url contains the authorization URL. + // data.user_code contains the code to enter on the website. }); -// 'auth' is fired once the authentication is complete +// Fired when authentication is successful. yt.session.on('auth', ({ credentials }) => { - // do something with the credentials, eg; save them in a database. + // Do something with the credentials, eg; save them in a database. console.log('Sign in successful'); }); -// 'update-credentials' is fired when the access token expires, if you do not save the updated credentials any subsequent request will fail -yt.session.on('update-credentials', ({ credentials }) => { - // do something with the updated credentials -}); +// Fired when the access token expires. +yt.session.on('update-credentials', ({ credentials }) => { /** do something with the updated credentials. */ }); await yt.session.signIn(/* credentials */); ``` @@ -56,7 +55,7 @@ await yt.session.oauth.removeCache(); # Cookies > **Note** -> This is not as reliable as OAuth2 as cookies expire and can be completely revoked at any time. +> This is not as reliable as OAuth2. Cookies can expire and are not very secure. ```js const yt = await Innertube.create({ diff --git a/examples/auth/custom-oauth2-creds/index.ts b/examples/auth/custom-oauth2-creds/index.ts index 91c9e417..788215f3 100644 --- a/examples/auth/custom-oauth2-creds/index.ts +++ b/examples/auth/custom-oauth2-creds/index.ts @@ -111,9 +111,11 @@ app.get('/login', async (req, res) => { await innertube.session.signIn({ access_token: tokens.access_token, refresh_token: tokens.refresh_token, - expires: new Date(tokens.expiry_date), - client_id: clientId, - client_secret: clientSecret, + expiry_date: new Date(tokens.expiry_date).toISOString(), + client: { + client_id: clientId, + client_secret: clientSecret + } }); await innertube.session.oauth.cacheCredentials(); diff --git a/examples/auth/yttv-oauth2.js b/examples/auth/yttv-oauth2.js index c326da89..1928ffc0 100644 --- a/examples/auth/yttv-oauth2.js +++ b/examples/auth/yttv-oauth2.js @@ -6,17 +6,17 @@ import { Innertube, UniversalCache } from 'youtubei.js'; cache: new UniversalCache(false) }); - // 'auth-pending' is fired with the info needed to sign in via OAuth. + // Fired when waiting for the user to authorize the sign in attempt. yt.session.on('auth-pending', (data) => { console.log(`Go to ${data.verification_url} in your browser and enter code ${data.user_code} to authenticate.`); }); - // 'auth' is fired once the authentication is complete + // Fired when authentication is successful. yt.session.on('auth', ({ credentials }) => { console.log('Sign in successful:', credentials); }); - // 'update-credentials' is fired when the access token expires, if you do not save the updated credentials any subsequent request will fail + // Fired when the access token expires. yt.session.on('update-credentials', async ({ credentials }) => { console.log('Credentials updated:', credentials); await yt.session.oauth.cacheCredentials(); diff --git a/src/core/OAuth.ts b/src/core/OAuth.ts deleted file mode 100644 index a6e02e77..00000000 --- a/src/core/OAuth.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { Log, Constants } from '../utils/index.js'; -import { OAuthError, Platform } from '../utils/Utils.js'; -import type Session from './Session.js'; - -/** - * Represents the credentials used for authentication. - */ -export interface Credentials { - /** - * Token used to sign in. - */ - access_token: string; - /** - * Token used to get a new access token. - */ - refresh_token: string; - /** - * Access token's expiration date, which is usually 24hrs-ish. - */ - expires: Date; - /** - * Optional client ID. - */ - client_id?: string; - /** - * Optional client secret. - */ - client_secret?: string; -} - -// TODO: actual type info for this. -export type OAuthAuthPendingData = any; - -export type OAuthAuthEventHandler = (data: { - credentials: Credentials; - status: 'SUCCESS'; -}) => any; - -export type OAuthAuthPendingEventHandler = (data: OAuthAuthPendingData) => any; -export type OAuthAuthErrorEventHandler = (err: OAuthError) => any; - -export type OAuthClientIdentity = { - client_id: string; - client_secret: string; -}; - -export default class OAuth { - static TAG = 'OAuth'; - - #identity?: Record; - #session: Session; - #credentials?: Credentials; - #polling_interval = 5; - - constructor(session: Session) { - this.#session = session; - } - - /** - * Starts the auth flow in case no valid credentials are available. - */ - async init(credentials?: Credentials): Promise { - this.#credentials = credentials; - - if (this.validateCredentials()) { - if (!this.has_access_token_expired) - this.#session.emit('auth', { - credentials: this.#credentials, - status: 'SUCCESS' - }); - } else if (!(await this.#loadCachedCredentials())) { - await this.#getUserCode(); - } - } - - async cacheCredentials(): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(JSON.stringify(this.#credentials)); - await this.#session.cache?.set('youtubei_oauth_credentials', data.buffer); - } - - async #loadCachedCredentials(): Promise { - const data = await this.#session.cache?.get('youtubei_oauth_credentials'); - if (!data) return false; - - const decoder = new TextDecoder(); - const credentials = JSON.parse(decoder.decode(data)); - - this.#credentials = { - access_token: credentials.access_token, - refresh_token: credentials.refresh_token, - client_id: credentials.client_id, - client_secret: credentials.client_secret, - expires: new Date(credentials.expires) - }; - - this.#session.emit('auth', { - credentials: this.#credentials, - status: 'SUCCESS' - }); - - return true; - } - - async removeCache(): Promise { - await this.#session.cache?.remove('youtubei_oauth_credentials'); - } - - /** - * Asks the server for a user code and verification URL. - */ - async #getUserCode(): Promise { - this.#identity = await this.#getClientIdentity(); - - const data = { - client_id: this.#identity.client_id, - scope: Constants.OAUTH.SCOPE, - device_id: Platform.shim.uuidv4(), - device_model: Constants.OAUTH.MODEL_NAME - }; - - const response = await this.#session.http.fetch_function(new URL('/o/oauth2/device/code', Constants.URLS.YT_BASE), { - body: JSON.stringify(data), - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }); - - const response_data = await response.json(); - - this.#session.emit('auth-pending', response_data); - this.#polling_interval = response_data.interval; - this.#startPolling(response_data.device_code); - } - - /** - * Polls the authorization server until access is granted by the user. - */ - #startPolling(device_code: string): void { - const poller = setInterval(async () => { - const data = { - ...this.#identity, - code: device_code, - grant_type: Constants.OAUTH.GRANT_TYPE - }; - - try { - const response = await this.#session.http.fetch_function(new URL('/o/oauth2/token', Constants.URLS.YT_BASE), { - body: JSON.stringify(data), - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }); - - const response_data = await response.json(); - - if (response_data.error) { - switch (response_data.error) { - case 'access_denied': - this.#session.emit('auth-error', new OAuthError('Access was denied.', { status: 'ACCESS_DENIED' })); - break; - case 'expired_token': - this.#session.emit('auth-error', new OAuthError('The device code has expired, restarting auth flow.', { status: 'DEVICE_CODE_EXPIRED' })); - clearInterval(poller); - this.#getUserCode(); - break; - default: - break; - } - return; - } - - const expiration_date = new Date(new Date().getTime() + response_data.expires_in * 1000); - - this.#credentials = { - access_token: response_data.access_token, - refresh_token: response_data.refresh_token, - client_id: this.#identity?.client_id, - client_secret: this.#identity?.client_secret, - expires: expiration_date - }; - - this.#session.emit('auth', { - credentials: this.#credentials, - status: 'SUCCESS' - }); - - clearInterval(poller); - } catch (err) { - clearInterval(poller); - return this.#session.emit('auth-error', new OAuthError('Could not obtain user code.', { status: 'FAILED', error: err })); - } - }, this.#polling_interval * 1000); - } - - /** - * Refresh access token if the same has expired. - */ - async refreshIfRequired(): Promise { - if (this.has_access_token_expired) { - await this.#refreshAccessToken(); - } - } - - async #refreshAccessToken(): Promise { - if (!this.#credentials) return; - this.#identity = await this.#getClientIdentity(); - - const data = { - ...this.#identity, - refresh_token: this.#credentials.refresh_token, - grant_type: 'refresh_token' - }; - - const response = await this.#session.http.fetch_function(new URL('/o/oauth2/token', Constants.URLS.YT_BASE), { - body: JSON.stringify(data), - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }); - - const response_data = await response.json(); - const expiration_date = new Date(new Date().getTime() + response_data.expires_in * 1000); - - this.#credentials = { - access_token: response_data.access_token, - refresh_token: response_data.refresh_token || this.#credentials.refresh_token, - client_id: this.#identity.client_id, - client_secret: this.#identity.client_secret, - expires: expiration_date - }; - - this.#session.emit('update-credentials', { - credentials: this.#credentials, - status: 'SUCCESS' - }); - } - - async revokeCredentials(): Promise { - if (!this.#credentials) return; - await this.removeCache(); - return this.#session.http.fetch_function(new URL(`/o/oauth2/revoke?token=${encodeURIComponent(this.#credentials.access_token)}`, Constants.URLS.YT_BASE), { - method: 'post' - }); - } - - /** - * Retrieves client identity from YouTube TV. - */ - async #getClientIdentity(): Promise { - if (this.#credentials?.client_id && this.credentials?.client_secret) { - Log.info(OAuth.TAG, 'Using custom OAuth2 credentials.\n'); - return { - client_id: this.#credentials.client_id, - client_secret: this.credentials.client_secret - }; - } - - const response = await this.#session.http.fetch_function(new URL('/tv', Constants.URLS.YT_BASE), { headers: Constants.OAUTH.HEADERS }); - - const response_data = await response.text(); - const url_body = Constants.OAUTH.REGEX.AUTH_SCRIPT.exec(response_data)?.[1]; - - if (!url_body) - throw new OAuthError('Could not obtain script url.', { status: 'FAILED' }); - - Log.info(OAuth.TAG, `Got YouTubeTV script URL (${url_body})`); - - const script = await this.#session.http.fetch(url_body, { baseURL: Constants.URLS.YT_BASE }); - - const client_identity = (await script.text()) - .replace(/\n/g, '') - .match(Constants.OAUTH.REGEX.CLIENT_IDENTITY); - - const groups = client_identity?.groups as OAuthClientIdentity | null; - - if (!groups) - throw new OAuthError('Could not obtain client identity.', { status: 'FAILED' }); - - Log.info(OAuth.TAG, 'OAuth2 credentials retrieved.\n', groups); - - return groups; - } - - get credentials(): Credentials | undefined { - return this.#credentials; - } - - get has_access_token_expired(): boolean { - const timestamp = this.#credentials ? new Date(this.#credentials.expires).getTime() : -Infinity; - return new Date().getTime() > timestamp; - } - - validateCredentials(): this is this & { credentials: Credentials } { - return this.#credentials && - Reflect.has(this.#credentials, 'access_token') && - Reflect.has(this.#credentials, 'refresh_token') && - Reflect.has(this.#credentials, 'expires') || false; - } -} \ No newline at end of file diff --git a/src/core/OAuth2.ts b/src/core/OAuth2.ts new file mode 100644 index 00000000..d4871510 --- /dev/null +++ b/src/core/OAuth2.ts @@ -0,0 +1,338 @@ +import { OAuth2Error, Platform } from '../utils/Utils.js'; +import { Log, Constants } from '../utils/index.js'; +import type Session from './Session.js'; + +const TAG = 'OAuth2'; + +export type OAuth2ClientID = { + client_id: string; + client_secret: string; +}; + +export type OAuth2Tokens = { + access_token: string; + expiry_date: string; + expires_in?: number; + refresh_token: string; + scope?: string; + token_type?: string; + client?: OAuth2ClientID; +}; + +export type DeviceAndUserCode = { + device_code: string; + expires_in: number; + interval: number; + user_code: string; + verification_url: string; + error_code?: string; +}; + +export type OAuth2AuthEventHandler = (data: { credentials: OAuth2Tokens; }) => void; +export type OAuth2AuthPendingEventHandler = (data: DeviceAndUserCode) => void; +export type OAuth2AuthErrorEventHandler = (err: OAuth2Error) => void; + +export default class OAuth2 { + #session: Session; + + YTTV_URL: URL; + AUTH_SERVER_CODE_URL: URL; + AUTH_SERVER_TOKEN_URL: URL; + AUTH_SERVER_REVOKE_TOKEN_URL: URL; + + client_id: OAuth2ClientID | undefined; + oauth2_tokens: OAuth2Tokens | undefined; + + constructor(session: Session) { + this.#session = session; + this.YTTV_URL = new URL('/tv', Constants.URLS.YT_BASE); + this.AUTH_SERVER_CODE_URL = new URL('/o/oauth2/device/code', Constants.URLS.YT_BASE); + this.AUTH_SERVER_TOKEN_URL = new URL('/o/oauth2/token', Constants.URLS.YT_BASE); + this.AUTH_SERVER_REVOKE_TOKEN_URL = new URL('/o/oauth2/revoke', Constants.URLS.YT_BASE); + } + + async init(tokens?: OAuth2Tokens): Promise { + if (tokens) { + this.setTokens(tokens); + + if (this.shouldRefreshToken()) { + await this.refreshAccessToken(); + } + + this.#session.emit('auth', { credentials: this.oauth2_tokens }); + + return; + } + + const loaded_from_cache = await this.#loadFromCache(); + + if (loaded_from_cache) { + Log.info(TAG, 'Loaded OAuth2 tokens from cache.', this.oauth2_tokens); + return; + } + + if (!this.client_id) + this.client_id = await this.getClientID(); + + // Initialize OAuth2 flow + const device_and_user_code = await this.getDeviceAndUserCode(); + + this.#session.emit('auth-pending', device_and_user_code); + + this.pollForAccessToken(device_and_user_code); + } + + setTokens(tokens: OAuth2Tokens): void { + const tokensMod = tokens; + + // Convert access token remaining lifetime to ISO string + if (tokensMod.expires_in) { + tokensMod.expiry_date = new Date(Date.now() + tokensMod.expires_in * 1000).toISOString(); + delete tokensMod.expires_in; // We don't need this anymore + } + + if (!this.validateTokens(tokensMod)) + throw new OAuth2Error('Invalid tokens provided.'); + + this.oauth2_tokens = tokensMod; + + if (tokensMod.client) { + Log.info(TAG, 'Using provided client id and secret.'); + this.client_id = tokensMod.client; + } + } + + async cacheCredentials(): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(JSON.stringify(this.oauth2_tokens)); + await this.#session.cache?.set('youtubei_oauth_credentials', data.buffer); + } + + async #loadFromCache(): Promise { + const data = await this.#session.cache?.get('youtubei_oauth_credentials'); + if (!data) + return false; + + const decoder = new TextDecoder(); + const credentials = JSON.parse(decoder.decode(data)); + + this.setTokens(credentials); + + this.#session.emit('auth', { credentials }); + + return true; + } + + async removeCache(): Promise { + await this.#session.cache?.remove('youtubei_oauth_credentials'); + } + + async pollForAccessToken(device_and_user_code: DeviceAndUserCode): Promise { + if (!this.client_id) + throw new OAuth2Error('Client ID is missing.'); + + const { device_code, interval } = device_and_user_code; + const { client_id, client_secret } = this.client_id; + + const payload = { + client_id, + client_secret, + code: device_code, + grant_type: 'http://oauth.net/grant_type/device/1.0' + }; + + const connInterval = setInterval(async () => { + const response = await this.#http.fetch_function(this.AUTH_SERVER_TOKEN_URL, { + body: JSON.stringify(payload), + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + const response_data = await response.json(); + + if (response_data.error) { + switch (response_data.error) { + case 'access_denied': + this.#session.emit('auth-error', new OAuth2Error('Access was denied.', response_data)); + clearInterval(connInterval); + break; + case 'expired_token': + this.#session.emit('auth-error', new OAuth2Error('The device code has expired.', response_data)); + clearInterval(connInterval); + break; + case 'authorization_pending': + case 'slow_down': + Log.info(TAG, 'Polling for access token...'); + break; + default: + this.#session.emit('auth-error', new OAuth2Error('Server returned an unexpected error.', response_data)); + clearInterval(connInterval); + break; + } + return; + } + + this.setTokens(response_data); + + this.#session.emit('auth', { credentials: this.oauth2_tokens }); + + clearInterval(connInterval); + }, interval * 1000); + } + + async revokeCredentials(): Promise { + if (!this.oauth2_tokens) + throw new OAuth2Error('Access token not found'); + + await this.removeCache(); + + const url = this.AUTH_SERVER_REVOKE_TOKEN_URL; + url.searchParams.set('token', this.oauth2_tokens.access_token); + + return this.#session.http.fetch_function(url, { method: 'POST' }); + } + + async refreshAccessToken(): Promise { + if (!this.client_id) + this.client_id = await this.getClientID(); + + if (!this.oauth2_tokens) + throw new OAuth2Error('No tokens available to refresh.'); + + const { client_id, client_secret } = this.client_id; + const { refresh_token } = this.oauth2_tokens; + + const payload = { + client_id, + client_secret, + refresh_token, + grant_type: 'refresh_token' + }; + + const response = await this.#http.fetch_function(this.AUTH_SERVER_TOKEN_URL, { + body: JSON.stringify(payload), + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) + throw new OAuth2Error(`Failed to refresh access token: ${response.status}`); + + const response_data = await response.json(); + + if (response_data.error_code) + throw new OAuth2Error('Authorization server returned an error', response_data); + + this.oauth2_tokens.access_token = response_data.access_token; + this.oauth2_tokens.expiry_date = new Date(Date.now() + response_data.expires_in * 1000).toISOString(); + + this.#session.emit('update-credentials', { credentials: this.oauth2_tokens }); + } + + async getDeviceAndUserCode(): Promise { + if (!this.client_id) + throw new OAuth2Error('Client ID is missing.'); + + const { client_id } = this.client_id; + + const payload = { + client_id, + scope: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content', + device_id: Platform.shim.uuidv4(), + device_model: 'ytlr::' + }; + + const response = await this.#http.fetch_function(this.AUTH_SERVER_CODE_URL, { + body: JSON.stringify(payload), + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) + throw new OAuth2Error(`Failed to get device/user code: ${response.status}`); + + const response_data = await response.json(); + + if (response_data.error_code) + throw new OAuth2Error('Authorization server returned an error', response_data); + + return response_data; + } + + async getClientID(): Promise { + const yttv_response = await this.#http.fetch_function(this.YTTV_URL, { + headers: { + 'User-Agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version', + 'Referer': 'https://www.youtube.com/tv', + 'Accept-Language': 'en-US' + } + }); + + if (!yttv_response.ok) + throw new OAuth2Error(`Failed to get client ID: ${yttv_response.status}`); + + const yttv_response_data = await yttv_response.text(); + + let script_url_body: RegExpExecArray | null; + + if ((script_url_body = Constants.OAUTH.REGEX.TV_SCRIPT.exec(yttv_response_data)) !== null) { + Log.info(TAG, `Got YouTubeTV script URL (${script_url_body[1]})`); + + const script_response = await this.#http.fetch(script_url_body[1], { baseURL: Constants.URLS.YT_BASE }); + + if (!script_response.ok) + throw new OAuth2Error(`TV script request failed with status code ${script_response.status}`); + + const script_response_data = await script_response.text(); + + const client_identity = script_response_data + .match(Constants.OAUTH.REGEX.CLIENT_IDENTITY); + + if (!client_identity || !client_identity.groups) + throw new OAuth2Error('Could not obtain client ID.'); + + const { client_id, client_secret } = client_identity.groups; + + Log.info(TAG, `Client identity retrieved (clientId=${client_id}, clientSecret=${client_secret}).`); + + return { + client_id, + client_secret + }; + } + + throw new OAuth2Error('Could not obtain script URL.'); + } + + shouldRefreshToken(): boolean { + if (!this.oauth2_tokens) + return false; + return Date.now() > new Date(this.oauth2_tokens.expiry_date).getTime(); + } + + validateTokens(tokens: OAuth2Tokens): boolean { + const propertiesAreValid = ( + Boolean(tokens.access_token) && + Boolean(tokens.expiry_date) && + Boolean(tokens.refresh_token) + ); + + const typesAreValid = ( + typeof tokens.access_token === 'string' && + typeof tokens.expiry_date === 'string' && + typeof tokens.refresh_token === 'string' + ); + + return typesAreValid && propertiesAreValid; + } + + get #http() { + return this.#session.http; + } +} \ No newline at end of file diff --git a/src/core/Player.ts b/src/core/Player.ts index 4df4b818..4d848613 100644 --- a/src/core/Player.ts +++ b/src/core/Player.ts @@ -38,10 +38,11 @@ export default class Player { // We have the player id, now we can check if we have a cached player. if (cache) { - Log.info(Player.TAG, 'Found a cached player.'); const cached_player = await Player.fromCache(cache, player_id); - if (cached_player) + if (cached_player) { + Log.info(Player.TAG, 'Found a cached player.'); return cached_player; + } } const player_url = new URL(`/s/player/${player_id}/player_ias.vflset/en_US/base.js`, Constants.URLS.YT_BASE); diff --git a/src/core/Session.ts b/src/core/Session.ts index ab11c71a..e986d27b 100644 --- a/src/core/Session.ts +++ b/src/core/Session.ts @@ -1,4 +1,4 @@ -import OAuth from './OAuth.js'; +import OAuth2 from './OAuth2.js'; import { Log, EventEmitter, HTTPClient } from '../utils/index.js'; import * as Constants from '../utils/Constants.js'; import * as Proto from '../proto/index.js'; @@ -12,10 +12,7 @@ import { import type { DeviceCategory } from '../utils/Utils.js'; import type { FetchFunction, ICache } from '../types/index.js'; -import type { - Credentials, OAuthAuthErrorEventHandler, - OAuthAuthEventHandler, OAuthAuthPendingEventHandler -} from './OAuth.js'; +import type { OAuth2Tokens, OAuth2AuthErrorEventHandler, OAuth2AuthPendingEventHandler, OAuth2AuthEventHandler } from './OAuth2.js'; export enum ClientType { WEB = 'WEB', @@ -172,7 +169,7 @@ export default class Session extends EventEmitter { #account_index: number; #player?: Player; - oauth: OAuth; + oauth: OAuth2; http: HTTPClient; logged_in: boolean; actions: Actions; @@ -187,23 +184,23 @@ export default class Session extends EventEmitter { this.#player = player; this.http = new HTTPClient(this, cookie, fetch); this.actions = new Actions(this); - this.oauth = new OAuth(this); + this.oauth = new OAuth2(this); this.logged_in = !!cookie; this.cache = cache; } - on(type: 'auth', listener: OAuthAuthEventHandler): void; - on(type: 'auth-pending', listener: OAuthAuthPendingEventHandler): void; - on(type: 'auth-error', listener: OAuthAuthErrorEventHandler): void; - on(type: 'update-credentials', listener: OAuthAuthEventHandler): void; + on(type: 'auth', listener: OAuth2AuthEventHandler): void; + on(type: 'auth-pending', listener: OAuth2AuthPendingEventHandler): void; + on(type: 'auth-error', listener: OAuth2AuthErrorEventHandler): void; + on(type: 'update-credentials', listener: OAuth2AuthEventHandler): void; on(type: string, listener: (...args: any[]) => void): void { super.on(type, listener); } - once(type: 'auth', listener: OAuthAuthEventHandler): void; - once(type: 'auth-pending', listener: OAuthAuthPendingEventHandler): void; - once(type: 'auth-error', listener: OAuthAuthErrorEventHandler): void; + once(type: 'auth', listener: OAuth2AuthEventHandler): void; + once(type: 'auth-pending', listener: OAuth2AuthPendingEventHandler): void; + once(type: 'auth-error', listener: OAuth2AuthErrorEventHandler): void; once(type: string, listener: (...args: any[]) => void): void { super.once(type, listener); @@ -390,31 +387,20 @@ export default class Session extends EventEmitter { return { context, api_key: Constants.CLIENTS.WEB.API_KEY, api_version: Constants.CLIENTS.WEB.API_VERSION }; } - async signIn(credentials?: Credentials): Promise { + async signIn(credentials?: OAuth2Tokens): Promise { return new Promise(async (resolve, reject) => { - const error_handler: OAuthAuthErrorEventHandler = (err) => reject(err); - - this.once('auth', (data) => { - this.off('auth-error', error_handler); - - if (data.status === 'SUCCESS') { - this.logged_in = true; - resolve(); - } - - reject(data); - }); + const error_handler: OAuth2AuthErrorEventHandler = (err) => reject(err); this.once('auth-error', error_handler); + this.once('auth', () => { + this.off('auth-error', error_handler); + this.logged_in = true; + resolve(); + }); + try { await this.oauth.init(credentials); - - if (this.oauth.validateCredentials()) { - await this.oauth.refreshIfRequired(); - this.logged_in = true; - resolve(); - } } catch (err) { reject(err); } diff --git a/src/core/index.ts b/src/core/index.ts index c0455249..42c21c46 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -7,8 +7,8 @@ export * from './Actions.js'; export { default as Player } from './Player.js'; export * from './Player.js'; -export { default as OAuth } from './OAuth.js'; -export * from './OAuth.js'; +export { default as OAuth2 } from './OAuth2.js'; +export * from './OAuth2.js'; export * as Clients from './clients/index.js'; export * as Endpoints from './endpoints/index.js'; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 2cb32cfe..2e633597 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -16,20 +16,9 @@ export const URLS = Object.freeze({ }) }); export const OAUTH = Object.freeze({ - SCOPE: 'http://gdata.youtube.com https://www.googleapis.com/auth/youtube-paid-content', - GRANT_TYPE: 'http://oauth.net/grant_type/device/1.0', - MODEL_NAME: 'ytlr::', - HEADERS: Object.freeze({ - 'accept': '*/*', - 'origin': 'https://www.youtube.com', - 'user-agent': 'Mozilla/5.0 (ChromiumStylePlatform) Cobalt/Version', - 'content-type': 'application/json', - 'referer': 'https://www.youtube.com/tv', - 'accept-language': 'en-US' - }), REGEX: Object.freeze({ - AUTH_SCRIPT: /