60

Right now, I'm struggling to understand AWS Cognito so maybe someone could help me out. I set a domain to serve Cognito's hosted UI for my User Pool like what's described here. So when I go to https://<my-domain>.auth.us-east-1.amazoncognito.com/login?response_type=code&client_id=<MY_POOL_CLIENT_ID>&redirect_uri=https://localhost:8080 I get a login page where my users can login to my app with Google. That part is working great.

I'm confused about what to do with the code that is returned from that page once my user logs in. So once I get redirected to Google and authorize the application to view my information, I get redirected back to one of my URLs with a code in the query params. Right now I'm redirecting to localhost, so the redirect URL look like this:

https://localhost:8080/?code=XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXX

What exactly is this code? Also, how do I use it to get access to AWS resources for my user?

arjabbar
  • 6,044
  • 4
  • 30
  • 46
  • 3
    You're using the Authorization Code flow. In this flow, the Hosted UI (running in the user's browser) will authenticate the user against the Cognito User Pool, and return the authorization code. You must then send this code to your webserver. Your webserver exchange this code for access tokens, which it uses to finally retrieve the user's information. As you can see, the user's information, as well as the access tokens, never reach the user's browser; this is the security promise of this entire scheme. How do your server exchange the code for the token? See answer by Marcio Ghiraldelli. – Sarsaparilla Dec 20 '20 at 19:50
  • 2
    This might be helpful : https://www.youtube.com/watch?v=oFSU6rhFETk&ab_channel=BeABetterDev – Mooncrater Nov 01 '21 at 10:03

8 Answers8

67

First off, screw authentication a thousand times. No one deserves to spend half a day looking at this shit.

Authentication for API Gateway Authorized with Cognito

Ingredients

  1. client_id and client_secret: In Cognito > General Settings > App clients you can find the App client id, then click on Show Details to find the App client secret

  2. For the header Authorization: Basic YWJjZGVmZzpvMWZjb28zc... you need to encode those two with: Base64Encode(client_id:client_secret), for example in Python:

    import base64  
    base64.b64encode('qcbstohg3o:alksjdlkjdasdksd'.encode()).decode()
    

    side note: Postman also has an option to generate this in Authorization > Basic Auth

  3. redirect_uri: passed in the body, it is the callback url that you configured in App integration > App client settings.
    This MUST match with what you configured or you will get a totally unhelpful message { "error": "invalid_grant" }

Example of a request to get a token from the code:

curl --location --request POST 'https://mycognitodomain.auth.us-east-1.amazoncognito.com/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic <base64 encoded client_id:client_secret>' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=<use the code you received post login>' \
--data-urlencode 'redirect_uri=https://myapp.com'

This will return your tokens:

{
  "access_token":"eyJz9sdfsdfsdfsd", 
  "refresh_token":"dn43ud8uj32nk2je",
  "id_token":"dmcxd329ujdmkemkd349r",
  "token_type":"Bearer", 
  "expires_in":3600
}

Then take the id_token and plug into your API call:

curl --location --request GET 'https://myapigateway.execute-api.us-east-1.amazonaws.com/' \
--header 'Authorization: <id_token>'

Ok, this is tagged as JavaScript but since we also suffer in Python

Friendly reminder: this is an example, please don't hardcode your secrets.

import requests

# In: General Settings > App clients > Show details
client_id = "ksjahdskaLAJS ..."
client_secret = "dssaKJHSAKJHDSsjdhksjHSKJDskdjhsa..."

# URL in your application that receives the code post-authentication
# (Cognito lets you use localhost for testing purposes)
callback_uri = "http://localhost:8001/accounts/amazon-cognito/login/callback/"

# Find this in: App Integration > Domain
cognito_app_url = "https://my-application-name.auth.us-west-2.amazoncognito.com"

# this is the response code you received - you can get a code to test by going to
# going to App Integration > App client settings > Lunch Hosted UI
# and doing the login steps, even if it redirects you to an invalid URL after login
# you can see the code in the querystring, for example:
# http://localhost:8001/accounts/amazon-cognito/login/callback/?code=b2ca649e-b34a-44a7-be1a-121882e27fe6
code="b2ca649e-b34a-44a7-be1a-121882e27fe6"

token_url = f"{cognito_app_url}/oauth2/token"
auth = requests.auth.HTTPBasicAuth(client_id, client_secret)

params = {
    "grant_type": "authorization_code",
    "client_id": client_id,
    "code": code,
    "redirect_uri": callback_uri
}

response = requests.post(token_url, auth=auth, data=params)

print(response.json()) # don't judge me, this is an example
Christian
  • 3,708
  • 3
  • 39
  • 60
bubbassauro
  • 3,969
  • 2
  • 45
  • 45
  • 2
    Super clear explanation. The confusing part in the original manual is how to build the Authorization in step #2. – Day.ong Li May 24 '21 at 14:53
  • 2
    Really clean clear answer. I'm a little confused on where the `` comes from? I'm guessing this comes from the first redirect to the "server" side that would then use the following. (ie https://.auth.us-east-2.amazoncognito.com/login?response_type=code&client_id=&redirect_uri=) – Nate-Wilkins Jun 24 '21 at 01:57
  • Yes, after you go through the login page, it will redirect to the `redirect_uri` with a code parameter, something like: `http:///?code=&state=AbcDefgh` – bubbassauro Jun 29 '21 at 02:31
  • Getting an `{"error":"invalid_request"}` message - any way to figure out what is going on? – Daniel Aug 12 '21 at 16:18
  • @Daniel I HATE how bad the return codes are with Cognito, it's really hard to find what's wrong! Most commonly it's routes that are not matching, check in your App Client settings if the callback routes match exactly with routes that are available in your application and check if your application is pointing to the right Cognito route. – bubbassauro Aug 12 '21 at 17:05
  • double checked it and i was missing the key `redirect_uri` - but now im slapped with `{"error":"invalid_grant"}` - should the redirect uri be the full url in the Callback URL(s) section of the app client? – Daniel Aug 12 '21 at 17:19
  • Actually - it looked like the "code" expired - I logged in again, got a new code, and got back a response - thanks very much for your help! – Daniel Aug 12 '21 at 17:20
  • @bubbassauro - is there any way to convert this to a python request? – Daniel Aug 12 '21 at 18:11
  • 2
    One more gotcha: if you configure multiple callback uris in your app client settings, then it seems that the one you use as the redirect_uri in the /oauth2/token request must match the one that you originally used to obtain the authorization code. – fblundun Sep 14 '21 at 09:42
  • I was stuck at `{"error":"invalid_request"}`. I then figured out that the request body should not be JSON, rather it is query param -_- – saintlyzero Jun 26 '22 at 01:49
  • After fighting for half a century with `invalid_grant` nonsense finally got it working, thanks! What else i can use these tokens for? Can I call AWS services, like start and EC2 using the aws cli? – Vergil C. Jul 06 '22 at 20:10
  • 1
    Found info here for the validation and the usage of the tokens https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-with-identity-providers.html – Vergil C. Jul 06 '22 at 20:34
14

To fetch AWS credentials (id_token, access_token and refresh_token) from the code request parameter returned by the authorisation code oath2 flow, you should use your Cognito User Pool web domain /oauth2/token endpoint, following https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html instructions.

Pay attention to the HTTP Basic Authorisation user and password instructions, it should be your Cognito App client_id and client_secret, otherwise, you get a invalid_client error.

The code flow is supposed to be used server side, as you avoid tokens floating around on URLs. If you're planning to do that client side, you should use the response_type=token, as it gives this result directly on the login redirect.

iamsuman
  • 1,413
  • 19
  • 32
  • How do you set the `response_type` to `token` for the redirect URL in the Cognito User Pool? – The Unknown Dev Feb 25 '19 at 20:14
  • The `response_type` shouldn't be set in the redirect URL, but in the initial URL request. See my cognito example application https://github.com/marciogh/my-react-dropbox/blob/master/react/src/App.js#L55 – Marcio Ghiraldelli Apr 12 '19 at 07:33
7

Not sure if this is going to be useful 10 months from since it was asked but might be helpful to others.

I have used response_type=token (Oauth flow=implicit grant & scope=openid) & provider as Cognito. After you login using the default login page, you will get an id_token & access_token. You can get a temporary identity for your user using this id_token. You also need to have an Federated identity pool setup for this, with roles assigned for authenticated & unauthenticated users, and linked to the user pool you are authenticating with. Once you have that (assuming you are using javascript), you can follow example at Cognito user identity pools javascript examples. My sample code derived from the same -

function getAccessToken(idToken, identityPoolId, userPool) {
        let provider = "cognito-idp.us-east-2.amazonaws.com/" + userPool;
        let login = {};

        login[provider] = idToken;

        // Add the User's Id Token to the Cognito credentials login map.
        let credentials = new AWS.CognitoIdentityCredentials({
            IdentityPoolId: identityPoolId,
            Logins: login
        });

        //call refresh method in order to authenticate user and get new temp credentials
        credentials.get((error) => {
            if (error) {
                console.error(error);

                let response = {
                    statusCode: 500,
                    body: JSON.stringify(error)
                };

                return response;

            } else {
                console.log('Successfully logged!');
                console.log('AKI:'+ credentials.accessKeyId);
                console.log('AKS:'+ credentials.secretAccessKey);
                console.log('token:' + credentials.sessionToken);

                let response = {
                    statusCode: 200,
                    body: JSON.stringify({
                        'AKI': credentials.accessKeyId,
                        'AKS': credentials.secretAccessKey,
                        'token': credentials.sessionToken
                    })
                };

                return response;
            }
        });
    }

Hope this helps.

asr9
  • 2,440
  • 1
  • 21
  • 37
  • Hi, I'm having the same issue, and I wanted to ask what do you mean by "default login". Is it the domain provided by AWS? Because it seems you can only use that website if you select "Authorization code grant" as your OAuth flow (which means, if I'm understanding this correctly, you will get a code and not a token). – Pablo Barría Urenda Jul 24 '18 at 20:53
  • 1
    Yes, the login URL you get (or create based on format) when you configure a domain on Cognito. It works with `implicit grant` as well. This is one which works for me - `https://exampledomain.auth.us-east-2.amazoncognito.com/login?response_type=token&client_id=xxxxx&redirect_uri=http://localhost/portal.html` – asr9 Jul 24 '18 at 22:16
3

Here is a more simple/clear way of doing this with Axios:

axios.post(`https://${yourCognitoPull}.auth.us-east-1.amazoncognito.com/oauth2/token`, null, {
    params: {
        grant_type: 'authorization_code',
        client_id: 'xxxxx replace with your client id xxxxxx',
        client_secret: 'xxxxx replace with your secret xxxxxx',
        code: 'xxxx code you received from cognito xxxx',
        redirect_uri: 'xxxx replace with the uri that you configure as valid xxxx'
    }
}).then(response => {
        const newAccessToken = response.data.access_token;
}).catch(error => {
        console.log('Error al renovar el token de acceso', error);
});

Very important: Add the ClientSecret AND the RedirectURI (even if your are not redirecting, just getting the token, because it seems that Cognito checks if you know the valid URI as an additional security check)

(I strongly recommend doing this in a backend environment to protect the ClientSecret... or encrypting the value someway at least)

Ari Waisberg
  • 1,186
  • 1
  • 12
  • 23
2

you can find the "Authorization code grant" in the doc :http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-idp-settings.html

Summer Guo
  • 269
  • 1
  • 5
  • 1
    Hmm, I did read that and see how to use that endpoint described here: http://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html. I guess I assumed there was something built into the SDKs that would handle this for me. So what I'm hearing basically is that I should stick to using the `response_type=code` query param for my login page. Using that places the tokens in the URL once the user gets directed. It appears it's either that or I have to make the request for the tokens myself. Does this seem right to you? – arjabbar Sep 11 '17 at 15:02
  • 2
    What I meant to say was "I should stick to using the `response_type=token`" – arjabbar Sep 15 '17 at 00:02
  • 3
    I'm still confused with the docs. Can I exchange that code with a token somehow? It seems to me the terminology is switching up in the docs. What method in the SDK do I call with the "code" to get user info? – JoshNaro Nov 20 '17 at 04:24
2

When this is true

#1: https://<my-domain>.auth.us-east-1.amazoncognito.com/login?response_type=code&client_id=<MY_POOL_CLIENT_ID>&redirect_uri=https://localhost:8080

The AWS Cognito Redirect will be

https://localhost:8080?code=8023253d-1c76-4c70-be3d-c8c29cc18c95

Then from your web client, use the returned code in the following app request.

NOTE: Post Header Content-Type MUST BE 'application/x-www-form-urlencoded'; Code is a one time use; redirect_uri from #1 and #2 requests must match.

#2: POST https://<my-domain>.auth.us-east-1.amazoncognito.com/oauth2/token?grant_type=authorization_code&code=8023253d-1c76-4c70-be3d-c8c29cc18c95&client_id=<MY_POOL_CLIENT_ID>&redirect_uri=https://localhost:8080

And will then get a response payload as follows

{
  "id_token": "...",
  "access_token": "...",
  "refresh_token": "...",
  "expires_in": 3600,
  "token_type": "Bearer"
}

NOTE: If you instead have a Custom Domain setup, then the two above calls would change to

#1: https://my.custom.auth.domain.com/login?...

#2: https://my.custom.auth.domain.com/oauth2/token?...
SoEzPz
  • 14,958
  • 8
  • 61
  • 64
0

Thank you for sharing @bubbassauro. Below my PHP version :


try {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $_ENV['URL_COGNITO'].'/oauth2/token');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_POST, 1);
    $headers = [];
    $headers[] = 'Content-Type: application/x-www-form-urlencoded';
    $headers[] = 'Authorization: Basic '.base64_encode($_ENV['CLIENTID'].':'.$_ENV['CLIENTSECRET']);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch,CURLOPT_POSTFIELDS,  http_build_query([
        'grant_type' => "authorization_code",
        'client_id' => $_ENV['CLIENTID'],
        'code' => $code,
        'redirect_uri' => $_ENV['URL_AUTH0']
    ]));
    $result = curl_exec($ch);
    curl_close($ch);
    $result= json_decode($result);
    return $result->id_token;

} catch (Exception $e) {
    return $e->getMessage();
}

bingo
  • 150
  • 3
  • 10
-2

Simply, You can request the id/access/refresh tokens using the code and the Cognito clientId+hostname, then use the id and access token to identify the user in your API calls.

0xZ
  • 1
  • 1
  • "How to use the code returned from Cognito..." is what the user asked. Where is your example? – SoEzPz Jan 05 '23 at 23:39