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

如果您的應用允許用戶使用 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。