Getting error NotAuthorizedException: Invalid login token. Token expired:
Before opening, please confirm:
- [x] I have searched for duplicate or closed issues and discussions.
- [x] I have read the guide for submitting bug reports.
- [x] I have done my best to include a minimal, self-contained set of instructions for consistently reproducing the issue.
JavaScript Framework
Angular
Amplify APIs
Authentication
Amplify Version
v6
Amplify Categories
auth, api
Backend
CDK
Environment information
System:
OS: macOS 14.4.1
CPU: (10) arm64 Apple M1 Max
Memory: 37.19 GB / 64.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.14.0 - /usr/local/bin/node
Yarn: 1.22.18 - ~/.npm-global/bin/yarn
npm: 8.19.1 - ~/.npm-global/bin/npm
pnpm: 9.0.0 - ~/.npm-global/bin/pnpm
Browsers:
Chrome: 135.0.7049.85
Safari: 17.4.1
npmPackages:
%name%: 0.1.0
@angular-devkit/build-angular: ^19.2.3 => 19.2.3
@angular/animations: ^19.2.2 => 19.2.2
@angular/cdk: ^19.2.3 => 19.2.3
@angular/cli: ^19.2.3 => 19.2.3
@angular/common: ^19.2.2 => 19.2.2
@angular/compiler: ^19.2.2 => 19.2.2
@angular/compiler-cli: ^19.2.2 => 19.2.2
@angular/core: ^19.2.2 => 19.2.2
@angular/elements: ^19.2.2 => 19.2.2
@angular/forms: ^19.2.2 => 19.2.2
@angular/material: ^19.2.3 => 19.2.3
@angular/material-moment-adapter: ^19.2.3 => 19.2.3
@angular/platform-browser: ^19.2.2 => 19.2.2
@angular/platform-browser-dynamic: ^19.2.2 => 19.2.2
@angular/router: ^19.2.2 => 19.2.2
@angular/youtube-player: ^19.2.3 => 19.2.3
@apollo/pdf: file:libs/pdf => 1.0.0
@apollo/shared: file:libs/shared => 1.0.1
@aws-appsync/utils: ^1.5.0 => 1.12.0
@aws-sdk/client-acm: ^3.568.0 => 3.758.0
@aws-sdk/client-appsync: ^3.568.0 => 3.770.0
@aws-sdk/client-cognito-identity: ^3.485.0 => 3.768.0
@aws-sdk/client-cognito-identity-provider: ^3.568.0 => 3.768.0
@aws-sdk/client-dynamodb: ^3.568.0 => 3.767.0
@aws-sdk/client-dynamodb-streams: ^3.563.0 => 3.758.0
@aws-sdk/client-kms: ^3.568.0 => 3.758.0
@aws-sdk/client-lambda: ^3.568.0 => 3.758.0
@aws-sdk/client-opensearch: ^3.563.0 => 3.758.0
@aws-sdk/client-osis: ^3.563.0 => 3.758.0
@aws-sdk/client-s3: ^3.568.0 => 3.758.0
@aws-sdk/client-secrets-manager: ^3.568.0 => 3.758.0
@aws-sdk/client-ses: ^3.568.0 => 3.758.0
@aws-sdk/client-sns: ^3.568.0 => 3.758.0
@aws-sdk/credential-provider-ini: ^3.568.0 => 3.758.0 (3.621.0)
@aws-sdk/s3-request-presigner: ^3.568.0 => 3.758.0
@aws-sdk/signature-v4: ^3.374.0 => 3.374.0
@aws-sdk/util-dynamodb: ^3.568.0 => 3.767.0
@cypress/angular: 0.0.0-development
@cypress/angular-signals: 0.0.0-development
@cypress/mount-utils: 0.0.0-development
@cypress/react: 0.0.0-development
@cypress/react18: 0.0.0-development
@cypress/svelte: 0.0.0-development
@cypress/vue: 0.0.0-development
@cypress/vue2: 0.0.0-development
@fontsource/poppins: ^5.0.8 => 5.2.5
@googleapis/sheets: ^5.0.5 => 5.0.5
@googleapis/youtube: ^12.0.0 => 12.0.0
@graphql-codegen/cli: ^2.16.4 => 2.16.5
@graphql-codegen/typed-document-node: ^2.3.12 => 2.3.13
@graphql-codegen/typescript: ^2.8.7 => 2.8.8
@graphql-codegen/typescript-operations: ^2.5.12 => 2.5.13
@graphql-tools/load-files: ^7.0.0 => 7.0.1
@graphql-tools/merge: ^9.0.0 => 9.0.24 (8.4.2)
@graphql-typed-document-node/core: ^3.1.1 => 3.2.0
@iplab/ngx-file-upload: ^ 19.0.3 => 19.0.3
@jest/globals: ^29.7.0 => 29.7.0
@slack/web-api: ^6.3.0 => 6.13.0
@swc/cli: ^0.1.62 => 0.1.65
@swc/core: ^1.3.68 => 1.11.11
@tailwindcss/postcss: ^4.0.8 => 4.0.14
@types/aws-lambda: ^8.10.82 => 8.10.147
@types/jasmine: ~5.1.0 => 5.1.7
@types/jest: ^29.5.12 => 29.5.14
@types/marked: ^4.0.8 => 4.3.2
@types/node: 18.0.6 => 18.0.6
@types/nodemailer: ^6.4.4 => 6.4.17
@types/oauth-signature: ^1.5.0 => 1.5.2
@types/pdf-parse: ^1.1.1 => 1.1.4
@types/pdfmake: ^0.2.2 => 0.2.11
@types/prettier: 2.6.0 => 2.6.0
@types/qrcode: ^1.5.5 => 1.5.5
@types/qs: ^6.9.7 => 6.9.18
@types/uuid: ^9.0.0 => 9.0.8
@types/vimeo: ^2.1.5 => 2.1.8
@types/vimeo__player: ^2.16.3 => 2.18.3
@types/youtube: ^0.0.47 => 0.0.47 (0.1.0)
@typescript-eslint/eslint-plugin: ^6.0.0 => 6.21.0
@typescript-eslint/parser: ^6.0.0 => 6.21.0
@vimeo/player: ^2.18.0 => 2.26.0
angular-google-tag-manager: ^1.11.0 => 1.11.0
angularx-qrcode: ^19.0.0 => 19.0.0
autoprefixer: ^10.4.16 => 10.4.21 (10.4.20)
aws-amplify: ^6.13.1 => 6.13.5
aws-amplify/adapter-core: undefined ()
aws-amplify/adapter-core/internals: undefined ()
aws-amplify/analytics: undefined ()
aws-amplify/analytics/kinesis: undefined ()
aws-amplify/analytics/kinesis-firehose: undefined ()
aws-amplify/analytics/personalize: undefined ()
aws-amplify/analytics/pinpoint: undefined ()
aws-amplify/api: undefined ()
aws-amplify/api/internals: undefined ()
aws-amplify/api/server: undefined ()
aws-amplify/auth: undefined ()
aws-amplify/auth/cognito: undefined ()
aws-amplify/auth/cognito/server: undefined ()
aws-amplify/auth/enable-oauth-listener: undefined ()
aws-amplify/auth/server: undefined ()
aws-amplify/data: undefined ()
aws-amplify/data/server: undefined ()
aws-amplify/datastore: undefined ()
aws-amplify/in-app-messaging: undefined ()
aws-amplify/in-app-messaging/pinpoint: undefined ()
aws-amplify/push-notifications: undefined ()
aws-amplify/push-notifications/pinpoint: undefined ()
aws-amplify/storage: undefined ()
aws-amplify/storage/s3: undefined ()
aws-amplify/storage/s3/server: undefined ()
aws-amplify/storage/server: undefined ()
aws-amplify/utils: undefined ()
aws-appsync: ^4.1.9 => 4.1.10
aws-cdk: ^2.173.1 => 2.1004.0
aws-cdk-lib: ^2.173.1 => 2.184.1
aws-sdk: ^2.1531.0 => 2.1692.0
axios: ^1.2.1 => 1.8.3
constructs: ^10.3.0 => 10.4.2
copyfiles: ^2.4.1 => 2.4.1
cross-fetch: ^3.1.5 => 3.2.0
cross-fetch-polyfill: 0.0.0
csvtojson: ^2.0.10 => 2.0.10
cypress: ^13.3.2 => 13.17.0
dayjs: ^1.11.10 => 1.11.13
dotenv: ^16.0.3 => 16.4.7
esbuild: ^0.18.11 => 0.18.20 (0.25.1)
eslint: ^8.57.1 => 8.57.1
google-auth-library: ^9.1.0 => 9.15.1
graphql: 14.7.0 => 14.7.0 (15.8.0)
graphql-tag: ^2.12.6 => 2.12.6
jasmine-core: ~5.1.0 => 5.1.2 (4.6.1)
jest: ^29.7.0 => 29.7.0
json-2-csv: ^3.18.0 => 3.20.0
jwt-simple: ^0.5.6 => 0.5.6
karma: ~6.4.0 => 6.4.4
karma-chrome-launcher: ~3.2.0 => 3.2.0
karma-coverage: ~2.2.0 => 2.2.1
karma-coverage-coffee-example: 1.0.0
karma-jasmine: ~5.1.0 => 5.1.0
karma-jasmine-html-reporter: ~2.1.0 => 2.1.0
marked: ^4.2.12 => 4.3.0
material-icons: ^1.13.12 => 1.13.14
material-symbols: ^0.28.2 => 0.28.2
mochawesome: ^7.1.3 => 7.1.3
mochawesome-merge: ^4.3.0 => 4.4.1
mochawesome-report-generator: ^6.2.0 => 6.2.0
node-device-detector: ^2.1.6 => 2.2.0
nodemailer: ^6.9.3 => 6.10.0
nosleep.js: ^0.12.0 => 0.12.0
nx: ^19.5.3 => 19.8.14
oauth-signature: ^1.5.0 => 1.5.0
pdf-parse: ^1.1.1 => 1.1.1
pdfmake: ^0.2.18 => 0.2.18
prettier: ^3.3.3 => 3.5.3
process: ^0.11.10 => 0.11.10
qs: ^6.11.0 => 6.14.0 (6.13.0)
rxjs: ~7.8.0 => 7.8.2 (7.8.1)
rxjs/ajax: undefined ()
rxjs/fetch: undefined ()
rxjs/operators: undefined ()
rxjs/testing: undefined ()
rxjs/webSocket: undefined ()
sharp: ^0.33.5 => 0.33.5
short-uuid: ^4.2.2 => 4.2.2
source-map-support: ^0.5.21 => 0.5.21 (0.5.13)
tailwindcss: 3.4.17 => 3.4.17 (4.0.14)
ts-jest: ^29.2.3 => 29.2.6
ts-node: ^10.9.1 => 10.9.2
tslib: ^2.3.0 => 2.8.1 (1.14.1, 2.4.1)
tsx: ^4.7.0 => 4.19.3
typescript: ^5.0.4 => 5.8.2
uuid: ^9.0.0 => 9.0.1 (8.3.2, 3.4.0, 8.0.0)
vimeo: ^2.3.1 => 2.3.1
web-animations-js: ^2.3.2 => 2.3.2
zen-observable-ts: 1.1.0 => 1.1.0 (0.8.21, 1.2.5)
zone.js: ~0.15.0 => 0.15.0
zxcvbn: ^4.4.2 => 4.4.2
npmGlobalPackages:
@angular/cli: 19.1.6
angular-http-server: 1.10.0
aws-cdk: 2.173.4
aws: 0.0.3-2
envinfo: 7.13.0
firebase-tools: 11.16.1
nativescript: 8.2.3
node-gyp: 8.4.1
npm: 8.19.1
pnpm: 9.0.0
sass-migrator: 2.3.1
yarn: 1.22.18
Describe the bug
On app refresh I am getting:
Uncaught (in promise) NotAuthorizedException: Invalid login token. Token expired: 1744757890 >= 1744728899
My idToken is expired, but my refreshToken is still valid (as far as I know).
The stack trace:
It is actually triggered by a cognito call trying to do Login,
which returns:
{"__type":"NotAuthorizedException","message":"Invalid login token. Token expired: 1744757890 >= 1744728899"}
I am not sure how to reproduce this, what happened is: I am logged in, my macbook ran out of battery overnight, I charged and turned it on and went back to the session of my app which now shows this error.
I also noticed a similar token error running virtual machine in Android Studio, and running my Angular app in Capacitor and after closing down the VM and start it back up, which keeps the state, it was not able to login or recover from the expired session.
Is there a bug somewhere when a token is expired?
Expected behavior
Request a new idtoken via refreshtoken if idToken is expired
Reproduction steps
I am not sure how to reproduce this, what happened is: I am logged in, my macbook ran out of battery overnight, I charged and turned it on and went back to the session of my app which now shows this error.
I also noticed a similar token error running virtual machine in Android Studio, and running my Angular app in Capacitor and after closing down the VM and start it back up, which keeps the state, it was not able to login or recover from the expired session.
I am adding some screenshots of the stack trace source code:
Code Snippet
// Put your code below this line.
Log output
// Put your logs below this line
aws-exports.js
No response
Manual configuration
No response
Additional configuration
No response
Mobile Device
No response
Mobile Operating System
No response
Mobile Browser
No response
Mobile Browser Version
No response
Additional information and screenshots
No response
Hi @mattiLeBlanc fetchAuthSession() should refresh expired tokens automatically, and then use the refreshed idToken to retrieve AWS credentials, this process is being verified by integration tests. I'm wondering if some external factors was disturbing the process.
Do you have the network requests records when this error occurred? I'd like to inspect whether the corresponding service calls were corrected made.
I dont have them at the moment. I think it recovered after numerous refreshes, because I was persistent. But it kind a was a dead end, and since it is running on a TV (android Tv using Capacitor) the only way out is kill the app.
If I see the issue again, I will try to record the Network error.
I remember it was just trying to do the first cognito call with payload Logins: { cognito-idp.ap...... : '323444234 }.
That one failed
Thanks for the additional details @mattiLeBlanc you mentioned
I remember it was just trying to do the first cognito call with payload Logins: { cognito-idp.ap...... : '323444234 }. That one failed
This actual matches the error shown in the screenshot, it's the step when fetch the AWS credentials using id token, but the id token expired.
fetchAuthSession() does automatically (when tokens are expired) and proactively (5 seconds prior to expiration) refresh tokens, and when tokens are refreshed, it also fetch new AWS credentials using refreshed id token.
Looking at the error returned by the service, however, it looks like that when it was fetching the AWS credentials, the id token is still active, but when the request reached to the service, the id token became expired. This sounds like a clock skew issue.
Amplify library does record the clock skew if there is a time gap between the client and service which mitigates this. Could you check whether the tokens store has an item with name that ends with .clockDrift?
In addition, since you mentioned that your app is running on the TV, I'm not sure if there is any system behavior that delays sending out the network request from your app, such as an app suspense or something similar?
Hi @mattiLeBlanc, following up to check if you got a change to look at the comment above
@ashwinkumar6 sorry for not getting back to you. I can confirm he there is a .clockDrift available in the sessions I see on tablet, laptop and tv. In regards to the CapacitorJS wrapped Angular app installed on a Chromecast; it the Chromecast could be turned off for a while but still has a valid refreshToken. I havent seen it there but I did see it on my Macbook as mentioned in the ticket. Is there a way the Auth code can recover from that specific error, like even IF the token is expired, but the refreshToken is still valid, I wouldnt expect that error to be displayed. I would expect it would just get a new token. But I am not aware of all the logic checks being done before issuing a new AccessToken.
@mattiLeBlanc our suggestion would be to retry the API call again if you hit Invalid login Token error. What you are seeing might be happening because of the OS involved and the calls are delayed causing this issue. And Amplify does not have any control over it. So retrying based on the specific error might be the best way to go about it. And when fetchAuthSession if the refreshToken is valid it will use that to get tokens again or if it got expired, it will fetch new refreshToken and accessTokens.
@ashika112 I set the refresh token to expiry in 90 years or so, so it should always attempt to get a new accesstoken, right? Problem is, when this happens the end user is just looking at white screen and cant do anything. Maybe I can catch the error and attempt a refresh or some other way to repair the situation.
@mattiLeBlanc do you have network records like hui has asked? it is difficult to say where exactly this is all happening without having more info. Since the API works as expected at our end and we dont have control over how the OS makes the request calls with delay, I would suggest you catch the error and make a retry. Let us know once you try that and if there are further issues here.
Any updates here?
@ashika112 I do not have any records. I only have the screenshots I posted above, I am sorry.
@mattiLeBlanc Got it. We are going to close this issue since there is no way for us to re-produce the exact issue and there is a workaround suggested. If there is anything else outside of this, feel free to open new issue. If you have further issue after retry and can provide with some replication here, we will re-open this issue and work it through.
@ashika112 I have a never version of this issue:
I am getting the error:
Graphql Error running query onResidentUpdate. Authmode Cognito. Error: Connection failed: UnauthorizedException Error: Graphql Error running query onResidentUpdate. Authmode Cognito. Error: Connection failed: UnauthorizedException
which is a Grapqlh subscription.
In my angular App I already run this in my main.ts:
Amplify.configure(awsConfig.clientApp, {
API: {
GraphQL: {
headers: async () => {
try {
let currentSession = await fetchAuthSession();
const tokenExpiry = currentSession.tokens.accessToken.payload.exp * 1000;
const HALF_MINUTE = 30000;
const now = new Date().getTime() + HALF_MINUTE;
if (tokenExpiry <= now) {
currentSession = await fetchAuthSession({forceRefresh: true});
console.log('============================= fetched a new token===================');
}
if (currentSession.tokens) {
const idToken = currentSession.tokens.idToken?.toString();
return { Authorization: idToken };
} else {
signOut();
return undefined
}
} catch (error) {
// signOut();
return undefined;
}
}
}
}
});
which should force refresh the token if is within 30 sec of expiry. I see this solution work well in my local env with my development service, the console log "fetched new token" is triggered often during development refreshes (ng serve) and no issues.
But on our production environment, we have live customers grabbing their tables out of storage, turning it on and expecting to be still logged into the app.
The error that was reported was accompanied by the IdToken which expired 40min before the error happened, AND the report also included a valid refreshToken. So the amplify client SHOULD be able to reissue a new access and id token, right? Still, it didnt.
Now, I am querying ChatGPT about this too and it made an interesting observation:
---------begin chatgpt-------------
✅ 1. fetchAuthSession returned a fresh token, but API.graphql still used an old one Why?
The headers() function is async, but Amplify may cache or ignore it during certain API.graphql flows — especially subscriptions
In @aws-amplify/api-graphql, subscription initialisation may capture the auth headers once and not re-evaluate them, even if expired
Evidence: The error occurred during a GraphQL subscription setup (onResidentUpdate), which uses a WebSocket connection — not regular HTTP.
🔥 WebSocket-based GraphQL subscriptions in Amplify use a separate authorization handshake — and may not honour late changes to the headers() function.
✅ 2. Amplify's internal session state was stale at the WebSocket layer Even though fetchAuthSession({ forceRefresh: true }) works correctly, the WebSocket connection may have:
Been created earlier
Used an expired token at the time of connection
Not reconnected correctly when forceRefresh occurred
------------end chatgpt-----------
Are websockets treated differently and should have add an check in my subscription call to see if the token has expired and force refresh it within my RXJS Pipe?
Full API service code:
export class ApiService {
private client = generateClient();
subscription<T>({ statement: query, variables, type }: subscriptionIput): Observable<T> {
const payload: any = {
query,
authMode: 'userPool',
}
if (variables) {
payload.variables = variables;
}
return from(this.client.graphql(payload) as Promise<GraphQLResult<object>>)
.pipe(
map(res => res.data[type as keyof object]),
catchError((error: GraphQLResult<object> | Error) => {
// graphql error
if ('errors' in error) {
const gqError = error.errors![0];
throw new Error(`Graphql Error running query ${type}. Authmode Cognito. Error: ${gqError?.message} ${gqError?.path ? gqError.path.toString() : ''}`);
// normal error
} else if ('stack' in error) {
throw new Error(`Runtime error running query ${type}. Authmode Cognito. Error: ${error.stack}}`);
// fallback for something else
} else {
throw new Error(`Unknown Error running query ${type}. Authmode Cognito. Error: ${error}}`);
}
}),
)
}
graphql<V>({ statement: query, variables, type, iam = false, familyApi = false }: queryInput) {
if (familyApi) {
Amplify.configure(awsConfig.familyApp);
} else {
Amplify.configure(awsConfig.clientApp);
}
const payload: any = {
query,
authMode: iam ? 'iam' : 'userPool',
}
if (variables) {
payload.variables = variables;
}
// return from(fetchAuthSession())
// .pipe(
// switchMap(() => {
return from(this.client.graphql(payload) as Promise<GraphQLResult<object>>)
.pipe(
map(res => res.data[type as keyof object]),
catchError((error: GraphQLResult<object> | Error) => {
// graphql error
if ('errors' in error) {
const gqError = error.errors![0];
throw new Error(`Graphql Error running query ${type}. Authmode ${iam ? 'IAM' : 'Cognito'}. Error: ${gqError?.message} ${gqError?.path ? gqError.path.toString() : ''}`);
// normal error
} else if ('stack' in error) {
throw new Error(`Runtime error running query ${type}. Authmode ${iam ? 'IAM' : 'Cognito'}. Error: ${error.stack}}`);
// fallback for something else
} else {
throw new Error(`Unknown Error running query ${type}. Authmode ${iam ? 'IAM' : 'Cognito'}. Error: ${error}}`);
}
}),
) as Observable<V>
// })
// )
}
}
These errors are quite problematic and I can't seem to reproduce them myself, but they happen a couple of times a day with multiple customers and I am afraid it is giving us a bad reputation. Any suggestions?
I am trying a new solution with a defer and a retry which will call fetchAuthSession multiple times with exponential backoff.
However, is their an internal problem with Subsrcription token renewal since the token is used during the initial connect and if browser goes to sleep and wakes up again, it may not reconnect with a new fresh token?
@mattiLeBlanc we have verified in web app this already. And we dont see an issue in that part at all.
@ashika112 Well it is happening daily with multiple users that use Android 10 tablets. I am recording the errors triggered in their UI. The subscription call was triggering these unauthorised errors on a daily basis whilst having a valid refreshtoken and an expired accessToken. I can't reproduce it on my side which is frustrating so I have no clue how they can trigger this error in the subscription call that is setup on page load.
However, I have to see how well the retry mechanism will work.
It would be nice if you can streamline the error message though:
From getMusicStream:
From graphlq query I get Error: UnauthorizedError: GraphQL Error ... Error: Unauthorized From graohql subscribtion I get Error: Connection failed: UnauthorizedException
So you use UnauthorizedError abd and UnauthorizedException. This could be the same error perhaps?
Re-opening this to track the conversation. Will take a look at this soon and update.
Hi @mattiLeBlanc
- Could you provide the entire source code to understand how you're performing the retry and also understand the graphQL implementations
- Also you mentioned users in Android 10 facing this issue, is there a possibility it's specific to this particular subset of users
Hi @mattiLeBlanc , could you provide the information requested by @ashwinkumar6 to help us continue the investigation?
Hi @mattiLeBlanc, I will close this ticket due to inactivity. Please, feel free to reopen the ticket if you are still facing any issues.
@ashwinkumar6 Hi sorry for not replying, the message got of my radar. the retry mechanism is as follows:
Firstly I have this graphql function in my api.service
graphqlWithRetry<T>({ statement: query, variables, type, iam = false, familyApi = false }: queryInput) {
const authMode = iam ? 'iam' : 'userPool';
const client = familyApi
? (this.familyClient ??= generateClient<never, any>({
endpoint: awsConfig.familyApp.API.GraphQL.endpoint,
authMode
}))
: this.client;
const payload: any = {
query,
authMode
}
if (variables) {
payload.variables = variables;
}
return defer(() => {
return from(client.graphql(payload) as Promise<GraphQLResult<object>>).pipe(
map(res => res.data[type as keyof object] as T),
withGraphqlErrors<T>(type, authMode),
withAuthRetry<T>(),
forwardErrorToGlobalHandler<T>()
);
});
}
and these are the helper functions:
import { Observable, throwError, timer, from, OperatorFunction } from 'rxjs';
import { catchError, retry, switchMap } from 'rxjs/operators';
import { fetchAuthSession } from 'aws-amplify/auth';
export class AuthRetryError extends Error {
constructor(public original: any) {
super('UnauthorizedException');
this.name = 'AuthRetryError';
}
}
function isUnauthorizedError(error: any): boolean {
const errorString = JSON.stringify(error).toLowerCase();
return (
errorString.includes('unauthorizedexception') ||
errorString.includes('unauthorizederror') ||
errorString.includes('"message":"unauthorized"') ||
error.message?.toLowerCase().includes('unauthorized') ||
errorString.includes('connection failed')
);
}
/**
* Operator that retries on Cognito Unauthorized errors using RxJS 8+ `retry` syntax.
* Generated by ChatGPT at 9/5/2025 based on my instructions.
*/
export function withAuthRetry<T>(maxRetries = 3, baseDelayMs = 300): OperatorFunction<T, T> {
return source$ => source$.pipe(
catchError((error: any) => {
return isUnauthorizedError(error)
? throwError(() => new AuthRetryError(error))
: throwError(() => error);
}),
retry({
count: maxRetries,
delay: (error, retryCount) => {
if (!(error instanceof AuthRetryError)) throw error;
const delayMs = 2 ** retryCount * baseDelayMs;
console.warn(`Retrying due to UnauthorizedException, delay ${delayMs}ms`);
return from(fetchAuthSession({ forceRefresh: true })).pipe(
// Wait for token refresh, then delay
switchMap(() => timer(delayMs))
);
}
})
);
}
export function forwardErrorToGlobalHandler<T>(): OperatorFunction<T, T> {
return catchError((err: any): Observable<T> => {
setTimeout(() => { throw err; });
return throwError(() => err) as Observable<T>;
});
}
export function withGraphqlErrors<T>(type: string, authmode: string): OperatorFunction<T, T> {
return (source$: Observable<T>): Observable<T> => {
return source$.pipe(
catchError((error: any) => {
if (error instanceof AuthRetryError) {
return throwError(() => error); // Let retry handle it
}
// Format all other errors
const gqlError = error?.errors?.[0];
const message = gqlError
? `GraphQL Error in "${type}". Authmode: ${authmode}. Error: ${gqlError.message}`
: error?.stack
? `Runtime Error in "${type}". Authmode: ${authmode}. Error: ${error.stack}`
: `Unknown Error in "${type}". Authmode: ${authmode}. Error: ${JSON.stringify(error)}`;
return throwError(() => new Error(message));
}),
);
};
}
It is hard to validate how well it works, since I cant get my api to simulate a session issue. ChatGPT helped with creating the retry setup, but I have seen that AI make errors and we have been through a couple of iterations.
Besides that, i swap out the accesstoken for the idtoken and do an expiry check there too (legacy solution but I haven't removed it yet)
Amplify.configure(awsConfig.clientApp, {
API: {
GraphQL: {
headers: async () => {
try {
let currentSession = await fetchAuthSession();
const tokenExpiry = currentSession.tokens.accessToken.payload.exp * 1000;
const HALF_MINUTE = 30000;
const now = new Date().getTime() + HALF_MINUTE;
if (tokenExpiry <= now) {
currentSession = await fetchAuthSession({forceRefresh: true});
console.log('============================= fetched a new token===================');
}
if (currentSession.tokens) {
const idToken = currentSession.tokens.idToken?.toString();
return { Authorization: idToken };
} else {
signOut();
return undefined
}
} catch (error) {
// signOut();
return undefined;
}
}
}
}
});