aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.38k stars 2.1k forks source link

How to refresh Cognito tokens #446

Closed nnurmano closed 4 years ago

nnurmano commented 6 years ago

There is a similar issue in https://github.com/aws/aws-amplify/issues/405, but the OP uses old approach, I would like to know how to refresh tokens based on aws-amplify library? Also, this is probably related, how could I make sure that the user is always logged in?

david114 commented 6 years ago

i literally tried everything, still doesn't work. wait 1hr refresh (works fine) wait 1hr refresh (error login token expired)

methods i tried:

AWS.config.credentials.get() AWS.config.credentials.refresh() AWS.config.credentials.getPromise() AWS.config.credentials.refreshPromise() cognitoUser.getSession() cognitoUser.refreshSession()

nnurmano commented 6 years ago

I would like to achieve the same with amplify library.

nnurmano commented 6 years ago

I had used export function retrieveUserFromLocalStorage() { return new Promise((success, failure) => { // grab thecognitoUserobject fromuserPool // this is possible without login because we had already logged in before (whereas verifyPIN and resetPassword have not) const cognitoUser = userPool.getCurrentUser(); if (cognitoUser != null) { // get the latest session fromcognitoUser cognitoUser.getSession(function (error, session) { // if failed to get session, reject the promise if (error) { console.log('Error occurred while retrieving user account', error); failure(error); return; } // check that the session is valid console.log('Is session still valid?: ' + session.isValid()); // save to localStorage the jwtToken from thesession` localStorage.setItem('idToken', session.getIdToken().getJwtToken());

            // Edge case, AWS Cognito does not allow for the Logins attr to be dynamically generated. So we must create the loginsObj beforehand
            const loginsObj = {
                // our loginsObj will just use the jwtToken to verify our user
                [USERPOOL_ID]: session.getIdToken().getJwtToken()
            }
            // create a new `CognitoIdentityCredentials` object to set our credentials
            // we are logging into a AWS federated identity pool
            AWS.config.credentials = new AWS.CognitoIdentityCredentials({
                IdentityPoolId: IDENTITY_POOL_ID, // your identity pool id here
                Logins: loginsObj
            })
            // refresh the credentials so we can use it in our app
            AWS.config.credentials.refresh(function () {
                // resolve the promise by again building the user object to be used in our React-Redux app
                success(buildUserObject(cognitoUser))
            })
        });
    } else {
        // if failure, reject the promise
        failure('Failed to retrieve user from localStorage');
    }
`

To refresh user session, are you using the same?

david114 commented 6 years ago

Yes, i'm pretty much doing the same thing. I've tried many variations. There seems to be a bug because I can successfully refresh after 1 hr, but after 2hrs I suddenly receive an error, although the login token should still be valid.

nnurmano commented 6 years ago

If this is a bug, could you try with different version of aws cognito packages?

david114 commented 6 years ago

I was using a ~ 1 year old version, than updated to the latest version of cognito and cognito identity, both didn't work.

retwedt commented 6 years ago

I am also trying to figure this out, anyone have any luck?

david114 commented 6 years ago

Since i can only refresh successfully once i'm doing it like this now:

if(AWS.config.credentials.expired) {
  confirm("Session is expired, Page needs to be refreshed!");
  if(confirm()) {
    location.reload();
  }
}

If one of you guys come up with a proper solution for this, please post them here. i'd be really happy.

Legym commented 6 years ago
import { Auth } from 'aws-amplify';

        // Async call to Cognito to check for token
        // This will be checked every time a protected route is loaded
        Auth.currentSession()
            .then((response) => {

                Axios.defaults.headers.common = {
                    authToken: response.idToken.jwtToken,
                };

            })
            .catch((error) => {
                console.log(error);
            });
nnurmano commented 6 years ago

How about this? It is from AUTH library itself, with some modifications export const keepAlive = () => { if (!Auth.credentials) { Auth.setCredentialsForGuest(); }

const ts = new Date().getTime();
const delta = 10 * 60 * 1000; // 10 minutes
let credentials = Auth.credentials || {};
const { expired, expireTime } = credentials;

if (!expired && expireTime > ts + delta) {
    return Promise.resolve(credentials);
}

return new Promise((resolve, reject) => {
    Auth.currentUserCredentials()
        .then(() => {
            credentials = Auth.credentials;
            credentials.refresh(error => {
                _Logger.debug('Credentials are changed from previous');
                if (error) {
                    _Logger.debug('Credentials refresh error', error);
                    resolve(null);
                } else {
                    _Logger.debug('Credentials has been refreshed', credentials);
                    resolve(credentials);
                }
            });
        })
        .catch(() => resolve(null));
});

}

bbernays commented 6 years ago

I have the same issue. I have taken to forcing the user to do a page reload.

xdl commented 6 years ago

I'd also like to know what the best way of doing this is; calling currentCredentials().then(credentials => credentials.refresh()) refreshes the expireTime of currentCredentials (what does this mean?) but hasn't refreshed the access/id jwt tokens.

bbernays commented 6 years ago

There seems to be a lot of questions on how to do this. Could it be possible that an example of this be added to the documentation?

david114 commented 6 years ago

yeeeeeeeeeeeeeeeeeeeeeeeeeeeah, after almost 2 weeks i finally solved it.

You need the Refresh Token to receive a new Id Token. Once the Refreshed Token is acquired, update the AWS.config.credentials object with the new Id Token.

here's an example on how to set this up, runs smoothly!

refresh_token = session.getRefreshToken();   // you'll get session from calling cognitoUser.getSession()
if (AWS.config.credentials.needsRefresh()) {
  cognitoUser.refreshSession(refresh_token, (err, session) => {
    if(err) {
      console.log(err);
    } 
    else {
      AWS.config.credentials.params.Logins['cognito-idp.<YOUR-REGION>.amazonaws.com/<YOUR_USER_POOL_ID>']  = session.getIdToken().getJwtToken();
      AWS.config.credentials.refresh((err)=> {
        if(err)  {
          console.log(err);
        }
        else{
          console.log("TOKEN SUCCESSFULLY UPDATED");
        }
      });
    }
  });
}
bbernays commented 6 years ago

@tipsfedora thank you very much for this! I will definitely check it out!!

powerful23 commented 6 years ago

Closing the issue. Feel free to reopen it if necessary.

retwedt commented 6 years ago

It would be nice to get some guidance on how to do this with the Amplify lib. It appears to me that the session is automatically refreshed if expired, but there is no way to refresh the token early. Is that the case?

mlabieniec commented 6 years ago

@retwedt that's correct, the session is automatically refreshed, you could technically refresh the token yourself however by doing this same approach since the amplify lib uses the cognito lib under the covers. We are working on giving more flexibility / simplifying this as well (in our backlog) to both refresh and also disable automatic refresh.

retwedt commented 6 years ago

Great thanks!

Jasminou commented 6 years ago

I wasn't sur about how to manage the refresh token and how to ask for id and access tokens, so if we are using aws amplify, all this is happened under cover, I don't have to ask for new id token or access token right ?

powerful23 commented 6 years ago

@Jasminou yes aws amplify will refresh your session automatically if expired.

jamesoflol commented 6 years ago

It will refresh if you call the SDK for it, e.g., with Auth.currentSession(), and it finds an expired token + a valid refresh token.

AFAIK there's no timing mechanism to update your localStorage for you in the background. (Auth0's JS SDK uses setTimeout to update localStorage, but that's got its own issues.) If like me you're using Amplify just for the auth, and doing the rest with REST, then you'll want to call Auth.currentSession() before every API call, to get the latest access token. E.g. using Axios HTTP request framework:

// Add latest auth access token to every http request
axios.interceptors.request.use(function (config) {
  return Auth.currentSession()
    .then(session => {
      // User is logged in. Set auth header on all requests
      config.headers.Authorization = 'Bearer ' + session.accessToken.jwtToken
      return Promise.resolve(config)
    })
    .catch(() => {
      // No logged-in user: don't set auth header
      return Promise.resolve(config)
    })
})
powerful23 commented 6 years ago

@jamesoflol yes. The session will be refreshed when calling Auth.currentSession() and Amplify will also call Auth.currentCredentials() before sending any request to AWS services like S3, Pinpoint so the credentials get automatically refreshed.

StevenDufresne commented 5 years ago

@jamesoflol It's probably a good idea to catch Auth.currentSession() and resolve in case you make unauthenticated calls.

jamesoflol commented 5 years ago

@StevenDufresne Agreed. I'll update my example as it seems this issue gets some traffic

kevlarr commented 5 years ago

It would definitely be helpful if the documentation actually mentioned that behavior for currentSession(). We are only using Auth, so we aren't using other modules that will automatically refresh, and we'd opted for the "set timeout to refresh tokens manually" approach until finding this thread.

powerful23 commented 5 years ago

@kevlarr hi, thanks for your feedback. I have sent a pr accordingly: https://github.com/aws-amplify/docs/pull/535

kevlarr commented 5 years ago

Whoa, seriously @powerful23? You are a rockstar! Apologies for not doing that myself.

powerful23 commented 5 years ago

@kevlarr No problem! It's great to have those feedbacks that would gradually improve the docs.

JLee21 commented 5 years ago

In case I'm not too late to the party, I noticed that Amplify's API docs suggest setting a custom header when you first configure the Amplify settings.

Amplify.configure({
  API: {
    endpoints: [
      {
        name: "sampleCloudApi",
        endpoint: "https://xyz.execute-api.us-east-1.amazonaws.com/Development",
        custom_header: async () => { 
          // With Cognito User Pools use this:
          return { Authorization: (await Auth.currentSession()).idToken.jwtToken } 
        }
      }
    ]
  }
});

I like this approach because now every request is syntatically simple

API.get('myRestEndpoint', "/mystuff")
kevlarr commented 5 years ago

@JLee21 Oh that's excellent, thank you. We have only been using the Auth module so had not seen that regarding the API module. Looks like it might be worth using instead of rolling our own mini-lib to handle refreshing tokens.

justingrant commented 5 years ago

FYI, for any others who end up here from Google, if you're using Cognito Identity Pools without Cognito User Pools, then @JLee21's sample above can be adapted as follows:

Amplify.configure({
  API: {
    endpoints: [
      {
        name: "sampleCloudApi",
        endpoint: "https://xyz.execute-api.us-east-1.amazonaws.com/Development",
        custom_header: async () => {
          // fetch the user that was previously authenticated with Facebook via a call
          // to `await Auth.federatedSignIn()`
          const user = await Auth.currentAuthenticatedUser();
          console.log (`will authenticate to AWS with this user: ${JSON.stringify(user)}`);
          return { 'cognito-identity-id' : user.id } 
          // With Cognito User Pools use the code below instead
          // return { Authorization: (await Auth.currentSession()).idToken.jwtToken } 
        }
      }
    ]
  }
}); 
mzohaibqc commented 5 years ago

Unfortunately, Amplify docs do not mention how to refresh token on demand but they always keep saying that Amplify will refresh when required but what if I want to refresh token now, I need to refresh due to some reason, they had not told us. Let's come to the point, here is now you can refresh token on demand.

import { Auth } from 'aws-amplify';

try {
  const cognitoUser = await Auth.currentAuthenticatedUser();
  const currentSession = await Auth.currentSession();
  cognitoUser.refreshSession(currentSession.refreshToken, (err, session) => {
    console.log('session', err, session);
    const { idToken, refreshToken, accessToken } = session;
    // do whatever you want to do now :)
  });
} catch (e) {
  console.log('Unable to refresh Token', e);
}
AndriiShtoiko commented 5 years ago

@mzohaibqc works like magic. thanks!

dcsena commented 5 years ago

@justingrant @JLee21

How would this be done in an app that uses a cognito identity pool with cognito user pools and federated fb/google sign in? Do you have an easy way to combine the two that you have proposed?

harsh-softvan commented 5 years ago

if you are using amplifyJS than this will helpful for you>>>>> https://aws-amplify.github.io/docs/js/authentication#retrieve-current-session

bradennapier commented 4 years ago

I keep running into cases where when passing the token to our api it says token expired. Not sure whats up as we are definitely calling Auth.currentSession() before every request.

rawadrifai commented 4 years ago

Amplify does not allow refreshing your tokens directly for good reasons. Long story short, you can retrieve a "CognitoUser" object from Amplify AuthClass using the currentAuthenticatedUser and from there you can do what a CognitoUser can do.

b0morris commented 4 years ago

I ran into a situation where my Cognito JWT token was expiring on long-running S3 uploads (fails at the 1 hour mark). I couldn't find anything that gave a solution as to how you refresh the token in the middle of a request, so after hours of digging through the Amplify lib and AWS SDK, I finally figured out a solution. You do have to use the AWS SDK directly (sorry Amplify Storage), but I was already doing that so I could pass in the queueSize option to fix the other timeout issue.

When using the AWS SDK, you basically have two options for passing in credentials: 1) the S3 constructor, and 2) the global AWS config. Amplify seems to always pass them in through the constructor, and I'm not sure you can update them that way; however, if you just set the credentials in the global AWS config, you can update the JWT token directly every time you refresh it.

So first I grab the current user's credentials and store it in AWS.config.credentials:

import {Credentials} from '@aws-amplify/core'

Credentials.get().then(creds => {
   AWS.config.credentials = creds
})

Then you can instantiate a S3 object without passing in credentials and start your upload.

const s3 = new S3({
  apiVersion: '2006-03-01',
  signatureVersion: 'v4',
  region: 'us-west-2',
  params: {Bucket: bucket}
})
// Upload params and opts here.
const upload = s3.upload(params, opts).on('httpUploadProgress', progress => {...}).promise()

Then what I do is use setInterval() to call a refresh method every so often. Technically the Cognito token last for an hour, so you can refresh it every 50 minutes or use AWS.config.credentials.needsRefresh() to keep it more generic.

const refreshToken = async () => {
    var session = await Auth.currentSession() //Will refresh token if needed.
    const region = Amplify._config.aws_project_region
    const user_pool_id = Amplify._config.aws_user_pools_id
    AWS.config.credentials.params.Logins['cognito-idp.'+region+'.amazonaws.com/'+user_pool_id] = session.getIdToken().getJwtToken()
    AWS.config.credentials.refresh((err) => {
        if(err)
            console.log("Error updating token")
        else
            console.log("Token updated")
    })
}

const cRef = setInterval(refreshToken, 50*60*1000)

I'm sure I'm violating some best practices here but it works. Just don't forget to clearInterval() when the upload finishes or catches an error.

And the code that @david114 posted earlier definitely sent me down the right path. I just added in the Amplify way of refreshing the JWT token instead of doing it manually just to keep Amplify in sync with S3. That code can be found in the amplify-js lib here under Use Case 32

Really hope this helps someone. Spent way too long trying to figure it out. :P

mzohaibqc commented 4 years ago

@b0morris Thanks a lot for sharing your solutions. A few months ago, actually in April, I faced the same issue that S3 uploads were failing if file upload took more than 1 hour so even after a lot of struggle, I could not find the right solution to refresh token that is being used by S3 upload. So I finally had to use custom temporary credentials from backend API to upload large files.

Amplify needs to handle this scenario but they always keep saying, it will automatically refresh the token but it's will not in such case.

Another issue or limitation with Cognito based credentials is that we can't restrict uploads on an s3 path based on some custom usepool attribute e.g. role or TenantID. but anyway, Thanks for sharing :)

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

justingrant commented 4 years ago

not stale

ripundeep commented 4 years ago

@david114 the solution that you proposed actually giving me an exception repeatedly: TypeError: Cannot read property 'Logins' of undefined Please suggest what could be the issue because in credentials there is no key exists named as 'params'.

david114 commented 4 years ago

@ripundeep

What Libaries are you using? Our Website is quite old now and we are not using AWS Amplify at all.

We're using: aws-sdk-2.172.0.min.js aws-cognito-sdk.min.js amazon-cognito-identity.min.js

Using these files, I can call AWS.config.credentials.params.Logins without any problems

ripundeep commented 4 years ago

@david114 thanks for the reply. I am working on node-js and using amazon-cognito-identity-js. I guess you have utilized in another JavaScript.

david114 commented 4 years ago

@ripundeep we were using plain client-sided javascript

jeffsheets commented 4 years ago

FWIW in case it helps anyone else, I was initially thinking that Auth.currentSession() was not refreshing the token. But in my case we are listening for auth changes with Hub to store our token and make api calls outside of the amplify api library by using axios direct. But when Auth.currentSession() refreshes the token it does not fire a Hub event so our application did not know that a token was updated.

Feels like a bug that no event is fired when a refresh token happens. So for now I've added an axios interceptor to insert the token from Auth.currentSession() into every axios call.

justingrant commented 4 years ago

@jeffsheets - care to share the code you used for that interceptor in case it might be useful to others?

jeffsheets commented 4 years ago

@justingrant Oh yep, here you go! This uses the Axios Interceptors setup to inject the Auth token on every call. I can't remember why our app uses idToken instead of accessToken, so depending on the app that might have to change, but here's the general idea. https://github.com/axios/axios#interceptors

export const axiosRequestInterceptor = async config => {
  //This will get the cached session, or refresh the token first if it has expired
  const session = await Auth.currentSession();

  if (session && session.idToken) {
    config.headers.Authorization = session.idToken.jwtToken;
  }
  return config;
};
axios.interceptors.request.use(axiosRequestInterceptor, e => Promise.reject(e));
Christilut commented 4 years ago

This is what I do with axios on every request:

axios.interceptors.request.use(async function (config: AxiosRequestConfig) {
  const session = await Auth.currentSession()

  try {
    const token = session.getAccessToken().getJwtToken()

    config.headers.authorization = token
  } catch (error) {
    log.error(error)

    await store.dispatch('auth/logout')

    await router.push({
      name: 'login'
    })
  }

  return config
}, async function (error: AxiosError) {
  return Promise.reject(error)
})

And because sometimes my users get logged out randomly, I refresh the token manually on an interval (still experimenting with this):

let cognitoKeepaliveInterval: any = null

function startCognitoKeepaliveInterval() {
  if (cognitoKeepaliveInterval) clearInterval(cognitoKeepaliveInterval)

  cognitoKeepaliveInterval = setInterval(async () => {
    const session = await Auth.currentSession()
    const user = await Auth.currentAuthenticatedUser()

    user.refreshSession(session.getRefreshToken(), () => {
      log.info('refreshed auth session')
    })
  }, 30 * 60 * 1000) // 30 minutes
}