every Tech Blog

株式会社エブリーのTech Blogです。

AppleとLINEのネイティブ認証をつくる(iOS編)

この記事は every Tech Blog Advent Calendar 2025 の 28 日目の記事です。

はじめに

こんにちは!開発1部で食事管理アプリ ヘルシカ の開発をしている新谷です。これまでサーバーサイドを担当していましたが、直近ではiOS開発にも携わっています。

ヘルシカiOSでは、これまでWebViewベースの認証を採用していましたが、AppleとLINEのネイティブ認証を導入しました。ネイティブ認証では、Appleなら顔認証やパスコード、LINEならLINEアプリでのワンタップ認証が可能になり、ユーザー体験が大きく向上します。

本記事では、iOS側の実装について解説します。認証の仕組みやサーバー側の設計については、明日公開予定の「サーバー編」をご覧ください。

ネイティブ認証の全体像

ネイティブ認証のフローは以下のようになります。

ポイントは、認証サーバーが生成したnonceをSDKに渡すことです。これにより、サーバー側でID Tokenの検証時にリプレイ攻撃を防ぐことができます。nonceの役割や検証の詳細については、明日の「サーバー編」で解説します。

Sign in with Appleの実装

Sign in with AppleにはAuthenticationServicesフレームワークを使用します。

developer.apple.com

ASAuthorizationAppleIDProviderの使い方

import AuthenticationServices

func signInWithApple(nonce: String) {
    let provider = ASAuthorizationAppleIDProvider()
    let request = provider.createRequest()
    request.requestedScopes = [.fullName, .email]
    request.nonce = nonce  // サーバーから取得したnonceを設定
    
    let controller = ASAuthorizationController(authorizationRequests: [request])
    controller.delegate = self
    controller.presentationContextProvider = self
    controller.performRequests()
}

Delegateでの結果受け取り

extension AppleNativeAuthProvider: ASAuthorizationControllerDelegate {
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential,
              let identityTokenData = credential.identityToken,
              let idToken = String(data: identityTokenData, encoding: .utf8),
              let authorizationCodeData = credential.authorizationCode,
              let authorizationCode = String(data: authorizationCodeData, encoding: .utf8)
        else {
            // エラーハンドリング
            return
        }
        
        // idToken と authorizationCode をサーバーに送信
    }
    
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithError error: Error
    ) {
        // ユーザーキャンセルやその他のエラー処理
    }
}

取得できるもの

Sign in with Appleからは以下の情報を取得できます。

項目 説明
ID Token JWTフォーマット。nonceが含まれる
Authorization Code サーバーでのトークン取得に使用
User Identifier ユーザーの一意な識別子
Full Name 初回認証時のみ取得可能
Email 初回認証時のみ取得可能

LINE SDKの実装

LINE LoginにはLINE SDK for iOS Swiftを使用します。

developers.line.biz

LINE SDKのセットアップ

Swift Package Managerで以下のURLを追加します。

https://github.com/line/line-sdk-ios-swift.git

Info.plistにも設定が必要です。

<key>LineSDKConfig</key>
<dict>
    <key>ChannelID</key>
    <string>YOUR_LINE_CHANNEL_ID</string>
</dict>

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>line3rdp.$(PRODUCT_BUNDLE_IDENTIFIER)</string>
        </array>
    </dict>
</array>

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>lineauth2</string>
</array>

LoginManagerの使い方

import LineSDK

func signInWithLine(nonce: String, from viewController: UIViewController) {
    LoginManager.shared.login(
        permissions: [.profile, .openID],
        in: viewController,
        parameters: .init(IDTokenNonce: nonce)  // サーバーから取得したnonceを設定
    ) { result in
        switch result {
        case .success(let loginResult):
            guard let idToken = loginResult.accessToken.IDToken else {
                // エラーハンドリング
                return
            }
            // idToken をサーバーに送信
            
        case .failure(let error):
            // ユーザーキャンセルやその他のエラー処理
        }
    }
}

Appleとの違い

LINE SDKとSign in with Appleの主な違いは、Authorization Codeの有無です。Appleではサーバーでのリフレッシュトークン取得にAuthorization Codeが必要ですが、LINEではリフレッシュトークンがSDK内部で管理されます。

共通点として、どちらも独自のnonceを設定でき、ID Tokenを取得できます。

最初の設計と問題点

クリーンアーキテクチャでの設計

ヘルシカiOSではクリーンアーキテクチャを採用しています。アーキテクチャの詳細についてはヘルシカiOSアプリのアーキテクチャについてをご覧ください。

当初、ネイティブ認証も既存のアーキテクチャに従って以下のように設計しました。

Feature層(ViewModel)
    ↓
UseCase層
    ↓
Repository層
    ↓
Infra層(SDK呼び出し)
    ↓
外部SDK(LINE SDK / AuthenticationServices)

問題点:認証処理がViewに影響を与える

実装を進める中で、この設計には問題があることがわかりました。

Sign in with Appleは ASAuthorizationController で認証処理を実行すると認証UIが表示され、ASAuthorizationControllerPresentationContextProviding で表示先のWindowを指定します。

// Sign in with Apple:認証UIを表示するためにWindowを指定
extension AppleAuthProvider: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        return window
    }
}

LINE SDKも同様に、認証処理を呼び出すとLINEアプリまたはWebViewが起動し、Viewに影響を与えます。

// LINE SDK:認証処理を呼び出すとLINEアプリまたはWebViewが起動
LoginManager.shared.login(
    permissions: [.profile],
    in: viewController,
    parameters: .init(IDTokenNonce: nonce)
)

つまり、これらの認証処理を呼び出すとViewレイヤーに影響を与えることになります。

Infra層は本来、外部APIやLocalStorageなど、UIに依存しない外部リソースへのアクセスを担当する層です。 認証処理がViewに影響を与えるものをInfra層に配置するのは、アーキテクチャとして適切でないと考えました。

解決策:NativeAuthパッケージの分離

この問題を解決するために、認証処理をクリーンアーキテクチャの外に独立したパッケージとして分離しました。

新しいアーキテクチャ

Feature層(ViewModel)
    │
    ├──────────────────────> NativeAuthパッケージ(独立)
    │                            ├── LineNativeAuthProvider
    │                            └── AppleNativeAuthProvider
    ↓
UseCase層
    ↓
Repository層
    ↓
Infra層

ポイント

  • NativeAuthパッケージをクリーンアーキテクチャとは独立した位置に配置
  • ViewModelから直接NativeAuthProviderを呼び出す構成に変更
  • UseCase/Repository/Infra層はサーバーとの通信(nonce取得、ID Token検証)に専念

この設計には、UIに依存する処理をInfra層に置かずに済み、認証処理を独立パッケージとして管理できるというメリットがあります。一方で、ViewModelが認証処理を直接呼び出すため、Feature層の責務が増えるというデメリットもあります。

ただ、Infra層にUI依存のコードを置くことの違和感の方が大きかったため、今回はこの設計を選びました。

まとめ

最近サーバーサイドからiOS開発も担当するようになったので、モバイルアプリ特有のアーキテクチャには苦戦しました。 特にViewはサーバーでは意識しない概念だったので、今後も適切な場所に配置できるよう気をつけていきたいです。

明日は「サーバー編」として、nonceの役割やID Tokenの検証など、サーバー側の実装についての記事が公開されます。ぜひそちらもご覧ください。

参考資料