如果您的应用允许用户使用 Google 账号登录其账号,您可以监听并响应跨账号保护服务提供的安全事件通知,从而提高这些共享用户账号的安全性。
这些通知会提醒您用户的 Google 账号发生重大变化,这通常也会对用户在您应用中的账号造成安全影响。例如,如果用户的 Google 账号遭到入侵,可能会导致用户在您应用中的账号通过电子邮件账号恢复功能或使用单点登录功能而遭到入侵。
为了帮助您降低此类事件的潜在风险,Google 会向您的服务对象发送名为安全事件令牌的令牌。这些令牌会披露的信息非常少,仅包括安全事件的类型、发生时间以及受影响用户的标识符,但您可以使用这些信息采取相应的响应措施。例如,如果用户的 Google 账号遭到入侵,您可以暂时为该用户停用“使用 Google 账号登录”功能,并阻止将账号恢复电子邮件发送到用户的 Gmail 地址。
跨账号保护功能基于 OpenID Foundation 开发的 RISC 标准。
概览
如需在您的应用或服务中使用跨账号保护功能,您必须完成以下任务:
在 中设置项目。
创建事件接收器端点,Google 会向该端点发送安全事件令牌。此端点负责验证收到的令牌,然后按照您选择的任何方式响应安全事件。
请向 Google 注册您的端点,以便开始接收安全事件令牌。
前提条件
您只会收到已向您的服务授予访问其个人资料信息或电子邮件地址权限的 Google 用户的安全事件令牌。您可以通过请求 profile
或 email
范围来获取此权限。较新的 Google 登录或旧版 Google 登录 SDK 默认会请求这些权限范围,但如果您不使用默认设置,或者直接访问 Google 的 OpenID Connect 端点,请确保您至少请求其中一个权限范围。
在 中设置项目
您必须先创建一个服务账号,然后在 项目中启用 RISC API,然后才能开始接收安全事件令牌。您必须使用在应用中访问 Google 服务(例如 Google 登录)时所用的 项目。
如需创建服务账号,请执行以下操作:
点击创建凭据 > 服务账号。
按照这些说明创建具有 RISC Configuration Admin 角色 (
roles/riscconfigs.admin
) 的新服务账号。为新创建的服务账号创建密钥。选择 JSON 密钥类型,然后点击创建。创建密钥后,您将下载包含服务账号凭据的 JSON 文件。请将此文件保存在安全的位置,同时也要确保事件接收器端点可以访问该文件。
在项目的“凭据”页面上,请记下您用于“使用 Google 账号登录”或“Google 登录(旧版)”的客户端 ID。通常,您为自己支持的每个平台都拥有一个客户端 ID。您需要这些客户端 ID 来验证安全事件令牌,如下一部分所述。
如需启用 RISC API,请执行以下操作:
在中打开 RISC API 页面。确保您用于访问 Google 服务的项目仍处于选中状态。
阅读 RISC 条款,确保您了解相关要求。
如果您要为组织拥有的项目启用该 API,请确保您有权将贵组织绑定到 RISC 条款。
仅当您同意 RISC 条款时,才应点击启用。
创建事件接收器端点
如需接收来自 Google 的安全事件通知,您需要创建一个用于处理 HTTPS POST 请求的 HTTPS 端点。您注册此端点(见下文)后,Google 将开始向该端点发布名为安全事件令牌的经过加密签名的字符串。安全事件令牌是经过签名的 JWT,其中包含与单个安全相关事件有关的信息。
对于您在端点收到的每个安全事件令牌,请先验证并解码令牌,然后根据您的服务相应地处理安全事件。请务必在解码之前验证事件令牌,以防范恶意攻击。以下部分介绍了这些任务:
1. 解码和验证安全事件令牌
由于安全事件令牌是一种特定类型的 JWT,因此您可以使用任何 JWT 库(例如 jwt.io 上列出的库)对其进行解码和验证。无论您使用哪个库,令牌验证代码都必须执行以下操作:
- 从 Google 的 RISC 配置文档(网址为
https://accounts.google.com/.well-known/risc-configuration
)中获取跨账号保护发行商标识符 (issuer
) 和签名密钥证书 URI (jwks_uri
)。 - 使用您选择的 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:
// ?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:
# ?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-credential-change-required |
建议:留意您服务中的可疑活动,并采取适当措施。 | |
https://schemas.openid.net/secevent/risc/event-type/verification |
state=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 请求以获取当前数据流配置,修改响应正文,然后将修改后的配置 POST 回 https://risc.googleapis.com/v1beta/stream:update
,如上所述。
停止和恢复事件流
如果您需要停止从 Google 获取事件流,请向 https://risc.googleapis.com/v1beta/stream/status:update
发出已获授权的 POST 请求,并在请求正文中包含 { "status": "disabled" }
。在数据流停用期间,Google 不会向您的端点发送事件,也不会缓冲发生的安全事件。如需重新启用事件流,请将 { "status": "enabled" }
以 POST 方式发送到同一端点。
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。我们不会将 RISC 事件发送到 HTTP 网址。 |
403 | 现有数据流配置不符合规范的 RISC 提交方法。 | 您的 Google Cloud 项目必须已具有 RISC 配置。如果您使用的是 Firebase 且已启用 Google 登录,则 Firebase 将为您的项目管理 RISC;您将无法创建自定义配置。如果您的 Firebase 项目未使用 Google 登录,请停用该功能,然后在一小时后尝试再次更新。 |
403 | 找不到项目。 | 确保您为正确的项目使用了正确的服务账号。您可能在使用与已删除的项目关联的服务账号。了解 如何查看与项目关联的所有服务账号。 |
403 | 服务账号需要有权访问您的 RISC 配置 | 前往项目的 ,然后按照这些说明为向项目发出调用的服务账号分配“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 标签。