├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .mocharc.json ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── settings.json └── tasks.json ├── .yo-rc.json ├── DEVELOPING.md ├── Dockerfile ├── README.md ├── client.js ├── index.js ├── package-lock.json ├── package.json ├── public └── index.html ├── src ├── __tests__ │ ├── README.md │ └── acceptance │ │ ├── home-page.acceptance.ts │ │ ├── ping.controller.acceptance.ts │ │ └── test-helper.ts ├── application.ts ├── authentication-strategies │ ├── auth0.ts │ ├── index.ts │ ├── jwt-service.ts │ └── types.ts ├── controllers │ ├── README.md │ ├── index.ts │ └── ping.controller.ts ├── datasources │ └── README.md ├── index.ts ├── migrate.ts ├── models │ └── README.md ├── repositories │ └── README.md └── sequence.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | /dist 4 | # Cache used by TypeScript's incremental build 5 | *.tsbuildinfo 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@loopback/eslint-config', 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Transpiled JavaScript files from Typescript 61 | /dist 62 | 63 | # Cache used by TypeScript's incremental build 64 | *.tsbuildinfo 65 | 66 | auth0-secret.json 67 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "recursive": true, 3 | "require": "source-map-support/register" 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.json 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "trailingComma": "all" 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [80], 3 | "editor.tabCompletion": "on", 4 | "editor.tabSize": 2, 5 | "editor.trimAutoWhitespace": true, 6 | "editor.formatOnSave": true, 7 | 8 | "files.exclude": { 9 | "**/.DS_Store": true, 10 | "**/.git": true, 11 | "**/.hg": true, 12 | "**/.svn": true, 13 | "**/CVS": true, 14 | "dist": true, 15 | }, 16 | "files.insertFinalNewline": true, 17 | "files.trimTrailingWhitespace": true, 18 | 19 | "typescript.tsdk": "./node_modules/typescript/lib", 20 | "eslint.run": "onSave", 21 | "eslint.nodePath": "./node_modules", 22 | "eslint.validate": [ 23 | "javascript", 24 | "typescript", 25 | ], 26 | "editor.codeActionsOnSave": { 27 | "source.fixAll.eslint": true, 28 | "source.organizeImports": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Watch and Compile Project", 8 | "type": "shell", 9 | "command": "npm", 10 | "args": ["--silent", "run", "build:watch"], 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | }, 15 | "problemMatcher": "$tsc-watch" 16 | }, 17 | { 18 | "label": "Build, Test and Lint", 19 | "type": "shell", 20 | "command": "npm", 21 | "args": ["--silent", "run", "test:dev"], 22 | "group": { 23 | "kind": "test", 24 | "isDefault": true 25 | }, 26 | "problemMatcher": ["$tsc", "$eslint-compact", "$eslint-stylish"] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@loopback/cli": { 3 | "version": "1.23.1" 4 | } 5 | } -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # Developer's Guide 2 | 3 | We use Visual Studio Code for developing LoopBack and recommend the same to our 4 | users. 5 | 6 | ## VSCode setup 7 | 8 | Install the following extensions: 9 | 10 | - [eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) 11 | - [prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 12 | 13 | ## Development workflow 14 | 15 | ### Visual Studio Code 16 | 17 | 1. Start the build task (Cmd+Shift+B) to run TypeScript compiler in the 18 | background, watching and recompiling files as you change them. Compilation 19 | errors will be shown in the VSCode's "PROBLEMS" window. 20 | 21 | 2. Execute "Run Rest Task" from the Command Palette (Cmd+Shift+P) to re-run the 22 | test suite and lint the code for both programming and style errors. Linting 23 | errors will be shown in VSCode's "PROBLEMS" window. Failed tests are printed 24 | to terminal output only. 25 | 26 | ### Other editors/IDEs 27 | 28 | 1. Open a new terminal window/tab and start the continous build process via 29 | `npm run build:watch`. It will run TypeScript compiler in watch mode, 30 | recompiling files as you change them. Any compilation errors will be printed 31 | to the terminal. 32 | 33 | 2. In your main terminal window/tab, run `npm run test:dev` to re-run the test 34 | suite and lint the code for both programming and style errors. You should run 35 | this command manually whenever you have new changes to test. Test failures 36 | and linter errors will be printed to the terminal. 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Check out https://hub.docker.com/_/node to select a new base image 2 | FROM node:10-slim 3 | 4 | # Set to a non-root built-in user `node` 5 | USER node 6 | 7 | # Create app directory (with user `node`) 8 | RUN mkdir -p /home/node/app 9 | 10 | WORKDIR /home/node/app 11 | 12 | # Install app dependencies 13 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 14 | # where available (npm@5+) 15 | COPY --chown=node package*.json ./ 16 | 17 | RUN npm install 18 | 19 | # Bundle app source code 20 | COPY --chown=node . . 21 | 22 | RUN npm run build 23 | 24 | # Bind to all network interfaces so that it can be mapped to the host OS 25 | ENV HOST=0.0.0.0 PORT=3000 26 | 27 | EXPOSE ${PORT} 28 | CMD [ "node", "." ] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback4-example-auth0 2 | 3 | This module contains an example application to use [Auth0](https://auth0.com/) for JWT based authentication. 4 | 5 | ## Define an authentication strategy for Auth0 6 | 7 | - src/authentication-strategies/auth0.ts 8 | 9 | ```ts 10 | export class JWTAuthenticationStrategy implements AuthenticationStrategy { 11 | name = 'auth0-jwt'; 12 | 13 | constructor( 14 | @inject(RestBindings.Http.RESPONSE) 15 | private response: Response, 16 | @inject(AuthenticationBindings.METADATA) 17 | private metadata: AuthenticationMetadata, 18 | @config() 19 | private options: Auth0Config, 20 | ) {} 21 | 22 | async authenticate(request: Request): Promise { 23 | // ... 24 | } 25 | ``` 26 | 27 | ## Register the authentication strategy 28 | 29 | Add the following code to `src/application.ts`: 30 | 31 | ```ts 32 | export class Loopback4ExampleAuth0Application extends BootMixin( 33 | ServiceMixin(RepositoryMixin(RestApplication)), 34 | ) { 35 | constructor(options: ApplicationConfig = {}) { 36 | super(options); 37 | 38 | // Bind authentication component related elements 39 | this.component(AuthenticationComponent); 40 | 41 | this.service(JWTServiceProvider); 42 | 43 | // Register the Auth0 JWT authentication strategy 44 | registerAuthenticationStrategy(this, JWTAuthenticationStrategy); 45 | this.configure(KEY).to({ 46 | jwksUri: 'https://apitoday.auth0.com/.well-known/jwks.json', 47 | audience: 'http://localhost:3000/ping', 48 | issuer: 'https://apitoday.auth0.com/', 49 | algorithms: ['RS256'], 50 | }); 51 | 52 | // Set up the custom sequence 53 | this.sequence(MySequence); 54 | } 55 | 56 | // ... 57 | } 58 | ``` 59 | 60 | ## Decorate a method to apply Auth0 JWT authentication 61 | 62 | Add a method to `src/controllers/ping.controller.ts`: 63 | 64 | ```ts 65 | @authenticate('auth0-jwt', {scopes: ['greet']}) 66 | async greet( 67 | @inject(SecurityBindings.USER) 68 | currentUserProfile: UserProfile, 69 | ): Promise { 70 | // (@jannyHou)FIXME: explore a way to generate OpenAPI schema 71 | // for symbol property 72 | currentUserProfile.id = currentUserProfile[securityId]; 73 | delete currentUserProfile[securityId]; 74 | return currentUserProfile; 75 | } 76 | } 77 | ``` 78 | 79 | ## Give it try 80 | 81 | 1. Start the server 82 | 83 | ```sh 84 | npm run start 85 | ``` 86 | 87 | 2. Run the client 88 | 89 | First add credentials to `auth0-secret.json`: 90 | 91 | ```json 92 | { 93 | "client_id": "client-id from auth0", 94 | "client_secret": "client-secret from auth0", 95 | "audience": "http://localhost:3000/ping" 96 | } 97 | ``` 98 | 99 | ```sh 100 | node client 101 | ``` 102 | 103 | You should see messages like: 104 | 105 | ``` 106 | node client 107 | { 108 | access_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IlFrSXhORU0wTkVRME9FRXdOa0k0TURBelJqTTJPVVl6TVRrMk9ESTVRelkxTXpsRk5rTTBNZyJ9.eyJpc3MiOiJodHRwczovL2FwaXRvZGF5LmF1dGgwLmNvbS8iLCJzdWIiOiJFVHJqSHpGY0VoRWt3ZTZvSHlZOGlUWDZKU3MxVnA0M0BjbGllbnRzIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwL3BpbmciLCJpYXQiOjE1NzAyMjA4NDEsImV4cCI6MTU3MDMwNzI0MSwiYXpwIjoiRVRyakh6RmNFaEVrd2U2b0h5WThpVFg2SlNzMVZwNDMiLCJzY29wZSI6ImdyZWV0IiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.h8yrNQSU5FqdzTjFYawV_nUBv43hHeSghCJOdupfHR5lM_kvGq5MnEfsX6-oGcUy3c0d8YD5lW8aBVcuSsM34Qtt-hbhqkWMqaicM4TldfiOWoEnu_lYIF4z6ybUqxxUWX0VE0DDl6sfPtmKqftm2u30ndHCxOb_2nEg_mon9Wmp8kRIpWIKAVLrQ8wObdCwnjIinK5h2HlWe0z53hPBKpaBeLW1y3uWbWAJ2UUOuEfK2Bn3KQ9TpO-mwnuvU7R0Z2IkYrf567wR3Xe3lDKY2lKeUFuXiDhlWpPcU_3vBJfsCs61BQoe4h5MZV0tQmdUJxdsqg4KYmpf_RKGd2djWw', 109 | expires_in: 86400, 110 | token_type: 'Bearer' 111 | } 112 | {"iss":"https://apitoday.auth0.com/","sub":"ETrjHzFcEhEkwe6oHyY8iTX6JSs1Vp43@clients","aud":"http://localhost:3000/ping","iat":1570220841,"exp":1570307241,"azp":"ETrjHzFcEhEkwe6oHyY8iTX6JSs1Vp43","scope":"greet","gty":"client-credentials"} 113 | 114 | ``` 115 | -------------------------------------------------------------------------------- /client.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | /** 4 | * Please add `auth0-secret.json` for your credentials 5 | * @example 6 | * ```json 7 | * { 8 | * "client_id": "{CLIENT_ID}", 9 | * "client_secret": "{CLIENT_SECRET}", 10 | * "audience": "http://localhost:3000/ping" 11 | * } 12 | * ``` 13 | */ 14 | const secrets = require('./auth0-secret'); 15 | 16 | /** 17 | * Request an access token using client credentials 18 | */ 19 | const tokenReq = { 20 | method: 'POST', 21 | url: 'https://apitoday.auth0.com/oauth/token', 22 | headers: {'content-type': 'application/json'}, 23 | responseType: 'json', 24 | data: { 25 | ...secrets, 26 | // eslint-disable-next-line @typescript-eslint/naming-convention 27 | grant_type: 'client_credentials', 28 | scope: 'greet', 29 | }, 30 | }; 31 | 32 | async function main() { 33 | let res = await axios(tokenReq); 34 | console.log(res.data); 35 | 36 | /** 37 | * Now try to run the /greet api using the access token 38 | */ 39 | const greetReq = { 40 | method: 'GET', 41 | url: 'http://localhost:3000/greet', 42 | headers: { 43 | authorization: `Bearer ${res.data.access_token}`, 44 | }, 45 | }; 46 | 47 | res = await axios(greetReq); 48 | 49 | console.log(res.data); 50 | } 51 | 52 | main().catch((err) => { 53 | console.error(err); 54 | process.exit(1); 55 | }); 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const application = require('./dist'); 2 | 3 | module.exports = application; 4 | 5 | if (require.main === module) { 6 | // Run the application 7 | const config = { 8 | rest: { 9 | port: +(process.env.PORT || 3000), 10 | host: process.env.HOST, 11 | openApiSpec: { 12 | // useful when used with OpenAPI-to-GraphQL to locate your application 13 | setServersFromRequest: true, 14 | }, 15 | }, 16 | }; 17 | application.main(config).catch((err) => { 18 | console.error('Cannot start the application.', err); 19 | process.exit(1); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback4-example-auth0", 3 | "version": "1.0.0", 4 | "description": "loopback4-example-auth0", 5 | "keywords": [ 6 | "loopback-application", 7 | "loopback" 8 | ], 9 | "main": "index.js", 10 | "types": "dist/index.d.ts", 11 | "engines": { 12 | "node": ">=8.9" 13 | }, 14 | "scripts": { 15 | "build": "lb-tsc", 16 | "build:watch": "lb-tsc --watch", 17 | "clean": "lb-clean dist *.tsbuildinfo", 18 | "lint": "npm run prettier:check && npm run eslint", 19 | "lint:fix": "npm run eslint:fix && npm run prettier:fix", 20 | "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"", 21 | "prettier:check": "npm run prettier:cli -- -l", 22 | "prettier:fix": "npm run prettier:cli -- --write", 23 | "eslint": "lb-eslint --report-unused-disable-directives .", 24 | "eslint:fix": "npm run eslint -- --fix", 25 | "pretest": "npm run clean && npm run build", 26 | "test": "lb-mocha --allow-console-logs \"dist/__tests__\"", 27 | "posttest": "npm run lint", 28 | "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", 29 | "docker:build": "docker build -t loopback4-example-auth0 .", 30 | "docker:run": "docker run -p 3000:3000 -d loopback4-example-auth0", 31 | "migrate": "node ./dist/migrate", 32 | "prestart": "npm run build", 33 | "start": "node -r source-map-support/register .", 34 | "prepublishOnly": "npm run test" 35 | }, 36 | "repository": { 37 | "type": "git" 38 | }, 39 | "author": "Raymond Feng ", 40 | "license": "MIT", 41 | "files": [ 42 | "README.md", 43 | "index.js", 44 | "index.d.ts", 45 | "dist", 46 | "src", 47 | "!*/__tests__" 48 | ], 49 | "dependencies": { 50 | "@loopback/authentication": "^6.0.1", 51 | "@loopback/boot": "^2.5.1", 52 | "@loopback/core": "^2.9.5", 53 | "@loopback/repository": "^2.11.2", 54 | "@loopback/rest": "^6.2.0", 55 | "@loopback/rest-explorer": "^2.2.10", 56 | "@loopback/service-proxy": "^2.3.8", 57 | "axios": "^0.20.0", 58 | "express-jwt": "^6.0.0", 59 | "express-jwt-authz": "^2.4.1", 60 | "jwks-rsa": "^1.9.0" 61 | }, 62 | "devDependencies": { 63 | "@loopback/build": "^6.2.2", 64 | "@loopback/eslint-config": "^9.0.2", 65 | "@loopback/testlab": "^3.2.4", 66 | "@types/node": "^10.17.28", 67 | "@typescript-eslint/eslint-plugin": "^4.1.0", 68 | "@typescript-eslint/parser": "^4.1.0", 69 | "eslint": "^7.7.0", 70 | "eslint-config-prettier": "^6.11.0", 71 | "eslint-plugin-eslint-plugin": "^2.3.0", 72 | "eslint-plugin-mocha": "^8.0.0", 73 | "request": "^2.88.2", 74 | "source-map-support": "^0.5.19", 75 | "typescript": "~4.0.2" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | loopback4-example-auth0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 55 | 56 | 57 | 58 |
59 |

loopback4-example-auth0

60 |

Version 1.0.0

61 | 62 |

OpenAPI spec: /openapi.json

63 |

API Explorer: /explorer

64 |
65 | 66 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/__tests__/README.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | Please place your tests in this folder. 4 | -------------------------------------------------------------------------------- /src/__tests__/acceptance/home-page.acceptance.ts: -------------------------------------------------------------------------------- 1 | import {Client} from '@loopback/testlab'; 2 | import {Loopback4ExampleAuth0Application} from '../..'; 3 | import {setupApplication} from './test-helper'; 4 | 5 | describe('HomePage', () => { 6 | let app: Loopback4ExampleAuth0Application; 7 | let client: Client; 8 | 9 | before('setupApplication', async () => { 10 | ({app, client} = await setupApplication()); 11 | }); 12 | 13 | after(async () => { 14 | await app.stop(); 15 | }); 16 | 17 | it('exposes a default home page', async () => { 18 | await client 19 | .get('/') 20 | .expect(200) 21 | .expect('Content-Type', /text\/html/); 22 | }); 23 | 24 | it('exposes self-hosted explorer', async () => { 25 | await client 26 | .get('/explorer/') 27 | .expect(200) 28 | .expect('Content-Type', /text\/html/) 29 | .expect(/LoopBack API Explorer/); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/__tests__/acceptance/ping.controller.acceptance.ts: -------------------------------------------------------------------------------- 1 | import {Client, expect} from '@loopback/testlab'; 2 | import {Loopback4ExampleAuth0Application} from '../..'; 3 | import {setupApplication} from './test-helper'; 4 | 5 | describe('PingController', () => { 6 | let app: Loopback4ExampleAuth0Application; 7 | let client: Client; 8 | 9 | before('setupApplication', async () => { 10 | ({app, client} = await setupApplication()); 11 | }); 12 | 13 | after(async () => { 14 | await app.stop(); 15 | }); 16 | 17 | it('invokes GET /ping', async () => { 18 | const res = await client.get('/ping?msg=world').expect(200); 19 | expect(res.body).to.containEql({greeting: 'Hello from LoopBack'}); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/__tests__/acceptance/test-helper.ts: -------------------------------------------------------------------------------- 1 | import {Loopback4ExampleAuth0Application} from '../..'; 2 | import { 3 | createRestAppClient, 4 | givenHttpServerConfig, 5 | Client, 6 | } from '@loopback/testlab'; 7 | 8 | export async function setupApplication(): Promise<AppWithClient> { 9 | const restConfig = givenHttpServerConfig({ 10 | // Customize the server configuration here. 11 | // Empty values (undefined, '') will be ignored by the helper. 12 | // 13 | // host: process.env.HOST, 14 | // port: +process.env.PORT, 15 | }); 16 | 17 | const app = new Loopback4ExampleAuth0Application({ 18 | rest: restConfig, 19 | }); 20 | 21 | await app.boot(); 22 | await app.start(); 23 | 24 | const client = createRestAppClient(app); 25 | 26 | return {app, client}; 27 | } 28 | 29 | export interface AppWithClient { 30 | app: Loopback4ExampleAuth0Application; 31 | client: Client; 32 | } 33 | -------------------------------------------------------------------------------- /src/application.ts: -------------------------------------------------------------------------------- 1 | import {BootMixin} from '@loopback/boot'; 2 | import {ApplicationConfig} from '@loopback/core'; 3 | import { 4 | RestExplorerBindings, 5 | RestExplorerComponent, 6 | } from '@loopback/rest-explorer'; 7 | import {RepositoryMixin} from '@loopback/repository'; 8 | import {RestApplication} from '@loopback/rest'; 9 | import {ServiceMixin} from '@loopback/service-proxy'; 10 | import * as path from 'path'; 11 | import {MySequence} from './sequence'; 12 | import {JWTAuthenticationStrategy, KEY} from './authentication-strategies'; 13 | import { 14 | registerAuthenticationStrategy, 15 | AuthenticationComponent, 16 | } from '@loopback/authentication'; 17 | import {JWTServiceProvider} from './authentication-strategies/jwt-service'; 18 | 19 | export class Loopback4ExampleAuth0Application extends BootMixin( 20 | ServiceMixin(RepositoryMixin(RestApplication)), 21 | ) { 22 | constructor(options: ApplicationConfig = {}) { 23 | super(options); 24 | 25 | // Bind authentication component related elements 26 | this.component(AuthenticationComponent); 27 | 28 | this.service(JWTServiceProvider); 29 | 30 | // Register the Auth0 JWT authentication strategy 31 | registerAuthenticationStrategy(this, JWTAuthenticationStrategy); 32 | this.configure(KEY).to({ 33 | jwksUri: 'https://apitoday.auth0.com/.well-known/jwks.json', 34 | audience: 'http://localhost:3000/ping', 35 | issuer: 'https://apitoday.auth0.com/', 36 | algorithms: ['RS256'], 37 | }); 38 | 39 | // Set up the custom sequence 40 | this.sequence(MySequence); 41 | 42 | // Set up default home page 43 | this.static('/', path.join(__dirname, '../public')); 44 | 45 | // Customize @loopback/rest-explorer configuration here 46 | this.bind(RestExplorerBindings.CONFIG).to({ 47 | path: '/explorer', 48 | }); 49 | this.component(RestExplorerComponent); 50 | 51 | this.projectRoot = __dirname; 52 | // Customize @loopback/boot Booter Conventions here 53 | this.bootOptions = { 54 | controllers: { 55 | // Customize ControllerBooter Conventions here 56 | dirs: ['controllers'], 57 | extensions: ['.controller.js'], 58 | nested: true, 59 | }, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/authentication-strategies/auth0.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthenticationBindings, 3 | AuthenticationMetadata, 4 | AuthenticationStrategy, 5 | } from '@loopback/authentication'; 6 | import {inject} from '@loopback/core'; 7 | import { 8 | ExpressRequestHandler, 9 | Request, 10 | Response, 11 | RestBindings, 12 | } from '@loopback/rest'; 13 | import {UserProfile} from '@loopback/security'; 14 | import {JWT_SERVICE} from './types'; 15 | 16 | const jwtAuthz = require('express-jwt-authz'); 17 | 18 | export class JWTAuthenticationStrategy implements AuthenticationStrategy { 19 | name = 'auth0-jwt'; 20 | 21 | constructor( 22 | @inject(RestBindings.Http.RESPONSE) 23 | private response: Response, 24 | @inject(AuthenticationBindings.METADATA) 25 | private metadata: AuthenticationMetadata, 26 | @inject(JWT_SERVICE) 27 | private jwtCheck: ExpressRequestHandler, 28 | ) {} 29 | 30 | async authenticate(request: Request): Promise<UserProfile | undefined> { 31 | return new Promise<UserProfile | undefined>((resolve, reject) => { 32 | this.jwtCheck(request, this.response, (err: unknown) => { 33 | if (err) { 34 | console.error(err); 35 | reject(err); 36 | return; 37 | } 38 | // If the `@authenticate` requires `scopes` check 39 | if (this.metadata.options && this.metadata.options.scopes) { 40 | jwtAuthz(this.metadata.options!.scopes, {failWithError: true})( 41 | request, 42 | this.response, 43 | (err2?: Error) => { 44 | if (err2) { 45 | console.error(err2); 46 | reject(err2); 47 | return; 48 | } 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 50 | resolve((request as any).user); 51 | }, 52 | ); 53 | } else { 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | resolve((request as any).user); 56 | } 57 | }); 58 | }); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/authentication-strategies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './jwt-service'; 3 | export * from './auth0'; 4 | -------------------------------------------------------------------------------- /src/authentication-strategies/jwt-service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | bind, 3 | BindingScope, 4 | config, 5 | ContextTags, 6 | Provider, 7 | } from '@loopback/core'; 8 | import jwt, {RequestHandler} from 'express-jwt'; 9 | import {Auth0Config, JWT_SERVICE, KEY} from './types'; 10 | 11 | const jwks = require('jwks-rsa'); 12 | 13 | @bind({tags: {[ContextTags.KEY]: JWT_SERVICE}, scope: BindingScope.SINGLETON}) 14 | export class JWTServiceProvider implements Provider<RequestHandler> { 15 | constructor( 16 | @config({fromBinding: KEY}) 17 | private options: Auth0Config, 18 | ) {} 19 | 20 | value() { 21 | const auth0Config = this.options || {}; 22 | // Use `express-jwt` to verify the Auth0 JWT token 23 | return jwt({ 24 | secret: jwks.expressJwtSecret({ 25 | cache: true, 26 | rateLimit: true, 27 | jwksRequestsPerMinute: 5, 28 | jwksUri: auth0Config.jwksUri, 29 | }), 30 | audience: auth0Config.audience, 31 | issuer: auth0Config.issuer, 32 | algorithms: auth0Config.algorithms || ['RS256'], 33 | // Customize `getToken` to allow `access_token` query string in addition 34 | // to `Authorization` header 35 | getToken: (req) => { 36 | if ( 37 | req.headers.authorization && 38 | req.headers.authorization.split(' ')[0] === 'Bearer' 39 | ) { 40 | return req.headers.authorization.split(' ')[1]; 41 | } else if (req.query && req.query.access_token) { 42 | return req.query.access_token; 43 | } 44 | return null; 45 | }, 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/authentication-strategies/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthenticationBindings, 3 | AuthenticationStrategy, 4 | } from '@loopback/authentication'; 5 | import {BindingKey} from '@loopback/core'; 6 | import jwt from 'express-jwt'; 7 | 8 | export interface Auth0Config { 9 | jwksUri: string; // 'https://apitoday.auth0.com/.well-known/jwks.json', 10 | audience: string; // 'http://localhost:3000/ping', 11 | issuer: string; // 'https://apitoday.auth0.com/'; 12 | algorithms: string[]; // ['RS256'], 13 | } 14 | 15 | export const JWT_SERVICE = BindingKey.create<jwt.RequestHandler>( 16 | 'services.JWTService', 17 | ); 18 | 19 | export const KEY = BindingKey.create<AuthenticationStrategy>( 20 | `${AuthenticationBindings.AUTHENTICATION_STRATEGY_EXTENSION_POINT_NAME}.JWTAuthenticationStrategy`, 21 | ); 22 | -------------------------------------------------------------------------------- /src/controllers/README.md: -------------------------------------------------------------------------------- 1 | # Controllers 2 | 3 | This directory contains source files for the controllers exported by this app. 4 | 5 | To add a new empty controller, type in `lb4 controller [<name>]` from the 6 | command-line of your application's root directory. 7 | 8 | For more information, please visit 9 | [Controller generator](http://loopback.io/doc/en/lb4/Controller-generator.html). 10 | -------------------------------------------------------------------------------- /src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ping.controller'; 2 | -------------------------------------------------------------------------------- /src/controllers/ping.controller.ts: -------------------------------------------------------------------------------- 1 | import {authenticate} from '@loopback/authentication'; 2 | import {inject} from '@loopback/core'; 3 | import {get, Request, ResponseObject, RestBindings} from '@loopback/rest'; 4 | import {SecurityBindings, securityId, UserProfile} from '@loopback/security'; 5 | 6 | /** 7 | * OpenAPI response for ping() 8 | */ 9 | const PING_RESPONSE: ResponseObject = { 10 | description: 'Ping Response', 11 | content: { 12 | 'application/json': { 13 | schema: { 14 | type: 'object', 15 | properties: { 16 | greeting: {type: 'string'}, 17 | date: {type: 'string'}, 18 | url: {type: 'string'}, 19 | headers: { 20 | type: 'object', 21 | properties: { 22 | 'Content-Type': {type: 'string'}, 23 | }, 24 | additionalProperties: true, 25 | }, 26 | }, 27 | }, 28 | }, 29 | }, 30 | }; 31 | 32 | export const UserProfileSchema = { 33 | type: 'object', 34 | required: ['id'], 35 | properties: { 36 | id: {type: 'string'}, 37 | email: {type: 'string'}, 38 | name: {type: 'string'}, 39 | }, 40 | }; 41 | 42 | /** 43 | * A simple controller to bounce back http requests 44 | */ 45 | export class PingController { 46 | constructor(@inject(RestBindings.Http.REQUEST) private req: Request) {} 47 | 48 | // Map to `GET /ping` 49 | @get('/ping', { 50 | responses: { 51 | '200': PING_RESPONSE, 52 | }, 53 | }) 54 | ping(): object { 55 | // Reply with a greeting, the current time, the url, and request headers 56 | return { 57 | greeting: 'Hello from LoopBack', 58 | date: new Date(), 59 | url: this.req.url, 60 | headers: Object.assign({}, this.req.headers), 61 | }; 62 | } 63 | 64 | @get('/greet', { 65 | responses: { 66 | '200': { 67 | description: 'Greet the logged in user', 68 | content: { 69 | 'application/json': { 70 | schema: UserProfileSchema, 71 | }, 72 | }, 73 | }, 74 | }, 75 | }) 76 | @authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}}) 77 | async greet( 78 | @inject(SecurityBindings.USER) 79 | currentUserProfile: UserProfile, 80 | ): Promise<Partial<UserProfile>> { 81 | // (@jannyHou)FIXME: explore a way to generate OpenAPI schema 82 | // for symbol property 83 | currentUserProfile.id = currentUserProfile[securityId]; 84 | const user: Partial<UserProfile> = {...currentUserProfile}; 85 | delete user[securityId]; 86 | return user; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/datasources/README.md: -------------------------------------------------------------------------------- 1 | # Datasources 2 | 3 | This directory contains config for datasources used by this app. 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Loopback4ExampleAuth0Application} from './application'; 2 | import {ApplicationConfig} from '@loopback/core'; 3 | 4 | export {Loopback4ExampleAuth0Application}; 5 | 6 | export async function main(options: ApplicationConfig = {}) { 7 | const app = new Loopback4ExampleAuth0Application(options); 8 | await app.boot(); 9 | await app.start(); 10 | 11 | const url = app.restServer.url; 12 | console.log(`Server is running at ${url}`); 13 | console.log(`Try ${url}/ping`); 14 | 15 | return app; 16 | } 17 | -------------------------------------------------------------------------------- /src/migrate.ts: -------------------------------------------------------------------------------- 1 | import {Loopback4ExampleAuth0Application} from './application'; 2 | 3 | export async function migrate(args: string[]) { 4 | const existingSchema = args.includes('--rebuild') ? 'drop' : 'alter'; 5 | console.log('Migrating schemas (%s existing schema)', existingSchema); 6 | 7 | const app = new Loopback4ExampleAuth0Application(); 8 | await app.boot(); 9 | await app.migrateSchema({existingSchema}); 10 | 11 | // Connectors usually keep a pool of opened connections, 12 | // this keeps the process running even after all work is done. 13 | // We need to exit explicitly. 14 | process.exit(0); 15 | } 16 | 17 | migrate(process.argv).catch((err) => { 18 | console.error('Cannot migrate database schema', err); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /src/models/README.md: -------------------------------------------------------------------------------- 1 | # Models 2 | 3 | This directory contains code for models provided by this app. 4 | -------------------------------------------------------------------------------- /src/repositories/README.md: -------------------------------------------------------------------------------- 1 | # Repositories 2 | 3 | This directory contains code for repositories provided by this app. 4 | -------------------------------------------------------------------------------- /src/sequence.ts: -------------------------------------------------------------------------------- 1 | import {MiddlewareSequence} from '@loopback/rest'; 2 | 3 | export class MySequence extends MiddlewareSequence {} 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/tsconfig", 3 | "extends": "@loopback/build/config/tsconfig.common.json", 4 | "compilerOptions": { 5 | "outDir": "dist", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"] 9 | } 10 | --------------------------------------------------------------------------------