はじめに
この記事はExpressフレームワークでNode.jsのApplicationを作成して、Auth0で認証機能を追加する手順で、こちらの原文Part1、こちらの原文Parrt2を元に作成しています。ユーザ認証とルートの保護はPassport.jsとAuth0、フロントエンドのレンダリングはPugを利用しています。nodeとnpmのインストール、Auth0の無料アカウントの取得とテナントの作成が完了していることが前提となっています。まだの方はこちらの記事を参照の上ご準備をお願いします。
完成版のソースコードはここで公開しています。
検証環境
-
OS :
macOS Mojave 10.14.6
-
node :
10.15.3
-
npm :
6.11.3
手順
Node.jsのプロジェクト作成・Express Applicationの作成
Applicationのプロジェクトディレクトを作成します。
$ mkdir whatabyte-portal
$ cd whatabyte-portal
Nodeを初期化します。
$pwd
~/whatabyte-portal
$ npm init -y
プライマリエントリーポイントを作成します。
$pwd
~/whatabyte-portal
$ touch index.js
ソースコードの修正を即時反映するためにnodemonをインストールします。
$pwd
~/whatabyte-portal
$ npm install --save-dev nodemon
package.jsonの"scripts"に"dev": "nodemon ./index.js"を追加します。以下、追加後のpackage.jsonです。
{
"name": "whatabyte-portal",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon ./index.js",
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"browser-sync": "^2.26.7",
"nodemon": "^1.19.1"
},
"dependencies": {
"dotenv": "^8.1.0",
"express": "^4.17.1",
"express-session": "^1.16.2",
"passport": "^0.4.0",
"passport-auth0": "^1.2.1",
"pug": "^2.0.4"
}
}
Express frameworkをインストールします。
$pwd
~/whatabyte-portal
$ npm install express
index.jsを修正してexpress moduleをインポート、ポート8000でHttpリクエストをListenします。
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
app.get("/", (req, res) => {
res.status(200).send("WHATABYTE: Food For Devs");
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
Applicationを起動してChromeでhttp://localhosot:8000
にアクセスします。以下の文字が表示されていれば成功です。
$ npm run dev

Pugテンプレートの作成
Pugをインストールします。
$ pwd
~/whatabyte-portal
$ npm install pug
Pugのテンプレートを保管するディレクトリを作成します。
$ pwd
~/whatabyte-portal
$ mkdir views
レイアウトテンプレートを作成します。
$ pwd
~/whatabyte-portal
$ touch views/layout.pug
layout.pugを修正します。以下、修正後です。
block variables
doctype html
html
head
meta(charset="utf-8")
link(rel="shortcut icon", href="/favicon.ico")
meta(name="viewport", content="width=device-width, initial-scale=1, shrink-to-fit=no")
meta(name="theme-color", content="#000000")
title #{title} | WHATABYTE
body
div#root
block layout-content
プライマリテンプレートを作成します。
$ pwd
~/whatabyte-portal
$ touch views/index.pug
index.pugを修正します。以下、修正後です。
extends layout
block layout-content
div.View
h1.Banner WHATABYTE
div.Message
div.Title
h3 Making the Best
h1 Food For Devs
span.Details Access the WHATABYTE Team Portal
div.NavButtons
if isAuthenticated
a(href="/user")
div.NavButton Just dive in!
else
a(href="/login")
div.NavButton Log in
index.jsを修正してViewerとしてPugを使うよう指定します。以下、修正後です。
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
Chromeでhttp://localhosot:8000
にアクセスします。以下の画面が表示されていれば成功です。
Browsersyncを用いたライブリローディング機能の追加
Browsersyncをインストールします。
$ pwd
~/whatabyte-portal
$ npm install --save-dev browser-sync
package.jsonの"scripts"にBrowsersyncを有効化するために"ui:...を追加します。以下、追加後のpackage.jsonです。
{
"name": "whatabyte-portal",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon ./index.js",
"ui": "browser-sync start --proxy=localhost:8000 --files='**/*.css, **/*.pug, **/*.js' --ignore=node_modules --reload-delay 10 --no-ui --no-notify"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"browser-sync": "^2.26.7",
"nodemon": "^1.19.1"
},
"dependencies": {
"dotenv": "^8.1.0",
"express": "^4.17.1",
"express-session": "^1.16.2",
"passport": "^0.4.0",
"passport-auth0": "^1.2.1",
"pug": "^2.0.4"
}
}
Applicationを起動したターミナルと別のターミナルでBrowsersyncを起動します。
$ pwd
~/whatabyte-portal
$ npm run ui
Chromeでhttp://localhost:3000
が新しいタブで開いてターミナルで以下が出力されていれば成功です。
$ npm run ui > whatabyte-portal@1.0.0 ui /Users/hisashiyamaguchi/whatabyte-portal
> browser-sync start --proxy=localhost:8000 --files='**/*.css, **/*.pug, **/*.js' --ignore=node_modules --reload-delay 10 --no-ui --no-notify
[Browsersync] Proxying: http://localhost:8000
[Browsersync] Access URLs:
----------------------------------
Local: http://localhost:3000
External: http://10.236.4.205:3000
----------------------------------
[Browsersync] Watching files...
静的コンテンツの作成
静的コンテンツを保管するディレクトリを作成します。
$ pwd
~/whatabyte-portal
$ mkdir public
index.jsを修正して静的コンテンツの保管ディレクトリを指定します。以下、修正後です。
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
スタイルシートを作成します。
$ pwd
~/whatabyte-portal
$ touch public/style.css
body {
background: aqua;
}
layout.pugを修正してスタイルシートをリンクします。以下、修正後です。
block variables
doctype html
html
head
meta(charset="utf-8")
link(rel="shortcut icon", href="/favicon.ico")
meta(name="viewport", content="width=device-width, initial-scale=1, shrink-to-fit=no")
meta(name="theme-color", content="#000000")
title #{title} | WHATABYTE
link(rel="stylesheet" href="/style.css")
body
div#root
block layout-content
style.cssを修正してルールを追加します。以下、修正後です。
@import url("https://fonts.googleapis.com/css?family=Raleway:800|Merriweather+Sans|Share+Tech+Mono");
:root {
--logo-font: "Share Tech Mono", monospace;
--header-font: "Raleway", sans-serif;
--core-font: "Merriweather Sans", sans-serif;
--primary: #ffffff;
--secondary: #2a3747;
--highlight: #fa4141;
--ui-shawdow: 0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 4px 5px 0 rgba(0, 0, 0, 0.06), 0 1px 10px 0 rgba(0, 0, 0, 0.08);
fill: rgba(0, 0, 0, 0.54);
--ui-shawdow-border: 1px solid rgba(0, 0, 0, 0.14);
}
* {
box-sizing: border-box;
}
html,
body,
# root {
height: 100%;
width: 100%;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
}
h1,
h2,
h3 {
font-family: var(--header-font);
text-transform: uppercase;
padding: 0;
margin: 0;
color: var(--secondary);
}
h1 {
font-size: 40px;
}
a {
color: inherit;
text-decoration: none;
cursor: pointer;
user-select: none;
}
# root {
display: flex;
flex-direction: column;
}
.View {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
height: 100%;
width: 100%;
padding: 20px;
background-size: cover;
font-family: var(--core-font);
}
.Banner {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
border-radius: 5px;
overflow: hidden;
background: white;
padding: 15px;
font-family: var(--logo-font);
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
.Message {
background: white;
padding: 30px;
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
.Message > .Title {
padding-bottom: 20px;
}
.Message > .Details {
display: flex;
flex-direction: column;
line-height: 1.5em;
}
.NavButtons {
display: flex;
width: 100%;
justify-content: space-around;
align-items: center;
padding: 0 20px;
}
.NavButton {
display: flex;
justify-content: center;
align-items: center;
height: 55px;
width: 150px;
background: var(--highlight);
border-radius: 30px;
font-size: 16px;
font-weight: bold;
color: white;
text-transform: capitalize;
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
Chromeが自動的にリロードされて以下の画面が表示されていれば成功です。
画像イメージの追加
こちらの画像をpublic/dev-food.jpgとして保存します。
style.cssを修正して画像イメージを取り込みます。以下、修正後です。
@import url("https://fonts.googleapis.com/css?family=Raleway:800|Merriweather+Sans|Share+Tech+Mono");
:root {
--logo-font: "Share Tech Mono", monospace;
--header-font: "Raleway", sans-serif;
--core-font: "Merriweather Sans", sans-serif;
--primary: #ffffff;
--secondary: #2a3747;
--highlight: #fa4141;
--ui-shawdow: 0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 4px 5px 0 rgba(0, 0, 0, 0.06), 0 1px 10px 0 rgba(0, 0, 0, 0.08);
fill: rgba(0, 0, 0, 0.54);
--ui-shawdow-border: 1px solid rgba(0, 0, 0, 0.14);
}
* {
box-sizing: border-box;
}
html,
body,
# root {
height: 100%;
width: 100%;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
}
h1,
h2,
h3 {
font-family: var(--header-font);
text-transform: uppercase;
padding: 0;
margin: 0;
color: var(--secondary);
}
h1 {
font-size: 40px;
}
a {
color: inherit;
text-decoration: none;
cursor: pointer;
user-select: none;
}
# root {
display: flex;
flex-direction: column;
}
.View {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
height: 100%;
width: 100%;
padding: 20px;
background-size: cover;
font-family: var(--core-font);
}
.Banner {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
border-radius: 5px;
overflow: hidden;
background: white;
padding: 15px;
font-family: var(--logo-font);
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
.Message {
background: white;
padding: 30px;
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
.Message > .Title {
padding-bottom: 20px;
}
.Message > .Details {
display: flex;
flex-direction: column;
line-height: 1.5em;
}
.NavButtons {
display: flex;
width: 100%;
justify-content: space-around;
align-items: center;
padding: 0 20px;
}
.NavButton {
display: flex;
justify-content: center;
align-items: center;
height: 55px;
width: 150px;
background: var(--highlight);
border-radius: 30px;
font-size: 16px;
font-weight: bold;
color: white;
text-transform: capitalize;
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
.WelcomeView {
background: url("dev-food.jpg") left bottom;
background-size: cover;
}
index.pugを修正して追加したスタイルシートのクラスを指定します。以下、修正後です。
extends layout
block layout-content
div.View.WelcomeView
h1.Banner WHATABYTE
div.Message
div.Title
h3 Making the Best
h1 Food For Devs
span.Details Access the WHATABYTE Team Portal
div.NavButtons
if isAuthenticated
a(href="/user")
div.NavButton Just dive in!
else
a(href="/login")
div.NavButton Log in
ユーザプロファイル用の画面を作成します。
$ pwd
~/whatabyte-portal
touch views/user.pug
extends layout
block layout-content
div.View.UserView
h1.Banner Howdy, #{userProfile.nickname}!
div.Message
div.Title
h3 Making Us the Best
h1 Teammate Profile
pre.Details=JSON.stringify(userProfile, null, 2)
div.NavButtons
a(href="/logout")
div.NavButton Log out
index.jsを修正してユーザプロファイルのルートを追加します。以下、修正後です。
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.get("/user", (req, res) => {
res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
こちらの画像をpublic/team.jpgとして保存します。
style.cssを修正して画像イメージを取り込みます。以下、修正後です。
@import url("https://fonts.googleapis.com/css?family=Raleway:800|Merriweather+Sans|Share+Tech+Mono");
:root {
--logo-font: "Share Tech Mono", monospace;
--header-font: "Raleway", sans-serif;
--core-font: "Merriweather Sans", sans-serif;
--primary: #ffffff;
--secondary: #2a3747;
--highlight: #fa4141;
--ui-shawdow: 0 2px 4px -1px rgba(0, 0, 0, 0.06),
0 4px 5px 0 rgba(0, 0, 0, 0.06), 0 1px 10px 0 rgba(0, 0, 0, 0.08);
fill: rgba(0, 0, 0, 0.54);
--ui-shawdow-border: 1px solid rgba(0, 0, 0, 0.14);
}
* {
box-sizing: border-box;
}
html,
body,
# root {
height: 100%;
width: 100%;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
}
h1,
h2,
h3 {
font-family: var(--header-font);
text-transform: uppercase;
padding: 0;
margin: 0;
color: var(--secondary);
}
h1 {
font-size: 40px;
}
a {
color: inherit;
text-decoration: none;
cursor: pointer;
user-select: none;
}
# root {
display: flex;
flex-direction: column;
}
.View {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
height: 100%;
width: 100%;
padding: 20px;
background-size: cover;
font-family: var(--core-font);
}
.Banner {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
border-radius: 5px;
overflow: hidden;
background: white;
padding: 15px;
font-family: var(--logo-font);
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
.Message {
background: white;
padding: 30px;
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
.Message > .Title {
padding-bottom: 20px;
}
.Message > .Details {
display: flex;
flex-direction: column;
line-height: 1.5em;
}
.NavButtons {
display: flex;
width: 100%;
justify-content: space-around;
align-items: center;
padding: 0 20px;
}
.NavButton {
display: flex;
justify-content: center;
align-items: center;
height: 55px;
width: 150px;
background: var(--highlight);
border-radius: 30px;
font-size: 16px;
font-weight: bold;
color: white;
text-transform: capitalize;
border-bottom: var(--ui-shawdow-border);
box-shadow: var(--ui-shawdow);
}
.WelcomeView {
background: url("dev-food.jpg") left bottom;
background-size: cover;
}
.UserView {
background: url("team.jpg") left bottom;
background-size: cover;
}
Chromeでhttp://localhost:3000/user
にアクセスします。以下の画面が表示されれば成功です。

Passportのセットアップ
Passport.js、 passport-auth0等、必要なパッケージをインストールします。passport-auth0はAuth0のPassport用認証ストラテジです。
$ pwd
~/whatabyte-portal
$ npm install passport passport-auth0 express-session dotenv --save
index.jsを修正してexpress-sessionをインポートします。以下、修正後です。
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
const expressSession = require("express-session");
const session = {
secret: "LoxodontaElephasMammuthusPalaeoloxodonPrimelephas",
cookie: {},
resave: false,
saveUninitialized: false
};
if (app.get("env") === "production") {
session.cookie.secure = true; // Serve secure cookies, requires HTTPS
}
app.use(expressSession(session));
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.get("/user", (req, res) => {
res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
.envファイルを作成、index.jsを修正してdotenv環境変数をインポートします。
$ pwd
~/whatabyte-portal
$ touch .env
以下、修正後のindex.jsです。
require("dotenv").config();
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
const expressSession = require("express-session");
const session = {
secret: "LoxodontaElephasMammuthusPalaeoloxodonPrimelephas",
cookie: {},
resave: false,
saveUninitialized: false
};
if (app.get("env") === "production") {
session.cookie.secure = true; // Serve secure cookies, requires HTTPS
}
app.use(expressSession(session));
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.get("/user", (req, res) => {
res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
index.jsを修正してpassport, passport-auth0をインポートします。以下、修正後です。
require("dotenv").config();
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
const expressSession = require("express-session");
const passport = require("passport");
const Auth0Strategy = require("passport-auth0");
const session = {
secret: "LoxodontaElephasMammuthusPalaeoloxodonPrimelephas",
cookie: {},
resave: false,
saveUninitialized: false
};
if (app.get("env") === "production") {
session.cookie.secure = true; // Serve secure cookies, requires HTTPS
}
app.use(expressSession(session));
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.get("/user", (req, res) => {
res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
index.jsを修正してAuth0Strategyを定義します。以下、修正後です。
require("dotenv").config();
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
const expressSession = require("express-session");
const passport = require("passport");
const Auth0Strategy = require("passport-auth0");
const session = {
secret: "LoxodontaElephasMammuthusPalaeoloxodonPrimelephas",
cookie: {},
resave: false,
saveUninitialized: false
};
const strategy = new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL:
process.env.AUTH0_CALLBACK_URL || "http://localhost:3000/callback"
},
function(accessToken, refreshToken, extraParams, profile, done) {
/**
* Access tokens are used to authorize users to an API
* (resource server)
* accessToken is the token to call the Auth0 API
* or a secured third-party API
* extraParams.id_token has the JSON Web Token
* profile has all the information from the user
*/
return done(null, profile);
}
);
if (app.get("env") === "production") {
session.cookie.secure = true; // Serve secure cookies, requires HTTPS
}
app.use(expressSession(session));
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.get("/user", (req, res) => {
res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
index.jsを修正して定義したAuth0StrategyをExpressで利用するようにします。以下、修正後です。
require("dotenv").config();
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
const expressSession = require("express-session");
const passport = require("passport");
const Auth0Strategy = require("passport-auth0");
const session = {
secret: "LoxodontaElephasMammuthusPalaeoloxodonPrimelephas",
cookie: {},
resave: false,
saveUninitialized: false
};
const strategy = new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL:
process.env.AUTH0_CALLBACK_URL || "http://localhost:3000/callback"
},
function(accessToken, refreshToken, extraParams, profile, done) {
/**
* Access tokens are used to authorize users to an API
* (resource server)
* accessToken is the token to call the Auth0 API
* or a secured third-party API
* extraParams.id_token has the JSON Web Token
* profile has all the information from the user
*/
return done(null, profile);
}
);
if (app.get("env") === "production") {
session.cookie.secure = true; // Serve secure cookies, requires HTTPS
}
app.use(expressSession(session));
passport.use(strategy);
app.use(passport.initialize());
app.use(passport.session());
app.use(expressSession(session));
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.get("/user", (req, res) => {
res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
index.jsを修正してセッションからユーザデータを取得します。以下、修正後です。
require("dotenv").config();
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
const expressSession = require("express-session");
const passport = require("passport");
const Auth0Strategy = require("passport-auth0");
const session = {
secret: "LoxodontaElephasMammuthusPalaeoloxodonPrimelephas",
cookie: {},
resave: false,
saveUninitialized: false
};
const strategy = new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL:
process.env.AUTH0_CALLBACK_URL || "http://localhost:3000/callback"
},
function(accessToken, refreshToken, extraParams, profile, done) {
/**
* Access tokens are used to authorize users to an API
* (resource server)
* accessToken is the token to call the Auth0 API
* or a secured third-party API
* extraParams.id_token has the JSON Web Token
* profile has all the information from the user
*/
return done(null, profile);
}
);
if (app.get("env") === "production") {
session.cookie.secure = true; // Serve secure cookies, requires HTTPS
}
app.use(expressSession(session));
passport.use(strategy);
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
app.use(expressSession(session));
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.get("/user", (req, res) => {
res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
Expressの認証エンドポイント作成
Expressの認証エンドポイントとしてauth.jsを作成します。
$ pwd
~/whatabyte-portal
$ touch auth.js
require("dotenv").config();
const express = require("express");
const router = express.Router();
const passport = require("passport");
const util = require("util");
const url = require("url");
const querystring = require("querystring");
auth.jsを修正して"GET /login"エンドポイントを追加します。以下、修正後です。
require("dotenv").config();
const express = require("express");
const router = express.Router();
const passport = require("passport");
const util = require("util");
const url = require("url");
const querystring = require("querystring");
router.get(
"/login",
passport.authenticate("auth0", {
scope: "openid email profile"
}),
(req, res) => {
res.redirect("/");
}
);
auth.jsを修正して"GET /callback"エンドポイントを追加します。以下、修正後です。
require("dotenv").config();
const express = require("express");
const router = express.Router();
const passport = require("passport");
const util = require("util");
const url = require("url");
const querystring = require("querystring");
// GET /login
router.get(
"/login",
passport.authenticate("auth0", {
scope: "openid email profile"
}),
(req, res) => {
res.redirect("/");
}
);
// GET /callback
router.get("/callback", (req, res, next) => {
passport.authenticate("auth0", (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.redirect("/login");
}
req.logIn(user, (err) => {
if (err) {
return next(err);
}
const returnTo = req.session.returnTo;
delete req.session.returnTo;
res.redirect(returnTo || "/");
});
})(req, res, next);
});
auth.jsを修正して"GET /logout"エンドポイントを追加します。以下、修正後です。
require("dotenv").config();
const express = require("express");
const router = express.Router();
const passport = require("passport");
const util = require("util");
const url = require("url");
const querystring = require("querystring");
// GET /login
router.get(
"/login",
passport.authenticate("auth0", {
scope: "openid email profile"
}),
(req, res) => {
res.redirect("/");
}
);
// GET /callback
router.get("/callback", (req, res, next) => {
passport.authenticate("auth0", (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.redirect("/login");
}
req.logIn(user, (err) => {
if (err) {
return next(err);
}
const returnTo = req.session.returnTo;
delete req.session.returnTo;
res.redirect(returnTo || "/");
});
})(req, res, next);
});
// GET /logout
router.get("/logout", (req, res) => {
req.logOut();
let returnTo = req.protocol + "://" + req.hostname;
const port = req.connection.localPort;
if (port !== undefined && port !== 80 && port !== 443) {
returnTo =
process.env.NODE_ENV === "production"
? `${returnTo}/`
: `${returnTo}:${port}/`;
}
const logoutURL = new URL(
util.format("https://%s/logout", process.env.AUTH0_DOMAIN)
);
const searchString = querystring.stringify({
client_id: process.env.AUTH0_CLIENT_ID,
returnTo: returnTo
});
logoutURL.search = searchString;
res.redirect(logoutURL);
});
auth.jsを修正して定義したルートをエクスポートします。以下、修正後です。
require("dotenv").config();
const express = require("express");
const router = express.Router();
const passport = require("passport");
const util = require("util");
const url = require("url");
const querystring = require("querystring");
// GET /login
router.get(
"/login",
passport.authenticate("auth0", {
scope: "openid email profile"
}),
(req, res) => {
res.redirect("/");
}
);
// GET /callback
router.get("/callback", (req, res, next) => {
passport.authenticate("auth0", (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.redirect("/login");
}
req.logIn(user, (err) => {
if (err) {
return next(err);
}
const returnTo = req.session.returnTo;
delete req.session.returnTo;
res.redirect(returnTo || "/");
});
})(req, res, next);
});
// GET /logout
router.get("/logout", (req, res) => {
req.logOut();
let returnTo = req.protocol + "://" + req.hostname;
const port = req.connection.localPort;
if (port !== undefined && port !== 80 && port !== 443) {
returnTo =
process.env.NODE_ENV === "production"
? `${returnTo}/`
: `${returnTo}:${port}/`;
}
const logoutURL = new URL(
util.format("https://%s/logout", process.env.AUTH0_DOMAIN)
);
const searchString = querystring.stringify({
client_id: process.env.AUTH0_CLIENT_ID,
returnTo: returnTo
});
logoutURL.search = searchString;
res.redirect(logoutURL);
});
module.exports = router;
index.jsを修正して認証ルートをインポート、マウントします。以下、修正後です。
require("dotenv").config();
const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || "8000";
const expressSession = require("express-session");
const passport = require("passport");
const Auth0Strategy = require("passport-auth0");
const authRouter = require("./auth");
const session = {
secret: "LoxodontaElephasMammuthusPalaeoloxodonPrimelephas",
cookie: {},
resave: false,
saveUninitialized: false
};
const strategy = new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
callbackURL:
process.env.AUTH0_CALLBACK_URL || "http://localhost:3000/callback"
},
function(accessToken, refreshToken, extraParams, profile, done) {
/**
* Access tokens are used to authorize users to an API
* (resource server)
* accessToken is the token to call the Auth0 API
* or a secured third-party API
* extraParams.id_token has the JSON Web Token
* profile has all the information from the user
*/
return done(null, profile);
}
);
if (app.get("env") === "production") {
session.cookie.secure = true; // Serve secure cookies, requires HTTPS
}
app.use(expressSession(session));
passport.use(strategy);
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
app.use(expressSession(session));
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.use(express.static(path.join(__dirname, "public")));
app.use((req, res, next) => {
res.locals.isAuthenticated = req.isAuthenticated();
next();
});
app.use("/", authRouter);
app.get("/", (req, res) => {
res.render("index", { title: "Home" });
});
app.get("/user", (req, res) => {
res.render("user", { title: "Profile", userProfile: { nickname: "Auth0" } });
});
app.listen(port, () => {
console.log(`Listening to requests on http://localhost:${port}`);
});
Auth0にApplicationを登録
Auth0のダッシュボードにログイン、左ペインの"Applications"をクリックして右上の"CREATE APPLICATION"を押します。
"Name"に任意の名前を入力、"Choose an application type"で"Regular Web Applications"を選択して”CREATE”を押します。
"Settings"タブをクリック、"Allowed Callback URLs"に"http://localhost:3000/callback
, http://localhost:8000/callback
"を、"Allowed Logout URLs"に"http://localhost:3000/
, http://localhost:8000/
"を入力して"SAVE CHANGES"を押します。
.envを修正して環境変数を設定します。環境変数は"Settings"タブから確認できます。
AUTH0_CLIENT_ID=xxxx
AUTH0_DOMAIN=mokomoko.auth0.com
AUTH0_CLIENT_SECRET=xxxx
ApplicationとBrowsersyncを起動して"Log In"ボタンを押します。Auth0のログインウィジェットが表示されれば成功です。
$ pwd
~/whatabyte-portal
$ npm run dev
$ pwd
~/whatabyte-portal
$ npm run ui

任意のアカウントでログインします。認可ダイアログで認可して下記の画面が表示されれば成功です。


おわりに
ありとあらゆるタイプのApplicationが毎日どこがで誰かに開発されていますが、認証・認可の機能を必要としないApplicationはまず存在しないため、認証・認可の実装は必須タスクと言えるでしょう。しかしながら、ソフトウェア開発者は認証・認可を実装することが目的ではなく、ビジネスバリューを実装することが目的です。認証・認可にまつわるワークロードは可能な限り"0"にして頂きたいですね。