Skip to content

Commit

Permalink
fix: resolve issues with graphiql explorer (#13691)
Browse files Browse the repository at this point in the history
* fix: resolve issues with graphiql explorer

fix: implement a new jwt library (jose) for signing and validating jwts

fix: remove unsupported jsonwebtoken

fix: graphiql explorer implementation

chore: bump package versions

* fix: remove unnecessary upgrades

* chore: extract api

* chore: refresh lockfile

* chore: remove node-pty dep

* chore: undo api extract format changes

---------

Co-authored-by: Ali Fuat Numanoglu <12843244+afnx@users.noreply.github.com>
  • Loading branch information
dpilch and afnx committed Apr 3, 2024
1 parent dcafcb2 commit 4d0677d
Show file tree
Hide file tree
Showing 19 changed files with 218 additions and 761 deletions.
2 changes: 2 additions & 0 deletions packages/amplify-appsync-simulator/API.md
Expand Up @@ -89,6 +89,8 @@ export class AmplifyAppSyncSimulator {
// (undocumented)
init(config: AmplifyAppSyncSimulatorConfig): void;
// (undocumented)
get localhostUrl(): string;
// (undocumented)
get pubsub(): PubSub;
// (undocumented)
reload(config: AmplifyAppSyncSimulatorConfig): void;
Expand Down
2 changes: 1 addition & 1 deletion packages/amplify-appsync-simulator/package.json
Expand Up @@ -64,7 +64,7 @@
"@types/express": "^4.17.3",
"@types/node": "^12.12.6",
"@types/ws": "^8.2.2",
"jsonwebtoken": "^9.0.0"
"jose": "^5.2.0"
},
"packageExtensions": {
"graphql-iso-date": {
Expand Down
Expand Up @@ -5,7 +5,7 @@
import * as http from 'http';
import type { GraphQLError } from 'graphql';
import { AmplifyAppSyncSimulator, AmplifyAppSyncSimulatorAuthenticationType } from '../../';
import jwt from 'jsonwebtoken';
import { SignJWT } from 'jose';

/**
* Minimal gql tag just for syntax highlighting and Prettier while writing client GraphQL queries
Expand Down Expand Up @@ -54,14 +54,14 @@ export async function appSyncClient<ResponseDataType = unknown, VarsType = Recor
break;

case AmplifyAppSyncSimulatorAuthenticationType.AMAZON_COGNITO_USER_POOLS:
headers.Authorization = jwt.sign(
{
username: auth.username,
'cognito:groups': auth.groups ?? [],
},
'mockSecret',
{ issuer: `https://cognito-idp.mock-region.amazonaws.com/mockUserPool` },
);
headers.Authorization = await new SignJWT({
username: auth.username,
'cognito:groups': auth.groups ?? [],
})
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setIssuer('https://cognito-idp.mock-region.amazonaws.com/mockUserPool')
.sign(new TextEncoder().encode('mockSecret'));
break;

case AmplifyAppSyncSimulatorAuthenticationType.AWS_IAM:
Expand Down
3 changes: 3 additions & 0 deletions packages/amplify-appsync-simulator/src/index.ts
Expand Up @@ -181,6 +181,9 @@ export class AmplifyAppSyncSimulator {
get url(): string {
return this._server.url.graphql;
}
get localhostUrl(): string {
return this._server.localhostUrl.graphql;
}
get config(): AmplifyAppSyncSimulatorConfig {
return this._config;
}
Expand Down
7 changes: 7 additions & 0 deletions packages/amplify-appsync-simulator/src/server/index.ts
Expand Up @@ -16,6 +16,7 @@ export class AppSyncSimulatorServer {
private _httpServer: Server;
private _realTimeSubscriptionServer: AppSyncSimulatorSubscriptionServer;
private _url: string;
private _localhostUrl: string;

constructor(private config: AppSyncSimulatorServerConfig, private simulatorContext: AmplifyAppSyncSimulator) {
this._operationServer = new OperationServer(config, simulatorContext);
Expand Down Expand Up @@ -49,6 +50,7 @@ export class AppSyncSimulatorServer {
this._httpServer.listen(port);
await fromEvent(this._httpServer, 'listening').then(() => {
this._url = `http://${getLocalIpAddress()}:${port}`;
this._localhostUrl = `http://localhost:${port}`;
});
}

Expand All @@ -61,4 +63,9 @@ export class AppSyncSimulatorServer {
graphql: this._url,
};
}
get localhostUrl() {
return {
graphql: this._localhostUrl,
};
}
}
5 changes: 3 additions & 2 deletions packages/amplify-graphiql-explorer/package.json
Expand Up @@ -29,15 +29,16 @@
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"graphiql": "^1.5.16",
"graphiql": ">=1.5.16 <=1.8.10",
"graphiql-explorer": "^0.6.2",
"graphql": "^15.5.0",
"graphql-ws": "^5.14.3",
"html-webpack-plugin": "^5.5.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.0.0",
"jest-resolve": "^26.0.2",
"jest-watch-typeahead": "^1.0.0",
"jsonwebtoken": "^9.0.0",
"jose": "^5.2.0",
"mini-css-extract-plugin": "^2.4.5",
"postcss": "^8.4.31",
"postcss-flexbugs-fixes": "^5.0.2",
Expand Down
65 changes: 45 additions & 20 deletions packages/amplify-graphiql-explorer/src/App.tsx
Expand Up @@ -2,7 +2,7 @@ import GraphiQL from 'graphiql';
import GraphiQLExplorer from 'graphiql-explorer';
import 'graphiql/graphiql.css';
import { buildClientSchema, getIntrospectionQuery, GraphQLSchema, parse } from 'graphql';
import React, { Component } from 'react';
import { Component } from 'react';
import 'semantic-ui-css/semantic.min.css';
import './App.css';
import { AuthModal, AUTH_MODE } from './AuthModal';
Expand Down Expand Up @@ -125,14 +125,14 @@ class App extends Component<{}, State> {
}
async componentDidMount() {
const apiInfo = await getAPIInfo();
this.loadCredentials(apiInfo);
await this.loadCredentials(apiInfo);
this.setState({ apiInfo });
const introspectionResult = await this.fetch({
query: getIntrospectionQuery(),
});

const editor = this._graphiql?.getQueryEditor();
editor.setOption('extraKeys', {
editor?.setOption('extraKeys', {
...(editor.options.extraKeys || {}),
'Shift-Alt-LeftClick': this._handleInspectOperation,
});
Expand Down Expand Up @@ -263,7 +263,7 @@ class App extends Component<{}, State> {
}));
}

loadCredentials(apiInfo = this.state.apiInfo) {
async loadCredentials(apiInfo = this.state.apiInfo) {
const credentials = {};
const authProviders = [apiInfo.defaultAuthenticationType, ...apiInfo.additionalAuthenticationProviders];
const possibleAuth = authProviders.map((auth) => auth.authenticationType);
Expand All @@ -273,12 +273,13 @@ class App extends Component<{}, State> {
}

if (possibleAuth.includes('AMAZON_COGNITO_USER_POOLS')) {
try {
credentials['cognitoJWTToken'] = refreshToken(window.localStorage.getItem(LOCAL_STORAGE_KEY_NAMES.cognitoToken) || '');
} catch (e) {
let token = window.localStorage.getItem(LOCAL_STORAGE_KEY_NAMES.cognitoToken);
if (token) {
credentials['cognitoJWTToken'] = await refreshToken(token);
} else {
console.warn('Invalid Cognito token found in local storage. Using the default OIDC token');
// token is not valid
credentials['cognitoJWTToken'] = refreshToken(DEFAULT_COGNITO_JWT_TOKEN);
credentials['cognitoJWTToken'] = await refreshToken(DEFAULT_COGNITO_JWT_TOKEN);
}
}

Expand All @@ -287,10 +288,10 @@ class App extends Component<{}, State> {
.filter((auth) => auth.authenticationType === AUTH_MODE.OPENID_CONNECT)
.map((auth: any) => auth.openIDConnectConfig.Issuer);
try {
credentials['oidcJWTToken'] = refreshToken(window.localStorage.getItem(LOCAL_STORAGE_KEY_NAMES.oidcToken) || '', issuers[0]);
credentials['oidcJWTToken'] = await refreshToken(window.localStorage.getItem(LOCAL_STORAGE_KEY_NAMES.oidcToken) || '', issuers[0]);
} catch (e) {
console.warn('Invalid OIDC token found in local storage. Using the default OIDC token');
credentials['oidcJWTToken'] = refreshToken(DEFAULT_OIDC_JWT_TOKEN, issuers[0]);
credentials['oidcJWTToken'] = await refreshToken(DEFAULT_OIDC_JWT_TOKEN, issuers[0]);
}
}

Expand Down Expand Up @@ -329,6 +330,35 @@ class App extends Component<{}, State> {
const clearDataModal = clearDataModalVisible ? (
<ClearDataModal onClose={this.hideDataModal} onClear={this.clearDataAndShowMessage} />
) : null;

const buttons = [
{
onClick: () => this._graphiql?.handlePrettifyQuery(),
label: 'Prettify',
title: 'Prettify Query (Shift-Ctrl-P)',
},
{
onClick: () => this._graphiql?.handleToggleHistory(),
label: 'History',
title: 'Show History',
},
{
onClick: this._handleToggleExplorer,
label: 'Explorer',
title: 'Toggle Explorer',
},
{
onClick: this.toggleAuthModal,
label: 'Update Auth',
title: 'Auth Setting',
},
{
onClick: this.toggleClearDataModal,
label: 'Clear data',
title: 'Clear Mock Data',
},
];

return (
<>
{authModal}
Expand All @@ -351,23 +381,18 @@ class App extends Component<{}, State> {
response={clearResponse}
>
<GraphiQL.Toolbar>
<GraphiQL.Button
onClick={() => this._graphiql?.handlePrettifyQuery()}
label="Prettify"
title="Prettify Query (Shift-Ctrl-P)"
/>
<GraphiQL.Button onClick={() => this._graphiql?.handleToggleHistory()} label="History" title="Show History" />
<GraphiQL.Button onClick={this._handleToggleExplorer} label="Explorer" title="Toggle Explorer" />
<GraphiQL.Button onClick={this.toggleAuthModal} label="Update Auth" title="Auth Setting" />
<GraphiQL.Button onClick={this.toggleClearDataModal} label="Clear data" title="Clear Mock Data" />
{buttons.map((button, index) => (
<GraphiQL.Button key={index} onClick={button.onClick} label={button.label} title={button.title} />
))}
<GraphiQL.Menu
label={`Auth - ${AUTH_TYPE_TO_NAME[this.state.currentAuthMode]}${
this.state.currentAuthMode === 'AWS_IAM' ? `(${this.state.credentials.iamRole} Role)` : ''
}`}
title={AUTH_TYPE_TO_NAME[this.state.currentAuthMode]}
>
{authModes.map((mode) => (
{authModes.map((mode, index) => (
<GraphiQL.MenuItem
key={index}
title={AUTH_TYPE_TO_NAME[mode]}
label={`Use: ${AUTH_TYPE_TO_NAME[mode]}`}
onSelect={() => this.switchAuthMode(mode)}
Expand Down
14 changes: 7 additions & 7 deletions packages/amplify-graphiql-explorer/src/AuthModal.tsx
Expand Up @@ -304,23 +304,23 @@ export class AuthModal extends Component<Props, State> {
</Modal>
);
}
onGenerate() {
async onGenerate() {
try {
const newState = {
isOpen: false,
};
if (this.state.currentAuthMode === AUTH_MODE.AMAZON_COGNITO_USER_POOLS) {
newState['currentCognitoToken'] = this.generateCognitoJWTToken();
newState['currentCognitoToken'] = await this.generateCognitoJWTToken();
} else if (this.state.currentAuthMode === AUTH_MODE.OPENID_CONNECT) {
newState['currentOIDCToken'] = this.generateOIDCJWTToken();
newState['currentOIDCToken'] = await this.generateOIDCJWTToken();
}
this.setState(newState, () => {
this.onClose();
});
} catch (e) {}
}

generateCognitoJWTToken() {
async generateCognitoJWTToken() {
let additionalFields;
try {
additionalFields = JSON.parse(this.state.additionalFields?.trim() || '{}');
Expand Down Expand Up @@ -348,14 +348,14 @@ export class AuthModal extends Component<Props, State> {
tokenPayload['cognito:groups'] = this.state.userGroups;
tokenPayload['auth_time'] = Math.floor(Date.now() / 1000); // In seconds

const token = generateToken(tokenPayload);
const token = await generateToken(tokenPayload);
return token;
}

generateOIDCJWTToken() {
async generateOIDCJWTToken() {
const tokenPayload = this.state.currentOIDCTokenDecoded || '';
try {
return generateToken(tokenPayload);
return await generateToken(tokenPayload);
} catch (e) {
this.setState({
oidcTokenError: e.message,
Expand Down
20 changes: 12 additions & 8 deletions packages/amplify-graphiql-explorer/src/utils/jwt.ts
@@ -1,21 +1,25 @@
import { decode, sign, verify } from 'jsonwebtoken';
import { decodeJwt, SignJWT, jwtVerify, JWTPayload } from 'jose';

export function generateToken(decodedToken: string | object): string {
export async function generateToken(decodedToken: string | object): Promise<string> {
try {
if (typeof decodedToken === 'string') {
decodedToken = JSON.parse(decodedToken);
}
const token = sign(decodedToken, 'open-secrete');
verify(token, 'open-secrete');
const secret = new TextEncoder().encode('open-secrete');
const token = await new SignJWT(decodedToken as JWTPayload).setProtectedHeader({ alg: 'HS256' }).sign(secret);
await jwtVerify(token, secret);
return token;
} catch (e) {
const err = new Error('Error when generating OIDC token: ' + e.message);
throw err;
}
}

export function parse(token): object {
const decodedToken = decode(token);
export function parse(token: string | undefined): object | null {
if (typeof token === 'undefined' || typeof token !== 'string') {
return null;
}
const decodedToken = decodeJwt(token);
return decodedToken as object;
}

Expand All @@ -25,7 +29,7 @@ export function parse(token): object {
* @param token
* @param issuer
*/
export function refreshToken(token: string, issuer?: string): string {
export async function refreshToken(token: string, issuer?: string): Promise<string> {
const tokenObj: any = parse(token);
if (!Object.keys(tokenObj).length) {
throw new Error(`Invalid token ${token}`);
Expand All @@ -34,5 +38,5 @@ export function refreshToken(token: string, issuer?: string): string {
tokenObj.iss = issuer;
}
tokenObj.exp = Math.floor(Date.now() / 100 + 20000);
return generateToken(JSON.stringify(tokenObj));
return await generateToken(JSON.stringify(tokenObj));
}
2 changes: 1 addition & 1 deletion packages/amplify-util-mock/package.json
Expand Up @@ -94,7 +94,7 @@
"graphql-versioned-transformer": "^5.2.75",
"isomorphic-fetch": "^3.0.0",
"jest": "^29.0.0",
"jsonwebtoken": "^9.0.0",
"jose": "^5.2.0",
"uuid": "^8.3.2",
"ws": "^7.5.7"
},
Expand Down
Expand Up @@ -131,7 +131,7 @@ type Stage @model @auth(rules: [{ allow: groups, groups: ["Admin"]}]) {
// Verify we have all the details
expect(GRAPHQL_ENDPOINT).toBeTruthy();

const idToken = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME1, USERNAME1, [
const idToken = await signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME1, USERNAME1, [
ADMIN_GROUP_NAME,
WATCHER_GROUP_NAME,
PARTICIPANT_GROUP_NAME,
Expand All @@ -140,12 +140,12 @@ type Stage @model @auth(rules: [{ allow: groups, groups: ["Admin"]}]) {
Authorization: idToken,
});

const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [DEVS_GROUP_NAME]);
const idToken2 = await signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [DEVS_GROUP_NAME]);
GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, {
Authorization: idToken2,
});

const idToken3 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME3, USERNAME3, []);
const idToken3 = await signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME3, USERNAME3, []);
GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, {
Authorization: idToken3,
});
Expand Down
Expand Up @@ -86,7 +86,7 @@ beforeAll(async () => {
// Verify we have all the details
expect(GRAPHQL_ENDPOINT).toBeTruthy();

const idToken = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME1, USERNAME1, [
const idToken = await signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME1, USERNAME1, [
ADMIN_GROUP_NAME,
PARTICIPANT_GROUP_NAME,
PARTICIPANT_GROUP_NAME,
Expand All @@ -95,12 +95,12 @@ beforeAll(async () => {
Authorization: idToken,
});

const idToken2 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [DEVS_GROUP_NAME]);
const idToken2 = await signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME2, USERNAME2, [DEVS_GROUP_NAME]);
GRAPHQL_CLIENT_2 = new GraphQLClient(GRAPHQL_ENDPOINT, {
Authorization: idToken2,
});

const idToken3 = signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME3, USERNAME3, []);
const idToken3 = await signUpAddToGroupAndGetJwtToken(USER_POOL_ID, USERNAME3, USERNAME3, []);
GRAPHQL_CLIENT_3 = new GraphQLClient(GRAPHQL_ENDPOINT, {
Authorization: idToken3,
});
Expand Down

0 comments on commit 4d0677d

Please sign in to comment.