1. Wprowadzenie
Te ćwiczenia z programowania są kontynuacją ćwiczeń Przycisk Zaloguj się przez Google na stronach internetowych, więc najpierw wykonaj tamte ćwiczenia.
W tym samouczku użyjesz biblioteki JavaScript usług tożsamości Google i promptów One Tap, aby dodać logowanie użytkowników do statycznych i dynamicznych stron internetowych za pomocą interfejsów HTML i JavaScript API.
Skonfigurujemy też punkt końcowy logowania po stronie serwera, aby weryfikować tokeny identyfikacyjne JWT.
Czego się nauczysz
- Jak skonfigurować punkt końcowy logowania po stronie serwera, aby weryfikować tokeny identyfikatora
- Jak dodać prośbę o zalogowanie się przez Google jednym dotknięciem do strony internetowej
- jako statyczny element HTML,
- dynamicznie za pomocą JavaScriptu.
- Jak działa prośba o zalogowanie się jednym dotknięciem
Czego potrzebujesz
- Podstawowa znajomość HTML-a, CSS-a, JavaScriptu i Narzędzi deweloperskich w Chrome (lub ich odpowiedników).
- Miejsce do edytowania i hostowania plików HTML i JavaScript.
- Identyfikator klienta uzyskany w poprzednim laboratorium.
- Środowisko umożliwiające uruchomienie podstawowej aplikacji w Pythonie.
Chodźmy!
2. Konfigurowanie punktu końcowego logowania
Najpierw utworzymy skrypt w Pythonie, który będzie działać jako podstawowy serwer WWW, i skonfigurujemy środowisko Pythona niezbędne do jego uruchomienia.
Skrypt uruchomiony lokalnie wyświetla w przeglądarce stronę docelową, statyczny kod HTML i dynamiczne strony One Tap. Akceptuje żądania POST, dekoduje token JWT zawarty w parametrze credential i sprawdza, czy został on wydany przez platformę OAuth usługi Google Identity.
Po zdekodowaniu i zweryfikowaniu tokena JWT skrypt przekierowuje użytkownika na stronę docelową index.html
, aby wyświetlić wyniki.
Skopiuj ten tekst do pliku o nazwie simple-server.py
:
"""Very basic web server to handle GET and POST requests."""
from http.server import SimpleHTTPRequestHandler
import json
import socketserver
from typing import Dict, Optional, Tuple
import urllib.parse
from urllib.parse import parse_qs
from google.auth.transport import requests as google_auth_requests
from google.oauth2 import id_token
""" NOTE: You'll need to change this """
CLIENT_ID = (
"PUT_YOUR_WEB_CLIENT_ID_HERE"
)
""" these may change for a Cloud IDE, but good as-is for local termainals """
SERVER_ADDRESS = "0.0.0.0"
PORT = 3000
TARGET_HTML_PAGE_URL = f"http://localhost:{PORT}/"
""" and this is the end of constants you might need to change """
HTTP_STATUS_OK = 200
HTTP_STATUS_BAD_REQUEST = 400
HTTP_STATUS_UNAUTHORIZED = 401
HTTP_STATUS_INTERNAL_SERVER_ERROR = 500
HTTP_STATUS_FOUND = 303 # For redirection after decode and verify
OIDC_SERVER = "accounts.google.com"
class OIDCJWTReceiver(SimpleHTTPRequestHandler):
"""Request handler to securely process a Google ID token response."""
def _validate_csrf(self, request_parameters: Dict) -> Tuple[bool, str]:
"""Validates the g_csrf_token to protect against CSRF attacks."""
csrf_token_body = request_parameters.get("g_csrf_token")
if not csrf_token_body:
return False, "g_csrf_token not found in POST body."
csrf_token_cookie = None
cookie_header = self.headers.get("Cookie")
if cookie_header:
cookie_pairs = (c.split("=", 1) for c in cookie_header.split(";"))
cookies = {k.strip(): v.strip() for k, v in cookie_pairs}
csrf_token_cookie = cookies.get("g_csrf_token")
if not csrf_token_cookie:
return False, "g_csrf_token not found in cookie."
if csrf_token_body != csrf_token_cookie:
return False, "CSRF token mismatch."
return True, "CSRF token validated successfully."
def _parse_and_validate_credential(
self, request_parameters: Dict
) -> Optional[Tuple[Optional[Dict], str]]:
"""Parse POST data, extract, decode and validate user credential."""
credential = request_parameters.get("credential")
if not credential:
return None, "Credential not provided"
try:
id_info = id_token.verify_oauth2_token(
credential, google_auth_requests.Request(), CLIENT_ID
)
return id_info, ""
except ValueError as e:
return None, f"Error during JWT decode: {e}"
except Exception as e:
return None, f"Unexpected error during credential validation: {e}"
def _redirect_to_html(self, response_data: Dict) -> None:
"""Redirect to the target HTML page with data in the URL fragment."""
try:
json_data = json.dumps(response_data)
encoded_data = urllib.parse.quote(json_data)
redirect_url = f"http://localhost:{PORT}/#data={encoded_data}"
self.send_response(HTTP_STATUS_FOUND)
self.send_header("Location", redirect_url)
self.send_header("Connection", "close")
self.end_headers()
except Exception as e:
print(f"An error occurred during redirection: {e}")
self.send_response(HTTP_STATUS_INTERNAL_SERVER_ERROR)
self.send_header("Content-type", "text/plain")
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(f"A redirect error occurred: {e}".encode("utf-8"))
def _send_bad_request(self, message: str) -> None:
"""Sends a 400 Bad Request response."""
self.send_response(HTTP_STATUS_BAD_REQUEST)
self.send_header("Content-type", "text/plain")
self.send_header("Connection", "close")
self.end_headers()
self.wfile.write(message.encode("utf-8"))
def do_POST(self):
"""Handle POST requests for the /user-login path."""
if self.path != "/user-login":
self.send_error(404, "File not found")
return
try:
content_length = int(self.headers.get("Content-Length", 0))
post_data_bytes = self.rfile.read(content_length)
post_data_str = post_data_bytes.decode("utf-8")
request_parameters = {
key: val[0]
for key, val in parse_qs(post_data_str).items()
if len(val) == 1
}
csrf_valid, csrf_message = self._validate_csrf(request_parameters)
if not csrf_valid:
print(f"CSRF verify failure: {csrf_message}")
self._send_bad_request(f"CSRF verify failure: {csrf_message}")
return
decoded_id_token, error_message = self._parse_and_validate_credential(
request_parameters
)
response_data = {}
if decoded_id_token:
response_data["status"] = "success"
response_data["message"] = decoded_id_token
elif error_message:
response_data["status"] = "error"
response_data["message"] = error_message
else:
response_data["status"] = "error"
response_data["message"] = "Unknown error during JWT validation"
self._redirect_to_html(response_data)
except Exception as e:
self._redirect_to_html(
{"status": "error", "error_message": f"Internal server error: {e}"}
)
with socketserver.TCPServer(("", PORT), OIDCJWTReceiver) as httpd:
print(
f"Serving HTTP on {SERVER_ADDRESS} port"
f" {PORT} (http://{SERVER_ADDRESS}:{PORT}/)"
)
httpd.serve_forever()
Ponieważ zamierzamy sprawdzić, czy token JWT został wydany Twojemu klientowi, korzystając z pola odbiorcy (aud) tokena identyfikatora, Twoja aplikacja w Pythonie musi wiedzieć, którego identyfikatora klienta używa. Aby to zrobić, zastąp PUT_YOUR_WEB_CLIENT_ID_HERE
identyfikatorem klienta używanym w poprzednim ćwiczeniu z kodem dotyczącym przycisku Zaloguj się przez Google.
Środowisko Pythona
Skonfigurujmy środowisko do uruchamiania skryptu serwera WWW.
Potrzebujesz Pythona w wersji 3.8 lub nowszej oraz kilku pakietów, które pomogą w weryfikacji i dekodowaniu tokenów JWT.
$ python3 --version
Jeśli Twoja wersja Pythona 3 jest starsza niż 3.8, może być konieczne zmodyfikowanie zmiennej PATH w powłoce, aby można było znaleźć oczekiwaną wersję, lub zainstalowanie nowszej wersji Pythona w systemie.
Następnie utwórz plik o nazwie requirements.txt
, który zawiera listę pakietów potrzebnych do dekodowania i weryfikacji tokena JWT:
google-auth
Uruchom te polecenia w tym samym katalogu co simple-server.py
i requirements.txt
, aby utworzyć środowisko wirtualne i zainstalować pakiety tylko dla tej aplikacji:
$ python3 -m venv env
$ source env/bin/activate
(env) $ pip install -r requirements.txt
Teraz uruchom serwer. Jeśli wszystko działa prawidłowo, zobaczysz ten komunikat:
(env) $ python3 ./simple-server.py
Serving HTTP on 0.0.0.0 port 3000 (http://0.0.0.0:3000/) ...
Środowiska IDE działające w chmurze
Te warsztaty zostały zaprojektowane do uruchamiania w terminalu lokalnym i na hoście lokalnym, ale po wprowadzeniu pewnych zmian można ich używać na platformach takich jak Replit czy Glitch. Każda platforma ma własne wymagania dotyczące konfiguracji i domyślne ustawienia środowiska Pythona, więc prawdopodobnie trzeba będzie zmienić kilka rzeczy, np. TARGET_HTML_PAGE_URL
i konfigurację Pythona.
Na przykład w Glitch nadal dodasz requirements.txt
, ale utworzysz też plik o nazwie start.sh
, aby automatycznie uruchomić serwer Pythona:
python3 ./simple-server.py
Adresy URL używane przez skrypt w Pythonie i pliki HTML również muszą zostać zaktualizowane do zewnętrznego adresu URL Twojego środowiska Cloud IDE. W tym przypadku będzie to wyglądać tak: TARGET_HTML_PAGE_URL = f"https://your-project-name.glitch.me/"
. Pliki HTML w tym samouczku domyślnie używają też hosta lokalnego, więc musisz je zaktualizować, podając zewnętrzny adres URL Cloud IDE: data-login_uri="https://your-project-name.glitch.me/user-login"
.
3. Tworzenie strony docelowej
Następnie utworzymy stronę docelową, na której będą wyświetlane wyniki logowania za pomocą One Tap. Na stronie wyświetli się zdekodowany token identyfikatora JWT lub błąd. Formularz na stronie może też służyć do wysyłania tokena JWT do punktu końcowego logowania na naszym serwerze HTTP w Pythonie, gdzie jest on dekodowany i weryfikowany. Używa ona pliku cookie z podwójnym przesyłaniem CSRF i parametru żądania POST, dzięki czemu może ponownie wykorzystać ten sam punkt końcowy serwera user-login
co przykłady interfejsu API HTML i JavaScript gsi/client
w samouczku.
W terminalu zapisz ten kod w pliku o nazwie index.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>JWT Verification</title>
<style>
body { font-family: sans-serif; margin: 0; }
.top-nav {
text-align: center; padding: 15px 0; background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6; width: 100%;
}
.top-nav a {
margin: 0 15px; text-decoration: none; font-weight: bold;
color: #007bff; font-size: 1.1em;
}
.top-nav a:hover { text-decoration: underline; }
.page-container { padding: 20px; }
pre {
background-color: #f9f9f9; padding: 10px; overflow-x: auto;
white-space: pre-wrap; word-break: break-all;
}
.error { color: red; }
.success { color: green; }
#jwt-form { margin-bottom: 20px; flex: 1; }
fieldset {
border: 1px solid #ddd; padding: 10px; margin: 0;
min-width: 0; flex: 1; }
legend { font-weight: bold; margin-bottom: 5px; }
textarea { width: 100%; box-sizing: border-box; vertical-align: top; }
button[type="submit"] { padding: 8px 15px; margin-top: 10px; }
@media (min-width: 1024px) {
.main-content { display: flex; gap: 20px; }
.main-content > #jwt-form,
.main-content > .result-container {
flex-basis: 50%; /* Each item takes up 50% of the width */
margin-bottom: 0; /* Remove bottom margin when side-by-side */
display: flex; /* Make the result container a flex container */
flex-direction: column; /* Stack children vertically */
flex: 1; /* Allows the result container to grow and shrink */
}
}
</style>
</head>
<body>
<nav class="top-nav">
<a href="static-page.html">One Tap Static Page</a>
<a href="dynamic-page.html">One Tap Dynamic Page</a>
<a href="prompt-outcomes.html">Prompt behaviors</a>
</nav>
<div class="page-container">
<h1>JWT Verification</h1>
<div class="main-content">
<form id="jwt-form" action="/user-login" method="post">
<fieldset>
<legend>Encoded JWT ID Token</legend>
<textarea id="credential" name="credential" rows="5"
cols="50"></textarea>
<button type="submit">Verify JWT</button>
</fieldset>
</form>
<section class="result-container">
<fieldset>
<legend>Decode and verify result</legend>
<p id="status"></p>
<pre id="result"></pre>
</fieldset>
</section>
</div>
</div>
<script>
const statusElement = document.getElementById("status");
const resultElement = document.getElementById("result");
const handleResponse = (responseData) => {
const { status, message } = responseData;
const result = message
? status === "success"
? JSON.stringify(message, null, 2)
: message
: "";
statusElement.textContent = status;
resultElement.textContent = result;
statusElement.className = "";
if (status === "success") {
statusElement.classList.add("success");
} else if (status === "error") {
statusElement.classList.add("error");
}
};
const getEncodedDataFromHash = (hash) => {
const urlParams = new URLSearchParams(hash.substring(1));
return urlParams.get("data");
};
const processHashData = (hash) => {
const encodedData = getEncodedDataFromHash(hash);
if (encodedData) {
try {
const jsonData = JSON.parse(decodeURIComponent(encodedData));
handleResponse(jsonData);
history.pushState(
"",
document.title,
window.location.pathname + window.location.search,
);
} catch (error) {
handleResponse({
status: "error",
message: "Error parsing data from URL: " + error,
});
}
}
};
window.addEventListener("load",
() => processHashData(window.location.hash));
window.addEventListener("hashchange",
() => processHashData(window.location.hash));
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {
const generateRandomString = (length) => {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"abcdefghijklmnopqrstuvwxyz" +
"0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
const csrfToken = generateRandomString(12);
document.cookie = `g_csrf_token=${csrfToken};path=/;SameSite=Lax`;
const form = document.getElementById("jwt-form");
const hiddenInput = document.createElement("input");
hiddenInput.setAttribute("type", "hidden");
hiddenInput.setAttribute("name", "g_csrf_token");
hiddenInput.setAttribute("value", csrfToken);
form.appendChild(hiddenInput);
});
</script>
</body>
</html>
Testowanie serwera WWW i dekodowania tokena JWT
Zanim zaczniemy korzystać z funkcji One Tap, upewnimy się, że środowisko punktu końcowego serwera jest skonfigurowane i działa.
Otwórz stronę docelową http://localhost:3000/ i kliknij przycisk Verify JWT (Zweryfikuj JWT).
Powinno się wyświetlić:
Naciśnięcie przycisku powoduje wysłanie do skryptu w Pythonie żądania POST z zawartością pola wprowadzania. Skrypt oczekuje, że w polu wprowadzania będzie obecny zakodowany token JWT, więc próbuje zdekodować i zweryfikować ładunek. Następnie przekierowuje z powrotem na stronę docelową, aby wyświetlić wyniki.
Ale czekaj, nie było tokena JWT… czy to się nie powiodło? Tak, ale z klasą!
Pole było puste, więc wyświetla się błąd. Teraz spróbuj wpisać w polu tekstowym dowolny tekst i ponownie kliknąć przycisk. Występuje inny błąd dekodowania.
Możesz wkleić zakodowany token identyfikatora JWT wydany przez Google w polu wprowadzania, a skrypt w Pythonie zdekoduje, zweryfikuje i wyświetli go. Możesz też użyć strony https://jwt.io, aby sprawdzić dowolny zakodowany token JWT.
4. Statyczna strona HTML
Teraz skonfigurujemy One Tap tak, aby działało na stronach HTML bez użycia JavaScriptu. Może to być przydatne w przypadku witryn statycznych lub systemów pamięci podręcznej i sieci CDN.
Zacznij od dodania tego przykładowego kodu do pliku o nazwie static-page.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://accounts.google.com/gsi/client" async></script>
<link rel="icon" href="data:," />
</head>
<body>
<h1>Google One Tap static HTML page</h1>
<div
id="g_id_onload"
data-client_id="PUT_YOUR_WEB_CLIENT_ID_HERE"
data-ux_mode="redirect"
data-login_uri="http://localhost:3000/user-login"
></div>
</body>
</html>
Następnie otwórz static-page.html
i zastąp PUT_YOUR_WEB_CLIENT_ID_HERE
identyfikatorem klienta używanym w poprzednim ćwiczeniu dotyczącym przycisku Zaloguj się przez Google.
Jak to działa?
Każdy element HTML z atrybutem id
o wartości g_id_onload
i jego atrybuty danych są używane do konfigurowania biblioteki usług tożsamości Google (gsi/client
). Wyświetla ona też prośbę o logowanie jednym kliknięciem, gdy dokument jest wczytywany w przeglądarce.
Atrybut data-login_uri
to identyfikator URI, który otrzyma żądanie POST z przeglądarki po zalogowaniu się użytkownika. To żądanie zawiera zakodowany token JWT wydany przez Google.
Pełną listę opcji logowania jednym kliknięciem znajdziesz w generatorze kodu HTML i dokumentacji interfejsu HTML API.
Zaloguj się
Kliknij http://localhost:3000/static-page.html.
W przeglądarce powinien pojawić się komunikat One Tap.
Aby się zalogować, naciśnij Kontynuuj jako.
Po zalogowaniu się Google wysyła żądanie POST do punktu końcowego logowania serwera Pythona. Żądanie zawiera zakodowany token JWT podpisany przez Google.
Następnie serwer używa jednego z kluczy publicznych Google do weryfikacji, czy token JWT został utworzony i podpisany przez Google. Następnie dekoduje i weryfikuje, czy odbiorcy pasują do identyfikatora klienta. Następnie przeprowadzane jest sprawdzenie CSRF, aby upewnić się, że wartość pliku cookie i wartość parametru żądania w treści żądania POST są równe. Jeśli nie, to znak, że coś jest nie tak.
Na koniec strona docelowa wyświetla zweryfikowany token JWT jako poświadczenie użytkownika w formacie JSON.
Typowe błędy
Proces logowania może się nie powieść z kilku powodów. Oto kilka najczęstszych przyczyn:
data-client_id
jest nieprawidłowy lub go brakuje. W takim przypadku w konsoli Narzędzi deweloperskich pojawi się błąd, a prośba o logowanie jednym kliknięciem nie będzie działać.data-login_uri
jest niedostępny, ponieważ wpisano nieprawidłowy identyfikator URI, serwer WWW nie został uruchomiony lub nasłuchuje na nieprawidłowym porcie. W takim przypadku prompt One Tap będzie działać, ale po zwróceniu danych logowania na karcie sieci Narzędzi deweloperskich będzie widoczny błąd.- Nazwa hosta lub port używany przez serwer internetowy nie jest wymieniony w sekcji Autoryzowane źródła JavaScript dla identyfikatora klienta OAuth. Zobaczysz komunikat w konsoli: „Podczas pobierania punktu końcowego potwierdzenia tożsamości otrzymano kod odpowiedzi HTTP 400.”. Jeśli podczas tego ćwiczenia zobaczysz ten komunikat, sprawdź, czy na liście znajdują się zarówno
http://localhost/
, jak ihttp://localhost:3000
.
5. Strona dynamiczna
Teraz wyświetlimy funkcję One Tap za pomocą wywołania JavaScriptu. W tym przykładzie zawsze wyświetlamy logowanie jednym kliknięciem po wczytaniu strony, ale możesz też wyświetlać prośbę tylko wtedy, gdy jest to potrzebne. Możesz na przykład sprawdzić, czy sesja użytkownika trwa dłużej niż 28 dni, i ponownie wyświetlić prośbę o zalogowanie się.
Dodaj ten przykładowy kod do pliku o nazwie dynamic-page.html
.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://accounts.google.com/gsi/client" async></script>
<link rel="icon" href="data:," />
</head>
<body>
<h1>Google One Tap dynamic page</h1>
<script>
const generateRandomString = (length) => {
const array = new Uint8Array(length / 2);
window.crypto.getRandomValues(array);
return Array.from(array, (byte) =>
byte.toString(16).padStart(2, "0")
).join("");
};
const setCookie = (name, value) => {
document.cookie = '${name}=${value};path=/;SameSite=Lax';
};
const getCookie = (name) => {
const nameEQ = name + "=";
const ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0)
return c.substring(nameEQ.length, c.length);
}
return null;
};
function handleResponse(rsp) {
console.log("ID Token received from Google: ", rsp.credential);
console.log("Submitting token to server via dynamic form POST.");
const form = document.createElement("form");
form.method = "POST";
form.action = "http://" + window.location.host + "/user-login";
// Add the credential and CSRF cookie value asa hidden fields
const hiddenField = document.createElement("input");
hiddenField.type = "hidden";
hiddenField.name = "credential";
hiddenField.value = rsp.credential;
form.appendChild(hiddenField);
const csrfToken = getCookie("g_csrf_token");
if (csrfToken) {
console.log("Found g_csrf_token cookie, adding to form.");
const csrfField = document.createElement("input");
csrfField.type = "hidden";
csrfField.name = "g_csrf_token";
csrfField.value = csrfToken;
form.appendChild(csrfField);
} else {
console.warn(
"Warning: g_csrf_token cookie not found. POSTing without it."
);
}
document.body.appendChild(form);
form.submit();
}
window.onload = function () {
const csrfToken = generateRandomString(12);
setCookie("g_csrf_token", csrfToken);
console.log("CSRF token cookie set on page load:", csrfToken);
google.accounts.id.initialize({
client_id: "PUT_YOUR_WEB_CLIENT_ID_HERE",
ux_mode: "popup",
callback: handleResponse,
});
google.accounts.id.prompt(); // Display the One Tap prompt
};
</script>
</body>
</html>
Otwórz dynamic-page.html
i zastąp PUT_YOUR_WEB_CLIENT_ID_HERE
identyfikatorem klienta używanym w poprzednich ćwiczeniach z kodowania dotyczących przycisku Zaloguj się przez Google.
Ten kod to połączenie HTML i JavaScriptu. Wykonuje on kilka czynności:
- konfiguruje bibliotekę Usług tożsamości Google (
gsi/client
), wywołując funkcjęgoogle.accounts.id.initialize()
, - generuje plik cookie CSRF (Cross-Site Request Forgery) ,
- dodaje moduł obsługi wywołania zwrotnego, który odbiera zakodowany token JWT od Google i przesyła go za pomocą żądania POST formularza do naszego skryptu w Pythonie
/user-login
, - wyświetla prośbę o potwierdzenie jednym kliknięciem za pomocą
google.accounts.id.prompt()
.
Pełną listę ustawień One Tap znajdziesz w dokumentacji interfejsu JavaScript API.
Zalogujmy się!
Otwórz w przeglądarce adres http://localhost:3000/dynamic-page.html.
Działanie promptu One Tap jest takie samo jak w przypadku statycznego kodu HTML, z tym że ta strona definiuje procedurę obsługi wywołania zwrotnego JavaScriptu, która tworzy plik cookie CSRF, odbiera token JWT od Google i wysyła go do punktu końcowego user-login
serwera Pythona. Interfejs HTML API wykonuje te czynności automatycznie.
6. Zachowania promptów
Wypróbujmy więc kilka opcji za pomocą funkcji One Tap, ponieważ w przeciwieństwie do przycisku prośba o zgodę nie zawsze jest wyświetlana. Może zostać odrzucone, zamknięte lub wyłączone przez przeglądarkę i użytkownika.
Najpierw zapisz ten kod w pliku o nazwie prompt-outcomes.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Google One Tap Prompt behaviors</title>
<style>
body { font-family: sans-serif; padding: 20px; }
#log {
border: 1px solid #ccc; background-color: #f0f0f0;
padding: 15px; margin-top: 20px;
white-space: pre-wrap; font-family: monospace;
}
.success { color: green; }
.error { color: red; }
.info { color: blue; }
.warning { color: orange; }
</style>
</head>
<body>
<h1>One Tap behaviors</h1>
<p>Open the developer console to see detailed logs.</p>
<div id="log">Awaiting events...</div>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<script>
// logging utility to display event and notification info
const logElement = document.getElementById("log");
function log(message, type = "info") {
const timestamp = new Date().toLocaleTimeString();
logElement.innerHTML +=
`\n<span class="${type}">[${timestamp}] ${message}</span>`;
console.log(`[${type.toUpperCase()}] ${message}`);
}
function decodeJwt(jwt) {
try {
const parts = jwt.split(".");
if (parts.length !== 3) {
throw new Error("Invalid JWT structure");
}
const base64Url = parts[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(
atob(base64)
.split("")
.map(function (c) {
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join(""),
);
return JSON.parse(jsonPayload);
} catch (e) {
log(`Error decoding JWT: ${e.message}`, "error");
return null;
}
}
/* Handles the credential response after a user signs in. */
function handleCredentialResponse(credentialResponse) {
log("Credential Response received.", "success");
const credential = credentialResponse.credential;
log(`Credential JWT: ${credential.substring(0, 30)}...`);
// For demonstration, we decode the JWT on the client side.
// REMEMBER: Always verify the token on your backend server!
const payload = decodeJwt(credential);
if (payload) {
log(`Welcome, ${payload.name}! (Email: ${payload.email})`);
log("Decoded JWT Payload: " + JSON.stringify(payload, null, 2));
}
}
/* Handles notifications about the One-Tap prompt's UI status. */
function handlePromptMomentNotification(notification) {
log(`Prompt Moment Notification received.`, "info");
if (notification.isNotDisplayed()) {
const reason = notification.getNotDisplayedReason();
log(`Prompt not displayed. Reason: <strong>${reason}</strong>`,
"error");
}
if (notification.isSkippedMoment()) {
const reason = notification.getSkippedReason();
log(`Prompt was skipped. Reason: <strong>${reason}</strong>`,
"warning");
if (reason === "auto_cancel") {
log("may have called prompt() multiple times in a row.");
} else if (reason === "user_cancel") {
log("The user manually closed the prompt.");
}
}
if (notification.isDismissedMoment()) {
const reason = notification.getDismissedReason();
log(`Prompt dismissed. Reason: <strong>${reason}</strong>`, "info");
if (reason === "credential_returned") {
log("Expected, credential sent to the JS handler.");
} else if (reason === "cancel_called") {
log("programmatic call to google.accounts.id.cancel().");
}
}
}
window.onload = function () {
try {
google.accounts.id.initialize({
client_id: "PUT_YOUR_WEB_CLIENT_ID_HERE",
callback: handleCredentialResponse,
ux_mode: "popup",
});
google.accounts.id.prompt(handlePromptMomentNotification);
log("One Tap initialized. Waiting for prompt...");
} catch (e) {
log(`Initialization Error: ${e.message}`, "error");
}
};
</script>
</body>
</html>
Następnie otwórz prompt-outcomes.html
, zastąp PUT_YOUR_WEB_CLIENT_ID_HERE
identyfikatorem klienta i zapisz plik.
W przeglądarce otwórz adres http://localhost:3000/prompt-outcomes.html.
Kliknięcia strony
Zacznij od kliknięcia dowolnego miejsca poza promptem One Tap. Na stronie i w konsoli powinien pojawić się komunikat „The request has been aborted.” (Żądanie zostało przerwane).
Zaloguj się
Następnie zaloguj się w normalny sposób. Zobaczysz aktualizacje logowania i powiadomień, które mogą służyć do wywoływania takich działań jak tworzenie lub odświeżanie sesji użytkownika.
Zamknij prompt
Teraz ponownie załaduj stronę i gdy pojawi się One Tap, kliknij „X” na pasku tytułu. Ten komunikat powinien zostać zarejestrowany w konsoli:
- Użytkownik odrzucił lub zamknął prośbę. Włączono wykładnicze wygaszanie interfejsu API.
Podczas testowania uruchomisz proces chłodzenia. W okresie oczekiwania nie wyświetla się prośba o skorzystanie z funkcji One Tap. Podczas testowania prawdopodobnie będziesz chcieć zresetować urządzenie, zamiast czekać na automatyczne zresetowanie… chyba że naprawdę chcesz iść na kawę lub wrócić do domu i się wyspać. Aby zresetować okres oczekiwania:
- Kliknij ikonę „Informacje o witrynie” po lewej stronie paska adresu przeglądarki.
- kliknąć przycisk „Reset Permissions” (Zresetuj uprawnienia),
- odśwież stronę.
Po zresetowaniu okresu oczekiwania i ponownym załadowaniu strony pojawi się prośba o logowanie jednym kliknięciem.
7. Podsumowanie
W tym module dowiedzieliśmy się kilku rzeczy, np. jak wyświetlać One Tap przy użyciu statycznego kodu HTML lub dynamicznie za pomocą JavaScriptu.
Skonfigurowano bardzo prosty serwer WWW w Pythonie na potrzeby testowania lokalnego i poznano kroki niezbędne do dekodowania i weryfikowania tokenów identyfikatora.
Znasz już najczęstsze sposoby interakcji użytkowników z prośbą o logowanie jednym kliknięciem i jej zamykania oraz masz stronę internetową, której możesz użyć do debugowania zachowania prośby.
Gratulacje!
Aby uzyskać dodatkowe punkty, spróbuj wrócić i użyć One Tap w różnych przeglądarkach, które obsługuje.
Te linki mogą Ci pomóc w wykonaniu kolejnych kroków:
- Google Identity Services HTML API
- JavaScript API usług tożsamości Google
- Jak skonfigurować logowanie się przez Google w internecie
- Weryfikowanie tokena identyfikatora Google
- Więcej informacji o projektach Google Cloud
- Metody uwierzytelniania w Google Identity