Machine Token Handling for Hasura w/ Auth0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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}.`); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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