Skip to content

Instantly share code, notes, and snippets.

@Fyko
Last active July 11, 2022 18:33
Show Gist options
  • Save Fyko/4eeab259d2629ee8a393d29b4cd381ce to your computer and use it in GitHub Desktop.
Save Fyko/4eeab259d2629ee8a393d29b4cd381ce to your computer and use it in GitHub Desktop.
Machine Token Handling for Hasura w/ Auth0
// MIT License - Copyright (c) 2022 PUSHAS PTY LTD.
import { ResourceNotFoundException, SecretsManager } from '@aws-sdk/client-secrets-manager';
import { SecretsManagerRotationHandler } from 'aws-lambda';
import fetch from 'node-fetch';
const secretsClient = new SecretsManager({});
export const generateAccessToken = async (roles: string) => {
const response = await fetch(`https://${process.env.AUTH0_M2M_DOMAIN}/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': roles,
},
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.AUTH0_M2M_CLIENT_ID!,
client_secret: process.env.AUTH0_M2M_CLIENT_SECRET!,
audience: process.env.AUTH0_M2M_AUDIENCE!,
}),
});
const body = (await response.json()) as Record<'access_token' | 'token_type', string> & { expires_in: number };
return body;
};
export const handler: SecretsManagerRotationHandler = async (event) => {
const { SecretId: arn, ClientRequestToken: token, Step: step } = event;
const meta = await secretsClient.describeSecret({ SecretId: arn });
// simple checks if we can actually run rotation
if (!meta.RotationEnabled) throw new TypeError('Rotation is not enabled for this secret');
const versions = meta.VersionIdsToStages!;
if (!(token in versions))
throw new TypeError(`Secret version ${token} has no stage for rotation of secret ${arn}.`);
if ('AWSCURRENT' in versions)
throw new TypeError(`Secret version ${token} already set as AWSCURRENT for secret ${arn}.`);
if (!versions[token].includes('AWSPENDING'))
throw new TypeError(`Secret version ${token} not set as AWSPENDING for rotation of secret ${arn}.`);
switch (step) {
case 'createSecret':
await createSecret(arn, token, meta.Description!);
break;
case 'setSecret':
setSecret();
break;
case 'testSecret':
testSecret();
break;
case 'finishSecret':
await finishSecret(arn, token);
break;
default:
throw new TypeError(`Unknown step ${step} for secret ${arn}.`);
}
};
const createSecret = async (arn: string, token: string, roles: string) => {
await secretsClient.getSecretValue({ SecretId: arn, VersionStage: 'AWSCURRENT' });
try {
await secretsClient.getSecretValue({ SecretId: arn, VersionId: token, VersionStage: 'AWSPENDING' });
console.log(`createSecret: secret ${arn} version ${token} already exists`);
} catch (err: unknown) {
if (err instanceof ResourceNotFoundException) {
// generate a new access token
const { access_token, token_type } = await generateAccessToken(roles);
await secretsClient.putSecretValue({
SecretId: arn,
ClientRequestToken: token,
SecretString: JSON.stringify({ access_token, token_type }),
VersionStages: ['AWSPENDING'],
});
console.log(`createSecret: secret ${arn} version ${token} created`);
}
}
};
const setSecret = () => new ReferenceError('Not Implemented');
const testSecret = () => new ReferenceError('Not Implemented');
const finishSecret = async (arn: string, token: string) => {
const { VersionIdsToStages } = await secretsClient.describeSecret({ SecretId: arn });
let current = '';
for (const version of Object.keys(VersionIdsToStages!)) {
if (VersionIdsToStages![version].includes('AWSCURRENT')) {
if (version === token) {
console.log(`finishSecret: Version ${version} already marked as AWSCURRENT for ${arn}`);
return;
}
current = version;
break;
}
}
await secretsClient.updateSecretVersionStage({
SecretId: arn,
VersionStage: 'AWSCURRENT',
MoveToVersionId: token,
RemoveFromVersionId: current,
});
console.log(`finishSecret: Successfully set AWSCURRENT stage to version ${token} for secret ${arn}.`);
};
// MIT License - Copyright (c) 2022 PUSHAS PTY LTD.
import { App, Stack, StackProps, Function } from '@serverless-stack/resources';
import { Duration } from 'aws-cdk-lib';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
export class HasuraSecretStack extends Stack {
public readonly emailerSecret = new Secret(this, 'EmailerHasuraAccessToken', { description: 'automaton_emailer' });
public constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props);
const environment: Record<string, string> = {
AUTH0_M2M_DOMAIN: process.env.AUTH0_M2M_DOMAIN!,
AUTH0_M2M_CLIENT_ID: process.env.AUTH0_M2M_CLIENT_ID!,
AUTH0_M2M_CLIENT_SECRET: process.env.AUTH0_M2M_CLIENT_SECRET!,
AUTH0_M2M_AUDIENCE: process.env.AUTH0_M2M_AUDIENCE!,
};
const hasuraTokenRotator = new Function(this, 'HasuraTokenRotationLambda', {
handler: 'function.handler',
environment,
});
hasuraTokenRotator.attachPermissions([
new PolicyStatement({
actions: [
'secretsmanager:GetSecretValue',
'secretsmanager:DescribeSecret',
'secretsmanager:PutSecretValue',
],
effect: Effect.ALLOW,
resources: ['*'],
}),
]);
this.emailerSecret.addRotationSchedule('EmailerRotationSchedule', {
automaticallyAfter: Duration.days(1),
rotationLambda: hasuraTokenRotator,
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment