Search code examples
typescriptoauth-2.0

Oauth token exchange returning a 400 error in Cognito


My auth model is set up so that the callback hits an endpoint on my site and then it parses out the code param and does the token exchange. This is what I have so far

   class TokenExchange extends React.Component <any, any> {
    constructor(props: any) {
        super(props);
    }

    componentDidMount() {
        const parsed = queryString.parse(location.hash);
        const code = parsed["code"]
        console.log(code);
        const postData = {
            "grant_type": 'authorization_code',
            "code": code,
            "client_id": OAUTH_CLIENT_ID,
            "redirect_uri": OAUTH_REDIRECT_URI
        }
        console.log(postData)
        const credential = btoa(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`)
        const headers = {
            "Accept": "application/json",
            "Content-Type": "application/x-www-form-urlencoded",
            "authorizationMethod": "body",
            "Authorization": `Basic ${credential}`
        }
        console.log(headers)

        const requestOptions = {
            method: 'POST',
            headers: headers,
            body: JSON.stringify(postData)
        };
        fetch(OAUTH_TOKEN_EXCHANGE_URL, requestOptions)
            .then(response => response.json())
            .then((result) => {
              console.log(result)
            })
    }
}

The problem I'm running into is that my code's running into an HTTP 400 error and it's not giving me much information to go by other than invalid request.

This is what the copied curl request looks like

curl 'https://MYAPP.us-west-2.amazoncognito.com/oauth2/token' \
  -H 'authority: MYAPP.us-west-2.amazoncognito.com' \
  -H 'pragma: no-cache' \
  -H 'cache-control: no-cache' \
  -H 'sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"' \
  -H 'accept: application/json' \
  -H 'authorizationmethod: body' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'authorization: Basic BASE64ENCODEDSTUFFHERE' \
  -H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36' \
  -H 'content-type: application/x-www-form-urlencoded' \
  -H 'origin: http://localhost:5000' \
  -H 'sec-fetch-site: cross-site' \
  -H 'sec-fetch-mode: cors' \
  -H 'sec-fetch-dest: empty' \
  -H 'referer: http://localhost:5000/' \
  -H 'accept-language: en-GB,en;q=0.9' \
  --data-raw '{"grant_type":"authorization_code","code":"SOMECODE","client_id":"SOMEID","redirect_uri":"https://SOMEURI"}' \
  --compressed

Could anyone spot what I may be doing wrong?


Solution

  • The data should be sent form URL encoded and not as JSON so update your code / script:

    STEP 1

    Use this type of syntax to send the data and get a working solution:

    fetch('https://example.com/login', {
        method: 'POST',
        body: new URLSearchParams({
            'field1': 'value1',
            'field2': 'value2'
        })
    });
    

    Or in curl you can do this:

    --data-urlencode "field1=value1" \
    --data-urlencode "field2=value2"
    

    STEP 2

    An SPA should not use a client secret since it cannot store it securely. Aim to update your requests to use Proof Key for Code Exchange (PKCE) as recommended for SPAs. Steps 4 and 8 in my blog post show what this should look like, and a sample of mine runs in Cognito like this.