Implémenter la validation des adresses e-mail avec l'API Digital Credentials

Ce guide explique comment implémenter la récupération d'adresses e-mail validées à l'aide de l' API Digital Credentials Verifier via une requête OpenID for Verifiable Presentations (OpenID4VP).

Ajouter des dépendances

Dans le fichier build.gradle de votre application, ajoutez les dépendances suivantes pour le Gestionnaire d'identifiants :

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"
}

Initialiser le Gestionnaire d'identifiants

Utilisez le contexte de votre application ou de votre activité pour créer un objet CredentialManager.

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

Construire la requête d'identifiants numériques

Pour demander une adresse e-mail validée, créez un GetCredentialRequest contenant un GetDigitalCredentialOption. Cette option nécessite une chaîne requestJson au format OpenID for Verifiable Presentations (OpenID4VP).

Le JSON de la requête OpenID4VP doit suivre une structure spécifique. Les fournisseurs actuels sont compatibles avec une structure JSON avec un wrapper externe "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))

La requête contient les informations clés suivantes :

  • Requête DCQL : dcql_query spécifie le type d'identifiant et les revendications demandées (email_verified). Vous pouvez demander d'autres revendications pour déterminer le niveau de validation. Voici quelques revendications possibles :

    • email_verified: dans la réponse, il s'agit d'une valeur booléenne indiquant si l'adresse e-mail est validée.
    • hd (domaine hébergé) : dans la réponse, ce champ est vide.
  • Si l'adresse e-mail n'est pas une adresse @gmail.com, Google l'a validée lors de la création du compte Google, mais il n'existe aucune revendication de fraîcheur. Par conséquent, pour les adresses e-mail non Google, vous devez envisager un défi supplémentaire, tel qu'un code OTP, pour valider l'utilisateur. Pour comprendre le schéma de l'identifiant et les règles spécifiques de validation des champs tels que email_verified, consultez les guides Google Identity.

  • Nonce : une valeur aléatoire unique et cryptographiquement sécurisée est générée pour chaque requête. Ceci est essentiel pour la sécurité, car cela empêche les attaques par relecture.

  • UserInfoCredential: cette valeur implique un type spécifique d'identifiant numérique contenant des attributs utilisateur. L'inclure dans la requête est essentiel pour distinguer le cas d'utilisation de la validation d'adresse e-mail.

Ensuite, encapsulez le JSON openId4vpRequest dans une GetDigitalCredentialOption, créez une GetCredentialRequest et appelez getCredential().

Présenter la requête à l'utilisateur

Présentez la requête à l'utilisateur à l'aide de l'interface utilisateur intégrée du 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
}

Analyser la réponse sur le client

Après avoir reçu la réponse, vous pouvez effectuer une analyse préliminaire sur le client. Cela est utile pour mettre à jour immédiatement l'interface utilisateur, par exemple en affichant le nom de l'utilisateur.

Le code suivant extrait le JWT de divulgation sélective (SD-JWT) brut et utilise un helper pour décoder ses revendications.

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

Gérer la réponse

L'API Credential Manager renvoie une DigitalCredential réponse.

Voici un exemple de l'apparence de responseJsonString brut et des revendications après l'analyse du SD-JWT interne, où vous obtenez également des métadonnées supplémentaires ainsi que l'adresse e-mail validée :

/*
// 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": "[email protected]",
  "email_verified": true,
  "given_name": "Jane",
  "family_name": "Doe",
  "name": "Jane Doe",
  "picture": "http://example.com/janedoe/me.jpg",
  "hd": ""
}
 */

Validation côté serveur pour la création de compte

Étant donné que l'adresse e-mail récupérée est validée de manière cryptographique, vous pouvez omettre l'étape de validation du code OTP par e-mail, ce qui réduit considérablement les frictions lors de l'inscription et augmente potentiellement la conversion. Il est préférable de gérer ce processus sur votre serveur. Le client envoie la réponse brute (contenant le vp_token) et le nonce d'origine à un nouveau point de terminaison du serveur.

Pour la validation, votre application doit envoyer l'intégralité de responseJsonString à votre serveur pour validation cryptographique avant de créer un compte ou de connecter l'utilisateur.

L'identifiant numérique fournit deux niveaux de validation essentiels pour votre serveur :

  • Authenticité des données : la validation de l'URL de l'émetteur (iss) et de la signature SD-JWT prouve qu'une autorité de confiance a émis ces données.
  • Identité du présentateur : la validation du champ cnf et de la signature de liaison de clé (kb) confirme que l'identifiant est partagé par l'appareil sur lequel il a été émis à l'origine, ce qui l'empêche d'être intercepté ou utilisé sur un autre appareil.

La validation sur le serveur doit atteindre les objectifs suivants :

  • Valider l'émetteur : assurez-vous que le champ iss (émetteur) correspond à https://verifiablecredentials-pa.googleapis.com.
  • Valider la signature : vérifiez la signature du SD-JWT à l'aide des clés publiques (JWK) disponibles sur https://verifiablecredentials-pa.googleapis.com/.well-known/vc-public-jwks.

Pour une sécurité totale, assurez-vous également de valider le nonce afin d'éviter les attaques par relecture.

En combinant ces étapes, votre serveur peut valider à la fois l'authenticité des données et l'identité du présentateur, en s'assurant que l'identifiant n'a pas été intercepté ni usurpé avant de provisionner le nouveau compte.

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
}

Création d'une clé d'accès

Une étape suivante facultative, mais fortement recommandée après le provisionnement d'un compte, consiste à immédiatement créer une clé d'accès pour ce compte. Cela permet à l'utilisateur de se connecter de manière sécurisée et sans mot de passe. Ce flux est identique à un enregistrement de clé d'accès standard.

Compatibilité avec WebView

Pour que le flux fonctionne sur une WebView, les développeurs doivent implémenter un JavaScript bridge (JS Bridge) afin de faciliter le transfert. Ce pont permet à la WebView de signaler l'application native, qui peut ensuite effectuer l'appel réel à l'API Credential Manager.

Voir aussi