0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Expo WebViewでログイン情報を渡すまでの試行錯誤

Posted at

背景

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)を発火させるようにしています。

index.tsx
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を渡すようにしています。

useLoginOnGoogleAuth.ts
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 };
}

上記の処理を図示化した時は以下のようになります。
無題のプレゼンテーション (4).png

この実装により、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認証を進めてくれるはずです。

無題のプレゼンテーション (3).png

つまり、これを利用すると開発者が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を作成しました。

無題のプレゼンテーション (1).png

googleAuthCallback.ts
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 });
}

これで

  1. ExpoアプリでGoogleログイン
  2. 認可コードを取得
  3. 自作のNext.js APIへリダイレクト
  4. GoogleにaccessTokenをリクエスト
  5. アプリにaccessTokenを返す
  6. WebViewにトークンを渡す

という流れが構築されました。

しかし、今度は code_verifier がないというエラーが発生。

まだ解決していないので、引き続き試行錯誤中です。。


まとめ

ExpoのWebView内でGoogle OAuthを使う場合、直接WebViewでログインはできないため、ネイティブアプリ側でログイン処理を行い、accessTokenをWebViewに渡す必要があります。しかし、auth.expo.ioの制約や、独自バックエンドを用いた場合のcode_verifierの問題など、まだ課題が残っている状況です。

今後も解決策を模索しつつ、進捗あれば追記したいです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?