はじめに
iimonでエンジニアをしています。腰丸です。 今回はソーシャルログインについて触れてみようと思います。
ソーシャルログインとは
こういうやつです。
Googleとか、Facebookでログインできるよって画面があって、
ボタンを押すと、外部サービスのログイン画面に飛んで、
ポチポチボタンを押すと、Googleとかの認証情報を使ってログインができてしまう
実装イメージ
画像のような流れのログインフローです。サンプルはGoogleで、IOS環境での実装をしています。 多くの場合、OAuth2という規格に則って対象の認証プロバイダー(GoogleとかFacebook)からユーザーの認証情報を取得することになります。
OAuth認証プロバイダーの設定
- 外部サービスを認証プロバイダーとして使用する場合は、事前に設定が必要になります。今回はGoogleをサンプルに実装するので、設定画面を簡単に記述します。
1 OAuthクライアントIDの作成
バンドルID: iosアプリの識別子らしく認証情報として設定が必要なようです。xcodeビルド時の設定画面から値を確認できます。
設定が完了すると下記のような画面で設定情報が見れます。(モバイル側の設定で使用します)
2 OAuth同意画面の作成 - ユーザータイプを外部にして作成します。以降の設定もありスコープの設定などができるのですが、省略します。
おおまかな実装
ほとんどの認証プロバイダーで認証方法を書いてくれているので、基本的には言われた通りに処理するコードを書けば問題ないはずです。
使用技術
- モバイル側: Flutterを使った実装です。認証プロバイダーへのリクエストは、appAuthというライブラリを使用しています。
- サーバー側: FastAPI.
サンプルとして使用したフレームワークを記載していますが、実装そのものよりも大まかな流れを把握することがメインのため、実装手段自体は深くは記述しません。
(言語や技術が変わっても同じフローであれば、やることは大きく変わらないはず)
実装と大まかな解説
1 認証プロバイダーへ認可コードを取得するリクエストを送信(実装イメージの①~④までの部分)
final AuthorizationResponse? result = await appAuth.authorize( AuthorizationRequest( clientId, redirectUri, discoveryUrl: discoveryUrl, scopes: ['email'], promptValues: ['login'], ),
このリクエストの送信でGoogle画面で入力する部分は終わりです。
注意事項(補足):
コード上ではわかりずらいですが、code_verifier(code_challenge)という値がリクエストに含まれています。 Google側に認可コードをもとにアクセストークンを取得する際リクエストを送信する際に、code_verifierを含める必要があります。 (認可コードの検証時に、code_verifierと認可コードの組み合わせで検証させるため)
Webブラウザと異なりモバイルアプリはリダイレクトを受け取るURLが存在しないため、リダイレクトURLではなく、スキームURLを設定します。コード上ではわかりにくいですが、resultの取得はスキームURLへのリダイレクトで取得されます。IOSの場合は、Info.plistに下記のような設定を入れるのかと思います
<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLSchemes</key> <array> <string>{プロバイダー指定のスキームURL}</string> </array> </dict> </array>
リクエスト内容詳細
- 他にも付与されるパラメータはありますが、メインの部分のみを記述しています
パラメータ名 | 説明 |
---|---|
client_id=クライアントID(認証情報で設定した値) | Google APIコンソールで取得したクライアントIDです。このIDを使って、どのアプリケーションがリクエストを送信しているかを識別します。 |
response_type=code | 認可コードを要求することを示します。OAuth2フローでは、これを使って後でアクセストークンを取得します。 |
code_challenge=ランダム文字列 | PKCEフローの一部で、code_verifier をハッシュ化したものです。後でアクセストークンを取得するときに、code_verifier とこのcode_challenge が一致するかどうかがチェックされます。 |
code_challenge_method=S256 | PKCE(Proof Key for Code Exchange)において、code_challenge を生成する方法としてSHA-256を使用することを指定しています。 |
redirect_uri=リダイレクト先のURI(認証情報で設定した値) | 認可コードを返すためのリダイレクト先のURIです。ここでは、カスタムスキームが使われています。モバイルアプリでは、認証後にこのURIにリダイレクトされて認可コードが送られます。 |
scope=email | ユーザーのメールアドレス情報にアクセスするために要求する権限の範囲です(スコープ)。Googleは、email スコープに基づいてメールアドレスのアクセスを提供します。 |
state=ランダム文字列 | リクエストの状態を表すランダムな文字列です。認可サーバーはこの値をリダイレクトの際に返すため、アプリケーションはリダイレクト時のstateのがリクエスト時と一致するかを検証することで、リクエストの一貫性を確認できます。 |
2 うまくいくと、認可コードが取得できます。
3 取得した認可コードを含めて自社サーバーへログインリクエスト(画像イメージの⑤の部分)
# ↓このリクエストの部分 Future<void> _sendAuthorizationCodeToServer(AuthorizationResponse googleAuthResponse) async { final response = await http.post( Uri.parse('http://localhost:8008/customer/auth/login/google'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'authorization_code': googleAuthResponse.authorizationCode, 'redirect_uri': redirectUri, 'client_id': clientId, 'code_verifier': googleAuthResponse.codeVerifier, }), ); if (response.statusCode == 200) { print('Login successful: ${response.body}'); final responseData = jsonDecode(response.body); String accessToken = responseData['access_token']; String refreshToken = responseData['refresh_token']; // アクセストークンとリフレッシュトークンをセキュアストレージに保存 await secureStorage.write(key: 'access_token', value: accessToken); await secureStorage.write(key: 'refresh_token', value: refreshToken); setState(() { _isLoggedIn = true; // ログイン状態を更新 }); print('Login successful: Access and Refresh tokens saved.'); } else { print('Failed to log in: ${response.body}'); } }
3 サーバー側で認証コードをもとにアクセストークンを取得(画像イメージの⑥~⑦の部分)
4 アクセストークンをもとに、Googleユーザーの情報を取得(画像イメージ⑧~⑨の部分)
class GoogleOAuthClient: TOKEN_URL = 'https://oauth2.googleapis.com/token' USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo' async def exchange_code_for_tokens(self, authorization_code: str, code_verifier: str, client_id: str, redirect_uri: str): headers = { 'Content-Type': 'application/x-www-form-urlencoded' } token_data = { 'code': authorization_code, 'client_id': client_id, 'redirect_uri': redirect_uri, 'grant_type': 'authorization_code', 'code_verifier': code_verifier, 'scope': 'email' } async with httpx.AsyncClient() as client: response = await client.post(url=self.TOKEN_URL, headers=headers, data=token_data) response.raise_for_status() return response.json() async def get_user_info(self, access_token: str): headers = {'Authorization': f'Bearer {access_token}'} async with httpx.AsyncClient() as client: response = await client.get(self.USERINFO_URL, headers=headers) response.raise_for_status() return response.json()
アクセストークンを取得して、Googleのユーザー情報を取得します。
token_response = await self.google_oauth_client.exchange_code_for_tokens( authorization_code=google_login_data.authorization_code, code_verifier=google_login_data.code_verifier, client_id=google_login_data.client_id, redirect_uri=google_login_data.redirect_uri) user_info_response = await self.google_oauth_client.get_user_info(access_token=token_response['access_token'])
5 ユーザー情報が自社サーバーで登録済みか検証し、未登録の場合はユーザー作成
6 JWT(アクセストークン、リフレッシュトークン)を自社サーバーで作成しレスポンスを返却
customer: Customer = await self.customer_repository.get_by_email(db=self.db_session, email=user_info_response['email']) if not customer: customer = await self.customer_repository.create(db=self.db_session, name='とりあえずテスト', email=user_info_response['email']) customer_id = customer.id await self.db_session.commit() access_token = AuthUtil.create_access_token(user_id=customer_id, type='customer') refresh_token = AuthUtil.create_refresh_token(customer_id, type='customer') return {"access_token": access_token, "refresh_token": refresh_token}
7 アクセストークン、リフレッシュトークンをモバイル側で受け取れればログイン成功
if (response.statusCode == 200) { print('Login successful: ${response.body}'); final responseData = jsonDecode(response.body); String accessToken = responseData['access_token']; String refreshToken = responseData['refresh_token']; // アクセストークンとリフレッシュトークンをセキュアストレージに保存 await secureStorage.write(key: 'access_token', value: accessToken); await secureStorage.write(key: 'refresh_token', value: refreshToken); setState(() { _isLoggedIn = true; // ログイン状態を更新 }); print('Login successful: Access and Refresh tokens saved.'); } else { print('Failed to log in: ${response.body}'); }
おわりに
- toC向けのサービスで実装する機会の多いソーシャルログインの実装を記述しました。 Webブラウザでの実装はイメージがついていたのですが、モバイルだと想定しているフローで実装できるかがわからなかったので、 今回の記事で実装イメージがついて勉強になりました。
- この記事を通じて、一通りのソーシャルログインの流れを理解する一助になれば幸いです。
弊社ではエンジニアを募集しております。 ご興味がありましたらカジュアル面談も可能ですので、是非ご応募ください!