タム
2024.03.22
149
こんにちは、タムです。
今回は表題の通り、Googleでログインを実装してみたのでご紹介です。
フロントはSSGで構築してみました。
↓
↓
↓
たったのこれだけです。
今回は以下のようなインフラ構成で実現しました。
ざっくりと説明するとフロントエンドがCloudFront + S3で静的サイト配信しJSで認可コードを取得、
バックエンドがAPI Gateway + Lambdaでフロントで取得した認可コードを使ってトークン取得を取得します。
図にするとこんな感じです。
まず考えられるのは認可コードをバックエンドに送信していますが、
このバックエンドはSSGからのリクエストを受け付ける必要があり
パブリックアクセスを許容する必要があるので、どこからでもアクセスできてしまいます。
そうすると攻撃者が総当り的に認可コードを大量に送ると、
ユーザのアクセストークンを奪うことができてしまう可能性があるかなと思います。
当然CORSは設定していますが、APIを直で叩く場合は関係ないです。
ただ少なくとも使用済みの認可コードは使えなくなるので、
実際にその攻撃が成功する可能性は極めて低いと思います。
他には、同じくフロント・バック通信のところで、リクエスト・レスポンスを
平文でやり取りしているため、万が一通信が傍受されると
同様にアクセストークンが奪われる可能性がありそうです。
公開鍵暗号を使おうとしても結局フロントがSSGなので
秘密鍵を安全に保管することができないと思います。
ただ、Googleからはアクセストークンがそのまま送られてくるわけで、
お門違いな心配をしているような気もします。
セキュリティの点はすぐに結論がでなそうなのでじっくり考えようと思います。
その他で今後やってみたいこととしては、
このあたりが実装できたらまた記事にしたいと思います。
フロントは個人的にシンプルで好きなsvelteを使っています。
src/routes/+page.svelte
<script lang="ts">
import axios from "axios";
async function login() {
// ディスカバリドキュメントを参照
const discoverRes = await axios.get('https://accounts.google.com/.well-known/openid-configuration');
sessionStorage.setItem('discoverDoc', JSON.stringify(discoverRes.data));
// 認可エンドポイントへのリクエストパラメータ設定
const params = {
response_type: 'code',
client_id: 'XXXXXXXXXXXXXXXXXXXXXX.apps.googleusercontent.com',
scope: 'openid profile email',
redirect_uri: 'XXXXXXXXXXXXXXXXXXXXXXXXXX/callback',
state: Math.random().toString(32).substring(2),
nonce: Math.random().toString(32).substring(2)
};
sessionStorage.setItem('state', params.state);
sessionStorage.setItem('nonce', params.nonce);
// 認可エンドポイントへ遷移
const href = `${discoverRes.data.authorization_endpoint}?response_type=${params.response_type}&client_id=${params.client_id}&scope=${params.scope}&redirect_uri=${params.redirect_uri}&state=${params.state}&nonce=${params.nonce}`;
location.href = href;
}
</script>
<h1>ログイン</h1>
<p><button on:click={login}>Googleでログイン</button></p>
src/routes/callback/+page.svelte
<script lang="ts">
import { error } from '@sveltejs/kit';
import axios, { type AxiosResponse } from 'axios';
import { onMount } from 'svelte';
let queryParams;
let userInfoRes: AxiosResponse;
onMount(async () => {
// コールバックエンドポイントにリダイレクトしてきた際のクエリパラメータを取得
const params = new URLSearchParams(window.location.search);
queryParams = Object.fromEntries(params.entries());
// 送られてきたstateが一致しているか確認
const sessionState = sessionStorage.getItem('state');
if (queryParams.state != sessionState) {
error(401, { message: 'Invalid state parameter.' });
}
// バックエンドからトークン取得
const res = await axios.post('https://XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/get_token', {
code: queryParams.code,
});
// userinfoエンドポイントにアクセス
const discoverDoc = JSON.parse(sessionStorage.getItem('discoverDoc') ?? '');
userInfoRes = await axios.get(discoverDoc.userinfo_endpoint, {headers: {Authorization: `Bearer ${res.data.access_token}`}});
console.log(userInfoRes);
});
</script>
{#if userInfoRes}
<h1>{userInfoRes.data?.name}</h1>
<img src={userInfoRes.data?.picture} alt="facePicture" />
{:else}
<h1>ログインしています...</h1>
{/if}
バックエンドは個人的に好きなScalaとかを使いたかったのですがCI/CDの構築に詰まったので一旦安定のpython(Chalice)で作りました
from chalice import Chalice, Response, CORSConfig
import logging
import requests
import boto3
import urllib
app = Chalice(app_name="XXXXXXXXXXXXXXXXXX")
app.log.setLevel(logging.DEBUG)
ssm = boto3.client("ssm")
CLIENT_ID = ssm.get_parameter(Name="/google_oauth/client_id")["Parameter"]["Value"]
CLIENT_SECRET = ssm.get_parameter(Name="/google_oauth/client_secret")["Parameter"]["Value"]
REDIRECT_URI = ssm.get_parameter(Name="/google_oauth/redirect_uri")["Parameter"]["Value"]
@app.route("/get_token", methods=["POST"], cors=CORSConfig(allow_origin="https://XXXXXXXXXXXXXXXXXXXXXXXXXXX"))
def index():
req = app.current_request.json_body or {}
if "code" not in req:
return Response(body={"message": "server error"}, status_code=500)
body = {
"code": req["code"],
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": REDIRECT_URI,
"grant_type": "authorization_code",
}
encoded = urllib.parse.urlencode(body)
res = requests.post(
"https://oauth2.googleapis.com/token",
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=encoded,
)
if res.status_code >= 300:
app.log.error(res.json())
return Response(body={"message": "server error"}, status_code=500)
return res.json()
11
原
2024.07.31
55
原
2024.07.31
1,070
だいち
2024.05.30