티스토리 뷰

728x90
반응형

Authorizing OAuth Apps  문서를 보고 따라한 기록-!!

 

저는 Github API를 이용해서 private repo에 접근하려고 (풀리퀘 등등 얻어오기 위함) 
Github OAuth Access Token이 필요해서 따라하게 되었습니다 :-) 

 

[ 순서 ]

1) OAuth application 등록하고 client_id 랑 client_secret 값을 알아낸다.

2) 사용자를 로그인 시켜서 임시 토큰이라고 할 수 있는 code 값을 받는다.

3) code값을 이용하여 access_token을 구한다. 

 

 

[1] OAuth application 등록하고 client-id & client-secret 값 얻기

 

register 에 들어가서

깃헙 로그인을 할 자신의 앱을 등록해주세요 

 

 

제 프로젝트 이름은 GithubPRViewer인데, Github으로 시작하는 거는 안된다고 해서 

PRViewer라는 이름으로 등록했어요. 

 

 

그리고 Authorization callback URL은 사용자가 로그인하고 Authorize버튼(초록색) 누르면 redirect해주는 URL입니다.

파란색 네모부분에 redirection하는 url을 보여줍니다. 

 

 

Xcode에서 URL Schemes를 githubprviewer로 등록해준뒤,

callback URL을 githubprviewer://login 으로 등록해줬습니다. 

 

 

 

정보를 다 입력하고 Register application 버튼을 눌러주면 이런 화면이 나오는데요, 

 

저기서 Client ID를 복붙해주세요.

 

 

[2] Code 얻기

 

그리고 이제 이 URL에 요청을 할 것입니다. 

파라미터로는 필수값인 client_id (1번에서 얻은 값)가 있고

 

redirect_uri, allow_signup, scope 등등 옵셔널 값들이 있습니다. 

 

1번에서 등록했던 callback url 말고 다른 url쓰고 싶으면

github.com/settings/developers  여기 들어가서 callback url 바꿔줄 수도 있지만,

redirect_uri 파라미터에 새로운 url값을 넣어보낼 수 있어요-! 

 

그리고 중요한 것이 scope인데 사용자의 깃헙 중 어디어디에 접근하겠다-! 라고 명시해주는 값입니다.

default값은 empty list입니다. 

scopes 문서를 보고 접근해야될 scope들을 콤마로 연결한 string값을 넘겨주면 됩니다. 

 

저는 repo와 user에 접근하겠다고 해서 스코프를 저렇게 해줬어요

func requestCode() {
    let scope = "repo,user"
    let urlString = "https://github.com/login/oauth/authorize?client_id=\(client_id)&scope=\(scope)"
    if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) {
        UIApplication.shared.open(url)
        // redirect to scene(_:openURLContexts:) if user authorized
    }
}

 

깃헙 로그인 버튼을 만들고 액션에 requestCode를 호출해줍니다. 

@objc func loginButtonDidTap() {
    LoginManager.shared.requestCode()
}

 

깃헙로그인 버튼을 누르면 아래의 플로우를 타게 됩니다. 

 

저기 파란 박스에 보면 아까 scope로 넣어준 것들을 사용자에게 알려줍니다. (너가 이 앱에 로그인을 허용하면 repo랑 user정보를 이 앱이 가져다쓰게 될 것이다. 라고 알려주는것-!)

그리고 이렇게 자세하게 너가 어떠어떠한 접근권한을 허용시켜주는 건지도 볼수있어요-! 사용자 입장에서 굿굿

 

 

 

사용자가 OK를 하면 redirection을 해줍니다.

redirection_ url은 

Appdelegate라면 application(_:open:options:) 메소드에 

SceneDelegate라면 scene(_:openURLContexts:) 메소드에 들어오게 됩니다. 

 

저는 SceneDelegate를 써서 이 메소드를 구현해주고 들어오는 url을 출력해볼게요-!

githubprviewer://login?code=어떤값 이 잘들어옵니다.

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        if let url = URLContexts.first?.url {
            print(url)
        }
    }

 

[3] Access Token 얻기 

위에서 얻은 코드는 일시적인 값으로 10분되면 expire됩니다. 

그래서 아래로 요청을 해서 access token을 얻어야합니다. 

 

 

문서에서는 "Code를 accessToken으로 교환해라" 라고 표현하고 있습니다. 

요청을 하면 아래와 같은 응답을 줍니다.

 

header에 Accept값을 추가해서 원하는 응답 포맷을 받을 수 있습니다. 

 

저는 json으로 받고 싶어서 아래와 같이 헤더 값을 설정해줬습니다-! 

  func requestAccessToken(with code: String) {
      let url = "https://github.com/login/oauth/access_token"
      let parameters = ["client_id": client_id,
                        "client_secret": client_secret,
                        "code": code]

      let headers: HTTPHeaders = ["Accept": "application/json"]

      AF.request(url, method: .post, parameters: parameters, headers: headers).responseJSON { (response) in
          switch response.result {
          case let .success(json):
              if let dic = json as? [String: String] {
                  print(dic["access_token"])
                  print(dic["scope"])
                  print(dic["token_type"])
              }
          case let .failure(error):
              print(error)
          }
      }
  }

 

이제 Access Token을 사용하여 github api에 요청할 수 있습니다 🎉

코드를 한번 정리하고 github api를 불러볼게요-!

 

[4] 코드 정리 

LoginManager를 만들어줍니다. 

import Foundation
import KeychainSwift
import Alamofire
import UIKit

class LoginManager {
    
    static let shared = LoginManager()
    
    private init() {}
    
    private let client_id = "어떤 값"
    private let client_secret = "어떤 값"
    
    func requestCode() {
        let scope = "repo,user"
        let urlString = "https://github.com/login/oauth/authorize?client_id=\(client_id)&scope=\(scope)"
        if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url)
            // redirect to scene(_:openURLContexts:) if user authorized
        }
    }
    
    func requestAccessToken(with code: String) {
        let url = "https://github.com/login/oauth/access_token"
        
        let parameters = ["client_id": client_id,
                          "client_secret": client_secret,
                          "code": code]
        
        let headers: HTTPHeaders = ["Accept": "application/json"]
        
        AF.request(url, method: .post, parameters: parameters, headers: headers).responseJSON { (response) in
            switch response.result {
            case let .success(json):
                if let dic = json as? [String: String] {
                    let accessToken = dic["access_token"] ?? ""
                    KeychainSwift().set(accessToken, forKey: "accessToken")
                }
            case let .failure(error):
                print(error)
            }
        }
    }
    
    func logout() {
        KeychainSwift().clear()
    }
}

 

로그인 버튼을 만들어주고 버튼이 눌리면 requestCode를 호출합니다. 

@objc func loginButtonDidTap() {
    LoginManager.shared.requestCode()
}

 

SceneDelegate의 openURL함수로 들어온 코드를 가지고

requestAccessToken을 호출합니다. 

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    if let url = URLContexts.first?.url {
        if url.absoluteString.starts(with: "githubprviewer://") {
            if let code = url.absoluteString.split(separator: "=").last.map({ String($0) }) {
                LoginManager.shared.requestAccessToken(with: code)
            }
        }
    }
}

 

 

[ 주의 ]

private let client_id = "어떤 값"
private let client_secret = "어떤 값"

제가 예제로 한 것처럼 client-id나 client-secret같은 키값을 앱에 하드코딩해서 쓰는 것은 안좋은 방식이에요-!!

여러 방법 중, 가장 괜찮은 방법을 찾고 있는 중입니다..

아래를 참고해주세요-!

 

medium.com/better-programming/how-to-hide-your-api-keys-c2b952bc07e6

 

How to Hide Your API Keys

Prevent theft by securing your API keys

medium.com

 

nshipster.com/secrets/

 

Secret Management on iOS

One of the great unsolved questions in iOS development is, “How do I store secrets securely on the client?”

nshipster.com

 

[5] Github API 호출해보기 

1) User 정보

    func getUser() {
        let url = "https://api.github.com/user"
        let accessToken = KeychainSwift().get("accessToken") ?? ""
        let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
                                    "Authorization": "token \(accessToken)"]

        AF.request(url, method: .get, parameters: [:], headers: headers).responseJSON(completionHandler: { (response) in
            switch response.result {
            case .success(let json):
                print(json as! [String: Any])
            case .failure:
                print("")
            }
        })
    }

 

2) Repo 정보

 

accessToken 덕분에 private repo까지 잘 출력되는 것을 볼 수 있다. 

    func getRepos() {
        let url = "https://api.github.com/user/repos"
        let accessToken = KeychainSwift().get("accessToken") ?? ""
        let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
                                    "Authorization": "token \(accessToken)"]
        AF.request(url, method: .get, parameters: [:], headers: headers).responseJSON(completionHandler: { (response) in
            switch response.result {
            case .success(let json):
                print(json)
            case .failure:
                print("")
            }
        })
    }

 

3) PR 정보

 

여기는 특별히 Pulls API 문서의 response를 보고 모델도 만들어줬다.

private repo를 요청해도 PR 목록을 잘 불러와준다. 

PRFetcher().getPulls(owner: "eunjin3786", repo: "GithubActionTest")

 

struct PullRequest: Decodable {
    let url: String
    let state: String
    let title: String
    let user: User
    let createdDate: String
    let updatedDate: String

    enum CodingKeys: String, CodingKey {
        case url
        case state
        case title
        case user
        case createdDate = "created_at"
        case updatedDate = "updated_at"
    }
}

struct User: Decodable {
    let name: String

    enum CodingKeys: String, CodingKey {
        case name = "login"
    }
}

class GithubFetcher {
    
    func getPulls(owner: String, repo: String) {
        let url = "https://api.github.com/repos/\(owner)/\(repo)/pulls"
        
        let accessToken = KeychainSwift().get("accessToken") ?? ""
        
        let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
                                    "Authorization": "token \(accessToken)"]
        
        let parameters: [String: Any] = ["state": "all"]
        
        AF.request(url, method: .get, parameters: parameters, headers: headers).responseData { (response) in
            switch response.result {
            case let .success(data):
                do {
                     let pullRequestList = try JSONDecoder().decode([PullRequest].self, from: data)
                    print(pullRequestList)
                 } catch let error as NSError {
                     print(error)
                 }
            case let .failure(error):
                print(error)
            }
        }
    }
}

 

728x90
반응형
댓글
  • 프로필사진 개발자 아라찌 덕분에 도움이 많이됬어요! 감사합니다

    혹시 userRepo 정보를 가져오는 부분에서 json 형식이 튜플로 묶여있어서 딕셔너리로 타입캐스팅이 안되는데 이부분은 혹시 어떻게 해결해야할지 질문해도 될까요 ㅠㅠ..
    2020.11.19 00:47 신고
  • 프로필사진 사용자 eungding 안녕하세요!!!

    https://docs.github.com/en/free-pro-team@latest/rest/reference/repos

    여기서 응답이 어떻게 내려오는지 봤는데
    딕셔너리말고
    딕셔너리로 된 array로 타입캐스팅하면 될것같아요!!


    let list = data as? Array<Dictionary<String, Any>>



    ## 전체코드

    let url = "https://api.github.com/user/repos"
    let accessToken = KeychainSwift().get("accessToken") ?? ""
    let headers: HTTPHeaders = ["Accept": "application/vnd.github.v3+json",
    "Authorization": "token \(accessToken)"]

    AF.request(url, method: .get, parameters: [:], headers: headers).responseJSON(completionHandler: { (response) in
    switch response.result {
    case .success(let data):
    if let list = data as? Array<Dictionary<String, Any>> {
    print(list.first?["description"])
    }
    case .failure:
    print("")
    }
    })
    2020.11.19 23:13 신고
  • 프로필사진 개발자 아라찌 감사합니다! ㅠㅠ 요즘 아파서 이제야 봤어요.. 제드님 블로그와 같이 항상 포스팅 잘 보고 있어요! 감사합니다! 그럼 개발도 좋지만 건강관리도 잘 하시고.. 답변 감사합니다~ 2020.11.26 07:54 신고
댓글쓰기 폼