DUO-LABS WebAuthn: Server validation of credential failed: registration failed. error: registration rejected. error: unable to verify origin

DUO-LABS WebAuthn: Server validation of credential failed: registration failed. error: registration rejected. error: unable to verify origin

我尝试使用 DUO-lab 的 Python 的 webauthn 包实现基于指纹的身份验证。然而我 运行 进入这个错误:

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

当我检查包的源代码时,我注意到这个错误 unable to verify origin.. 可能是您的身份验证器配置不正确引起的。

有没有一种方法可以明确说明我只需要 platform 个身份验证器而不是 roaming 个身份验证器,而无需增加包的源代码?如果有,请包含 Flask 的完整工作代码(这是我在错误将我赶出 Django 后现在使用的代码)。我当前的配置是:

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

该应用程序目前在 heroku 上,可以通过 naces register 实时访问。我希望应用程序单独使用 platform authenticators

更新:

部分代码,在客户端(取自duo-lab's python webauthn flask demon js,是:

/**
 * 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();
};

在服务器端,我们有:

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)

这个函数可能也很有趣:

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.'})

第二次更新:下面的完整日志是我得到的:

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

我认为问题在于您的 ORIGIN 值中有尾部斜线。

Peering into the attestation response's cliendDataJSON,来源报告为"https://nacesdecide.herokuapp.com":

Looking at how the Duo WebAuthn library verifies this response,基本原点比较失败,因为 "https://nacesdecide.herokuapp.com/"ORIGIN 不等同于响应的原点:

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

如果您删除尾部的斜杠,那么我敢打赌一切都会按预期进行验证。

@IAmKale 的回答解决了最初的问题。但是,请务必注意,您可能 运行 变成 server error: unexpected token < in JSON at position 0。我还没有找到具体的解决方案,但确保使用 distinct username 进行注册修复了它。此外,似乎多次注册需要不同的设备——每次注册一台设备。