公式LINEアカウントへのログイン処理をAWS Lambda経由で実装してみた

## はじめに
本記事は[PERSOL PROCESS & TECHNOLOGY Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ppt)の記事です。
業務でLINEとAWS実装を経験することがあり、独学ながら実装したお話を書いてみようと思います。といってもLINEの公式ドキュメントがすごく丁寧に教えてくれているので、こんなふうに実装できるんだ〜便利〜とか思ってくれれば良きです。では

## 背景
今回日本で利用者数が多いSNSであるLINEと、サーバーレスであるAWS Lambdaを利用しています。
>LINEは、月間ユーザー数9,500万人（2023年6月末時点）と多くのユーザー数を抱えています。
参照：[LINEユーザー層](https://campus.line.biz/line-ads/courses/user/lessons/oada-1-2-2)

ともあり、年齢層に関係なく利用ユーザーも多いです。（個人的には思ったより高齢層というのも関係なく高いのに驚き）使いやすくて便利。ですね。

Lambdaはサーバーレスのサービスのため、サーバー運用について考える必要がなく、簡単に実装できます。こちらも使いやすくて便利。です。
ユーザーも使いやすい。（使われやすいともいう。）開発者も開発しやすい。のでこのような構成を選択しています。

## 構成図
今回はどこかのサイトのユーザーが自環境へ入り、LINEログインのリダイレクトを行います。リダイレクト結果が自環境に戻り、結果をどこかのサイトへ結果を返す、というような処理を実装します。

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/c118b595-e790-0366-37b6-e2cdd574a148.png">

（今回の事象では認証前後で、Lambda関数による処理が必要なためこのような構成としています。LINE認証を行うのに必ずLamdba関数を通す必要はありません。）

## ユーザー視点遷移
ユーザーはどこかのサイトの”LINEID連携”ボタンを押下すると、紐づいている公式LINEアカウントへアクセス権限を"許可する"ボタンを押下、無事にユーザー情報を取得できれば完了！となります。
（ユーザーのLINEアカウントと分けるため本記事では認証されるLINEアカウントを”公式アカウント”と呼んでいます。）

<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/1fab0474-b285-7a94-9fcf-fae5725fefee.png">

では実際の実装内容に入ります。

## 事前準備
必要なものは
・LINEDevelopersアカウント
・AWSアカウント
となります。

登録方法は今回省略します。どちらもぽちぽちと簡単に作成できます。

LINE DevelopersではLINE公式チャネルを作成します。
<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/db332ddf-5ab4-6a55-66d3-f0b835fbded9.png">

どのアカウントへログインするのかなどを特定するID情報が実装時必要となります。呼び出し時は上記”開発中”→”公開済み”に変更するのを忘れずに！

AWSではLambda関数、APIGatewayを作成します。（権限や設定値等は各自の設計および環境に依存するため、参考程度にお考えください。）

まずはLambda関数です。
<img width="100%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/5c73db6a-209b-94c5-f75a-2d5562065caa.png">

作業環境によって１から作成しても、コンテナイメージから作成してどちらでも大丈夫です。
（後日本開発環境のCI/CDに関する記事を投稿予定です→[PERSOL PROCESS & TECHNOLOGY Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ppt)）

次にAPIGatewayです。
今回はREST APIを選択しています。
<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/6a25b182-4e92-071c-701a-9896257db8f3.png">

作成したREST APIにリソースを作成して、その中にメソッドを作成します。ここでは先ほど作成したLambda関数と統合したメソッドとなります。
また今回はLambdaプロキシ統合を利用しています。簡単に言えば、リクエストやレスポンスなどAPIGatewayでマッピングしなくても簡単に使えるよ〜というやつです。
<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/c7971335-aaf9-6cdd-40fe-628357aa22db.png">

作成したAPIはこんな感じです。1つめのAPIのメソッドタイプはGETでもPOSTでもこのAPIを呼び出す先に合わせ選択ください。2つめのAPIメソッドタイプはLINEからのリダイレクトを受信する箇所になるため、"GET"とします。
<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/f5c8d958-9dab-2e08-0c8e-5ed211cd6191.png">


## AWS Lambda実装
### 1つ目のLamdba関数
1つ目のLambda関数ではAPIを受けてLINEの認可URLを作成し、ユーザーに向けてリダイレクトを実施します。

LINE認可は、LINEとユーザー間で認証と認可のプロセスを行います。
ここでの検討ポイントは”どんなユーザー情報が欲しいのか”ということです。
認可URLのパラメータ”scope”で、どのアクセス権が欲しいのかを指定します。

scope　= profile →　プロフィール情報（LINEユーザーID、LINE表示名前、LINEアイコンURLなど）
scope　= openid →　ユーザー識別子（LINEユーザーID、IDトークンの有効期間など）
scope　= email →　メールアドレス
メールアドレスへのアクセス権が必要な場合は別途LINEDevelopersページよりLINE公式アカウントのメールアドレス取得権限の申請が必要です。


その他必要情報は
・LINEログインチャネルのチャネルID→LINEDevelopersページより、公式アカウントのチャネル基本情報に記載あり。
・リダイレクトURL→AWS APIGatewayページより、デプロイしたAPIのステージ詳細に記載あり。

このリダイレクトURLはLINEDevelopersページ上で設定したコールバックURLと同じURLとなります。合わせて設定します。
<img width="80%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1827273/4ea38a69-7be7-eb49-349d-20ea92034bb6.png">

今回リダイレクトを実施するため、レスポンスのstatuscodeは"302"、headersはリダイレクトURLを付与します。

参考ソースコード↓
```python:lambda1.py
import json
import os

def lambda_handler(event, context):

    # ステート生成
    state = hashlib.sha256(os.urandom(32)).hexdigest()

    # 環境値（ご自身の環境値に置き換えてください。）
    redirect_uri = 'REDIRECT_URL'
    client_id = 'LINE_CHANNEL_ID'

    base_url = "https://access.line.me/oauth2/v2.1/authorize"
    params = {
        "response_type": "code",
        "client_id": client_id,
        "redirect_uri": redirect_uri,
        "state": state,
        "scope": "profile"
    }

    full_url = f"{base_url}?{'&'.join([f'{key}={value}' for key, value in params.items()])}"

    response = {
        'statusCode': 302,
        'headers': {
            'Location': full_url
        }
    }

    return response
```


### 2つ目のLambda関数
2つ目のLambda関数では、LINE認可したユーザー情報を取得し、完了した旨を返します。

完了すると、パラメータ"code"、"state"含みリダイレクトされます。
"code"パラメータをもとに、アクセストークンを取得します。
scope=profileの場合、このアクセストークンからLINEユーザーIDやLINE表示名を取得することも可能です（今回は対象外）

もしユーザーが認可しなかった場合、パラメーター"error"、"error_description"となります。されなかった場合の処理を別途考慮する必要があります。今回の実装ではエラーとして処理しています。


参考ソースコード
```python:lambda2.rb
import json
import os
import urllib

def lambda_handler(event, context):

    #リクエスト情報取得
    parameters = event.get('queryStringParameters')

    #LINEエラーの場合は処理を終了する
    if 'error' in parameters:
        return {
            'statusCode': parameters['error'],
            'body': json.dumps({'message': parameters['error_description']}, ensure_ascii=False)
        }
    
    #リクエスト情報取得
    code = parameters['code']
    state = parameters['state']

    # 環境値（ご自身の環境値に置き換えてください。）
    redirect_uri = 'REDIRECT_URL'
    client_id = 'LINE_CHANNEL_ID'
    client_secret = 'LINE_CHANNEL_SEACRET'

    # LINE Messaging APIのエンドポイント
    line_api_endpoint_token = 'https://api.line.me/oauth2/v2.1/token'

    # LINEへアクセストークン取得リクエスト送信
    # リクエストヘッダー
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    # LINE APIの認証情報
    data = urllib.parse.urlencode({
        'grant_type': 'authorization_code',  
        'code': code,  
        'redirect_uri': redirect_uri,
        'client_id': client_id,  
        'client_secret': client_secret
    }).encode('utf-8')

    try:
        # HTTP POSTリクエストの送信
        request = urllib.request.Request(line_api_endpoint_token, data=data, headers=headers, method='POST')
        with urllib.request.urlopen(request, data=data) as f:
            response = f.read()
        line_info = json.loads(response)
        access_token = line_info['access_token']
        return {
            'statusCode': 200,
            'body': {
                'message': access_token
            }
        }

    except Exception as e:
        # エラーハンドリング
        error_message = e.read().decode("utf-8")
        return {
            'statusCode': e.code,
            'body': {
                'message': error_message
            }
        }

    return response

```

## まとめ

今回は公式LINEアカウントのログイン認証行い、認可したユーザーのLINEユーザー情報を取得することができました。今回Lambda関数ではログイン処理しか行っていませんが、実際の実装では取得したユーザー情報をDB保存したりと他の処理を組み合わせて実装することが可能です。ウェブアプリにLINEログインをAWSで実装する際の参考になればと思います。

自身にとってAWSとLINE機能、両方において技術の理解を深めることができました。また後日、本開発環境のCI/CDに関する記事を投稿予定です。そちらもぜひ→[PERSOL PROCESS & TECHNOLOGY Advent Calendar 2023](https://qiita.com/advent-calendar/2023/ppt)


## 参考文献
[ウェブアプリにLINEログインを組み込む](https://developers.line.biz/ja/docs/line-login/integrate-line-login/)