P5-7142 Merge pull request #5 from Photowall/P5-7142-add-cognito
P5-7142 add cognito
This commit is contained in:
+3
-1
@@ -10,7 +10,9 @@ services:
|
||||
container_name: photowall.graphql
|
||||
hostname: graphql.local
|
||||
ports:
|
||||
- "4000:4000"
|
||||
- '4000:4000'
|
||||
environment:
|
||||
- COGNITO_POOL_ID=eu-west-1_3O4VfvPn7
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
Generated
+1923
-13049
File diff suppressed because it is too large
Load Diff
+19
-15
@@ -12,32 +12,36 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"apollo-datasource": "^0.9.0",
|
||||
"apollo-datasource-rest": "^0.13.0",
|
||||
"apollo-server": "^2.24.1",
|
||||
"async-redis": "^1.1.7",
|
||||
"apollo-datasource-rest": "^0.14.0",
|
||||
"apollo-server": "^2.25.2",
|
||||
"async-redis": "^2.0.0",
|
||||
"axios": "^0.21.1",
|
||||
"datasource-sql": "^1.4.1",
|
||||
"dotenv": "^9.0.2",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql-scalars": "^1.9.3",
|
||||
"dotenv": "^10.0.0",
|
||||
"graphql": "^15.5.1",
|
||||
"graphql-scalars": "^1.10.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jwk-to-pem": "^2.0.5",
|
||||
"knex-stringcase": "^1.4.5",
|
||||
"nodemon": "^2.0.7",
|
||||
"nodemon": "^2.0.9",
|
||||
"pg": "^8.6.0",
|
||||
"pg-hstore": "^2.3.3",
|
||||
"prettier": "^2.3.0",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"prettier": "^2.3.2",
|
||||
"stringcase": "^4.3.1",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.2.4",
|
||||
"ts-node": "^10.0.0",
|
||||
"typescript": "^4.3.5",
|
||||
"util": "^0.12.4",
|
||||
"validator": "^13.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^26.0.23",
|
||||
"@typescript-eslint/eslint-plugin": "^4.24.0",
|
||||
"@typescript-eslint/parser": "^4.24.0",
|
||||
"eslint": "^7.26.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.2",
|
||||
"@typescript-eslint/parser": "^4.28.2",
|
||||
"eslint": "^7.30.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"jest": "^27.0.4",
|
||||
"jest": "^27.0.6",
|
||||
"testcontainers": "^7.11.1",
|
||||
"ts-jest": "^27.0.3"
|
||||
}
|
||||
|
||||
@@ -115,6 +115,8 @@ export const createAllTestContainers =
|
||||
'DATABASE_URL',
|
||||
`postgresql://testuser:test@${dbIpAddress}:5432/photowalltest1`,
|
||||
)
|
||||
.withEnv('NODE_ENV', 'test')
|
||||
.withEnv('COGNITO_POOL_ID', 'eu-west-1_3O4VfvPn7')
|
||||
.withWaitStrategy(
|
||||
Wait.forLogMessage('Server is running on http://localhost:4000'),
|
||||
)
|
||||
|
||||
+1
-132
@@ -1,139 +1,8 @@
|
||||
{
|
||||
"name": "db_generator",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fake-people": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lorem-ipsum": "^2.0.3",
|
||||
"random-date-generator": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chalk": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/fake-people": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fake-people/-/fake-people-1.0.0.tgz",
|
||||
"integrity": "sha1-PRdr4BufkwBwceu0gWIuxebN2BM=",
|
||||
"dependencies": {
|
||||
"faker": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/faker": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/faker/-/faker-3.1.0.tgz",
|
||||
"integrity": "sha1-D5CPr05uwCUk5UpX5DLFwBPgjJ8="
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lorem-ipsum": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lorem-ipsum/-/lorem-ipsum-2.0.3.tgz",
|
||||
"integrity": "sha512-CX2r84DMWjW/DWiuzicTI9aRaJPAw2cvAGMJYZh/nx12OkTGqloj8y8FU0S8ZkKwOdqhfxEA6Ly8CW2P6Yxjwg==",
|
||||
"dependencies": {
|
||||
"commander": "^2.17.1"
|
||||
},
|
||||
"bin": {
|
||||
"lorem-ipsum": "dist/bin/lorem-ipsum.bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.x",
|
||||
"npm": ">= 5.x"
|
||||
}
|
||||
},
|
||||
"node_modules/random-date-generator": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/random-date-generator/-/random-date-generator-1.0.2.tgz",
|
||||
"integrity": "sha1-xjZmW7/tCM7J4iBT1J8jPI/YTdE="
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
import { promisify } from 'util';
|
||||
import * as Axios from 'axios';
|
||||
import * as jsonwebtoken from 'jsonwebtoken';
|
||||
import { Maybe } from '../datasources/types';
|
||||
const jwkToPem = require('jwk-to-pem');
|
||||
|
||||
export interface ClaimVerifyRequest {
|
||||
readonly token?: string;
|
||||
}
|
||||
|
||||
export interface ClaimVerifyResult {
|
||||
readonly userName: string;
|
||||
readonly isValid: boolean;
|
||||
readonly error?: string;
|
||||
readonly accessToken: Maybe<AccessTokenClaim>;
|
||||
readonly idToken: Maybe<IdTokenClaim>;
|
||||
}
|
||||
|
||||
interface TokenHeader {
|
||||
kid: string;
|
||||
alg: string;
|
||||
}
|
||||
interface PublicKey {
|
||||
alg: string;
|
||||
e: string;
|
||||
kid: string;
|
||||
kty: string;
|
||||
n: string;
|
||||
use: string;
|
||||
}
|
||||
interface PublicKeyMeta {
|
||||
instance: PublicKey;
|
||||
pem: string;
|
||||
}
|
||||
|
||||
interface PublicKeys {
|
||||
keys: PublicKey[];
|
||||
}
|
||||
|
||||
interface MapOfKidToPublicKey {
|
||||
[key: string]: PublicKeyMeta;
|
||||
}
|
||||
|
||||
interface Claim {
|
||||
token_use?: string;
|
||||
auth_time: number;
|
||||
iat: number;
|
||||
exp: number;
|
||||
iss: string;
|
||||
}
|
||||
|
||||
interface AccessTokenClaim extends Claim {
|
||||
token_use: string;
|
||||
username: string;
|
||||
client_id: string;
|
||||
'cognito:groups': string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
interface IdTokenClaim extends Claim {
|
||||
'custom:adgroups': string; // 'custom:adgroups': '[Domain Users, Images_Write, VPN_Users, WebAdmin, WebAdminSuperUser]',
|
||||
'cognito:username': string;
|
||||
token_use: string;
|
||||
email_verified: boolean;
|
||||
nonce: string;
|
||||
at_hash: string;
|
||||
email: string;
|
||||
aud: string;
|
||||
sub: string;
|
||||
identities: [Identity];
|
||||
}
|
||||
|
||||
interface Identity {
|
||||
userId: string;
|
||||
providerName: string;
|
||||
providerType: string;
|
||||
issuer: string;
|
||||
primary: string;
|
||||
dateCreated: string;
|
||||
}
|
||||
|
||||
const cognitoPoolId = process.env.COGNITO_POOL_ID || '';
|
||||
if (!cognitoPoolId) {
|
||||
throw new Error('env var required for cognito pool');
|
||||
}
|
||||
const cognitoIssuer = `https://cognito-idp.eu-west-1.amazonaws.com/${cognitoPoolId}`;
|
||||
|
||||
let cacheKeys: MapOfKidToPublicKey | undefined;
|
||||
const getPublicKeys = async (): Promise<MapOfKidToPublicKey> => {
|
||||
if (!cacheKeys) {
|
||||
const url = `${cognitoIssuer}/.well-known/jwks.json`;
|
||||
const publicKeys = await Axios.default.get<PublicKeys>(url);
|
||||
cacheKeys = publicKeys.data.keys.reduce((agg, current) => {
|
||||
const pem = jwkToPem(current);
|
||||
agg[current.kid] = { instance: current, pem };
|
||||
return agg;
|
||||
}, {} as MapOfKidToPublicKey);
|
||||
return cacheKeys;
|
||||
} else {
|
||||
return cacheKeys;
|
||||
}
|
||||
};
|
||||
|
||||
const verifyPromised = promisify(jsonwebtoken.verify.bind(jsonwebtoken));
|
||||
|
||||
const verifyToken = async (
|
||||
request: ClaimVerifyRequest,
|
||||
): Promise<ClaimVerifyResult> => {
|
||||
try {
|
||||
// console.log(`user claim verify invoked for ${JSON.stringify(request)}`);
|
||||
const token = request.token;
|
||||
const tokenSections = (token || '').split('.');
|
||||
if (tokenSections.length < 2) {
|
||||
throw new Error('requested token is invalid');
|
||||
}
|
||||
const headerJSON = Buffer.from(tokenSections[0], 'base64').toString('utf8');
|
||||
const header = JSON.parse(headerJSON) as TokenHeader;
|
||||
const keys = await getPublicKeys();
|
||||
const key = keys[header.kid];
|
||||
if (key === undefined) {
|
||||
throw new Error('claim made for unknown kid');
|
||||
}
|
||||
const claim = (await verifyPromised(token, key.pem)) as Claim;
|
||||
const currentSeconds = Math.floor(new Date().valueOf() / 1000);
|
||||
|
||||
if (currentSeconds > claim.exp || currentSeconds < claim.auth_time) {
|
||||
throw new Error('claim is expired or invalid');
|
||||
}
|
||||
if (claim.iss !== cognitoIssuer) {
|
||||
throw new Error('claim issuer is invalid');
|
||||
}
|
||||
if (claim.token_use !== 'access' && claim.token_use !== 'id') {
|
||||
throw new Error('invalid token type');
|
||||
}
|
||||
if (claim.token_use === 'access') {
|
||||
// Access token, check for scopes
|
||||
const accessClaim = claim as AccessTokenClaim;
|
||||
console.debug(`accessClaim confirmed for ${accessClaim.username}`);
|
||||
return {
|
||||
userName: accessClaim.username,
|
||||
isValid: true,
|
||||
idToken: null,
|
||||
accessToken: accessClaim,
|
||||
};
|
||||
} else {
|
||||
const idClaim = claim as IdTokenClaim;
|
||||
console.debug(`idClaim confirmed for ${idClaim.email}`);
|
||||
return {
|
||||
userName: idClaim.email,
|
||||
isValid: true,
|
||||
idToken: idClaim,
|
||||
accessToken: null,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
userName: '',
|
||||
error: error.message,
|
||||
isValid: false,
|
||||
idToken: null,
|
||||
accessToken: null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export { verifyToken };
|
||||
+15
-2
@@ -1,7 +1,7 @@
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import { ApolloServer } from 'apollo-server';
|
||||
import { ApolloServer, AuthenticationError } from 'apollo-server';
|
||||
import knexStringcase from 'knex-stringcase';
|
||||
import { dbConfig } from './config';
|
||||
import { typeDefs } from './schema';
|
||||
@@ -10,6 +10,7 @@ import { OrderAPI } from './datasources/order-api';
|
||||
import { ProductAPI } from './datasources/product-api';
|
||||
import { DesignerAPI } from './datasources/designer-api';
|
||||
import { Api2DataSource } from './datasources/api2-datasource';
|
||||
import { verifyToken } from './cognito/cognito-client';
|
||||
|
||||
console.log(`dbConfig`, dbConfig);
|
||||
|
||||
@@ -27,7 +28,19 @@ const dataSources = () => ({
|
||||
});
|
||||
|
||||
const context = async ({ req }) => {
|
||||
return { data: { foo: 'bar' } };
|
||||
if (process.env.NODE_ENV == 'test') {
|
||||
return { auth: 'testuser' };
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization || '';
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7, authHeader.length);
|
||||
const res = await verifyToken({ token });
|
||||
return { auth: res };
|
||||
} else if (authHeader == 'Basic ZGV2OmRldg==') {
|
||||
return { auth: 'during development' };
|
||||
}
|
||||
throw new AuthenticationError('Not authorized');
|
||||
};
|
||||
|
||||
async function main() {
|
||||
|
||||
Reference in New Issue
Block a user