every Tech Blog

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

Databricks DATA + AI WORLD TOUR に参加しました!

目次

はじめに

2025年11月28日に開催された「Databricks DATA + AI WORLD TOUR」に参加させていただきました。

今回は参加レポートとして、セッションの感想をお届けします!

dataaisummit.databricks.com

セッション紹介

zerobus Ingest

開発1部の吉田です。
Databricks Sessionで紹介されたzerobus ingestについてまとめます。

zerobus ingestは、クライアントからDatabricksへデータを直接ストリーミングできるマネージドサービスです。
従来のような複雑なメッセージバス(KafkaやKinesisなど)を介することなく、レコード単位でのデータ取り込みを実現します。

docs.databricks.com

中間インフラを排除できるため、運用管理の手間とコストが削減できます。

エブリーでは現在、サイネージ端末やWebアプリケーションのログ収集のために専用のインフラを構築・運用しています。
このインフラをzerobus ingestで置き換えることができるか、検証を進めていく予定です。

Agent Bricks

こんにちは!
開発1部デリッシュキッチンの蜜澤です。
僕からは、Databricks Sessionで紹介されたAgent Bricksについてまとめます!

Agent BricksはノーコードでAIエージェントの構築、評価、最適化できる機能になっています!
AIエージェントを作成する際の下記のような課題に簡単に対応することができます。

  • 調整すべき項目が多すぎる
  • 評価するのが難しい
  • コストと品質のバランスを取るのが難しい

Agent Bricksではタスクを選び、エージェントの役割をざっくり指定するだけで自動でエージェントを作成してくれるので、調整するべき項目が少なくなっているようです。
ドキュメントを元に質問に答える「ナレッジアシスタント」や、要約・分類などのカスタムテキスト変換を行う「カスタムLMM」、Genieスペースとエージェントを統合する「マルチエージェントスーパーバイザー」などのユースケースがあるようです。
エージェントの評価に関しては、自動でベンチマークを作成し、エージェントを自動で最適化できるようです。
また、good/badのような評価しかできないツールもありますが、Agent Learning from Human Feedback(ALHF)によって、自然言語での指示に基づき、システムを自動調整することもできます。
コストと品質に関しては、「コスト最適化」モデルと「品質最適化」モデルのどちらかを選択することができ、多くの場合は両立も可能なため、自前で構築するよりも高品質かつ低コストなエージェントを作成できることが多いようです。

Agent Bricksを使用してAIエージェントを作成すると、自前で作成する際に課題となる点を簡単に解決し、気軽にAIエージェントを作成できるのが魅力だと感じました。

デモパートでは、マルチエージェントスーパーバイザーを使用して、複数のエージェントを簡単にまとめる方法をご紹介いただきました。
PowerPointやWord形式のドキュメントと役割を与えられたナレッジアシスタントと、テーブルを読み取るためのGenieスペースを登録することで、ユーザーからの質問に対して、ナレッジを元に必要な情報を考え、必要な情報をGenieがクエリを作成しテーブルから取得するというマルチエージェントが簡単に作成できていました。
個人的に特に便利だなと感じたのは、ナレッジを元に回答できないパターンの質問に関しては、ガイドラインを登録し回答を準備しておくことが簡単にできることでした。

2025年11月時点ではasia-northeast1リージョンではAgent Bricksはまだ使用できないのですが、使用できるようになるのがとても楽しみになりました!

[企業セッション] イオンにおけるマルチエージェントシステムの開発

開発1部の岩﨑です。私からはイオン様におけるマルチエージェントシステムの開発について紹介します。

イオン様ではマルチエージェントシステムを「業務特化型エージェント」と「顧客向けAIエージェント」に大別して紹介されていました。

中でも業務特化型エージェントでは、自然言語の質問に基づいたクエリを実行して情報抽出するエージェントが紹介されました。

具体的には、データ抽出エージェントや可視化エージェントといったそれぞれのタスクに対するエージェントがあり、それをまとめるマルチエージェントスーパーバイザーをユーザとLLMとのインターフェースとしておくような構造となっています。

これによってクエリの生成から可視化、整合性の評価まで一気通貫で実行するようなマルチエージェントシステムを構築しています。

このエージェントはコンテキストが曖昧だった場合は推測で答えるのではなく、ユーザに聞き返すような仕組みとなっています。

また大規模なクエリをAIが実行しないように、システムリソースを大量に消費するクエリが生成された場合にはエージェントが主体となって実行しない意思決定が行われるようになっていたり、クエリをキャッシュする戦略など大規模データを扱っている企業ならではの工夫点などもみられました。

クエリの実行はDatabricks GenieとUnity Catalogを使用してAPIとして公開することでシームレスかつ低レイテンシで連携することができます。
また、Unity Catalogはガバナンスを意識した設計になっているためAIエージェントによる不正なデータアクセスなどの心配もありません。

さらにエージェントを作って終わりではなく、Genieへのリクエストを収集、フィードバックの作成、最適化のループを回すことによって精度改善にも取り組んでいるそうです。

他にも色々なエージェントの工夫点が紹介されており、非常に有意義なセッションでした。

まとめ

今回のDatabricks DATA + AI WORLD TOURでは、zerobus IngestやAgent Bricksといった機能の紹介から企業様の活用事例まで、幅広いセッションを聴講することができました。

AIエージェントに関するセッションが多く、ノーコードでのエージェント構築から大規模な業務システムへの適用までAIエージェントの活用が急速に広がっていました。

今回得られた知見を活かし、Databricksの機能の検証やAIエージェントの活用を進めていきたいと思います。

最後に

エブリーでは、ともに働く仲間を募集しています。

テックブログを読んで少しでもエブリーに興味を持っていただけた方は、ぜひ一度カジュアル面談にお越しください!

corp.every.tv

最後までお読みいただき、ありがとうございました!

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

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

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

こんにちは!開発1部で食事管理アプリ ヘルシカ のサーバーサイドの開発をしている 赤川 です。約1ヶ月にわたって続いたアドベントカレンダーも最終日となりました。

本記事では、ヘルシカiOS で Apple と LINE のネイティブ認証を導入した経験をもとに、ネイティブ認証のサーバー側の実装と周辺知識、実装時の細かいポイントについてお話しします。

iOS側の実装については、昨日公開の AppleとLINEのネイティブ認証をつくる(iOS編) をご覧ください。

tech.every.tv

前提

ヘルシカではもともと Web アプリベースの認証方法が採用されており、アプリ内で WebView が開きそこでIDやパスワードを入力して サインアップ / サインイン を行います。以下、AppleやLINEのIDプロバイダーを外部IdP、ヘルシカの認証サーバーを単に認証サーバーと呼ぶことにします。認証サーバーではOpenID Connectに則った実装がされており、おおまかには以下のような流れです。

  • ログインボタンを押す
  • アプリ内のWebViewで外部IdP(AppleやLINE)の入力画面が開く
  • 入力成功後、外部IdPから認証サーバーにコールバック
  • 認証サーバーが外部IdPとToken Exchangeを行い、認証サーバーが外部IdPのトークンを受け取る
  • 外部IdPのトークンの検証に成功した後、ヘルシカAPIサーバー用のトークンを発行し、安全にClientに渡す

アプリはほぼWebViewを開くだけで、IdPとメインでやり取りするのはサーバーであることがわかります。

上記の形だと、例えばヘルシカにWebアプリが増えた、という場合でも同じエンドポイントが使え、認証サーバーの追加実装なしに拡張が可能なのがメリットと言えます。しかし、パスワードなどの入力は面倒ですし、忘れがちです。そこで、ユーザーにより簡単にサインアップ・サインインをしてもらえるように、Appleなら顔認証などiOSネイティブな方法、LINEならLINEアプリが開いてワンタップで認証できる方法を新しく採用することになりました。

Appleの認証画面

LINEの認証画面

アプリ側のAppleとLINEのネイティブ認証実装

(サーバー編)とタイトルに入れましたが、サーバー側の設計をするためにはまずアプリ側でネイティブ認証を実装する方法を知らなければなりません。AppleではASAuthorizationAppleIDProviderなどのクラス、LINEではLINEログインSDKというものが用意されています。

developer.apple.com

developers.line.biz

元のフローを思い出してみると、外部IdPのトークンを受け取るのは認証サーバーでした。しかし、上記を使用した場合、IDトークンを受け取るのはアプリになります。それぞれについて、少し深掘りしてみましょう。

Apple

LINE

  • LINEでは、 LoginManager を使います。LoginManager.shared.login でリクエストを送信しますが、Appleの時と違い nonce と state はSDK内で自動的に生成、検証されます。ただし、 nonce は独自に指定することも可能です。
  • 成功すると、LoginResultを受け取ります。この中にアクセストークンやIDトークンなどが含まれています。

比較すると、以下の2つが共通していることがわかりました。

  • リクエストには独自の nonce を設定することができる。
  • 成功すると、アプリがIDトークンなどを受け取ることができる。

IDトークンと nonce

サーバー側の設計に進む前に、IDトークンと nonce について復習しておきます。ご存知の方は無視して次のパートに進んでいただいて問題ありません。

まず、IDトークンは次のような形をしています。

eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.
ewogImlzcyI6ICJodHRwczovL3NlcnZlci5leGFtcGxlLmNvbSIsCiAic3ViIjogIjI0ODI4OTc2MTAwMSIsCiAiYXVkIjogInM2QmhkUmtxdDMiLAogIm5vbmNlIjogIm4tMFM2X1d6QTJNaiIsCiAiZXhwIjogMTMxMTI4MTk3MCwKICJpYXQiOiAxMzExMjgwOTcwLAogIm5hbWUiOiAiSmFuZSBEb2UiLAogImdpdmVuX25hbWUiOiAiSmFuZSIsCiAiZmFtaWx5X25hbWUiOiAiRG9lIiwKICJnZW5kZXIiOiAiZmVtYWxlIiwKICJiaXJ0aGRhdGUiOiAiMDAwMC0xMC0zMSIsCiAiZW1haWwiOiAiamFuZWRvZUBleGFtcGxlLmNvbSIsCiAicGljdHVyZSI6ICJodHRwOi8vZXhhbXBsZS5jb20vamFuZWRvZS9tZS5qcGciCn0.
NTibBYW_ZoNHGm4ZrWCqYA9oJaxr1AVrJCze6FEcac4t_EOQiJFbD2nVEPkUXPuMshKjjTn7ESLIFUnfHq8UKTGibIC8uqrBgQAcUQFMeWeg-PkLvDTHk43Dn4_aNrxhmWwMNQfkjqx3wd2Fvta9j8yG2Qn790Gwb5psGcmBhqMJUUnFrGpyxQDhFIzzodmPokM7tnUxBNj-JuES_4CE-BvZICH4jKLp0TMu-WQsVst0ss-vY2RPdU1MzL59mq_eKk8Rv9XhxIr3WteA2ZlrgVyT0cwH3hlCnRUsLfHtIEb8k1Y_WaqKUu3DaKPxqRi6u0rN7RO2uZYPzC454xe-mg

https://openid.net/specs/openid-connect-core-1_0.html#id_tokenExample

2つのドットがあり3つのパートに区切られていることがわかります。これらは前から順にヘッダー、ペイロード、署名と呼ばれていて、このような形式のトークンを署名付き JWT (JSON Web Token) と言います。それぞれについて詳しく見ていきましょう。

まずヘッダーを Base64url デコードすると、以下のようなJSONが得られます。

{"kid":"1e9gdk7","alg":"RS256"}

algはJWTの署名に使用されたアルゴリズムを表します。 kid は署名検証用の公開鍵の識別子で、公開鍵暗号方式の署名の場合に含まれます。type: "JWT" というフィールドが含まれている場合もあります。

次に、ペイロードを検証、デコードしてみると、だいたい以下のような JSON が得られます。

{
"iss": "https://server.example.com",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"gender": "female",
"birthdate": "0000-10-31",
"email": "[email protected]",
"picture": "http://example.com/janedoe/me.jpg"
}

下の方はプロフィール的な情報なので無視して、上側のフィールドの説明を書き込むと以下のようになります。

{
"iss": "トークンを発行したサーバーの識別子",
"sub": "ユーザーの識別子",
"aud": "トークンを受け取るアプリの識別子(クライアントIDなど)",
"nonce": "nonce(下で説明します)",
"exp": "トークンの有効期限(UNIXタイムスタンプ形式)",
"iat": "トークンの発行日時(UNIXタイムスタンプ形式)"
}

nonce フィールドがありますね! nonce があることで、どのような利点があるかを以下で説明します。

登場人物は以下です。

  • IdP: IDトークンを発行する
  • Relying Party: 認証のサービスを使う(アプリや、そのサーバー)
  • ユーザー: ログインしようとしている
  • 攻撃者: IDトークンを盗んで、不正にログインしようとしている

まず、 nonce がない場合です。

nonceがない場合

IDトークンが正しく、有効期限内であれば、誰でも何回でもログインできてしまうことがわかります。このような攻撃を、リプレイ攻撃と呼びます。

次に nonce がある場合です。

nonceがある場合

ユーザーのログイン後、保存されていた nonce はすでに削除されているので、攻撃者が盗んだIDトークンは使えなくなっています。 nonce が "Number used once" の略である、ということが納得できるかと思います。

最後に署名です。署名は、ヘッダーとペイロードから、IdPだけが持っている秘密鍵を使って計算、付与されます。IDトークンを受け取ったクライアントは、alg に書いてある方法に従って署名を検証します。ここで検証に失敗すれば、何者かによってヘッダーまたはペイロードが書き換えられているということになります。

サーバー側の実装

それでは、サーバー側の実装について考えていきましょう。結果的に、エンドポイントは2つになりました。

IDトークンの検証は nonce を含めサーバー側で行います。 LINEログインSDK のように nonce をアプリ側で検証をすることも可能ですが、結局それではIDトークンを何度も使えてしまい、サーバーから見ると nonce を含めていないのと同じ状態になってしまいます。そもそも、クライアントから送られてきたものをサーバーがそのまま信じることはよろしくありません。

そうすると、必然的に nonce の生成もサーバー側が行うことになります。 LINE と Apple の比較で、リクエストには独自の nonce を設定することができる、ということを確認したので、サーバー側から nonce を渡すエンドポイントを作れば良いですね。また、 nonce を生成してキャッシュするのにキーが必要なので、その認証認可セッションの ID もランダムに生成して返却します。これが1つ目のエンドポイントになります(わかりやすさのため、一部フィールドを省略しています)。

リクエスト

{
    app_id: "アプリの識別子",
    service_id: "IdPの識別子(line or apple)",
    type: "signup or signin"
}

レスポンス

{
    nonce: "nonce",
    session_id: "その認証認可セッションの識別子",
}

クライアント側はこの送られてきた nonce を使い、IdPからIDトークンを取得し、セッションIDとともにサーバーに送ります。これが2つ目のエンドポイントです。

リクエスト

{
    app_id: "アプリの識別子",
    session_id: "その認証認可セッションの識別子",
    id_token: "IDトークン",
    authorization_code: "認可コード(アクセストークンなどの取得に使う、Appleのみ)"
}

サーバー側はセッション ID からキャッシュしていた nonce を取り出し、IDトークンの検証時に一致を確認します。検証に成功後、サーバーのDBにユーザーを作成、または更新し、アプリのIDトークン、アクセストークン、リフレッシュトークンを発行して返却します。アプリのトークンの発行部分については Amazon Cognito のカスタム認証を使っているのですが、ここでは触れないことにします。

レスポンス

{
    access_token: "アプリのアクセストークン",
    id_token: "アプリのIDトークン",
    refresh_token: "アプリのリフレッシュトークン",
    expires_in: "トークンの有効期限"
}

nonce を使うことで、安全にIDトークンをやり取りしてネイティブ認証を実現できました!最終的な流れは以下の通りです。

ネイティブ認証のフロー

LINE の公式ドキュメントでも、同様の方法が推奨されています。図も付いていてわかりやすいので、合わせてご覧ください。

developers.line.biz

Appleの細かいポイント

Bundle ID と Service ID

Bundle ID はアプリ固有の識別子、 Service ID はWebアプリなどがサインインなどのWebサービスを使う際に使用する識別子です。

元々の Web アプリベースの認証方法では Service ID が使われていましたが、ネイティブ認証では Bundle ID が使われます。

この使い分けが必要になるのは、トークンの Exchange の処理、つまり、クライアントから送られた authorization_code を使ってアクセストークンやリフレッシュトークンを取得する処理です。

取得のためには専用のエンドポイントを叩きますが、リクエストのパラメータの一つに client_secret というのがあります。詳しい作成方法については以下を参照ください。

developer.apple.com

client_secret は署名した JWT で、 sub フィールドを持ちます。ここに入るものが、 Web アプリベースの認証なら Service ID 、ネイティブ認証なら Bundle ID となります(上記リンク先には App ID と書いてありますが、Bundle ID でも機能します)。

LINEの細かいポイント

API

SDKは自動で nonce や state を生成、検証してくれたりとなかなかリッチでしたが、APIも充実しています。特に、IDトークン検証用のエンドポイントが用意されています。

developers.line.biz

ローカルでIDトークンを検証するのは少しだけ大変なので、これは有難いです。今回はAppleとIDトークンの検証処理を共通化させたかったので採用しませんでしたが、LINEのみ実装する場合には良い選択肢かと思います。

IDトークンの検証アルゴリズム

LINEでは、ネイティブアプリと Web アプリで署名アルゴリズムが異なります。公式ドキュメントで以下のように記されています。

ネイティブアプリやLINE SDK、LIFFアプリに対してはES256(ECDSA using P-256 and SHA-256)が、ウェブログインに対してはHS256(HMAC using SHA-256)が返されます。

ES256 は公開鍵暗号方式、HS256 は共通鍵暗号方式で、ES256の方が鍵管理のリスクが低いです。私も実装後に知ったのですが、一般に MPA (Multiple Page Application) の Web アプリなどはAPIサーバーとIdPが同一管理下にあることが多く、クライアントで署名検証をすることがないため共通鍵暗号方式でも十分、ネイティブアプリや SPA (Single Page Application) の Web アプリは IdP と API サーバーが分離していることが多く、場合によってはクライアント側で検証をすることもあるため公開鍵暗号方式が推奨されている、という背景があるようです。

最後に

既に動いているサービスで安全な認証認可を実現するために、いろいろな記事、動画を参考にさせていただきました。受け売りですが、認証認可や課金などの実装は、いろいろな方の集合知の上に成り立つ類のものだと考えています。この記事がまた、これから認証認可を実装する方の一助となれば幸いです。

それでは少し早いですが、皆様、今年も1年お世話になりました。 来年もどうぞよろしくお願いいたします。

参考資料

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の検証など、サーバー側の実装についての記事が公開されます。ぜひそちらもご覧ください。

参考資料

私たちのLaravelプロジェクトにおけるGit hooks設定のご紹介

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

はじめに

こんにちは。リテールハブ開発部の清水です。

私たちは小売向けサービスをLaravelで開発しています。
このプロジェクトではGit hooksのpre-commit設定を使用してコミットのタイミングでLaravel Pint, Larastanを呼び出すことでコード品質を整えるための仕組みを使用しています。
この仕組みのベースは、プロジェクト初期に整備されたものを引き継いだもので、今回その内容を見直しながら整理しました。
ちょうど良い機会でしたので、本記事で私たちが使用している設定内容をご紹介いたします。

Git hooksとは?

Git hooksとは、git commit や git push などの Git 操作をきっかけに、自動でスクリプトを実行できる仕組みです。
コミット前にチェック処理を挟むなど、人の操作ミスを防ぐための自動処理を組み込む用途で使われます。
https://git-scm.com/docs/githooks

Laravel Pintとは?

Laravel Pint は、Laravel公式が提供している PHPコードの自動フォーマッターです。
決められたコーディング規約(Laravel / PSR-12 など)に従って、PHPコードの書き方を自動的に統一します。
https://laravel.com/docs/12.x/pint

Larastanとは?

Larastan は、PHP の静的解析ツール PHPStan を Laravel 向けに拡張したツールです。
コードを実行せずに解析し、存在しないプロパティや型の不整合などの問題を事前に検出します。
https://github.com/larastan/larastan

コミットを行った時の処理の流れ

  1. git commitを実行すると、Git hooksのpre-commitフックが起動
  2. コミット対象のPHPファイルを取得
  3. Laravel Pintによるフォーマットチェック
  4. Larastanによる静的解析
  5. 3 or 4のチェックでエラーが検出された場合、コミットを中断
  6. 全てのチェックをパスした場合のみ、コミットが完了

pre-commit設定内容

#!/bin/sh
set -eu

# --- 1) コミット対象(ステージ済み)のPHPファイルだけ拾う ---
php_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.php$' || true)

# PHPファイルがなければ何もしない
if [ -z "$php_files" ]; then
  exit 0
fi

echo "コミット対象のPHPファイルをチェックしています..."

# --- 2) Pint:フォーマットチェック(修正はしない) ---
echo "Pintでフォーマットをチェックしています..."
if ! echo "$php_files" | xargs ./vendor/bin/pint --test; then
  echo ""
  echo "❌ フォーマットエラーがあります。"
  echo "   以下のコマンドで修正してください:"
  echo "   make lint"
  exit 1
fi
echo "✓ フォーマットチェック OK"

# --- 3) Larastan:静的解析 ---
if echo "$php_files" | grep -qE '^app/'; then
  echo "Larastanで静的解析を実行しています..."
  if ! ./vendor/bin/phpstan analyse --no-progress --memory-limit=1G; then
    echo ""
    echo "❌ 静的解析エラーがあります。"
    echo "   エラーを修正してから再度コミットしてください。"
    exit 1
  fi
  echo "✓ 静的解析 OK"
fi

echo ""
echo "✓ 全てのチェックが完了しました。"
exit 0

Makeコマンドの紹介

コミットのタイミングだけではなく、手動でLaravel Pint, Larastanを実行したい場面もあります。
以下のMakeコマンドで実行できるようにしています。

# コードスタイルチェック&修正
lint:
    @files=$$(git diff --cached --name-only --diff-filter=ACM | grep "\.php$$" | sed "s|^$$(basename $$(pwd))/||"); \
    if [ -n "$$files" ]; then \
        docker compose -f $(COMPOSE_FILE) exec -T $(CONTAINER_PHP) sh -c "./vendor/bin/pint $$files"; \
    else \
        echo "ステージされたPHPファイルがありません"; \
    fi

# コードスタイルチェック
lint-check:
    @files=$$(git diff --cached --name-only --diff-filter=ACM | grep "\.php$$" | sed "s|^$$(basename $$(pwd))/||"); \
    if [ -n "$$files" ]; then \
        docker compose -f $(COMPOSE_FILE) exec -T $(CONTAINER_PHP) sh -c "./vendor/bin/pint --test $$files"; \
    else \
        echo "ステージされたPHPファイルがありません"; \
    fi

# 静的解析
larastan:
    docker compose -f $(COMPOSE_FILE) exec $(CONTAINER_PHP) ./vendor/bin/phpstan analyse --memory-limit=1G

実際にコミットする流れ

Laravel Pint, Larastanで弾かれる内容のコードを作成

<?php

namespace App\Http\Controllers;

// importが名前順になっていない
use Retailapp\Common\Models\User;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;

class TestController extends Controller
{
    public function index(): JsonResponse
    {
        $user = User::find(1);
        $name = $user->undefined_property; // 存在しないプロパティを呼び出している

        return response()->json(['name' => $name]);
    }
}

コミットを試みると、フォーマットエラーで弾かれる

% git commit -m '弾かれてほしいコミット'
コミット対象のPHPファイルをチェックしています...

Pintでフォーマットをチェックしています...

  ⨯

  ──────────────────────────────────────────────────────────────────── Laravel  
    FAIL   ............................................. 1 file, 1 style issue  
  ⨯ app/Http/Controllers/TestController.php no_unused_imports, ordered_import…  

❌ フォーマットエラーがあります。
   以下のコマンドで修正してください:
   make lint

pintで自動的に修正

% make lint

  ✓

  ──────────────────────────────────────────────────────────────────── Laravel  
    FIXED   ...................................... 1 file, 1 style issue fixed  
  ✓ app/Http/Controllers/TestController.php no_unused_imports, ordered_import… 

もう一度コミットすると、今度はLarastanで弾かれる

% git commit -m '弾かれてほしいコミット'
コミット対象のPHPファイルをチェックしています...

Pintでフォーマットをチェックしています...

  ──────────────────────────────────────────────────────────────────── Laravel  
    PASS   ............................................................ 1 file  

✓ フォーマットチェック OK
Larastanで静的解析を実行しています...

 ------ -------------------------------------------------------------------------- 
  Line   Http/Controllers/TestController.php                                       
 ------ -------------------------------------------------------------------------- 
  :14    Access to an undefined property                                           
         Retailapp\Common\Models\User::$undefined_property.                        
         💡 Learn more:                                                            
            https://phpstan.org/blog/solving-phpstan-access-to-undefined-property  
 ------ -------------------------------------------------------------------------- 

 [ERROR] Found 1 error                                                          
                                                                                
❌ 静的解析エラーがあります。
   エラーを修正してから再度コミットしてください。

Larastanに違反する部分を修正

<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Retailapp\Common\Models\User;

class TestController extends Controller
{
    public function index(): JsonResponse
    {
        $user = User::find(1);
        $name = $user->name; // 修正

        return response()->json(['name' => $name]);
    }
}

コミット成功

% git commit -m '通ってほしいコミット'
コミット対象のPHPファイルをチェックしています...

Pintでフォーマットをチェックしています...

  .

  ──────────────────────────────────────────────────────────────────── Laravel  
    PASS   ............................................................ 1 file  

✓ フォーマットチェック OK
Larastanで静的解析を実行しています..
                                                                                
 [OK] No errors                                                                 
                                                                                
✓ 静的解析 OK

✓ 全てのチェックが完了しました。

おわりに

本記事では、私たちの Laravel プロジェクトで使用している Git hookのpre-commit 設定と、その中で Laravel Pint・Larastan をどのように組み込んでいるかをご紹介しました。
同様の仕組みを検討されている方の参考になれば幸いです。

最後までお読みいただきましてありがとうございました。

Go 1.26で変わるgo fix

記事サムネイル
Go 1.26で変わるgo fix

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

はじめに

開発本部でデリッシュキッチンアプリ課金ユーザー向けの開発を担当しているhondです!
先日2026年2月にリリース予定のgo1.26のRelease Candidate 1であるgo1.26rc1がリリースされました。もうrc1は確認できたでしょうか?確認がまだの方はこちらから確認できるのでぜひ!
今回はGo 1.26でgo fixが大幅に変更されるとのことだったのでそちらについて説明しようと思います。

go fixとは

go fixはGo 1リリースにて破壊的変更を含む古いAPIを特定し、新しいものに修正するツールとして追加されました。 go fixによって大まかな修正はできるため、私たちはコアな部分の修正に集中できるようになっていました。
Go 1リリースの際はgo fixでの修正対象は多くGo 1のRelease NotesUpdating: Running go fixと調べるだけでも14件ヒットし、ソースコードを確認すると37件ものルールがあることが確認できます。その後もgo fixのルールは追加削除が繰り返されていきましたが、Goがメジャーバージョン内での後方互換性を担保しているので最近はなかなか変更が行われず使わないコマンドとなっていました。

Go 1.26での変更点

github.com

Go 1.26ではcmd/go: fix: apply fixes from modernizers, inline, and other analyzersにてgo fixが大幅に変更される旨のProposalがAcceptされました。

この変更の背景は、元のissueであるcmd/fix: remove all functionalityとGopherCon 2025でのAlan Donovanの発表(Analysis and Transformation Tools for Go Codebase Modernization)から確認できます。具体的には以下の3点が挙げられています。

  • 「go fixとは」でも触れた既存go fixの機能であるcontextbuildtagの修正が不要になったこと
  • コードレビュー時間の短縮や開発者の教育のためにモダン化が必要だったこと
  • AI/LLMが生成するコードの品質改善のためにモダン化が必要だったこと

これらの背景を踏まえ、modernize//go:fix inlineのようなGoのコードのモダン化を目的とするアナライザーをgo vet同様にgo fixでも利用可能にすることがこのProposalの目的となっています。
go vetgo fixの使い方は似ていますが、あくまでgo vetはアナライザーを用いて静的解析した診断結果を報告するツール、go fixは検出した問題をアナライザーが提案する修正案に修正するツールという使い分けになるようです。

以降では、Go 1.26のgo fixで利用可能になるmodernizeinlineについて詳しく説明します。

modernizeとは

pkg.go.dev

modernizegolang.org/x/tools/go/analysis/passes/modernizeで公開されているアナライザーのスイートです。Goのコードを最新の構文や標準ライブラリの機能を利用して、より簡潔でモダンな形式に修正することを目的としています。

現在24個のアナライザーが登録されており、その中から代表的なものを紹介します。Go 1.26で追加されるerrors.AsTypeのアナライザーも既に登録されています!

Analyzer 説明
any interface{}anyに修正する
errorsastype errors.AsをGo 1.26で追加されたerrors.AsTypeに修正する

使い方

以下のコマンドでmodernizeを実行することができます。

$ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./...

また、特定のアナライザーを有効・無効化することも可能です。

# anyアナライザーを有効化
$ modernize -any=true -fix ./...

# anyアナライザーを無効化
$ modernize -any=false -fix ./...

基本的には修正を適用しても動作が変わらないよう設計されていますが、bloopのように完全に動作を保証できないアナライザーもあり、それらはデフォルトでは無効化されています。

実行例

以下では実際にanyアナライザーを使ってinterface{}anyに修正してみます。

修正前のコード:

package main

import "fmt"

type OldInterface interface{}

func main() {
    old := OldInterface(1)
    fmt.Println(old)
}

コマンド実行:

$ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./...

出力(差分):

 import "fmt"
 
-type OldInterface interface{}
+type OldInterface any
 
 func main() {
        old := OldInterface(1)

このように、interface{}anyに自動で修正されたことが確認できます。

inlineとは

pkg.go.dev

inlinegolang.org/x/tools/go/analysis/passes/inlineで公開されているパッケージで、inlinegofixdirectiveの2つのアナライザーが含まれています。

inlineアナライザーは//go:fix inlineコメントディレクティブに基づいて修正を行います。主に非推奨になった関数や定数を新しいものに置き換えるために使用され、ioutil packageなどで利用されています。

具体的な使い方は以下の通りです。

  • 関数の場合:修正したい関数をラップした関数を作成し、その関数にコメントディレクティブを加えることで修正が可能です。
//go:fix inline
func Square(x int) int { return Pow(x, 2) }
  • 定数の場合:修正したい変数を左辺に、修正後の変数を右辺に記述し、コメントディレクティブを加えることで修正が可能です。
//go:fix inline
const Ptr = Pointer

使い方

以下のコマンドでinlineを実行することができます。

$ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./...

また、goplsを用いているIDE上では対象をホバーすることでも修正が可能です。

実行例

以下では、OldFunctionNewFunctionを定義し、OldFunctionNewFunctionに修正する際の例を示します。

修正前のコード:

package main

import "fmt"

func main() {
    OldFunction()
    NewFunction()
}

//go:fix inline
func OldFunction() {
    NewFunction()
}

func NewFunction() {
    fmt.Println("NewFunction")
}

コマンド実行:

$ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./...

出力(差分):

 import "fmt"
 
 func main() {
-       OldFunction()
+       NewFunction()
        NewFunction()
 }

このように、OldFunctionNewFunctionに修正されるのが確認できます。

IDEでの修正

コマンドを使わずに、IDE上で対象をホバーすることでも修正が可能です。

IDEでホバーした時の画像

Quick Fixを選択すると、修正候補が表示されクリックすることで修正が可能です。

Quick Fixを押した時の表示

新しいgo fixでできること

ここまで説明したmodernizeinlineは、Go 1.26からはgo fixを実行するだけで適用できるようになります。今までは破壊的変更を修正するためのツールだったgo fixが、Goのコードをモダン化していくためのツールへとシフトしました。

実際にgo1.26rc1を使ってgo fixを実行してみます。

$ go1.26rc1 fix ./...

modernizeの結果:

 import "fmt"
 
-type OldInterface interface{}
+type OldInterface any
 
 func main() {
        old := OldInterface(1)

inlineの結果:

 import "fmt"
 
 func main() {
-       OldFunction()
+       NewFunction()
        NewFunction()
 }

このように、go fixを実行するだけでコードをモダン化された状態に持っていくことができます。コード品質の担保がより容易になるため、pre-commitフックやCI、AI/LLMの出力後に適用するルールとして定義するのが良さそうです!

まとめ

私自身go fixを使う機会がなかったので、今回のアップデートで既存のgo fixとモダン化の為のツールとして進化したgo fixを会社のコードに適用するいい機会になりました。既存のgo fixでの修正点は一部のcontextのみでしたが、新しいgo fixを実行したところ1249の修正点が確認されました。10年もののサービスという事もありこれらのコードを全て手動で特定し、修正していくとなると莫大な労力を要するので公式からこのようなツールが出ていることはとても有り難いなと思いました。
上記で修正されたのはmodernizeに定義されたアナライザーやそれぞれのpackageにて//go:fix inlineが定義されているものですが、//go:fix inlineに関しては普段の開発でも非推奨化したがバージョン互換性担保の観点で削除できないコードを管理する際にとても有用なので積極的に利用して行きたいです。
AIはどうしても既存のコードベースに品質が左右されてしまうので新しくなったgo fixを用いて、継続的な品質改善が重要だと感じました。また、公開されたコードベースを元にAIは学習していくので今後の自分を含めたGopherがAIを通してより良いコードを書くためにも積極的にモダン化を行おうと思います。
ここまで読んでいただきありがとうございます。Go 1.26でgo fixがどう変わるか迷っている方の助けになれたら幸いです!