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.39k stars 2.11k 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?

artuska commented 4 years ago

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 commented 4 years ago

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

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 commented 4 years ago

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

carlos357 commented 4 years ago

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));
            })
        ));
    }

}```
stale[bot] commented 4 years ago

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

nullptrerror commented 3 years ago

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[bot] commented 2 years ago

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.