Add token verification with development basic header
This commit is contained in:
@@ -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 };
|
||||||
+11
-2
@@ -1,7 +1,7 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
import { ApolloServer } from 'apollo-server';
|
import { ApolloServer, AuthenticationError } from 'apollo-server';
|
||||||
import knexStringcase from 'knex-stringcase';
|
import knexStringcase from 'knex-stringcase';
|
||||||
import { dbConfig } from './config';
|
import { dbConfig } from './config';
|
||||||
import { typeDefs } from './schema';
|
import { typeDefs } from './schema';
|
||||||
@@ -10,6 +10,7 @@ import { OrderAPI } from './datasources/order-api';
|
|||||||
import { ProductAPI } from './datasources/product-api';
|
import { ProductAPI } from './datasources/product-api';
|
||||||
import { DesignerAPI } from './datasources/designer-api';
|
import { DesignerAPI } from './datasources/designer-api';
|
||||||
import { Api2DataSource } from './datasources/api2-datasource';
|
import { Api2DataSource } from './datasources/api2-datasource';
|
||||||
|
import { verifyToken } from './cognito/cognito-client';
|
||||||
|
|
||||||
console.log(`dbConfig`, dbConfig);
|
console.log(`dbConfig`, dbConfig);
|
||||||
|
|
||||||
@@ -27,7 +28,15 @@ const dataSources = () => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const context = async ({ req }) => {
|
const context = async ({ req }) => {
|
||||||
return { data: { foo: 'bar' } };
|
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() {
|
async function main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user