Search code examples
node.jsoauth-2.0amazon-cognito

AWS Cognito OAuth2 with PCKE "Invalid Request" Code Challenge


I have been trying to add the state and code_challenge to our flow but for some reason, I continue to get invalid_request responses from Amazon.

I followed this Auth0 tutorial to a tee.

  /**
   * Converts buffer to Base64 URL encoded string
   *
   * @param {Buffer} buf The buffer to convert
   * @returns {string}
   */
  private base64URLEncode(str: any): string {
    return str.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  }

  /**
   * Generates a new code challenge
   *
   * @returns {string} The code challange string
   */
  private generateCodeChallenge(): string {
    if (!this._verifier) this._verifier = this.base64URLEncode(crypto.randomBytes(32));

    return this.base64URLEncode(crypto.createHash('sha256').update(this._verifier).digest());
  }

  /**
   * Generates the Authorization URL
   */
  public generateAuthUrl(): string {
    if (!this.config.clientId) throw new Error('Client ID is missing from configuration.');
    if (!this.config.redirectUrl) throw new Error('Redirect URI is missing from configuration.');

    if (!this._state) this._state = this.base64URLEncode(crypto.randomBytes(28));

    return `${this.config.protocol}://signin.${
      this.config.host
    }/login?response_type=code&client_id=${this.config.clientId}&redirect_uri=${
      this.config.redirectUrl
    }&scope=${this._scope.join('%20')}&state=${
      this._state
    }&code_challenge_method=S256&code_challenge=${this.generateCodeChallenge()}`;
  }

  /**
   * Verifies that the state matches
   *
   * @returns {boolean}
   */
  public verifyState(state: string): boolean {
    return this._state === state;
  }

  /**
   * Retrieves a new OAuth Authorization Grant token
   */
  public async getToken(code: string): Promise<UserAccessToken> {
    if (!this.config.clientId) throw new Error('Client ID is missing from configuration.');
    if (!this.config.redirectUrl) throw new Error('Redirect URI is missing from configuration.');

    try {
      const token = (await this.req.postform(
        `/oauth2/token`,
        {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Cache-Control': 'no-store',
          },
        },
        {
          grant_type: 'authorization_code',
          code,
          client_id: this.config.clientId,
          code_verifier: this._verifier,
          redirect_uri: this.config.redirectUrl,
        }
      )) as UserAccessToken;

      this._userAccessToken = token;

      return token;
    } catch (err) {
      throw err;
    }
  }

Everything is working well up until exchanging a code for a token. We redirect to our hosted UI and successfully get a code back. When we exchange that for a token, no matter what I have tried, we get an invalid_request response. If I remove the code challenge, everything works as expected so I am pretty confident that it's what is causing the issue.

Hosted UI

Redirect Back to Admin

Attempt to get Access Token

Update 1

This function is called in our React app inside of a useEffect hook like so:

  useEffect(() => {
    const authUrl = sdk.auth.generateAuthUrl();
    console.log(authUrl);
    if (!code) window.location.assign(authUrl);
    console.log(`New Auth URL: ${authUrl}`);
    sdk.auth
      .getToken(code as string)
      .then((token) => {
        localStorage.setItem('sdk_access_token', JSON.stringify(token));
        sdk.organizations.getUser().then((user) => {
          setUser({ ...user });
          history.push('/');
        });
      })
      .catch((err) => console.log(err));
  }, []);

Both console.log statements show the same URL: Console Screenshot

But on the Network debugging tab, the request URL is different: Network Screenshot

How is that possible? Does window.location.href somehow encode the URL?


Solution

  • Your code and messages look fine as far as I can see. If you have an App Client Secret configured in Cognito that would explain the problem (Cognito error responses are not the best).

    Maybe try these hard coded values from a working Cognito sample of mine - or run the values you are calculating through this tool. This will enable you to know whether the problem is with the PKCE values:

    • challenge: tQV5Ny5h0r5pwqpDr0OGnQWgfU4DTTniuu99HZGOOE0
    • verifier: 4ffc114e504047deb57f3e8dfeeda70d93bbfb6d8f0444c9b9b147f3872dbde24f58cbe3047d49ce9b5194fe80e0905a

    There are plenty of libraries with some working PKCE code though I expect the Auth0 code is fine ...