7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-09-25

はじめに

この記事は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します。

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

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

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

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

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

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を修正して静的コンテンツの保管ディレクトリを指定します。以下、修正後です。

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
style.css
body {
  background: aqua;
}

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
    link(rel="stylesheet" href="/style.css")
  body
    div#root
      block layout-content

style.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が自動的にリロードされて以下の画面が表示されていれば成功です。

画像イメージの追加

こちらの画像をpublic/dev-food.jpgとして保存します。
style.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を修正して追加したスタイルシートのクラスを指定します。以下、修正後です。

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

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を修正して画像イメージを取り込みます。以下、修正後です。

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

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環境変数をインポートします。

$ pwd
~/whatabyte-portal
$ touch .env

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

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

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

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

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

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

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

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

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を修正して定義したルートをエクスポートします。以下、修正後です。

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

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"タブから確認できます。

.env
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"にして頂きたいですね。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?