
この記事は 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フレームワークを使用します。
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 | 初回認証時のみ取得可能 |
| 初回認証時のみ取得可能 |
LINE SDKの実装
LINE LoginにはLINE SDK for iOS Swiftを使用します。
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の検証など、サーバー側の実装についての記事が公開されます。ぜひそちらもご覧ください。