Open Source & Free  

TIP: Activate via URL and Send Arguments

TIP: Activate via URL and Send Arguments

Header Image

The most secure password in the world is the one that doesn’t exist. You remove the user from the equation with a completely random key. To be fair this has some drawbacks and a password still exists somewhere (in your phone/email) but generally this works rather well…​

The trick is simple, if we want to authenticate a user we can email him a single use URL e.g. mycoolapp://act-32548b09-d328-4330-8243-d7d30c322e40. As you can see that’s pretty hard to guess or brute force. Once clicked the URL becomes invalid so even if it’s exposed somehow it would still be irrelevant. To do this we need two parts:

  • The server logic

  • Client URL handling

Both are pretty easy.

The Server

One caveat is that the mycoolapp will work on the device but you can’t click it in an email or in a browser. So we will need an https URL from your server.

The server would look something like this, notice that this is Spring Boot Controller code but you should be able to use any server out there:

public boolean sendSigninEmail(String e) {
    List<UserObj> ul = users.findByEmailIgnoreCase(e);
    if(ul.isEmpty()) {
        return false;
    }
    UserObj u = ul.get(0);
    u.setHashedActivationToken(UUID.randomUUID().toString()); (1)
    users.save(u); (2)
    email.sendEmail(e, "Signin to the Codename One App", "This is a one time link to activate the Codename One App. Click this link on your mobile device: nnhttps://ourserverurl.com/app/activateURL?token=act-" + u.getHashedActivationToken()); (3)
    return true;
}
public User activateViaToken(String t) throws ServerAppAPIException {
    List<UserObj> ul = users.findByHashedActivationToken(t); (4)
    if(ul.isEmpty()) {
        throw new ServerAppAPIException(ServerErrorCodes.NOT_FOUND);
    }
    UserObj u = ul.get(0);
    String val = u.getAppToken(); (5)
    u.setHashedActivationToken(null); (6)
    users.save(u);
    User r = u.getUser();
    r.setAppToken(u.getAppToken());
    return r;
}
1 We use UUID to generate the long activation string
2 We save it in the database overwriting an older URL if it exists
3 We can send an email or SMS with the HTTPS URL to activate the app
4 Next we activate the user account with the received token. We find the right account entry
5 An access token is a secure password generated by the server that’s completely random and only visible to the app
6 The activation token used in the URL is removed now making the URL a single use tool

All of that is mostly simple but there is still one missing piece. Our app will expect a mycoolapp URL and an HTTPS URL won’t launch it. The solution is a 302 redirect:

@RequestMapping(value="/activateURL", method=RequestMethod.GET)
public void activateURL(@RequestParam String token, HttpServletResponse httpServletResponse)  {
    httpServletResponse.setHeader("Location", "mycoolapp://" + token);
    httpServletResponse.setStatus(302);
}

This sends the device to the mycoolapp URL automatically and launches your app with the token!

Client Side

On the client we need to intercept the mycoolapp URL and parse it. First we need to add two new build hints:

android.xintent_filter=<intent-filter>   <action android_name="android.intent.action.VIEW" />    <category android_name="android.intent.category.DEFAULT" />    <category android_name="android.intent.category.BROWSABLE" />    <data android_scheme="mycoolapp" />  </intent-filter>
ios.plistInject=<key>CFBundleURLTypes</key>     <array>         <dict>             <key>CFBundleURLName</key>             <string>com.mycompany.myapp.package.name</string>         </dict>         <dict>             <key>CFBundleURLSchemes</key>             <array>                 <string>mycoolapp</string>             </array>         </dict>     </array>
Don’t forget to fix mycoolapp and com.mycompany.myapp.package.name to the appropriate values in your app

Next all we need to do is detect the URL in the start() method. This needs to reside before the code that checks the current Form:

String arg = getProperty("AppArg", null); (1)
if(arg != null) {
    if(arg.contains("//")) { (2)
        List<String> strs = StringUtil.tokenize(arg, "/");
        arg = strs.get(strs.size() - 1);
        while(arg.startsWith("/")) {
            arg = arg.substring(1);
        }
    }
    if(!arg.startsWith("act-")) { (3)
        showLoginForm();
        callSerially(() ->
            Dialog.show("Invalid Key", "The Activation URL is invalid", "OK", null));
        return;
    }
    arg = arg.substring(4);
    Form activating = new Form("Activating", new BorderLayout(BorderLayout.CENTER_BEHAVIOR_CENTER));
    activating.add(CENTER, new InfiniteProgress());
    activating.show();
    sendActivationTokenToServer(arg); (4)
    return;
}
1 This is from the CN class globally imported. The app argument is the URL
2 We remove the URL portion of the argument
3 The act- prefix is there to validate the URL is correct
4 This sends the activation key to the server logic we discussed above

Testing in The Simulator

This will work in iOS and Android. Starting next week you could also test this on the simulator using the new Send App Argument menu option in the simulator.

To integrate this properly into an app you would normally have a login menu that accepts only the email/phone. Or a system in your web based UI to send an invite link to the app.

Whatsapp uses an inverse of this trick to activate their desktop app. They show a QR code to your device and once you scan that QR code with your whatsapp phone install the desktop version is activated. That’s much better than passwords.

1 Comment

  • Francesco Galgani says:

    Thank you Shai for this article, the server redirect is a great idea to circumvent the “impossible to click” links with a custom protocol in Gmail. Some email providers allow to click any link with any custom protocol, others block any custom protocol. I circumvented this issue to send an activation link with a completely different approach, however your redirect solution is very good 😉

Leave a Reply