『入る学科間違えた高専生』の日記

プログラミングのコードを書いたりする予定です。あとは日記等。あといつまで高専生やねん

SFSafariViewControllerでTwitterOAuthをしてアプリにコールバックさせる

はじめに

今、作ってるアプリとサーバサイドでSFSafariViewControllerでTwitterOAuthしてアプリ側にコールバックさせたいっていうことで実装しました。

環境

クライアント側

サーバサイド側

概要

PCからのアクセスを全て遮断するために独自のURLを作成してTwitterログイン画面へのリダイレクトをさせています。

サーバサイド側のURLの説明

BASE_URL = 'http://hostname:8000/'
URL 説明
api/user/login ログイン処理
accounts/login/twitter TwitterログインURLを生成してリダイレクト
accounts/complete/twitter Twitterログインコールバック後のURL

実装

クライアント側

URLスキーマの設定

今回は app-callback:// を指定するとアプリが開くようにしました。

ViewController.swift

import UIKit
import SafariServices

class ViewController: UIViewController {

    static let kCloseSafariViewController = NSNotification.Name(rawValue: "kCloseSafariViewControllerNotification")

    private var safari: SFSafariViewController!
    private var hasToken = false

    override func viewDidLoad() {
        super.viewDidLoad()
        NotificationCenter.default.addObserver(self, selector: #selector(safariLogin(_:)),
                                               name: ViewController.kCloseSafariViewController,
                                               object: nil)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        if hasToken {
            return
        }
        // ログイン画面の表示
        let url = URL(string: "http://hostname:8000/user/login")!
        safari = SFSafariViewController(url: url)
        present(safari, animated: true, completion: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self,
                                                  name: ViewController.kCloseSafariViewController,
                                                  object: nil)
    }

    @objc func safariLogin(_ notification: NSNotification) {
        guard let url = notification.object as? URL else {
            return
        }
        print(url.absoluteString)
        hasToken = true
        safari.dismiss(animated: true, completion: nil)
    }
}

AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    ・・・

    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
        if let sourceApplication = options[.sourceApplication] as? String {
            # SFSafariViewControllerからのものであるか
            if String(describing: sourceApplication) == "com.apple.SafariViewService" {
                NotificationCenter.default.post(name: ViewController.kCloseSafariViewController, object: url)
                return true
            }
        }
        return true
    }
}

サーバサイド側

詳しい実装は、今回は省略します。多分調べてもらったら沢山出てくると思う。

qiita.com

上記リンク、詳しくは見てないけど、書いてあったので貼っておきます。

views.py

from django.core.exceptions import DisallowedRedirect
from django.http.response import HttpResponseForbidden
from django.views.generic import TemplateView


class TwitterLoginView(TemplateView):

    template_name = 'login.html'

    def get(self, request, *args, **kwargs):
        # 初回アクセス
        if '_auth_user_id' not in request.session and request.user.is_anonymous:
            return super().get(request, *args, **kwargs)

        ・・・  # ログイン処理とか

        try:
            self.template_name = 'success.html'
            kwargs['hoge'] = 'nnsnodnb'  # サンプルパラメータだよ
            return super().get(request, *args, **kwargs)
        except DisallowedRedirect:
            # 一応戻しておく
            self.template_name = 'login.html'
            logout(request)
            return HttpResponseForbidden()

api/urls.py

startapp で作成した api の配下に urls.py を作成しています。

from django.urls import path
from .views import TwitterLoginView

app_name = 'api'

urlpatterns = [
    ・・・
    path('user/login', TwitterLoginView.as_view(), name='twitter_login_view'),
]

project/urls.py

from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.urls import include, path
from rest_framework_swagger.views import get_swagger_view

schema_view = get_swagger_view(title='Hogehoge API')

urlpatterns = [
    ・・・
    path('accounts/', include('social.apps.django_app.urls', namespace='social')),
    path('api/', include('api.urls', namespace='api')),
]

login.html

JavaScripthref を使用してログイン画面に遷移させます。
URLのパラメータに next を指定することで `accounts/complete/twitter が呼ばれた後のリダイレクト先を指定できます。
今回は自分自身にリダイレクトさせます。

<script type="text/javascript">
    location.href = "{% url 'social:begin' 'twitter' %}?next={% url 'api:twitter_login_view' %}";
</script>

success.html

なぜか django.shortcuts.redirectapp-callback:// を指定しても DisallowedRedirect がSFSafariViewControllerが呼ばれてしまったので敢えて作りました。

context を設定してればJavaScriptであろうと見られるのでクエリパラメータ追加しておきます。

<script type="text/javascript">
    location.href = "app-callback://?hoge={{ hoge }}";
</script>

動作サンプル

わかりづらいですが、Twitterログイン画面での青い ログイン ボタンしか押していないです。

最後に

意外と簡単に実装できたので、 Twitterアプリが入ってないときの TwitterKit 等でされている実装を再現できるようになりました。
今回は、Twitterアプリは最新のものが入っていますが、どうしてもブラウザ経由でないといけないという制約があったので、SFSafariViewControllerで実装しました。

もしやる気があったらいつか BitBucket にでもサンプルをアップロードしようと思います。(多分しない)

参考

strawberrycode.com