preface

More and more laptops now have built-in fingerprint recognition for quick access from the lock screen to the desktop, and some client software supports fingerprint authentication.

The other day I was thinking that since the client software can call the fingerprint device, the Web side should also be able to call it. After a lot of work, I finally realized this feature and used it in my open source project.

This article will share with you my implementation ideas and process, welcome interested developers to read this article.

Implementation approach

The browser provides a Web Authentication API that can be used to invoke a user’s fingerprint device to authenticate user information.

The final realization effect video, please step: Web side fingerprint login

Registered fingerprints

First, we need to get the server returns the user credentials, then the user credentials to the fingerprint equipment, tone up the system of fingerprint authentication, certification through after the callback function returns the device id and the client information, we need to save the information on the server, used to call back the fingerprint device to verify user identity, so as to realize the login.

Next, let’s summarize the process of registering fingerprints, as follows:

  • After a user successfully logs in to the web site using other methods, the server returns the user credentials and saves them locally
  • Check whether a fingerprint device exists on the client
  • If so, the user credentials and user information returned by the server are passed to the fingerprint registration function to create the fingerprint
  • If the authentication succeeds, the callback function returns the device ID and client information, and saves the device ID to the local computer
  • The device ID and client information are sent to the server and stored in the specified user data.

⚠️ Note: Registered fingerprints can only work on websites that use HTTPS connections or localhost.

Fingerprint authentication

User authorization fingerprints after login in our website, user credentials and the device id will be stored in the local, when a user access to our website, will get the the two data from local and suggest whether it need to login system by fingerprint, agreed to after the device id and the user credentials to the fingerprint equipment, tone up the system of fingerprint authentication, certification through, call the login interface, Obtain user information.

Next, let’s summarize the fingerprint authentication process, as follows:

  • Obtain the user credentials and device ID from the local device
  • Check whether a fingerprint device exists on the client
  • If yes, pass the user credentials and device ID to the fingerprint authentication function for verification
  • If the authentication succeeds, the login interface is invoked to obtain the user information

⚠️ Note: Fingerprint authentication only works with HTTPS connections, or sites that use localhost.

The implementation process

In the last chapter, we clarified the specific implementation ideas of fingerprint login, and then we will look at the specific implementation process and code.

Server-side implementation

First, we need to write three interfaces on the server: get TouchID, register TouchID, and fingerprint login

Get TouchID

This interface is used to judge whether the login user has registered the fingerprint on this website. If the user has registered, the TouchID will be returned to the client to facilitate the user’s next login.

  • The controller layer code is as follows
    @apioperation (value = "Get TouchID", notes =" Get fingerprint login credentials from user ID")
    @CrossOrigin()
    @RequestMapping(value = "/getTouchID", method = RequestMethod.POST)
    publicResultVO<? > getTouchID(@apiparam (name = "pass userId", required = true) @Valid @RequestBody GetTouchIdDto touchIdDto, @RequestHeader(value = "token") String token) {
        JSONObject result = userService.getTouchID(JwtUtil.getUserId(token));
        if (result.getEnum(ResultEnum.class, "code").getCode() == 0) {
            // touchId obtained successfully
            return ResultVOUtil.success(result.getString("touchId"));
        }
        // Return an error message
        return ResultVOUtil.error(result.getEnum(ResultEnum.class, "code").getCode(), result.getEnum(ResultEnum.class, "code").getMessage());
    }
Copy the code
  • The implementation code of the interface is as follows
    / / get TouchID
    @Override
    public JSONObject getTouchID(String userId) {
        JSONObject returnResult = new JSONObject();
        // Query the touchId from the database based on the current user ID
        User user = userMapper.getTouchId(userId);
        String touchId = user.getTouchId();
        if(touchId ! =null) {
           / / touchId exist
            returnResult.put("code", ResultEnum.GET_TOUCHID_SUCCESS);
            returnResult.put("touchId", touchId);
            return returnResult;
        }
        // touchId does not exist
        returnResult.put("code", ResultEnum.GET_TOUCHID_ERR);
        return returnResult;
    }
Copy the code

Registered TouchID

This interface is used to receive the TouchID and client information returned by the client fingerprint device and save the obtained information to the specified user in the database.

  • The controller layer code is as follows
    @apioperation (value = "register TouchID", notes =" Save TouchID returned by client ")
    @CrossOrigin()
    @RequestMapping(value = "/registeredTouchID", method = RequestMethod.POST)
    publicResultVO<? > registeredTouchID(@apiparam (name = "pass userId", required = true) @Valid @RequestBody SetTouchIdDto touchIdDto, @RequestHeader(value = "token") String token) {
        JSONObject result = userService.registeredTouchID(touchIdDto.getTouchId(), touchIdDto.getClientDataJson(), JwtUtil.getUserId(token));
        if (result.getEnum(ResultEnum.class, "code").getCode() == 0) {
            // touchId obtained successfully
            return ResultVOUtil.success(result.getString("data"));
        }
        // Return an error message
        return ResultVOUtil.error(result.getEnum(ResultEnum.class, "code").getCode(), result.getEnum(ResultEnum.class, "code").getMessage());
    }
Copy the code
  • The implementation code of the interface is as follows
    / / register TouchID
    @Override
    public JSONObject registeredTouchID(String touchId, String clientDataJson, String userId) {
        JSONObject result = new JSONObject();
        User row = new User();
        row.setTouchId(touchId);
        row.setClientDataJson(clientDataJson);
        row.setUserId(userId);
       // Update the touchId and client information based on the userId
        int updateResult = userMapper.updateTouchId(row);
        if (updateResult>0) {
            result.put("code", ResultEnum.SET_TOUCHED_SUCCESS);
            result.put("data"."Touch_id set successfully");
            return result;
        }
        result.put("code", ResultEnum.SET_TOUCHED_ERR);
        return result;
    }
Copy the code

Fingerprint login

This interface receives the user credentials and touchId sent by the client, verifies them against data in the database, and returns the user information.

  • The controller layer code is as follows
    @apiOperation (value = "Fingerprint login ", notes =" Login system by touchId and user credentials ")
    @CrossOrigin()
    @RequestMapping(value = "/touchIdLogin", method = RequestMethod.POST)
    publicResultVO<? > touchIdLogin(@apiparam (name = "pass in Touch ID and user credentials ", required = true) @Valid @RequestBody TouchIDLoginDto touchIDLogin) {
        JSONObject result = userService.touchIdLogin(touchIDLogin.getTouchId(), touchIDLogin.getCertificate());
        return LoginUtil.getLoginResult(result);
    }
Copy the code
  • The implementation code of the interface is as follows
    // Fingerprint login
    @Override
    public JSONObject touchIdLogin(String touchId, String certificate) {
        JSONObject returnResult = new JSONObject();
        User row = new User();
        row.setTouchId(touchId);
        row.setUuid(certificate);
        User user = userMapper.selectUserForTouchId(row);
        String userName = user.getUserName();
        String userId = user.getUserId();
        // If the user name is null, an error message is returned
        if (userName == null) {
            // Fingerprint authentication failed
            returnResult.put("code", ResultEnum.TOUCHID_LOGIN_ERR);
            return returnResult;
        }
       // If the fingerprint authentication is successful, the user information is returned to the client
       / /... Here the code is omitted, according to their own needs to return user information can be... //
       returnResult.put("code", ResultEnum.LOGIN_SUCCESS);
       return returnResult;
    }
Copy the code

The front-end implementation

In the front end, we need to combine the existing login logic and fingerprint authentication. We need to implement two functions: fingerprint registration and fingerprint login.

The fingerprint register

This function needs to receive three parameters: user name, user ID, and user credentials. We need these three parameters to call the fingerprint device to generate the fingerprint. The specific implementation code is as follows:

    touchIDRegistered: async function(
      userName: string,
      userId: string,
      certificate: string
    ) {
      // Verify that the device supports touchID
      const hasTouchID = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
      if (
        hasTouchID &&
        window.confirm("We have detected that your device supports fingerprint login. Do you enable it?")) {// Update the registration certificate
        this.touchIDOptions.publicKey.challenge = this.base64ToArrayBuffer(
          certificate
        );
        // Update the user name and id
        this.touchIDOptions.publicKey.user.name = userName;
        this.touchIDOptions.publicKey.user.displayName = userName;
        this.touchIDOptions.publicKey.user.id = this.base64ToArrayBuffer(
          userId
        );
        // Call the fingerprint device to create the fingerprint
        const publicKeyCredential = await navigator.credentials.create(
          this.touchIDOptions
        );
        if (publicKeyCredential && "rawId" in publicKeyCredential) {
          // Convert rowId to base64
          const rawId = publicKeyCredential["rawId"];
          const touchId = this.arrayBufferToBase64(rawId);
          const response = publicKeyCredential["response"];
          // Get the client information
          const clientDataJSON = this.arrayBufferToString(
            response["clientDataJSON"]);// Call the register TouchID interface
          this.$api.touchIdLogingAPI
            .registeredTouchID({
              touchId: touchId,
              clientDataJson: clientDataJSON
            })
            .then((res: responseDataType<string>) = > {
              if (res.code === 0) {
                // Save the touchId for fingerprint login
                localStorage.setItem("touchId", touchId);
                return; } alert(res.msg); }); }}}Copy the code

When creating a fingerprint in the above function, we use an object that must be passed to create a fingerprint. The definition of the object and the explanation of each parameter are as follows:

const touchIDOptions = {
  publicKey: {
    rp: { name: "chat-system" }, // Website information
    user: {
      name: ""./ / user name
      id: ""./ / user id (ArrayBuffer)
      displayName: "" / / user name
    },
    pubKeyCredParams: [{type: "public-key".alg: -7 // Accept the algorithm}].challenge: ""./ / credentials (touchIDOptions)
    authenticatorSelection: {
      authenticatorAttachment: "platform"}}}Copy the code

Since some parameters in touchIDOptions need ArrayBuffer type, the data stored in our database is in base64 format, so we need to implement the conversion function between Base64 and ArrayBuffer. The implementation code is as follows:

    base64ToArrayBuffer: function(base64: string) {
      const binaryString = window.atob(base64);
      const len = binaryString.length;
      const bytes = new Uint8Array(len);
      for (let i = 0; i < len; i++) {
        bytes[i] = binaryString.charCodeAt(i);
      }
      return bytes.buffer;
    },
    arrayBufferToBase64: function(buffer: ArrayBuffer) {
      let binary = "";
      const bytes = new Uint8Array(buffer);
      const len = bytes.byteLength;
      for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
      }
      return window.btoa(binary);
    }
Copy the code

After the fingerprint authentication is passed, the client information will be returned in the callback function. The data type is ArrayBuffer, and the format required by the database is string. Therefore, we need to implement the function of transferring ArrayBuffer to String.

    arrayBufferToString: function(buffer: ArrayBuffer) {
      let binary = "";
      const bytes = new Uint8Array(buffer);
      const len = bytes.byteLength;
      for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
      }
      return binary;
    }
Copy the code

Note ⚠️ : The user credentials cannot contain the characters _ and **-**, otherwise the base64ToArrayBuffer function will fail to convert successfully.

Fingerprint login

This function takes two parameters: user credentials and device ID. We will use these two parameters to call the fingerprint device of the client to authenticate the identity. The specific implementation code is as follows:

touchIDLogin: async function(certificate: string, touchId: string) {
      // Verify that the device supports touchID
      const hasTouchID = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
      if (hasTouchID) {
        // Update login credentials
        this.touchIDLoginOptions.publicKey.challenge = this.base64ToArrayBuffer(
          certificate
        );
        / / update the touchID
        this.touchIDLoginOptions.publicKey.allowCredentials[0].id = this.base64ToArrayBuffer(
          touchId
        );
        // Start fingerprint verification
        await navigator.credentials.get(this.touchIDLoginOptions);
        // Invoke the fingerprint login interface
        this.$api.touchIdLogingAPI
          .touchIdLogin({
            touchId: touchId,
            certificate: certificate
          })
          .then((res: responseDataType) = > {
            if (res.code == 0) {
              // Store the current user information
              localStorage.setItem("token", res.data.token);
              localStorage.setItem("refreshToken", res.data.refreshToken);
              localStorage.setItem("profilePicture", res.data.avatarSrc);
              localStorage.setItem("userID", res.data.userID);
              localStorage.setItem("username", res.data.username);
              const certificate = res.data.certificate;
              localStorage.setItem("certificate", certificate);
              // Jump the message component
              this.$router.push({
                name: "message"
              });
              return;
            }
            // Switch back to the login page
            this.isLoginStatus = loginStatusEnum.NOT_LOGGED_IN; alert(res.msg); }); }}Copy the code

Note: ⚠️ : After registering a new fingerprint, the old Touch ID will be invalid, and you can only log in through the new Touch ID. Otherwise, the system cannot adjust the fingerprint device, and an error will be reported: there is a problem with the authentication.

Integrate existing login logic

With the above steps in place, we have implemented the entire fingerprint registration, login logic, let’s look at how to integrate it with existing logins.

Calling fingerprint registration

When the user successfully logs in using the user name, password or third-party platform authorization, we will call the fingerprint registration function to prompt the user whether to authorize the website. The implementation code is as follows:

 authLogin: function(state: string, code: string, platform: string) {
      this.$api.authLoginAPI
        .authorizeLogin({
          state: state,
          code: code,
          platform: platform
        })
        .then(async (res: responseDataType) => {
          if (res.code == 0) {
            / /... Authorized login successful, other code omitted... //
            // Save user credentials for fingerprint login
            const certificate = res.data.certificate;
            localStorage.setItem("certificate", certificate);
            // Verify that the device supports touchID
            const hasTouchID = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
            if (hasTouchID) {
              / /... Other code omitted... //
              // Obtain the Touch ID to check whether the user has authorized the fingerprint login to the website
              this.$api.touchIdLogingAPI
                .getTouchID({
                  userId: userId
                })
                .then(async (res: responseDataType) => {
                  if(res.code ! = =0) {
                    // If the touchId does not exist, ask the user to register the touchId
                    await this.touchIDRegistered(username, userId, certificate);
                  }
                  / / save touchid
                  localStorage.setItem("touchId", res.data);
                  // Jump the message component
                  await this.$router.push({
                    name: "message"
                  });
                });
              return;
            }
            // The device does not support touchID
            await this.$router.push({
              name: "message"
            });
            return;
          }
          // The login fails and the login page is switched back
          this.isLoginStatus = loginStatusEnum.NOT_LOGGED_IN;
          alert(res.msg);
        });
    }
Copy the code

The final result looks like this:

Each time a third-party platform authorizes a login, it checks whether the current user is authorized to log in to the website. If the user is authorized, the Touch ID is saved locally for direct login through fingerprint.

Invoke fingerprint login

After the login page is loaded for 1 second, we will take out the user credentials and Touch ID from the local user. If they exist, we will prompt the user whether to log in to the system by fingerprint. The specific code is as follows:

  mounted() {
    const touchId = localStorage.getItem("touchId");
    const certificate = localStorage.getItem("certificate");
    // If touchId exists, invoke fingerprint login
    if (touchId && certificate) {
      // Prompt the user whether to require touchId login
      setTimeout(() = > {
        if (window.confirm("You have authorized this site to log in with your fingerprint. Do you want to log in now?")) {
          this.touchIDLogin(certificate, touchId); }},1000); }}Copy the code

The final result is as follows:

The project address

For the full address of this code, go to login.vue

  • Online experience address: Chat-system
  • Project GitHub address: Chat-system-Github

Write in the last

I am planning to change my job recently. Is there any company in Guangzhou that can push me to 😁, my wechat: Baymax-kt

  • Feel free to correct any mistakes in the comments section, and if this post helped you, feel free to like and follow 😊
  • This article was originally published in Nuggets and cannot be reproduced without permission at 💌