twitter-api-typescript-sdk icon indicating copy to clipboard operation
twitter-api-typescript-sdk copied to clipboard

Missing required parameter [code_verifier]

Open radzionc opened this issue 3 years ago • 15 comments

await authClient.requestAccessToken(code)

crashes with

error: {
  error: 'invalid_request',
  error_description: 'Missing required parameter [code_verifier].'
}

I think the problem is that #codeVerifier is undefined here 👇 https://github.com/twitterdev/twitter-api-typescript-sdk/blob/0d4954c675dbfc566c6911adc4d4178dce926ca4/src/OAuth2User.ts#L170

radzionc avatar Jun 23 '22 18:06 radzionc

I am having this exact same issue. The only thing I can think is that the oAuth has a different code_verifier value than my redirect URL for the callback-->both use new auth.OAuth2User to create a client for use on either oAuth or the callback (which is slightly different than what the example code is doing in an app). This is just speculation though.

wmencoder avatar Jun 29 '22 23:06 wmencoder

Thanks @RodionChachura, you need to call generateAuthURL to create the code_verifier. Will add a check and throw a helpful error to improve this

refarer avatar Jun 30 '22 02:06 refarer

@wmencoder thats right, you need to use the same Auth Client that generated the authentication URL

refarer avatar Jun 30 '22 02:06 refarer

I am having the same issue, but this solution will not work for me. Is there a way to generate an access token statelessly? My app runs in a serverless environment and I can't persist an instance of the OAuth2User class between requests. I can store any data necessary, but the code_verifier property is private and it can't be accessed from outside the class.

EDIT: I have resolved the issue by passing my own code_challenge and setting the code_challenge_method as 'plain'. I then store the code_challenge and generate another authURL with the same code_challenge before generating the access token. I hope that helps if anyone else is in the same situation.

jgjr avatar Jul 12 '22 14:07 jgjr

@jgjr I am also trying to use this SDK in a serverless environment. Could you please a minimal working code for the same?

@refarer An official example to run this SDK on serverless environment would be really appreciated.

sasivarnan avatar Jul 30 '22 16:07 sasivarnan

@sasivarnan The solution was fairly simple. In the initial call of generateAuthURL() I use code_challenge_method: 'plain' and save the code_challenge that I use. Then when the user is redirected back to my platform I call the generateAuthURL() method again with the same saved code_challenge, and then the requestAccessToken() method with the code I have received.

jgjr avatar Aug 02 '22 10:08 jgjr

@sasivarnan I was able to generate a stateless client by creating a class

class OAuth2UserStateless extends auth.OAuth2User

That overloads the constructor and assigns the Token that I pass as a cookie from the client. You can theoretically do the same with the code_verifier property instead of doing void call to generateAuthURL to populate that property.

This is a hack and clearly this SDK isn't designed for statelessness at this time.

mathieuhelie avatar Aug 04 '22 17:08 mathieuhelie

@sasivarnan The solution was fairly simple. In the initial call of generateAuthURL() I use code_challenge_method: 'plain' and save the code_challenge that I use. Then when the user is redirected back to my platform I call the generateAuthURL() method again with the same saved code_challenge, and then the requestAccessToken() method with the code I have received.

Feels crazy hacky but works

elie222 avatar Aug 13 '22 23:08 elie222

I created a PR to solve this issue, I need to work with the maintainers to get a code review and eventually this feature can be merge, the PR is here if you want to take a look:

https://github.com/twitterdev/twitter-api-typescript-sdk/pull/42

danforero avatar Sep 28 '22 02:09 danforero

The 1.2.0 version published 6 days ago (thanks @refarer!) allows the token to be passed on the constructor. So, now you could do something like this:

  1. Create the endpoint to start the authentication process: generate state and challenge and call generateAuthURL; persist these values to recreate the OAuth2User later on;
  2. Create another endpoint for the callback: recreate the auth with state and challenge and call requestAccessToken passing the code received; store the token returned by that function;
  3. Pass the token on the OAuth2UserOptions during user creation.

Using firebase functions, my simplified code is:

// authenticate.ts
export const authenticate = functions
  .region('southamerica-east1')
  .https.onRequest(async (req, res) => {
    res.redirect(await generateAuthURL());
  });
// authenticationHandler.ts
export const authenticationHandler = functions
  .region('southamerica-east1')
  .https.onRequest(async (req, res) => {
    const { code } = req.query;
    await handleAuthCode(code as string);
    res.send('OK');
  });
// auth.ts
let user: auth.OAuth2User | null = null;
const getUser = async () => {
  if (!user) {
    const { token } = (await getPlatformTokens()) ?? {};
    user = new auth.OAuth2User({
      client_id: <CLIENT_ID>,
      client_secret: <SECRET>,
      callback: <CALLBACK_URL>,
      scopes: ['tweet.read', 'tweet.write', 'users.read', 'offline.access'],
      token: token ? JSON.parse(token) : undefined,
    });
  }
  return user;
};

let client: Client | null = null;
const getClient = async () => {
  if (!client) client = new Client(await getUser());
  return client;
};

export const generateAuthURL = async () => {
  const state = randomBytes(12).toString('hex');
  const challenge = randomBytes(12).toString('hex');
  await updatePlatformTokens({
    state,
    challenge,
  });
  const user = await getUser();
  return user.generateAuthURL({
    state,
    code_challenge_method: 'plain',
    code_challenge: challenge,
  });
};

export const handleAuthCode = async (code: string) => {
  const user = await getUser();
  const { state, challenge } = (await getPlatformTokens()) ?? {};
  if (state && challenge) {
    user.generateAuthURL({
      state,
      code_challenge_method: 'plain',
      code_challenge: challenge,
    });
    const { token } = await user.requestAccessToken(code);
    await updatePlatformTokens({
      token: JSON.stringify(token),
    });
  }
};

pdandradeb avatar Oct 08 '22 20:10 pdandradeb

@pdandradeb what is getPlatformTokens and it's type?

apecollector avatar Oct 16 '22 00:10 apecollector

@apecollector

// types.ts
export interface PlatformTokens {
  token?: string;
  state?: string;
  challenge?: string;
}
// tokens.ts
export const getPlatformTokens = async () => {
  const tokens = await getFirestore()
    .collection('platform')
    .doc('tokens')
    .get();
  return tokens.data() as PlatformTokens;
};

export const updatePlatformTokens = async (tokens: Partial<PlatformTokens>) => {
  await getFirestore()
    .collection('platform')
    .doc('tokens')
    .set(tokens, { merge: true });
};

pdandradeb avatar Oct 17 '22 20:10 pdandradeb

// authenticate.ts
export const authenticate = functions
  .region('southamerica-east1')
  .https.onRequest(async (req, res) => {
    res.redirect(await generateAuthURL());
  });

My implementation is almost exactly the same except using an onCall instead of on request, in hopes of limiting the ability for this function to be called from any browser.

PMLyf avatar Oct 21 '22 17:10 PMLyf

This thread was super helpful! Got me past a few roadblocks while trying to use this sdk with nextjs and serverless api functions

jacklynch00 avatar Dec 09 '22 19:12 jacklynch00

This SDK is very problematic. In particular, the inability to inject code_verifier from the outside should be fixed as soon as possible. I'm extending it with a Service class that has a constructor that can inject the code_verifier and code_challenge from the outside. I hope this implementation helps those who are having trouble. Also, if there are any problems, please point them out.

https://gist.github.com/amemiya/14f1614819210b8be5d4afcbf4727ca6

amemiya avatar Dec 28 '22 12:12 amemiya