LINEログイン PKCE対応メモ
- LINEログインをPKCE対応を試してみたのでメモしておく。
- ACG +PKCEでアクセストークンを取得→ユーザー情報取得まで試した。
PKCEとは
- Proof Key Code Exchange の略称。
-
認可コード横取り攻撃への対策を目的として定義されているOAuth2.0拡張仕様。
- 認可コード横取り攻撃:悪意のあるアプリが何らかの方法で認可コードを含むカスタムURIを取得し、ユーザー固有のアクセストークンを横取りする。
通常のLINEログインとの変更点(クライアント側)
-
認可パラメータに
code_challenge_method
とcode_challenge
を含める。-
code_verifier
を生成する。 -
code_verifier
を元にcode_challenge_method
(S256)を利用してcode_challenge
を生成する。
-
トークンリクエスト時にリクエストに
code_verifier
を含める。
作成するAPI
認可リクエスト作成API
-
LINEログイン認可エンドポイントへアクセスするためのURLをパラメータをつけて生成・返却する。
-
state
,code_verifier
をAPI呼び出し時に生成し、レスポンスとして返却する。 -
client_id
など固定の属性値は環境変数から取得する。 - リクエスト例
POST /auth/authz_request/create HTTP/1.1 Host: localhost:8000
- レスポンス例
{ "authz_request_url": "https://access.line.me/dialog/oauth/weblogin?client_id=YOUR_CHANNEL_ID&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fauth%2Fcallback&state=AAABBBCCC&response_type=code&code_challenge=XXXYYYZZZ&code_challenge_method=S256", "state": "AAABBBCCC", "code_verifier": "ZZZYYYXXX", }
-
トークンリクエスト+ユーザー情報取得API
-
認可レスポンスとしてLINEからのリダイレクトで受け取った認可コード(
code
)と前述の認可リクエスト作成APIレスポンスのコードベリフィア(code_verifier
)を指定し、トークンリクエスト+ユーザー情報取得を行う。- 今回は簡略化のため認可リクエストパラメータのDB保存まで実装しておらず、検証処理は行っていないが、stateパラメータの検証は必ず行うこと。
- リクエスト例
POST /auth/authz_request/complete HTTP/1.1 Host: localhost:8000 Content-Type: application/json Content-Length: 128 { "code_verifier":"fugafuga", "code":"hogehoge" }
- レスポンス例
{ "userId": "hogefuga", "displayName": "hoge", "pictureUrl": "https://profile.line-scdn.net/fugahoge" }
実装
root - docker-compose.yml
- line.env
- be - Dockerfile
- requirements.txt
- api - main.py
docker-compose.yml
version: "3"
services:
api:
container_name: "api"
build: ./be
env_file: line.env
ports:
- "8000:8000"
volumes:
- ./be/api:/usr/src/server
-
line.env
- クライアント+LINEエンドポイント情報
LINE_CLIENT_ID=YOUR_CHANNEL_ID
LINE_CLIENT_SECRET=YOUR_CHANNEL_SECRET
LINE_REDIRECT_URI=YOUR_CALLBACK_URL
LINE_AUTHZ_ENDPOINT=https://access.line.me/dialog/oauth/weblogin
LINE_TOKEN_ENDPOINT=https://api.line.me/v2/oauth/accessToken
LINE_USER_INFO_ENDPOINT=https://api.line.me/v2/profile
Dockerfile
FROM python:3.8
WORKDIR /usr/src/server
ADD requirements.txt .
RUN pip install -r requirements.txt
# uvicornのオプションに--reloadを付与すると、
# main.pyを編集と同時に変更内容が反映される。
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"]
-
requirements.txt
- Python依存ライブラリ
uvicorn==0.11.5
uvloop==0.16.0
fastapi==0.68.1
-
main.py
- FastAPIで上記2APIを用意。
from fastapi import FastAPI
import hashlib
import urllib.parse as parse
import urllib.request as req
import urllib.error as error
import json
import os
import base64
from pydantic import BaseModel
# Client Param
client_id = os.getenv('LINE_CLIENT_ID')
client_secret = os.getenv('LINE_CLIENT_SECRET')
redirect_uri = os.getenv('LINE_REDIRECT_URI')
# LINE Endpoint
authz_endpoint = os.getenv('LINE_AUTHZ_ENDPOINT')
token_endpoint = os.getenv('LINE_TOKEN_ENDPOINT')
user_info_endpoint = os.getenv('LINE_USER_INFO_ENDPOINT')
app = FastAPI()
# Create Authorization Request Endpoint
@app.post("/auth/authz_request/create")
def create():
# Generate state
state = hashlib.sha256(os.urandom(32)).hexdigest()
# https://developers.line.biz/ja/docs/line-login/integrate-pkce/#how-to-integrate-pkce
# Generate code_verifier
code_verifier = hashlib.sha256(os.urandom(43)).hexdigest()
# Generate code_challenge
code_challenge = hashlib.sha256(code_verifier.encode()).hexdigest()
code_challenge = base64.b64encode(code_challenge.encode()).decode().replace('+', '-').replace('\/', '-').replace('=','')
# Create Authz Request URL
authz_request_url = authz_endpoint+'?{}'.format(parse.urlencode({
'client_id': client_id,
'redirect_uri': redirect_uri,
'state': state,
'response_type': 'code',
'code_challenge':code_challenge,
'code_challenge_method':'S256'
}))
res_body = {
"authz_request_url": authz_request_url,
"state": state,
"code_verifier":code_verifier
}
return json.loads(json.dumps(res_body))
# Token Request And Get User Info Endpoint
# Request Body
class ReqBody(BaseModel):
code: str
code_verifier:str
@app.post("/auth/authz_request/complete")
def complete(reqBody: ReqBody):
# Parse Req Body
code = reqBody.code
code_verifier = reqBody.code_verifier
# Token Request
# https://developers.line.biz/ja/docs/line-login/integrate-line-login-v2/#get-access-token
token_req_body = parse.urlencode({
'code': code,
'client_id': client_id,
'client_secret': client_secret,
'redirect_uri': redirect_uri,
'grant_type': 'authorization_code',
'code_verifier':code_verifier
}).encode('utf-8')
token_req = req.Request(token_endpoint)
err_str = ''
try:
with req.urlopen(token_req, data=token_req_body) as token_res:
token_res_body = token_res.read()
except error.HTTPError as err:
err_str = str(err.code) + ':' + err.reason + ':' + str(err.read())
pprint(err_str)
except error.URLError as err:
err_str = err.reason
pprint(err_str)
access_token = json.loads(token_res_body)['access_token']
# Get User Profile
# https://developers.line.biz/ja/docs/line-login/managing-users/#get-profile
headers = {
'Authorization': 'Bearer ' + access_token
}
user_info_req = req.Request(
user_info_endpoint, headers=headers, method='GET')
try:
with req.urlopen(user_info_req) as user_info_res:
user_info_res_body = user_info_res.read()
except error.HTTPError as err:
err_str = str(err.code) + ':' + err.reason + ':' + str(err.read())
pprint(err_str)
except error.URLError as err:
err_str = err.reason
pprint(err_str)
return json.loads(user_info_res_body)
起動
docker-compose up -d