HTTP 与 Web

Vert.x FIDO2 webauthn 在 Web 应用程序中的应用

本指南将向您展示如何构建和保护一个简单的 FIDO2 CONFORMANT(通常称为 Web Authentication API)Vert.x Web 应用程序。Web Authentication API(也称为 WebAuthn)是由 W3C 和 FIDO Alliance,并由 Google、Mozilla、Microsoft、Yubico 等公司参与编写的规范。该 API 允许服务器使用公钥加密而非密码来注册和认证用户。

您将构建什么

完成本指南后,您将拥有一个无需要求或存储密码即可执行身份验证的可用应用程序。这将通过 WebAuthn 实现,WebAuthn 是一个新的 W3C 全球标准,用于 Web 上的安全认证,并得到所有主流浏览器和平台的支持。

您需要什么

  • 文本编辑器或 IDE

  • Java 8 或更高版本(建议使用 11 或 >=15 版本以获得额外的安全算法)

  • 互联网连接

创建项目

  • Vert.x Web

  • Webauthn 认证

project

安全优先

Web Authentication API 是一个安全的 API,供应商决定遵循最佳实践。虽然您可以在不使用 SSL 的情况下构建服务器,但现代 Web 浏览器不会连接或允许 Webauthn API 与不使用 SSL 的服务器一起使用,即使在开发期间也是如此。

在使用 webauthn 之前,我们需要确保即使是我们的开发应用程序也已准备好 SSL。为此,我们需要创建一个有效的 SSL 证书。请注意,自签名证书仍然允许使用。

要为您的 IP 地址创建自签名证书,请执行以下操作

export IP=10.0.0.2
export CERTSTORE_SECRET=password    (1)
keytool \
  -genkeypair \
  -alias rsakey \
  -keyalg rsa \
  -storepass ${CERTSTORE_SECRET} \
  -keystore certstore.jks \
  -storetype JKS \
  -dname "CN=${IP}.nip.io,O=Vert.x Development" (2)
1 不要使用此密码!
2 将 CN 替换为您自己的 IP 地址(非 localhost),并带上 .nip.io 后缀

为此设置,我们依赖于一个免费的 DNS 服务器,该服务器在查询时返回您的 IP 地址。网络上还存在提供相同结果的其他服务,例如

目前我们有了一个 SSL 证书,但其格式对于现代 Java 版本来说被认为是已弃用的,因此我们需要进行第二步将其转换为 PKCS#12 格式。

keytool \
  -importkeystore \
  -srckeystore certstore.jks \
  -destkeystore certstore.jks \
  -deststoretype pkcs12

您的新 SSL 证书在 certstore.jks 文件中。

注册流程

在能够*登录*您的应用程序之前,我们需要注册一个 FIDO2 验证器。此过程类似于 Web 应用程序中的“注册”流程。但是,存在一些差异,此图试图说明这些差异

register flow
  1. 用户仅使用用户名注册

  2. 您的服务器(依赖方)创建一个安全挑战

  3. 浏览器将此信息传递给令牌设备

  4. 令牌为此信息生成一个新的密钥对

  5. 挑战被签名并返回给服务器(RP)

  6. 服务器验证挑战的正确性并存储公钥

认证流程

authn flow
  1. 用户仅使用用户名进行认证

  2. 您的服务器(依赖方)创建一个安全挑战

  3. 浏览器将此信息传递给令牌设备

  4. 令牌生成并验证凭证并签署挑战

  5. 浏览器创建一个认证断言

  6. 服务器验证断言的正确性并允许用户访问

编写服务器代码

处理验证器对象

您可以在此处找到完整的源代码,现在我们只介绍重要部分。

为了使注册和认证正常工作,我们需要能够存储和查询验证器数据。为此,我们需要提供一些专门用于此目的的函数。您可以在此处查看其源代码。在您的 verticle 中,您首先创建此对象,如下所示:

    // Dummy database, real world workloads
    // use a persistent store or course!
    InMemoryStore database = new InMemoryStore();

配置 Webauthn 对象

为了使用 FIDO2,我们需要配置认证提供者如何工作。为此,我们需要创建并配置一个 WebAuthn 对象

    // create the webauthn security object
    WebAuthn webAuthN = WebAuthn.create(
      vertx,
      new WebAuthnOptions()   (1)
        .setRelyingParty(new RelyingParty()
          .setName("Vert.x FIDO2/webauthn Howto"))
        .setUserVerification(UserVerification.DISCOURAGED)  (2)
        .setAttestation(Attestation.NONE)   (3)
        .setRequireResidentKey(false)   (4)
        .setChallengeLength(64)   (5)
        .addPubKeyCredParam(PublicKeyCredential.ES256)    (6)
        .addPubKeyCredParam(PublicKeyCredential.RS256)
        .addTransport(AuthenticatorTransport.USB)   (7)
        .addTransport(AuthenticatorTransport.NFC)
        .addTransport(AuthenticatorTransport.BLE)
        .addTransport(AuthenticatorTransport.INTERNAL))
      // where to load/update authenticators data
      .authenticatorFetcher(database::fetcher)
      .authenticatorUpdater(database::updater);
1 所有配置都在 WebAuthnOptions 中进行。这只是一个合理默认值的小例子,有关更多选项,请查阅 javadoc。
2 在验证用户时,我们并不严格要求其必须被验证。
3 在注册期间,我们不希望对硬件进行认证。
4 我们不需要常驻密钥供用户进行认证。
5 定义挑战的长度,最小值为 32
6 我们接受哪些安全算法。
7 我们允许从浏览器到验证器使用哪种传输类型。

Web 路由初始化

让我们开始配置我们的 HTTP 路由以确保安全。在使用 FIDO2 之前,有几个处理程序是始终需要就位的

  • BodyHandler

  • SessionHandler

此外,也建议使用 StaticHandler,因为整个过程既需要 Vert.x 代码(我们目前正在探索的代码),也需要一个小的 JavaScript 辅助脚本。为了简化开发,Vert.x 也通过 Maven 依赖项提供了此类辅助脚本

  <dependencies>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-web</artifactId>
    </dependency>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-auth-webauthn</artifactId>
    </dependency>
    <dependency>    (1)
      <groupId>io.vertx</groupId>
      <artifactId>vertx-auth-webauthn</artifactId>
      <version>${vertx.version}</version>
      <classifier>client</classifier>
      <type>js</type>
    </dependency>
  </dependencies>
1 提供与 Vert.x 后端交互的简单辅助脚本。

现在我们可以初始化 Web 路由,如下所示:

    final Router app = Router.router(vertx);
    app.route()   (1)
      .handler(StaticHandler.create());
    app.post()    (2)
      .handler(BodyHandler.create());
    app.route()   (3)
      .handler(SessionHandler
        .create(LocalSessionStore.create(vertx)));

    WebAuthnHandler webAuthnHandler = WebAuthnHandler.create(webAuthN) (4)
      .setOrigin(String.format("https://%s.nip.io:8443", System.getenv("IP")))
      // required callback
      .setupCallback(app.post("/webauthn/callback"))
      // optional register callback
      .setupCredentialsCreateCallback(app.post("/webauthn/register"))
      // optional login callback
      .setupCredentialsGetCallback(app.post("/webauthn/login"));

    app.route()
      .handler(webAuthnHandler);

    app.route("/protected")   (5)
      .handler(ctx ->
        ctx.response()
          .end(
            "FIDO2 is Awesome!\n" +
              "No Password phishing here!\n"));
1 提供客户端应用程序(稍后详述)。
2 启用 POST 请求体的解析。
3 启用会话。
4 使用之前定义的配置挂载 webauthn 处理程序。
5 安全路由示例。

服务器引导

现在我们有了一个最小路由,我们需要创建一个 HTTPS 服务器。请注意,这是必需的步骤,也是我们为开发环境创建自签名证书并使用自定义域名 的原因。

    vertx.createHttpServer(
      new HttpServerOptions()
        .setSsl(true)
        .setKeyStoreOptions(
          new JksOptions()
            .setPath("certstore.jks")
            .setPassword(System.getenv("CERTSTORE_SECRET"))))
      .requestHandler(app)
      .listen(8443, "0.0.0.0")
      .onSuccess(v -> {
        System.out.printf("Server: https://%s.nip.io:8443%n", System.getenv("IP"));
        start.complete();
      })
      .onFailure(start::fail);

此时,我们的后端应用程序已完成,且 `/protected` 路由将由 FIDO2 保护。运行应用程序如下所示:

IP=10.0.0.2 \
mvn exec:java

# The following line should be present in your console:
# Server listening at: https://10.0.0.2.nip.io:8443

您的浏览器会给出关于自签名证书的警告,这是正常的。

selfsigned

这是为了您的安全。在实际的应用程序中,您应该使用适当的 SSL 证书,例如由 Let's Encrypt 颁发的证书。

用浏览器导航到:https://10.0.0.2.nip.io:8443/protected 应该会显示一个 Forbidden 错误。下一步是创建一个最小化的登录和注册 Web 应用程序。

编写客户端代码

对于客户端应用程序,不使用任何框架,以展示该脚本可以与任何框架一起使用,也可以独立使用。

我们将创建一个包含 3 个部分的最小 HTML 页面

  1. 一个表单,用户只需输入其以下信息即可注册或登录

    • 显示名称:例如,一个用户友好的名称,如“John Doe”

    • 用户名:一个唯一的用户名,例如“[email protected]

  2. 注册登录按钮

  3. 一个指向受保护资源的链接,只有在注册登录后才允许访问。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>WebAuthn Howto</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

<div id="message"></div>
<div> (1)
  <label for="displayName">Display Name: </label>
  <input id="displayName" name="displayName" type="text" value="Your Name"><br/>
  <label for="username">Username: </label>
  <input id="username" name="username" type="email" value="[email protected]">
</div>
<hr>
<div> (2)
  <button id="register">register</button>
  <button id="login">login</button>
</div>
<hr>
<div> (3)
  <a href="/protected">Want some secret info?</a>
</div>

<script src="vertx-auth-webauthn-client.js"></script>   (4)
<script src="main.js"></script>   (5)
</body>
</html>
1 如上所述的第一部分。
2 如上所述的第二部分。
3 如上所述的第三部分。
4 添加到项目依赖项中的辅助脚本
5 您的应用程序脚本

客户端脚本代码

您可以在此处查看完整脚本。我们只介绍重要部分。

包含的脚本 vertx-auth-webauthn-client.js 定义了一个全局类型 WebAuthn。第一步是创建此对象的一个实例,并将其配置为与我们的后端配置匹配

const webAuthn = new WebAuthn({
  callbackPath: '/webauthn/callback',
  registerPath: '/webauthn/register',
  loginPath: '/webauthn/login'
});

配置应该很简单,只需定义与您的后端路由匹配的路径即可。

接下来,我们需要为我们的按钮添加事件处理程序。我们从注册操作开始

registerButton.onclick = () => {
  webAuthn
    .register({
      name: document.getElementById('username').value,
      displayName: document.getElementById('displayName').value
    })
    .then(() => {
      displayMessage('registration successful');
    })
    .catch(err => {
      displayMessage('registration failed');
      console.error(err);
    });
};

onclick 事件将只使用 Webauthn 对象。webauthn 对象只有两个方法,并且它是基于 Promise 的,因此对于 Vert.x 用户来说应该不难理解。如果您主要是 Java Vert.x 开发者,只需将 JavaScriptPromise 视为 Vert.x 的 Future,一切都将非常相似。

要注册用户,只需要 2 个必需属性

  1. name 唯一的用户名,例如,一个电子邮件地址。

  2. displayName 一个人类友好的文本描述,例如,您的名字和姓氏。

填写表单并点击注册按钮后,用户应该会看到一个弹出窗口,要求授权注册

register start

触碰您的验证器后,流程应该会成功完成,并显示

register end

此时,您可以关闭浏览器,甚至打开另一个浏览器。注册过程已完成,您现在可以使用您的令牌从任何地方登录。

我们现在需要处理登录按钮的 onclick 事件

loginButton.onclick = () => {
  webAuthn
    .login({
      name: document.getElementById('username').value
    })
    .then(() => {
      displayMessage('You are logged in');
    })
    .catch(err => {
      displayMessage('Invalid credential');
      console.error(err);
    });
};

正如 FIDO2 所描述的,使用 webauthn 是一种无密码认证,因此执行登录唯一需要的字段是

  1. name 唯一的用户名,例如,一个电子邮件地址。

login start

就像在注册屏幕中一样,将弹出一个窗口,告知您有登录意图并请求用户授权。当您触碰您的验证器时,您可以看到

login end

现在您已登录,最终可以尝试查看秘密信息链接,它将显示类似以下内容

secret

总结

在本指南中,我们涵盖了

  1. 创建 Web 项目

  2. 使用 Webauthn 保护 Web 应用程序

  3. 借助 webauthn-client.js 编写客户端代码

希望您现在可以在您的下一个项目中使用 FIDO2/webauthn!