Skip to content

Commit

Permalink
fix: Enhance the "unauthorized" error message with recovery instructions
Browse files Browse the repository at this point in the history
  • Loading branch information
stocaaro committed Nov 29, 2023
1 parent 8ca7c15 commit a9f11b9
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 16 deletions.
118 changes: 118 additions & 0 deletions packages/api-graphql/__tests__/GraphQLAPI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,75 @@ describe('API test', () => {
expect(thread).toEqual(graphqlResponse.data.getThread);
});

test('auth-error-case', async () => {
expect.assertions(1);
Amplify.configure({
API: {
GraphQL: {
defaultAuthMode: 'apiKey',
apiKey: 'FAKE-KEY',
endpoint: 'https://localhost/graphql',
region: 'local-host-h4x',
},
},
});

const threadToGet = {
id: 'some-id',
topic: 'something reasonably interesting',
};

const graphqlVariables = { id: 'some-id' };

const err = {
name: 'GraphQLError',
message: 'Unknown error',
originalError: {
name: 'UnauthorizedException',
underlyingError: {
name: 'UnauthorizedException',
$metadata: {
httpStatusCode: 401,
requestId: '12345abcde-test-error-id',
},
},
$metadata: {
httpStatusCode: 401,
requestId: '12345abcde-test-error-id',
},
},
};

const spy = jest
.spyOn((raw.GraphQLAPI as any)._api, 'post')
.mockImplementation(() => {
return {
body: {
json: () => ({
errors: [err],
}),
},
};
});

try {
const result: GraphQLResult<GetThreadQuery> = await client.graphql({
query: typedQueries.getThread,
variables: graphqlVariables,
authMode: 'apiKey',
});
} catch (e: any) {
const errors = e.errors;
expect(errors).toEqual([
expect.objectContaining({
message: expect.stringContaining(
`UnauthorizedError: If you're calling an Amplify-generated API, make sure to set the "authMode" in generateClient`
),
}),
]);
}
});

test('cancel-graphql-query', async () => {
Amplify.configure({
API: {
Expand Down Expand Up @@ -942,6 +1011,55 @@ describe('API test', () => {
expect(observable).not.toBe(undefined);
});

test('subscription auth permissions error', done => {
expect.assertions(3);

const spyon_appsync_realtime = jest
.spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe')
.mockImplementation(
jest.fn(() => {
return new Observable(observer => {
observer.error({
errors: [
{
message:
'Connection failed: {"errors":[{"errorType":"UnauthorizedException","message":"Permission denied"}]}',
},
],
});
});
})
);

const query = `subscription SubscribeToEventComments($eventId: String!) {
subscribeToEventComments(eventId: $eventId) {
eventId
commentId
content
}
}`;

const variables = { id: '809392da-ec91-4ef0-b219-5238a8f942b2' };

const observable = (
client.graphql({ query, variables }) as unknown as Observable<object>
).subscribe({
error: e => {
expect(spyon_appsync_realtime).toHaveBeenCalledTimes(1);
expect(e.errors).toEqual([
expect.objectContaining({
message: expect.stringContaining(
`UnauthorizedError: If you're calling an Amplify-generated API, make sure to set the "authMode" in generateClient`
),
}),
]);
done();
},
});

expect(observable).not.toBe(undefined);
});

test('happy case subscription with additionalHeaders', done => {
const spyon_appsync_realtime = jest
.spyOn(AWSAppSyncRealTimeProvider.prototype, 'subscribe')
Expand Down
42 changes: 26 additions & 16 deletions packages/api-graphql/src/internals/InternalGraphQLAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
GraphQLError,
OperationTypeNode,
} from 'graphql';
import { Observable } from 'rxjs';
import { Observable, catchError } from 'rxjs';
import { AmplifyClassV6, ConsoleLogger } from '@aws-amplify/core';
import {
GraphQLAuthMode,
Expand All @@ -31,6 +31,7 @@ import {
import { AWSAppSyncRealTimeProvider } from '../Providers/AWSAppSyncRealTimeProvider';
import { CustomHeaders } from '@aws-amplify/data-schema-types';
import { resolveConfig, resolveLibraryOptions } from '../utils';
import { repackageAuthError } from '../utils/errors/repackageAuthError';

const USER_AGENT_HEADER = 'x-amz-user-agent';

Expand Down Expand Up @@ -236,7 +237,7 @@ export class InternalGraphQLAPIClass {
endpoint: appSyncGraphqlEndpoint,
customEndpoint,
customEndpointRegion,
defaultAuthMode
defaultAuthMode,
} = resolveConfig(amplify);

const authMode = explicitAuthMode || defaultAuthMode || 'iam';
Expand Down Expand Up @@ -391,7 +392,7 @@ export class InternalGraphQLAPIClass {
const { errors } = response;

if (errors && errors.length) {
throw response;
throw repackageAuthError(response);
}

return response;
Expand Down Expand Up @@ -424,19 +425,28 @@ export class InternalGraphQLAPIClass {
): Observable<any> {
const config = resolveConfig(amplify);

return this.appSyncRealTime.subscribe(
{
query: print(query as DocumentNode),
variables,
appSyncGraphqlEndpoint: config?.endpoint,
region: config?.region,
authenticationType: authMode || config?.defaultAuthMode,
apiKey: config?.apiKey,
additionalHeaders,
authToken,
},
customUserAgentDetails
);
return this.appSyncRealTime
.subscribe(
{
query: print(query as DocumentNode),
variables,
appSyncGraphqlEndpoint: config?.endpoint,
region: config?.region,
authenticationType: authMode || config?.defaultAuthMode,
apiKey: config?.apiKey,
additionalHeaders,
authToken,
},
customUserAgentDetails
)
.pipe(
catchError(e => {
if (e.errors) {
throw repackageAuthError(e);
}
throw e;
})
);
}
}

Expand Down
35 changes: 35 additions & 0 deletions packages/api-graphql/src/utils/errors/repackageAuthError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { GraphQLError } from 'graphql';
import { GraphQLResult } from '../../types';

type ErrorObject = {
errors: GraphQLError[];
};

export function repackageAuthError<T extends ErrorObject>(content: T): T {
if (content.errors && Array.isArray(content.errors)) {
content.errors.forEach(e => {
if (isAuthError(e)) {
e.message =
`UnauthorizedError: If you're calling an Amplify-generated API, make sure ` +
`to set the "authMode" in generateClient({ authMode: '...' }) to the backend authorization ` +
`rule's auth provider ('apiKey', 'userPool', 'iam', 'oidc', 'lambda')`;
}
});
}
return content;
}

function isAuthError(error: any): boolean {
// Error pattern corresponding to appsync calls
if (error?.['originalError']?.['name']?.startsWith('UnauthorizedException')) {
return true;
}
// Error pattern corresponding to appsync subscriptions
if (
error.message?.startsWith('Connection failed:') &&
error.message?.includes('Permission denied')
) {
return true;
}
return false;
}

0 comments on commit a9f11b9

Please sign in to comment.