使用跨帐号保护功能保护用户帐号

如果您的应用允许用户使用 Google 账号登录,您可以改进 这些共享用户通过监听和响应 跨账号保护服务提供的安全性事件通知。

这些通知旨在提醒您,您的 Google 账号发生重大变化, 这往往也会影响其账号的安全 。例如,如果用户的 Google 账号被盗用, 可能会导致用户通过电子邮件盗用您应用的账号 账号恢复或单点登录。

为帮助您降低此类事件的潜在风险,Google 会将您的 称为安全性事件令牌的服务对象这些令牌所暴露的 信息——安全事件的类型、事件发生时间、 受影响用户的标识符,但您可以使用这些标识符来 采取适当的行动例如,如果用户的 Google 账号 您可以暂时停用该用户的“使用 Google 账号登录”功能,并 阻止将账号恢复电子邮件发送到用户的 Gmail 地址。

跨账号保护以 RISC 标准制定, OpenID Foundation。

概览

若要在您的应用或服务中使用跨账号保护功能,您必须完成 以下任务:

  1. 在 中设置您的项目。

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

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

前提条件

对于已授予您的 服务权限,以访问其个人资料信息或电子邮件地址。您 可通过请求 profileemail 范围来获取此权限。较新的 使用 Google 账号登录或旧版 Google 登录 SDK 会默认请求这些范围,但 如果您不使用默认设置,或者访问 Google 的 OpenID 直接连接端点,确保 您至少要申请其中一个范围。

在 中设置项目

您必须先创建服务,然后才能开始接收安全性事件令牌 并在您的 项目中。您必须使用 您用于访问的项目 应用中的 Google 服务,例如 Google 登录。

如需创建服务账号,请执行以下操作:

  1. 打开 。出现提示时,选择 项目。

  2. 点击创建凭据 > 服务账号

  3. 创建拥有 RISC Configuration Admin 角色的新服务账号 (roles/riscconfigs.admin) 关注 这些说明

  4. 为新创建的服务账号创建密钥。选择 JSON 密钥 类型,然后点击创建。密钥创建好后 您将下载一个包含您的服务账号的 JSON 文件 凭据。请将此文件保存在安全的地方,并确保您的 事件接收器端点。

在项目的“凭据”页面上,也要记下客户端 您用于“使用 Google 账号登录”或“Google 登录”(旧版)的 ID。通常,每个 支持的平台您需要使用这些客户端 ID 来验证安全性事件 令牌,如下一部分所述。

如需启用 RISC API,请执行以下操作:

  1. 打开 RISC API 页面 。确保您使用的是 访问 Google 服务的权限仍处于选中状态。

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

    如果您要为组织拥有的项目启用此 API,请确保 您有权代表贵组织遵守 RISC 条款。

  3. 请仅在您同意 RISC 条款的情况下点击启用

创建事件接收器端点

如需接收来自 Google 的安全事件通知,请创建 HTTPS 端点 用于处理 HTTPS POST 请求。注册此端点后(见下文), Google 将开始发布称为“安全性事件”的经过加密签名的字符串 将令牌传递给端点。安全性事件令牌是已签名的 JWT,其中包含 提供关于单个安全相关事件的信息。

对于您在端点收到的每个安全事件令牌,首先验证并 解码令牌,然后根据您的需要处理 服务。解码之前必须验证事件令牌,以防 来自作恶方的恶意攻击。以下各部分介绍了这些任务:

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

由于安全性事件令牌是一种特定类型的 JWT,您可以使用 JWT 库(例如 jwt.io 中列出的库)来解码和 验证它们。无论您使用哪种库,您的令牌验证码都必须 以下:

  1. 获取跨账号保护颁发者标识符 (issuer) 和签名密钥 Google 的 RISC 配置文档中的证书 URI (jwks_uri), 您可在 https://accounts.google.com/.well-known/risc-configuration
  2. 使用您选择的 JWT 库,从标头中获取签名密钥 ID 安全事件令牌的名称
  3. 从 Google 的签名密钥证书文档中,使用 您在上一步中获得的密钥 ID。如果文档不包含键 与您要查找的 ID 相关联,那么安全性事件令牌 无效,端点应返回 HTTP 错误 400。
  4. 使用您选择的 JWT 库,验证以下内容: <ph type="x-smartling-placeholder">
      </ph>
    • 该安全事件令牌使用您在 上一步。
    • 令牌的 aud 声明是您的应用之一客户端 ID
    • 令牌的 iss 声明与您从中获得的发卡机构标识符一致 RISC 发现文档。 请注意,您无需验证令牌的到期日期 (exp),因为 安全性事件令牌代表历史事件,因此不会过期。

例如:

Java

使用 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:
                // ?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"
    }
  }
}

issaud 声明指示令牌的颁发者 (Google) 和 令牌的目标收件人(您的服务)。你已在 上一步。

jti 声明是一个标识单个安全性事件的字符串, 是数据流独有的您可以使用此标识符来跟踪哪些安全性事件 。

events 声明包含有关令牌安全性事件的信息 代表什么。此声明是从事件类型标识符到 subject 的映射 声明,该声明指定了此事件所涉及的用户。 有关该活动的详细信息。

subject 声明通过用户的唯一 Google 代码来识别特定用户 账号 ID (sub)。此 Google 账号 ID 与文件中包含的标识符 (sub) 相同 包含在新版“使用 Google 账号登录”(JavaScriptHTML) 库、旧版 Google 登录库,或 OpenID Connect。当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

必需:如果账号被停用的原因 hijacking,请通过终止其 当前打开的会话。

建议:如果账号被停用的原因 bulk-account,分析用户在服务中的活动,以及 确定适当的后续操作。

建议:如果未提供原因,请为 使用与 关联的电子邮件地址停用账号恢复, 用户的 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 必须附带授权令牌。

您只会收到应用用户的安全事件,因此您需要配置 OAuth 权限请求页面 先在 GCP 项目中完成下述步骤。

1. 生成授权令牌

要为 RISC API 生成授权令牌,请使用 以下声明:

{
  "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-jwtGoogle 的身份验证库

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 的事件流,请执行经过授权的 POST 向https://risc.googleapis.com/v1beta/stream/status:update发出的请求:{ "status": "disabled" } 。停用数据流后,Google 不会发送事件 并且不会在安全事件发生时缓冲事件。接收者 重新启用事件流,即向同一端点 POST { "status": "enabled" }

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

您可以验证数据流配置和接收器端点是否正常运行 通过事件流发送验证令牌,将验证令牌正确组合在一起。 此令牌可以包含唯一字符串,该字符串可用于验证 您的端点收到了一个令牌。要使用此流程,请务必 订阅 https://schemas.openid.net/secevent/risc/event-type/verification 事件类型注册接收器

要请求验证令牌,请向以下用户发送经过授权的 HTTPS POST 请求: https://risc.googleapis.com/v1beta/stream:verify。在请求的正文中,指定 标识字符串:

{
  "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 现有数据流配置没有符合规范的传送 方法。 您的 Google Cloud 项目必须已拥有 RISC 配置。如果 并且启用了 Google 登录,则 Firebase 会 管理项目的 RISC;您将无法创建自定义 配置。如果您未对 Firebase 项目使用 Google 登录, 请先将其停用,然后在一小时后再次尝试更新。
403 找不到项目。 请确保您为正确的服务账号 项目。您使用的可能是与已删除的 项目。了解 <ph type="x-smartling-placeholder"></ph> 了解如何查看与项目关联的所有服务账号
403 服务账号需要获得访问您的 RISC 的权限 配置 前往项目的 分配“RISC Configuration Admin”角色 (roles/riscconfigs.admin) 发送到通过 Cloud Storage 调用您的项目的服务账号, 正在关注 这些说明
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

需要帮助吗?

首先,请查看我们的错误代码参考部分。如果您仍然 有疑问,请将问题发布在 Stack Overflow 上,并附上 #SecEvents 标记前面。