如果您的应用允许用户使用 Google 账号登录,您可以监听并响应跨账号保护服务提供的安全事件通知,从而提高这些共享用户账号的安全性。
这些通知会提醒您用户 Google 账号的重大更改,这些更改通常也会对用户在您的应用中的账号产生安全影响。例如,如果用户的 Google 账号被盗,可能会导致用户在您的应用中的账号通过电子邮件账号恢复或使用单点登录而被盗用。
为了帮助您降低此类事件的潜在风险,Google 会向您的服务发送名为安全事件令牌的服务对象。这些令牌只会公开极少量的信息,即安全事件的类型、发生时间以及受影响用户的标识符,但您可以使用这些信息采取适当的应对措施。例如,如果用户的 Google 账号被盗,您可以暂时为该用户停用“使用 Google 账号登录”功能,并阻止向用户的 Gmail 邮箱发送账号恢复电子邮件。
跨账号保护基于 RISC 标准,由 OpenID 基金会开发。
概览
如需将跨账号保护与您的应用或服务搭配使用,您必须完成以下任务:
在 API 控制台中设置项目。
创建事件接收器端点,Google 会向该端点发送安全事件令牌。此端点负责验证收到的令牌,然后以您选择的任何方式响应安全事件。
向 Google 注册您的端点,开始接收安全事件令牌。
前提条件
您只会收到已向您的服务授予访问其个人资料信息或电子邮件地址权限的 Google 用户的安全事件令牌。您可以通过请求 profile 或 email 范围来获取此权限。较新的
“使用 Google 账号登录”或旧版
Google 登录 SDK 默认会请求这些范围,但
如果您不使用默认设置,或者直接访问 Google 的OpenID
Connect 端点,请确保
您至少请求了其中一个范围。
在 API 控制台中设置项目
在开始接收安全事件令牌之前,您必须创建服务账号,并在 API 控制台项目中启用 RISC API。您必须使用在应用中访问 Google 服务(例如 Google 登录)时使用的同一 API 控制台项目。
如需创建服务账号,请执行以下操作:
打开 API 控制台 “凭据”页面。系统提示时,选择在应用中访问 Google 服务时使用的 API 控制台项目。
点击创建凭据 > 服务账号 。
为新创建的服务账号创建密钥。选择 JSON 密钥类型,然后点击创建 。创建密钥后,您将下载一个包含服务账号凭据的 JSON 文件。请将此文件保存在安全的位置,但也要确保您的事件接收器端点可以访问该文件。
在项目的“凭据”页面上,还要记下您用于“使用 Google 账号登录”或 Google 登录(旧版)的客户端 ID。通常,您为支持的每个平台都有一个客户端 ID。您将需要这些客户端 ID 来验证安全事件令牌,如下一部分所述。
如需启用 RISC API,请执行以下操作:
在 API 控制台中打开RISC API 页面。确保您用于访问 Google 服务的项目仍处于选中状态。
阅读 RISC 条款,确保您了解相关要求。
如果您要为组织拥有的项目启用 API,请确保您已获得将组织绑定到 RISC 条款的授权。
只有在您同意 RISC 条款的情况下,才点击启用 。
创建事件接收器端点
如需接收来自 Google 的安全事件通知,请创建一个处理 HTTPS POST 请求的 HTTPS 端点。注册此端点后(见下文),Google 将开始向该端点发布加密签名的字符串,称为安全事件令牌。安全事件令牌是已签名的 JWT,其中包含有关单个安全相关事件的信息。
对于您在端点收到的每个安全事件令牌,请先验证并解码该令牌,然后根据您的服务处理相应的安全事件。在解码之前验证事件令牌至关重要 ,以防止恶意攻击。以下部分介绍了这些任务:
1. 解码并验证安全事件令牌
由于安全性事件令牌是一种特定的 JWT,因此您可以使用任何 JWT 库(例如 jwt.io 上列出的库)来解码和验证它们。无论您使用哪个库,您的令牌验证代码都必须执行以下操作:
- 从 Google 的 RISC 配置文档中获取跨账号保护发布者标识符 (
issuer) 和签名密钥证书 URI (jwks_uri),您可以在https://accounts.google.com/.well-known/risc-configuration中找到该文档。 - 使用您选择的 JWT 库,从安全事件令牌的标头中获取签名密钥 ID。
- 从 Google 的签名密钥证书文档中,获取在上一步中获取的密钥 ID 的公钥。如果该文档不包含具有您要查找的 ID 的密钥,则安全事件令牌可能无效,您的端点应返回 HTTP 错误 400。
- 使用您选择的 JWT 库,验证以下内容:
- 安全事件令牌是使用在上一步中获取的公钥签名的。
- 令牌的
aud声明是您的某个应用的客户端 ID。 - 令牌的
iss声明与您从 RISC 发现文档中获取的发布者标识符一致。请注意,您无需验证令牌的到期时间 (exp),因为安全事件令牌代表历史事件,因此不会过期。
例如:
Java
使用 java-jwt 和 jwks-rsa-java:
public DecodedJWT validateSecurityEventToken(String token) {
DecodedJWT jwt = null;
try {
// In a real implementation, get these values from
// https://accounts.google.com/.well-known/risc-configuration
String issuer = "accounts.google.com";
String jwksUri = "https://www.googleapis.com/oauth2/v3/certs";
// Get the ID of the key used to sign the token.
DecodedJWT unverifiedJwt = JWT.decode(token);
String keyId = unverifiedJwt.getKeyId();
// Get the public key from Google.
JwkProvider googleCerts = new UrlJwkProvider(new URL(jwksUri), null, null);
PublicKey publicKey = googleCerts.get(keyId).getPublicKey();
// Verify and decode the token.
Algorithm rsa = Algorithm.RSA256((RSAPublicKey) publicKey, null);
JWTVerifier verifier = JWT.require(rsa)
.withIssuer(issuer)
// Get your apps' client IDs from the API console:
// https://console.developers.google.com/apis/credentials?project=_
.withAudience("123456789-abcedfgh.apps.googleusercontent.com",
"123456789-ijklmnop.apps.googleusercontent.com",
"123456789-qrstuvwx.apps.googleusercontent.com")
.acceptLeeway(Long.MAX_VALUE) // Don't check for expiration.
.build();
jwt = verifier.verify(token);
} catch (JwkException e) {
// Key not found. Return HTTP 400.
} catch (InvalidClaimException e) {
} catch (JWTDecodeException exception) {
// Malformed token. Return HTTP 400.
} catch (MalformedURLException e) {
// Invalid JWKS URI.
}
return jwt;
}
Python
import json
import jwt # pip install pyjwt
import requests # pip install requests
def validate_security_token(token, client_ids):
# Get Google's RISC configuration.
risc_config_uri = 'https://accounts.google.com/.well-known/risc-configuration'
risc_config = requests.get(risc_config_uri).json()
# Get the public key used to sign the token.
google_certs = requests.get(risc_config['jwks_uri']).json()
jwt_header = jwt.get_unverified_header(token)
key_id = jwt_header['kid']
public_key = None
for key in google_certs['keys']:
if key['kid'] == key_id:
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key))
if not public_key:
raise Exception('Public key certificate not found.')
# In this situation, return HTTP 400
# Decode the token, validating its signature, audience, and issuer.
try:
token_data = jwt.decode(token, public_key, algorithms='RS256',
options={'verify_exp': False},
audience=client_ids, issuer=risc_config['issuer'])
except:
raise
# Validation failed. Return HTTP 400.
return token_data
# Get your apps' client IDs from the API console:
# https://console.developers.google.com/apis/credentials?project=_
client_ids = ['123456789-abcedfgh.apps.googleusercontent.com',
'123456789-ijklmnop.apps.googleusercontent.com',
'123456789-qrstuvwx.apps.googleusercontent.com']
token_data = validate_security_token(token, client_ids)
如果令牌有效且已成功解码,则返回 HTTP 状态 202。 然后,处理令牌指示的安全事件。
2. 处理安全事件
解码后,安全事件令牌类似于以下示例:
{
"iss": "https://accounts.google.com/",