Как реализовать подтверждение адреса электронной почты

В этом руководстве описано, как реализовать получение подтвержденного адреса электронной почты с помощью Digital Credentials Verifier API через запрос OpenID for Verifiable Presentations (OpenID4VP).

Добавьте зависимости

В файле build.gradle своего приложения добавьте следующие зависимости для Менеджера учетных данных:

dependencies {
    implementation("androidx.credentials:credentials:1.7.0-alpha02")
    implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02")
}

Инициализация Менеджера учетных данных

Используйте контекст приложения или действия, чтобы создать объект CredentialManager.

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)

Как создать запрос цифрового удостоверения

Чтобы запросить письмо с подтверждением, создайте объект GetCredentialRequest, содержащий GetDigitalCredentialOption. Для этого варианта требуется строка requestJson, отформатированная как запрос OpenID for Verifiable Presentations (OpenID4VP).

JSON-запрос OpenID4VP должен иметь определенную структуру. Текущие поставщики поддерживают структуру JSON с внешней оболочкой "digital": {"requests": [...]}.

    val nonce = generateSecureRandomNonce()

    // This request follows the OpenID4VP spec
    val openId4vpRequest = """
{
  "requests": [
    {
      "protocol": "openid4vp-v1-unsigned",
      "data": {
        "response_type": "vp_token",
        "response_mode": "dc_api",
        "nonce": "$nonce",
        "dcql_query": {
          "credentials": [
            {
              "id": "user_info_query",
              "format": "dc+sd-jwt",
               "meta": {
                  "vct_values": ["UserInfoCredential"]
               },
              "claims": [
                {"path": ["email"]},
                {"path": ["name"]},  
                {"path": ["given_name"]},
                {"path": ["family_name"]},
                {"path": ["picture"]},
                {"path": ["hd"]},
                {"path": ["email_verified"]}
              ]
            }
          ]
        }
      }
    }
  ]
}
"""

    val getDigitalCredentialOption = GetDigitalCredentialOption(requestJson = openId4vpRequest)
    val request = GetCredentialRequest(listOf(getDigitalCredentialOption))

Запрос содержит следующую ключевую информацию:

  • DCQL-запрос. dcql_query указывает тип учетных данных и запрашиваемые утверждения (email_verified). Вы можете запросить другие утверждения, чтобы определить уровень проверки. Вот несколько примеров:
    • email_verified – логическое значение в ответе, которое указывает, подтвержден ли адрес электронной почты.
    • hd (припаркованный домен) – в ответе это поле пустое.
  • nonce – уникальное криптографически защищенное случайное значение, которое генерируется для каждого запроса. Это критически важно для безопасности, поскольку предотвращает атаки повторного воспроизведения.
  • UserInfoCredential – это значение подразумевает определенный тип цифрового удостоверения, содержащего атрибуты пользователя. Это необходимо, чтобы отличить подтверждение адреса электронной почты от других сценариев использования.

Затем оберните JSON-код openId4vpRequest в GetDigitalCredentialOption, создайте GetCredentialRequest и вызовите getCredential().

Покажите запрос пользователю

Покажите пользователю запрос, используя встроенный интерфейс Менеджера учетных данных.

try {
    // Requesting Digital Credential from user...
    val result = credentialManager.getCredential(activity, request)

    when (val credential = result.credential) {
        is DigitalCredential -> {
            val responseJsonString = credential.credentialJson

            // Successfully received digital credential response.

            // Next, parse this response and send it to your server.
            // ...
        }

        else -> {
            // handle Unexpected State() - Up to the developer
        }
    }
} catch (e: Exception) {
    // handle exceptions - Up to the developer
}

Обработка ответа на стороне клиента

Получив ответ, вы можете выполнить предварительный синтаксический анализ на клиенте. Это полезно для немедленного обновления пользовательского интерфейса, например, путем отображения имени пользователя.

Приведенный ниже код извлекает необработанный веб-токен JSON с выборочным раскрытием (SD-JWT) и использует вспомогательный инструмент для декодирования утверждений.

// 1. Parse the outer JSON wrapper to get the `vp_token`
val responseData = JSONObject(responseJsonString)
val vpToken = responseData.getJSONObject("vp_token")

// 2. Extract the raw SD-JWT string
val credentialId = vpToken.keys().next()
val rawSdJwt = vpToken.getJSONArray(credentialId).getString(0)

// 3. Use your parser to get the verified claims
// Server-side validation/parsing is highly recommended.

// Assumes a local parser like the one in our SdJwtParser.kt sample
val claims = SdJwtParser.parse(rawSdJwt)
Log.d("TAG", "Parsed Claims: ${claims.toString(2)}")

// 4. Create your VerifiedUserInfo object with REAL data
val userInfo = VerifiedUserInfo(
    email = claims.getString("email"),
    displayName = claims.optString("name", claims.getString("email"))
)

Обработка ответного сообщения

Credential Manager API вернет ответ DigitalCredential.

Ниже показано, как выглядит необработанный responseJsonString и как выглядят утверждения после синтаксического анализа внутреннего SD-JWT, где вы получаете дополнительные метаданные вместе с подтвержденным адресом электронной почты:

/*
// Example of the raw JSON response from credential.credentialJson:
{
  "vp_token": {
    // This key matches the 'id' you set in your dcql_query
    "user_info_query": [
      // The SD-JWT string (Issuer JWT ~ Disclosures ~ Key Binding JWT)
      "eyJhbGciOiJ...~WyI...IiwgImVtYWlsIiwgInVzZXJAZXhhbXBsZS5jb20iXQ~...~eyJhbGciOiJ..."
    ]
  }
}

// Example of the parsed and verified claims from the SD-JWT on your server:
{
  "cnf": {
    "jwk": {..}
  },
  "exp": 1775688222,
  "iat": 1775083422,
  "iss": "https://verifiablecredentials-pa.googleapis.com",
  "vct": "UserInfoCredential",
  "email": "jane.doe.246745@gmail.com",
  "email_verified": true,
  "given_name": "Jane",
  "family_name": "Doe",
  "name": "Jane Doe",
  "picture": "http://example.com/janedoe/me.jpg",
  "hd": ""
}
 */

Проверка на стороне сервера при создании аккаунта

Поскольку полученный адрес электронной почты криптографически подтвержден, вы можете пропустить этап проверки одноразового кода электронной почты, что значительно снизит трудности при регистрации и потенциально увеличит конверсию. Эту процедуру лучше всего выполнять на сервере. Клиент отправляет необработанный ответ (содержащий vp_token) и исходный одноразовый код новой конечной точке сервера.

Для проверки приложение должно отправить полный объект responseJsonString на ваш сервер для криптографической валидации, прежде чем создавать аккаунт или выполнять вход пользователя.

Цифровой сертификат обеспечивает два уровня проверки сервера:

  • Подлинность данных. Проверка URL эмитента (iss) и подписи SD-JWT подтверждает, что эти данные были выданы доверенным органом.
  • Идентификация предъявителя. Проверка поля cnf и подписи Key Binding (kb) подтверждает, что удостоверение передается с того же устройства, на которое оно было выдано. Это предотвращает перехват или использование удостоверения на другом устройстве.

Проверка на сервере должна обеспечивать следующее:

  • Проверьте издателя. Убедитесь, что поле iss (издатель) совпадает с https://verifiablecredentials-pa.googleapis.com.
  • Проверка подписи. Проверьте подпись SD-JWT с помощью открытых ключей (JWK), доступных по адресу https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks.

Чтобы обеспечить полную безопасность, также проверяйте nonce. Это поможет предотвратить атаки повторного воспроизведения.

Выполнив эти шаги, ваш сервер сможет проверить как подлинность данных, так и личность предъявителя, гарантируя, что учетные данные не были перехвачены или подделаны до создания нового аккаунта.

try {
    // Send the raw credential response and the original nonce to your server.
    // Your server must validate the response. createAccountWithVerifiedCredentials
    // is a custom implementation per each RP for server side verification and account creation.
    val serverResponse = createAccountWithVerifiedCredentials(responseJsonString, nonce)

    // Server returns the new account info (e.g., email, name)
    val claims = JSONObject(serverResponse.json)

    val userInfo = VerifiedUserInfo(
        email = claims.getString("email"),
        displayName = claims.optString("name", claims.getString("email"))
    )

    // handle response - Up to the developer
} catch (e: Exception) {
    // handle exceptions - Up to the developer
}

Создание ключа доступа

После инициализации аккаунта настоятельно рекомендуется сразу же создать для него ключ доступа. Это безопасный способ входа без пароля. Этот процесс идентичен стандартной регистрации ключа доступа.

Поддержка WebView

Чтобы процесс работал в WebView, разработчикам нужно реализовать мост JavaScript (JS Bridge) для передачи данных. Этот мост позволяет объекту WebView отправлять сигнал нативному приложению, которое затем может выполнить фактический вызов Credential Manager API.

Дополнительная информация