Microsoft GraphでOAuthのアクセストークンを更新する仕組みを作る

#背景
MicrosoftのOffice365にあるExcel OnlineのシートをAPI経由で更新したい
->そのためには当然Office365へのログイン（認証）が必要
さらに、一度の認証だけでは足りなくて、認証を維持する仕組みが必要（理由は後述）

そこで今回はOAuth認証からトークンの更新をする仕組みの作り方について書きます。

#この記事の内容
この記事では
1.Microsoft Graph（Office365を触るためのAPI？）のアプリ作成からOAuth認証の方法
2.アクセストークンの取得方法
3.期限が切れた場合のアクセストークンの更新方法
をまとめます。

###前提・環境
Oauth認証部分にはPython3系とflask
処理にはAWSのLambda（Python2.7）を、データの保存にはAWSのDynamoDBを使ってます

OAuth認証の仕組みはざっくり知ってる程度の知識で書いていますので、手順が間違っていたり・飛ばしていたりする可能性があります。その際はご指摘いただけるとありがたいです。


#1.アプリの作成からOAuth認証まで
##1-1.アプリの登録

[アプリを Azure AD v2.0 エンドポイントに登録する](https://developer.microsoft.com/ja-jp/graph/docs/concepts/auth_register_app_v2)
このページを参考にアプリを新規登録して、**「アプリケーション ID」と「アプリケーション シークレット」をメモ**しておきます。


###ここでの注意点
**↓Microsoft Graph のアクセス許可で必ずoffline_accessをスコープに入れること**
これをスコープに入れないと後述するrefresh tokenが返って来ません。
（refresh tokenが無いとアクセストークンの有効期限が切れるたびに手作業（ブラウザ）での認証が必要になる？）

![アクセス許可](https://qiita-image-store.s3.amazonaws.com/0/58437/df8427c4-4e1f-894e-5f6e-8f914a2dab80.png)


##1-2.OAuth認証
[Microsoft Graph アプリの構築を開始する](https://developer.microsoft.com/ja-jp/graph/docs/concepts/get-started)
こちらのページで各言語でのサンプルソースが置いてあります。

今回はPythonを使いましたので[こちら](https://github.com/microsoftgraph/python3-connect-rest-sample)です。

###認証までの流れ
GithubのREADMEに記載している流れそのままですが

- 上のソースをgit cloneしてソースを持って来る
- 必要なパッケージをpipでインストール
- READMEに書いている設定↓をして
- flaskを実行してローカルにWebサーバーを立てる
- アクセスしてOAuth認証の動作確認

といった感じです。
解説するまでもないかもしれませんが、一つずつ見ていきます。

####1.git cloneする
`git clone https://github.com/microsoftgraph/python3-connect-rest-sample.git`

####2.必要なモジュールをpipでインストール（オプション）
もしも5まで進めて `no module named ~~`といったエラーが出た場合は
`pip intall ~~`で必要なモジュールを入れてください。

####3.READMEに書いている設定をする
```text:README和訳
1.好きなエディタで_PRIVATE.txtを開いて
2.ENTER_YOUR_CLIENT_IDという文字の部分を1-1でメモしたアプリケーション IDに書き換えて
3.ENTER_YOUR_SECRETという文字の部分を1-1でメモしたアプリケーションシークレットに書き換えて
```

####4.flaskを実行してローカルにWebサーバーを立てる

```text:README和訳
4.python manage.py runserver ってコマンド打ってね
5.http://localhost:5000/ にアクセスしたらOAuth認証するためのページが開くよ
```

####5.動作確認
http://localhost:5000/ にアクセスした時にこんな画面が出たら、起動は成功です。
![flask](https://qiita-image-store.s3.amazonaws.com/0/58437/4d854b66-2733-c211-0d38-9e702b667776.png)


認証をした後は↓こんな画面になって
![認証後](https://qiita-image-store.s3.amazonaws.com/0/58437/4cd21a56-3fe5-dec3-6d22-b41983a37647.jpeg)

**Send mailボタンを押した時にメールが飛んでくればOAuth認証成功です。**


#2.アクセストークンの取得
ここまででOAuth認証は成功しました。
ここからはその認証情報を維持する方法を書いていきます。

上のflaskのソース内にアクセストークンを使っている場所があるので、そこでprintをしてaccess_tokenとrefresh_tokenを可視化します。

connectsample.py 内の80行目あたりに下記を追加するとターミナルに二つのトークンが表示されますので、メモしておきます。

```python:add_to_connectsample
print(response['access_token'])
print(response['refresh_token'])
```


#3.期限が切れた場合のアクセストークンの更新方法

アクセストークンには有効期限があります。その有効期限が切れた後は、アクセストークンの更新が必要です。

[更新トークンを使用して新しいアクセス トークンを取得する](https://developer.microsoft.com/ja-jp/graph/docs/concepts/auth_v2_user#5-use-the-refresh-token-to-get-a-new-access-token)

今回はアクセストークンとリフレッシュトークンをDynamoDBに保存しておいて、利用時に更新＆取り出しを行うようにしました。


##3-1.DynamoDBの準備
![DynamoDB](https://qiita-image-store.s3.amazonaws.com/0/58437/2f03bcf9-cbd1-5262-1c40-bbdc01799fd1.png)
こんな感じでテーブルを作っておきます。（usernameと書いたつもりがusenameになってました。）
andoとなっているところは一意に判定できる名前を入れておいてください。

access_token と refresh_tokenには上で取得したものを保存しておいてください。

##3-2.Lambdaの準備

```python:getaccess_token.py
# coding:utf-8
import os
import boto3
import json
from boto3.dynamodb.conditions import Key, Attr
import requests

client_id = 'アプリケーション ID'
client_secret = 'アプリケーション シークレット'
username = 'ando'

def get_access_token():
    dynamodb = boto3.resource('dynamodb')
    table    = dynamodb.Table('excelOnlineToken')
    
    response = table.query(
        KeyConditionExpression=Key('usename').eq(username), Limit=1)
    refresh_token = response['Items'][0]['refresh_token']
    headers = { 'Accept' : 'application/json',
                'Content-Type' : 'application/x-www-form-urlencoded'
                }

    payload = {'client_id': client_id, 'scope': 'User.Read Mail.Send offline_access Files.ReadWrite.All','refresh_token':refresh_token,'redirect_uri':'http://localhost:5000/login/authorized','grant_type':'refresh_token','client_secret':client_secret}

    response = requests.post(
        'https://login.microsoftonline.com/common/oauth2/v2.0/token',
        headers=headers,data=payload)
    jsonObj =  json.loads(response.text)


    table.update_item(
        Key={
            'usename': 'ando'
        },
        UpdateExpression='SET refresh_token = :val1,access_token = :val2',
        ExpressionAttributeValues={
            ':val1': jsonObj["refresh_token"],
            ':val2': jsonObj["access_token"]
        }
    )

    return jsonObj["access_token"]
```

このLambdaを実行すればMicrosoft GraphにアクセスをしてDynamo内のアクセストークン・リフレッシュトークンを更新して、最新のアクセストークンを返してくれます。

#最後に
今回はアクセストークンの期限が切れているかどうかの判定をせずに毎回更新しちゃってますが、期限が切れていた場合のみ更新という風にした方が良さそうですね。