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
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"
: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:If you remove that trailing slash then I'll bet everything will verify as expected.