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

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

这些通知会提醒您用户的Google帐户发生重大更改,这些更改通常还可能会影响他们在您的应用中使用的帐户的安全性。例如,如果某个用户的Google帐户被劫持,则有可能通过恢复电子邮件帐户或使用单点登录来破坏该用户帐户与您的应用程序。

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

跨帐户保护基于由OpenID Foundation开发的RISC标准

概述

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

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

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

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

先决条件

您只会收到已授予您访问其个人资料信息或电子邮件地址的服务的Google用户的安全事件令牌。您可以通过请求profileemail范围来获得此权限。 Google登录SDK默认情况下会请求这些范围,但是,如果您不使用默认设置,或者直接访问Google的OpenID Connect端点,请确保您至少请求了这些范围之一。

在API Console中设置一个项目

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

要创建服务帐户:

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

  2. 单击创建凭证>服务帐户密钥

  3. 创建一个具有编辑者角色的新服务帐户。

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

当您进入项目的“凭据”页面时,还请注意用于Google登录的客户端ID。通常,您为所支持的每个平台都有一个客户端ID。您将需要这些客户端ID来验证安全事件令牌,如下一节所述。

要启用RISC API:

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

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

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

  3. 仅在您同意RISC条款的情况下,单击“启用”

创建一个事件接收者端点

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

对于在端点接收到的每个安全事件令牌,首先验证并解码令牌,然后根据您的服务处理安全事件。以下各节描述了这些任务:

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

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

  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声明指定了该事件涉及的用户,以及到可能可用的事件的任何其他详细信息。

subject声明使用其唯一的Google帐户ID( sub )来标识特定用户。此ID与Google Sign-in产生的ID令牌中包含的标识符相同。当subject_type的索赔没有id_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声明(事件的唯一标识符)来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客户端。仅当您构建支持Google登录的应用程序时,RISC才有效。此连接需要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上。