twitter-api-typescript-sdk: Missing required parameter [code_verifier]

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

About this issue

Most upvoted comments

@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.

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),
    });
  }
};

@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.

@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 });
};

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

@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

@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.

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