Tombol “Login dengan Google” kelihatannya cuma satu klik. Di baliknya ada beberapa request HTTP bolak-balik antara browser kamu, server aplikasi, dan server Google. Kalau kamu cuma memanggil SDK bawaan framework, seluruh proses itu tersembunyi — dan begitu ada yang meleset, kamu tidak tahu harus mencari di mana.

Artikel ini membongkar alurnya satu per satu. Kamu akan membangun login Google dari nol di FastAPI memakai flow OAuth 2.0 Authorization Code dengan PKCE, standar yang disarankan Google untuk aplikasi web. Kalau belum pernah menyentuh FastAPI, mampir dulu ke Cara Membuat REST API dengan FastAPI.

Alur Authorization Code + PKCE dalam 30 Detik

Browser kamu diarahkan ke halaman Google sambil membawa satu kode acak bernama state dan sepasang nilai PKCE (code_verifier dan code_challenge). Google menampilkan layar persetujuan. Begitu kamu klik izinkan, Google mengirim browser balik ke aplikasi kamu dengan sebuah authorization code. Server kamu lalu menukar code itu dengan access_token lewat request langsung ke Google, dan terakhir mengambil data user. Tiga langkah inti: redirect, tukar code, ambil data.

PKCE (Proof Key for Code Exchange) memastikan hanya aplikasi yang memulai login yang bisa menukar code-nya. code_verifier cuma kamu yang punya, sedangkan code_challenge adalah hasil hash-nya yang dikirim ke Google. Saat menukar code, kamu kirim verifier-nya. Cocok, baru token keluar.

Menyiapkan Kredensial di Google Cloud Console

Buka console.cloud.google.com. Buat project baru atau pakai yang sudah ada. Lalu masuk ke menu APIs & Services → OAuth consent screen, pilih tipe External, isi nama aplikasi dan email kontak, simpan. Di tab Audience (Test users), tambahkan alamat Gmail kamu — kalau langkah ini dilewati, akun kamu akan ditolak saat mencoba login dengan pesan access_blocked.

Setelah itu ke APIs & Services → Credentials → Create Credentials → OAuth client ID. Pilih Application type: Web application. Di bagian Authorized redirect URIs, isi http://localhost:8000/auth/callback. Simpan, lalu salin Client ID dan Client secret yang muncul.

Dependensi dan Konfigurasi

Kita cuma butuh FastAPI, Uvicorn, dan httpx untuk request ke Google. python-dotenv dipakai supaya kredensial tidak nempel di kode.

pip install fastapi uvicorn httpx python-dotenv

Simpan Client ID dan Client secret di file .env di root proyek:

bash
GOOGLE_CLIENT_ID=xxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxx

Jangan pernah commit file .env ke git. Tambahkan ke .gitignore, dan kalau client_secret sempat bocor ke repository publik, cabut kredensial itu di Google Cloud Console lalu buat yang baru — Google cukup agresif menonaktifkan project yang kredensialnya ketahuan bocor.

Berikut kode lengkapnya. Simpan sebagai main.py. Dua endpoint saja: /auth/login yang melempar user ke Google, dan /auth/callback yang menerima balikannya.

import base64
import hashlib
import os
import secrets
from urllib.parse import urlencode

import httpx
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import RedirectResponse

load_dotenv()

GOOGLE_CLIENT_ID = os.environ["GOOGLE_CLIENT_ID"]
GOOGLE_CLIENT_SECRET = os.environ["GOOGLE_CLIENT_SECRET"]
REDIRECT_URI = "http://localhost:8000/auth/callback"

AUTHORIZATION_URL = "https://accounts.google.com/o/oauth2/v2/auth"
TOKEN_URL = "https://oauth2.googleapis.com/token"
USERINFO_URL = "https://openidconnect.googleapis.com/v1/userinfo"
SCOPES = "openid email profile"

app = FastAPI()


def generate_pkce_pair() -> tuple[str, str]:
    code_verifier = secrets.token_urlsafe(64)
    digest = hashlib.sha256(code_verifier.encode()).digest()
    code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
    return code_verifier, code_challenge


@app.get("/auth/login")
def login():
    state = secrets.token_urlsafe(16)
    code_verifier, code_challenge = generate_pkce_pair()

    params = {
        "client_id": GOOGLE_CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "response_type": "code",
        "scope": SCOPES,
        "state": state,
        "code_challenge": code_challenge,
        "code_challenge_method": "S256",
    }
    url = f"{AUTHORIZATION_URL}?{urlencode(params)}"

    response = RedirectResponse(url)
    response.set_cookie("oauth_state", state, httponly=True, max_age=600)
    response.set_cookie("code_verifier", code_verifier, httponly=True, max_age=600)
    return response


@app.get("/auth/callback")
async def callback(request: Request):
    code = request.query_params.get("code")
    state = request.query_params.get("state")

    if not code or not state:
        raise HTTPException(status_code=400, detail="Parameter tidak lengkap")

    saved_state = request.cookies.get("oauth_state")
    code_verifier = request.cookies.get("code_verifier")

    if not saved_state or state != saved_state:
        raise HTTPException(status_code=400, detail="state tidak cocok, kemungkinan CSRF")

    async with httpx.AsyncClient() as client:
        token_resp = await client.post(
            TOKEN_URL,
            data={
                "client_id": GOOGLE_CLIENT_ID,
                "client_secret": GOOGLE_CLIENT_SECRET,
                "code": code,
                "grant_type": "authorization_code",
                "redirect_uri": REDIRECT_URI,
                "code_verifier": code_verifier,
            },
        )
        token_resp.raise_for_status()
        tokens = token_resp.json()

        user_resp = await client.get(
            USERINFO_URL,
            headers={"Authorization": f"Bearer {tokens['access_token']}"},
        )
        user_resp.raise_for_status()
        user = user_resp.json()

    return {"email": user["email"], "name": user.get("name")}

Dua baris di generate_pkce_pair paling sering bikin salah. code_challenge harus dihitung dengan SHA-256 dari code_verifier, lalu di-encode base64url tanpa padding — itu fungsi dari .rstrip(b"="). Kalau padding = ikut terkirim, Google akan menolak verifier-nya.

Pemeriksaan state (nilai yang kamu kirim saat redirect harus sama dengan yang dikirim balik Google) itu yang menghalau serangan CSRF pada endpoint login. Tanpa cek ini, penyerang bisa memaksa korbannya login ke akun milik penyerang. Jangan pernah skip baris itu.

Pertukaran code dengan token terjadi server-to-server, bukan lewat browser, jadi client_secret aman dipakai di sini. Respons token dari Google terlihat kira-kira seperti ini:

json
{
  "access_token": "ya29.a0AfH6SM...",
  "expires_in": 3599,
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "scope": "openid email profile",
  "token_type": "Bearer"
}

Data User dan Session

access_token dipakai untuk memanggil endpoint userinfo dan mengambil email plus nama user. Contoh responsnya:

json
{
  "sub": "110169484474386276334",
  "name": "Budi Santoso",
  "given_name": "Budi",
  "family_name": "Santoso",
  "picture": "https://lh3.googleusercontent.com/a/...",
  "email": "budi@example.com",
  "email_verified": true
}

Email itu identitas user di sistem kamu. Langkah berikutnya adalah membuat session supaya user tidak login ulang di tiap request — di sinilah kamu menggabungkannya dengan Autentikasi JWT di FastAPI. Tandai user yang login lewat Google, dan untuk akun dengan hak akses tinggi pertimbangkan menambahkan Two-Factor Authentication (2FA) dengan TOTP.

Menjalankan dan Menguji

bash
uvicorn main:app --reload

Buka http://localhost:8000/auth/login di browser. Kamu akan dilempar ke Google, diminta persetujuan, lalu balik ke /auth/callback dengan JSON berisi email kamu. Kalau ketemu error access_blocked, cek lagi daftar test user di Google Cloud Console. Dari sini, coba simpan user ke database dan bungkus seluruh alur ini jadi dependency FastAPI yang reusable.