speakeasy: Not working with Authy

Having a few issues getting this working with Authy. It works with Google Authenticator no problem. If I have Authenticator and Authy scan the same QR code they’ll give me different OTPs, with Authy’s being incorrect.

Using the latest npm version of speakeasy and qrcode on Node v8.6.0.

generating the secret/url

const speakeasy = require('speakeasy');
const qr = require('qrcode');

const email = 'user@email.com';

const {ascii: secret} = speakeasy.generateSecret({
  issuer: 'MyApp',
  length: 128,
  name: email
});

let url = speakeasy.otpauthURL({
  algorithm: 'sha512',
  issuer: 'MyApp',
  digits: 8,
  secret,
  label: email
});

// Convert otpauth url to qr code url
url = await new Promise((resolve, reject) =>
  qr.toDataURL(url, (e, u) => e ? reject(e) : resolve(u))
);

req.session.otpTempSecret = secret;

verifying the token

const verified = speakeasy.totp.verify({
  algorithm: 'sha512',
  secret: req.session.otpTempSecret,
  digits: 8,
  token: req.body.token.replace(/\D/g, '')
});

if (!verified) throw 'Invalid token';

Only on Authy am I having two issues:

  1. There is no issuer. ‘MyApp’ is not shown, only the user’s email that was passed as a label.
  2. It generates incorrect codes.

I also tried loading the latest version of speakeasy from Github but there was no change to either issues.

Any ideas what I’m doing wrong? I’m assuming somewhere there’s a communication issue between my implementation of speakeasy and Authy as I have no problem with speakeasy on Google Authenticator and no problem with Authy on other sites. I don’t really care about the issuer not showing on Authy but generating incorrect codes is a bit of problem…

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 1
  • Comments: 29

Most upvoted comments

All, sorry for the late response here. I’ve been busy with work and haven’t been able to make plans for the next release because of the breaking changes it would introduce, but at the least I think I can help with your issue, @LukeXF.

@LukeXF: Based on your latest comment, you are generating a secret that has a base32 encoding that starts with FZBWG4J7.... The key issue here is that when you create your own otpauth:// URL with otpauthURL(), you are passing in the base32 secret without specifying the encoding for the secret. By default, otpauthURL() assumes a secret that is passed in without an encoding argument is ASCII-encoded, so it will use base32 to convert it again. (docs)

You can see this if you try to base32-encode FZBWG4J7... – you’ll get the secret that you see in the otpauth:// URL, IZNEEV2H.... This is also why the secret.otpauth_url that you get back from generateSecret() is working correctly, even in Authy, since it uses the correct base32-encoded URL. The solution to this is to pass in the encoding parameter as base32 which will bypass the conversion to base32. Hope this helps.

I still need to look into why this is not working with Authy. It absolutely should support Authy and any other system that implements the spec.

(Sorry for closing the issue – that was accidental.)

Based on this documentation about the otpauth URL syntax, the recommendation seems to be that you include the issuer both as an issue key in the otpauthURL options and as the prefix to the label key.

We also recently discovered that Authy and Google Authenticator on iOS will reject the otpauth URL if the issuer-portion of the label contains a space. (I imagine it would reject it if there is any non-encoded space in the label, but I’ve only tested it in the issuer portion of the label.)

We had been doing this…

// Sample values:
var issuer = "Has Space";
var label = "First Last";

const otpSecrets = speakeasy.generateSecret({length: 10});
let otpAuthUrlOptions = {
  'label': label,
  'secret': otpSecrets.base32,
  'encoding': 'base32'
};
if (issuer) {
  otpAuthUrlOptions.issuer = issuer;
  otpAuthUrlOptions.label = issuer + ':' + label;
}

… but I’m about to change to the following in an attempt to fix this iOS problem (which, as a side note, I probably should have been doing all along). Note the uses of encodeURIComponent():

// Sample values:
var issuer = "Has Space";
var label = "First Last";

const otpSecrets = speakeasy.generateSecret({length: 10});
let otpAuthUrlOptions = {
  'label': encodeURIComponent(label),
  'secret': otpSecrets.base32,
  'encoding': 'base32'
};
if (issuer) {
  otpAuthUrlOptions.issuer = issuer;

  // Note: The issuer and account-name used in the `label` need to be
  // URL-encoded. Presumably speakeasy doesn't automatically do so because
  // the colon (:) separating them needs to NOT be encoded.
  otpAuthUrlOptions.label = encodeURIComponent(issuer) + ':' + encodeURIComponent(label);
}

Here is an example output from speakeasy.otpauthURL(otpAuthUrlOptions);:
otpauth://totp/Has%20Space:First%20Last?secret=HJ2VWR3RF4SEMNZJ&issuer=Has%20Space

Thanks for the findings. I am not sure about the root cause but i can take a look.

@LukeXF The issue is that there are some breaking changes in the next release. I’ve been waiting to do an assessment of what the potential impact of the breaking changes would be, but that takes some time and I haven’t been able to get around to it.