背景
Expoを使ってWebViewを表示するネイティブアプリを作成し、その中で自作のWebアプリにログインできるようにしようとしました。しかし、ログイン情報をWebView側に渡せない問題に直面し、さまざまな試行錯誤を行いました。本記事では、その経緯を記録します。
現在、完全に迷路に迷い込み中…
Webアプリ側のログイン機能
WebアプリはNext.jsで作成しており、NextAuth.jsとGoogleのOAuth 2.0認証を使用してログイン機能を実装しています。
吉井健文さんの著書『実践Next.js』の第7章「認証機能」を大いに参考にしているので、興味がある方は読んでみてください。
Expo WebViewでログインを試みる
まず、Expoでネイティブアプリを作成し、WebView内でWebアプリを表示してログインを試みました。
エラー発生
しかし、ログインしようとすると、以下のエラーが発生しました。
403: disallowed_useragent
これは、GoogleのOAuth 2.0認証がWebView内での認証を禁止しているために発生するエラーです。Googleはセキュリティの観点からWebView内でのOAuth認証を制限しています。
解決策の検討
このエラーを回避するためには、
- WebViewでログイン処理を行わず、Expo側でログイン処理を実装
- ネイティブアプリでログイン後にaccessTokenをWebView側に渡す
という方法が必要であることが調べてわかりました。
Expo側のログイン処理を実装
WebViewの実装
webアプリを表示するためのWebviewコンポーネントにref
, injectedJavaScript
, onMessage
を設定します。
injectedJavascript
では、webview内のwebアプリからnativeアプリ側にpostMessage
するためのwindow関数を設定しています。このwindow
関数をwebアプリ側のログインボタンを押下した時に発火させることでnativeアプリ側に特定のメッセージ「google-login」を送信できるようにします。
onMessage
ではnativeアプリ側のmessage
イベントを検知し、それが「google-login」であればnativeアプリ側で設定したログイン処理(promptAsync
)を発火させるようにしています。
import { WebView } from "react-native-webview";
import { useLoginOnGoogleAuth } from "@/hooks/useLoginOnGoogleAuth";
export default function HomeScreen() {
const { webViewRef, promptAsync } = useLoginOnGoogleAuth();
return (
<WebView
ref={webViewRef}
source={{ uri: "https://web_application/" }}
injectedJavaScript={`
window.requestGoogleLogin = function() {
window.ReactNativeWebView.postMessage("google-login");
};
`}
onMessage={(event) => {
const message = event.nativeEvent.data;
if (message === "google-login") {
promptAsync();
}
}}
userAgent="ExpoWebView"
/>
);
}
Expo側のGoogleログイン処理
message
イベントを検知し発火させるpromptAsync
を返しているuseLoginOnGooleAuth
関数では、Expo側で提供されているライブラリを使ってgoogleへの認証処理を実装しています。
useEffect
では認証が成功したら取得したaccessToken
をwebview表示しているwebアプリ側にaccessTokenを渡すようにしています。
import { useEffect, useRef } from "react";
import * as Google from "expo-auth-session/providers/google";
import { WebView } from "react-native-webview";
import * as WebBrowser from "expo-web-browser";
export function useLoginOnGoogleAuth() {
WebBrowser.maybeCompleteAuthSession(); // 設定すると別タブでログイン後、タブを閉じてアプリ側に戻る
const webViewRef = useRef<WebView | null>(null);
const [request, response, promptAsync] = Google.useAuthRequest({
clientId: process.env.EXPO_PUBLIC_GOOGLE_ID || "",
redirectUri: process.env.EXPO_PUBLIC_GOOGLE_REDIRECT_URI || "",
scopes: ["profile", "email"],
});
useEffect(() => {
// google認証に成功したらaccessTokenを取得しwebview表示しているwebアプリ側にaccessTokenを渡す。
if (response?.type === "success") {
const { authentication } = response;
if (authentication?.accessToken) {
webViewRef.current?.postMessage(
JSON.stringify({ token: authentication.accessToken })
);
}
}
}, [response]);
return { webViewRef, promptAsync };
}
この実装により、WebView内のWebアプリのログインボタンを押すと、Expo側のログイン処理が発火し、Googleの認証画面が表示されます。
しかし、ログイン後にリダイレクトされた画面で、
Something went wrong trying to finish signing in. Please close this screen to go back to the app.
というエラーが発生しました。
auth.expo.ioが問題?
エラーが発生した時のリダイレクトURIを確認すると、
https://auth.expo.io/@useName/appName?state=...&code=...
となっており、Googleの認可コード(code
)は発行されていることがわかりました。
Expoには auth.expo.io
というOAuthプロキシがあり、本来であればcode
がExpo側に返されればOAuthプロキシがaccessToken
を取得するためにOAuth認証を進めてくれるはずです。
つまり、これを利用すると開発者がcode
取得後からaccessToken
取得までの処理を自動でやってくれるはずですが、なぜかうまくいきません。。Expoを使えばバックエンドを用意しなくてもOAuth認証が完結するはずなのに。。
エラーについて調べてみるとauth.expo.io
の代わりに自分でバックエンドAPIを実装する方法もありました。
BEの環境はwebアプリを構築しているNext.jsのapi routes
を使えば実装できると思ったのでこの方法を試してみることにしました。
自作のバックエンドAPIでトークン取得を試す
auth.expo.io
を使わず、自作のバックエンドAPIを redirectUri
に設定し、accessTokenを取得するように変更しました。webアプリ側のNext.jsの環境でapi routesを使ってバックエンドAPIを作成しました。
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
if (!code) {
return NextResponse.json({ error: 'Missing code' }, { status: 400 });
}
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID || "",
client_secret: process.env.GOOGLE_CLIENT_SECRET || "",
redirect_uri: 'https://your-backend.com/api/auth/google/callback',
grant_type: 'authorization_code',
code, // 認可コード(code)をパラメータに設定する
}),
});
const tokenData = await tokenResponse.json();
if (!tokenResponse.ok) {
return NextResponse.json({ error: tokenData }, { status: 400 });
}
return NextResponse.json({ access_token: tokenData.access_token });
}
これで
- ExpoアプリでGoogleログイン
- 認可コードを取得
- 自作のNext.js APIへリダイレクト
- GoogleにaccessTokenをリクエスト
- アプリにaccessTokenを返す
- WebViewにトークンを渡す
という流れが構築されました。
しかし、今度は code_verifier
がないというエラーが発生。
まだ解決していないので、引き続き試行錯誤中です。。
まとめ
ExpoのWebView内でGoogle OAuthを使う場合、直接WebViewでログインはできないため、ネイティブアプリ側でログイン処理を行い、accessToken
をWebViewに渡す必要があります。しかし、auth.expo.ioの制約や、独自バックエンドを用いた場合のcode_verifier
の問題など、まだ課題が残っている状況です。
今後も解決策を模索しつつ、進捗あれば追記したいです。