Fast access to Google two-step authentication Google Authenticator

Posted by whare on Fri, 24 Dec 2021 03:47:55 +0100

(1) Introduction

Since you read this article, you should know what Google's two-step authentication is for. Here is another link to download the app

(apkpure search Google authenticator)

Explanation of verification principle:

  1. Find the 32-bit random code bound before the login user in the database (the code is generally stored in the database)
  2. Call API to input 32-bit random code and generate correct 6-bit verification code (it will change every 1min)
  3. Match the 6-digit verification code entered by the user with the correct 6-digit verification code. If it is the same, the login is successful, and if it is different, the verification code time is invalid or wrong

User binding explanation:

  1. Call API to generate 32-bit random code and prepare to bind it to the user
  2. Call API to generate QR string of QR code. User information (such as mailbox, id, nickname, etc.), title and generated 32-bit random code need to be passed in
  3. Call API to convert QR string of QR code into picture and display it on front-end page in Base64 mode
  4. After adding by scanning the app code, click confirm binding on the front page and enter the 6-digit verification code you see this time
  5. According to the 32-bit random code obtained this time, the user information (used to determine the user record in the database) and the 6-bit verification code entered, the back end obtains the correct 6-bit verification code by passing in the 32-bit random code through the API. When it is the same as the entered verification code, the binding is successful and the 32-bit random code is persisted to the corresponding user record in the database

(2) Preparatory work

Import Maven dependency

         <!--google Two step certification related-->
        <dependency>
            <groupId>de.taimos</groupId>
            <artifactId>totp</artifactId>
            <version>1.0</version>
        </dependency>

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.10</version>
        </dependency>

        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.2.1</version>
        </dependency>

Import tool class Google authentication tool

import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import de.taimos.totp.TOTP;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;
import sun.misc.BASE64Encoder;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URLEncoder;
import java.security.SecureRandom;

/**
 * @Author bilibili-nanoda
 * @Date 2021/8/13 10:33
 * @Version 1.0
 */
public class GoogleAuthenticationTool {

    public static String generateSecretKey() {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        Base32 base32 = new Base32();
        return base32.encodeToString(bytes);
    }

    /**
     * Obtain the correct 6-bit number according to the 32-bit random code
     *
     * @param secretKey
     * @return
     */
    public static String getTOTPCode(String secretKey) {
        Base32 base32 = new Base32();
        byte[] bytes = base32.decode(secretKey);
        String hexKey = Hex.encodeHexString(bytes);
        return TOTP.getOTP(hexKey);
    }


    /**
     * Generate binding QR code (string)
     *
     * @param account   Account information (displayed in Google Authenticator App)
     * @param secretKey secret key
     * @param title     Title (displayed in Google Authenticator App)
     * @return
     */
    public static String spawnScanQRString(String account, String secretKey, String title) {
        try {
            return "otpauth://totp/"
                    + URLEncoder.encode(title + ":" + account, "UTF-8").replace("+", "%20")
                    + "?secret=" + URLEncoder.encode(secretKey, "UTF-8").replace("+", "%20")
                    + "&issuer=" + URLEncoder.encode(title, "UTF-8").replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Generate QR code (file) [return base64 of the picture, and output to the file synchronously if the output path is specified]
     *
     * @param barCodeData QR code string information
     * @param outPath     Output address
     * @param height
     * @param width
     * @throws WriterException
     * @throws IOException
     */
    public static String createQRCode(String barCodeData, String outPath, int height, int width)
            throws WriterException, IOException {
        BitMatrix matrix = new MultiFormatWriter().encode(barCodeData, BarcodeFormat.QR_CODE,
                width, height);
        BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(matrix);

        ByteArrayOutputStream bof = new ByteArrayOutputStream();
        ImageIO.write(bufferedImage, "png", bof);
        String base64 = imageToBase64(bof.toByteArray());
        if(outPath!=null&&!outPath.equals("")) {
            try (FileOutputStream out = new FileOutputStream(outPath)) {
                MatrixToImageWriter.writeToStream(matrix, "png", out);
            }
        }
        return base64;
    }

    /**
     * Convert the picture file to base64 string, and the parameter is the path of the picture
     *
     * @param dataBytes
     * @return java.lang.String
     */
    private static String imageToBase64(byte[] dataBytes) {
        // Encode byte array Base64
        BASE64Encoder encoder = new BASE64Encoder();
        if (dataBytes != null) {
            return "data:image/jpeg;base64," + encoder.encode(dataBytes);// Returns a Base64 encoded byte array string
        }
        return null;
    }

}

(3) Use process

Tips: actually, you already know how to use the tool class, but I still post my code for reference

  • First binding logic judgment

Judge whether the login user has a 32-bit random code in the login of UserController

    //Login logic
    @PostMapping("/login")
    public String login(WebLoginDTO webLoginDTO, HttpSession httpSession, Model model, HttpServletRequest httpServletRequest,RedirectAttributes redirectAttributes) {
        System.out.println("Try to log in:" + webLoginDTO.getEmail() + ":" + webLoginDTO.getEmail());
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(webLoginDTO.getEmail(), webLoginDTO.getPassword());
        try {
            subject.login(token);
        } catch (IncorrectCredentialsException e) {
            e.printStackTrace();
            model.addAttribute("msg", "Password error");
            return "error/systemError";
        } catch (AuthenticationException e) {
            e.printStackTrace();
            model.addAttribute("msg", "Account does not exist");
            return "error/systemError";
        }
        //Description login succeeded
        ActiveUser activeUser = (ActiveUser) subject.getPrincipal();
        if (activeUser.isLokced()) {
            model.addAttribute("msg", "Account blocked");
            return "error/systemError";
        }
        //There is no 32-bit random code
        if(activeUser.getTwoFactorCode()==null||activeUser.getTwoFactorCode().equals(""))            
        {
            //Go to code binding page
            redirectAttributes.addAttribute("userId",activeUser.getUser_id());
            //todo processing design: the page is bound with Google authentication code (QR QR code)
            return "redirect:/user/bindingGoogleTwoFactorValidate";
        }

If it does not exist, it will be directed to the binding page (to carry user information, such as id)

    /**
     * Go to Google two-step verification binding page
     * @param userId
     * @return
     */
    @GetMapping("/bindingGoogleTwoFactorValidate")
    public String toBindingGoogleTwoFactorValidate(@RequestParam("userId")int userId,Model model){
        String randomSecretKey = GoogleAuthenticationTool.generateSecretKey();
        User user = userService.getUserByUserId(userId);
        //The parameters set in this step are the parameters displayed after App code scanning
        String qrCodeString = GoogleAuthenticationTool.spawnScanQRString(user.getEmail(),randomSecretKey,"pilipili2333");
        String qrCodeImageBase64 = null;
        try {
             qrCodeImageBase64 = GoogleAuthenticationTool.createQRCode(qrCodeString,null,512,512);
        } catch (WriterException | IOException e) {
            e.printStackTrace();
        }
        model.addAttribute("randomSecretKey",randomSecretKey);
        model.addAttribute("qrCodeImageBase64",qrCodeImageBase64);

        return "bindingGoogleTwoFactorValidate";
    }

The front-end page initiates ajax binding and enters the 6-digit verification code for verification

function confirmBinding() {
    var googleRegex =/\d{6}/;
    var inputGoogleCode = window.prompt("Please enter 6 digits google Verification Code");
    if(googleRegex.test(inputGoogleCode)){
        $.ajax({
            url:"[[@{/user/bindingGoogleTwoFactorValidate}]]",
            type:"post",
            data:{
                "userId":"[[${param.userId}]]",
                "randomSecretKey":"[[${randomSecretKey}]]",
                "inputGoogleCode":inputGoogleCode

            },
            dataType:"json",
            success:function (data) {
                if(data.state==='success'){
                    window.alert("Binding succeeded");
                }else if(data.state==='fail'){
                    window.alert("Operation failed:"+data.msg);
                }
            }
        });
    }else {
        window.alert("Please input 6 digits correctly google Verification Code")
    }

}

The backend performs another verification on whether the 6-digit verification code is correct for binding execution

     /**
     * Perform Google two-step validation binding
     * @return
     */
    @PostMapping("/bindingGoogleTwoFactorValidate")
    @ResponseBody
    public String bindingGoogleTwoFactorValidate(@RequestParam("userId")int userId,@RequestParam("randomSecretKey")String randomSecretKey,@RequestParam("inputGoogleCode")String inputGoogleCode){
        JSONObject respJsonObj =new JSONObject();
        User user = userService.getUserByUserId(userId);
        if(user.getTwoFactorCode()!=null&&!user.getTwoFactorCode().equals("")){
            respJsonObj.put("state","fail");
            respJsonObj.put("msg","The user has been bound and cannot be bound repeatedly. If you accidentally delete the token, please contact the administrator to reset it");
            return respJsonObj.toString();
        }
        String rightCode =GoogleAuthenticationTool.getTOTPCode(randomSecretKey);
        if(!rightCode.equals(inputGoogleCode)){
            respJsonObj.put("state","fail");
            respJsonObj.put("msg","Verification code is invalid or wrong, please try again");
            return respJsonObj.toString();
        }
        user.setTwoFactorCode(randomSecretKey);
        int res = userService.updateUserByUser(user);

        if(res>0){
            respJsonObj.put("state","success");
        }else {
            respJsonObj.put("state","fail");
            respJsonObj.put("msg","Database operation failed");
        }
        return respJsonObj.toString();
    }
  • Logic for checking 6-digit verification code during login

Handled in the login method of UserController

@PostMapping("/login")
    public String login(WebLoginDTO webLoginDTO, HttpSession httpSession, Model model, HttpServletRequest httpServletRequest,RedirectAttributes redirectAttributes) {
        System.out.println("Try to log in:" + webLoginDTO.getEmail() + ":" + webLoginDTO.getEmail());
        /*
         shiro Certification related code...
        */
        //Note: effective within 1min
        String rightGoogleCode = GoogleAuthenticationTool.getTOTPCode(activeUser.getTwoFactorCode());
        if(!webLoginDTO.getGoogleCode().equals(rightGoogleCode)){
            model.addAttribute("msg","Google verification code is incorrect or timed out");
            return "error/systemError";
        }
        /*
         Subsequent logic
        */
        
}

Note:

Different from SMS verification and email verification, the generation and refresh of verification code are controlled by ourselves. For this Google two-step authentication, it is refreshed once in 1min. For the same time, we have agreed on a set of encryption and decryption rules in advance. Therefore, when verifying the input 6-bit verification code, you should obtain the correct 6-bit code after input, rather than generating the correct code in advance and waiting for user input. The latter may cause frequent verification code invalidation due to the delay problem (the user's action is very touching, the on the app has been updated, but the system retains the last time)

For more tutorials, see my official website: the most cool online tutorial: www.zuikakuedu.top

Topics: Java Google JavaEE Maven Spring