From 626f3e70c227e39a1145b5611389384637182e95 Mon Sep 17 00:00:00 2001 From: David Moore Date: Wed, 11 Dec 2024 16:23:31 +1100 Subject: [PATCH] fix: update auth0 and cognito guides to latest security rules --- docs/guides/nodejs/amazon-cognito.mdx | 222 +++++++++--------------- docs/guides/nodejs/secure-api-auth0.mdx | 49 +++--- 2 files changed, 103 insertions(+), 168 deletions(-) diff --git a/docs/guides/nodejs/amazon-cognito.mdx b/docs/guides/nodejs/amazon-cognito.mdx index cb99c8d58..51a9eed19 100644 --- a/docs/guides/nodejs/amazon-cognito.mdx +++ b/docs/guides/nodejs/amazon-cognito.mdx @@ -8,7 +8,7 @@ languages: - typescript - javascript published_at: 2023-10-09 -updated_at: 2024-05-15 +updated_at: 2024-12-11 --- # Securing APIs with Amazon Cognito @@ -42,14 +42,16 @@ In this new Nitric project there will be an example API with a single `GET: /hel -```typescript !! title:services/hello.ts +```typescript title:services/api.ts import { api } from '@nitric/sdk' -const helloApi = api('main') +const mainApi = api('main') -helloApi.get('/hello/:name', async (ctx) => { +mainApi.get('/hello/:name', async (ctx) => { const { name } = ctx.req.params + ctx.res.body = `Hello ${name}` + return ctx }) ``` @@ -58,14 +60,16 @@ helloApi.get('/hello/:name', async (ctx) => { -```javascript !! title:services/hello.js +```javascript title:services/api.js import { api } from '@nitric/sdk' -const helloApi = api('main') +const mainApi = api('main') -helloApi.get('/hello/:name', async (ctx) => { +mainApi.get('/hello/:name', async (ctx) => { const { name } = ctx.req.params + ctx.res.body = `Hello ${name}` + return ctx }) ``` @@ -78,30 +82,32 @@ helloApi.get('/hello/:name', async (ctx) => { [Nitric APIs](/apis#api-security) allow initial token authentication to be performed by the API Gateways of various cloud providers, such as AWS API Gateway. When configured correctly this will ensure unauthenticated requests are rejected before reaching your application code. -To add this API Gateway authentication we need to create a `security definition` and then apply that definition to specific routes or the entire API. Here we'll update the `main` API by adding a new security definition named `cognito`. +To add this API Gateway authentication we need to create a `security definition` and then apply that definition to specific routes or the entire API. Here we'll update the `main` API by adding a new security definition named `cognito`. This will apply to all routes in the API. -```typescript !! title:services/hello.ts -import { api } from '@nitric/sdk' +```typescript title:services/api.ts +import { api, oidcRule } from '@nitric/sdk' + +const defaultSecurityRule = oidcRule({ + name: 'cognito', + issuer: + 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', + audiences: [''], +}) -const helloApi = api('main', { - // declare security definition named 'cognito'. - securityDefinitions: { - cognito: { - kind: 'jwt', - issuer: - 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', - audiences: [''], - }, - }, +const mainApi = api('main', { + // apply the security definition to all routes in this API. + security: [defaultSecurityRule()], }) -helloApi.get('/hello/:name', async (ctx) => { +mainApi.get('/hello/:name', async (ctx) => { const { name } = ctx.req.params + ctx.res.body = `Hello ${name}` + return ctx }) ``` @@ -110,24 +116,26 @@ helloApi.get('/hello/:name', async (ctx) => { -```javascript !! title:services/hello.js -import { api } from '@nitric/sdk' +```javascript title:services/api.js +import { api, oidcRule } from '@nitric/sdk' -const helloApi = api('main', { - // declare security definition named 'cognito'. - securityDefinitions: { - cognito: { - kind: 'jwt', - issuer: - 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', - audiences: [''], - }, - }, +const defaultSecurityRule = oidcRule({ + name: 'cognito', + issuer: + 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', + audiences: [''], }) -helloApi.get('/hello/:name', async (ctx) => { +const mainApi = api('main', { + // apply the security definition to all routes in this API. + security: [defaultSecurityRule()], +}) + +mainApi.get('/hello/:name', async (ctx) => { const { name } = ctx.req.params + ctx.res.body = `Hello ${name}` + return ctx }) ``` @@ -141,74 +149,6 @@ helloApi.get('/hello/:name', async (ctx) => { values to match your values from Amazon Cognito. -Next, we need to ensure this security definition is applied, otherwise it won't be enforced. APIs can have multiple security definitions applied at different levels or to different routes. For example, you might have one security definition for external users and another for administrators/internal users. - -In this example we'll apply the security at the API level, ensuring all routes require authentication. - - - - - -```typescript !! title:services/hello.ts -import { api } from '@nitric/sdk' - -const helloApi = api('main', { - // declare security definition named 'cognito'. - securityDefinitions: { - cognito: { - kind: 'jwt', - issuer: - 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', - audiences: [''], - }, - }, - // apply the 'cognito' security definition to all routes in this API. - security: { - cognito: [], - }, -}) - -helloApi.get('/hello/:name', async (ctx) => { - const { name } = ctx.req.params - ctx.res.body = `Hello ${name}` - return ctx -}) -``` - - - - - -```javascript !! title:services/hello.js -import { api } from '@nitric/sdk' - -const helloApi = api('main', { - // declare security definition named 'cognito'. - securityDefinitions: { - cognito: { - kind: 'jwt', - issuer: - 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', - audiences: [''], - }, - }, - // apply the 'cognito' security definition to all routes in this API. - security: { - cognito: [], - }, -}) - -helloApi.get('/hello/:name', async (ctx) => { - const { name } = ctx.req.params - ctx.res.body = `Hello ${name}` - return ctx -}) -``` - - - - - It's worth noting that these security definitions are *not* enforced when testing your Nitric services locally. Currently, they're only enforced when @@ -231,8 +171,8 @@ In the example below we're simply checking whether the user is a member of a par -```typescript !! title:services/hello.ts -import { api, HttpContext, HttpMiddleware } from '@nitric/sdk' +```typescript title:services/api.ts +import { api, HttpContext, HttpMiddleware, oidcRule } from '@nitric/sdk' import * as jwt from 'jsonwebtoken' interface AccessToken { @@ -255,6 +195,8 @@ const authorizeAuthors: HttpMiddleware = (ctx: AuthContext, next) => { : ctx.req.headers['authorization'] const token = authHeader?.split(' ')[1] + // If no token is present, deny access. + // There should be a token as the security rules would be checked first, but this is a good practice. if (!token) { ctx.res.status = 401 ctx.res.body = 'Unauthorized' @@ -264,8 +206,11 @@ const authorizeAuthors: HttpMiddleware = (ctx: AuthContext, next) => { // It's valuable to validate the token's shape, skipped here for brevity. ctx.user = jwt.decode(token) as AccessToken - // Ensure the user is a member of the "authors" cognito group - if (!ctx.user['cognito:groups'].includes('authors')) { + // If the user is not a member of any groups or not an "author", deny access + if ( + !ctx.user['cognito:groups'] || + !ctx.user['cognito:groups'].includes('authors') + ) { ctx.res.status = 401 ctx.res.body = 'Unauthorized' return ctx @@ -274,24 +219,19 @@ const authorizeAuthors: HttpMiddleware = (ctx: AuthContext, next) => { return next(ctx) } -const helloApi = api('main', { - // declare security definition named 'cognito'. - securityDefinitions: { - cognito: { - kind: 'jwt', - issuer: - 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', - audiences: [''], - }, - }, - // apply the 'cognito' security definition to all routes in this API. - security: { - cognito: [], - }, +const defaultSecurityRule = oidcRule({ + name: 'cognito', + issuer: + 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', + audiences: [''], +}) + +const mainApi = api('main', { + security: [defaultSecurityRule()], middleware: [authorizeAuthors], }) -helloApi.get('/hello/:name', async (ctx: AuthContext) => { +mainApi.get('/hello/:name', async (ctx: AuthContext) => { const { name } = ctx.req.params ctx.res.body = `Hello ${name}, you're group memberships include: ${ctx.user[ 'cognito:groups' @@ -304,8 +244,8 @@ helloApi.get('/hello/:name', async (ctx: AuthContext) => { -```javascript !! title:services/hello.js -import { api, HttpContext } from '@nitric/sdk' +```javascript title:services/api.js +import { api, oidcRule } from '@nitric/sdk' import * as jwt from 'jsonwebtoken' /** @@ -320,17 +260,22 @@ const authorizeAuthors = (ctx, next) => { : ctx.req.headers['authorization'] const token = authHeader?.split(' ')[1] + // If no token is present, deny access. + // There should be a token as the security rules would be checked first, but this is a good practice. if (!token) { ctx.res.status = 401 ctx.res.body = 'Unauthorized' return ctx } - // It's valuable to check the token's shape, skipped here for brevity. + // It's valuable to validate the token's shape, skipped here for brevity. ctx.user = jwt.decode(token) - // Ensure the user is a member of the "authors" cognito group - if (!ctx.user['cognito:groups'].includes('authors')) { + // If the user is not a member of any groups or not an "author", deny access + if ( + !ctx.user['cognito:groups'] || + !ctx.user['cognito:groups'].includes('authors') + ) { ctx.res.status = 401 ctx.res.body = 'Unauthorized' return ctx @@ -339,24 +284,19 @@ const authorizeAuthors = (ctx, next) => { return next(ctx) } -const helloApi = api('main', { - // declare security definition named 'cognito'. - securityDefinitions: { - cognito: { - kind: 'jwt', - issuer: - 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', - audiences: [''], - }, - }, - // apply the 'cognito' security definition to all routes in this API. - security: { - cognito: [], - }, +const defaultSecurityRule = oidcRule({ + name: 'cognito', + issuer: + 'https://cognito-idp..amazonaws.com//.well-known/openid-configuration', + audiences: [''], +}) + +const mainApi = api('main', { + security: [defaultSecurityRule()], middleware: [authorizeAuthors], }) -helloApi.get('/hello/:name', async (ctx) => { +mainApi.get('/hello/:name', async (ctx) => { const { name } = ctx.req.params ctx.res.body = `Hello ${name}, you're group memberships include: ${ctx.user[ 'cognito:groups' diff --git a/docs/guides/nodejs/secure-api-auth0.mdx b/docs/guides/nodejs/secure-api-auth0.mdx index f17b8c892..b18315cbc 100644 --- a/docs/guides/nodejs/secure-api-auth0.mdx +++ b/docs/guides/nodejs/secure-api-auth0.mdx @@ -6,7 +6,7 @@ languages: - typescript - javascript published_at: 2022-05-23 -updated_at: 2024-10-11 +updated_at: 2024-12-11 --- # Securing your API with Auth0 @@ -56,14 +56,14 @@ Second we'll get the `issuer` for our API, this will be our Auth0 environment en ![get audience value](/docs/images/guides/auth0/auth0-get-issuer.png) -In our new Nitric service you will have the following in the `hello.ts` file +In our new Nitric service you will have the following in the `api.ts` file -```typescript +```typescript title:services/api.ts import { api } from '@nitric/sdk' -const helloApi = api('main') +const mainApi = api('main') -helloApi.get('/hello/:name', async (ctx) => { +mainApi.get('/hello/:name', async (ctx) => { const { name } = ctx.req.params ctx.res.body = `Hello ${name}` @@ -74,27 +74,22 @@ helloApi.get('/hello/:name', async (ctx) => { To secure our API we will need to import the `jwt` function from the Nitric SDK and configure our API gateway with the values we got from Auth0 in the above steps. -```typescript -import { api, jwt } from '@nitric/sdk' - -const helloApi = api('main', { - // define security for this API - securityDefinitions: { - user: jwt({ - issuer: 'https://your-auth0-app.region.auth0.com', // 👀 your issuer value goes here - audiences: ['testing'], // 👀 your audience value goes here - }), - }, - - security: { - // Apply the above security to the entire API, note the string matches the above key - user: [ - // NOTE: The array option here is to specify required scopes, for simplicity we'll leave this blank for now - ], - }, +```typescript title:services/api.ts +import { api, oidcRule } from '@nitric/sdk' + +// define security for this API +const defaultSecurityRule = oidcRule({ + name: 'auth0', + issuer: + 'https://your-auth0-app.region.auth0.com/.well-known/openid-configuration', // 👀 your issuer value goes here audience: "api://default", + audiences: ['testing'], // 👀 your audience value goes here +}) + +const mainApi = api('main', { + security: [defaultSecurityRule()], }) -helloApi.get('/hello/:name', async (ctx) => { +mainApi.get('/hello/:name', async (ctx) => { const { name } = ctx.req.params ctx.res.body = `Hello ${name}` @@ -105,13 +100,13 @@ helloApi.get('/hello/:name', async (ctx) => { ## Testing the Security Definition -To test our security we will need to deploy our application to a cloud that Nitric supports (security rules are currently not enforced when using `nitric run` for local development). +To test our security we will need to deploy our application to a cloud that Nitric supports (security rules are currently not enforced for local development). If you don't have a stack already defined we'll create one with `nitric stack new`. Run `nitric stack new` and follow the prompts to create a new stack. -Then run `nitric up` to deploy your application. +Configure your stack, then run `nitric up` to deploy your application. We can check to see if our application is secure by calling it without an `Authorization` header @@ -133,4 +128,4 @@ curl -H "Authorization: Bearer " /hello/world Which should return `Hello world`. -And that's all it takes to secure your Nitric APIs with Auth0 🎉. For more detailed information on available options for securing your APIs check out our [API docs](https://nitric.io/docs). +And that's all it takes to secure your Nitric APIs with Auth0 🎉. For more detailed information on available options for securing your APIs check out our [API docs](/apis#api-security).