node.jsでAzureADのシングルサインオンを利用してみた

# やりたいこと
- AzureADの認証基盤を使って汎用アプリのシングルサインオン（SSO認証）を実現する
- シングルサインアウトボタンの実装
- localhostから認証ページに飛ぶ
# 環境
- Windows 11
- node v16.17.0
# 手順
1. AzureADへのアプリケーション登録
2. アプリの作成
3. 実装
### 1. AzureADへのアプリケーション登録
- [AzureADのホームページ](https://portal.azure.com/#home)にサインインして、トップメニューの**設定**よりアプリケーションを登録するテナント/ディレクトリに切り替える
- **エンタープライズアプリケーション** を検索する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3235673/576dbb9a-2eea-4bc2-97be-c5e4d038b66b.png)
- **新しいアプリケーション** > **独自のアプリケーションの作成** > アプリケーションの **名前** を入力 > **作成**の順に作成する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3235673/7d696138-bf93-e72b-8765-5bf10f61036b.png)
- SSO認証の設定 
    - **シングルサインオン** > **SAML** > **基本的な SAML 構成** の順に選択し、識別子を追加し、応答URLに`http://localhost:3000/redirect`を設定する。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3235673/cb740686-d048-a878-b171-87c6bdc599de.png)
    - **ユーザーとグループ**にアプリにアクセス可能なユーザーを追加する
    
- シークレートの作成＆必要な情報をメモする
    - **プロパティ** > **アプリケーション登録** > **概要**の順に選択する。後で使う`アプリケーション (クライアント) ID`、`ディレクトリ (テナント) ID`をメモする。
    - **証明書とシークレット** > **新しいクライアントシークレート**の順にシークレートを作成し、`シークレート値`をメモする。シークレート値は作成時のみ表示される。


### 2. アプリの作成
- Expressで[テンプレートエンジン](https://expressjs.com/en/starter/generator.html)を生成する
```powershell
npm install express-generator -g
express --view=ejs sampleApp

#npmパッケージのインストール
npm install @azure/msal-node
npm install express express-ejs-layouts ejs cookie-parser
```
生成されたアプリケーションには、以下のディレクトリー構造になります。
```
.
├── app.js
├── bin
│   └── www
├── package.json
├── public
│   ├── images
│   ├── javascripts
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.ejs
    ├── index.ejs
    └── layout.ejs

7 directories, 9 files
```
- viewsフォルダー下のページ作成
    - user.ejsを作成する
    - layout.ejsを作成する
```layout.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Azure AD認証テスト</title>
</head>
<body>
    <h1>認証成功！</h1>

    <!-- サインアウトボタンの実装 -->
    <button class="signoutBtn" onclick="location.href = 'https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=http://localhost:3000'" id ="signout">signout</button>

</body>
</html>
```
- サーバー側のコード (詳細は[こちら](https://qiita.com/yamatairiku-dev/items/df3a9db64ecb772dbfa9#top-page)を参照)
```app.js
require('dotenv').config();

// msalの設定
const msal = require('@azure/msal-node')
const express = require('express'),
    expressEjsLayouts = require('express-ejs-layouts'),
    cookieParser = require('cookie-parser')

const SERVER_PORT = process.env.PORT
const REDIRECT_URI = process.env.REDIRECT_URI

const config = {
    auth: {
        clientId: process.env.CLIENT_ID, 
        authority: process.env.CLOUD_INSTANCE + process.env.TENANT_ID,
        clientSecret: process.env.CLIENT_SECRET 
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message)
            },
            piiLoggingEnabled: false,
            logLevel: msal.LogLevel.Verbose
        }
    }
}

// Create msal application object
const cca = new msal.ConfidentialClientApplication(config)

// view engine setup
const app = express()
app.set('view engine', 'ejs')
//cookieの設定
app.use(cookieParser())
app.use(expressEjsLayouts)
app.use(express.static('public'))
const cookieSettings = {
    cookie:{
        maxAge: 3600    //秒
    },
    resave: false,
    saveUninitialized: false
}

// AzureADの認証画面をユーザに表示
app.get('/', (req, res) => {
    const authCodeUrlParameters = {
        scopes: ['user.read'],
        redirectUri: REDIRECT_URI
    }

    // get url to sign user in and consent to scopes needed for application
    cca.getAuthCodeUrl(authCodeUrlParameters).then((response) => {
        console.log(`\nResponse:\n${response}\n`)
        res.redirect(response)
    }).catch((error) => console.log(JSON.stringify(error)))
})

// アカウント情報を取得し、クッキーに保存する
app.get('/redirect', (req, res) => {
    const tokenRequest = {
        code: req.query.code,
        scopes: ['user.read'],
        redirectUri: REDIRECT_URI,
    }
    console.log(`\nCode:\n${tokenRequest.code}\n`)

    // アカウント情報からaccount.usernameとaccount.nameを取得してres.cookieでレスポンスにcookieを設定し、/userへリダイレクトする。
    cca.acquireTokenByCode(tokenRequest).then((response) => {
        console.log("\nResponse: \n:", response)
        const userName = response.account.username
        const name = response.account.name
        res.cookie('username', userName, cookieSettings)
        res.cookie('name', name, cookieSettings)
        res.redirect('/user')
    }).catch((error) => {
        console.log(error)
        res.status(500).send(error)
    })
})

//cookieからアカウント情報を取得
app.get('/user', (req, res) => {
    const data = req.cookies
    console.log(data)
    res.render('user', {title: 'User Info', userName: data.username, name: data.name})
})

app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`))

```
- .envに認証情報を記載する
```.env
PORT=3000
CLOUD_INSTANCE="https://login.microsoftonline.com/" # cloud instance string should end with a trailing slash
TENANT_ID="テナントID"
CLIENT_ID="クライアントID"
CLIENT_SECRET="シークレート値"
REDIRECT_URI="http://localhost:3000/redirect"
```

### 3. 実行
```powershell
node app.js
```
`http://localhost:3000`にアクセスすると、リダイレクト先としてAzureADの認証画面が表示されます。
既にブラウザでAzureADにサインインしている場合は、認証画面は表示されず自動的に次のステップに進みます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3235673/c9e8d5a5-f0af-ed2b-97ba-9a030dbed4f8.png)
認証後、`layout.ejs`のページにリダイレクトされ、ターミナルにアカウント情報が表示されます。
`signout`ボタンを押下してサインアウトできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3235673/76acffc6-dc4a-996b-ccf1-e6875a488727.png)

# AzureADについて
### 「エンタープライズアプリケーション」と「アプリの登録」の違い
Azure AD でアプリケーションを設定する場合、「エンタープライズアプリケーション」と「アプリの登録」2種類の設定方法があります。どちらを利用するかは目的によって異なります。
[公式記事](https://jpazureid.github.io/blog/azure-active-directory/enterprise-applications-app-registrations/)によると、

> - 「エンタープライズアプリケーション」：既存の SaaS アプリケーション (オンプレミスの環境で提供しているアプリを含む) をテナントに統合する場合に用いる。(ここで、統合が指す意味は、SaaS アプリケーションに対するシングルサインオン (SSO) の構成や、ユーザーのプロビジョニング構成を行うことなどを指します。)
> - 「アプリの登録」：ユーザーやテナントの情報にアクセスして動作するために開発したアプリケーションを公開、もしくは利用するために用いる。

結論として、
- 組織のユーザーに対してアプリを利用させたい場合は 「エンタープライズアプリケーション」の設定をする。
    - 例えば、“Azure ADの条件つきアクセスを使ってユーザーにアクセスをさせたい” 、シングルサインオンを使ってアプリを利用したい”、“オンプレミスのアプリケーションを Azure ADと統合させたい(アプリケーションプロキシの利用)” 。

- アプリに対して権限を付与させたい場合は「アプリの登録」を利用する。
    - 「アプリの登録」を設定すると自動で「エンタープライズアプリケーション」にも登録される（これが混乱する要因だと思います…)

今回はシングルサインオンを利用したいので、「エンタープライズアプリケーション」から登録を行いました。
# SAML認証とOAuthの違い

SAML (Security Assertion Markup Language) と OAuth (Open Authorization) は、Web アプリケーションの認証方法として広く使われている。

- SAML: SAML は、Web アプリケーション間のシングルサインオン (SSO) を実現するために使用されます。SAML は、ユーザが一度認証された後、アクセスを許可する情報を含む断片 (アサーション) を提供することで、他のアプリケーションに自動的にアクセスすることができる。

- OAuth: OAuth は、ユーザのアカウント情報を利用するために、アプリケーションが他のアプリケーションまたはサービスにアクセスするための権限を与えるために使用されます。OAuth では、ユーザがアクセスを許可する前に、アプリケーションに対して認証が必要です。

結論として、SAML は Web アプリケーション間の SSO を実現するために使用され、OAuth はユーザのアカウント情報を利用するためにアプリケーションが他のアプリケーションまたはサービスにアクセスするための権限を与えるために使用される。

# 参考
[node.jsでAzureADを認証基盤として利用する](https://qiita.com/yamatairiku-dev/items/df3a9db64ecb772dbfa9)
[「エンタープライズアプリケーション」と「アプリの登録」の違いについて ](https://jpazureid.github.io/blog/azure-active-directory/enterprise-applications-app-registrations/)