🤩

SwiftUIのASWebAuthenticationSessionを使ってインスタグラムのアカウント認証をする😘

2024/11/05に公開
2

はじめに

この記事はSwiftUIのASWebAuthenticationSessionを使用し
インスタグラムのアカウントを認証して認証したアカウントの情報(username,profileimage)
などを取得する記事でございます

まずASWebAuthenticationSessionとインスタグラムAPIの相性がクソゴミだったのと
Meta公式のインスタグラムのAPIを使えるようにするための設定のやり方が
間違えすぎ、わからなさすぎという素晴らしい状態

またASWebAuthenticationSessionのcallbackschemがカスタムURLを使用できない
場合はどうすればええんやってことです。

ASWebAuthenticationSessionについてもなかなか
いい記事が見当たらなかったのでそこについての詳しい説明も兼ねて

SwiftUIでこのようなことをやっている人がいないので情報がなく
また、ASWebAuthenticationSessionを応用を効かさないと認証しないという

苦痛、絶望、地獄を乗り越えた私が
これ以上みんなを苦しめないために記事を書きました

本当に頑張ったんでマジで褒めてください

それでは、作っていきましょう

今回使用するもの&どうやって実装するか

今回使用するもの

今回使うものはこの三つだわさ

SwiftUI

Python (Django(認証を受け取るためのサーバー))

Instagram API

簡単な流れを言うと

Swiftで認証するための情報を送信 → Instagramで認証 → Djangoでトークンを受け取ってモバイルに渡す → Swiftで受け取ったトークンを元に情報をいただく

の順番で処理します。

「なんでDjangoいるん?別にSwiftUIだけでええやん」

って思った人もいると思います。

普通はそう思いますよね??僕もそう思ってました、理由は次で解説します。

そしてPythonはDjangoを使って認証ができた時にモバイル側に渡す処理を行なって
モバイルにトークンを渡します

InstagramAPIは、アカウント情報を取得するのに必要です。

どうやって実装するか

はい。まずはどうやって実装するかなんですけど

まずはSwiftUIのASWebAuthenticationSessionを使って認証を行います。
このやつです ↓↓↓

https://developer.apple.com/jp/documentation/authenticationservices/authenticating_a_user_through_a_web_service/

まあ簡単に言ったらWebを使って認証をするにはこのAPIを使いましょうって話
詳しいことはこの公式を読んでクレメンス

そしてこれが厄介なんですと
このAPIの引数はどこに行くかの行き道と帰ってくる道が必要なんです。

帰ってくる道を示すにはカスタムスキームと言って、アプリのURLを設定しなくちゃいけません。
普通はこのカスタムスキームを設定すればいいんですけど今回はそうはいきません。

InstagramのAPIでリダイレクトURL(帰り道)を設定する時に
カスタムスキームがセキュリティーなどの都合で設定できないのです

つまり行き道があっても帰り道がないのでトークンをもらっても持って帰れません。

じゃあこの場合どうすりゃいいんだよってことで
私はAppleDeveloperやMetaDeveloperに質問しました

3日経った今でも誰からも返信がありません。

まあMetaは、質問が帰ってこないだろうと思っていたので期待はしませんでしたが
いつも返答が早いAppleまで3日間誰からも返信が来ないのでびっくりしました

多分嫌われたんですかね。

そこで私は苦肉の策でバックエンドを作成し
そこでトークンを受けとりモバイルで渡す処理を
行うことにしました

これしか策がなかったので、もっといいやり方もあるかもしれないが...

今までの話を説明すると
こんな感じでモバイル側から認証したいからトークンを取ってきてと言われて
認証ができたらモバイルに戻るってのが普通であり理想です。

ですがしかし今回は
普通に認証をもらいに行くと

こんな感じで本来なら使える帰り道がなくなります。
普通ではありえないでしょ

だからPythonのバックエンドを用いて、そこからモバイルに戻らないとダメだという
究極にめんどくさいことをやらなきゃダメなのです。

めっちゃ簡単に説明したけど、大体はわかった??
詳しくは自分で調べてくれ
俺はもう疲れた

ってことで実際に作ってみるよ

実装するぞ

Meta Developerの登録

InstagramのAPIを使用するにはMetaのDeveloperアカウントが必要になります。
Developerアカウントと入っても、FacebookのアカウントがあればそれでOK

ここから↓↓↓

https://developers.facebook.com/?locale=ja_JP

ここのDeveloperアカウントを作成する時に気を付けて欲しいのが
たまに謎の認証で作れない時、ログインできない時があります。

これ↓↓↓

これになったらもうアカウントを削除して作り直すか、諦めてください。

調べたところ
この警告は治るか治らないかは不明らしいです
終わってます

僕も一回この警告が出て一度アカウントを削除して作り直しました
その割にはサブ垢は作ったらすぐ弾かれたからびっくりしました
その労力があればさっさと認証しろよハゲって思いました

アカウントを作成できたら次です

InstagramAPIの設定

今からInstagramAPIを設定していきます。
一応、全く使えない公式ドキュメントです。

これが噂の間違えすぎ、わからなさすぎのやつです

https://developers.facebook.com/docs/instagram-basic-display-api/getting-started?locale=ja_JP

こんなひどいスタートガイドは見たことがない
唖然としました

そして今から設定をしていきますがもし僕の設定が分からなければ
この記事の見て設定してください。
この記事は限られた情報の中で最大級の価値のある記事ですので

https://qiita.com/john-rocky/items/ea6fc83f5b83ccee1692

では、設定していくにょ

Developerに登録した次は

一応、公式ドキュメントを見ながら
この記事を見てながら下記の公式のスタートガイドがおかしいかみよう

https://developers.facebook.com/docs/instagram-basic-display-api/getting-started

一番最初にアプリを作成からユースケースの一番下
その他を選択しましょう

次にアプリタイプを生活者を選択
アプリ名は適当にわかりやすいものをつけましょう。

これでアプリの新規作成は終了です

ちなみにアプリタイプを登録するときに公式ドキュメントでは

[マイアプリ]にクリックしてアプリを新規作成し、[消費者] または**[なし]**アプリタイプを選択します」

ってありますが、そんな選択肢はありません。

当時構築途中の私が書いたドキュメントには

「ここまでの道のりでもうブチギレ
アカウントの承認に関しても、この構築に関しても
調べても出てこない。公式ドキュメントにも載ってないし間違っている
問い合わせても返事がない」

と書いている。
めっちゃブチギレながら書いてたんだろう

次にアプリに製品を追加ではInstagram Basic Displayを選択する

選択したら下にあるアプリを作成するをクリックし
そこでも適当な名前を入れる

そしてInstagramアプリIDとInstagram App Secretをどこかにコピーをしておきましょう
有効なOAuthリダイレクトURIはPythonでサーバーを作った時に必要です

次にはアカウント認証で使うテストアカウントを作っていきましょう

アプリの役割から
メンバーを追加するを押して

インスタグラムテスターを選択しましょう
そして実際に使うInstagramのアカウントIDを記入し招待しましょう

招待が終われば
Web上でインスタグラムを開いてテスターを承認しましょう

設定からウェブサイトのアクセス許可のアプリとウェブサイトからテスターの招待で
許可を出しましょう!

次はSwifttUIで認証を送る処置を書いていきましょう。

SwiftUIの実装

import SwiftUI
import AuthenticationServices

struct InstagramLoginView: View {
    // ユーザー名を表示するための@Stateプロパティ
    @State private var username: String = ""
    
    var body: some View {
        VStack {
            // Instagramのユーザー名を表示
            if username.isEmpty {
                Text("Login with Instagram") // 認証前のタイトル
            } else {
                Text("Instagram Username: \(username)") // 認証後にユーザー名を表示
            }
            
            Button(action: {
                // ボタンが押された時にInstagramのログイン処理を開始
                InstagramLoginHelper { fetchedUsername in
                    self.username = fetchedUsername // ユーザー名を更新
                }.startInstagramLogin()
            }) {
                Text(username.isEmpty ? "Login with Instagram" : "Logged in as \(username)")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .padding()
    }
}

class InstagramLoginHelper: NSObject, ASWebAuthenticationPresentationContextProviding {
    var completion: ((String) -> Void)?
    
    init(completion: @escaping (String) -> Void) {
        self.completion = completion
    }
    
    func startInstagramLogin() {
        let clientID = "XXXXXXXXXXX"  // InstagramのクライアントID
        let redirectURI = "https://example.onrender.com/accounts/auth/"
        let authURL = "https://api.instagram.com/oauth/authorize?client_id=\(clientID)&redirect_uri=\(redirectURI)&scope=user_profile,user_media&response_type=code"
        let scheme = "example"  // アプリのカスタムスキーム
        
        if let url = URL(string: authURL) {
            let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { callbackURL, error in
                if let error = error {
                    print("Error during authentication: \(error.localizedDescription)")
                    return
                }
                
                if let callbackURL = callbackURL {
                    if let accessToken = URLComponents(string: callbackURL.absoluteString)?.queryItems?.first(where: { $0.name == "access_token" })?.value {
                        DispatchQueue.main.async {
                            self.useInstagramAccessToken(accessToken)
                        }
                    } else {
                        print("アクセストークンが含まれていません")
                    }
                } else {
                    print("callbackURLがnilです")
                }
            }
            
            session.presentationContextProvider = self
            session.start()
        }
    }
    
    // Instagram APIでアクセストークンを使用してユーザー情報を取得
    func useInstagramAccessToken(_ accessToken: String) {
        let userInfoURL = "https://graph.instagram.com/me?fields=id,username&access_token=\(accessToken)"
        
        guard let url = URL(string: userInfoURL) else {
            print("Invalid URL")
            return
        }
        
        let task = URLSession.shared.dataTask(with: url) { data, response, error in
            if let error = error {
                print("Error fetching Instagram user info: \(error.localizedDescription)")
                return
            }
            
            guard let data = data else {
                print("No data returned")
                return
            }
            
            do {
                if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
                   let username = json["username"] as? String {
                    print("Instagram Username: \(username)")
                    DispatchQueue.main.async {
                        // 取得したユーザー名をViewに反映
                        self.completion?(username)
                    }
                }
            } catch {
                print("Error parsing JSON: \(error.localizedDescription)")
            }
        }
        
        task.resume()
    }
    
    // ASWebAuthenticationPresentationContextProvidingの実装
    func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
        return UIApplication.shared.windows.first { $0.isKeyWindow } ?? ASPresentationAnchor()
    }
}

#Preview {
    InstagramLoginView()
}

ここではInfo.plistにカスタムスキームを設定する必要があります
以下のように設定してください

<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
     <string>example</string> // カスタムスキーム
</array>

クライアントIDはDeveloperのInstagramアプリIDです
Instagram Basic Displayのところに書いてあります

解説

InstagramLoginView構造体

@Stateで定義されるusernameプロパティは、Instagramから取得したユーザー名を保持するために使用します

VStack内では、Instagramにログインする前と後で表示が変わるTextビューが配置されています

if username.isEmptyのところでは
usernameが空の状態なら「Login with Instagram」と表示し、ユーザーがログインしていないことを示します
そうでなければ、Instagramで取得したユーザー名が表示されます

Buttonのところは
ボタンを押すと、InstagramLoginHelperインスタンスが生成され、startInstagramLogin()メソッドが呼ばれます

ボタンのテキストは、usernameが空なら「Login with Instagram」、それ以外の場合はログイン済みであることを示すテキスト(Logged in as (username))が表示されます

InstagramLoginHelperクラス

InstagramLoginHelperは、ASWebAuthenticationSessionを使ってInstagram認証プロセスを管理するためのクラスです。

このクラスはASWebAuthenticationPresentationContextProvidingプロトコルを準拠しています

var completion: ((String) -> Void)?は
認証プロセスが完了した後に、取得したユーザー名をInstagramLoginViewに渡すためのクロージャです

startInstagramLogin()メソッドでInstagramのOAuth認証を開始します

clientIDとredirectURIは
clientIDはInstagram API用に事前に取得したクライアントIDです
redirectURIは、認証が完了した後にリダイレクトされるURLです

このURLは、Instagramの認証ページにユーザーを誘導するために使います。

schemeではカスタムスキームを設定します
アプリがインストールされている場合、このスキームでリダイレクトが行われます。

ASWebAuthenticationSessionを使って、ユーザーをInstagramのログイン画面にリダイレクトします。

認証完了後、InstagramからリダイレクトされたURLが渡されます

callbackURLにアクセスできた場合、access_tokenを取得し、useInstagramAccessToken()メソッドを呼び出してユーザー情報を取得します。

accessTokenを使って、Instagram Graph APIからユーザー情報(ここではユーザー名)を取得します

ユーザー情報のリクエストURLを生成します。

URLSessionを使って非同期リクエストを送り、取得したデータをJSONSerializationでデコードしてユーザー名を取得します。

ASWebAuthenticationPresentationContextProvidingプロトコルのメソッドで、認証セッションのために使用するウィンドウを提供します。
UIApplication.shared.windowsを使い、キーウィンドウを返します。

これを実装して次は中間地点が必要です

次はサーバーサイドの部分を作っていきます。

Pythonの実装

多分モバイルエンジニアの人できないと思うので
このリポジトリをクローンして
アプリのIDやシークレットIDなどを書き換えてください
Renderでも何でもいいのでサーバーにあげてください

https://github.com/Iccyan21/django_instagram

一応コードです

from django.http import HttpResponse
import requests

# 認証が終わればInstagramからリダイレクトされるURL
def instagram_callback(request):
    # Instagramから送られてきた認証コードを取得
    code = request.GET.get('code')
    # コードがない場合はエラーを返す
    if not code:
        return HttpResponse("Error: Missing authorization code", status=400)
    
    # Instagram APIへアクセストークンをリクエスト
    token_url = "https://api.instagram.com/oauth/access_token"
    data = {
        # 'client_id': 'XXXXXXXXXXXXXXXXX',  # InstagramのクライアントID
        'client_id': 'XXXXXXXXXXXXXXXXX',
        # Instagramのクライアントシークレット
        'client_secret': 'XXXXXXXXXXXXXXXXX',
        'grant_type': 'authorization_code', #クライアントがどのようにしてアクセストークンを取得するかを指定するためのパラメータ
        'redirect_uri': 'https://example.onrender.com/accounts/auth/',  # このバックエンドのURL
        'code': code  # 取得した認証コード
    }
    
    # POSTリクエストでアクセストークンを取得
    response = requests.post(token_url, data=data)
    
    # アクセストークンが取得できた場合は、JavaScriptを使ってカスタムスキームのURLにリダイレクト
    if response.status_code == 200:
        token_data = response.json()
        access_token = token_data.get('access_token')
        
        # JavaScriptを使ってカスタムスキームのURLにリダイレクト
        redirect_url = f"example://auth?access_token={access_token}"
        return HttpResponse(f"""
            <html>
                <head>
                    <script type="text/javascript">
                        window.location.href = "{redirect_url}";
                    </script>
                </head>
                <body>
                    Redirecting...
                </body>
            </html>
        """)
    else:
        # アクセストークンが取得できなかった場合はエラーを返す
        return HttpResponse("Error: Failed to retrieve access token", status=400)

これはDjangoの環境で動かしているので
Djangoの環境を作って実際に動かしてみてください!

解説はコード上に結構書いていますので省略します!

注意点

実際に動かすには、ちゃんとしたサーバーにデプロイしないと、動かないので注意

僕的にはRenderにあげて試してみるのがおすすめです
無料なので

https://render.com/

そしてサーバーにあげたらそのURLを
Instagram Basic API有効なOAuthリダイレクトURIに設定してください!

これで完了です!

終わりに

これでインスタグラムのアカウント認証は終了です
疲れましたね

誰に聞いてもわからない最悪の状況から作りきったことが誇りに思えます

それぐらいしんどくて、難しかったです

この記事でもし誰かを救えたら幸いだと感じます

以上でこの記事は終了です

お疲れ様でした!

Twitterもよければフォローお願いします🥺

https://twitter.com/GIANT_KILLING_0

Discussion

hayatahayata

一点、ご質問があるのですがinstagramのアカウントタイプによって認証の処理は分けたりしておりますでしょうか?

いっちゃんいっちゃん

コメントありがとうございます!
その点につきましては
一般のアカウントだと記事で扱ったようなInstagram Basic APIを使うべきだと思います

プロ(ビジネス・クリエイター)アカウントだと、Instagramログインを使ったAPI(https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login)
こちらを使うべきかと思います

ちなみにInstagram Basic APIはMetaの改悪により12月4日に廃止されるので
これからは一般のアカウント情報は取得できなくなるらしいです😢
https://developers.facebook.com/blog/post/2024/09/04/update-on-instagram-basic-display-api/

現状では他の代替のAPIもないので
これからは一般のアカウント情報は取得できないかもしれないです

詳しくは公式ドキュメント(https://developers.facebook.com/docs/instagram-platform?locale=ja_JP)
の方に詳しく載っています!