如果您的应用允许用户使用 Google 帐号登录他们的帐号,您可以监听并响应跨帐号保护服务提供的安全事件通知,从而提高这些共享用户帐号的安全性。
这些通知会提醒您用户的 Google 帐号发生重大变化,而这些应用通常也会影响其使用您应用的帐号。例如,如果用户的 Google 帐号被盗用,那么就可能会导致用户通过电子邮件帐号恢复或使用单点登录导致您的应用遭到盗用。
为了帮助您降低此类事件的风险,Google 会发送您的服务对象,称为安全事件令牌。这些令牌提供的信息很少(安全事件的类型、发生时间以及受影响用户的标识符),但您可以使用它们做出适当的响应。例如,如果用户的 Google 帐号遭到入侵,您可以暂时停用该用户的“使用 Google 帐号登录”功能,并阻止将帐号恢复电子邮件发送到该用户的 Gmail 地址。
跨帐号保护功能基于 OpenID Foundation 开发的 RISC 标准。
概览
若要将跨帐号保护功能用于您的应用或服务,您必须完成以下任务:
在 API Console中设置项目。
创建一个事件接收器端点,Google 会将端点发送到该端点。此端点负责验证收到的令牌,然后以您选择的任何方式响应安全事件。
在 Google 上注册您的端点,开始接收安全性事件令牌。
前提条件
您只会向已授权您的服务访问其个人资料信息或电子邮件地址的 Google 用户接收安全事件令牌。您可以通过请求 profile
或 email
范围来获得此权限。默认情况下,较新的使用 Google 帐号登录或旧版 Google 登录 SDK 会请求这些范围,但如果您不使用默认设置,或者直接访问 Google 的 OpenID Connect 端点,请确保至少请求其中一个范围。
在 API Console中设置项目
您必须先创建服务帐号并在API Console 项目中启用 RISC API,才能开始接收安全事件令牌。您必须在应用中使用您用于访问 Google 服务(如 Google 登录)的API Console 项目。
如需创建服务帐号,请执行以下操作:
打开 API Console Credentials page。出现提示时,请选择您在应用中访问 Google 服务所用的API Console项目。
点击创建凭据 > 服务帐号。
按照这些说明创建具有 RISC Configuration Admin 角色 (
roles/riscconfigs.admin
) 的新服务帐号。为新创建的服务帐号创建密钥。选择 JSON 密钥类型,然后点击创建。创建密钥后,您将下载包含您的服务帐号凭据的 JSON 文件。请将此文件保存在安全的位置,但同时也可供事件接收器端点访问。
在项目的“凭据”页面上,另请注意您用于“使用 Google 帐号登录”或 Google 登录(旧版)的客户端 ID。通常情况下,对于您支持的每个平台,您都会有一个客户端 ID。您需要使用这些客户端 ID 来验证安全性事件令牌,如下一部分所述。
如需启用 RISC API,请执行以下操作:
在API Console中打开 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/",
"aud": "123456789-abcedfgh.apps.googleusercontent.com",
"iat": 1508184845,
"jti": "756E69717565206964656E746966696572",
"events": {
"https://schemas.openid.net/secevent/risc/event-type/account-disabled": {
"subject": {
"subject_type": "iss-sub",
"iss": "https://accounts.google.com/",
"sub": "7375626A656374"
},
"reason": "hijacking"
}
}
}
iss
和 aud
声明表示令牌的颁发者 (Google) 和令牌的预期接收者(您的服务)。您在上一步中验证了这些声明。
jti
声明是一个字符串,用于标识单个安全性事件,是数据流唯一的。您可以使用此标识符来跟踪已接收的安全事件。
events
声明包含有关令牌代表的安全事件的信息。该声明是从事件类型标识符到 subject
声明的映射,后者指定了此事件涉及的用户,以及有关可用事件的所有其他详细信息。
subject
声明通过用户的唯一 Google 帐号 ID (sub
) 来识别特定用户。此 Google 帐号 ID 是由较新的 Google 帐号登录 (JavaScript、HTML) 库、旧版 Google 登录库或 OpenID Connect 中包含的 JWT ID 令牌中包含的同一标识符 (sub
)。当声明的 subject_type
为 id_token_claims
时,它还可能包含一个 email
字段,其中包含用户的电子邮件地址。
使用 events
声明中的信息,对指定用户帐号的事件类型采取相应措施。
OAuth 令牌标识符
对于各个令牌的 OAuth 事件,令牌主题标识符类型包含以下字段:
token_type
:仅支持refresh_token
。token_identifier_alg
:如需了解可能的值,请参见下表。token
:请参见下表。
token_identifier_alg | 令牌 |
---|---|
prefix |
令牌的前 16 个字符。 |
hash_base64_sha512_sha512 |
令牌的双哈希值(使用 SHA-512)。 |
如果您与这些事件集成,建议根据这些可能的值将您的令牌编入索引,以确保收到事件时快速匹配。
支持的事件类型
跨帐号保护功能支持以下类型的安全事件:
事件类型 | 属性 | 如何回答 |
---|---|---|
https://schemas.openid.net/secevent/risc/event-type/sessions-revoked |
必需:通过结束用户当前打开的会话来重新保护用户的帐号。 | |
https://schemas.openid.net/secevent/oauth/event-type/tokens-revoked |
必需:如果令牌适用于 Google 登录,请终止其当前打开的会话。此外,您还可以建议用户设置备用登录方法。 建议:如果令牌是用于访问其他 Google API,请删除您存储的用户的任何 OAuth 令牌。 |
|
https://schemas.openid.net/secevent/oauth/event-type/token-revoked |
如需了解令牌标识符,请参阅 OAuth 令牌标识符部分 |
必需:如果您存储了相应的刷新令牌,请将其删除,并在下次需要访问令牌时请求用户重新同意。 |
https://schemas.openid.net/secevent/risc/event-type/account-disabled |
reason=hijacking 、reason=bulk-account |
必需:如果帐号停用的原因是 建议:如果帐号停用的原因为 建议:如果未提供原因,请停用用户的 Google 登录功能,并使用与用户 Google 帐号关联的电子邮件地址(通常但不一定要是 Gmail 帐号)停用帐号恢复功能。 为用户提供备用登录方法。 |
https://schemas.openid.net/secevent/risc/event-type/account-enabled |
建议:为用户重新启用 Google 登录功能,并使用该用户的 Google 帐号电子邮件地址重新启用帐号恢复功能。 | |
https://schemas.openid.net/secevent/risc/event-type/account-purged |
建议:删除用户的帐号或为其提供其他登录方法。 | |
https://schemas.openid.net/secevent/risc/event-type/account-credential-change-required |
建议:注意您的服务是否存在可疑活动,并采取相应措施。 | |
https://schemas.openid.net/secevent/risc/event-type/verification |
状态=state | 建议:记录已收到测试令牌。 |
重复和错过的活动
跨帐号保护计划会尝试重新提交其认为尚未送达的事件。因此,有时您可能会多次收到同一事件。如果这可能会导致重复操作给用户带来不便,请考虑使用 jti
声明(事件的唯一标识符)来去除事件。Google Cloud Dataflow 等外部工具可帮助您执行去重数据流。
请注意,传送事件的重试次数有限,因此如果您的接收器长时间关闭,您可能会永久错过某些事件。
注册接收器
如需开始接收安全性事件,请使用 RISC API 注册接收器端点。对 RISC API 的调用必须附有授权令牌。
您只会收到针对应用用户的安全事件,因此您需要在 GCP 项目中配置 OAuth 权限请求页面,以执行下述步骤。
1. 生成授权令牌
如需为 RISC API 生成授权令牌,请创建包含以下声明的 JWT:
{ "iss": SERVICE_ACCOUNT_EMAIL, "sub": SERVICE_ACCOUNT_EMAIL, "aud": "https://risc.googleapis.com/google.identity.risc.v1beta.RiscManagementService", "iat": CURRENT_TIME, "exp": CURRENT_TIME + 3600 }
使用服务帐号的私钥为 JWT 签名,您可以在创建服务帐号密钥时下载的 JSON 文件中找到私钥。
例如:
Java
使用 java-jwt 和 Google 的身份验证库:
public static String makeBearerToken() {
String token = null;
try {
// Get signing key and client email address.
FileInputStream is = new FileInputStream("your-service-account-credentials.json");
ServiceAccountCredentials credentials =
(ServiceAccountCredentials) GoogleCredentials.fromStream(is);
PrivateKey privateKey = credentials.getPrivateKey();
String keyId = credentials.getPrivateKeyId();
String clientEmail = credentials.getClientEmail();
// Token must expire in exactly one hour.
Date issuedAt = new Date();
Date expiresAt = new Date(issuedAt.getTime() + 3600000);
// Create signed token.
Algorithm rsaKey = Algorithm.RSA256(null, (RSAPrivateKey) privateKey);
token = JWT.create()
.withIssuer(clientEmail)
.withSubject(clientEmail)
.withAudience("https://risc.googleapis.com/google.identity.risc.v1beta.RiscManagementService")
.withIssuedAt(issuedAt)
.withExpiresAt(expiresAt)
.withKeyId(keyId)
.sign(rsaKey);
} catch (ClassCastException e) {
// Credentials file doesn't contain a service account key.
} catch (IOException e) {
// Credentials file couldn't be loaded.
}
return token;
}
Python
import json
import time
import jwt # pip install pyjwt
def make_bearer_token(credentials_file):
with open(credentials_file) as service_json:
service_account = json.load(service_json)
issuer = service_account['client_email']
subject = service_account['client_email']
private_key_id = service_account['private_key_id']
private_key = service_account['private_key']
issued_at = int(time.time())
expires_at = issued_at + 3600
payload = {'iss': issuer,
'sub': subject,
'aud': 'https://risc.googleapis.com/google.identity.risc.v1beta.RiscManagementService',
'iat': issued_at,
'exp': expires_at}
encoded = jwt.encode(payload, private_key, algorithm='RS256',
headers={'kid': private_key_id})
return encoded
auth_token = make_bearer_token('your-service-account-credentials.json')
此授权令牌可用于进行 RISC API 调用,时长为一小时。令牌过期后,请生成一个新令牌以继续进行 RISC API 调用。
2. 调用 RISC 数据流配置 API
现在您已经有了授权令牌,可以使用 RISC API 配置项目的安全事件流了,包括注册接收器端点。
为此,请向 https://risc.googleapis.com/v1beta/stream:update
发出 HTTPS POST 请求,并指定您的接收器端点和您感兴趣的安全事件类型:
POST /v1beta/stream:update HTTP/1.1 Host: risc.googleapis.com Authorization: Bearer AUTH_TOKEN { "delivery": { "delivery_method": "https://schemas.openid.net/secevent/risc/delivery-method/push", "url": RECEIVER_ENDPOINT }, "events_requested": [ SECURITY_EVENT_TYPES ] }
例如:
Java
public static void configureEventStream(final String receiverEndpoint,
final List<String> eventsRequested,
String authToken) throws IOException {
ObjectMapper jsonMapper = new ObjectMapper();
String streamConfig = jsonMapper.writeValueAsString(new Object() {
public Object delivery = new Object() {
public String delivery_method =
"https://schemas.openid.net/secevent/risc/delivery-method/push";
public String url = receiverEndpoint;
};
public List<String> events_requested = eventsRequested;
});
HttpPost updateRequest = new HttpPost("https://risc.googleapis.com/v1beta/stream:update");
updateRequest.addHeader("Content-Type", "application/json");
updateRequest.addHeader("Authorization", "Bearer " + authToken);
updateRequest.setEntity(new StringEntity(streamConfig));
HttpResponse updateResponse = new DefaultHttpClient().execute(updateRequest);
Header[] responseContentTypeHeaders = updateResponse.getHeaders("Content-Type");
StatusLine responseStatus = updateResponse.getStatusLine();
int statusCode = responseStatus.getStatusCode();
HttpEntity entity = updateResponse.getEntity();
// Now handle response
}
// ...
configureEventStream(
"https://your-service.example.com/security-event-receiver",
Arrays.asList(
"https://schemas.openid.net/secevent/risc/event-type/account-credential-change-required",
"https://schemas.openid.net/secevent/risc/event-type/account-disabled"),
authToken);
Python
import requests
def configure_event_stream(auth_token, receiver_endpoint, events_requested):
stream_update_endpoint = 'https://risc.googleapis.com/v1beta/stream:update'
headers = {'Authorization': 'Bearer {}'.format(auth_token)}
stream_cfg = {'delivery': {'delivery_method': 'https://schemas.openid.net/secevent/risc/delivery-method/push',
'url': receiver_endpoint},
'events_requested': events_requested}
response = requests.post(stream_update_endpoint, json=stream_cfg, headers=headers)
response.raise_for_status() # Raise exception for unsuccessful requests
configure_event_stream(auth_token, 'https://your-service.example.com/security-event-receiver',
['https://schemas.openid.net/secevent/risc/event-type/account-credential-change-required',
'https://schemas.openid.net/secevent/risc/event-type/account-disabled'])
如果请求返回 HTTP 200,则表示事件流已成功配置,您的接收器端点应开始接收安全性事件令牌。下一部分将介绍如何测试数据流配置和端点以验证一切是否共同正常运行。
获取并更新您当前的数据流配置
以后,如果您想修改数据流配置,可以通过向 https://risc.googleapis.com/v1beta/stream
发出授权的 GET 请求来获取当前数据流配置,修改响应正文,然后将修改后的配置发布回 https://risc.googleapis.com/v1beta/stream:update
(如上所述)。
停止和恢复事件流
如果您需要停止来自 Google 的事件流,请在请求正文中通过 { "status": "disabled" }
向 https://risc.googleapis.com/v1beta/stream/status:update
发出已授权的 POST 请求。停用数据流后,Google 不会向您的端点发送事件,也不会在安全性事件发生时进行缓冲。如需重新启用事件流,请将 { "status": "enabled" }
发布到同一端点。
3. 可选:测试数据流配置
您可以通过事件流发送验证令牌,以验证数据流配置和接收器端点能否正常工作。此令牌可能包含唯一字符串,您可以使用该字符串来验证您的端点是否收到了端点。如需使用此流程,请务必在注册接收器时订阅 https://schemas.openid.net/secevent/risc/event-type/verification 事件类型。
如需请求验证令牌,请向 https://risc.googleapis.com/v1beta/stream:verify
发出已授权的 HTTPS POST 请求。在请求正文中,指定一些可识别的字符串:
{ "state": "ANYTHING" }
例如:
Java
public static void testEventStream(final String stateString,
String authToken) throws IOException {
ObjectMapper jsonMapper = new ObjectMapper();
String json = jsonMapper.writeValueAsString(new Object() {
public String state = stateString;
});
HttpPost updateRequest = new HttpPost("https://risc.googleapis.com/v1beta/stream:verify");
updateRequest.addHeader("Content-Type", "application/json");
updateRequest.addHeader("Authorization", "Bearer " + authToken);
updateRequest.setEntity(new StringEntity(json));
HttpResponse updateResponse = new DefaultHttpClient().execute(updateRequest);
Header[] responseContentTypeHeaders = updateResponse.getHeaders("Content-Type");
StatusLine responseStatus = updateResponse.getStatusLine();
int statusCode = responseStatus.getStatusCode();
HttpEntity entity = updateResponse.getEntity();
// Now handle response
}
// ...
testEventStream("Test token requested at " + new Date().toString(), authToken);
Python
import requests
import time
def test_event_stream(auth_token, nonce):
stream_verify_endpoint = 'https://risc.googleapis.com/v1beta/stream:verify'
headers = {'Authorization': 'Bearer {}'.format(auth_token)}
state = {'state': nonce}
response = requests.post(stream_verify_endpoint, json=state, headers=headers)
response.raise_for_status() # Raise exception for unsuccessful requests
test_event_stream(auth_token, 'Test token requested at {}'.format(time.ctime()))
如果请求成功,系统会将验证令牌发送到您注册的端点。举例来说,如果您的端点通过简单地记录验证令牌来处理令牌,您可以检查日志以确认是否收到了令牌。
错误代码参考
RISC API 可能会返回以下错误:
错误代码 | 错误消息 | 建议采取的措施 |
---|---|---|
400 | 数据流配置必须包含 $fieldname 字段。 | 您对 https://risc.googleapis.com/v1beta/stream:update 端点的请求无效或无法解析。请在请求中包含 $fieldname。 |
401 | 未获授权。 | 授权失败。请确保您在请求中附上了 授权令牌,并且该令牌有效且未过期。 |
403 | 递送端点必须是 HTTPS 网址。 | 您的传送端点(即您希望传送 RISC 事件的端点)必须是 HTTPS。我们不会向 HTTP 网址发送 RISC 事件。 |
403 | 现有的视频流配置没有符合 RISC 的规范提交方式。 | 您的 Google Cloud 项目必须已有 RISC 配置。如果您使用的是 Firebase 并且启用了 Google 登录,则 Firebase 将管理项目的 RISC;您将无法创建自定义配置。如果您没有为您的 Firebase 项目使用 Google 登录功能,请停用该功能,然后一小时后再次尝试更新。 |
403 | 找不到项目。 | 确保您针对正确的项目使用的是正确的服务帐号。您使用的可能是与已删除项目关联的服务帐号。了解 如何查看与项目关联的所有服务帐号。 |
403 | 服务帐号需要有权访问您的 RISC 配置 | 转到项目的 API Console ,然后按照这些说明为调用项目的服务帐号分配“RISC Configuration Admin”角色 (roles/riscconfigs.admin )。
|
403 | 流管理 API 只能由服务帐号调用。 | 如需了解详情,请参阅如何使用服务帐号调用 Google API。 |
403 | 交付端点不属于您的项目的任何网域。 | 每个项目都有一组已获授权的网域。如果您的传送端点(即您期望用于接收 RISC 事件的端点)未托管在其中某个端点上,您需要将相应端点的网域添加到其中。 |
403 | 要使用此 API,您的项目必须至少配置一个 OAuth 客户端。 | 只有当您构建支持 Google 登录的应用时,RISC 才能正常工作。此连接需要 OAuth 客户端。如果您的项目没有 OAuth 客户端,则 RISC 可能对您没用。详细了解 Google 如何为我们的 API 使用 OAuth。 |
403 |
状态不受支持。 状态无效。 |
目前,我们仅支持“enabled ”和“disabled ”数据流状态。 |
404 |
项目没有 RISC 配置。 项目尚无 RISC 配置,无法更新状态。 |
调用 https://risc.googleapis.com/v1beta/stream:update 端点以创建新的数据流配置。 |
4XX/5XX | 无法更新状态。 | 如需了解详情,请查看详细的错误消息。 |
访问令牌范围
如果您决定使用访问令牌向 RISC API 进行身份验证,那么您的应用必须请求以下范围:
端点 | 范围 |
---|---|
https://risc.googleapis.com/v1beta/stream/status |
https://www.googleapis.com/auth/risc.status.readonly
或https://www.googleapis.com/auth/risc.status.readwrite |
https://risc.googleapis.com/v1beta/stream/status:update |
https://www.googleapis.com/auth/risc.status.readwrite |
https://risc.googleapis.com/v1beta/stream |
https://www.googleapis.com/auth/risc.configuration.readonly
或https://www.googleapis.com/auth/risc.configuration.readwrite
|
https://risc.googleapis.com/v1beta/stream:update |
https://www.googleapis.com/auth/risc.configuration.readwrite |
https://risc.googleapis.com/v1beta/stream:verify |
https://www.googleapis.com/auth/risc.verify |
需要帮助?
首先,查看我们的错误代码参考文档。如果您仍有疑问,请在 Stack Overflow 上发布带有 #SecEvents 标记的问题。