スタイリッシュなNode Expressアプリケーションを作成してAuth0で認証機能を追加する

# はじめに
この記事はExpressフレームワークでNode.jsのApplicationを作成して、Auth0で認証機能を追加する手順で、[こちらの原文Part1](https://auth0.com/blog/create-a-simple-and-stylish-node-express-app/)、[こちらの原文Parrt2](https://auth0.com/blog/create-a-simple-and-secure-node-express-app/)を元に作成しています。ユーザ認証とルートの保護はPassport.jsとAuth0、フロントエンドのレンダリングはPugを利用しています。nodeとnpmのインストール、Auth0の無料アカウントの取得とテナントの作成が完了していることが前提となっています。まだの方は[こちら](https://qiita.com/furuth/items/8826b66a3ec2b2b14222)の記事を参照の上ご準備をお願いします。
完成版のソースコードは[ここ](https://github.com/auth0-blog/whatabyte-portal-node-express)で公開しています。

# 検証環境
- **OS** : `macOS Mojave 10.14.6`
- **node** : `10.15.3`
- **npm** : `6.11.3`

# 手順
## Node.jsのプロジェクト作成・Express Applicationの作成
Applicationのプロジェクトディレクトを作成します。

```console
$ mkdir whatabyte-portal
$ cd whatabyte-portal
```

Nodeを初期化します。

```console
$pwd
~/whatabyte-portal
$ npm init -y
```

プライマリエントリーポイントを作成します。

```console
$pwd
~/whatabyte-portal
$ touch index.js
```

ソースコードの修正を即時反映するためにnodemonをインストールします。

```console
$pwd
~/whatabyte-portal
$ npm install --save-dev nodemon
```

package.jsonの"scripts"に"dev": "nodemon ./index.js"を追加します。以下、追加後のpackage.jsonです。

```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をインストールします。

```console
$pwd
~/whatabyte-portal
$ npm install express
```

index.jsを修正してexpress moduleをインポート、ポート8000でHttpリクエストをListenします。

```jsx:index.js
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`にアクセスします。以下の文字が表示されていれば成功です。

```console
$ npm run dev
```

<img src="https://i.imgur.com/3dzd10j.png" width=70%>

## Pugテンプレートの作成
Pugをインストールします。

```console
$ pwd
~/whatabyte-portal
$ npm install pug
```

Pugのテンプレートを保管するディレクトリを作成します。

```console
$ pwd
~/whatabyte-portal
$ mkdir views
```

レイアウトテンプレートを作成します。

```console
$ pwd
~/whatabyte-portal
$ touch views/layout.pug
```

layout.pugを修正します。以下、修正後です。

```jsx: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
```

プライマリテンプレートを作成します。

```console
$ pwd
~/whatabyte-portal
$ touch views/index.pug
```

index.pugを修正します。以下、修正後です。

```jsx: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を使うよう指定します。以下、修正後です。

```jsx: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.get("/", (req, res) => {
  res.render("index", { title: "Home" });
});

app.listen(port, () => {
  console.log(`Listening to requests on http://localhost:${port}`);
});
```

Chromeで`http://localhosot:8000`にアクセスします。以下の画面が表示されていれば成功です。
<img src="https://i.imgur.com/PDYsJna.png" width=70%>

## Browsersyncを用いたライブリローディング機能の追加
Browsersyncをインストールします。

```console
$ pwd
~/whatabyte-portal
$ npm install --save-dev browser-sync
```

package.jsonの"scripts"にBrowsersyncを有効化するために"ui:...を追加します。以下、追加後のpackage.jsonです。

```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を起動します。

```console
$ pwd
~/whatabyte-portal
$ npm run ui
```

Chromeで`http://localhost:3000`が新しいタブで開いてターミナルで以下が出力されていれば成功です。

```console
$ 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...
```

## 静的コンテンツの作成
静的コンテンツを保管するディレクトリを作成します。

```console
$ pwd
~/whatabyte-portal
$ mkdir public
```

index.jsを修正して静的コンテンツの保管ディレクトリを指定します。以下、修正後です。

```jsx: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}`);
});
```

スタイルシートを作成します。

```console
$ pwd
~/whatabyte-portal
$ touch public/style.css
```

```css:style.css
body {
  background: aqua;
}
```

layout.pugを修正してスタイルシートをリンクします。以下、修正後です。

```jsx: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を修正してルールを追加します。以下、修正後です。

```css: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が自動的にリロードされて以下の画面が表示されていれば成功です。
<img src="https://i.imgur.com/1uwXG5h.png" width=70%>

## 画像イメージの追加
[こちらの画像](https://cdn.auth0.com/blog/whatabyte:dev-food.jpg)をpublic/dev-food.jpgとして保存します。
style.cssを修正して画像イメージを取り込みます。以下、修正後です。

```css: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を修正して追加したスタイルシートのクラスを指定します。以下、修正後です。

```jsx: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
```

ユーザプロファイル用の画面を作成します。

```console
$ pwd
~/whatabyte-portal
touch views/user.pug
```

```jsx: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を修正してユーザプロファイルのルートを追加します。以下、修正後です。

```jsx: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}`);
});
```

[こちらの画像](https://cdn.auth0.com/blog/whatabyte:team.jpg)をpublic/team.jpgとして保存します。
style.cssを修正して画像イメージを取り込みます。以下、修正後です。

```css: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`にアクセスします。以下の画面が表示されれば成功です。

<img src="https://i.imgur.com/NiyDHyQ.jpg" width=70%>


## Passportのセットアップ
Passport.js、　passport-auth0等、必要なパッケージをインストールします。**passport-auth0はAuth0のPassport用認証ストラテジです。**

```console
$ pwd
~/whatabyte-portal
$ npm install passport passport-auth0 express-session dotenv --save
```

index.jsを修正してexpress-sessionをインポートします。以下、修正後です。

```jsx:index.js
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環境変数をインポートします。

```console
$ pwd
~/whatabyte-portal
$ touch .env
```

以下、修正後のindex.jsです。

```jsx: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をインポートします。以下、修正後です。

```jsx: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
};

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を定義します。以下、修正後です。

```jsx: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));
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で利用するようにします。以下、修正後です。

```jsx: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());

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を修正してセッションからユーザデータを取得します。以下、修正後です。

```jsx: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を作成します。

```console
$ pwd
~/whatabyte-portal
$ touch auth.js
```

```jsx: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"エンドポイントを追加します。以下、修正後です。

```jsx: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");

router.get(
  "/login",
  passport.authenticate("auth0", {
    scope: "openid email profile"
  }),
  (req, res) => {
    res.redirect("/");
  }
);
```

auth.jsを修正して"GET /callback"エンドポイントを追加します。以下、修正後です。

```jsx: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);
});
```

auth.jsを修正して"GET /logout"エンドポイントを追加します。以下、修正後です。

```jsx: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);
});
```

auth.jsを修正して定義したルートをエクスポートします。以下、修正後です。

```jsx: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を修正して認証ルートをインポート、マウントします。以下、修正後です。

```jsx: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"を押します。
<img src="https://i.imgur.com/ZtGP5PL.png" width=70%>

"Name"に任意の名前を入力、"Choose an application type"で"Regular Web Applications"を選択して”CREATE”を押します。
<img src="https://i.imgur.com/P6v8Oxu.png" width=70%>

"Settings"タブをクリック、"Allowed Callback URLs"に"`http://localhost:3000/callback`, `http://localhost:8000/callback`"を、"Allowed Logout URLs"に"`http://localhost:3000/`, `http://localhost:8000/`"を入力して"SAVE CHANGES"を押します。
<img src="https://i.imgur.com/2U5kl1W.png" width=70%>

.envを修正して環境変数を設定します。**環境変数は"Settings"タブから確認できます。**
<img src="https://i.imgur.com/RFQ48n6.png" width=70%>

```dot:.env
AUTH0_CLIENT_ID=xxxx
AUTH0_DOMAIN=mokomoko.auth0.com
AUTH0_CLIENT_SECRET=xxxx
```
ApplicationとBrowsersyncを起動して"Log In"ボタンを押します。Auth0のログインウィジェットが表示されれば成功です。

```console
$ pwd
~/whatabyte-portal
$ npm run dev
```

```console
$ pwd
~/whatabyte-portal
$ npm run ui
```

<img src="https://i.imgur.com/0FN4hFy.png" width=70%>

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

<img src="https://i.imgur.com/e57ZERf.png" width=70%>

<img src="https://i.imgur.com/SYvk0zL.jpg" width=70%>
<br>

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