Golang Google OIDC 이용

Golang Google OIDC 이용

Golang을 활용하여 Google OIDC 기반의 Token을 획득하고 분석한다.

1. OIDC 설정

Google Cloud Platform에서 OIDC 기반의 ID Token, OAuth 기반의 Access Token을 얻기 위해서는 설정이 필요하다.

[Figure 1] Project 생성

[Figure 1] Project 생성

[Figure 1]과 같이 https://console.developers.google.com에 접근하여 Project를 생성한다.

[Figure 2] OAuth 추가

[Figure 2] OAuth 추가

[Figure 2]와 같이 “API 및 서비스” 항목으로 들어가 “OAuth 클라이언트 ID” 추가를 선택하여 OAuth 인증 방식을 추가한다.

[Figure 3] OAuth Client ID 생성

[Figure 3] OAuth Client ID 생성

[Figure 3]과 같이 “웹 애플리케이션” 유형의 Client ID를 생성한다. “이름"은 임의로 지정하면 된다. “리다이렉션 URI"의 경우에는 예제 Code에서 처리할 경로인 “/auth/google/callback"을 명시한다. 생성이 완료되면 Client IDClient Secret을 확인한다.

2. App Code

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
// Code : https://github.com/ssup2/golang-Google-OIDC/blob/master/main.go

func main() {
	// Init variables
	ctx := context.Background()

	// Set OIDC, oauth oidcProvider
	oidcProvider, err := oidc.NewProvider(ctx, "https://accounts.google.com")
	if err != nil {
		log.Fatal(err)
	}
	oauth2Config := oauth2.Config{
		ClientID:     clientID,
		ClientSecret: clientSecret,
		Endpoint:     oidcProvider.Endpoint(),
		RedirectURL:  "http://127.0.0.1:3000/auth/google/callback",   // Set callback URL
		Scopes:       []string{oidc.ScopeOpenID, "profile", "email"}, // Set scope
	}

	// Define handler to redirect for login and permissions
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		state, err := randString(16)
		if err != nil {
			http.Error(w, "Internal error", http.StatusInternalServerError)
			return
		}
		nonce, err := randString(16)
		if err != nil {
			http.Error(w, "Internal error", http.StatusInternalServerError)
			return
		}
		setCallbackCookie(w, r, "state", state)
		setCallbackCookie(w, r, "nonce", nonce)

		// Redirect to Google login and permissions page
		http.Redirect(w, r, oauth2Config.AuthCodeURL(state, oidc.Nonce(nonce)), http.StatusFound)
	})

	// Define callback (redirect) handler
	http.HandleFunc("/auth/google/callback", func(w http.ResponseWriter, r *http.Request) {
		// Get state from URL and validate it
		state, err := r.Cookie("state")
		if err != nil {
			http.Error(w, "state not found", http.StatusBadRequest)
			return
		}
		if r.URL.Query().Get("state") != state.Value {
			http.Error(w, "state did not match", http.StatusBadRequest)
			return
		}

		// Get authorization code from URL
		authCode := r.URL.Query().Get("code")

		// Get ID token and access token through authorization code
		oauth2Token, err := oauth2Config.Exchange(ctx, authCode)
		if err != nil {
			http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
			return
		}

		// Get and validate ID token
		oidcConfig := &oidc.Config{
			ClientID: clientID,
		}
		oidcVerifier := oidcProvider.Verifier(oidcConfig)
		rawIDToken, ok := oauth2Token.Extra("id_token").(string)
		if !ok {
			http.Error(w, "No id_token field in oauth2 token.", http.StatusInternalServerError)
			return
		}
		idToken, err := oidcVerifier.Verify(ctx, rawIDToken)
		if err != nil {
			http.Error(w, "Failed to verify ID Token: "+err.Error(), http.StatusInternalServerError)
			return
		}

		// Get nonce from ID token and validate it
		nonce, err := r.Cookie("nonce")
		if err != nil {
			http.Error(w, "nonce not found", http.StatusBadRequest)
			return
		}
		if idToken.Nonce != nonce.Value {
			http.Error(w, "nonce did not match", http.StatusBadRequest)
			return
		}

		// Marshal and make up response
		resp := struct {
			OAuth2Token   *oauth2.Token
			IDToken       string
			IDTokenClaims *json.RawMessage // ID Token payload is just JSON.
		}{oauth2Token, rawIDToken, new(json.RawMessage)}
		if err := idToken.Claims(&resp.IDTokenClaims); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Write response
		data, err := json.MarshalIndent(resp, "", "    ")
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		w.Write(data)
	})

	// Run HTTP server
	log.Printf("listening on http://%s/", "127.0.0.1:3000")
	log.Fatal(http.ListenAndServe("127.0.0.1:3000", nil))
}
[Code 1] Golang Google OIDC Example App

[Code 1]은 Google OIDC를 이용하여 ID Token과 Access Token을 얻는 Golang App의 일부분을 나타내고 있다. 전체 App Code는 다음의 Repo에서 확인 할 수 있다.

동작 과정은 다음과 같다.

  • User가 Golang App의 “/” Path에 접속하면 Golang App은 User를 Google 인증/인가 Web Page로 Redirect 한다.
  • Google 인증/인가 Web Page는 User의 인증 및 인가 과정이 완료되면 Google 인증/인가 Web Page는 다시 Golang App의 “/auth/google/callback” Path로 Redirect 한다. 이 경우 Authorization Code를 URL Query로 같이 전달한다.
  • User가 Golang App의 “/auth/google/callback” Path로 접속하면 Golang App은 URL에 있는 Authorization Code를 얻은 다음, 얻은 Authorization Code를 통해서 ID Token, Access Token을 얻고 출력한다.

[Code 1]의 각 Line별 설명은 다음과 같다.

  • Line 16 : Scope는 ID Token 값에 포함되는 User의 정보 범위를 설정한다.
  • Line 21, 41 : State는 User의 CSRF 공격을 막기 위한 임시 문자열이다. 인증/인가전에 State를 생성 및 Cookie에 저장하며, Redirect 이후에 URL의 State와 Cookie의 State가 일치하는지 확인한다.
  • Line 26, 78 : Nonce는 ID Token이 유효한지 검증하는 용도로 이용되는 문자열이다. Nonce가 포함되도록 ID Token을 생성 및 Cookie에 저장하며, Redirect 이후에 얻은 ID Token의 Nonce와 Cookie의 Nonce가 일치하는지 확인한다.
  • Line 52 : Authorization Code는 URL의 “code” Query에 존재한다.

3. Google 인증/인가

[Figure 4] Google 인증

[Figure 4] Google 인증

1
2
3
4
5
6
7
https://accounts.google.com/o/oauth2/v2/auth/identifier?
client_id=554362356429-cu4gcpn45gb3incmm2v32sofslliffg2.apps.googleusercontent.com&
nonce=at7kn_XhHFySNbXRelfLwQ&
redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fauth%2Fgoogle%2Fcallback&
response_type=code&
scope=openid%20profile%20email&
state=usovevjnYZYpCTOaalbSWw&flowName=GeneralOAuthFlow
[Text 1] Google 인증 URL

[그림 3]은 Golang App의 “/” Path에 접속하면 Redirect 되어 접속되는 Google 인증 화면이다. [Text 1]은 Google 인증화면 접속시 이용되는 URL을 나타낸다. URL에 Query 형태로 Client ID, Nonce, Callback URL (Redirect URL), Scope, State 정보가 포함되어 있는것을 확인할 수 있다.

4. ID Token, Access Token

1
2
3
4
5
http://127.0.0.1:3000/auth/google/callback?state=usovevjnYZYpCTOaalbSWw&
code=4%2F0AX4XfWgoC8l0tmZ9anfZVb9mgtiYquLKZekXouqAoFKSN4-BZimF7m8wa5nDUElbBD8Jhg&
scope=email+profile+openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&
authuser=0&
prompt=consent
[Text 2] Callback URL

[Text 2]은 Redirect URL의 예제를 나타내고 있다. “code” Query에는 Authorization Code, “scope” Query에는 Scope 정보등이 포함되어 있는것을 확인할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
    "OAuth2Token": {
        "access_token": "ya29.A0ARrdaM8GQORQw-hKwlyNHfVB1cyGkmwRmWZiYOC_AEj8xV8QMEs35nfmVXtGDNrORxvcm9WCSBbujzEX2P-EeTeYmzJyrufai9QttcHQAgq1hfkGjQv7Gd3ZI4aZNl6GiY6fBSToILzPgF1dB-iXwymDVk3F",
        "token_type": "Bearer",
        "expiry": "2022-03-26T01:30:17.032547872+09:00"
    },
    "IDToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjU4YjQyOTY2MmRiMDc4NmYyZWZlZmUxM2MxZWIxMmEyOGRjNDQyZDAiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI1NTQzNjIzNTY0MjktY3U0Z2NwbjQ1Z2IzaW5jbW0ydjMyc29mc2xsaWZmZzIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI1NTQzNjIzNTY0MjktY3U0Z2NwbjQ1Z2IzaW5jbW0ydjMyc29mc2xsaWZmZzIuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTM2MzI0NTgzMjQwNTY4MzY2MjEiLCJlbWFpbCI6InN1cHN1cDU2NDJAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImF0X2hhc2giOiJndDNndXI5c3VkUmpXNHRsazJQVnl3Iiwibm9uY2UiOiJhdDdrbl9YaEhGeVNOYlhSZWxmTHdRIiwibmFtZSI6InNzcyBzc3MiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2otLXUybVgtOEFITkNyeGJ4ZmJOR1R6YnJ4QmJSbExoT2dpM3M0dFE9czk2LWMiLCJnaXZlbl9uYW1lIjoic3NzIiwiZmFtaWx5X25hbWUiOiJzc3MiLCJsb2NhbGUiOiJrbyIsImlhdCI6MTY0ODIyMjIyMCwiZXhwIjoxNjQ4MjI1ODIwfQ.rpv5ZonWufD9CSDZi9hn3tjBEMDrwMTEW7BwXilvPGa2U1pzzvGZhiY6Qxjbs4rqoJaaVu4vPdd5iob1oxloeY7e5s4I2mIaVxONpvOdeO3yj1FzWAnVgwZ7zRHEk0J2PVgwcXG-InWOX4TwFE8ZgWHnQD1WLQTQvB-PzQF7OUCy_0SYEz2qU5YCA2tYj7u59oMI1aWdLQXEhJwFlXyIdh3lTlWXLAXP04NSO9w5M7kQfk480nqk9kTyo29aZtnSM_c-uvxP4z95HHq9pBaktDiptZtOMf5EoLrQYDG1UQzQylozTWsw5GOeFlRsOfsvMOZDV8uwCGR648Qn6eME2A",
    "IDTokenClaims": {
        "iss": "https://accounts.google.com",
        "azp": "554362356429-cu4gcpn45gb3incmm2v32sofslliffg2.apps.googleusercontent.com",
        "aud": "554362356429-cu4gcpn45gb3incmm2v32sofslliffg2.apps.googleusercontent.com",
        "sub": "113632458324056836621",
        "email": "supsup5642@gmail.com",
        "email_verified": true,
        "at_hash": "gt3gur9sudRjW4tlk2PVyw",
        "nonce": "at7kn_XhHFySNbXRelfLwQ",
        "name": "sss sss",
        "picture": "https://lh3.googleusercontent.com/a-/AOh14Gj--u2mX-8AHNCrxbxfbNGTzbrxBbRlLhOgi3s4tQ=s96-c",
        "given_name": "sss",
        "family_name": "sss",
        "locale": "ko",
        "iat": 1648222220,
        "exp": 1648225820
    }
}
[Text 3] Callback Result - Access Token, ID Token

[Text 3]는 ID Token의 Claim과 Access Token의 예제를 나타내고 있다.

5. 참조