들어가며...

다양한 앱을 사용해보며 카카오, 구글, Apple, FaceBook 등 여러 소셜 로그인을 경험한 사례가 많았고
프로젝트에 소셜 로그인을 구현해보며 정리하였습니다.

처음에는 Apple 로그인을 구현하려고 했으나.. Apple 로그인 구현은 개발자 계정이 꼭 필요합니다..😂

바로 코드를 보기에 앞서 OAuth 2.0의 동작 방식을 조금이나마 이해하고 넘어가보도록 하겠습니다.

🅰️ OAuth 2.0 동작방식

처음 OAuth를 이해하기에는 다소 익숙치 않은 개념들이 있어 역할군과 동작 방식을 코드와 함께 예시를 통해 이해해보도록 하겠습니다.

예를 들어, 무신사와 같은 앱을 이용할 때 카카오, Apple과 같은 소셜 로그인 화면이 있습니다.
(물론 자사의 아이디와 비밀번호를 통해 회원가입하는 방법도 존재한다는 점이 있죠)

OAuth 2.0 에서 가장 중요하게 관리되는 것이 바로 AccessToken입니다.
직접적인 사용자의 아이디와 비밀번호 같은 개인정보를 서드파티 앱에 저장 또는 제공하지않고 사용할 수 있도록 하는게 핵심이죠❗️

여기서 그럼 역할군을 생각해봅니다.

  1. 서드파티앱 (무신사, 쿠팡, 배달의 민족 등등)
  2. 사용자 (서드파티 앱을 사용하고자 하는 소비자)
  3. 소셜 로그인을 제공하는 공급자 (Apple, 카카오, 네이버 등등)

위에 쉽게 표현한 방법을 코드에 적용할만한 역할군으로 다시 정의해보면

  1. Mine (Client)
  2. User (Resource Owner)
  3. Their (Resource Server)

이렇게 표현할 수 있을 것 같습니다.

역할군은 어느정도 이해가 되었으니 그렇다면 서드파티 앱에서 소셜 로그인 공급자에게

내 앱의 사용자(User)들이 너희가 제공하는 서비스와 함께 사용하고싶어❗️ (ex. 결제, 캘린더, 회원가입 우회 등)
그래서 말인데.. 너희 서비스에 내 앱을 등록하고싶어 ❗️❗️

이렇게 이해할 수 있을 것 같습니다.
그렇다면 이제는 서드파티 앱을 등록해야겠죠 ❓❗️

1️⃣ 앱 등록하기

Resource Server를 이용하기 위해서는 Resouce Server의 승인을 사전에 미리 받아야하겠죠??
이러한 행위를 등록이라고 합니다. (서비스마다 다르게 표현하기도 합니다.)

등록을 위해서는 다음과 같은 요구사항을 Resource Server에서 Client에게 요구합니다.

  1. Client ID
  2. Client Secret
  3. Authorized redirect URLs

하나씩 항목들을 살펴보겠습니다.

  1. 서드파티 앱의 고유 아이디 값
  2. 서드파티 앱의 고유 비밀번호
  3. 되돌아가고 싶은 URL 주소 값

아래는 저의 프로젝트를 Github에 프로젝트 앱을 등록하게되었을 때 예시를 볼 수 있습니다.

✅ Client_ID 값은 공개해도 괜찮지만 Client_Secret 값은 유출되지 않도록 주의합니다❗️

이렇게 앱 등록은 마쳤으니 이제 사용자(User - Resource Owner)가 서드파티 앱(Client)로 부터
접근하고 사용하고 싶은 필요한 부분만 Resouce Server가 제공할 수 있도록 승인 절차가 필요하겠네요!

2️⃣ User(Resource Owner)의 승인

그럼 위에서 처음 보여주었던 소셜 로그인 버튼을 사용자가 눌러서 시도하면 아래의 URL같은 내용을 포함하여 생성하게됩니다.

https://resouce.server/?client_id=""&scope=""&redirect_url=https://client/callback

Resouce Owner가 로그인 버튼을 누르고 로그인을 시도하게 되면 Resource Server로 위 URL을 통해 요청이 이루어지고
기존에 로그인이 안되어 있다면 아래와 같이 새로 로그인을 요청하게된다.

비유를 하자면 Resource Owner(사용자)가 Resource Server(Github)의 서비스를 이용하기 위해 Client는 로그인 화면을 User에게 보여주기만 하면된다.

여기서 redirect_URLs는 로그인을 마치고 Client URL로 돌아가기 위해 필요한 파라미터이다.

이후 Resource Owner가 로그인을 하게 되면 Resource Server는 그때서야
Client로부터 넘어온 Client_ID 값과 같은 자신에게 이전에 서드파티 앱(Client)가 등록했었던 Client_ID 값을 비교하게된다.
비교 후 Resource Sever에서 값이 동일하다고 판단되면 Resouce Owner에게 scope에 해당되는 권한Client에게 부여할 것인지를
확인하는 화면을 전송하게된다. 아래의 이미지와 같이 말이다.

여기서 scope는 User가 사용하고자 하는 기능의 범위를 말한다 ❗️

Resource Owner의 인증 허용이 이루어지면 Resource Server는 User_IDscope에 대한 값을 얻게된다.

3️⃣ Resource Server의 승인

Resource Owner의 접근에 대한 승인이 이루어지고나면 Resource Server에서 Client로부터의 승인이 필요하게된다.

여기서 어려운 점이 바로 AccessToken을 바로 발행하지 않는다는 점이다❗️

대신 Authorization Code를 발행하여 임시 비밀번호를 가지게되며 해당 Code를 Resource Owner에게 전달하게된다.
아래의 그림을 통해 이해해볼 수 있게된다.

이때 Owner가 인식하지 못하게 (은밀하게) 위 주소로 이동하게 되고 Client는 이렇게 Authorization Code의 값을 알게된다.

여기까지가 Client가 Resource Server에게 Authorization Code의 정보를 전송해서 AccessToken을 발행하기 직전까지의 과정이다.

이제 Client는 Resource Owner를 거치지 않고 Resource Server에게 직접 접근하게된다.
여기서 중요한 점은 Client_Secret 값을 준다는 점이다.

인증이 완료되면 Authorization Code는 삭제되고 Resource Server는 AccessToken을 발행하여 Client에게 전달된다.

이렇게 AccessToken을 통해 이제는 Resource Server에서 User가 접근을 허용한 서비스 항목에 대하여
API를 요청하여 데이터를 사용할 수 있게 되었다 ❗️❗️❗️


🅱️ Github 로그인 코드 구현

이제 OAuth 2.0에 대한 동작 방식을 이해하였으니 코드로 구현해보자!

🔗 GitHub Docs를 참고하여 앱에서 구현 할 수 있다.

위 링크를 참고하여 따라가면 앞의 OAuth에 대해 좀 더 쉽게 이해가 될 수 있습니다.

Github에 프로젝트를 등록(A-1. 앱 등록하기)하는 과정은 다른 참고사이트에서 훨씬 자세하게 다루어 생략했습니다.

1️⃣ Github ID 요청하기 (Authorization Code 얻기)

GET https://github.com/login/oauth/authorize
  • HTTPMethod = GET
  • URL Components
    • client_id (필수)
    • redirect_url
    • login
    • scope
    • state
    • allow_signup

위 파라미터와 HTTP 요청 타입을 지정하여 URL을 요청할 수 있습니다.
코드로 구현하면 아래와 같습니다.

func requestCodeToGithub() {
	// repo와 user 매개변수에 접근하겠다는 명시
	let scope = "repo,user"
    let urlString = "http://github.com/login/oauth/authorize?client_id=\(client_id)&scope=\(scope)"
    
    if let url = URL(string: urlString), UIApplication.shared.canOpenURL(url) {
    	// redirect to scene(_:openURLContexts:) if user authorized 
        UIApplication.shared.open(url)
    }
}

...

// Button addTarget Method
@objc func touchUpGithubLoginButton() { 
	LoginManager.shared.requestCodeToGithub()
}

앞의 동작방식에서 설명했던 scope는 요청하고싶은 항목이였으며

여기서는 repository(저장소), user(사용자 정보)에 접근하고싶다는 형식으로 작성되었습니다.

A-2에서 설명한 이미지를 여기서 볼 수 있게됩니다.

2️⃣ Github가 사용자를 사이트로 다시 redirection

위에서 얻은 Authorization Code는 일시적인 값으로 10분 후에 만료됩니다.
그래서 아래의 요청을 통해 AccessToken을 얻어야하죠!

POST https://github.com/login/oauth/access_token
  • HTTPMethod = POST
  • URL Components
    • client_id (필수)
    • client_secret (필수)
    • code (필수)
    • redirect_url

위에서 동작방식을 잘 이해했다면 Resource Server가 Client에 요청하는 항목을 바로 알아차릴 수 있습니다.
문서에서는 기본적으로 AccessToken의 응답의 형식은 아래와 같다고 합니다.

access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer

실제로 AccessToken을 받아서 출력해 본 결과도 아래와 같습니다.

Access Token을 받아보기위해 작성한 코드는 아래와 같습니다.

func getUser(accessToken: String) {
	print("My AccessToken = \(accessToken)")
	guard let url = URL(string: "https://api.github.com/user") else { return }
	var request = URLRequest(url: url)
	
    request.httpMethod = "GET"
	request.addValue("token \(accessToken)", forHTTPHeaderField: "Authorization")
	request.addValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")

	let task = URLSession.shared.dataTask(
		with: request as URLRequest, completionHandler: { data, response, error in
			guard let data = data else { return }
            guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
				print(String(data: data, encoding: .utf8) ?? "Not String!!")
				return	
            }	
		print(json["avatar_url"] as Any)
		print(json["name"] as Any)
     })
	task.resume()
}
    
 func requestAccessTokenToGitHub(with code: String) {
    guard let url = URL(string: "https://github.com/login/oauth/access_token") else { return }
    let parameters = ["client_id": client_id, 
                      "client_secret": client_secret,
                      "code": code]
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    
    do {
        request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: .prettyPrinted)
    } catch let error {
        print(error.localizedDescription)
    }
    
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("application/json", forHTTPHeaderField: "Accept")
    
    let task = URLSession.shared.dataTask(
        with: request as URLRequest, completionHandler: { data, response, error in
            guard let data = data else { return }
            print(data)
            guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
                print(String(data: data, encoding: .utf8) ?? "Not String!")
                return
            }
            self.getUser(accessToken: json["access_token"] as! String)
        })
    task.resume()
}

🎉이제 위 AccessToken을 통해 Github API에 요청할 수 있습니다 ❗️❗️

여기서 requestAccessTokenToGitHub 메서드는 호출을 SceneDelegate에서 처리해줍니다.
이 부분은 redirection을 통해 resource Server로 넘어갔다가 다시 돌아오기에 가장 적합한 자리라고 판단된다고 생각되어 작성했습니다.
기존의 SceneDelegate 메서드 내에는 없기에 아래의 코드를 추가적으로 작성합니다.

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    guard let url = URLContexts.first?.url else {
        return
    }
    
    if url.absoluteString.starts(with: "ios-airbnb://") {
        if let code = url.absoluteString.split(separator: "=").last.map({ String($0) }) {
            LoginManager.shared.requestAccessTokenToGitHub(with: code)
        }
    } else {
        print(url)
    }
}

감사합니다.

🌐 Reference Site

[iOS] Github OAuth Access Token 얻기 (깃헙으로 로그인)
Github OAuth로 로그인하기
[생활코딩] WEB2 - OAuth 2.0

profile
🧑🏼‍💻 iOS developer

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN