🎃

Googleでログインお勉強メモ1:Google Identity Services

2021/09/23に公開

はじめに

Googleでログインする方法はいくつかあります。これは Google Identity Services を使って実装をお試ししたメモです

Google Identity Services

以下のことは書いていません

❌Google Sign-In(g-signin2)

❌ Spring Security OAuth2/OpenID Connect Client を使った実装

❌ macOS以外の実装

❌ Google One Tapについて

❌ JavaScriptでの実装

❌ Kotlin(Java)以外の実装

参考

環境

フロントはJavaScriptを使わずHTMLで構成する
バックエンドはSpring Boot & Kotlin で実装する

  • macOS Big Sur
  • Kotlin
  • Spring Boot

準備

Spring Bootプロジェクト作成

spring initializr で以下の設定でプロジェクトを作成する

  • Project
    • Gradle Project
  • Language
    • Kotlin
  • Dependencies
    • Spring Web
    • Thymeleaf

クライアントID取得

Google Cloud Platform で クライアント ID を取得する

以下の説明を参考にする

Get your Google API client ID

以下の設定をする

  • 承認済みの JavaScript 生成元(Authorized JavaScript origins) には http://localhosthttp://localhost:8080 の2つを設定する

実装

1. Sign-In With Googleボタンを配置する

Front - HTML

resources/templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Google Identity Services Demo</title>
</head>
<body>
    <script src="https://accounts.google.com/gsi/client" async defer></script>
    <div id="g_id_onload"
         data-client_id="CLIENT ID"
         data-login_uri="http://localhost:8080/signin"
         data-ux_mode="popup"
         data-auto_prompt="false">
    </div>
    <div class="g_id_signin"
         data-type="standard"
         data-size="large"
         data-theme="outline"
         data-text="sign_in_with"
         data-shape="rectangular"
         data-logo_alignment="left">
    </div>
</body>
</html>

g_id_onload

  • data-client_id には取得したクライアントID(xxxx.apps.googleusercontent.com)を設定する
  • data-login_uri はログイン完了のコールバックURLを設定する
  • data-ux_modepopupredirect を設定する

g_id_signin

  • 上記リンクの設定でお好みに調整する

Backend - Kotlin

上記 index.html を表示するコントローラを作成する

RootController.kt

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping

@Controller
class RootController {

    @GetMapping("/")
    fun index(): String {
        return "index"
    }
}

動作確認

この時点でGoogleでログインは動作します。(ログイン完了後のエンドポイントが無いのでエラーになります)

Googleにログインしていないとき

Googleにログイン済みのとき

2. ログイン完了コールバックを実装する

Googleでログイン完了した時のコールバックを作成します。

Gradle

build.gradle.kts

  • 依存関係にcom.google.api-client:google-api-client を追加する
dependencies {
	implementation("com.google.api-client:google-api-client:1.31.5")
	implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Backend - Kotlin

  • ログイン後のコールバックには credential パラメータでIDトークン(JWT)がついてくる

  • このIDトークンをDecode(Verify)して真正性を確認する必要がある

  • IDトークンのVerifyは GoogleIdTokenVerifier を使う

  • IDトークンのVerifyに成功すると中のユーザー情報が取り出せる

    • subject がユーザーを特定するID
  • IDトークンから取り出したユーザー情報をセッション保存してuserinfoページにリダイレクトする

SignInController.kt

import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import javax.servlet.http.HttpServletRequest

@Controller
class SignInController {

    @PostMapping("signin")
    fun signIn(
        @RequestParam("credential") credential: String,
        request: HttpServletRequest,
    ): String {
        // verify ID Token
        val verifier = GoogleIdTokenVerifier.Builder(
            NetHttpTransport(), GsonFactory.getDefaultInstance()
        )
            .setAudience(listOf("CLIENT ID"))
            .build()

        val idToken = verifier.verify(credential) ?: throw Exception()
        val payload = idToken.payload

        // Get profile information from payload
        val session = request.getSession(true)
        session.setAttribute("subject", payload.subject)
        session.setAttribute("email", payload.email)
        session.setAttribute("emailVerified", payload.emailVerified)
        session.setAttribute("name", payload["name"])
        session.setAttribute("picture", payload["picture"])
        session.setAttribute("family_name", payload["family_name"])
        session.setAttribute("given_name", payload["given_name"])

        return "redirect:userinfo"
    }
}

3. ログイン後ページを実装する

ログインしたユーザー情報を表示するページを実装します

Front - HTML

resources/templates/userinfo.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>UserInfo</title>
</head>
<body>
    <div>subject(User ID):<span th:text="${subject}"></span></div>
    <div>email:<span th:text="${email}"></span></div>
    <div>emailVerified:<span th:text="${emailVerified}"></span></div>
    <div>name:<span th:text="${name}"></span></div>
    <div>picture:<span th:text="${picture}"></span></div>
    <div>family_name:<span th:text="${family_name}"></span></div>
    <div>given_name:<span th:text="${given_name}"></span></div>

    <a href="/">Back</a>

</body>
</html>

Backend - Kotlin

セッションからユーザー情報を取り出してフロントに引き渡します

UserInfoController.kt

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import javax.servlet.http.HttpServletRequest

@Controller
class UserInfoController {

    @GetMapping("userinfo")
    fun userInfo(
        request: HttpServletRequest,
        model: Model
    ): String {

        // Get User Info from session
        val session = request.getSession(false) ?: throw Exception()
        model.addAttribute("subject", session.getAttribute("subject"))
        model.addAttribute("email", session.getAttribute("email"))
        model.addAttribute("emailVerified", session.getAttribute("emailVerified"))
        model.addAttribute("name", session.getAttribute("name"))
        model.addAttribute("picture", session.getAttribute("picture"))
        model.addAttribute("locale", session.getAttribute("locale"))
        model.addAttribute("family_name", session.getAttribute("family_name"))
        model.addAttribute("given_name", session.getAttribute("given_name"))

        return "userinfo"
    }
}

動作確認

Googleでログインすると Backend の SignInController.signin()が実行された後 userinfo.html に遷移する

4. 実際の実装は

実際の実装は以下のようになる

①Googleでログインをクリック

②Googleの画面でログインする

  • 2要素認証などで

③ログイン後のコールバックでIDトークンをデコードする

④IDトークンからsubject(ユーザーID)などのユーザー情報を取得し、自アプリに登録済みのユーザーかチェックする

  • 自アプリに登録済みであればそのユーザーでログインし、ログイン後の画面に遷移する
  • 未登録であれば登録画面に遷移する(登録画面ではユーザー情報を入力補完する)

おつかれさまでした

  • 実装は非常に簡単
  • バックエンドのIDトークン検証のところだけOpenID Connectの知識があるといいかも
  • レガシーなGoogleログインはIDトークンをVerifyしなくてもユーザー情報を取り出せたが今回は必ずVerifyが必要がなのでよりセキュアになっている印象

Discussion