[Capacitor/Auth0]Auth0 SDKのCapacitorプラグインを作成し、Capacitorアプリに認証機能を導入する

Tomoki Mizogami
nextbeat-engineering
26 min readOct 24, 2023

こんにちは、ネクストビートでエンジニアをしている溝上と申します。子育てフレンドリーな社会の実現を目指す、KIDSNA プラットフォームの ID 基盤の開発を担当しております。

今回は、認証基盤である Auth0 を使って、Capacitor で作られたモバイルアプリケーション (iOS, Android) に認証機能を導入する方法について書いていきます。

Capacitor アプリケーションに Auth0 を導入する方法はいくつかありますが、今回は Auth0 用の Capacitor プラグインを独自に作成し、JavaScript のフレームワークに依らずに認証機能を実装する方法について述べていきます。

Auth0 公式では、Angular / React / Vue それぞれの Auth0 SDK を使った方法が紹介されております。JavaScript フレームワークベースでの認証が必要な方は、こちらも併せてご確認ください。

本記事は、以下の内容で構成しています。

  • iOS の Auth0 SDK について
  • Android の Auth0 SDK について
  • iOS、Android の Auth0 SDK をラップする Capacitor プラグインの作成
  • 認証機能の実装

それでは、始めましょう。

Auth0 SDK について

Auth0 が用意している SDK を使って、アプリケーションに簡単に認証機能を実装することができます。

iOS、Android のそれぞれのチュートリアルは以下のドキュメントで解説されています。このチュートリアルに基づいて、Capacitor プラグインを作成していきます。

共通の設定

iOS、Android 共通に必要な設定は、Auth0 側のクライアント設定です。異なるのはログイン完了後、ログアウト完了後のコールバック URI のみですので、共通のクライアントとして作成します。

コールバック URI は、iOS と Android どちらもカスタム URL スキームです。

  • iOS: YOUR_BUNDLE_IDENTIFIER://{yourDomain}/ios/YOUR_BUNDLE_IDENTIFIER/callback
  • Android: demo://{yourDomain}/android/YOUR_APP_PACKAGE_NAME/callback

クライアントの設定は以下になります (Auth0 のアプリケーションは Terraform で管理できます!)。

client.tf

locals {
auth0_domain = "ここにドメインを入れる"
}

resource "auth0_client" "capacitor-auth0-sample" {
name = "capacitor-auth0 Sample"
description = "capacitor-auth0 プラグインのサンプルプロジェクト"
app_type = "native"
callbacks = [
# iOS のログイン用コールバック
format(
"com.github.taretmch.SampleApp://%s/ios/com.github.taretmch.SampleApp/callback",
local.auth0_domain
),
# Android のログイン用コールバック
format(
"demo://%s/android/com.github.taretmch/callback",
local.auth0_domain
),
]
allowed_logout_urls = [
# iOS のログアウト用コールバック
format(
"com.github.taretmch.SampleApp://%s/ios/com.github.taretmch.SampleApp/callback",
local.auth0_domain
),
# Android のログアウト用コールバック
format(
"demo://%s/android/com.github.taretmch/callback",
local.auth0_domain
),
]
grant_types = [
"authorization_code",
"refresh_token",
]
client_metadata = {}

jwt_configuration {
alg = "RS256"
lifetime_in_seconds = 36000
scopes = {}
secret_encoded = false
}

native_social_login {
apple {
enabled = false
}
facebook {
enabled = false
}
}
}

また、ネイティブアプリだと認可コードフロー+PKCEの認証フローのみ利用するため、Auth0 のダッシュボード上で「Authentication Methods」を「None」に変更する必要があります。ここは terraform での設定方法がわからなかったので、手動で設定しました。

iOS SDK について

iOS の SDK を利用するにあたり必要な作業は以下の通りです。

  • Auth0 SDK を依存関係に入れる。
  • iOS アプリにカスタムURLスキームを設定する。
  • Auth0.plist にドメインとクライアントIDを設定するか、都度 SDK のメソッドに引数として渡す。

また、Web 認証での基本的な機能は以下の通りです。

  • ログイン機能: Auth0.webAuth().start { ... }
  • ログアウト機能: Auth0.webAuth().clearSession { ... }
  • IDToken からユーザー情報を取得する機能

Android SDK について

Android の SDK を利用するにあたり必要な作業は以下の通りです。

  • Auth0 SDK を依存関係に入れる。
  • strings.xml からドメインとクライアントIDを取得するために、manifestPlaceholdersbuild.gradle に追加する。
  • android.permission.INTERNET を permission に追加する。

また、Web 認証に基本的な機能も iOS と同様に提供されています。

以上の iOS / Android 共通の機能を、Capacitor プラグインの機能として作成していきます。

Auth0 SDK 用の Capacitor プラグイン

それでは、上記の iOS / Android の SDK をラップして JavaScript から呼び出せるようにする、Capacitor プラグインを作成していきます。

Capacitor プラグインを作成するコマンドが用意されているので、簡単にテンプレートを作成することができます。

npm init @capacitor/plugin@latest

@taretmch/capacitor-auth0 というパッケージで CapacitorAuth0Plugin を作成しました。

プラグインの設計

まずは、JavaScript から呼び出すプラグインのインタフェースを作成します。 今回は、SDK が提供する機能に従って以下の3つのメソッドを定義しました。

  • configure: Auth0 SDK の設定を初期化するメソッド。
  • login: ログインを実行するメソッド。
  • logout: ログアウトを実行するメソッド。

src/definition.ts に、Plugin のメソッドとインタフェースを定義します。簡略化のため、ログイン完了時に得られるユーザー情報は id name email のみとしました。

export interface CapacitorAuth0Plugin {
configure(options: CapacitorAuth0Conf): Promise<void>;
login(): Promise<Auth0User>;
logout(): Promise<void>;
}

export interface CapacitorAuth0Conf {
domain: string;
clientId: string;
}

export interface Auth0User {
id: string;
name: string;
email: string;
}

メソッドとインタフェースを定義したら、Web 用のプラグインを実装します。Web 用の実装は src/web.ts に実装します。今回は Web アプリケーションは対象外とするので、Web の実装では Promise.reject を返すようにしています。なるべく Web の場合も実装できると良いですが、JavaScript の SDK が必要になるので、今回は触らないことにします。

export class CapacitorAuth0Web
extends WebPlugin
implements CapacitorAuth0Plugin {

configure(_options: CapacitorAuth0Conf): Promise<void> {
return Promise.reject('Not implemented on web.');
}
login(): Promise<Auth0User> {
return Promise.reject('Not implemented on web.');
}
logout(): Promise<void> {
return Promise.reject('Not implemented on web.');
}
}

こちらが終わったら build して、特にコンパイルエラーが出ないこと、README が更新されていることを確認します。

npm install
npm run build

プラグイン実装 — iOS

インタフェースを実装したら、ネイティブ側の実装を行ないます。iOS のチュートリアルに従って、以下を行ないます。

  • Auth0 SDK の依存関係への追加
  • ログイン機能の実装
  • ログアウト機能の実装

まずは、Auth0 SDK を ios/PodfileTaretmchCapacitorAuth0.podspec に追加します。Podfile に追加後、 ios ディレクトリで pod install すると依存関係が正常にインストールされるはずです。

platform :ios, '13.0'

def capacitor_pods
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
pod 'Capacitor', :path => '../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../node_modules/@capacitor/ios'
end

target 'Plugin' do
capacitor_pods
# Auth0.swift の依存を追加
pod 'Auth0', '~> 2.5'
end

target 'PluginTests' do
capacitor_pods
end
require 'json'

package = JSON.parse(File.read(File.join(__dir__, 'package.json')))

Pod::Spec.new do |s|
s.name = 'TaretmchCapacitorAuth0'
s.version = package['version']
s.summary = package['description']
s.license = package['license']
s.homepage = package['repository']['url']
s.author = package['author']
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
s.ios.deployment_target = '13.0'
s.dependency 'Capacitor'
s.dependency 'Auth0' <~~~~~~~~~~~ [comment]Auth0.swift の依存を追加
s.swift_version = '5.1'
end

プラグインの機能は CapacitorAuth0Plugin.swift に実装します。ここでは、インタフェースとして定義した3つのメソッド、 configure login logout を実装していきます。

configure は、 loginlogout に必要なパラメータを受け取るだけなので、クラスのフィールド変数に保存する処理を定義しています。

var clientId: String?
var domain: String?

@objc func configure(_ call: CAPPluginCall) {
guard
let clientId = call.getString("clientId"),
let domain = call.getString("domain") else {
call.reject("Failed to init CapacitorAuth0")
return
}

self.clientId = clientId
self.domain = domain
call.resolve()
}

login は、 configure で設定したパラメータを利用して、Auth0 SDK のログイン機能を呼び出します。ログインが完了すればユーザー情報を返し、失敗すればエラーを返すようにしています。

@objc func login(_ call: CAPPluginCall) {
guard
let clientId = self.clientId,
let domain = self.domain else {
call.reject("Plugin is not configured. Call configure method before login.")
return
}

Auth0
.webAuth(clientId: clientId, domain: domain)
.start { result in
switch result {
case .success(let credentials):
let user = User(from: credentials.idToken)
call.resolve([
"id": user?.id,
"name": user?.name,
"email": user?.email
])
case .failure(let error):
call.reject("Failed with: \(error)")
}
}

}

User は利用するフィールドだけ入れており、metadata など追加したい場合にはこちらに入れるようにできればと思っています。

struct User {
let id: String
let name: String
let email: String
}

extension User {
init?(from idToken: String) {
guard let jwt = try? decode(jwt: idToken),
let id = jwt.subject,
let name = jwt["name"].string,
let email = jwt["email"].string else {
return nil
}
self.id = id
self.name = name
self.email = email
}
}

logout は、configure で設定したパラメータを利用して、Auth0 SDK のログアウト機能を呼び出します。ログアウトが完了すれば Resolve を返し、失敗すればエラーを返すようにしています。

@objc func logout(_ call: CAPPluginCall) {
guard
let clientId = self.clientId,
let domain = self.domain else {
call.reject("Plugin is not configured. Call configure method before login.")
return
}

Auth0
.webAuth(clientId: clientId, domain: domain)
.clearSession { result in
switch result {
case .success:
call.resolve()
case .failure(let error):
call.reject("Failed with: \(error)")
}
}

}

ここでプラグインの iOS の実装は完了しました。あとは Capacitor アプリでプラグインをインストールし、カスタム URL スキームの設定を追加すれば、認証機能を利用できます。

npm install ../path/to/capacitor-auth0
npm run build
npx cap sync

カスタム URL スキームの設定: Info.plist

<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>${PRODUCT_BUNDLE_IDENTIFIER}</string>
</array>
</dict>
</array>

Capacitor アプリからは、通常の Capacitor プラグインと同様に利用できます。

import { CapacitorAuth0 } from '@taretmch/capacitor-auth0';

CapacitorAuth0.configure({
clientId: 'Auth0 のクライアント ID',
domain: 'Auth0 のドメイン'
});

// ログインボタン押下
self.shadowRoot.querySelector('#login').addEventListener('click', async function (e) {
try {
const user = await CapacitorAuth0.login()
console.log('login', user);
updateUI(user);
} catch (e) {
console.error(e);
}
});

// ログアウトボタン押下
self.shadowRoot.querySelector('#logout').addEventListener('click', async function (e) {
try {
const res = await CapacitorAuth0.logout()
console.log('logout', res);
updateUI(null);
} catch (e) {
console.error(e);
}
});

プラグイン実装 — Android

続いて、Android 側の実装を行ないます。Android もチュートリアルに従って実装していきましょう。

  • Auth0 SDK の依存関係への追加
  • ログイン機能の実装
  • ログアウト機能の実装

Capacitor 5 のプラグインを作成するので、JDK バージョンは 17、Gradle バージョンは 8.0.0 を使用しています。

Auth0 SDK をプラグインの build.gradle に追加します。また、プラグインを Kotlin で実装するので、Kotlin のライブラリも追加しておきます(Kotlin を追加してなくて3時間くらい runtime エラーでハマりました)。

ext {
...
androidxCoreKTXVersion = project.hasProperty('androidxCoreKTXVersion') ? rootProject.ext.androidxCoreKTXVersion : '1.10.0'
auth0Version = project.hasProperty('auth0Version') ? rootProject.ext.auth0Version : '2.10.2'
}

buildscript {
ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '1.9.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.0'
// Enable kotlin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

...
apply plugin: 'kotlin-android'

dependencies {
...
implementation "com.auth0.android:auth0:$auth0Version"
...
// Kotlin
implementation "androidx.core:core-ktx:$androidxCoreKTXVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

プラグインの機能は CapacitorAuth0Plugin.kt に実装します。Android も、インタフェースとして定義した3つのメソッド、 configure login logout を実装していきます。

Android の SDK では、 Auth0 のインスタンスを作成する必要があります。チュートリアルでは MainActivityonCreate メソッド上で初期化していますが、Capacitor アプリ側でインスタンス生成するよりもプラグイン側でこちらの処理は担っておきたいです。configure メソッドは、このインスタンスの初期化を行ないます。

private lateinit var auth0: Auth0

@PluginMethod
fun configure(call: PluginCall) {
try {
val clientId = call.getString("clientId")
?: throw IllegalArgumentException("clientId is required.")
val domain = call.getString("domain")
?: throw IllegalArgumentException("domain is required.")
this.auth0 = Auth0(clientId, domain)
call.resolve()
} catch (e: IllegalArgumentException) {
call.reject(e.message)
}
}

login は、 configure で初期化した Auth0 のインスタンスを利用して、Auth0 SDK のログイン機能を呼び出します。ログインが完了すればユーザー情報を返し、失敗すればエラーを返すようにしています。コルーチンを使って書いていますが、コールバックの書き方でも問題ありません。

@PluginMethod
fun login(call: PluginCall) {
CoroutineScope(Dispatchers.Main).launch {
try {
val credentials = WebAuthProvider.login(auth0)
.withScheme("demo")
.withScope("openid profile email")
.await(context)
val userProfile = getUserProfile(credentials.accessToken)

val data = JSObject()
data.put("id", userProfile.getId())
data.put("name", userProfile.name)
data.put("email", userProfile.email)
call.resolve(data)
} catch(e: AuthenticationException) {
call.reject(e.getDescription())
}
}
}

ちょっと余談なのですが、コルーチンを使わず suspend の修飾子をつけてメソッドを実行すると、Capacitor の実行時エラー java.lang.IllegalArgumentException: Wrong number of arguments; expected 2, got 1 になりました。どうやら suspend をつけたときの挙動が異なるようで、Capacitor プラグインは suspend をつけたメソッドをサポートしていないようです。

getUserProfile は、iOS と同様 JWT からユーザー情報を取得する処理です。

private suspend fun getUserProfile(accessToken: String): UserProfile {
val apiClient = AuthenticationAPIClient(this.auth0)
return apiClient.userInfo(accessToken).await()
}

logout は、configure で初期化した Auth0 インスタンスを利用して、Auth0 SDK のログアウト機能を呼び出します。ログアウトが完了すれば Resolve を返し、失敗すればエラーを返すようにしています。こちらもコルーチンで書いていますが、コールバックで書いていただいて構いません。

@PluginMethod
fun logout(call: PluginCall) {
CoroutineScope(Dispatchers.Main).launch {
try {
WebAuthProvider.logout(auth0)
.withScheme("demo")
.await(context)

call.resolve()
} catch (e: AuthenticationException) {
call.reject(e.getDescription())
}
}
}

これでプラグイン側の実装は完了しました。あとは Capacitor アプリで strings.xml へのドメインとクライアントIDの設定、 manifestPlaceholders の追加、android.permission.INTERNET の permission 追加を行なえば、認証機能を利用することができます。Android のカスタム URL スキームは Auth0 の SDK 側でインテントフィルターが設定されているので、追加の設定は必要ありません。

strings.xml

<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="com_auth0_domain">{Auth0ドメイン}</string>
<string name="com_auth0_client_id">{Auth0クライアントID}</string>
</resources>

アプリ側の build.gradle

android {
...
defaultConfig {
...
manifestPlaceholders = [auth0Domain: "@string/com_auth0_domain", auth0Scheme: "demo"]
}
}

android.permission.INTERNET に関しては、Capacitor アプリであれば初期設定で入っているかもしれません。

Android 側も動作確認してみましょう。ChromeCustomTabs が開かれ、ログイン画面が出るはずです。

以上で、Auth0 の iOS / Android の標準 SDK を使った Capacitor プラグインの作成ができました。

終わりに

今回作成したものは npm package として公開しており、自由にご使用いただくことができます。Auth0 のテナント、アプリケーションについてはご自身でご用意ください。

Capacitor でネイティブの SDK を使った認証機能を実装することを目的としていたので、プラグインのインタフェースのちゃんとした設計や細かいハンドリングは行なっておりません(Auth0 の設定エラー、JWT のパースエラー、user_metadata の取得など)。

また、capacitor-community にも同様のプラグインがあるのですが、3年前の状態で更新が止まっております。保守できそうであれば、community にも貢献してみたいと思います。

ここまで読んでいただき、ありがとうございました。

ネクストビートでは、社内でもいくつか Capacitor プラグインの作成と運用、保守を行なっております。

Capacitor、Ionic、Angular、Svelte を用いたモバイルアプリケーションの開発に興味のある方、カジュアルにお話できると嬉しいです。記事下のリンクからお申し込みいただけます。

We are hiring!

本記事をご覧いただき、ネクストビートの技術や組織についてもっと話を聞いてみたいと思われた方、カジュアルにお話しませんか?

・今後のキャリアについて悩んでいる
・記事だけでなく、より詳しい内容について知りたい
・実際に働いている人の声を聴いてみたい

など、まだ転職を決められていない方でも、ネクストビートに少しでもご興味をお持ちいただけましたら、ぜひカジュアルにお話しましょう!

🔽申し込みはこちら
https://hrmos.co/pages/nextbeat/jobs/1000008

また、ネクストビートについてはこちらもご覧ください。

🔽エントランスブック
https://note.nextbeat.co.jp/n/nd6f64ba9b8dc

--

--