node.jsでAzureADを認証基盤として利用する

## 初めての投稿
初めて記事を投稿します
いつもお世話になっているので、これからは少しでもアウトプットしていきたいと思います

## 記事の中でやっていること
* AzureADを認証基盤として使う
* AzureADから取得したアカウント情報をcookieに保存する
* cookieに保存したアカウント情報を取得する

## 環境
* ubuntu 20.04
* node v16.13.1

## AzureADへのアプリケーション登録
AzureADを認証基盤として利用したいアプリをAzurePortalに登録します（登録方法は[本家（Microsoft）](https://docs.microsoft.com/ja-jp/azure/active-directory/develop/quickstart-v2-nodejs-webapp-msal)を参照）
今回はアプリ登録時に設定するリダイレクトURIに`http://localhost:3000/redirect`を設定します
AzurePortalにアプリを登録することで、アプリのクライアントIDとクライアントシークレットを取得することができます

## MSAL Node
承認コードフロー用のnpmパッケージをインストールします

```javascript
npm install @azure/msal-node
```

## その他のnpmパッケージ
ルーティング、画面表示、cookie取得などのためにインストールしたnpmパッケージは以下の通りです

```javascript
npm install express express-ejs-layouts ejs cookie-parser
```

## サンプルアプリ
### msal の設定
後でmsalオブジェクトに渡すために`REDIRECT_URI`にAzurePortalで登録した`http://localhost:3000/redirect`を設定します
`system:`の部分はログを出力するための設定です（あまり詳しくないです...）
`config`を使ってmsalオブジェクト`cca`を作成します

```javascript
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 || 3000
const REDIRECT_URI = 'http://localhost:3000/redirect'

const config = {
    auth: {
        clientId: '自分のアプリのクライアントID',
        authority: 'https://login.microsoftonline.com/common',
        clientSecret: '自分のアプリのクライアントシークレット'
    },
    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)
```

### expressとcookieの設定
expressでejs(テンプレートエンジン)とcookieを扱えるようにするための設定です

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

### Top Page
特に理由はありませんがTop Pageを設定しました

```javascript
app.get('/', (req, res) => {
    res.render('index',{title: 'Top Page'})
    console.log(req.headers)
})
```

スクリーンショットはこんな感じです
青い`Azure AD認証`は`/auth`へのリンクです
![TopPage](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286822/667b7fcd-4cff-84c4-0df1-fd1b35f5f558.png)

### AzureADの認証画面をユーザに表示
認証に使用するスコープとリダイレクトURIを設定します
登録したアプリが利用できるスコープはAzurePortalの画面に表示されています
`user.read`はアカウント情報の読み取りのみが可能です
ユーザが`/auth`にアクセスすると`cca.getAuthCodeUrl`が実行され、ユーザが認証を行うための画面へのリンクが`response`として得られるので、それをリダイレクトします。

```javascript
app.get('/auth', (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)))
})
```

リダイレクト先としてユーザにAzureADの認証画面が表示されます
ただし、既にブラウザでAzureADにサインインしている場合は、認証画面は表示されず自動的に次のステップに進みます
![AzureAD 認証画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1286822/37986568-6afc-11ff-124f-09c20c050437.png)
ユーザ認証が正常に行われると、AzureADはクエリパラメータにトークンリクエストコードを含めて`redirectUri: REDIRECT_URI`にリダイレクトします

### アカウント情報を取得し、クッキーに保存する
`/redirect`のクエリパラメータからリクエストコード`code: req.query.code`を取得します
それを使って`cca.acquireTokenByCode`を実行することで、アカウント情報とIDトークン、アクセストークンが `response`として得られます
今回はアカウント情報から`account.username`と`account.name`を取得して`res.cookie`でレスポンスにcookieを設定し、`/user`へリダイレクトしています

```javascript
app.get('/redirect', (req, res) => {
    const tokenRequest = {
        code: req.query.code,
        scopes: ['user.read'],
        redirectUri: REDIRECT_URI,
    }
    console.log(`\nCode:\n${tokenRequest.code}\n`)

    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からアカウント情報を取得
`/user`へのリクエストには先ほど設定したcookieが含まれているため、そのcookieからアカウント情報を取り出し、画面に表示しています
今回はcookieにアカウント情報を平文で保存していますが、cookieはブラウザで簡単に編集ができるため実用には暗号化などの対策が必要だと思います

```javascript
app.get('/user', (req, res) => {
    const data = req.cookies
    console.log(data)
    res.render('user', {title: 'User Info', userName: data.username, name: data.name})
})
```

### サーバの起動
サーバを起動します

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

## ソースコードまとめ
最後にソースコードをまとめて貼っておきます

```javascript:index.js
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 || 3000
const REDIRECT_URI = 'http://localhost:3000/redirect'

const config = {
    auth: {
        clientId: '自分のアプリのクライアントID',
        authority: 'https://login.microsoftonline.com/common',
        clientSecret: '自分のアプリのクライアントシークレット'
    },
    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)

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

app.get('/', (req, res) => {
    res.render('index',{title: 'Top Page'})
    console.log(req.headers)
})

app.get('/auth', (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`)

    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)
    })
})

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}!`))

```