Implementar a verificação de e-mail com a API Digital Credentials

Este guia descreve como implementar a recuperação de e-mail verificado usando a API Digital Credentials Verifier por meio de uma solicitação do OpenID for Verifiable Presentations (OpenID4VP).

Adicionar dependências

No arquivo build.gradle do app, adicione as seguintes dependências do Credential Manager:

Kotlin

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

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.7.0-alpha01"
    implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha01"
}

Inicializar o Credential Manager

Use o contexto do app ou da atividade para criar um objeto CredentialManager.

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

Criar a solicitação de credencial digital

Para solicitar um e-mail verificado, crie um GetCredentialRequest que contenha a GetDigitalCredentialOption. Essa opção exige uma string requestJson formatada como uma solicitação do OpenID for Verifiable Presentations (OpenID4VP).

O JSON de solicitação do OpenID4VP precisa seguir uma estrutura específica. Os provedores atuais oferecem suporte a uma estrutura JSON com um wrapper externo "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))

A solicitação contém as seguintes informações importantes:

  • Consulta DCQL: o dcql_query especifica o tipo de credencial e as declarações que estão sendo solicitadas (email_verified). Você pode solicitar outras declarações para determinar o nível de verificação. Algumas declarações possíveis são as seguintes:

    • email_verified: na resposta, esse é um booleano que indica se o e-mail foi verificado.
    • hd (domínio hospedado): na resposta, esse campo está vazio.
  • Se o e-mail não for @gmail.com, o Google o verificou quando a Conta do Google foi criada, mas não há declaração de atualização. Portanto, para e-mails que não são do Google, considere um desafio adicional, como um OTP, para verificar o usuário. Para entender o esquema da credencial e as regras específicas para validar campos como email_verified, consulte os guias de identidade do Google.

  • nonce: um valor aleatório exclusivo e criptograficamente seguro é gerado para cada solicitação. Isso é fundamental para a segurança, porque evita ataques repetidos.

  • UserInfoCredential: esse valor implica um tipo específico de credencial digital que contém atributos do usuário. Incluir isso na solicitação é fundamental para distinguir o caso de uso de verificação de e-mail.

Em seguida, envolva o JSON openId4vpRequest em um GetDigitalCredentialOption, crie um GetCredentialRequest e chame getCredential().

Apresentar a solicitação ao usuário

Apresente a solicitação ao usuário usando a interface integrada do Credential Manager.

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
}

Analisar a resposta no cliente

Depois de receber a resposta, você pode realizar uma análise preliminar no cliente. Isso é útil para atualizar imediatamente a interface, por exemplo, mostrando o nome do usuário.

O código a seguir extrai o JWT de divulgação seletiva (SD-JWT) bruto e usa um auxiliar para decodificar as declarações.

// 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"))
)

Processar a resposta

A API Credential Manager vai retornar uma DigitalCredential resposta.

Confira abaixo um exemplo de como é o responseJsonString bruto e como as declarações aparecem após a análise do SD-JWT interno, em que você também recebe metadados adicionais e e-mail verificado:

/*
// 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": ""
}
 */

Validação do lado do servidor para criação de contas

Como o e-mail recuperado é verificado criptograficamente, você pode omitir a etapa de verificação de OTP por e-mail, reduzindo significativamente a dificuldade de inscrição e possivelmente aumentando a conversão. É melhor processar isso no servidor. O cliente envia a resposta bruta (que contém o vp_token) e o nonce original para um novo endpoint do servidor.

Para verificação, seu aplicativo precisa enviar o responseJsonString completo ao servidor para validação criptográfica antes de criar uma conta ou fazer login do usuário.

A credencial digital oferece dois níveis críticos de verificação para o servidor:

  • Autenticidade dos dados: a verificação do URL do emissor (iss) e da SD-JWT assinatura prova que uma autoridade confiável emitiu esses dados.
  • Identidade do apresentador: a verificação do campo cnf e da assinatura de vinculação de chave (kb) confirma que a credencial está sendo compartilhada pelo mesmo dispositivo em que foi emitida originalmente, impedindo que ela seja interceptada ou usada em outro dispositivo.

A validação no servidor precisa alcançar o seguinte:

  • Verificar o emissor: verifique se o campo iss (emissor) corresponde a https://verifiablecredentials-pa.googleapis.com.
  • Verificar a assinatura: verifique a assinatura do SD-JWT usando as chaves públicas (JWKs) disponíveis em https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks.

Para segurança total, valide também o nonce para evitar ataques repetidos.

Ao combinar essas etapas, o servidor pode validar a autenticidade dos dados e a identidade do apresentador, garantindo que a credencial não tenha sido interceptada ou falsificada antes do provisionamento da nova conta.

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
}

Criação de chaves de acesso

Uma próxima etapa opcional, mas altamente recomendada, após o provisionamento de uma conta é criar imediatamente uma chave de acesso para ela. Isso oferece um método seguro e sem senha para o usuário fazer login. Esse fluxo é idêntico a um registro de chave de acesso padrão.

Suporte à WebView

Para que o fluxo funcione em uma WebView, os desenvolvedores precisam implementar uma ponte JavaScript (JS Bridge) para facilitar a transferência. Essa ponte permite que a WebView sinalize o app nativo, que pode fazer a chamada real para a API Credential Manager.

Veja também