├── Makefile
├── public
├── custom.css
├── callback.js
└── main.css
├── lib
├── handlers
│ ├── index.js
│ ├── configuration.js
│ ├── invite.js
│ ├── auth_api.js
│ ├── login.js
│ └── bootstrap.js
├── pkce.js
├── jar.js
├── api2.js
├── env.js
├── constants.js
└── client_authentication.js
├── .env.example
├── views
├── error.jade
├── samluser.jade
├── menusaml.jade
├── callback.jade
├── layout.jade
├── menu.jade
├── user.jade
└── index.jade
├── .vscode
└── launch.json
├── README.md
├── package.json
├── scripts
└── reset_tenant.js
├── .gitignore
├── server.js
├── app.js
├── bin
└── www
└── routes
├── user.js
└── index.js
/Makefile:
--------------------------------------------------------------------------------
1 | start-local:
2 | npm run start
--------------------------------------------------------------------------------
/public/custom.css:
--------------------------------------------------------------------------------
1 | img.user-picture {
2 | width: 25px;
3 | height: 25px;
4 | vertical-align: middle;
5 | }
6 |
--------------------------------------------------------------------------------
/lib/handlers/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | auth_api: require('./auth_api'),
3 | invite: require('./invite'),
4 | login: require('./login'),
5 | configuration: require('./configuration')
6 | };
7 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | AUTH0_MGMT_CLIENT_ID=some-other-client-id
2 | AUTH0_MGMT_CLIENT_SECRET=some-other-client-secret
3 | AUTH0_DOMAIN=your-tenant.auth0.com
4 | APP_JAR_KEY_ALG=RS256
5 | APP_CLIENT_AUTHENTICATION_METHOD=client_secret_post
6 | AUTH0_MGMT_CLIENT_AUTHENTICATION_METHOD=client_secret_post
7 | ENABLE_JAR=false
--------------------------------------------------------------------------------
/views/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | div(class="row mt-3")
5 | div(class="col-sm-12")
6 | div(class="card")
7 | h5(class="card-header") An error occurred during login.
8 | div(class="card-body")
9 | h5(class="card-title") #{error}
10 | p(class="card-text") #{error_description}
11 | a(href="/")
12 | button(id="apiCallButton" class="btn btn-danger") Home
13 |
--------------------------------------------------------------------------------
/views/samluser.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 | block content
3 | include menusaml
4 | p
5 | h1 Welcome #{user.profile.email}!
6 | div(class="card")
7 | div(class="card-header" id="headingOne")
8 | h5(class="mb-0")
9 | User Profile SAML Response
10 | div(id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion")
11 | div(class="card-body")
12 | pre
13 | code #{JSON.stringify(samlProfile, null, 4)}
14 |
--------------------------------------------------------------------------------
/views/menusaml.jade:
--------------------------------------------------------------------------------
1 | nav(class="navbar navbar-expand-sm navbar-dark bg-primary")
2 | a(class="navbar-brand") Fake SaaS SAML App Demo
3 | ul(class="navbar-nav")
4 | li(class="nav-item active")
5 | a(href="/" class="nav-link") Go Home Without Logout
6 | li(class="nav-item active")
7 | a(href='https://#{config.AUTH0_DOMAIN}/v2/logout?returnTo=#{config.APP_LOGOUT_URL}&client_id=#{config.APP_CLIENT_ID}' class="nav-link") Logout
8 | |
9 | |
10 | span(class="navbar-text") Logged in as #{user.profile.email}
11 |
--------------------------------------------------------------------------------
/lib/pkce.js:
--------------------------------------------------------------------------------
1 | const crypto = require("crypto");
2 | const base64url = require("base64url");
3 |
4 | module.exports = {
5 | generateCodeVerifier() {
6 | const unencodedVerifier = crypto.randomBytes(32);
7 | return base64url.encode(unencodedVerifier);
8 | },
9 |
10 | generateCodeChallengeFromVerifier(codeChallengeMethod, codeChallenge) {
11 | if (codeChallengeMethod === "plain") {
12 | return codeChallenge;
13 | }
14 | return base64url.encode(crypto.createHash("sha256").update(codeChallenge, "utf-8").digest());
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/views/callback.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | div(class="jumbotron")
5 | h1(class="display-4")= title
6 | form(name="callback_form" method="post" action="/callback")
7 | input(type="text", name="error")
8 | input(type="text", name="error_description")
9 | input(type="text", name="code")
10 | input(type="text", name="state")
11 | input(type="text", name="access_token")
12 | input(type="text", name="id_token")
13 | input(type="text", name="response")
14 | input(type="submit" value="Redirect" class="btn btn-primary")
15 |
--------------------------------------------------------------------------------
/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title= title
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | body
13 | div(class="container")
14 | block content
15 |
--------------------------------------------------------------------------------
/views/menu.jade:
--------------------------------------------------------------------------------
1 | nav(class="navbar navbar-expand-sm navbar-dark bg-primary")
2 | a(class="navbar-brand") Fake SaaS App Demo
3 | ul(class="navbar-nav")
4 | li(class="nav-item active")
5 | a(href="/" class="nav-link") Go Home Without Logout
6 | li(class="nav-item active")
7 | a(href='https://#{config.AUTH0_DOMAIN}/v2/logout?returnTo=#{config.APP_LOGOUT_URL}&client_id=#{config.APP_CLIENT_ID}' class="nav-link") Logout
8 | li(class="nav-item active")
9 | a(href="/user/refresh" class="nav-link") Refresh Tokens
10 | li(class="nav-item active")
11 | a(href="/user/userinfo" class="nav-link") Call Userinfo
12 | |
13 | |
14 | span(class="navbar-text") Logged in as #{user.profile.nickname}
15 | img(id="userPicture" class="user-picture" src="#{user.profile.picture}")
16 |
--------------------------------------------------------------------------------
/lib/jar.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 | const { getEnv } = require("./env");
3 |
4 | const createJARPayload = (params) => {
5 | const {
6 | APP_JAR_KEY_ID,
7 | APP_JAR_PRIVATE_KEY,
8 | AUTH0_DOMAIN,
9 | } = getEnv();
10 | const client_id = params.client_id;
11 |
12 | const assertion = jwt.sign(
13 | {
14 | ...params,
15 | jti: "" + Date.now(),
16 | iat: Math.floor(Date.now() / 1000),
17 | nbf: Math.floor(Date.now() / 1000),
18 | },
19 | APP_JAR_PRIVATE_KEY,
20 | {
21 | audience: `https://${AUTH0_DOMAIN}/`,
22 | issuer: client_id,
23 | subject: client_id,
24 | keyid: APP_JAR_KEY_ID,
25 | algorithm: "PS256",
26 | expiresIn: "1m",
27 | }
28 | );
29 | return { request: assertion, client_id: client_id };
30 | };
31 |
32 | exports.createJARPayload = createJARPayload;
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Attach Local",
9 | "type": "node",
10 | "request": "attach",
11 | "port": 7370,
12 | "address": "127.0.0.1",
13 | "restart": false,
14 | "sourceMaps": false,
15 | "outFiles": [],
16 | "remoteRoot": "${workspaceRoot}",
17 | "localRoot": "${workspaceRoot}",
18 | "protocol": "inspector",
19 | "skipFiles": [
20 | "!**/node_modules/**",
21 | "**/$KNOWN_TOOLS$/**",
22 | "/**",
23 | "/internal/async_hooks.js",
24 | "/internal/inspector_async_hook.js"
25 | ]
26 | }
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # demozero-token-demo
2 |
3 | ## Setup
4 |
5 | - `npm install`
6 | - Copy .env.example to .env and set the appropriate values.
7 | - The Management API client (M2M app) that you use must have at minimum read:client_grants and update:client_grants scopes assigned via a client grant for the API2 resource server in your tenant. This is the minimum setup that is required. The app will automatically bootstrap the rest.
8 | - Add myapp.com to your /etc/hosts, mapped to 127.0.0.1.
9 | - Create self-signed cert as described here https://bit.ly/3oj6t9u. Save as server.key and server.cert in the root directory of the application.
10 | - Now you can navigate to the app at https://myapp.com:4040/
11 | ## Running the example
12 |
13 | Use `npm start` to run the project.
14 |
15 | ## Reset Tenant
16 |
17 | You can reset the tenant used with this app, which will delete the client, resource server, etc. that are created during the bootstrap process. To do this, run `npm run reset-tenant`.
18 |
--------------------------------------------------------------------------------
/public/callback.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const OAUTH_PARAM_NAMES = [
4 | "error",
5 | "error_description",
6 | "code",
7 | "state",
8 | "access_token",
9 | "id_token",
10 | "response"
11 | ];
12 |
13 | function hasImplicitOAuthParams(hashParams) {
14 | return OAUTH_PARAM_NAMES.find((paramName) => hashParams.has(paramName));
15 | }
16 |
17 | document.addEventListener("DOMContentLoaded", async () => {
18 | const url = new URL(document.location);
19 | if (url.pathname === "/callback") {
20 | const hashParams = new URLSearchParams(
21 | document.location.hash.replace("#", "")
22 | );
23 |
24 | if (hasImplicitOAuthParams(hashParams)) {
25 | // This path is followed when an implicit/hybrid flow is used. Take the
26 | // parameters from the hash, put them into a form, and submit
27 |
28 | OAUTH_PARAM_NAMES.forEach((paramName) => {
29 | if (hashParams.has(paramName)) {
30 | document
31 | .querySelector(`[name=${paramName}]`)
32 | .setAttribute("value", hashParams.get(paramName));
33 | }
34 | });
35 |
36 | document.callback_form.submit();
37 | } else {
38 | document.location = "/";
39 | }
40 | }
41 | });
42 |
--------------------------------------------------------------------------------
/lib/handlers/configuration.js:
--------------------------------------------------------------------------------
1 | const { getEnv, setEnv } = require("../env");
2 | const _ = require("lodash");
3 |
4 | const ALLOWED_CONFIGURATION_PARAMS = [
5 | "acr_values",
6 | "app_client_authentication_method",
7 | "audience",
8 | "authorization_details",
9 | "claims",
10 | "jar_enabled",
11 | "login_hint",
12 | "owp",
13 | "par_enabled",
14 | "pkce_code_challenge_method",
15 | "pkce",
16 | "redirect_uri",
17 | "response_mode",
18 | "response_type",
19 | "scope",
20 | "send_authorization_details",
21 | "prompt",
22 | ];
23 |
24 | const saveConfiguration = (req, res) => {
25 | const updatedConfiguration = _.pick(req.body, ALLOWED_CONFIGURATION_PARAMS);
26 | Object.keys(updatedConfiguration).forEach((configurationKey) =>
27 | setEnv(configurationKey, updatedConfiguration[configurationKey], {
28 | logging: false,
29 | })
30 | );
31 |
32 | setEnv("jar_enabled", !!updatedConfiguration.jar_enabled);
33 | setEnv("owp", !!updatedConfiguration.owp);
34 | setEnv("par_enabled", !!updatedConfiguration.par_enabled);
35 | setEnv("pkce", !!updatedConfiguration.pkce);
36 | setEnv(
37 | "send_authorization_details",
38 | !!updatedConfiguration.send_authorization_details
39 | );
40 |
41 | console.log("current environment", JSON.stringify(getEnv(), null, 2));
42 |
43 | res.redirect("/");
44 | };
45 |
46 | module.exports = {
47 | saveConfiguration,
48 | };
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nodejs-quickstart",
3 | "version": "1.0.0",
4 | "description": "This is a prototype nodejs quickstart project to demonstrate new OAuth as a service features with Auth0.",
5 | "main": "app.js",
6 | "scripts": {
7 | "start": "nodemon --inspect=7370 server.js",
8 | "start:debug": "nodemon --inspect-brk server.js",
9 | "reset-tenant": "node scripts/reset_tenant.js"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "axios": "^0.21.1",
15 | "base64url": "^3.0.1",
16 | "body-parser": "^1.15.2",
17 | "cookie-parser": "^1.4.3",
18 | "dotenv": "^2.0.0",
19 | "express": "^4.17.1",
20 | "express-openid-connect": "^2.0.0",
21 | "express-session": "^1.14.1",
22 | "jade": "^1.11.0",
23 | "jose": "^4.13.2",
24 | "jsonwebtoken": "^9.0.0",
25 | "jwt-decode": "^2.2.0",
26 | "lodash": "^4.17.11",
27 | "morgan": "^1.7.0",
28 | "pem-jwk": "^2.0.0",
29 | "req-flash": "0.0.3",
30 | "update-dotenv": "^1.1.1",
31 | "uuid": "^9.0.0"
32 | },
33 | "now": {
34 | "files": [
35 | ".env",
36 | "./env_map.js",
37 | "./public",
38 | "./views",
39 | "app.js",
40 | "server.js",
41 | "./routes",
42 | "./app_passport.js"
43 | ],
44 | "name": "node-hosted-demo",
45 | "alias": "node-hosted-demo"
46 | },
47 | "devDependencies": {
48 | "nodemon": "^3.0.1"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/scripts/reset_tenant.js:
--------------------------------------------------------------------------------
1 | const dotenv = require('dotenv');
2 | const { APP_RESOURCE_SERVER_IDENTIFIER, CLIENT_NAME_FOR_DEMO_APP } = require('../lib/constants');
3 | const { makeApi2Request } = require('../lib/api2');
4 |
5 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
6 |
7 | dotenv.config();
8 |
9 | const resetTenant = async () => {
10 | console.log('This script will remove artifacts from the tenant used for this demo app.');
11 | console.log('The next time the app boots, these artifacts will be re-created.');
12 | console.log('>>> DELETING CLIENT');
13 | await deleteAppClient();
14 | console.log('>>> DELETING RESOURCE SERVER');
15 | await deleteAppResourceServer();
16 | };
17 |
18 | const deleteAppClient = async () => {
19 | const getClientsRequest = {
20 | path: 'clients?page=0&per_page=100',
21 | };
22 |
23 | const clients = await makeApi2Request(getClientsRequest);
24 | const appClient = clients.filter((client) => client.name === CLIENT_NAME_FOR_DEMO_APP);
25 | if (appClient.length < 1) {
26 | return;
27 | }
28 |
29 | const request = {
30 | method: 'delete',
31 | path: `clients/${appClient[0].client_id}`,
32 | };
33 | await makeApi2Request(request);
34 | };
35 |
36 | const deleteAppResourceServer = async () => {
37 | const request = {
38 | method: 'delete',
39 | path: `resource-servers/${APP_RESOURCE_SERVER_IDENTIFIER}`,
40 | };
41 | await makeApi2Request(request);
42 | };
43 |
44 | resetTenant();
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | server.cert
3 | server.key
4 |
5 | # Created by https://www.gitignore.io/api/node,osx
6 |
7 | ### Node ###
8 | # Logs
9 | logs
10 | *.log
11 | npm-debug.log*
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # node-waf configuration
32 | .lock-wscript
33 |
34 | # Compiled binary addons (http://nodejs.org/api/addons.html)
35 | build/Release
36 |
37 | # Dependency directories
38 | node_modules
39 | jspm_packages
40 |
41 | # Optional npm cache directory
42 | .npm
43 |
44 | # Optional eslint cache
45 | .eslintcache
46 |
47 | # Optional REPL history
48 | .node_repl_history
49 |
50 | # Output of 'npm pack'
51 | *.tgz
52 |
53 |
54 | ### OSX ###
55 | *.DS_Store
56 | .AppleDouble
57 | .LSOverride
58 |
59 | # Icon must end with two \r
60 | Icon
61 |
62 |
63 | # Thumbnails
64 | ._*
65 |
66 | # Files that might appear in the root of a volume
67 | .DocumentRevisions-V100
68 | .fseventsd
69 | .Spotlight-V100
70 | .TemporaryItems
71 | .Trashes
72 | .VolumeIcon.icns
73 | .com.apple.timemachine.donotpresent
74 |
75 | # Directories potentially created on remote AFP share
76 | .AppleDB
77 | .AppleDesktop
78 | Network Trash Folder
79 | Temporary Items
80 | .apdisk
81 |
82 | envs/
83 | .env
84 | set_env
85 | set_env.js
86 |
87 | keys/
--------------------------------------------------------------------------------
/lib/api2.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 |
3 | const { API2_BASE_URL, TOKEN_ENDPOINT, API2_AUDIENCE } = require("./constants");
4 | const { getManagementClientAuthentication } = require('./client_authentication');
5 | const { getEnv } = require('./env');
6 |
7 | let api2Token;
8 |
9 | const getToken = async (force = false) => {
10 | if (!api2Token || force) {
11 | const tokenRequest = {
12 | client_id: getEnv("AUTH0_MGMT_CLIENT_ID"),
13 | grant_type: "client_credentials",
14 | audience: API2_AUDIENCE,
15 | ...getManagementClientAuthentication()
16 | };
17 | const config = {
18 | method: "post",
19 | url: TOKEN_ENDPOINT,
20 | headers: {
21 | "Content-Type": "application/json",
22 | },
23 | data: JSON.stringify(tokenRequest),
24 | };
25 |
26 | const response = await axios(config);
27 | api2Token = response.data.access_token;
28 | }
29 | return api2Token;
30 | };
31 |
32 | const makeApi2Request = async (options) => {
33 | try {
34 | const api2Token = await getToken();
35 | const method =
36 | (options && options.method && options.method.toLowerCase()) || "get";
37 |
38 | const url = `${API2_BASE_URL}${options.path}`;
39 | const config = {
40 | method,
41 | url,
42 | headers: {
43 | Authorization: `Bearer ${api2Token}`,
44 | "Content-Type": "application/json",
45 | },
46 | data: JSON.stringify(options.data),
47 | };
48 |
49 | const response = await axios(config);
50 | return response.data;
51 | } catch (error) {
52 | const api2Error = new Error(error.message);
53 | if (error.response) {
54 | api2Error.data = error.response.data;
55 | }
56 |
57 | throw api2Error;
58 | }
59 | };
60 |
61 | module.exports = {
62 | getToken,
63 | makeApi2Request,
64 | };
65 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const dotenv = require("dotenv");
2 | const debug = require("debug")("nodejs-regular-webapp2:server");
3 | const https = require("https");
4 | const fs = require("fs");
5 | dotenv.config();
6 |
7 | const env = require("./lib/env");
8 | const boostrap = require("./lib/handlers/bootstrap");
9 |
10 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
11 |
12 | const init = async () => {
13 | await boostrap.bootstrapProcess();
14 | const app = require("./app");
15 |
16 | const port = normalizePort(process.env.PORT || "4040");
17 | app.set("port", port);
18 |
19 | console.log(">>> Using env:");
20 | console.log(env.getEnv());
21 |
22 | const server = https.createServer(
23 | {
24 | key: fs.readFileSync("server.key"),
25 | cert: fs.readFileSync("server.cert"),
26 | },
27 | app
28 | );
29 |
30 | server.listen(port);
31 |
32 | server.on("error", (error) => {
33 | if (error.syscall !== "listen") {
34 | throw error;
35 | }
36 |
37 | const bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
38 |
39 | switch (error.code) {
40 | case "EACCES":
41 | console.error(bind + " requires elevated privileges");
42 | process.exit(1);
43 | break;
44 | case "EADDRINUSE":
45 | console.error(bind + " is already in use");
46 | process.exit(1);
47 | break;
48 | default:
49 | throw error;
50 | }
51 | });
52 |
53 | server.on("listening", () => {
54 | const addr = server.address();
55 | const bind =
56 | typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
57 | debug("Listening on " + bind);
58 | console.log("Listening on " + bind);
59 | });
60 | };
61 |
62 | const normalizePort = (portValue) => {
63 | const port = parseInt(portValue, 10);
64 |
65 | if (isNaN(port)) {
66 | return portValue;
67 | }
68 |
69 | if (port >= 0) {
70 | return port;
71 | }
72 |
73 | return false;
74 | };
75 |
76 | init();
77 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const logger = require('morgan');
4 | const session = require('express-session');
5 | const flash = require('req-flash');
6 | const bodyParser = require('body-parser');
7 | const routes = require('./routes/index');
8 | const user = require('./routes/user');
9 |
10 | const app = express();
11 |
12 | app.use(bodyParser.urlencoded({ extended: false }));
13 |
14 | app.set('views', path.join(__dirname, 'views'));
15 | app.set('view engine', 'jade');
16 | app.set('view options', { pretty: true });
17 |
18 | app.use(logger('dev'));
19 | app.use(
20 | session({
21 | secret: 'yourSessionSecret',
22 | resave: true,
23 | saveUninitialized: true,
24 | })
25 | );
26 | app.use(flash());
27 | app.use(express.static(path.join(__dirname, 'public')));
28 |
29 | app.use(function authErrorHandler(req, res, next) {
30 | if (req && req.query && req.query.error) {
31 | req.flash('error', req.query.error);
32 | }
33 | if (req && req.query && req.query.error_description) {
34 | req.flash('error_description', req.query.error_description);
35 | }
36 | next();
37 | });
38 |
39 | app.use('/', routes);
40 | app.use('/user', user);
41 |
42 | app.use(function catch404Error(req, res, next) {
43 | var err = new Error('Not Found');
44 | err.status = 404;
45 | next(err);
46 | });
47 |
48 | if (app.get('env') === 'development') {
49 | app.use(function devErrorHandler(err, req, res, next) {
50 | // TODO: A better way to output diagnostic info in the console
51 | console.log(err.data);
52 | res.status(err.status || 500);
53 | res.render('error', {
54 | message: err.message,
55 | error: err,
56 | });
57 | });
58 | }
59 |
60 | app.use(function prodErrorHandler(err, req, res, next) {
61 | // TODO: What makes to log in production?
62 | res.status(err.status || 500);
63 | res.render('error', {
64 | message: err.message,
65 | error: {},
66 | });
67 | });
68 |
69 | module.exports = app;
70 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('nodejs-regular-webapp2:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || "4040");
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | console.log(`Listening on ${addr} ${bind}`);
91 | }
92 |
--------------------------------------------------------------------------------
/lib/handlers/invite.js:
--------------------------------------------------------------------------------
1 | const { makeApi2Request } = require('../api2');
2 | const { getEnv } = require('../env');
3 |
4 | const deleteTestUsers = async (email) => {
5 | const requestOptions = {
6 | path: `users?q=email:"${email}"&search_engine=v3`,
7 | };
8 |
9 | const response = await makeApi2Request(requestOptions);
10 | const userIds = response.map((user) => user.user_id);
11 | for (let userId of userIds) {
12 | await deleteSingleUser(userId);
13 | }
14 | };
15 |
16 | const deleteSingleUser = async (userId) => {
17 | const requestOptions = {
18 | method: 'delete',
19 | path: `users/${userId}`,
20 | };
21 |
22 | await makeApi2Request(requestOptions);
23 | };
24 |
25 | const inviteFlow = async (req, res, next) => {
26 | // TODO: We should validate these instead of directly passing them to the backend
27 | const email = req.body.email;
28 | const organizationId = req.body.organization_id;
29 | const roleId = req.body.role_id;
30 | const connectionId = req.body.connection_id;
31 |
32 | try {
33 | const inviteRequest = {
34 | client_id: getEnv("APP_CLIENT_ID"),
35 | invitee: { email },
36 | inviter: { name: 'John Doe' },
37 | app_metadata: {
38 | source: 'Invited via test app',
39 | },
40 | roles: [roleId],
41 | };
42 |
43 | if (connectionId !== 'not-specified') {
44 | inviteRequest.connection_id = connectionId;
45 | }
46 |
47 | await deleteTestUsers(email);
48 |
49 | const requestOptions = {
50 | method: 'post',
51 | path: `organizations/${organizationId}/invitations`,
52 | data: inviteRequest,
53 | };
54 |
55 | const response = await makeApi2Request(requestOptions);
56 |
57 | const invitationAppUrl = response.invitation_url;
58 | console.log(`Will redirect to ${invitationAppUrl}`);
59 | res.redirect(invitationAppUrl);
60 | } catch (error) {
61 | console.log('Error while creating invitation: ');
62 | console.log(error);
63 | return next(error);
64 | }
65 | };
66 |
67 | module.exports = {
68 | inviteFlow,
69 | };
70 |
--------------------------------------------------------------------------------
/lib/handlers/auth_api.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 |
3 | const {
4 | USERINFO_ENDPOINT,
5 | TOKEN_ENDPOINT,
6 | AUTH_REQUESTED_SCOPES,
7 | APP_CALLBACK_URL,
8 | } = require("../constants");
9 | const { getEnv } = require("../env");
10 | const { setAppClientAuthentication } = require("../client_authentication");
11 |
12 | const getUserInfo = async (accessToken) => {
13 | const config = {
14 | method: "GET",
15 | url: USERINFO_ENDPOINT,
16 | headers: {
17 | "content-type": "application/json",
18 | Authorization: `Bearer ${accessToken}`,
19 | },
20 | json: true,
21 | };
22 |
23 | try {
24 | const response = await axios(config);
25 | return response.data;
26 | } catch (error) {
27 | console.error("error fetching userinfo", {
28 | status: error.response.status,
29 | data: error.response.data,
30 | statusText: error.response.statusText,
31 | errorText:
32 | error.response.headers["www-authenticate"] || error.response.body,
33 | });
34 | throw error;
35 | }
36 | };
37 |
38 | const getAccessTokenFromCode = async (authorizationCode, params = {}) => {
39 | const APP_CLIENT_ID = getEnv("APP_CLIENT_ID");
40 |
41 | const config = setAppClientAuthentication({
42 | method: "POST",
43 | url: TOKEN_ENDPOINT,
44 | headers: { "content-type": "application/json" },
45 | data: {
46 | ...params,
47 | grant_type: "authorization_code",
48 | client_id: APP_CLIENT_ID,
49 | code: authorizationCode,
50 | redirect_uri: APP_CALLBACK_URL,
51 | },
52 | json: true,
53 | });
54 |
55 | const response = await axios(config);
56 | return response.data;
57 | };
58 |
59 | const getRefreshToken = async (refreshToken) => {
60 | const config = setAppClientAuthentication({
61 | method: "POST",
62 | url: TOKEN_ENDPOINT,
63 | headers: { "content-type": "application/json" },
64 | data: {
65 | grant_type: "refresh_token",
66 | client_id: getEnv("APP_CLIENT_ID"),
67 | refresh_token: refreshToken,
68 | scope: AUTH_REQUESTED_SCOPES,
69 | },
70 | json: true,
71 | });
72 |
73 | const response = await axios(config);
74 | return response.data;
75 | };
76 |
77 | module.exports = {
78 | getUserInfo,
79 | getRefreshToken,
80 | getAccessTokenFromCode,
81 | };
82 |
--------------------------------------------------------------------------------
/views/user.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 | block content
3 | include menu
4 | p
5 | h1 Welcome #{user.profile.displayName}!
6 | if userinfoResponse
7 | p
8 | strong Userinfo result
9 | pre#api-call-result #{JSON.stringify(userinfoResponse, null, 4)}
10 | div(id="accordion")
11 | div(class="card")
12 | div(class="card-header" id="headingOne")
13 | h5(class="mb-0")
14 | button(class="btn btn-link" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne") Detached Signature
15 | div(id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion")
16 | div(class="card-body")
17 | pre
18 | h6 header
19 | code #{JSON.stringify(decodedDetachedSignature.header, null, 4)}
20 | pre
21 | h6 payload
22 | code #{JSON.stringify(decodedDetachedSignature.payload, null, 4)}
23 | div(class="card-header" id="headingTwo")
24 | h5(class="mb-0")
25 | button(class="btn btn-link" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="true" aria-controls="collapseTwo") ID Token Payload
26 | div(id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordion")
27 | div(class="card-body")
28 | pre
29 | h6 header
30 | code #{JSON.stringify(decodedIDToken.header, null, 4)}
31 | pre
32 | h6 payload
33 | code #{JSON.stringify(decodedIDToken.payload, null, 4)}
34 | div(class="card")
35 | div(class="card-header" id="headingThree")
36 | h5(class="mb-0")
37 | button(class="btn btn-link" data-toggle="collapse" data-target="#collapseThree" aria-expanded="true" aria-controls="collapseThree") Access Token Payload
38 | div(id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#accordion")
39 | div(class="card-body")
40 | pre
41 | h6 header
42 | code #{JSON.stringify(decodedAccessToken.header, null, 4)}
43 | pre
44 | h6 payload
45 | code #{JSON.stringify(decodedAccessToken.payload, null, 4)}
46 | div(class="card")
47 | div(class="card-header" id="headingFour")
48 | h5(class="mb-0")
49 | button(class="btn btn-link" data-toggle="collapse" data-target="#collapseFour" aria-expanded="true" aria-controls="collapseFour") Tokens
50 | div(id="collapseFour" class="collapse" aria-labelledby="headingFour" data-parent="#accordion")
51 | div(class="card-body")
52 | pre Detached Signature ID Token:
53 | pre #{tokens.detached_signature}
54 | pre Access Token:
55 | pre #{tokens.access_token}
56 | pre ID Token:
57 | pre #{tokens.id_token}
58 | pre Refresh Token:
59 | pre #{tokens.refresh_token}
60 |
--------------------------------------------------------------------------------
/lib/env.js:
--------------------------------------------------------------------------------
1 | const writeDotEnv = require("update-dotenv");
2 |
3 | const {
4 | AUTH_REQUESTED_SCOPES,
5 | APP_CALLBACK_URL,
6 | APP_RESOURCE_SERVER_IDENTIFIER,
7 | PKCE_CODE_CHALLENGE_METHODS,
8 | CLIENT_AUTHENTICATION_METHODS,
9 | PROMPT_TYPES,
10 | } = require("../lib/constants");
11 |
12 | function importKey(envName) {
13 | return process.env[envName]?.replace(/\\n/g, "\n") || "";
14 | }
15 |
16 | const resolvedEnv = {
17 | AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
18 | AUTH0_MGMT_CLIENT_ID: process.env.AUTH0_MGMT_CLIENT_ID,
19 | AUTH0_MGMT_CLIENT_SECRET: process.env.AUTH0_MGMT_CLIENT_SECRET,
20 | AUTH0_MGMT_CLIENT_AUTHENTICATION_METHOD:
21 | process.env.AUTH0_MGMT_CLIENT_AUTHENTICATION_METHOD,
22 | AUTH0_MGMT_JWTCA_PRIVATE_KEY: importKey("AUTH0_MGMT_JWTCA_PRIVATE_KEY"),
23 | AUTH0_MGMT_JWTCA_PUBLIC_KEY: importKey("AUTH0_MGMT_JWTCA_PUBLIC_KEY"),
24 | AUTH0_MGMT_JWTCA_KEY_ID: process.env.AUTH0_MGMT_JWTCA_KEY_ID,
25 | AUTH0_MGMT_JWTCA_CREDENTIAL_ID: process.env.AUTH0_MGMT_JWTCA_CREDENTIAL_ID,
26 | APP_CLIENT_ID: process.env.APP_CLIENT_ID,
27 | APP_CLIENT_SECRET: process.env.APP_CLIENT_SECRET,
28 | app_client_authentication_method:
29 | process.env.APP_CLIENT_AUTHENTICATION_METHOD,
30 | APP_CLIENT_AUTHENTICATION_METHOD:
31 | process.env.APP_CLIENT_AUTHENTICATION_METHOD,
32 | APP_JAR_PRIVATE_KEY: importKey("APP_JAR_PRIVATE_KEY"),
33 | APP_JAR_PUBLIC_KEY: importKey("APP_JAR_PUBLIC_KEY"),
34 | APP_JAR_KEY_ALG: process.env.APP_JAR_KEY_ALG,
35 | APP_JAR_KEY_ID: process.env.APP_JAR_KEY_ID,
36 | APP_JAR_CREDENTIAL_ID: process.env.APP_JAR_CREDENTIAL_ID,
37 | APP_JWTCA_PRIVATE_KEY: importKey("APP_JWTCA_PRIVATE_KEY"),
38 | APP_JWTCA_PUBLIC_KEY: importKey("APP_JWTCA_PUBLIC_KEY"),
39 | APP_JWTCA_KEY_ID: process.env.APP_JWTCA_KEY_ID,
40 | APP_JWTCA_CREDENTIAL_ID: process.env.APP_JWTCA_CREDENTIAL_ID,
41 | APP_MTLS_CERTIFICATE: importKey("APP_MTLS_CERTIFICATE"),
42 | APP_MTLS_PRIVATE_KEY: importKey("APP_MTLS_PRIVATE_KEY"),
43 | APP_MTLS_PUBLIC_KEY: importKey("APP_MTLS_PUBLIC_KEY"),
44 | acr_values: process.env.ACR_VALUES,
45 | claims: process.env.CLAIMS,
46 | audience: APP_RESOURCE_SERVER_IDENTIFIER,
47 | authorization_details: '[{"type":"urn:auth0:temp:sca"}]',
48 | client_authentication_methods_list: Object.values(
49 | CLIENT_AUTHENTICATION_METHODS
50 | ),
51 | jar_enabled: process.env.APP_JAR_ENABLED === "true",
52 | login_hint: '',
53 | owp: false,
54 | par_enabled: process.env.APP_PAR_ENABLED === "true",
55 | pkce_code_challenge_method_list: PKCE_CODE_CHALLENGE_METHODS,
56 | pkce_code_challenge_method: "S256",
57 | pkce: true,
58 | prompt: [],
59 | prompt_list: PROMPT_TYPES,
60 | redirect_uri: APP_CALLBACK_URL,
61 | response_mode: "",
62 | response_mode_list: [
63 | "",
64 | "query",
65 | "fragment",
66 | "form_post",
67 | "jwt",
68 | "query.jwt",
69 | "fragment.jwt",
70 | "form_post.jwt",
71 | "auth0_owp",
72 | ],
73 | response_type: "code id_token",
74 | scope: AUTH_REQUESTED_SCOPES,
75 | send_authorization_details: false,
76 | };
77 |
78 | const getEnv = (envVariableName) => {
79 | if (typeof envVariableName === "string") {
80 | return resolvedEnv[envVariableName];
81 | } else {
82 | return resolvedEnv;
83 | }
84 | };
85 |
86 | const setEnv = async (envVariableName, value, { write = false } = {}) => {
87 | resolvedEnv[envVariableName] = value;
88 | if (write) {
89 | await writeDotEnv({ [envVariableName]: value });
90 | }
91 | };
92 |
93 | module.exports = {
94 | getEnv,
95 | setEnv,
96 | };
97 |
--------------------------------------------------------------------------------
/routes/user.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const jsonwebtoken = require("jsonwebtoken");
4 |
5 | const { getUserInfo, getRefreshToken } = require("../lib/handlers/auth_api");
6 | const { getEnv } = require("../lib/env");
7 | const { APP_LOGOUT_URL } = require("../lib/constants");
8 |
9 | const ensureLoggedIn = (req, res, next) => {
10 | if (req.session.user) {
11 | req.user = req.session.user;
12 | } else {
13 | return res.redirect("/");
14 | }
15 |
16 | next();
17 | };
18 |
19 | router.get("/", ensureLoggedIn, (req, res, next) => {
20 | renderUserPage(req, res);
21 | });
22 |
23 | router.get("/refresh", ensureLoggedIn, async (req, res, next) => {
24 | try {
25 | const refreshTokenResponse = await getRefreshToken(
26 | req.user.extraParams.refresh_token
27 | );
28 |
29 | renderUserPage(req, res, {
30 | idToken: refreshTokenResponse.id_token,
31 | accessToken: refreshTokenResponse.access_token,
32 | });
33 | } catch (error) {
34 | next(error);
35 | }
36 | });
37 |
38 | router.get("/userinfo", ensureLoggedIn, async (req, res, next) => {
39 | try {
40 | const userinfoResponse = await getUserInfo(
41 | req.user.extraParams.access_token
42 | );
43 |
44 | renderUserPage(req, res, { userinfoResponse });
45 | } catch (error) {
46 | next(error);
47 | }
48 | });
49 |
50 | const renderUserPage = (req, res, data = {}) => {
51 | const detachedSignature =
52 | data.detachedSignature || req.user.extraParams.detached_signature;
53 | const idToken = data.idToken || req.user.extraParams.id_token;
54 | const accessToken = data.accessToken || req.user.extraParams.access_token;
55 | let decodedDetachedSignature = "";
56 | let decodedIDToken = "";
57 | let decodedAccessToken = "";
58 |
59 | try {
60 | // TODO rather than just decoding, verify JWT.
61 | decodedDetachedSignature = jsonwebtoken.decode(detachedSignature, {
62 | complete: true,
63 | });
64 | } catch (error) {
65 | decodedDetachedSignature = "Unable to decode";
66 | }
67 |
68 | try {
69 | decodedIDToken = jsonwebtoken.decode(idToken, { complete: true });
70 | } catch (error) {
71 | decodedIDToken = "Unable to decode";
72 | }
73 |
74 | try {
75 | decodedAccessToken = jsonwebtoken.decode(accessToken, { complete: true });
76 | } catch (error) {
77 | decodedAccessToken = "Unable to decode";
78 | }
79 |
80 | res.render("user", {
81 | user: req.user,
82 | decodedDetachedSignature,
83 | decodedIDToken,
84 | decodedAccessToken,
85 | userinfoResponse: data.userinfoResponse,
86 | title: "Fake SaaS App",
87 | tokens: {
88 | detached_signature: detachedSignature,
89 | refresh_token: req.user.extraParams.refresh_token,
90 | id_token: idToken,
91 | access_token: accessToken,
92 | },
93 | config: {
94 | APP_LOGOUT_URL,
95 | AUTH0_DOMAIN: getEnv("AUTH0_DOMAIN"),
96 | APP_CLIENT_ID: getEnv("APP_CLIENT_ID"),
97 | },
98 | });
99 | };
100 |
101 | router.get("/saml", ensureLoggedIn, (req, res, next) => {
102 | renderUserPageWithSAML(req, res);
103 | });
104 |
105 | const renderUserPageWithSAML = (req, res) => {
106 | const samlProfile = req.user.profile;
107 |
108 | res.render("samluser", {
109 | user: req.user,
110 | samlProfile,
111 | title: "Fake SAML SaaS App",
112 | config: {
113 | APP_LOGOUT_URL,
114 | AUTH0_DOMAIN: getEnv("AUTH0_DOMAIN"),
115 | APP_CLIENT_ID: getEnv("SAML_APP_CLIENT_ID"),
116 | },
117 | });
118 | };
119 |
120 | module.exports = router;
121 |
--------------------------------------------------------------------------------
/lib/constants.js:
--------------------------------------------------------------------------------
1 | const dotenv = require("dotenv");
2 |
3 | dotenv.config();
4 |
5 | const API2_BASE_URL = `https://${process.env.AUTH0_DOMAIN}/api/v2/`;
6 | const API2_AUDIENCE = API2_BASE_URL;
7 | const MFA_AUDIENCE = `https://${process.env.AUTH0_DOMAIN}/mfa`;
8 | const AUTH_REQUESTED_SCOPES =
9 | "openid email profile create:foo read:foo update:foo delete:foo offline_access";
10 | const USERINFO_ENDPOINT = `https://${process.env.AUTH0_DOMAIN}/userinfo`;
11 | const USERINFO_AUDIENCE = USERINFO_ENDPOINT;
12 | const TOKEN_ENDPOINT = `https://${process.env.AUTH0_DOMAIN}/oauth/token`;
13 | const AUTHORIZE_ENDPOINT = `https://${process.env.AUTH0_DOMAIN}/authorize`;
14 | const PAR_ENDPOINT = `https://${process.env.AUTH0_DOMAIN}/oauth/par`;
15 |
16 | const REQUIRED_SCOPES_FOR_BACKEND_CLIENT = [
17 | "create:clients",
18 | "read:client_grants",
19 | "update:client_grants",
20 | "read:clients",
21 | "delete:clients",
22 | "read:client_keys",
23 | "create:connections",
24 | "read:connections",
25 | "delete:connections",
26 | "create:roles",
27 | "delete:roles",
28 | "update:roles",
29 | "read:roles",
30 | "read:users",
31 | "delete:users",
32 | "update:prompts",
33 | "create:resource_servers",
34 | "read:resource_servers",
35 | "delete:resource_servers",
36 | ];
37 | const APP_RESOURCE_SERVER_IDENTIFIER = "urn:demo-saas-api";
38 | // CLIENT SETTINGS
39 | // TODO: Make these dynamic e.g. retrieve port
40 | const APP_CALLBACK_URL = "https://myapp.com:4040/callback";
41 | const APP_LOGOUT_URL = "https://myapp.com:4040/logout";
42 | const APP_INITIATE_LOGIN_URL = "https://myapp.com:4040/login";
43 | const CLIENT_NAME_FOR_DEMO_APP = "Demozero";
44 | const APP_LOGO_URI = "https://static.thenounproject.com/png/66350-200.png";
45 |
46 | const CLIENT_SECRET_BASIC = "client_secret_basic";
47 | const CLIENT_SECRET_POST = "client_secret_post";
48 | const PRIVATE_KEY_JWT = "jwtca";
49 | const CA_MTLS = "ca_mtls";
50 | const SELF_SIGNED_MTLS = "self_signed_mtls";
51 | const CA_NONE = "none";
52 |
53 | // Response Types
54 | const AUTHORIZATION_CODE_RESPONSE = "code";
55 | const TOKEN_RESPONSE = "token";
56 | const ID_TOKEN_RESPONSE = "id_token";
57 | const ID_TOKEN_TOKEN_RESPONSE = "id_token token";
58 | const CODE_ID_TOKEN = "code id_token";
59 | const CODE_ID_TOKEN_TOKEN = "code id_token token";
60 |
61 | const PKCE_CODE_CHALLENGE_METHOD_PLAIN = "plain";
62 | const PKCE_CODE_CHALLENGE_METHOD_S256 = "S256";
63 |
64 | module.exports = {
65 | API2_BASE_URL,
66 | API2_AUDIENCE,
67 | AUTH_REQUESTED_SCOPES,
68 | AUTHORIZE_ENDPOINT,
69 | PAR_ENDPOINT,
70 | USERINFO_ENDPOINT,
71 | TOKEN_ENDPOINT,
72 | REQUIRED_SCOPES_FOR_BACKEND_CLIENT,
73 | APP_RESOURCE_SERVER_IDENTIFIER,
74 | APP_CALLBACK_URL,
75 | APP_LOGOUT_URL,
76 | APP_INITIATE_LOGIN_URL,
77 | CLIENT_NAME_FOR_DEMO_APP,
78 | APP_LOGO_URI,
79 |
80 | AUTHORIZATION_CODE_RESPONSE,
81 | TOKEN_RESPONSE,
82 | ID_TOKEN_RESPONSE,
83 | ID_TOKEN_TOKEN_RESPONSE,
84 |
85 | AUDIENCES: [API2_AUDIENCE, USERINFO_AUDIENCE, MFA_AUDIENCE],
86 |
87 | RESPONSE_TYPES: [
88 | AUTHORIZATION_CODE_RESPONSE,
89 | TOKEN_RESPONSE,
90 | ID_TOKEN_RESPONSE,
91 | ID_TOKEN_TOKEN_RESPONSE,
92 | CODE_ID_TOKEN,
93 | CODE_ID_TOKEN_TOKEN,
94 | ],
95 |
96 | PKCE_CODE_CHALLENGE_METHODS: [
97 | PKCE_CODE_CHALLENGE_METHOD_PLAIN,
98 | PKCE_CODE_CHALLENGE_METHOD_S256,
99 | ],
100 |
101 | CLIENT_AUTHENTICATION_METHODS: {
102 | CLIENT_SECRET_BASIC,
103 | CLIENT_SECRET_POST,
104 | PRIVATE_KEY_JWT,
105 | CA_MTLS,
106 | SELF_SIGNED_MTLS,
107 | CA_NONE,
108 | },
109 |
110 | PROMPT_TYPES: [
111 | "",
112 | "none",
113 | "consent",
114 | "login",
115 | "select_account"
116 | ]
117 | };
118 |
--------------------------------------------------------------------------------
/public/main.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/handlers/login.js:
--------------------------------------------------------------------------------
1 | const axios = require("axios");
2 | const url = require("url");
3 | const querystring = require("node:querystring");
4 | const uuid = require("uuid");
5 |
6 | const { getEnv } = require("../env");
7 | const {
8 | APP_CALLBACK_URL,
9 | AUTHORIZE_ENDPOINT,
10 | PAR_ENDPOINT,
11 | } = require("../constants");
12 | const { setAppClientAuthentication } = require("../client_authentication");
13 | const { createJARPayload } = require("../jar");
14 | const pkce = require("../pkce");
15 |
16 | function maybeValue(value, name) {
17 | if (value) {
18 | return { [name]: value };
19 | }
20 | return {};
21 | }
22 |
23 | function maybeArrayMaybeValue(value, name) {
24 | if (Array.isArray(value)) {
25 | return { [name]: value.join(" ") };
26 | }
27 | return maybeValue(value, name);
28 | }
29 |
30 | function maybeJSONValue(value, name) {
31 | if (value) {
32 | /* let parsedValue = value;
33 | try {
34 | parsedValue = JSON.parse(value);
35 | } catch {
36 | // ignore
37 | }*/
38 | return { [name]: value };
39 | }
40 | return {};
41 | }
42 |
43 | const getAuthorizeParams = (req, extras = {}) => {
44 | const {
45 | APP_CLIENT_ID,
46 | acr_values,
47 | audience,
48 | authorization_details,
49 | claims,
50 | login_hint,
51 | owp,
52 | pkce_code_challenge_method,
53 | pkce: isPKCEEnabled,
54 | prompt,
55 | redirect_uri,
56 | response_mode,
57 | response_type,
58 | scope,
59 | send_authorization_details: isRAREnabled,
60 | } = getEnv();
61 |
62 | let pkceParams = {};
63 |
64 | if (isPKCEEnabled) {
65 | pkceParams.code_challenge_method = pkce_code_challenge_method;
66 | const codeVerifier = pkce.generateCodeVerifier();
67 | req.session.code_verifier = codeVerifier;
68 |
69 | pkceParams.code_challenge = pkce.generateCodeChallengeFromVerifier(
70 | pkce_code_challenge_method,
71 | codeVerifier
72 | );
73 | }
74 |
75 | const state = uuid.v4();
76 | req.session.state = state;
77 |
78 | let nonce;
79 | if ((response_type || "").indexOf("id_token") > -1) {
80 | nonce = uuid.v4();
81 | req.session.nonce = nonce;
82 | }
83 |
84 | let authorizationDetailsParams = {};
85 | if (isRAREnabled) {
86 | authorizationDetailsParams = {
87 | authorization_details,
88 | };
89 | }
90 |
91 | const authorizeParams = {
92 | ...(acr_values ? { acr_values: acr_values.split(/,/g) } : {}),
93 | ...maybeValue(audience, "audience"),
94 | ...maybeValue(nonce, "nonce"),
95 | ...maybeValue(!!owp, "owp"),
96 | ...maybeValue(req.query.invitation, "invitation"),
97 | ...maybeValue(req.query.organization, "organization"),
98 | ...maybeValue(response_mode, "response_mode"),
99 | ...maybeValue(login_hint, "login_hint"),
100 | ...maybeValue(scope, "scope"),
101 | ...authorizationDetailsParams,
102 | ...pkceParams,
103 | ...maybeJSONValue(claims, "claims"),
104 | ...maybeArrayMaybeValue(prompt, "prompt"),
105 | client_id: APP_CLIENT_ID,
106 | redirect_uri,
107 | response_type,
108 | state,
109 | ...extras,
110 | };
111 |
112 | if (getEnv("jar_enabled")) {
113 | return createJARPayload(authorizeParams);
114 | }
115 |
116 | return authorizeParams;
117 | };
118 |
119 | const authenticate = (req, res) => {
120 | const authorizeUrl = url.parse(AUTHORIZE_ENDPOINT);
121 | authorizeUrl.query = getAuthorizeParams(req);
122 | console.log(
123 | `Calling ${AUTHORIZE_ENDPOINT} with ${JSON.stringify(authorizeUrl.query)}`
124 | );
125 |
126 | res.redirect(url.format(authorizeUrl));
127 | };
128 |
129 | const authenticateWithPar = async (req, res) => {
130 | console.log(
131 | `Calling ${PAR_ENDPOINT} with ${JSON.stringify(
132 | getAuthorizeParams(req),
133 | null,
134 | 2
135 | )}`
136 | );
137 |
138 | let response;
139 | try {
140 | const authorizeParams = getAuthorizeParams(req);
141 | const config = setAppClientAuthentication({
142 | method: "POST",
143 | url: PAR_ENDPOINT,
144 | data: {
145 | ...authorizeParams,
146 | },
147 | headers: { "Content-Type": "application/x-www-form-urlencoded" },
148 | json: false,
149 | });
150 | config.data = querystring.stringify(config.data);
151 | response = await axios(config);
152 | } catch (error) {
153 | const callbackUrl = url.parse(APP_CALLBACK_URL);
154 | const body = error.response?.data || error;
155 | console.log({ statusCode: error.response?.status, response: body });
156 | callbackUrl.query = {
157 | error: body.error,
158 | error_description: body.error_description,
159 | state: req.session.state,
160 | };
161 |
162 | return res.redirect(url.format(callbackUrl));
163 | }
164 | const authorizeUrl = url.parse(AUTHORIZE_ENDPOINT);
165 | authorizeUrl.query = {
166 | client_id: getEnv("APP_CLIENT_ID"),
167 | request_uri: response.data.request_uri,
168 | };
169 |
170 | res.redirect(url.format(authorizeUrl));
171 | };
172 |
173 | module.exports = {
174 | authenticate,
175 | authenticateWithPar,
176 | };
177 |
--------------------------------------------------------------------------------
/views/index.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | div(class="jumbotron")
5 | h1(class="display-4")= title
6 | p(class="lead") Welcome to the demo-zero.
7 | #msg
8 | div(class="row mt-3")
9 | div(class="col-sm-6")
10 | div(class="card")
11 | h5(class="card-header") Login
12 | div(class="card-body")
13 | p(class="card-text") Standard OAuth/OIDC flow
14 | form(name="login" method="post" action="login")
15 | input(type="submit" value="Login" class="btn btn-primary" )
16 | div(class="row mt-3")
17 | div(class="col-sm-12")
18 | div(class="card")
19 | h5(class="card-header") Runtime Configuration
20 | div(class="card-body")
21 | form(name="configuration" method="post" action="saveconfiguration")
22 | div(class="form-group")
23 | label
24 | input(type="checkbox" name="par_enabled" id="par_enabled" checked=(par_enabled))
25 | = " Use Pushed Authorization Requests (PAR)"
26 | div(class="form-group")
27 | label
28 | input(type="checkbox" name="jar_enabled" id="jar_enabled" checked=(jar_enabled))
29 | = " Use JWT-Secured Authorization Request (JAR)"
30 | div(class="form-group")
31 |
32 | select(id="app_client_authentication_method" name="app_client_authentication_method" class="form-control" multiple)
33 | each clientAuthenticationMethod in clientAuthenticationMethods
34 | if (selectedClientAuthenticationMethod.includes(clientAuthenticationMethod))
35 | option(value=clientAuthenticationMethod selected) #{clientAuthenticationMethod}
36 | else
37 | option(value=clientAuthenticationMethod ) #{clientAuthenticationMethod}
38 | div(class="form-group")
39 |
40 | select(id="audience" name="audience" class="form-control")
41 | option
42 | each audience in audienceList
43 | if (selectedAudience === audience)
44 | option(value=audience selected) #{audience}
45 | else
46 | option(value=audience ) #{audience}
47 | div(class="form-group")
48 |
49 | input(id="scope" name="scope" value="#{scope}" class="form-control" size="20")
50 | div(class="form-group")
51 |
52 | select(id="response_type" name="response_type" class="form-control")
53 | each responseType in responseTypeList
54 | if (selectedResponseType === responseType)
55 | option(value=responseType selected) #{responseType}
56 | else
57 | option(value=responseType) #{responseType}
58 | div(class="form-group")
59 |
60 | select(id="response_mode" name="response_mode" class="form-control")
61 | each responseMode in responseModeList
62 | if (selectedResponseMode === responseMode)
63 | option(value=responseMode selected) #{responseMode}
64 | else
65 | option(value=responseMode) #{responseMode}
66 | div(class="form-group")
67 | label
68 | input(type="checkbox" name="owp" id="owp" checked=(owp))
69 | = " Use owp=true"
70 | div(class="form-group")
71 | label
72 | input(type="checkbox" name="pkce" id="pkce" checked=(pkce))
73 | = " Use Proof Key for Code Exchange (PKCE)"
74 | div(class="form-group")
75 |
76 | select(id="pkce_code_challenge_method" name="pkce_code_challenge_method" class="form-control")
77 | each pkceCodeChallengeMethod in pkceCodeChallengeMethodList
78 | if (selectedPkceCodeChallengeMethod === pkceCodeChallengeMethod)
79 | option(value=pkceCodeChallengeMethod selected) #{pkceCodeChallengeMethod}
80 | else
81 | option(value=pkceCodeChallengeMethod) #{pkceCodeChallengeMethod}
82 | div(class="form-group")
83 |
84 | select(id="prompt" name="prompt" class="form-control" multiple)
85 | each prompt in promptList
86 | if (selectedPrompt.includes(prompt))
87 | option(value=prompt selected) #{prompt}
88 | else
89 | option(value=prompt) #{prompt}
90 | div(class="form-group")
91 | label(for="redirect_uri") Redirect URI
92 | input(type="text", name="redirect_uri", value=(redirectURI) class="form-control")
93 | div(class="form-group")
94 | label(for="acr_values") acr_values
95 | input(type="text", name="acr_values", value=(acrValues) class="form-control")
96 | div(class="form-group")
97 | label(for="claims") claims
98 | input(type="text", name="claims", value=(claims) class="form-control")
99 | div(class="form-group")
100 | label
101 | input(type="checkbox", name="send_authorization_details", checked=(sendAuthorizationDetails))
102 | = " Send authorization_details"
103 | textarea(id="authorization_details" name="authorization_details" class="form-control" placeholder='#{authorizationDetails}') #{authorizationDetails}
104 | div(class="form-group")
105 | label(for="login_hint") login_hint
106 | input(type="text", name="login_hint", value=(login_hint) class="form-control")
107 |
108 | input(type="submit" value="Save Configuration" class="btn btn-primary")
109 | small(id="configurationHelp" class="form-text text-muted") Future calls to /authorize or /oauth/par will use these values. They will be reset when the app restarts.
110 |
111 | div(class="col-sm-6")
112 |
--------------------------------------------------------------------------------
/lib/client_authentication.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 | const https = require("node:https");
3 |
4 | const { getEnv } = require("./env");
5 | const { CLIENT_AUTHENTICATION_METHODS } = require("./constants");
6 | const setAppClientAuthentication = (reqConfig) => {
7 | if (!reqConfig) {
8 | throw new Error("missing reqConfig");
9 | }
10 | const {
11 | APP_CLIENT_ID,
12 | app_client_authentication_method,
13 | APP_CLIENT_SECRET,
14 | APP_JWTCA_KEY_ID,
15 | APP_JWTCA_PRIVATE_KEY,
16 | APP_MTLS_CERTIFICATE,
17 | APP_MTLS_PRIVATE_KEY,
18 | } = getEnv();
19 |
20 | const { data, headers, httpsAgent } = clientAuthentication(
21 | APP_CLIENT_ID,
22 | app_client_authentication_method,
23 | {
24 | privateKeyPEM: APP_JWTCA_PRIVATE_KEY,
25 | clientSecret: APP_CLIENT_SECRET,
26 | keyid: APP_JWTCA_KEY_ID,
27 | mtlsClientCertificate: APP_MTLS_CERTIFICATE,
28 | mtlsPrivateKey: APP_MTLS_PRIVATE_KEY,
29 | }
30 | );
31 |
32 | console.log({ data, headers });
33 | reqConfig.data = {
34 | ...reqConfig.data,
35 | ...data,
36 | };
37 |
38 | reqConfig.headers = {
39 | ...reqConfig.headers,
40 | ...headers,
41 | };
42 |
43 | if (httpsAgent) {
44 | reqConfig.httpsAgent = httpsAgent;
45 | } else {
46 | delete reqConfig;
47 | }
48 |
49 | return reqConfig;
50 | };
51 |
52 | const getManagementClientAuthentication = () => {
53 | const {
54 | AUTH0_MGMT_CLIENT_ID,
55 | AUTH0_MGMT_CLIENT_AUTHENTICATION_METHOD,
56 | AUTH0_MGMT_CLIENT_SECRET,
57 | AUTH0_MGMT_JWTCA_KEY_ID,
58 | AUTH0_MGMT_JWTCA_PRIVATE_KEY,
59 | } = getEnv();
60 |
61 | const { data } = clientAuthentication(
62 | AUTH0_MGMT_CLIENT_ID,
63 | [AUTH0_MGMT_CLIENT_AUTHENTICATION_METHOD],
64 | {
65 | privateKeyPEM: AUTH0_MGMT_JWTCA_PRIVATE_KEY,
66 | clientSecret: AUTH0_MGMT_CLIENT_SECRET,
67 | keyid: AUTH0_MGMT_JWTCA_KEY_ID,
68 | }
69 | );
70 |
71 | return data;
72 | };
73 |
74 | const clientAuthentication = (
75 | clientID,
76 | clientAuthenticationMethods,
77 | clientAuthenticationOptions
78 | ) => {
79 | const request = {};
80 |
81 | function addHeaders(headers) {
82 | for (const [headerName, headerValue] of Object.entries(headers)) {
83 | request.headers ??= {};
84 | request.headers[headerName] = headerValue;
85 | }
86 | }
87 |
88 | function addBody(params) {
89 | for (const [paramName, paramValue] of Object.entries(params)) {
90 | request.data ??= {};
91 | request.data[paramName] = paramValue;
92 | }
93 | }
94 | const AUTH0_DOMAIN = getEnv("AUTH0_DOMAIN");
95 | if (!Array.isArray(clientAuthenticationMethods)) {
96 | clientAuthenticationMethods = [clientAuthenticationMethods];
97 | }
98 | clientAuthenticationMethods.forEach((clientAuthenticationMethod) => {
99 | if (clientAuthenticationMethod === CLIENT_AUTHENTICATION_METHODS.CA_NONE) {
100 | return {};
101 | } else if (
102 | clientAuthenticationMethod ===
103 | CLIENT_AUTHENTICATION_METHODS.PRIVATE_KEY_JWT
104 | ) {
105 | console.log("using JWT client authentication");
106 | if (!clientID) {
107 | throw new Error("missing clientID");
108 | }
109 | const assertion = jwt.sign(
110 | {
111 | jti: "" + Date.now(),
112 | iat: Math.floor(Date.now() / 1000),
113 | },
114 | clientAuthenticationOptions.privateKeyPEM,
115 | {
116 | audience: `https://${AUTH0_DOMAIN}/`,
117 | issuer: clientID,
118 | subject: clientID,
119 | keyid: clientAuthenticationOptions.keyid,
120 | algorithm: "RS256",
121 | expiresIn: "1m",
122 | }
123 | );
124 |
125 | addBody({
126 | client_assertion_type:
127 | "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
128 | client_assertion: assertion,
129 | });
130 | } else if (
131 | clientAuthenticationMethod ===
132 | CLIENT_AUTHENTICATION_METHODS.CLIENT_SECRET_POST
133 | ) {
134 | console.log("using client_secret_post client authentication");
135 |
136 | addBody({
137 | client_secret: clientAuthenticationOptions.clientSecret,
138 | });
139 | } else if (
140 | clientAuthenticationMethod ===
141 | CLIENT_AUTHENTICATION_METHODS.CLIENT_SECRET_BASIC
142 | ) {
143 | console.log("using client_secret_basic client authentication");
144 | const credentials = Buffer.from(
145 | `${clientID}:${clientAuthenticationOptions.clientSecret}`,
146 | "utf-8"
147 | ).toString("base64");
148 | const header = `Basic ${credentials};`;
149 | addHeaders({
150 | authorization: header,
151 | });
152 | } else if (
153 | clientAuthenticationMethod === CLIENT_AUTHENTICATION_METHODS.CA_MTLS
154 | ) {
155 | console.log("using CA signed mTLS Client Authentication");
156 | /*
157 | const httpsAgent = new https.Agent({
158 | rejectUnauthorized: false,
159 | cert: clientAuthenticationOptions.mtlsClientCertificate,
160 | key: clientAuthenticationOptions.mtlsPrivateKey,
161 | });
162 |
163 | return { httpsAgent };*/
164 | addHeaders({
165 | "Client-Certificate": encodeURIComponent(clientAuthenticationOptions.mtlsClientCertificate),
166 | "Client-Certificate-CA-Verified": "SUCCESS",
167 | });
168 | } else if (
169 | clientAuthenticationMethod ===
170 | CLIENT_AUTHENTICATION_METHODS.SELF_SIGNED_MTLS
171 | ) {
172 | console.log("using self-signed signed mTLS Client Authentication");
173 | /*
174 | const httpsAgent = new https.Agent({
175 | rejectUnauthorized: false,
176 | cert: clientAuthenticationOptions.mtlsClientCertificate,
177 | key: clientAuthenticationOptions.mtlsPrivateKey,
178 | });
179 |
180 | return { httpsAgent };*/
181 |
182 | addHeaders({
183 | "Client-Certificate": encodeURIComponent(clientAuthenticationOptions.mtlsClientCertificate),
184 | "Client-Certificate-CA-Verified": "FAILED: this is a multi word reason",
185 | });
186 | } else {
187 | throw new Error("invalid client authentication config");
188 | }
189 | });
190 | console.log({ clientAuthenticationMethods, request });
191 |
192 | return request;
193 | };
194 |
195 | exports.clientAuthentication = clientAuthentication;
196 | exports.setAppClientAuthentication = setAppClientAuthentication;
197 | exports.getManagementClientAuthentication = getManagementClientAuthentication;
198 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | const jose = require("jose");
2 | const express = require("express");
3 | const jwtDecode = require("jwt-decode");
4 | const pemToJwk = require("pem-jwk").pem2jwk;
5 |
6 | const router = express.Router();
7 | const handlers = require("../lib/handlers");
8 | const authAPI = require("../lib/handlers/auth_api");
9 |
10 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
11 |
12 | const { authenticate, authenticateWithPar } = handlers.login;
13 | const { inviteFlow } = handlers.invite;
14 | const { getEnv } = require("../lib/env");
15 | const {
16 | APP_RESOURCE_SERVER_IDENTIFIER,
17 | AUDIENCES,
18 | RESPONSE_TYPES,
19 | } = require("../lib/constants");
20 |
21 | const { saveConfiguration } = handlers.configuration;
22 |
23 | router.get("/", async function (req, res, next) {
24 | try {
25 | const audienceList = [APP_RESOURCE_SERVER_IDENTIFIER, ...AUDIENCES];
26 |
27 | res.render("index", {
28 | acrValues: getEnv("acr_values"),
29 | audienceList,
30 | authorizationDetails: getEnv("authorization_details"),
31 | claims: getEnv("claims"),
32 | clientAuthenticationMethods: getEnv("client_authentication_methods_list"),
33 | jar_enabled: getEnv("jar_enabled"),
34 | login_hint: getEnv("login_hint"),
35 | owp: getEnv("owp"),
36 | par_enabled: getEnv("par_enabled"),
37 | pkce: getEnv("pkce"),
38 | pkceCodeChallengeMethodList: getEnv("pkce_code_challenge_method_list"),
39 | promptList: getEnv("prompt_list"),
40 | redirectURI: getEnv("redirect_uri"),
41 | responseModeList: getEnv("response_mode_list"),
42 | responseTypeList: RESPONSE_TYPES,
43 | scope: getEnv("scope"),
44 | selectedAudience: getEnv("audience"),
45 | selectedPkceCodeChallengeMethod: getEnv("pkce_code_challenge_method"),
46 | selectedPrompt: getEnv("prompt"),
47 | selectedResponseMode: getEnv("response_mode"),
48 | selectedResponseType: getEnv("response_type"),
49 | sendAuthorizationDetails: getEnv("send_authorization_details"),
50 | selectedClientAuthenticationMethod: getEnv(
51 | "app_client_authentication_method"
52 | ),
53 | title: "Fake SaaS App",
54 | });
55 | } catch (error) {
56 | return next(error);
57 | }
58 | });
59 |
60 | router.post("/login", (req, res) => {
61 | if (getEnv("par_enabled")) {
62 | return authenticateWithPar(req, res);
63 | }
64 | authenticate(req, res);
65 | });
66 | router.post("/invite", inviteFlow);
67 |
68 | router.get("/diag", (req, res) => {
69 | res.json({
70 | AUTH0_CLIENT_ID: process.env.AUTH0_CLIENT_ID,
71 | AUTH0_CLIENT_SECRET: process.env.AUTH0_CLIENT_SECRET.substr(0, 3) + "...",
72 | AUTH0_DOMAIN: process.env.AUTH0_DOMAIN,
73 | AUTH0_CALLBACK_URL: process.env.AUTH0_CALLBACK_URL,
74 | LOGOUT_URL: process.env.LOGOUT_URL,
75 | });
76 | });
77 |
78 | router.get("/logout", (req, res) => {
79 | req.session.user = null;
80 | delete req.session.user;
81 | res.redirect("/");
82 | });
83 |
84 | router.get("/loggedOut", (req, res) => {
85 | res.json({ status: "logged out" });
86 | });
87 |
88 | router.post(
89 | "/callback",
90 | (req, res, next) => {
91 | const {
92 | error,
93 | error_description,
94 | code,
95 | state,
96 | id_token,
97 | access_token,
98 | response,
99 | } = req.body;
100 |
101 | if (
102 | !state &&
103 | !code &&
104 | !error &&
105 | !error_description &&
106 | !id_token &&
107 | !access_token &&
108 | !response
109 | ) {
110 | res.redirect("/");
111 | }
112 |
113 | next();
114 | },
115 | callbackHandler,
116 | (req, res) => {
117 | res.redirect(req.session.returnTo || "/user");
118 | }
119 | );
120 |
121 | router.get(
122 | "/callback",
123 | (req, res, next) => {
124 | const { error, error_description, code, state, response } = req.query;
125 |
126 | if (!state && !code && !error && !error_description && !response) {
127 | // assume this is an implicit flow and the parameters are on the URL.
128 | // The front end will copy the params from the URL into a form and
129 | // POST them to /callback
130 | return res.render("callback", { title: "callback" });
131 | }
132 |
133 | next();
134 | },
135 | callbackHandler,
136 | (req, res) => {
137 | res.redirect(req.session.returnTo || "/user");
138 | }
139 | );
140 |
141 | router.get("/error", (req, res) => {
142 | const error = req.flash("error");
143 | const error_description = req.flash("error_description");
144 |
145 | delete req.session.user;
146 | delete req.session.code_verifier;
147 | delete req.session.state;
148 | delete req.session.returnTo;
149 |
150 | res.render("error", {
151 | error: error,
152 | error_description: error_description,
153 | });
154 | });
155 |
156 | router.get("/unauthorized", (req, res) => {
157 | res.render("unauthorized");
158 | });
159 |
160 | router.post("/saveconfiguration", saveConfiguration);
161 |
162 | router.get("/.well-known/jwks.json", async (req, res) => {
163 | const jwtcaJWK = await jose.exportJWK(
164 | await jose.importSPKI(getEnv("APP_JWTCA_PUBLIC_KEY"))
165 | );
166 | const jarJWK = await jose.exportJWK(
167 | await jose.importSPKI(getEnv("APP_JAR_PUBLIC_KEY"))
168 | );
169 | const keys = [
170 | {
171 | ...jarJWK,
172 | kid: getEnv("APP_JAR_KEY_ID"),
173 | },
174 | {
175 | ...jwtcaJWK,
176 | kid: getEnv("APP_JWTCA_KEY_ID"),
177 | },
178 | ];
179 | console.log("serving jwks.json", keys);
180 | res.status(200).setHeader("content-type", "application/jwk-set+json").json({
181 | keys,
182 | });
183 | });
184 |
185 | async function callbackHandler(req, res, next) {
186 | let source = Object.keys(req.body).length ? req.body : req.query;
187 |
188 | const response = source.response;
189 | if (response) {
190 | // we have a JWT response. Decode
191 | source = jwtDecode(response);
192 | }
193 |
194 | const {
195 | error,
196 | error_description,
197 | code,
198 | state,
199 | access_token,
200 | id_token: detached_signature,
201 | } = source;
202 |
203 | if (req.session.state && state !== req.session.state) {
204 | req.flash("error", "state mismatch");
205 | return res.redirect("/error");
206 | }
207 |
208 | req.session.state = null;
209 | delete req.session.state;
210 |
211 | if (error || error_description) {
212 | req.flash("error", error);
213 | req.flash("error_description", error_description);
214 | return res.redirect("/error");
215 | }
216 |
217 | try {
218 | let userData = {};
219 |
220 | let atData = {
221 | access_token,
222 | id_token: detached_signature,
223 | };
224 |
225 | if (detached_signature) {
226 | // TODO - detached signature, check s_hash, c_hash
227 | }
228 |
229 | if (code) {
230 | const params = {};
231 | if (req.session.code_verifier) {
232 | params.code_verifier = req.session.code_verifier;
233 | req.session.code_verifier = null;
234 | delete req.session.code_verifier;
235 | }
236 |
237 | atData = await authAPI.getAccessTokenFromCode(code, params);
238 | console.log({ atData });
239 | }
240 |
241 | if (atData.access_token) {
242 | userData = await authAPI.getUserInfo(atData.access_token);
243 | }
244 |
245 | req.session.user = {
246 | profile: userData,
247 | extraParams: {
248 | detached_signature: detached_signature,
249 | access_token: atData.access_token,
250 | refresh_token: atData.refresh_token,
251 | id_token: atData.id_token,
252 | },
253 | };
254 |
255 | next();
256 | } catch (error) {
257 | next(error);
258 | }
259 | }
260 |
261 | module.exports = router;
262 |
--------------------------------------------------------------------------------
/lib/handlers/bootstrap.js:
--------------------------------------------------------------------------------
1 | const { generateKeyPair: generateKeyPairCallback } = require("node:crypto");
2 | const { promisify } = require("node:util");
3 | const { getEnv, setEnv } = require("../env");
4 |
5 | const generateKeyPair = promisify(generateKeyPairCallback);
6 |
7 | const { makeApi2Request } = require("../api2");
8 | const {
9 | CLIENT_NAME_FOR_DEMO_APP,
10 | REQUIRED_SCOPES_FOR_BACKEND_CLIENT,
11 | APP_RESOURCE_SERVER_IDENTIFIER,
12 | APP_CALLBACK_URL,
13 | APP_LOGOUT_URL,
14 | APP_INITIATE_LOGIN_URL,
15 | APP_LOGO_URI,
16 | } = require("../constants");
17 |
18 | const getClientGrantId = async () => {
19 | const requestOptions = {
20 | path: `client-grants?client_id=${process.env.AUTH0_MGMT_CLIENT_ID}&audience=https://${process.env.AUTH0_DOMAIN}/api/v2/`,
21 | };
22 |
23 | try {
24 | const response = await makeApi2Request(requestOptions);
25 | if (response && response.length === 1) {
26 | return response[0].id;
27 | }
28 |
29 | // TODO: Better error message
30 | throw new Error("Could not find client grant");
31 | } catch (error) {
32 | console.error(error);
33 | // TODO: Better error, e.g. "make sure you setup the right client grant"
34 | console.error(`Error while getting client grant: ${error.message}`);
35 | }
36 | };
37 |
38 | const getClientGrant = async (clientGrantId) => {
39 | const requestOptions = {
40 | method: "get",
41 | path: `client-grants`,
42 | };
43 |
44 | try {
45 | const resp = await makeApi2Request(requestOptions);
46 | return resp.find((item) => item.id === clientGrantId);
47 | } catch (error) {
48 | console.error(error);
49 | // TODO: Better error, e.g. "make sure you setup the right client grant"
50 | console.error(`Error while getting client grants: ${error.message}`);
51 | }
52 | };
53 |
54 | const setRequiredClientGrant = async (clientGrantId) => {
55 | const grant = (await getClientGrant(clientGrantId)) || { scope: [] };
56 |
57 | const requestOptions = {
58 | method: "patch",
59 | path: `client-grants/${clientGrantId}`,
60 | data: {
61 | scope: [...REQUIRED_SCOPES_FOR_BACKEND_CLIENT, ...grant.scope].reduce(
62 | (scopes, currValue) => {
63 | if (scopes.indexOf(currValue) === -1) {
64 | scopes.push(currValue);
65 | }
66 | return scopes;
67 | },
68 | []
69 | ),
70 | },
71 | };
72 |
73 | try {
74 | await makeApi2Request(requestOptions);
75 | } catch (error) {
76 | console.error(error);
77 | // TODO: Better error, e.g. "make sure you setup the right client grant"
78 | console.error(`Error while updating client grant: ${error.message}`);
79 | }
80 | };
81 |
82 | const getAppClient = async () => {
83 | // TODO: Only works if less than 100 clients in a tenant
84 | const requestOptions = {
85 | path: "clients?page=0&per_page=100",
86 | };
87 |
88 | try {
89 | const response = await makeApi2Request(requestOptions);
90 | console.log({ response });
91 | const matchingClient = response.filter(
92 | (client) => client.name === CLIENT_NAME_FOR_DEMO_APP
93 | );
94 |
95 | if (matchingClient.length === 0) {
96 | return;
97 | }
98 |
99 | return matchingClient[0];
100 | } catch (error) {
101 | console.error(error);
102 | // TODO: Better error, e.g. "make sure you setup the right client grant"
103 | console.error(`Error while getting client grant: ${error.message}`);
104 | }
105 | };
106 |
107 | const generateJWTCAKeypair = async () => {
108 | const { publicKey, privateKey } = await generateKeyPair("rsa", {
109 | modulusLength: 4096,
110 | publicKeyEncoding: {
111 | type: "spki",
112 | format: "pem",
113 | },
114 | privateKeyEncoding: {
115 | type: "pkcs8",
116 | format: "pem",
117 | },
118 | });
119 |
120 | return { publicKey, privateKey };
121 | };
122 |
123 | const addKeyToClientCredentials = async (
124 | clientId,
125 | publicKey,
126 | alg = "RS256"
127 | ) => {
128 | const requestOptions = {
129 | method: "post",
130 | path: `clients/${clientId}/credentials`,
131 | data: {
132 | name: `key-${new Date().toISOString()}`,
133 | credential_type: "public_key",
134 | pem: publicKey,
135 | alg,
136 | },
137 | };
138 | try {
139 | const response = await makeApi2Request(requestOptions);
140 | return { kid: response.kid, id: response.id };
141 | } catch (error) {
142 | console.error(error);
143 | console.error(`Error while posting client credential: ${error.message}`);
144 | }
145 | };
146 |
147 | const patchClientToUseJWTCAKey = async (clientId, credentialId) => {
148 | const requestOptions = {
149 | method: "PATCH",
150 | path: `clients/${clientId}`,
151 | data: {
152 | token_endpoint_auth_method: null,
153 | client_authentication_methods: {
154 | private_key_jwt: {
155 | credentials: [{ id: credentialId }],
156 | },
157 | },
158 | jwt_configuration: {
159 | alg: "RS256"
160 | }
161 | },
162 | };
163 | try {
164 | await makeApi2Request(requestOptions);
165 | } catch (error) {
166 | console.error(error);
167 | console.error(`Error while patching client for JWTCA: ${error.message}`);
168 | }
169 | };
170 |
171 | const patchClientToUseClientSecret = async (clientId) => {
172 | const requestOptions = {
173 | method: "PATCH",
174 | path: `clients/${clientId}`,
175 | data: {
176 | token_endpoint_auth_method: "client_secret_post",
177 | client_authentication_methods: null,
178 | },
179 | };
180 | try {
181 | await makeApi2Request(requestOptions);
182 | } catch (error) {
183 | console.error(error);
184 | console.error(`Error while patching client for JWTCA: ${error.message}`);
185 | }
186 | };
187 |
188 | const generateNewAppClient = async () => {
189 | const requestOptions = {
190 | method: "post",
191 | path: "clients",
192 | data: {
193 | name: CLIENT_NAME_FOR_DEMO_APP,
194 | description: "Created by orgs demo",
195 | callbacks: [APP_CALLBACK_URL],
196 | allowed_logout_urls: [APP_LOGOUT_URL],
197 | initiate_login_uri: APP_INITIATE_LOGIN_URL,
198 | logo_uri: APP_LOGO_URI,
199 | organization_usage: "allow",
200 | oidc_conformant: true,
201 | },
202 | };
203 |
204 | try {
205 | const response = await makeApi2Request(requestOptions);
206 | return response;
207 | } catch (error) {
208 | console.error(error);
209 | // TODO: Better error, e.g. "make sure you setup the right client grant"
210 | console.error(`Error while getting client grant: ${error.message}`);
211 | }
212 | };
213 |
214 | const createAppClient = async () => {
215 | let appClient = await getAppClient();
216 | if (!appClient) {
217 | console.log("Demo app client does not exist. Creating...");
218 | appClient = await generateNewAppClient();
219 | }
220 | return appClient;
221 | };
222 |
223 | const getAppResourceServer = async () => {
224 | const requestOptions = {
225 | path: `resource-servers/${APP_RESOURCE_SERVER_IDENTIFIER}`,
226 | };
227 |
228 | try {
229 | const response = await makeApi2Request(requestOptions);
230 | return response;
231 | } catch (error) {
232 | if (error.data.statusCode === 404) {
233 | // expected if the demo resource server is not already created
234 | return;
235 | }
236 | console.error(error);
237 | // TODO: Better error, e.g. "make sure you setup the right client grant"
238 | console.error(`Error while getting resource server: ${error.message}`);
239 | }
240 | };
241 |
242 | const generateNewAppResourceServer = async () => {
243 | const requestOptions = {
244 | method: "post",
245 | path: "resource-servers",
246 | data: {
247 | name: APP_RESOURCE_SERVER_IDENTIFIER,
248 | identifier: APP_RESOURCE_SERVER_IDENTIFIER,
249 | scopes: [
250 | { value: "create:foo", description: "create:foo" },
251 | { value: "read:foo", description: "read:foo" },
252 | { value: "update:foo", description: "update:foo" },
253 | { value: "delete:foo", description: "delete:foo" },
254 | ],
255 | enforce_policies: true,
256 | token_dialect: "access_token_authz",
257 | skip_consent_for_verifiable_first_party_clients: true,
258 | allow_offline_access: true,
259 | },
260 | };
261 |
262 | try {
263 | const response = await makeApi2Request(requestOptions);
264 | return response;
265 | } catch (error) {
266 | console.error(error);
267 | // TODO: Better error, e.g. "make sure you setup the right client grant"
268 | console.error(`Error while getting client grant: ${error.message}`);
269 | }
270 | };
271 |
272 | const createAppResourceServer = async () => {
273 | let appResourceServer = await getAppResourceServer();
274 | if (!appResourceServer) {
275 | console.log("Demo app resource server does not exist. Creating...");
276 | appResourceServer = await generateNewAppResourceServer();
277 | }
278 | return appResourceServer;
279 | };
280 |
281 | const bootstrapProcess = async () => {
282 | console.log(">>> Bootstrapping Demo <<<");
283 | console.log("----- Get Client Grant");
284 | const clientGrantId = await getClientGrantId();
285 |
286 | console.log("----- Update Client Grant");
287 | await setRequiredClientGrant(clientGrantId);
288 |
289 | if (!getEnv("APP_CLIENT_ID")) {
290 | console.log("----- Create App Client");
291 | const appClient = await createAppClient();
292 | await setEnv("APP_CLIENT_ID", appClient.client_id, { write: true });
293 | if (appClient.client_secret) {
294 | await setEnv("APP_CLIENT_SECRET", appClient.client_secret, {
295 | write: true,
296 | });
297 | }
298 | }
299 |
300 | console.log("----- Create Resource Server");
301 | await createAppResourceServer();
302 |
303 | if (!getEnv("APP_JWTCA_PUBLIC_KEY")) {
304 | console.log("--- Generating JWTCA keys");
305 | const { publicKey, privateKey } = await generateJWTCAKeypair();
306 | const { kid, id } = await addKeyToClientCredentials(
307 | getEnv("APP_CLIENT_ID"),
308 | publicKey
309 | );
310 | await setEnv("APP_JWTCA_PUBLIC_KEY", publicKey, { write: true });
311 | await setEnv("APP_JWTCA_PRIVATE_KEY", privateKey, { write: true });
312 | await setEnv("APP_JWTCA_KEY_ID", kid, { write: true });
313 | await setEnv("APP_JWTCA_CREDENTIAL_ID", id, { write: true });
314 | }
315 |
316 | const appClientAuthMethod = getEnv("APP_CLIENT_AUTHENTICATION_METHOD");
317 | console.log({appClientAuthMethod})
318 | if (appClientAuthMethod === "jwtca") {
319 | await patchClientToUseJWTCAKey(
320 | getEnv("APP_CLIENT_ID"),
321 | getEnv("APP_JWTCA_CREDENTIAL_ID")
322 | );
323 | } else if (appClientAuthMethod === "client_secret_post") {
324 | await patchClientToUseClientSecret(getEnv("APP_CLIENT_ID"));
325 | }
326 |
327 | if (!getEnv("APP_JAR_PUBLIC_KEY")) {
328 | console.log("--- Generating JAR keys");
329 | const { publicKey, privateKey } = await generateJWTCAKeypair();
330 | const { kid, id } = await addKeyToClientCredentials(
331 | getEnv("APP_CLIENT_ID"),
332 | publicKey,
333 | getEnv("APP_JAR_KEY_ALG") || "PS256"
334 | );
335 | await setEnv("APP_JAR_PUBLIC_KEY", publicKey, { write: true });
336 | await setEnv("APP_JAR_PRIVATE_KEY", privateKey, { write: true });
337 | await setEnv("APP_JAR_KEY_ID", kid, { write: true });
338 | await setEnv("APP_JAR_CREDENTIAL_ID", id, { write: true });
339 | }
340 |
341 | console.log("!!! REMEMBER TO ADD 127.0.0.1 myapp.com to /etc/hosts !!!");
342 | console.log(">>> Bootstrap Complete <<<");
343 | };
344 |
345 | module.exports = {
346 | addKeyToClientCredentials,
347 | createAppClient,
348 | createAppResourceServer,
349 | generateJWTCAKeypair,
350 | getClientGrantId,
351 | patchClientToUseClientSecret,
352 | patchClientToUseJWTCAKey,
353 | setRequiredClientGrant,
354 | bootstrapProcess,
355 | };
356 |
--------------------------------------------------------------------------------