samdjstevens / java-totp

A java library for implementing Time-based One Time Passwords for Multi-Factor Authentication.
MIT License
422 stars 103 forks source link

Basic usage help needed #39

Open shahareldad opened 3 years ago

shahareldad commented 3 years ago

Hi

Sorry for asking here if it's not the place but I tried stackoverflow with no response. I'm trying to validate a token generated by an App (FortiToken Mobile if it matters) but the validator keeps returning false to me. I went over the usage section in the home page here several times but I can't see what I missed. Here is the code I play with - It has the qr code generation, validation, and trying to get a token so I can email the user for him to enter it back.

Any help/direction would be great Thanks

`

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // TODO Auto-generated method stub
    response.getWriter().append("Served at: ").append(request.getContextPath());

    //SecretGenerator secretGenerator = new DefaultSecretGenerator();
    //String secret = secretGenerator.generate();
    String secret = "XAFXRG3TNMLHENVAQTD5ZJOTC2MHTIVE";

    // ========================================
    // QR code generation section
    // ========================================
    QrData data = new QrData.Builder()
               .label("dummyuser@dummy.com")
               .secret(secret)
               .issuer("PORTAL")
               .algorithm(HashingAlgorithm.SHA256) // More on this below
               .digits(6)
               .period(60)
               .build();

    try {
        QrGenerator generator = new ZxingPngQrGenerator();
        byte[] imageData = generator.generate(data);
        String mimeType = generator.getImageMimeType();
        String dataUri = getDataUriForImage(imageData, mimeType);
        response.getWriter().append("\r\ndataUri: " + dataUri);//.append(request.getContextPath());
    } catch (QrGenerationException ex) {
        // TODO Auto-generated catch block
        ex.printStackTrace();
    }

    CodeGenerator codeGenerator = new DefaultCodeGenerator(HashingAlgorithm.SHA256);
    // ========================================
    // Generating a code to email the user
    // ========================================

    try {
    String codeToCheck = codeGenerator.generate(secret, 1); // what should come here instead of the "1"
    // codeToCheck is never the ok - I guess because of the second parameter which I don't know what to send.
    response.getWriter().append("\r\ncodeToCheck: " + codeToCheck);//.append(request.getContextPath());
    } catch (CodeGenerationException ex){
        ex.printStackTrace();
    }

    // ========================================
    // Getting code from the url and checking it
    // ========================================
    String code = request.getQueryString().replace("code=", "");
    response.getWriter().append("\r\nCode: " + code);//.append(request.getContextPath());

    TimeProvider timeProvider = new SystemTimeProvider();
    DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
    verifier.setTimePeriod(60);
    verifier.setAllowedTimePeriodDiscrepancy(2);

    // secret = the shared secret for the user
    // code = the code submitted by the user
    boolean successful = verifier.isValidCode(secret, code);
    if (successful) System.out.println(successful);
    response.getWriter().append("\r\nResult: " + successful);//.append(request.getContextPath());

}

`

samdjstevens commented 3 years ago

Hey - just reading through the code snippet, I see that you are generating and emailing a code to the user here - why exctly are you doing that? You should never need to generate a code yourself, the DefaultCodeGenerator is only used by other classes in the library to verify codes. The codes are generated by the MFA app on your phone.

An example flow would be:

1) Generate a secret and create a QR code from it (as you are doing) 2) Have the MFA app on your phone scan the QR code. 3) The MFA app will now be generating codes every 60 seconds for the secret. 4) You can try entering one of these app generated codes into a servlet that verifies it (as you are doing)

I would also get the code from the query string by doing request.getParameter('code') rather than doing string manipulation on the URL.

Hope this helps

shahareldad commented 3 years ago

Some of the users who will be using our web app will not have smart phones (per their religion) and the best I can do is send the code to their work email so they can enter it back to the web app for verification. This is why I want to generate code and send it to their email and when they enter it back I will verify it.

The main issue in my code is that I couldn't generate a code similar to my app on my smart phone. The secondary issue was that I'm missing something in the code to verify it.

I'm new to Java so I have some learning curve but I already solved the request.getParameter('code') part

Thanks

samdjstevens commented 3 years ago

I see - okay I understand the use case. The problem does indeed lie with the second parameter passed into the generate method on the CodeGenerator. This param is the current "time bucket" counter - which is basically the number of full 60 seconds (as that is the time period you are using) since Jan 1st 1970, 00:00.

You can see how the library calculates and uses this counter number on this line in the DefaultCodeVerifier: https://github.com/samdjstevens/java-totp/blob/master/totp/src/main/java/dev/samstevens/totp/code/DefaultCodeVerifier.java#L30

So what you will need to do is replace the 1 with Math.floorDiv(timeProvider.getTime(), 60) (move the instantiation of SystemTimeProvider object up a bit so you have access to it here).

That should then start generating valid codes.

shahareldad commented 3 years ago

Thanks I will try that at work on Sunday and let you know.

shahareldad commented 3 years ago

Hi

It failed to work. I moved the timeProvider as you suggested and sent Math.floorDiv(timeProvider.getTime(), 60)

Here is the updated code:

`

    CodeGenerator codeGenerator = new DefaultCodeGenerator(HashingAlgorithm.SHA256);
    // ========================================
    // Generating a code to email the user
    // ========================================

    TimeProvider timeProvider = new SystemTimeProvider();

    try {
    String codeToCheck = codeGenerator.generate(secret, Math.floorDiv(timeProvider.getTime(), 60)); // what should come here instead of the "1"
    // codeToCheck is never the ok - I guess because of the second parameter which I don't know what to send.
    response.getWriter().append("\r\nCode generated: " + codeToCheck);//.append(request.getContextPath());
    } catch (CodeGenerationException ex){
        ex.printStackTrace();
    }

    // ========================================
    // Getting code from the url and checking it
    // ========================================
    String code = request.getParameter("code");
    response.getWriter().append("\r\nCode from request: " + code);//.append(request.getContextPath());

    DefaultCodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider);
    verifier.setTimePeriod(60);
    verifier.setAllowedTimePeriodDiscrepancy(2);

    // secret = the shared secret for the user
    // code = the code submitted by the user
    boolean successful = verifier.isValidCode(secret, code);
    if (successful) System.out.println(successful);
    response.getWriter().append("\r\nIs code verified: " + successful);//.append(request.getContextPath());

`

vishalzanzrukia commented 1 year ago

Math.floorDiv(timeProvider.getTime(), 60)

I had same requirements, this worked for me, thank you