使用跨帐户保护保护用户帐户

如果您的应用允许用户使用 Google 登录他们的帐户,您可以通过收听和响应跨帐户保护服务提供的安全事件通知来提高这些共享用户帐户的安全性。

这些通知会提醒您用户的 Google 帐户发生重大变化,这些变化通常也会对他们在您的应用中的帐户产生安全影响。例如,如果用户的 Google 帐户被劫持,则可能会通过电子邮件帐户恢复或使用单点登录导致用户帐户在您的应用中遭到破坏。

为了帮助您降低此类事件的潜在风险,Google 会向您发送称为安全事件令牌的服务对象。这些令牌公开的信息很少——只是安全事件的类型和发生时间,以及受影响用户的标识符——但您可以使用它们来采取适当的行动作为响应。例如,如果用户的 Google 帐户遭到入侵,您可以暂时为该用户禁用通过 Google 登录,并阻止将帐户恢复电子邮件发送到用户的 Gmail 地址。

跨账户保护基于 OpenID 基金会开发的RISC 标准

概述

要将跨账户保护与您的应用或服务一起使用,您必须完成以下任务:

  1. 在 API Console中设置您的项目。

  2. 创建一个事件接收器端点,Google 将向其发送安全事件令牌。该端点负责验证它接收到的令牌,然后以您选择的任何方式响应安全事件。

  3. 向 Google 注册您的端点以开始接收安全事件令牌。

先决条件

您只会收到授予您服务权限以访问其个人资料信息或电子邮件地址的 Google 用户的安全事件令牌。您可以通过请求profileemail范围来获得此权限。较新的Sign In With Google或旧版Google Sign-in SDK 默认会请求这些范围,但如果您不使用默认设置,或者如果您直接访问 Google 的OpenID Connect 端点,请确保您至少请求其中一个范围。

在 API Console中建立一个项目

在开始接收安全事件令牌之前,您必须创建一个服务帐户并在您的API Console 项目中启用 RISC API。您必须在应用中使用与访问 Google 服务(例如 Google 登录)相同的API Console 项目。

要创建服务帐户:

  1. 打开API ConsoleCredentials page 。出现提示时,选择您在应用中用于访问 Google 服务的API Console项目。

  2. 单击创建凭据 > 服务帐户

  3. 创建一个具有 Editor 角色的新服务帐号。

  4. 为您新创建的服务帐户创建一个密钥。选择 JSON 密钥类型,然后单击Create 。创建密钥后,您将下载一个 JSON 文件,其中包含您的服务帐户凭据。将此文件保存在安全的地方,但您的事件接收器端点也可以访问。

当您在项目的凭据页面上时,还要记下您用于使用 Google 登录或 Google 登录(旧版)的客户端 ID。通常,您支持的每个平台都有一个客户端 ID。您将需要这些客户端 ID 来验证安全事件令牌,如下一节所述。

要启用 RISC API:

  1. 打开API Console中的RISC API 页面。确保您用于访问 Google 服务的项目仍处于选中状态。

  2. 阅读RISC 条款并确保您了解要求。

    如果您为组织拥有的项目启用 API,请确保您有权将您的组织绑定到 RISC 条款。

  3. 仅当您同意 RISC 条款时才单击启用

创建事件接收器端点

要从 Google 接收安全事件通知,您需要创建一个 HTTPS 端点来处理 HTTPS POST 请求。在您注册此端点(见下文)后,Google 将开始向端点发布称为安全事件令牌的加密签名字符串。安全事件令牌是经过签名的 JWT,其中包含有关单个安全相关事件的信息。

对于您在端点收到的每个安全事件令牌,首先验证和解码令牌,然后根据您的服务处理安全事件。在解码之前验证事件令牌以防止恶意攻击者的恶意攻击至关重要。以下部分描述了这些任务:

1. 解码并验证安全事件令牌

由于安全事件令牌是一种特定类型的 JWT,因此您可以使用任何 JWT 库(例如jwt.io上列出的库)来解码和验证它们。无论您使用哪个库,您的令牌验证代码都必须执行以下操作:

  1. 从 Google 的 RISC 配置文档中获取跨账户保护颁发者标识符 ( issuer ) 和签名密钥证书 URI ( jwks_uri ),您可以在https://accounts.google.com/.well-known/risc-configuration找到该文档。
  2. 使用您选择的 JWT 库,从安全事件令牌的标头中获取签名密钥 ID。
  3. 从 Google 的签名密钥证书文档中,使用您在上一步中获得的密钥 ID 获取公钥。如果文档不包含具有您要查找的 ID 的密钥,则安全事件令牌可能无效,并且您的端点应返回 HTTP 错误 400。
  4. 使用您选择的 JWT 库,验证以下内容:
    • 使用您在上一步中获得的公钥对安全事件令牌进行签名。
    • 令牌的aud声明是您应用的客户端 ID 之一。
    • 令牌的iss声明与您从 RISC 发现文档中获得的颁发者标识符相匹配。请注意,您不需要验证令牌的过期时间 ( exp ),因为安全事件令牌代表历史事件,因此不会过期。

例如:

爪哇

使用java-jwtjwks-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"
    }
  }
}

issaud声明指示令牌的颁发者(Google)和令牌的预期接收者(您的服务)。您在上一步中验证了这些声明。

jti声明是标识单个安全事件的字符串,并且对于流是唯一的。您可以使用此标识符来跟踪您收到了哪些安全事件。

events声明包含有关令牌所代表的安全事件的信息。此声明是从事件类型标识符到subject声明的映射,主题声明指定了此事件所涉及的用户,以及有关可能可用的事件的任何其他详细信息。

subject声明使用用户的唯一 Google 帐户 ID ( sub ) 标识特定用户。此 Google 帐户 ID 与由较新的 Sign In With Google( JavascriptHTML )库、旧版Google Sign-in库或OpenID Connect颁发的 JWT ID 令牌中包含的标识符( sub )相同。当声明的subject_typeid_token_claims时,它还可能包含一个包含用户电子邮件地址的email字段。

使用events声明中的信息对指定用户帐户的事件类型采取适当的操作。

支持的事件类型

跨账户保护支持以下类型的安全事件:

事件类型属性如何回应
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/risc/event-type/account-disabled reason=hijacking
reason=bulk-account

必需:如果帐户被禁用的原因是hijacking ,请通过结束当前打开的会话来重新保护用户的帐户。

建议:如果帐户被禁用的原因是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-jwtGoogle 的 auth 库

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
  ]
}

例如:

爪哇

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 不会向您的端点发送事件,并且不会在安全事件发生时缓冲它们。要重新启用事件流,请 POST { "status": "enabled" }到同一端点。

3. 可选:测试您的流配置

您可以通过事件流发送验证令牌来验证您的流配置和接收器端点是否正常工作。此令牌可以包含一个唯一的字符串,您可以使用该字符串来验证您的端点是否收到了令牌。

要请求验证令牌,请向https://risc.googleapis.com/v1beta/stream:verify发出授权的 HTTPS POST 请求。在请求的正文中,指定一些标识字符串:

{
  "state": "ANYTHING"
}

例如:

爪哇

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 URL。您的交付端点(即您希望将 RISC 事件交付到的端点)必须是 HTTPS。我们不会将 RISC 事件发送到 HTTP URL。
403现有的流配置没有符合规范的 RISC 交付方法。您的 Google Cloud 项目必须已经具有 RISC 配置。如果您使用 Firebase 并启用了 Google 登录,那么 Firebase 将为您的项目管理 RISC;您将无法创建自定义配置。如果您的 Firebase 项目没有使用 Google 登录,请禁用它,然后在一小时后再次尝试更新。
403找不到项目。确保您为正确的项目使用正确的服务帐户。您可能正在使用与已删除项目关联的服务帐户。了解如何查看与项目关联的所有服务帐户
403服务帐号必须在您的项目中拥有编辑权限。转到您项目的 Google Cloud Platform 控制台,并按照这些说明向进行调用的服务帐户授予对您的项目的编辑/所有者权限。
403流管理 API 只能由服务帐户调用。以下是有关如何使用服务帐户调用 Google API的更多信息。
403交付端点不属于您项目的任何域。每个项目都有一组授权域。如果您的交付端点(即您希望将 RISC 事件交付到的端点)未托管在其中一个上,我们要求您将端点的域添加到该集合中。
403要使用此 API,您的项目必须至少配置一个 OAuth 客户端。 RISC 仅在您构建支持Google 登录的应用程序时才有效。此连接需要 OAuth 客户端。如果您的项目没有 OAuth 客户端,那么 RISC 可能对您没有用处。详细了解Google 将 OAuth 用于我们的 API
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.readonlyhttps://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.readonlyhttps://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

需要帮忙?

首先,查看我们的错误代码参考部分。如果您仍有疑问,请使用#SecEvents标签将它们发布到 Stack Overflow。