I tried implementing a fingerprint-based authentication using DUO-lab's Python's webauthn package. I however ran into this error:

server validation of credential failed: registration failed. error: registration rejected. error: unable to verify origin..

When I checked the package's source code, I noticed this error unable to verify origin.. was raised when maybe your authenticator wasn't properly configured.

Is there a way I can specifically state that I only need platform authenticators rather than roaming authenticators without thickering with the package's source code? If there is, kindly include a full working code for Flask(This is what I am using now after the error chased me out of Django). My current configurations are:

RP_ID = 'nacesdecide.herokuapp.com' #The app is currently hosted on heroku
RP_NAME = 'nacesdecides nacesdecide'
ORIGIN = 'https://nacesdecide.herokuapp.com/'

The application is currently on heroku and can be accessed live via naces register. I want the application to use platform authenticators alone.

Update:

Some part of the code, on the client side (drafted from duo-lab's python webauthn flask demon js, is:

/**
 * REGISTRATION FUNCTIONS
 */

/**
 * Callback after the registration form is submitted.
 * @param {Event} e
 */
const didClickRegister = async (e) => {
  e.preventDefault();

  // gather the data in the form
  const form = document.querySelector("#register-form");
  const formData = new FormData(form);

  // post the data to the server to generate the PublicKeyCredentialCreateOptions
  let credentialCreateOptionsFromServer;
  try {
    credentialCreateOptionsFromServer = await getCredentialCreateOptionsFromServer(
      formData
    );
  } catch (err) {
    showErrorAlert(`Failed to generate credential request options: ${err}`);
    return console.error("Failed to generate credential request options:", err);
  }

  // convert certain members of the PublicKeyCredentialCreateOptions into
  // byte arrays as expected by the spec.
  const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
    credentialCreateOptionsFromServer
  );

  // request the authenticator(s) to create a new credential keypair.
  let credential;
  *try {
    credential = await navigator.credentials.create({
      publicKey: publicKeyCredentialCreateOptions,
    });*
  } catch (err) {
    showErrorAlert(`Error creating credential: ${err}`);
    return console.error("Error creating credential:", err);
  }

  // we now have a new credential! We now need to encode the byte arrays
  // in the credential into strings, for posting to our server.
  const newAssertionForServer = transformNewAssertionForServer(credential);

  // post the transformed credential data to the server for validation
  // and storing the public key
  let assertionValidationResponse;
  try {
    assertionValidationResponse = await postNewAssertionToServer(
      newAssertionForServer
    );
  } catch (err) {
    showErrorAlert(`Server validation of credential failed: ${err}`);
    return console.error("Server validation of credential failed:", err);
  }

  // reload the page after a successful result
  setTimeout(function () {
    window.location.href = Flask.url_for("accounts.login");
  }, 1000);
  //   window.location.reload();
};

On the server side, we have:

def webauthn_begin_activate():
    # MakeCredentialOptions
    username = request.form.get('register_username')
    display_name = request.form.get('register_display_name')

    if not util.validate_username(username):
        return make_response(jsonify({'fail': 'Invalid username.'}), 401)
    if not util.validate_display_name(display_name):
        return make_response(jsonify({'fail': 'Invalid display name.'}), 401)

    if User.query.filter_by(username=username).first():
        return make_response(jsonify({'fail': 'User already exists.'}), 401)

    #clear session variables prior to starting a new registration
    session.pop('register_ukey', None)
    session.pop('register_username', None)
    session.pop('register_display_name', None)
    session.pop('challenge', None)

    session['register_username'] = username
    session['register_display_name'] = display_name

    challenge = util.generate_challenge(32)
    ukey = util.generate_ukey()

    # We strip the saved challenge of padding, so that we can do a byte
    # comparison on the URL-safe-without-padding challenge we get back
    # from the browser.
    # We will still pass the padded version down to the browser so that the JS
    # can decode the challenge into binary without too much trouble.
    session['challenge'] = challenge.rstrip('=')
    session['register_ukey'] = ukey

    *make_credential_options = webauthn.WebAuthnMakeCredentialOptions(
        challenge, RP_NAME, RP_ID, ukey, username, display_name,
        'https://example.com')*

    return jsonify(make_credential_options.registration_dict)

This function might also be of interest:

def verify_credential_info():
    challenge = session['challenge']
    username = session['register_username']
    display_name = session['register_display_name']
    ukey = session['register_ukey']

    registration_response = request.form
    trust_anchor_dir = os.path.join(
        os.path.dirname(os.path.abspath(__file__)), TRUST_ANCHOR_DIR)
    trusted_attestation_cert_required = True
    self_attestation_permitted = True
    none_attestation_permitted = True

    webauthn_registration_response = webauthn.WebAuthnRegistrationResponse(
        RP_ID,
        ORIGIN,
        registration_response,
        challenge,
        trust_anchor_dir,
        trusted_attestation_cert_required,
        self_attestation_permitted,
        none_attestation_permitted,
        uv_required=False)  # User Verification

    try:
        webauthn_credential = webauthn_registration_response.verify()
    except Exception as e:
        return jsonify({'fail': 'Registration failed. Error: {}'.format(e)})

    # Step 17.
    #
    # Check that the credentialId is not yet registered to any other user.
    # If registration is requested for a credential that is already registered
    # to a different user, the Relying Party SHOULD fail this registration
    # ceremony, or it MAY decide to accept the registration, e.g. while deleting
    # the older registration.
    credential_id_exists = User.query.filter_by(
        credential_id=webauthn_credential.credential_id).first()
    if credential_id_exists:
        return make_response(
            jsonify({
                'fail': 'Credential ID already exists.'
            }), 401)

    existing_user = User.query.filter_by(username=username).first()
    if not existing_user:
        if sys.version_info >= (3, 0):
            webauthn_credential.credential_id = str(
                webauthn_credential.credential_id, "utf-8")
            webauthn_credential.public_key = str(
                webauthn_credential.public_key, "utf-8")
        user = User(
            ukey=ukey,
            username=username,
            display_name=display_name,
            pub_key=webauthn_credential.public_key,
            credential_id=webauthn_credential.credential_id,
            sign_count=webauthn_credential.sign_count,
            rp_id=RP_ID,
            icon_url='https://example.com')
        db.session.add(user)
        db.session.commit()
    else:
        return make_response(jsonify({'fail': 'User already exists.'}), 401)

    flash('Successfully registered as {}.'.format(username))

    return jsonify({'success': 'User successfully registered.'})

Second update: The full log below is what I have got:

webauthn.js:101 
{id: "ATdDPQneoYF3tA6HYW8_dr2eBDy53VNoEIHRWUDfnmT2URKIs0SQ_lQ7BujdmcfM9Hc2xNH8bvLf4k3lQJ-7RX4", 
rawId: "ATdDPQneoYF3tA6HYW8_dr2eBDy53VNoEIHRWUDfnmT2URKIs0SQ_lQ7BujdmcfM9Hc2xNH8bvLf4k3lQJ-7RX4",
 type: "public-key", 
 attObj: "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFD32HDgTSvc6zIlggmLLxXTKQyiabSwuLWNiTpJ3WQfmMoC_qX_QTuWPWHo4", 
 clientData: "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIj9pZFBhY2thZ2VOYW1lIjoiY29tLmFuZHJvaWQuY2hyb21lIn0", …}
attObj: "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFD32HDgTSvcJxUiUIT6ViS4biCWKTR25PIW3beO9V5NdFAAAAALk_2WHy5kYvsSKCACJH3ngAQQE3Qz0J3qGBd7QOh2FvP3a9ngQ8ud1TaBCB0VlA355k9lESiLNEkP5UOwbo3ZnHzPR3NsTR_G7y3-JN5UCfu0V-pQECAyYgASFYID93HTRf5UtMsCsW9D5TyWQDSgMW2MDhiYWKnz3sq16zIlggmLLxXTKQyiabSwuLWNiTpJ3WQfmMoC_qX_QTuWPWHo4"
clientData: "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoidFNOS3g5RnVyWFI4dlhVdVBkVms5azhDcEhlMWMydnlrbkdwYUhseXZKYyIsIm9yaWdpbiI6Imh0dHBzOlwvXC9uYWNlc2RlY2lkZS5oZXJva3VhcHAuY29tIiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoiY29tLmFuZHJvaWQuY2hyb21lIn0"
id: "ATdDPQneoYF3tA6HYW8_dr2eBDy53VNoEIHRWUDfnmT2URKIs0SQ_lQ7BujdmcfM9Hc2xNH8bvLf4k3lQJ-7RX4"
rawId: "ATdDPQneoYF3tA6HYW8_dr2eBDy53VNoEIHRWUDfnmT2URKIs0SQ_lQ7BujdmcfM9Hc2xNH8bvLf4k3lQJ-7RX4"
registrationClientExtensions: "{}"
type: "public-key"__proto__: Object

webauthn.js:107 Server validation of credential failed: Registration failed. Error: Registration rejected. Error: Unable to verify origin..
didClickRegister @ webauthn.js:107
async function (async)
didClickRegister @ webauthn.js:68
2

There are 2 answers

6
IAmKale On BEST ANSWER

I think the issue is that there's a trailing slash on your ORIGIN value.

Peering into the attestation response's cliendDataJSON, the origin is reported as "https://nacesdecide.herokuapp.com":

clientDataJSON origin value

Looking at how the Duo WebAuthn library verifies this response, the basic origin comparison is failing because your ORIGIN of "https://nacesdecide.herokuapp.com/" is not equivalent to the response's origin:

Response: "https://nacesdecide.herokuapp.com"
ORIGIN:   "https://nacesdecide.herokuapp.com/"

If you remove that trailing slash then I'll bet everything will verify as expected.

0
Sirjon On

@IAmKale answer solved the initial problem. However, it is important to be aware that you might run into server error: unexpected token < in JSON at position 0. I haven't found a concrete solution to this but ensuring that distinct username is used for registration fixed it. Also, it seems distinct devices are required for multiple registrations — one device per registration.