aws / aws-sdk-js-v3

Modularized AWS SDK for JavaScript.
Apache License 2.0
3.05k stars 573 forks source link

`fromWebToken` Unable to handle ID token expiration / unsuitable for long lived clients #5270

Closed tgoodsell-tempus closed 5 months ago

tgoodsell-tempus commented 12 months ago

Checkboxes for prior research

Describe the bug

Based on my experimentation and general lack of information on this in the docs:

The fromWebToken method in the credential-providers package is unable to deal with the eventual expiration of an ID token. Since the token value is passed as a string instead of a promise/function (or something else), the value is statically encoded into the configuration and is not detected or able to handle refreshing.

Perhaps this function should be explicitly updated to take one of the aforementioned types, which would allow the user to fulfill with whatever logic / source they need to ensure a valid ID token is returned on each call.

Docs: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-credential-providers/#fromwebtoken

SDK version number

@aws-sdk/credential-providers@3.315.0

Which JavaScript Runtime is this issue in?

Node.js

Details of the browser/Node.js/ReactNative version

Node 20.6.1

Reproduction Steps

async function main() {
  const stsClient = new STSClient({credentials: fromWebToken({
    roleArn: "someARN",
    webIdentityToken: await myIdTokenGetter(),
    roleSessionName: "sessionName",
    durationSeconds: 900,
  }), region: 'us-east-1'});

  const testCmd = new GetCallerIdentityCommand({});
  const resp = await stsClient.send(testCmd)

  setInterval(() => {
    stsClient.send(testCmd).then(data => {
      console.log(data);
    }).catch(err => {
      console.error(err);
    });
  }, 900000)
}

Observed Behavior

Basically we get errors like the following once the ID token expires:

"An auth error occured! ExpiredTokenException: Token expired: current date/time 1695851903 must be before the expiration date/time1695844782 ExpiredTokenException: Token expired: current date/time 1695851903 must be before the expiration date/time1695844782\n    at de_ExpiredTokenExceptionRes (/app/node_modules/@aws-sdk/client-sts/dist-cjs/protocols/Aws_query.js:400:23)\n    at de_AssumeRoleWithWebIdentityCommandError (/app/node_modules/@aws-sdk/client-sts/dist-cjs/protocols/Aws_query.js:210:25)\n    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n    at async /app/node_modules/@smithy/middleware-serde/dist-cjs/deserializerMiddleware.js:7:24\n    at async /app/node_modules/@smithy/middleware-retry/dist-cjs/retryMiddleware.js:27:46\n    at async /app/node_modules/@aws-sdk/middleware-logger/dist-cjs/loggerMiddleware.js:7:26\n    at async /app/node_modules/@aws-sdk/client-sts/dist-cjs/defaultStsRoleAssumers.js:58:33\n    at async coalesceProvider (/app/node_modules/@smithy/property-provider/dist-cjs/memoize.js:14:24)\n    at async SignatureV4.credentialProvider (/app/node_modules/@smithy/property-provider/dist-cjs/memoize.js:43:13)\n    at async SignatureV4.signRequest (/app/node_modules/@smithy/signature-v4/dist-cjs/SignatureV4.js:106:29)"

Expected Behavior

Generally I would expect the interface which we provide our ID token to allow for some need for that value to change due to expiration or other reasons. I suppose we had expected that when feeding the function value instead of a string const or something it would take care of that, however that goes against the primitive pass by value conventions so that would be wrong to expect in reality.

Possible Solution

Instead of using a primitive string input, something like a promise, function, generator, etc would be more useful for this. Something that the middleware would know to go call and fetch/retrieve a real token value from before it performs the AWS token refresh cycle. Outside of that, the logic on handling the ID token should probably still remain in the hands of the developer.

Additional Information/Context

No response

yenfryherrerafeliz commented 11 months ago

Hi @tgoodsell-tempus, thanks for opening this issue. The credential provider fromWebToken is not intended to handle expiration, since it expects to always be provided with a valid token. Something you can do here is to add a custom credential provider where you handle when a token is expired and supply that custom implementation as the credential provider for the SDK client you will use. Here is a simple example:

import {GetCallerIdentityCommand, STSClient} from "@aws-sdk/client-sts";
import {fromWebToken} from "@aws-sdk/credential-providers";

async function main() {
    const isExpired = (token) => {
        // Check if token is expired
        return true;
    };
    const myIdTokenGetter = async () => {
        // Generate a new fresh token
        return ''
    };
    let currentToken;
    const stsClient = new STSClient({
        credentials: async () => {
            if (!currentToken || isExpired(currentToken)) {
                currentToken = await myIdTokenGetter();
            }

            return await fromWebToken({
                roleArn: "someARN",
                webIdentityToken: currentToken,
                roleSessionName: "sessionName",
                durationSeconds: 900,
            })();
        },
        region: 'us-east-1'
    });
    const testCmd = new GetCallerIdentityCommand({});
    const resp = await stsClient.send(testCmd);

    console.log(resp);
    setInterval(() => {
        stsClient.send(testCmd).then(data => {
            console.log(data);
        }).catch(err => {
            console.error(err);
        });
    }, 900000)
}

await main();

Please let me know if that helps!

Thanks!

tgoodsell-tempus commented 11 months ago

Hi @yenfryherrerafeliz

Your workaround idea has been working as expected for our initial testing, when swapping in our own ID token getting code, so I appreciate your guidance there!

However, I'm not fully satisfied with this outcome:

  1. The fromWebToken provider itself not being setup to handle the expiration of the input token, and my interpretation you (or the managing group for this as a whole) believe that to be "correct" is difficult to understand. As the AssumeRoleWithWebIdentity is entirely based around the use of OAuth 2.0 Access Tokens or OIDC Identity Tokens, both of which will have some sort of expiration as a best practice (and really a practical security requirement), that choice goes against the fundamentals of this sort of mechanism. The workaround requiring the entire fromWebToken block instead of only the token field (since that's really the only item which changes in a "normal" circumstance) also seems really unintuitive.
  2. Considering the above, and the fact that I can't find any references on needing to do the wrapping of the provider object in the anonymous function and sticking that in the credentials field, minimally this should be dealt with by including the need to do this in the SDK documentation and calling out the behavior of the underlying fromWebToken provider.
github-actions[bot] commented 5 months ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs and link to relevant comments in this thread.