[13.4.0] Requests Authenticated using Cookies Not Working in React Native
Steps to reproduce
- Start a basic expo go-based react-native app,
npx create-expo-app --template blank - Install YouTube.js, follow the instructions for setting it up in a RN project
- Change the App.tsx file and add an effect that creates an innertube instance, passing in cookies that you've gotten from an authenticated session.
- After making the innertube instance, using it to make any sort of authenticated request (for example getBasicInfo, getInfo, or even getUnseenNotificationCount), just hangs and never makes the request or gives a result.
Failure Logs
No relevant logs, using a custom fetch implementation to log all requests the instance makes, shows that the request to fetch from the authenticated endpoint never gets called (only requests that are needed to initialize an instance are shown).
Expected behavior
Expect it to make the authenticated requests correctly and fetch the data specific to that user (ex: I do getInfo, it shows me watchNextFeed that is relevant to the users interests, shows me if they previously liked it, etc).
Current behavior
Just hangs forever and never makes the requests, no matter how long is waited. Like I said, making a debug fetch implementation to see requests made shows it as the library not making the request.
Version
Default
Anything else?
I've already verified the cookie is not the issue, since using the same cookie on NodeJS with YouTube.js works perfectly as expected.
Checklist
- [x] I am running the latest version.
- [x] I checked the documentation and found no answer.
- [x] I have searched the existing issues and made sure this is not a duplicate.
- [x] I have provided sufficient information.
Here's an MRE Repo. You shouldn't need anything outside of what's in this repo to be able to run it on your phone besides the Expo Go app that can be downloaded on the Play Store. It should be as easy as
npm install, npx expo start, and scanning the QR generated from the npx expo start command using the expo go app.
Was able to get this fixed by forcing yt.session.logged_in to be true, and using a custom fetch that added on necessary headers for cookie authentication to work correctly.
I don't know what the root cause of this could possibly be, but when messing around with the internals of the library, it looked like session.logged_in was never set to true, so that was why the auth and cookie headers weren't being added to the request. I might look into making a better fix instead of manually appending headers myself sometime soon and try and make a commit.
Thanks ^
I'll keep this open for now, as I'm also investigating it.
Great, did the MRE reproduce the issue?
edit: i dunno anymore. import { Platform } from 'youtubei.js'; const dd = await Platform.shim.sha1Hash('fasfafafa') doesnt run/hangs
===
~I believe this is due to platform.shim being undefined (which is important in calculating sapisidhash in the header from cookie). I dont recall this being set in the initial rn usage sheet.~
this is my setup. screenshot below uses a dummy cookie without sapisid. the network request can be caught in reactotron.
if I add a cookie with a sapisid, the header parsing will fail instead and request wont be made as it couldnt even get to that part. So rather than it beign hang, it just never made it to the request part. try doing a catch block and see if u could catch an error that way.
~i wrote the header parsing for another ytm lib and can confirm expo-crypto can do the SHA1 fine.~
Yeah, that's why setting yt.session.logged_in to true worked for me. Without that, it didn't even make the request if I passed in cookies. It stopped somewhere else before that.
since u do have a working cookie, could u try
import { Platform } from 'youtubei.js';
import * as crypto from 'expo-crypto';
Platform.load({
...Platform.shim,
sha1Hash: i =>
crypto.digestStringAsync(crypto.CryptoDigestAlgorithm.SHA1, i),
});
const y = await yt();
try {
console.log('res', await y.getUnseenNotificationsCount());
} catch (e) {
console.error('err');
}
b4 ur getUnseenNotificationsCount()? with this sha1hash swap I can catch the request again, just another 404.
Sorry for the late reply. Yeah, changing that at least did allow the request to go through, although it still gives 0 as notification count.
To actually fetch the authenticated data though, I had to use the react-native-cookie-manager library to update the actual device cookie jar's cookies for https://youtube.com with the cookies I'm using, since when making a fetch request, it sends out those cookies instead of the cookies we provide manually to YouTube.js (or, you can let the user sign in in a webview and use the cookies that way). And then on top of that, since YouTube.js wasn't adding on the necessary headers, I had to make a custom fetch that added some headers that were missing/not right like Origin, Authorization, User-Agent, Credentials: includeand sec-fetch-mode. That was able to get authenticated requests working for me.
thx for the reply! I still struggle to get a response other than 404. still, unsure if i'm even using a valid cookie or not. looking at another ytm lib i maintained it works fine, i doubt it has to do with the device cookie jar. guess i'll start with node.js and charles next.
If you'd like, I can update my github repo over at https://github.com/sgebr01/ytjs-cookie-mre to show you how it's working for me.
that would help immensely. much appreciated!
On Tue, May 20, 2025, 3:58 PM Summon @.***> wrote:
sgebr01 left a comment (LuanRT/YouTube.js#960) https://github.com/LuanRT/YouTube.js/issues/960#issuecomment-2896012838
If you'd like, I can update my github repo over at https://github.com/sgebr01/ytjs-cookie-mre to show you how it's working for me.
— Reply to this email directly, view it on GitHub https://github.com/LuanRT/YouTube.js/issues/960#issuecomment-2896012838, or unsubscribe https://github.com/notifications/unsubscribe-auth/AZMOVVU3AJFK3MYLAUKRRTD27OXSHAVCNFSM6AAAAAB4AX75TWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDQOJWGAYTEOBTHA . You are receiving this because you commented.Message ID: <LuanRT/YouTube. @.***>
Hello @sgebr01, could you please upload the necessary changes you made to get cookies working on YouTube with React Native?
I tried to follow what you said in the comment: I added a WebView, got the cookies, added the extra headers, but it didn’t work.
Thanks!
I'm working on making a repo, I'll let you know when I finish
@giovanijfc @lovegaoshi I've updated my MRE repo. This fix does work for me, please try it out and let me know if it works for you all. It works by letting users sign in through a webview and it will automatically pull cookies from there and use them in the requests (so you don't need to manually feed in cookie to the Innertube instance). Let me know if this works and if you have any questions.
Also, keep in mind, you will need to make a sha-1 hash, using whatever crypto polyfill library you have chosen (like expo-crypto or react-native-quick-crypto, my demo is using react-native-quick-crypto).
TYSM for sharing ur snippet!
~turns out I'm missing out yt.session.logged_in = true. I can confirm with this and the above Platform.load snippet, ytbi works~
turns out i've been using the WRONG client type all this time; u cant use client_type: ClientType.IOS for account stuff. it will always be 404
so TLDR:
- RN's crypto polyfill does NOT work in ytbi. ur options are either rn-quick-crypto or expo-crypto and u can overload this via the Platform.load snippet I shared above.
no customfetch is necessary.
Oh wow, great to hear, I will try that out myself.
I don't know, what you recommended isn't working for me. The library is at least making the request and I'm getting a 200, but I'm not getting the actual authenticated user data.
I see most necessary headers, like the Cookie and authorization headers, but it's still not working regardless (maybe since Origin and sec-fetch-mode headers are missing and the User agent is okhttp's. Did you change any of these somehow?
try account.getInfo(true) which should work. I saw .account.getInfo(false) errors out (request is made but no returns), I suspect its due to using https://github.com/LuanRT/YouTube.js/blob/09718a717f49633f4382133050647fe4ff6ca007/src/core/managers/AccountManager.ts#L43, so similar to how I used the iOS client and wont work. this is likely a lib issue than SAPISID implementation.
now thinking about it, u might be manually overriding the client in ur custom fetch. i need a closer look. i dont even know if the content is right. how is this working in node? is anyone bothered to try with charles to intercept the request?
-- hmm. .music.getLibrary() gives a 401 (something i'm more interested in). id like to make sure the node ver even works to begin with.
have to admit my previous comment was based on getUnseenNotificationsCount (the method you mentioned originally) successfully returning something. given that the requests did return 200, im still more inclined to believe the fetch part is fine. the parser however i dont know.
That's the issue I'm getting with your method now too. I'm getting 200's, but the actual data that I get isn't the authenticated accounts (for example yt.account.getInfo() isn't returning any actual account data, the way I was doing it before though was without any params passed, just yt.account.getInfo()). Doing both yt.account.getInfo(true) & yt.account.getInfo(false), isn't making a difference either. The way I was doing it earlier overriding the fetch is the only way that I've been able to get it to work.
I am getting the exact same response you've showed in your screenshot, no matter what I try with the yt.account.getInfo() with just your method of overriding the sha1 hash and setting yt.session.logged_in to true.
appreciate you pointing out I'm completely using the wrong API to test (oof) getUnseenNotifCount seems to be more tolerant vs the others.
I spent more time and pinned origin is the issue. Now i'm getting proper responses for both .account.getInfo() and .music.getLibrary() with just origin set. I overload Platform.shim.sha1hash and fetch with a simple wrapper that sets the origin. repo. i'd think at least UA should be included but doesnt seem necessary yet. I havent dug deeper but node.js works just fine.
req:
data:
Great to hear. I'll try implementing this soon.
I'm also having issues with cookies — unfortunately, they don't seem to work on iOS. I’ve tried everything that was recommended, but when I attempt to fetch the streaming URL of a song, the request returns a 401 error with the following message:
{
"error": {
"code": 401,
"message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"errors": [
{
"message": "Login Required.",
"domain": "global",
"reason": "required",
"location": "Authorization",
"locationType": "header"
}
],
"status": "UNAUTHENTICATED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "CREDENTIALS_MISSING",
"domain": "googleapis.com",
"metadata": {
"method": "youtube.innertube.OPInnerTubeService.GetPlayer",
"service": "youtubei.googleapis.com"
}
}
]
}
}
I’ve already updated my code like this and tested with different clients:
Platform.load({
...Platform.shim,
sha1Hash: (i) =>
new Promise((r) =>
r(QuickCrypto.createHash('sha1').update(i).digest('hex'))
),
});
return Innertube.create({
client_type: ClientType.TV,
retrieve_player: false,
...(cookie ? { cookie } : {}),
fetch: (
url: RequestInfo | URL,
init?: RequestInit
): Promise<Response> => {
const h = new Headers(init?.headers || {});
h.set('origin', 'https://www.youtube.com');
return Platform.shim.fetch(url, {
...init,
headers: h,
});
},
...(visitorData ? { visitor_data: visitorData } : {}),
...(poToken ? { po_token: poToken } : {}),
})
.then((client) => {
logManager.debug(
'[ytbi.js] YouTube web client initialized successfully',
{
authenticated: !!cookie,
sessionHasCookie: client.session?.cookie,
}
);
return client;
})
.catch((error) => {
logManager.error('[ytbi.js] Error initializing web client:', error);
this._client = null;
throw error;
});
After several attempts, I found that if I remove the Authorization header, the request returns a 200 status — but the playabilityStatus is LOGIN_REQUIRED, because the video is age-gated.
Does anyone have any ideas?
I'm also having issues with cookies — unfortunately, they don't seem to work on iOS. I’ve tried everything that was recommended, but when I attempt to fetch the streaming URL of a song, the request returns a 401 error with the following message:
{ "error": { "code": 401, "message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", "errors": [ { "message": "Login Required.", "domain": "global", "reason": "required", "location": "Authorization", "locationType": "header" } ], "status": "UNAUTHENTICATED", "details": [ { "@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "CREDENTIALS_MISSING", "domain": "googleapis.com", "metadata": { "method": "youtube.innertube.OPInnerTubeService.GetPlayer", "service": "youtubei.googleapis.com" } } ] } }
I’ve already updated my code like this and tested with different clients:
Platform.load({ ...Platform.shim, sha1Hash: (i) => new Promise((r) => r(QuickCrypto.createHash('sha1').update(i).digest('hex')) ), });
return Innertube.create({ client_type: ClientType.TV, retrieve_player: false, ...(cookie ? { cookie } : {}), fetch: ( url: RequestInfo | URL, init?: RequestInit ): Promise<Response> => { const h = new Headers(init?.headers || {}); h.set('origin', 'https://www.youtube.com'); return Platform.shim.fetch(url, { ...init, headers: h, }); }, ...(visitorData ? { visitor_data: visitorData } : {}), ...(poToken ? { po_token: poToken } : {}), }) .then((client) => { logManager.debug( '[ytbi.js] YouTube web client initialized successfully', { authenticated: !!cookie, sessionHasCookie: client.session?.cookie, } ); return client; }) .catch((error) => { logManager.error('[ytbi.js] Error initializing web client:', error); this._client = null; throw error; });
After several attempts, I found that if I remove the
Authorizationheader, the request returns a 200 status — but theplayabilityStatusisLOGIN_REQUIRED, because the video is age-gated.Does anyone have any ideas?
you cant use web cookies on the IOS client
I'm also having issues with cookies — unfortunately, they don't seem to work on iOS. I’ve tried everything that was recommended, but when I attempt to fetch the streaming URL of a song, the request returns a 401 error with the following message: { "error": { "code": 401, "message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.", "errors": [ { "message": "Login Required.", "domain": "global", "reason": "required", "location": "Authorization", "locationType": "header" } ], "status": "UNAUTHENTICATED", "details": [ { "@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "CREDENTIALS_MISSING", "domain": "googleapis.com", "metadata": { "method": "youtube.innertube.OPInnerTubeService.GetPlayer", "service": "youtubei.googleapis.com" } } ] } } I’ve already updated my code like this and tested with different clients: Platform.load({ ...Platform.shim, sha1Hash: (i) => new Promise((r) => r(QuickCrypto.createHash('sha1').update(i).digest('hex')) ), }); return Innertube.create({ client_type: ClientType.TV, retrieve_player: false, ...(cookie ? { cookie } : {}), fetch: ( url: RequestInfo | URL, init?: RequestInit ): Promise => { const h = new Headers(init?.headers || {}); h.set('origin', 'https://www.youtube.com'); return Platform.shim.fetch(url, { ...init, headers: h, }); }, ...(visitorData ? { visitor_data: visitorData } : {}), ...(poToken ? { po_token: poToken } : {}), }) .then((client) => { logManager.debug( '[ytbi.js] YouTube web client initialized successfully', { authenticated: !!cookie, sessionHasCookie: client.session?.cookie, } ); return client; }) .catch((error) => { logManager.error('[ytbi.js] Error initializing web client:', error); this._client = null; throw error; }); After several attempts, I found that if I remove the
Authorizationheader, the request returns a 200 status — but theplayabilityStatusisLOGIN_REQUIRED, because the video is age-gated. Does anyone have any ideas?you cant use web cookies on the IOS client
I’ve tried with several clients: WEB, MWEB, and the last one you see in the code above is TV, but all of them produce the same error. ~~The strange thing is that it seems works on my Android device…~~
Hello everyone, I managed to get account.getInfo, interact.like, and getUnseenNotificationsCount working on ANDROID. I haven't tested or fixed the other methods yet.
Remember, you need to insert your own YouTube cookies into the "cookies" variable.
If it doesn't work, check if you have valid cookies.
Below is the code needed to make this work.
// === START === Making Youtube.js work
import 'event-target-polyfill'
import 'react-native-url-polyfill/auto'
import 'text-encoding-polyfill'
import 'web-streams-polyfill'
import { decode, encode } from 'base-64'
import QuickCrypto from 'react-native-quick-crypto'
// === END === Making Youtube.js work
// eslint-disable-next-line import/no-named-as-default
import Innertube, { ClientType, Platform } from 'youtubei.js/react-native'
if (!global.btoa) {
global.btoa = encode
}
if (!global.atob) {
global.atob = decode
}
// @ts-expect-error to avoid typings' fuss
global.mmkvStorage = storageYotubeiJS
// See https://github.com/nodejs/node/issues/40678#issuecomment-1126944677
class CustomEvent extends Event {
#detail
constructor(type: string, options?: CustomEventInit<any[]>) {
super(type, options)
this.#detail = options?.detail ?? null
}
get detail() {
return this.#detail
}
}
global.CustomEvent = CustomEvent as any
export const createInnertube = async (
withSession = true
): Promise<Innertube> => {
const cookies = '' // your cookies
const sapisid = cookies ? getCookie(cookies, 'SAPISID') : undefined
const authorization = sapisid ? await generateSidAuth(sapisid) : undefined
const innertubeInstance = await Innertube.create({
retrieve_player: false,
enable_session_cache: false,
generate_session_locally: false,
lang: 'pt-BR',
client_type: ClientType.WEB,
fetch:
withSession && cookies
? async (input, init) => {
init = init ?? {}
let url = ''
if (input instanceof Request) url = input.url
else if (input instanceof URL) url = JSON.stringify(input.href)
else url = input.toString()
const newHeaders = init.headers
? new Headers(init.headers)
: new Headers()
if (
url !==
'https://www.youtube.com/youtubei/v1/player?prettyPrint=false&alt=json' &&
url !==
'https://www.youtube.com/youtubei/v1/config?prettyPrint=false'
) {
const headers = {
cookie: cookies || '',
authorization: authorization || '',
accept: '*/*',
'accept-language': '*',
'content-type': 'application/json',
'x-goog-authuser': '0',
'x-goog-visitor-id':
'CgtyUnVyeUZMbm9Edyjb-PTEBjIKCgJVUxIEGgAgMA%3D%3D',
'x-youtube-client-name': '3',
'x-youtube-client-version': '2.20250813.01.00',
cookies: cookies,
origin: 'https://www.youtube.com',
'user-agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36'
}
Object.entries(headers).forEach(([key, value]) => {
newHeaders.set(key, value)
})
}
const body = init.body
? (JSON.parse(init.body as string) as Record<string, any>)
: null
if (
url ===
'https://www.youtube.com/youtubei/v1/like/like?prettyPrint=false&alt=json'
) {
if (body?.target) {
body.target = { videoId: body.target }
}
}
return Platform.shim.fetch(input, {
...init,
headers: newHeaders,
body: body ? JSON.stringify(body) : null
})
}
: undefined
})
if (withSession && cookies) {
innertubeInstance.session.logged_in = true
}
return innertubeInstance
}
export async function generateSidAuth(sid: string): Promise<string> {
const youtube = 'https://www.youtube.com'
const timestamp = Math.floor(new Date().getTime() / 1000)
const input = [timestamp, sid, youtube].join(' ')
const gen_hash = await QuickCrypto.subtle.digest(
'SHA-1',
new TextEncoder().encode(input)
)
const byteToHex = [
'00',
'01',
'02',
'03',
'04',
'05',
'06',
'07',
'08',
'09',
'0a',
'0b',
'0c',
'0d',
'0e',
'0f',
'10',
'11',
'12',
'13',
'14',
'15',
'16',
'17',
'18',
'19',
'1a',
'1b',
'1c',
'1d',
'1e',
'1f',
'20',
'21',
'22',
'23',
'24',
'25',
'26',
'27',
'28',
'29',
'2a',
'2b',
'2c',
'2d',
'2e',
'2f',
'30',
'31',
'32',
'33',
'34',
'35',
'36',
'37',
'38',
'39',
'3a',
'3b',
'3c',
'3d',
'3e',
'3f',
'40',
'41',
'42',
'43',
'44',
'45',
'46',
'47',
'48',
'49',
'4a',
'4b',
'4c',
'4d',
'4e',
'4f',
'50',
'51',
'52',
'53',
'54',
'55',
'56',
'57',
'58',
'59',
'5a',
'5b',
'5c',
'5d',
'5e',
'5f',
'60',
'61',
'62',
'63',
'64',
'65',
'66',
'67',
'68',
'69',
'6a',
'6b',
'6c',
'6d',
'6e',
'6f',
'70',
'71',
'72',
'73',
'74',
'75',
'76',
'77',
'78',
'79',
'7a',
'7b',
'7c',
'7d',
'7e',
'7f',
'80',
'81',
'82',
'83',
'84',
'85',
'86',
'87',
'88',
'89',
'8a',
'8b',
'8c',
'8d',
'8e',
'8f',
'90',
'91',
'92',
'93',
'94',
'95',
'96',
'97',
'98',
'99',
'9a',
'9b',
'9c',
'9d',
'9e',
'9f',
'a0',
'a1',
'a2',
'a3',
'a4',
'a5',
'a6',
'a7',
'a8',
'a9',
'aa',
'ab',
'ac',
'ad',
'ae',
'af',
'b0',
'b1',
'b2',
'b3',
'b4',
'b5',
'b6',
'b7',
'b8',
'b9',
'ba',
'bb',
'bc',
'bd',
'be',
'bf',
'c0',
'c1',
'c2',
'c3',
'c4',
'c5',
'c6',
'c7',
'c8',
'c9',
'ca',
'cb',
'cc',
'cd',
'ce',
'cf',
'd0',
'd1',
'd2',
'd3',
'd4',
'd5',
'd6',
'd7',
'd8',
'd9',
'da',
'db',
'dc',
'dd',
'de',
'df',
'e0',
'e1',
'e2',
'e3',
'e4',
'e5',
'e6',
'e7',
'e8',
'e9',
'ea',
'eb',
'ec',
'ed',
'ee',
'ef',
'f0',
'f1',
'f2',
'f3',
'f4',
'f5',
'f6',
'f7',
'f8',
'f9',
'fa',
'fb',
'fc',
'fd',
'fe',
'ff'
]
function hex(arrayBuffer: ArrayBuffer): string {
const buff = new Uint8Array(arrayBuffer)
const hexOctets = []
for (let i = 0; i < buff.length; ++i) hexOctets.push(byteToHex[buff[i]])
return hexOctets.join('')
}
return ['SAPISIDHASH', [timestamp, hex(gen_hash)].join('_')].join(' ')
}
export const getCookie = (
cookies: string,
name: string,
matchWholeName = false
): string | undefined => {
const regex = matchWholeName
? `(^|\\s?)\\b${name}\\b=([^;]+)`
: `(^|s?)${name}=([^;]+)`
const match = cookies.match(new RegExp(regex))
return match ? match[2] : undefined
}
On Android, I can use authentication required methods on my app built with React Native. But for some reason, when I run it on iOS, it does not work.