├── .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 | 
3 | 
4 | [](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 |
--------------------------------------------------------------------------------