Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

Google oAuth

구글 oAuth2 인증을 이용하는 웹 애플리케이션을 개발하려고 한다. oAuth2를 이용하는 이유는 아래와 같다.
  • 유저는 각 서비스 별로 ID/Password 방식으로 로그인 하는 것을 싫어한다. 그 많은 아이디 패스워드를 어떻게 기억하나.
  • 서비스 제공자 입장에서는 구글, 페이스북, 카카오, 네이버의 유저들을 "원 버튼 클릭으로" 서비스를 사용 하도록 할 수 있다. 기존 인터넷 서비스 제공자의 유저기반을 사용 할 수 있다는 의미다.
oAuth2는 임베디드 애플리케이션, 모바일 애플리케이션, 웹 애플리케이션(브라우저) 등 다양한 유형의 애플리케이션 인증에 사용 할 수 있다. 애플리케이션 유형에 따라 인증 프로세스가 달라질 수 있다. 아래는 웹 애플리케이션 기반의 oAuth2 인증 프로세스다.

 oAuth2 프로세스

  1. 먼저 www.joinc.co.kr 서비스가 구글 인증을 사용 할 수 있도록 "구글에 인증을 수행 할 수 있도록 위임해 줄 것을 요청" 한다. 구글이 허가하면, access token과 secret token 을 발급한다. www.joinc.co.kr 서비스 서버는 이 두개의 토큰을 이용해서 구글과 oAuth2 작업을 수행할 수 있다.
  2. Rsouer Owner(www.joinc.co.kr 서비스를 사용하려는 유저)는 클라이언트(chrome 웹 브라우저)를 이용한다.
  3. 클라이언트는 www.joinc.co.kr에 접근 한다. 아직 google 인증을 받지 않았기 때문에 www.joinc.co.kr 서버는 구글 인증 페이지로 보낸다.
  4. 클라이언트는 구글 인증 사이트에 접근한다. 우리가 흔히 보는 구글 로그인 페이지가 실행된다.
  5. 인증이 성공되면 구글은 access token과 함께 www.joinc.co.kr의 콜백주소로 리다이렉트 한다.
  6. 클라이언트로 부터 access token을 받은 www.joinc.co.kr 콜백 사이트는 구글에 검증을 요청한다. 검증이 성공하면, oAuth2 인증 프로세스가 완료된다.
oAuth2 인증이 끝나고 나면, www.joinc.co.kr 서버는 이 유저가 첫 로그인 유저인지 확인한다. 첫 로그인 유저라면 데이터베이스 유저 정보를 저장하고 세션(Session)을 발급한다. 이전에 로그인했던 유저라면 세션을 발급한다.

Federated identity

이렇게 사용자가 자신의 계정으로 다른 서비스에 로그인 할 수 있는데, 이를 Federated identity라고 한다. 예를 들어 google 계정으로 github.com, medium.com 서비스를 이용 할 수 있다. Federated(연합)라는 뜻 그대로 하나의 계정으로 여러 서비스들을 묶을 수 있는 것이다.

Go oAuth2 애플리케이션 개발

Go언어로 oAuth2 애플리케이션을 만들어보기로 했다. 이 애플리케이션은 "로그인 페이지"와 "콜백 페이지"로 구성된다.
  1. 로그인 페이지 : Google Login 버튼을 가지고 있다. Login 버튼을 클릭하면 구글 로그인 페이지로 이동한다.
  2. 콜백(callback) 페이지 : 구글 인증이 끝나면, 호출할 페이지. 이 페이지에서 구글 인증 완료를 검증한다.

구글에 서비스 등록하기

구글 oAuth2를 사용하기 위해서는 구글에 서비스를 등록해야 한다. Google API 및 서비스페이지로 이동하자. 여기에서 구글에 등록할 프로젝트를 관리 할 수 있다.

 Project 등록

대시보드에 들어가면 프로젝트 리스트가 보일 것이다. 상단 메뉴의 프로젝트 리스트를 선택하면 프로젝트 관리 창이 뜬다. 새 프로젝트를를 클릭하면 프로젝트 만들기 화면으로 넘어간다.

 Project 만들기

프로젝트 이름을 설정하고 "만들기" 버튼을 누르면 구글을 기반으로 하는 프로젝트가 만들어진다. 이렇게 만들어진 프로젝트들은 구글의 수많은 기술/기능(구글맵, 머신러닝 등)을 사용 할 수 있다. 지금 관심있는 oAuth2를 집중적으로 살펴보겠다.

 동의화면 구성

사용자 인증 정보 화면으로 이동한다. 여기에서 OAuth2.0 클라이언트 ID를 만들면된다. 그 전에 동의화면을 구성해야 한다. oAuth2 서비스로 로그인을 하면 "어떤 서비스가 유저 인증을 요청합니다. 이 서비스는 고객의 이메일 정보와 구글 드라이브에 대한 접근을 요청합니다"와 같은 동의 메시지가 뜨는걸 경험한 적이 있을 것이다. 고객에게 동의를 구하기 위한 정보를 제출하는 과정이다. "동의화면 구성"을 클릭하자.

 oAuth 동의 화면

유저 타입을 선택한다. 인터넷 공간의 일반 사용자를 위한 서비스이므로 "외부"를 선택했다. 만들기 버튼을 클릭한다.

 oAuth2 동의화면 설정
  1. 애플리케이션 이름 : myApp
  2. 애플리케이션 로고 : 서비스 대표 이미지.
  3. 지원 이메일 : 서비스 관리자 이메일.
  4. Google API 범위 : 서비스 제공자가 읽을 수 있는 정보의 범위다.
 oauth2 동의화면 설정 - 2

나머지 서비스 도메인과 페이지에 대한 정보를 입력한다.

 동의화면 설정 리뷰

동의화면 설정이 만들어졌다. 아직은 서비스가 확인된 상태가 아니기 때문에 "확인 필요" 상태다. 테스트하는데는 상관 없으니 넘어가자.

 OAuth 클라이언트 ID 만들기

"사용자 인증 정보 > 사용자 인증 정보 만들기 > OAuth 클라이언트 ID"를 클릭해서 새로운 OAuth 클라이언트를 등록 할 수 있다.

 oAuth 클라이언트 ID 만들기

  • 애플리케이션 유형 : 웹 애플리케이션, Android, Chrome 앱, iOS, TV 및 입력제한기기, 데스크톱 앱, UWP 등 유형을 선택 할 수 있다. 웹 애플리케이션을 선택했다.
  • 이름 : 애플리케이션 이름을 설정한다.
  • URI : oAuth2 클라이언트가 설치될 URI를 입력한다. 테스트이므로 http://localhost:8080 을 설정했다.
  • 승인된 리디렉션 URI : Google 인증 창에서 인증을 끝낸 후 리다이렉트 할 콜백 URL을 설정한다. 나는 http://localhost:8080/login/callback 로 설정했다.
 OAuth 클라이언트 생성

클라이언트가 만들어지면 클라이언트 ID클라이언트 보안 비밀번호가 생성된다. 이들은 oAuth2 애플리케이션을 만들때 사용하는 값이다. 저장해 두자.

oAuth2 애플리케이션 개발

클라이언트 ID와 클라이언트 보안 비밀번호를 확보했으니 애플리케이션을 개발해보자. 개발언어는 go 를 사용한다.

테스트에 사용한 코드다
package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"io/ioutil"
	"net/http"
)

var oauthConf *oauth2.Config

const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="

func oauthInit() {
	oauthConf = &oauth2.Config{
		ClientID:     "xxxxxxxxx.apps.googleusercontent.com",
		ClientSecret: "A-yyyyyyyyyyyyyyyyyyy",
		RedirectURL:  "http://localhost:8080/login/callback",
		Scopes: []string{
			"https://www.googleapis.com/auth/userinfo.email",
		},
		Endpoint: google.Endpoint,
	}
}

func getToken() string {
	b := make([]byte, 32)
	rand.Read(b)
	return base64.RawStdEncoding.EncodeToString(b)
}

func getLoginURL(state string) string {
	return oauthConf.AuthCodeURL(state)
}

func main() {
	oauthInit()

	r := gin.Default()

	r.LoadHTMLGlob("templates/*")
	r.GET("/login", func(c *gin.Context) {
		token := getToken()
		url := getLoginURL(token)
		c.HTML(http.StatusOK, "login.html", gin.H{
			"title": "Socal Login",
			"url":   url,
		})
	})
	r.GET("/login/callback", func(c *gin.Context) {
		code := c.Query("code")
		token, err := oauthConf.Exchange(oauth2.NoContext, code)
		if err != nil {
			c.JSON(403, gin.H{"Message": err.Error()})
			return
		}
		response, err := http.Get(oauthGoogleUrlAPI + token.AccessToken)
		if err != nil {
			c.JSON(403, gin.H{"Message": err.Error()})
			return
		}
		defer response.Body.Close()
		contents, err := ioutil.ReadAll(response.Body)
		if err != nil {
			c.JSON(403, gin.H{"Message": err.Error()})
			return
		}
		fmt.Println(string(contents))
		jsonMap := make(map[string]interface{})
		json.Unmarshal(contents, &jsonMap)
		c.JSON(200, jsonMap)
	})
	r.Run()
}
gin 프레임워크를 사용했다.

로그인 페이지를 위해서 사용할 템플릿 파일 login.html의 내용이다.
<html>
    <head>
        <title>{{ .title }}</title>
    </head>
    <body>                                       
        <a href="{{ .url }}">Google Login</a>
    </body>
</html>

서버를 실행하고 파이어폭스로 테스트 했다.
# go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:	export GIN_MODE=release
 - using code:	gin.SetMode(gin.ReleaseMode)

[GIN-debug] Loaded HTML Templates (2): 
	- 
	- login.html

[GIN-debug] GET    /login                    --> main.main.func1 (3 handlers)
[GIN-debug] GET    /login/callback           --> main.main.func2 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

파이어폭스로 localhost:8080/login 페이지에 접근한다.

 Login Page

Google Login링크를 클릭하면 Google 인증 페이지로 이동한다.

 구글 인증 페이지

로그인을 원하는 계정을 선택하면 된다. 로그인이 성공하면 콜백 페이지로 이동한다.

 인증 완료

구글 인증이 성공하면 code를 URL로 넘기는데, 이 code를 이용해서 access token을 가져올 수 있다. 이 access token으로 google 인증 api를 호출해서 올바르게 인증을 했는지 확인 할 수 있다. 올바른 인증 과정을 거쳤다면 email, picture와 같은 유저 정보를 얻을 수 있다.

중요 코드를 검토해 보자.
const oauthGoogleUrlAPI = "https://www.googleapis.com/oauth2/v2/userinfo?access_token="

func oauthInit() {
	oauthConf = &oauth2.Config{
		ClientID:     "xxxxxxxxx.apps.googleusercontent.com",
		ClientSecret: "A-yyyyyyyyyyyyyyyyyyy",
		RedirectURL:  "http://localhost:8080/login/callback",
		Scopes: []string{
			"https://www.googleapis.com/auth/userinfo.email",
		},
		Endpoint: google.Endpoint,
	}
}
oauthGoogleUrlAPI은 구글 유저 정보를 가져오기 위한 API다. 유저 정보를 가져오기 위해서는 올바른 access_token이 필요하다. access_token를 가져오기 위해서는 구글 인증에 성공한 유저의 code가 필요하다.

oauth2.Config 에 구글 oAuth 인증을 위한 주요 값들을 설정한다.
  • ClientID : myApp 애플리케이션의 클라이언트 ID
  • ClientSecret : myApp 애플리케이션의 시크릿 키 값
  • RedirectURL : 인증을 끝내고 호출할 콜백 URL
  • Scopes : myApp 애플리케이션에서 사용할 유저 정보의 범위
로그인 링크를 만드는 코드를 보자.
r.GET("/login", func(c *gin.Context) {
    token := getToken()
    url := getLoginURL(token)
    c.HTML(http.StatusOK, "login.html", gin.H{                                                            
        "title": "Socal Login",
        "url":   url,
    })
})
getToken() 함수는 state 파리미터를 만들기 위해서 사용한다. 이 파라미터는 애플리케이션과 서버 응답 사이의 상태를 유지하기 위해서 사용하는 문자열 값이다. 이 값은 랜덤하게 생성되기 때문에 값이 일치하는 지를 확인해서 요청이 올바른지(위조되지 않았는지 등을)를 검토하기 위해서 사용 할 수 있다.

getLoginURL 함수는 oauthConf.AuthCodeURL(state)를 이용해서 Google 인증 서버 URL을 리턴한다. URL은 아래와 같다.
https://accounts.google.com/o/oauth2/auth?client_id=xxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com \
&amp;redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin%2Fcallback \
&amp;response_type=code \
&amp;scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email \
&amp;state=zzzzzzzzzzzzzzzzzzzzzz

URL은 google 인증페이지로 이동한다. 인증이 끝나면 redirect_uri 로 "리다이렉트" 되나. 이때 URL은 아래와 같다.
http://localhost:8080/login/callback? \
state=aAM5FDFLA58NiD8aeCQL9R44SCPxQWUUqYLOfsCJvyk \
&code=4%2F1QHyKksVo8Ya18daZxBLIbTWNam4uqg2EQvmjTIdgnTosnKHB-liLEuRyyOY4g11Q7z2SmUfZrO7LR0xR2eUa5k \
&scope=email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+openid \
&authuser=0 \
&prompt=consent

/login/callback는 state, code, scope, authuser 등의 정보를 받아서 검증 한다.
  1. state : 내 사이트에서 만든 요청인지를 확인한다. 서버는 state를 일정 시간 REDIS 등에 저장하고 있어야 할 것이다. 이 코드는 예제라서 state를 확인하지는 않고 있다.
  2. code : code를 key로 google 인증서버에 검증 요청을 한다. 검증이 성공하면 token을 리턴하고, 이 token을 이용해서 google 서비스를 사용 할 수 있다. 가장 대표적인 api 가 사용자의 profile을 얻어오는 API다.
이렇게 사용자 인증이 검증되고 프로파일을 가져오면 oauth 작업이 완료된다.