Ce document explique comment intégrer l'API Gestionnaire d'identifiants à une application Android qui utilise WebView.
Présentation
Avant d'explorer le processus d'intégration, il est important de comprendre le flux de communication entre le code Android natif, un composant Web affiché dans une WebView qui gère l'authentification de votre application et un backend. Ce flux comprend l'inscription (création d'identifiants) et l'authentification (obtention d'identifiants existants).
Inscription (création d'une clé d'accès)
- Le backend génère un fichier JSON d'inscription initial et l'envoie à la page Web affichée dans la WebView.
- La page Web utilise
navigator.credentials.create()
pour enregistrer de nouveaux identifiants. Vous utiliserez le code JavaScript injecté pour remplacer cette méthode lors d'une prochaine étape, afin d'envoyer la requête à l'application Android. - L'application Android utilise l'API Gestionnaire d'identifiants pour créer la requête d'identification et l'utiliser pour
createCredential
. - L'API Gestionnaire d'identifiants partage les identifiants de clé publique avec l'application.
- L'application renvoie les identifiants de clé publique à la page Web afin que le code JavaScript injecté puisse analyser les réponses.
- La page Web envoie la clé publique au backend, qui la vérifie et l'enregistre.
Authentification (obtention d'une clé d'accès)
- Le backend génère un fichier JSON d'authentification pour obtenir les identifiants et l'envoie à la page Web affichée dans le client WebView.
- La page Web utilise
navigator.credentials.get
. Utilisez le code JavaScript injecté pour remplacer cette méthode et rediriger la requête vers l'application Android. - L'application récupère les identifiants à l'aide de l'API Gestionnaire d'identifiants en appelant
getCredential
. - L'API Gestionnaire d'identifiants renvoie les identifiants à l'application.
- L'application obtient la signature numérique de la clé privée et l'envoie à la page Web afin que le code JavaScript injecté puisse analyser les réponses.
- La page Web l'envoie ensuite au serveur qui vérifie la signature numérique à l'aide de la clé publique.
Le même flux peut être utilisé pour les mots de passe ou les systèmes d'identité fédérée.
Conditions préalables
Pour utiliser l'API Gestionnaire d'identifiants, suivez la procédure décrite dans la section Conditions préalables du guide dédié et effectuez les opérations suivantes :
- Ajoutez les dépendances requises.
- Préservez les classes dans le fichier ProGuard.
- Ajoutez la prise en charge de Digital Asset Links.
Communication JavaScript
Pour autoriser JavaScript dans une WebView et le code Android natif à communiquer entre eux, vous devez envoyer des messages et gérer les requêtes entre les deux environnements. Pour ce faire, injectez un code JavaScript personnalisé dans une WebView. Cela vous permet de modifier le comportement du contenu Web et d'interagir avec le code Android natif.
Injection JavaScript
Le code JavaScript suivant établit la communication entre la WebView et l'application Android. Il remplace les méthodes navigator.credentials.create()
et navigator.credentials.get()
utilisées par l'API WebAuthn pour les flux d'inscription et d'authentification décrits précédemment.
Utilisez la version réduite de ce code JavaScript dans votre application.
Créer un écouteur pour les clés d'accès
Configurez une classe PasskeyWebListener
qui gère la communication avec JavaScript. Cette classe doit hériter de WebViewCompat.WebMessageListener
. Cette classe reçoit des messages de JavaScript et effectue les actions nécessaires dans l'application Android.
Kotlin
// The class talking to Javascript should inherit:
class PasskeyWebListener(
private val activity: Activity,
private val coroutineScope: CoroutineScope,
private val credentialManagerHandler: CredentialManagerHandler
) : WebViewCompat.WebMessageListener
// ... Implementation details
Java
// The class talking to Javascript should inherit:
class PasskeyWebListener implements WebViewCompat.WebMessageListener {
// Implementation details
private Activity activity;
// Handles get/create methods meant for Java:
private CredentialManagerHandler credentialManagerHandler;
public PasskeyWebListener(
Activity activity,
CredentialManagerHandler credentialManagerHandler
) {
this.activity = activity;
this.credentialManagerHandler = credentialManagerHandler;
}
// ... Implementation details
}
Dans PasskeyWebListener
, implémentez la logique pour les requêtes et les réponses, comme décrit dans les sections suivantes.
Gérer la requête d'authentification
Pour gérer les requêtes pour les opérations WebAuthn navigator.credentials.create()
ou navigator.credentials.get()
, la méthode onPostMessage
de la classe PasskeyWebListener
est appelée lorsque le code JavaScript envoie un message à l'application Android :
Kotlin
class PasskeyWebListener(...)... {
// ...
/** havePendingRequest is true if there is an outstanding WebAuthn request.
There is only ever one request outstanding at a time. */
private var havePendingRequest = false
/** pendingRequestIsDoomed is true if the WebView has navigated since
starting a request. The FIDO module cannot be canceled, but the response
will never be delivered in this case. */
private var pendingRequestIsDoomed = false
/** replyChannel is the port that the page is listening for a response on.
It is valid if havePendingRequest is true. */
private var replyChannel: ReplyChannel? = null
/**
* Called by the page during a WebAuthn request.
*
* @param view Creates the WebView.
* @param message The message sent from the client using injected JavaScript.
* @param sourceOrigin The origin of the HTTPS request. Should not be null.
* @param isMainFrame Should be set to true. Embedded frames are not
supported.
* @param replyProxy Passed in by JavaScript. Allows replying when wrapped in
the Channel.
* @return The message response.
*/
@UiThread
override fun onPostMessage(
view: WebView,
message: WebMessageCompat,
sourceOrigin: Uri,
isMainFrame: Boolean,
replyProxy: JavaScriptReplyProxy,
) {
val messageData = message.data ?: return
onRequest(
messageData,
sourceOrigin,
isMainFrame,
JavaScriptReplyChannel(replyProxy)
)
}
private fun onRequest(
msg: String,
sourceOrigin: Uri,
isMainFrame: Boolean,
reply: ReplyChannel,
) {
msg?.let {
val jsonObj = JSONObject(msg);
val type = jsonObj.getString(TYPE_KEY)
val message = jsonObj.getString(REQUEST_KEY)
if (havePendingRequest) {
postErrorMessage(reply, "The request already in progress", type)
return
}
replyChannel = reply
if (!isMainFrame) {
reportFailure("Requests from subframes are not supported", type)
return
}
val originScheme = sourceOrigin.scheme
if (originScheme == null || originScheme.lowercase() != "https") {
reportFailure("WebAuthn not permitted for current URL", type)
return
}
// Verify that origin belongs to your website,
// it's because the unknown origin may gain credential info.
if (isUnknownOrigin(originScheme)) {
return
}
havePendingRequest = true
pendingRequestIsDoomed = false
// Use a temporary "replyCurrent" variable to send the data back, while
// resetting the main "replyChannel" variable to null so it’s ready for
// the next request.
val replyCurrent = replyChannel
if (replyCurrent == null) {
Log.i(TAG, "The reply channel was null, cannot continue")
return;
}
when (type) {
CREATE_UNIQUE_KEY ->
this.coroutineScope.launch {
handleCreateFlow(credentialManagerHandler, message, replyCurrent)
}
GET_UNIQUE_KEY -> this.coroutineScope.launch {
handleGetFlow(credentialManagerHandler, message, replyCurrent)
}
else -> Log.i(TAG, "Incorrect request json")
}
}
}
private suspend fun handleCreateFlow(
credentialManagerHandler: CredentialManagerHandler,
message: String,
reply: ReplyChannel,
) {
try {
havePendingRequest = false
pendingRequestIsDoomed = false
val response = credentialManagerHandler.createPasskey(message)
val successArray = ArrayList<Any>();
successArray.add("success");
successArray.add(JSONObject(response.registrationResponseJson));
successArray.add(CREATE_UNIQUE_KEY);
reply.send(JSONArray(successArray).toString())
replyChannel = null // setting initial replyChannel for the next request
} catch (e: CreateCredentialException) {
reportFailure(
"Error: ${e.errorMessage} w type: ${e.type} w obj: $e",
CREATE_UNIQUE_KEY
)
} catch (t: Throwable) {
reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY)
}
}
companion object {
const val TYPE_KEY = "type"
const val REQUEST_KEY = "request"
const val CREATE_UNIQUE_KEY = "create"
const val GET_UNIQUE_KEY = "get"
}
}
Java
class PasskeyWebListener implements ... {
// ...
/**
* Called by the page during a WebAuthn request.
*
* @param view Creates the WebView.
* @param message The message sent from the client using injected JavaScript.
* @param sourceOrigin The origin of the HTTPS request. Should not be null.
* @param isMainFrame Should be set to true. Embedded frames are not
supported.
* @param replyProxy Passed in by JavaScript. Allows replying when wrapped in
the Channel.
* @return The message response.
*/
@UiThread
public void onPostMessage(
@NonNull WebView view,
@NonNull WebMessageCompat message,
@NonNull Uri sourceOrigin,
Boolean isMainFrame,
@NonNull JavaScriptReplyProxy replyProxy,
) {
if (messageData == null) {
return;
}
onRequest(
messageData,
sourceOrigin,
isMainFrame,
JavaScriptReplyChannel(replyProxy)
)
}
private void onRequest(
String msg,
Uri sourceOrigin,
boolean isMainFrame,
ReplyChannel reply
) {
if (msg != null) {
try {
JSONObject jsonObj = new JSONObject(msg);
String type = jsonObj.getString(TYPE_KEY);
String message = jsonObj.getString(REQUEST_KEY);
boolean isCreate = type.equals(CREATE_UNIQUE_KEY);
boolean isGet = type.equals(GET_UNIQUE_KEY);
if (havePendingRequest) {
postErrorMessage(reply, "The request already in progress", type);
return;
}
replyChannel = reply;
if (!isMainFrame) {
reportFailure("Requests from subframes are not supported", type);
return;
}
String originScheme = sourceOrigin.getScheme();
if (originScheme == null || !originScheme.toLowerCase().equals("https")) {
reportFailure("WebAuthn not permitted for current URL", type);
return;
}
// Verify that origin belongs to your website,
// Requests of unknown origin may gain access to credential info.
if (isUnknownOrigin(originScheme)) {
return;
}
havePendingRequest = true;
pendingRequestIsDoomed = false;
// Use a temporary "replyCurrent" variable to send the data back,
// while resetting the main "replyChannel" variable to null so it’s
// ready for the next request.
ReplyChannel replyCurrent = replyChannel;
if (replyCurrent == null) {
Log.i(TAG, "The reply channel was null, cannot continue");
return;
}
if (isCreate) {
handleCreateFlow(credentialManagerHandler, message, replyCurrent));
} else if (isGet) {
handleGetFlow(credentialManagerHandler, message, replyCurrent));
} else {
Log.i(TAG, "Incorrect request json");
}
} catch (JSONException e) {
e.printStackTrace();
}
}
}
}
Pour handleCreateFlow
et handleGetFlow
, reportez-vous à l'exemple sur GitHub.
Gérer la réponse
Pour gérer les réponses envoyées depuis l'application native à la page Web, ajoutez JavaScriptReplyProxy
dans JavaScriptReplyChannel
.
Kotlin
class PasskeyWebListener(...)... {
// ...
// The setup for the reply channel allows communication with JavaScript.
private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) :
ReplyChannel {
override fun send(message: String?) {
try {
reply.postMessage(message!!)
} catch (t: Throwable) {
Log.i(TAG, "Reply failure due to: " + t.message);
}
}
}
// ReplyChannel is the interface where replies to the embedded site are
// sent. This allows for testing since AndroidX bans mocking its objects.
interface ReplyChannel {
fun send(message: String?)
}
}
Java
class PasskeyWebListener implements ... {
// ...
// The setup for the reply channel allows communication with JavaScript.
private static class JavaScriptReplyChannel implements ReplyChannel {
private final JavaScriptReplyProxy reply;
JavaScriptReplyChannel(JavaScriptReplyProxy reply) {
this.reply = reply;
}
@Override
public void send(String message) {
reply.postMessage(message);
}
}
// ReplyChannel is the interface where replies to the embedded site are
// sent. This allows for testing since AndroidX bans mocking its objects.
interface ReplyChannel {
void send(String message);
}
}
Veillez à détecter toutes les erreurs dans l'application native et à les renvoyer côté JavaScript.
Intégrer à la WebView
Cette section explique comment configurer l'intégration de votre WebView.
Initialiser la WebView
Dans l'activité de votre application Android, initialisez une WebView
et configurez un WebViewClient
associé. Le WebViewClient
gère la communication avec le code JavaScript injecté dans la WebView
.
Configurez la WebView et appelez le Gestionnaire d'identifiants :
Kotlin
val credentialManagerHandler = CredentialManagerHandler(this)
// ...
AndroidView(factory = {
WebView(it).apply {
settings.javaScriptEnabled = true
// Test URL:
val url = "https://credman-web-test.glitch.me/"
val listenerSupported = WebViewFeature.isFeatureSupported(
WebViewFeature.WEB_MESSAGE_LISTENER
)
if (listenerSupported) {
// Inject local JavaScript that calls Credential Manager.
hookWebAuthnWithListener(this, this@MainActivity,
coroutineScope, credentialManagerHandler)
} else {
// Fallback routine for unsupported API levels.
}
loadUrl(url)
}
}
)
Java
// Example shown in the onCreate method of an Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webView = findViewById(R.id.web_view);
// Test URL:
String url = "https://credman-web-test.glitch.me/";
Boolean listenerSupported = WebViewFeature.isFeatureSupported(
WebViewFeature.WEB_MESSAGE_LISTENER
);
if (listenerSupported) {
// Inject local JavaScript that calls Credential Manager.
hookWebAuthnWithListener(webView, this,
coroutineScope, credentialManagerHandler)
} else {
// Fallback routine for unsupported API levels.
}
webView.loadUrl(url);
}
Créez un objet client WebView et injectez du code JavaScript dans la page Web :
Kotlin
// This is an example call into hookWebAuthnWithListener
val passkeyWebListener = PasskeyWebListener(
activity, coroutineScope, credentialManagerHandler
)
val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
// Handle page load events
passkeyWebListener.onPageStarted();
webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null)
}
}
webView.webViewClient = webViewClient
Java
// This is an example call into hookWebAuthnWithListener
PasskeyWebListener passkeyWebListener = new PasskeyWebListener(
activity, credentialManagerHandler
)
WebViewClient webiewClient = new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
// Handle page load events
passkeyWebListener.onPageStarted();
webView.evaulateJavascript(PasskeyWebListener.INJECTED_VAL, null);
}
};
webView.setWebViewClient(webViewClient);
Configurer un écouteur de messages Web
Pour autoriser la publication de messages entre JavaScript et l'application Android, configurez un écouteur de messages Web avec la méthode WebViewCompat.addWebMessageListener
.
Kotlin
val rules = setOf("*")
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(
webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener
)
}
Java
Set<String> rules = new HashSet<>(Arrays.asList("*"));
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(
webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener
)
}
Intégration Web
Pour découvrir comment créer une intégration Web, consultez Créer une clé d'accès pour les connexions sans mot de passe et Se connecter avec une clé d'accès via la saisie automatique des formulaires.
Tests et déploiement
Testez l'ensemble du flux de manière approfondie dans un environnement contrôlé afin de garantir une communication appropriée entre l'application Android, la page Web et le backend.
Déployez la solution intégrée en production, en vous assurant que le backend peut gérer les requêtes d'inscription et d'authentification entrantes. Le code backend doit générer un fichier JSON initial pour les processus d'inscription (create) et d'authentification (get). Il doit également gérer la validation et la vérification des réponses reçues de la page Web.
Vérifiez que l'implémentation correspond aux recommandations relatives à l'expérience utilisateur.
Remarques importantes
- Utilisez le code JavaScript fourni pour gérer les opérations
navigator.credentials.create()
etnavigator.credentials.get()
. - La classe
PasskeyWebListener
constitue la passerelle entre l'application Android et le code JavaScript dans la WebView. Elle gère la transmission des messages, la communication et l'exécution des actions requises. - Adaptez les extraits de code fournis à la structure de votre projet, aux conventions de dénomination et à vos éventuelles exigences spécifiques.
- Détectez les erreurs côté application native et renvoyez-les vers JavaScript.
En suivant ce guide et en intégrant l'API Gestionnaire d'identifiants à votre application Android qui utilise une WebView, vous pouvez offrir à vos utilisateurs une expérience de connexion sécurisée et fluide via des clés d'accès, tout en gérant efficacement leurs identifiants.