├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── 1-feature.md │ ├── 2-bug.md │ ├── 3-docs.md │ ├── 4-question.md │ └── 5-other.md ├── PULL_REQUEST_TEMPLATE.md ├── renovate.json └── workflows │ ├── next.yml │ ├── pr.yml │ └── stable.yml ├── .gitignore ├── .prettierignore ├── README.md ├── package.json ├── src ├── index.ts ├── runtime.ts ├── schema.ts └── settings.ts ├── tests ├── test.spec.ts └── tsconfig.json ├── tsconfig.base.json ├── tsconfig.json ├── tsconfig.tsbuildinfo └── yarn.lock /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | For **questions**, please use the repo's [GitHub Discussions](https://github.com/graphql-nexus/nexus/discussions) 2 | 3 | --- 4 | 5 | For **feature requests**, please fill out the [feature request template](https://github.com/graphql-nexus/nexus/issues/new?template=1-feature.md) 6 | 7 | --- 8 | 9 | For **bug reports**, please fill out the [bug report issue template](https://github.com/graphql-nexus/nexus/issues/new?template=2-bug.md) 10 | 11 | --- 12 | 13 | For **documentation issues**, please fill out the [documentation issue template](https://github.com/graphql-nexus/nexus/issues/new?template=3-docs.md) 14 | 15 | --- 16 | 17 | For **something else**, please fill out the [something else template](https://github.com/graphql-nexus/nexus/issues/new?template=5-other.md) 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature 3 | about: You have an idea for a new capability or a refinement to an existing one 4 | title: '' 5 | labels: type/feat 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | #### Perceived Problem 15 | 16 | #### Ideas / Proposed Solution(s) 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: You encountered something that is not working the way it should 4 | title: '' 5 | labels: type/bug 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | #### Nexus Report 15 | 16 | #### Screenshot 17 | 18 | #### Description 19 | 20 | #### Repro 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-docs.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docs 3 | about: Feedback or ideas about the documentation 4 | title: '' 5 | labels: type/docs 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | #### What 15 | 16 | #### Why 17 | 18 | #### How 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/4-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Question, need support, confused, unsure about something 4 | title: 'Stop' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/5-other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Something Else 3 | about: Feedback, support, docs, performance, ... 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | #### What 15 | 16 | #### Why 17 | 18 | #### How 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | closes #... 2 | 3 | #### TODO 4 | 5 | - [ ] docs 6 | - [ ] tests 7 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>prisma-labs/renovate-config"] 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/next.yml: -------------------------------------------------------------------------------- 1 | name: next 2 | 3 | on: 4 | push: 5 | branches: [next] 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] 12 | node-version: [10.x, 12.x, 14.x] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: yarn --frozen-lockfile 21 | - run: yarn build 22 | - run: yarn -s test 23 | # 24 | publish: 25 | needs: [test] 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Get all git commits and tags 30 | run: git fetch --prune --unshallow --tags 31 | - uses: actions/setup-node@v1 32 | - name: Install Deps 33 | run: yarn --frozen-lockfile 34 | - name: Build 35 | run: yarn build 36 | - name: Make release 37 | id: release 38 | env: 39 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 40 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | run: | 42 | yarn -s dripip preview --json > result.json 43 | echo '==> Publish Result' 44 | jq '.' < result.json 45 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] 12 | node-version: [10.x, 12.x, 14.x] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: yarn --frozen-lockfile 21 | - run: yarn build 22 | - run: yarn -s test 23 | -------------------------------------------------------------------------------- /.github/workflows/stable.yml: -------------------------------------------------------------------------------- 1 | name: stable 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] 12 | node-version: [10.x, 12.x, 14.x] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: yarn --frozen-lockfile 21 | - run: yarn build 22 | - run: yarn -s test 23 | # 24 | publish: 25 | needs: [test] 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - name: Get all git commits and tags 30 | run: git fetch --prune --unshallow --tags 31 | - uses: actions/setup-node@v1 32 | - name: Install Deps 33 | run: yarn --frozen-lockfile 34 | - name: Build 35 | run: yarn build 36 | - name: Make release 37 | id: release 38 | env: 39 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 40 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 41 | run: | 42 | yarn -s dripip stable --json > result.json 43 | echo '==> Publish Result' 44 | jq '.' < result.json 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_esm 6 | .rts2_cache_umd 7 | .rts2_cache_system 8 | dist 9 | .vscode -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nexus-plugin-auth0 2 | ![npm](https://img.shields.io/npm/v/nexus-plugin-auth0?style=flat-square) 3 | ![npm (tag)](https://img.shields.io/npm/v/nexus-plugin-auth0/next?style=flat-square) 4 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 5 | 6 | 7 | **Contents** 8 | 9 | 10 | 11 | 12 | 13 | - [Installation](#installation) 14 | - [How it Works](#how-it-works) 15 | - [Examples](#examples) 16 | - [Protected Paths](#protected-paths) 17 | - [Usage with **nexus-plugin-shield**](#usage-with-nexus-plugin-shield) 18 | - [Plugin Settings](#plugin-settings) 19 | 20 | 21 | 22 |
23 | 24 | ## Installation 25 | 26 | ``` 27 | npm install nexus-plugin-auth0 28 | ``` 29 | 30 |
31 | 32 | ## How it Works 33 | 34 | The plugin currently expects the "UsersAccessToken" to be in the following format on the header of the incoming request. 35 | 36 | ```json 37 | { 38 | "authorization": "Bearer UsersAccessToken" 39 | } 40 | ``` 41 | 42 | There are two main ways to use this plugin. 43 | 44 | 1. Using the `protectedPaths` to deny access to certain paths. 45 | 1. Using it to only validate and decode then to using the decoded token (available as ctx.token) to control access using another plugin such as `nexus-plugin-sheild` 46 | 47 | The decoded token will be added to Nexus Context under `ctx.token` which has the following type 48 | 49 | ```ts 50 | type DecodedAccessToken = { 51 | iss: string 52 | sub: string 53 | aud: string[] 54 | iat: number 55 | exp: number 56 | azp: string 57 | scope: string 58 | } 59 | // ctx.token 60 | type ContextToken = DecodedAccessToken | null 61 | ``` 62 | 63 | ## Examples 64 | 65 | ### Protected Paths 66 | 67 | If `protectedPaths` is passed, then only valid access tokens will be allowed to access these paths 68 | 69 | ```ts 70 | import { use } from 'nexus' 71 | import { auth } from 'nexus-plugin-auth0' 72 | 73 | use( 74 | auth({ 75 | auth0Audience: 'nexus-plugin-auth0', 76 | auth0Domain: 'graphql-nexus.eu.auth0.com', 77 | protectedPaths: ['Query.posts'], 78 | }) 79 | ) 80 | ``` 81 | 82 | ### Usage with **nexus-plugin-shield** 83 | 84 | All paths will have the decoded token added to `ctx` only if the token is validated but will not deny access. The token can then be used by `nexus-plugin-shield` to control access. 85 | 86 | ```ts 87 | import { use } from 'nexus' 88 | import { auth } from 'nexus-plugin-auth0' 89 | import { rule } from 'nexus-plugin-shield' 90 | 91 | 92 | const isAuthenticated = rule({ cache: 'contextual' })(async (parent, args, ctx: NexusContext, info) => { 93 | const userId = ctx?.token?.sub 94 | return Boolean(userId) 95 | }) 96 | 97 | const rules = { 98 | Query: { 99 | posts: isAuthenticated, 100 | }, 101 | Mutation: { 102 | deletePost: isAuthenticated, 103 | }, 104 | } 105 | 106 | use( 107 | auth({ 108 | auth0Audience: 'nexus-plugin-auth0', 109 | auth0Domain: 'graphql-nexus.eu.auth0.com', 110 | }) 111 | ) 112 | 113 | use( 114 | shield({ 115 | rules, 116 | }) 117 | ) 118 | ``` 119 | 120 | ## Plugin Settings 121 | 122 | ```ts 123 | type Settings = { 124 | auth0Domain: string 125 | auth0Audience: string 126 | protectedPaths?: string[] 127 | debug?: boolean 128 | } 129 | ``` 130 | 131 |
132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nexus-plugin-auth0", 3 | "version": "0.0.0-dripip", 4 | "main": "dist/index.js", 5 | "module": "dist/nexus-plugin-auth0.esm.js", 6 | "description": "A Nexus framework plugin", 7 | "repository": "git@github.com:graphql-nexus/plugin-auth0.git", 8 | "author": "Prisma", 9 | "license": "MIT", 10 | "files": [ 11 | "dist" 12 | ], 13 | "scripts": { 14 | "format": "prettier --write .", 15 | "dev": "tsc --build --watch", 16 | "build:doc": "doctoc README.md --notitle", 17 | "build:ts": "tsc", 18 | "build": "yarn -s build:ts && yarn -s build:doc", 19 | "test": "jest", 20 | "clean": "rm -rf dist", 21 | "publish:stable": "dripip stable", 22 | "publish:preview": "dripip preview", 23 | "publish:pr": "dripip pr", 24 | "prepublishOnly": "yarn -s build" 25 | }, 26 | "prettier": "@prisma-labs/prettier-config", 27 | "jest": { 28 | "preset": "ts-jest", 29 | "testEnvironment": "node", 30 | "watchPlugins": [ 31 | "jest-watch-typeahead/filename", 32 | "jest-watch-typeahead/testname" 33 | ] 34 | }, 35 | "devDependencies": { 36 | "@prisma-labs/prettier-config": "0.1.0", 37 | "@types/async": "3.2.3", 38 | "@types/jsonwebtoken": "8.5.0", 39 | "@types/node": "14.14.2", 40 | "@types/jest": "26.0.15", 41 | "doctoc": "1.4.0", 42 | "dripip": "0.10.0", 43 | "nexus": "0.30.1", 44 | "jest": "26.6.0", 45 | "jest-watch-typeahead": "0.6.1", 46 | "prettier": "2.1.2", 47 | "ts-jest": "26.4.1", 48 | "typescript": "3.9.7" 49 | }, 50 | "dependencies": { 51 | "jwks-rsa": "^1.8.1" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginEntrypoint } from 'nexus/plugin' 2 | import { Settings } from './settings' 3 | 4 | export const auth: PluginEntrypoint = (settings: Settings) => ({ 5 | settings, 6 | packageJsonPath: require.resolve('../package.json'), 7 | runtime: { 8 | module: require.resolve('./runtime'), 9 | export: 'plugin' 10 | } 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /src/runtime.ts: -------------------------------------------------------------------------------- 1 | import { RuntimePlugin, RuntimeLens } from 'nexus/plugin' 2 | import { verify, decode } from 'jsonwebtoken' 3 | import { Settings } from './settings' 4 | import { Auth0Plugin } from './schema' 5 | import jwksClient from 'jwks-rsa' 6 | 7 | export type DecodedAccessToken = { 8 | iss: string, 9 | sub: string, 10 | aud: string[], 11 | iat: number, 12 | exp: number, 13 | azp: string, 14 | scope: string 15 | } 16 | 17 | export const plugin: RuntimePlugin = (settings: Settings) => (project) => { 18 | var plugins = [] 19 | const protectedPaths = settings.protectedPaths 20 | if (protectedPaths) { 21 | plugins.push(Auth0Plugin(protectedPaths)) 22 | } 23 | 24 | return { 25 | context: { 26 | create: async (req: any) => { 27 | if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { 28 | const token = req.headers.authorization.split(' ')[1] 29 | return await verifyToken(project, token, settings) 30 | } 31 | return { 32 | token: null, 33 | } 34 | }, 35 | 36 | typeGen: { 37 | fields: { 38 | token: `{ 39 | iss: string, 40 | sub: string, 41 | aud: string[], 42 | iat: number, 43 | exp: number, 44 | azp: string, 45 | scope: string 46 | } | null`, 47 | } 48 | }, 49 | }, 50 | schema: { 51 | plugins, 52 | }, 53 | } 54 | } 55 | 56 | /** 57 | * Verify a token 58 | * 59 | * @param token 60 | * @param auth0Domain 61 | * @param auth0Audience 62 | * 63 | */ 64 | const verifyToken = async ( 65 | project: RuntimeLens, 66 | token: string, 67 | settings: Settings 68 | ): Promise<{ token: DecodedAccessToken | null }> => { 69 | try { 70 | const client = jwksClient({ 71 | cache: true, 72 | rateLimit: true, 73 | jwksRequestsPerMinute: 5, 74 | strictSsl: true, 75 | jwksUri: `https://${settings.auth0Domain}/.well-known/jwks.json`, 76 | }) 77 | const secret = await getSecret(client, token) 78 | 79 | if (secret) { 80 | const decodedToken = verify(token, secret, { audience: settings.auth0Audience }) 81 | settings.debug && project.log.info(JSON.stringify(decodedToken)) 82 | return { token: decodedToken as DecodedAccessToken } 83 | } else { 84 | return { token: null } 85 | } 86 | } catch (err) { 87 | project.log.error(err) 88 | throw err 89 | } 90 | } 91 | 92 | function getSecret(client: jwksClient.JwksClient, token: string): Promise { 93 | return new Promise(function (resolve, reject) { 94 | const decodedToken = decode(token, { complete: true }) 95 | const header = decodedToken && typeof decodedToken === 'object' && decodedToken['header'] 96 | if (!header || header.alg !== 'RS256') { 97 | reject(new Error('No Header or Incorrect Header Alg, Only RS256 Allowed')) 98 | } 99 | client.getSigningKey(header.kid, (err, key) => { 100 | if (err) { 101 | return reject(err) 102 | } 103 | //@ts-ignore 104 | return resolve(key.publicKey || key.rsaPublicKey) 105 | }) 106 | }) 107 | } 108 | -------------------------------------------------------------------------------- /src/schema.ts: -------------------------------------------------------------------------------- 1 | import { plugin } from '@nexus/schema' 2 | 3 | export function Auth0Plugin(protectedPaths: string[]) { 4 | return plugin({ 5 | name: 'Auth0 Plugin', 6 | description: 'A nexus schema plugin for Auth0', 7 | 8 | onCreateFieldResolver(config) { 9 | return async (root, args, ctx, info, next) => { 10 | const parentType = config.parentTypeConfig.name 11 | 12 | if (parentType != 'Query' && parentType != 'Mutation') { 13 | return await next(root, args, ctx, info) 14 | } 15 | 16 | const resolver = `${parentType}.${config.fieldConfig.name}` 17 | 18 | if (!protectedPaths.includes(resolver)) { 19 | return await next(root, args, ctx, info) 20 | } 21 | if (!ctx.token) { 22 | throw new Error('Not Authorized!') 23 | } 24 | 25 | return await next(root, args, ctx, info) 26 | } 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | export type Settings = { 2 | auth0Domain: string 3 | auth0Audience: string 4 | protectedPaths?: string[] 5 | debug?: boolean 6 | } 7 | -------------------------------------------------------------------------------- /tests/test.spec.ts: -------------------------------------------------------------------------------- 1 | it('works', () => { 2 | expect(true).toEqual(true) 3 | }) 4 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "references": [{ "path": ".." }], 4 | "include": ["."] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "declarationMap": true, 7 | "importHelpers": true, 8 | "module": "commonjs", 9 | "esModuleInterop": true, 10 | "strict": true, 11 | "target": "ES2018" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "outDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"], 9 | "exclude": ["src/**/*.spec.ts"] 10 | } 11 | --------------------------------------------------------------------------------