Google致力於提高黑人社區的種族平等。 怎麼看。

使用跨帳戶保護保護用戶帳戶

如果您的應用允許用戶使用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聲明(事件的唯一標識符)來取消重複事件。有一些外部工具,例如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上。