Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to refresh Cognito tokens #446

Closed
nnurmano opened this issue Mar 14, 2018 · 58 comments
Closed

How to refresh Cognito tokens #446

nnurmano opened this issue Mar 14, 2018 · 58 comments
Assignees
Labels
Cognito Related to cognito issues investigating This issue is being investigated question General question

Comments

@nnurmano
Copy link

There is a similar issue in #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
Copy link

david114 commented Mar 14, 2018

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
Copy link
Author

I would like to achieve the same with amplify library.

@nnurmano
Copy link
Author

nnurmano commented Mar 14, 2018

I had used
export function retrieveUserFromLocalStorage() { return new Promise((success, failure) => { // grab the cognitoUserobject 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 fromcognitoUsercognitoUser.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
Copy link

david114 commented Mar 14, 2018

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
Copy link
Author

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

@david114
Copy link

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

@retwedt
Copy link

retwedt commented Mar 14, 2018

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

@david114
Copy link

david114 commented Mar 16, 2018

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
Copy link

Legym commented Mar 16, 2018

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
Copy link
Author

nnurmano commented Mar 17, 2018

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
Copy link

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

@xdl
Copy link

xdl commented Mar 22, 2018

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
Copy link

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
Copy link

david114 commented Mar 22, 2018

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
Copy link

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

@powerful23 powerful23 added question General question Cognito Related to cognito issues labels Mar 27, 2018
@powerful23
Copy link
Contributor

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

@retwedt
Copy link

retwedt commented Mar 27, 2018

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
Copy link
Contributor

@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
Copy link

retwedt commented Mar 27, 2018

Great thanks!

@Jasminou
Copy link

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
Copy link
Contributor

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

@jamesoflol
Copy link

jamesoflol commented May 16, 2018

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
Copy link
Contributor

powerful23 commented May 16, 2018

@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
Copy link

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

@jamesoflol
Copy link

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

@ripundeep
Copy link

@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
Copy link

david114 commented Sep 2, 2019

@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
Copy link

@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
Copy link

david114 commented Sep 3, 2019

@ripundeep we were using plain client-sided javascript

@jeffsheets
Copy link

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
Copy link
Contributor

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

@jeffsheets
Copy link

@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
Copy link

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
}

@artuska
Copy link

artuska commented Jan 2, 2020

Well, i don't understand — if i console.log session token it remains the same:

let session = await Auth.currentSession();
let token = session.getIdToken().getJwtToken();
console.log(token); // this token is always the same...

So... what is the point of running the currentSession?

@ahallora
Copy link

ahallora commented Jan 4, 2020

@artuska , from the documentation: https://aws-amplify.github.io/docs/js/authentication#retrieve-current-session

This method will automatically refresh the accessToken and idToken if tokens are expired and a valid refreshToken presented. So you can use this method to refresh the session if needed.

@norbertdurcansk
Copy link

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

Thank you for your solution. We had the same issue with large uploads. We tried many solutions but none of them worked for us.

A good start is to check AWSS3Provider implementation:
https://github.com/aws-amplify/amplify-js/blob/a047ce73/packages/storage/src/Providers/AWSS3Provider.ts#L62

We created a custom Storage class according to AWSS3Provider but with authentication refresh.

import { AWS } from "@aws-amplify/core";
import { Auth } from "aws-amplify";

export default class Storage {
  private _refreshTimeout;
  // Token expiration is 60 min
  private readonly REFRESH_INTERVAL = 50 * 60 * 1000; // 50 min
  private readonly MIN_REFRESH_INTERVAL = 30 * 1000; // 30 sec
  private readonly COGNITO_IDP_URL;

  constructor({ region, userPoolId }) {
    this.COGNITO_IDP_URL = `cognito-idp.${region}.amazonaws.com/${userPoolId}`;
  }

  private _clean = () => {
    if (this._refreshTimeout) {
      clearTimeout(this._refreshTimeout);
      this._refreshTimeout = null;
    }
  };

  private _scheduleCredentialsRefresh = (interval = this.REFRESH_INTERVAL) => {
    this._refreshTimeout = setTimeout(async () => {
      const session = await Auth.currentSession();

      // @ts-ignore
      AWS.config.credentials.params.Logins[
        this.COGNITO_IDP_URL
      ] = session.getIdToken().getJwtToken();
      // @ts-ignore
      AWS.config.credentials.refresh(async error => {
        if (this._refreshTimeout) {
          this._scheduleCredentialsRefresh(
            error ? this.MIN_REFRESH_INTERVAL : this.REFRESH_INTERVAL
          );
        }
      });
    }, interval);
  };

  private _createS3 = ({ bucket, region, ...restParams }) => {
    // @ts-ignore
    return new AWS.S3({
      apiVersion: "2006-03-01",
      signatureVersion: "v4",
      params: { Bucket: bucket },
      region,
      ...restParams,
    });
  };

  private _initCredentials = async () => {
    try {
      const credentials = await Auth.currentUserCredentials();
      AWS.config.credentials = credentials;

      return credentials;
    } catch (e) {
      return null;
    }
  };

  private _getParams = ({ credentials, level = "public", key, object }) => {
    if (!credentials && level !== "public") {
      throw new Error("Missing credentials");
    }

    let prefix;
    const identityId = credentials.identityId;

    switch (level) {
      case "public":
        prefix = "public";
        break;
      case "protected":
        prefix = `protected/${identityId}`;
        break;
      case "private":
        prefix = `private/${identityId}`;
        break;
    }

    return {
      // @ts-ignore
      Key: `${prefix}/${key}`,
      Body: object,
      ContentType: "binary/octet-stream",
    };
  };

  public put = (
    key,
    object,
    { progressCallback, level, bucket, region, ...restParams }
  ) =>
    new Promise(async (resolve, reject) => {
      try {
        const credentials = await this._initCredentials();

        const s3 = this._createS3({ bucket, region, ...restParams });
        const params = this._getParams({ credentials, level, key, object });

        if (credentials) {
          this._scheduleCredentialsRefresh();
        }

        s3.upload(params)
          .on("httpUploadProgress", progressCallback)
          .promise()
          .then(data => {
            this._clean();
            resolve(data);
          })
          .catch(error => {
            this._clean();
            reject(error);
          });
      } catch (error) {
        this._clean();
        reject(error);
      }
    });
}

export const MyStorage = new Storage({
  region: "your-region",
  userPoolId: "your-user-pool-id",
});

U can use Storage in the same way like u use Amplify Storage

 MyStorage.put(`fileName`, file, {
        progressCallback: onProgress,
        level: "private",
        bucket: "bucket",
        region: "region",
      })
        .then(onSuccess)
        .catch(onFailure);

The token is refreshed every 50 min (REFRESH_INTERVAL). If the refresh fails for some reason we are still trying to refresh the token every 30 sec (MIN_REFRESH_INTERVAL).

@tigran10
Copy link

tigran10 commented Apr 4, 2020

@norbertdurcansk trying your custom storage class now as a shortcut, If it works, I will buy you a coffee after the pandemic :)

@carlos357
Copy link

carlos357 commented Apr 11, 2020

After some trial and error I have found that the answer from @jamesoflol is the most correct.
Here it is the code that worked for me, adapted to the Angular HttpClient:

/**
 * As the AWS Amplify SDK does not notify when the current access token expires
 * we have to check the current token on every HTTP call. This class
 * implements an HTTP request interceptor for the Angular HttpClient
 */
@Injectable()
export class AddAccessTokenToHttpHeader implements HttpInterceptor
{
    constructor(protected amplify_service: AmplifyService)
    {
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
    {
        // Amplify AuthClass.currentSession() checks the
        // access token and refreshes it if required.         
        return (from((this.amplify_service.auth() as AuthClass).currentSession()).pipe(
            switchMap(cognito_session =>
            {
                let auth_header: string = 'Bearer ' + cognito_session.getAccessToken().getJwtToken()
                let new_request: HttpRequest<any> = request.clone({
                    setHeaders: {
                        Authorization: auth_header
                    }
                });

                return (next.handle(new_request));
            })
        ));
    }

}```

@mauerbac mauerbac added investigating This issue is being investigated pending-close-response-required labels Apr 20, 2020
@stale
Copy link

stale bot commented Apr 28, 2020

This issue has been automatically closed because of inactivity. Please open a new issue if are still encountering problems.

@nullptrerror
Copy link

After some trial and error I have found that the answer from @jamesoflol is the most correct.
Here it is the code that worked for me, adapted to the Angular HttpClient:

/**
 * As the AWS Amplify SDK does not notify when the current access token expires
 * we have to check the current token on every HTTP call. This class
 * implements an HTTP request interceptor for the Angular HttpClient
 */
@Injectable()
export class AddAccessTokenToHttpHeader implements HttpInterceptor
{
    constructor(protected amplify_service: AmplifyService)
    {
    }

    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
    {
        // Amplify AuthClass.currentSession() checks the
        // access token and refreshes it if required.         
        return (from((this.amplify_service.auth() as AuthClass).currentSession()).pipe(
            switchMap(cognito_session =>
            {
                let auth_header: string = 'Bearer ' + cognito_session.getAccessToken().getJwtToken()
                let new_request: HttpRequest<any> = request.clone({
                    setHeaders: {
                        Authorization: auth_header
                    }
                });

                return (next.handle(new_request));
            })
        ));
    }

}```

How would you re-route the request on an unauthorized request?

@github-actions
Copy link

github-actions bot commented Sep 3, 2021

This issue has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs.

Looking for a help forum? We recommend joining the Amplify Community Discord server *-help channels or Discussions for those types of questions.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 3, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Cognito Related to cognito issues investigating This issue is being investigated question General question
Projects
None yet
Development

No branches or pull requests