🔐

SwiftUI で独自 Backend API とユーザー認証する方法

2025/03/13に公開

🎯 目的

Swift / SwiftUI で実装された iPhone アプリと独自で実装された Backend API とでユーザー認証を行う方法を解説する

💡 前提

この記事では、SwiftUI で実装された iOS アプリと独自で実装された Backend API とアクセストークンを用いて通信する方法やトークンを取得するためのユーザー認証にまつわる方法を Swift / SwiftUI で実装された iOS アプリに焦点を当てた記事です。

⚡️ Backend API

Backend API ではすでに以下の認証エンドポイントが実装されているものとします。

POST /auth/token : トークンを発行する(ログイン)

リクエスト:

curl -X 'POST' \
  'http://localhost:8000/auth/token' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "email_address": "string",
  "password": "string"
}'

200レスポンス:

{
  "access_token": "アクセストークン",
  "refresh_token": "リフレッシュトークン",
  "token_type": "bearer",
  "expires_at": 123456789
}
PUT /auth/token : トークンを更新

リクエスト:

curl -X 'PUT' \
  'http://localhost:8000/auth/token' \
  -H 'accept: application/json'
  -H 'Authorization: bearer リフレッシュトークン'

200レスポンス:

{
  "access_token": "新しいアクセストークン",
  "refresh_token": "新しいリフレッシュトークン",
  "token_type": "bearer",
  "expires_at": 123456789
}
DELETE /auth/token : トークンを削除(ログアウト)

リクエスト:

curl -X 'PUT' \
  'http://localhost:8000/auth/token' \
  -H 'accept: application/json'
  -H 'Authorization: bearer アクセストークン'

200レスポンス:

# 空レスポンス
{}

🗺️ システムアーキテクチャ

システム全体のアーキテクチャは以下の通りです。

  • FastAPI から発行されたアクセストークンやリフレッシュトークンは Keychain で暗号化して保持します。(UserDefaults で保持しようか迷ったけど結局どっちがいいのか?🤔)
  • Backend 側も発行したアクセストークンやリフレッシュトークンを TTL 付きで Redis に保存し、認証に利用します。

実際の運用では、Cloud Run の前段に Load Balancing を配置したりしますが、わかりやすくするため省略しています。詳しいシステムアーキテクチャは以下の記事を参照してください。

https://zenn.dev/taiyou/articles/d07983a189a6cc

🛠️ アプリケーション設計

🔐 トークンのリフレッシュ

アクセストークン、リフレッシュトークンは以下のタイミングでリフレッシュします。

  • アプリ起動時/フォアグラウンドでアクセストークンの有効期限を確認し、期限が切れていたらリフレッシュする
  • バックグラウンドでアクセストークンの有効期限を確認し、期限が切れていたらリフレッシュする
  • アクセストークンを取得する時にアクセストークンの有効期限を確認し、期限が切れていたらリフレッシュする
actor AuthManager {
    private var refreshTask: Task<Token, Error>?
    
    func refreshToken() async throws -> Token {
        if let existingTask = refreshTask { return try await existingTask.value }
        
        let task = Task { 
            defer { refreshTask = nil }
            let newToken = try await performTokenRefresh()
            return newToken
        }
        
        refreshTask = task
        return try await task.value
    }
}

Discussion