💳
iOSアプリでGitHubAPIのOAuth認証を行う
概要
- GitHubAPIの認証方法にはいくつか種類があります
-
GitHub Apps
がドキュメントでおすすめされているのでiOSアプリを作って試してみたのですが、権限不足でできないことがありました。 - 例えばスターをつけようとすると権限が不足してエラーになります。
- scopeの設定など色々試したのですがけっ結局うまくいかず(、恐らく組織/個人以外のリポジトリに対する用途ではないのかな、と解釈しています。
- そういう理由で今回は
OAuth
での認証を行います
コード全体
コード全体
import SwiftUI
struct LoginView: View {
let callbackURL = URL(string: "apollodemo://callback")!
@State private var sessionCode: String?
@State private var accessToken: String?
var body: some View {
Form {
LabeledContent("Code", value: sessionCode ?? "(nil)")
LabeledContent("AccessToken", value: accessToken ?? "(nil)")
Button("Log in") {
let url = URL(string: "https://github.com/login/oauth/authorize?client_id=\(GitHubAPICredentials.clientID)&redirect_uri=\(callbackURL.absoluteString)")!
UIApplication.shared.open(url)
}
.frame(maxWidth: .infinity, alignment: .center)
Button("Fetch AccessToken") {
Task {
do {
guard let sessionCode else {
return
}
self.accessToken = try await fetchAccessToken(sessionCode: sessionCode)
} catch {
print(error.localizedDescription)
}
}
}
.disabled(sessionCode == nil)
.frame(maxWidth: .infinity, alignment: .center)
}
.onOpenURL { url in
guard
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems,
let sessionCode = queryItems.first(where: { $0.name == "code" })?.value
else {
fatalError("コールバックURLの値が不正です")
}
self.sessionCode = sessionCode
}
}
private func fetchAccessToken(sessionCode: String) async throws -> String {
// リクエストの作成
let url = URL(string: "https://github.com/login/oauth/access_token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
// ヘッダの設定
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
// bodyの設定
let bodyDict = [
"client_id": GitHubAPICredentials.clientID,
"client_secret": GitHubAPICredentials.clientSecret,
"code": sessionCode
]
let body = try JSONSerialization.data(withJSONObject: bodyDict, options: [])
request.httpBody = body
// リクエストの送信
let (data, _) = try await URLSession.shared.data(for: request)
// レスポンスのデコード
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
guard
let jsonDict = jsonObject as? [String: Any],
let accessToken = jsonDict["access_token"] as? String
else {
fatalError("レスポンスのデコードに失敗")
}
return accessToken
}
}
#Preview {
LoginView()
}
OAuth Appの作成
- 下記のページよりOAuth Appを作成します
- 名前とコールバックURLを設定します
- コールバックURLはこの後アプリ側でも同じ値をセットします
apollodemo://callback
- また新しく
Client secret
を作成し、Client ID
とClient secrets
をメモしておいて、後ほどアプリ側で利用します
-
OAuth App
の作成は以上です
コールバックURLの設定
- 新規にiOSのプロジェクトを作成し、
UTL Types > URL Scheme
に先程のコールバックのSchemeを設定します - これでブラウザで認証を行った際、コールバックURLへのリダイレクトが起こり、アプリが立ち上がってコールバックURLを受け取ることができます
認証ページの作成
- 先ほどの認証情報を含んだenumのファイルを作成します
- e.g.
GitHubAPICredentials.swift
- e.g.
- セキュリティ的にこのファイルをGit管理するのは危ないので、
.gitignore
に追加して無視させるといいです
enum GitHubAPICredentials {
static let clientID = "YOUR_CLIENT_ID"
static let clientSecret = "YOUR_CLIENT_SECRET"
}
- では公式ドキュメントの通り、認証用のURLを作成します
- またコールバックURLはonOpenURL(perform:)で受け取れます
struct LoginView: View {
let callbackURL = URL(string: "apollodemo://callback")!
var body: some View {
Form {
Button("Log in") {
let url = URL(string: "https://github.com/login/oauth/authorize?client_id=\(GitHubAPICredentials.clientID)&redirect_uri=\(callbackURL.absoluteString)")!
UIApplication.shared.open(url)
}
.frame(maxWidth: .infinity, alignment: .center)
}
.onOpenURL { url in
print(url.absoluteString) // apollodemo://callback?code=8649aab669c51e066004
}
}
}
- ブラウザで認証画面を開き、認証完了後に
onOpenURL
が呼ばれていれば成功です
コールバックURLからアクセストークンの取得
- 続いてこのコールバックURLに含まれるクエリパラメータのcodeを使ってアクセストークンを取得します
apollodemo://callback?code=8649aab669c51e066004
- コールバックURLのcodeを
sessionCode
として抽出します
guard
let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems,
let sessionCode = queryItems.first(where: { $0.name == "code" })?.value
else {
fatalError("コールバックURLの値が不正です")
}
self.sessionCode = sessionCode
- この
sessionCode
を使ってアクセストークンが取得できます - 下記の通りリクエストを作成して、成功するとレスポンスからアクセストークンを取得できます
func fetchAccessToken(sessionCode: String) async throws -> String {
// リクエストの作成
let url = URL(string: "https://github.com/login/oauth/access_token")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
// ヘッダの設定
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
// bodyの設定
let bodyDict = [
"client_id": GitHubAPICredentials.clientID,
"client_secret": GitHubAPICredentials.clientSecret,
"code": sessionCode
]
let body = try JSONSerialization.data(withJSONObject: bodyDict, options: [])
request.httpBody = body
// リクエストの送信
let (data, _) = try await URLSession.shared.data(for: request)
// レスポンスのデコード
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
guard
let jsonDict = jsonObject as? [String: Any],
let accessToken = jsonDict["access_token"] as? String
else {
fatalError("レスポンスのデコードに失敗")
}
return accessToken
}
- 今回リクエストのヘッダで
Accept
でjson形式を指定しているので、下記の形でレスポンスのデータが返ってきています - これを
Dictionary
としてデコードして、アクセストークン値を取り出すことができます
{
"access_token":"gho_16C7e42F292c6912E7710c838347Ae178B4a",
"scope":"repo,gist",
"token_type":"bearer"
}
- ちなみにaccessTokenの期限は無期限とのことです
【保存版】GitHubアクセストークン全種別解説: Personal Access Token、OAuth、GitHub App、GITHUB_TOKENの違い #GitHubAPI - Qiita
- 以上でアクセストークンの取得までできたので、これを使って認証が必要なAPIを呼び出すことができるようになります
- ログアウトを考えるのであれば、下記を実行してローカルに保存しているアクセストークンの情報を消せば良いかと思います
Discussion