Laragear / webpass

The most simple WebAuthn (Passkeys) helper for browsers.
https://github.com/sponsors/DarkGhostHunter
MIT License
8 stars 0 forks source link

[1.2] AttestationCancelled: The credentials creation was cancelled by the user or a timeout. #10

Closed Thotsuya closed 7 months ago

Thotsuya commented 7 months ago

PHP & Platform

8.2 & Mac OS Ventura 13.6.3

Database

MySQL 8.0.33

Laravel version

10.48.2

Have you done this?

Expectation

The attest should work and return a success credential.

Description

When I try to create a passkey, even if I successfully set my fingerprint, scan a QR on my phone or whatever, I get the error AttestationCancelled: The credentials creation was cancelled by the user or a timeout.

image

This is what the /register/options endpoint returns:

{
    "rp": {
        "name": "langsys"
    },
    "authenticatorSelection": {
        "userVerification": "discouraged"
    },
    "user": {
        "name": "jarliev2@gmail.com",
        "displayName": null,
        "id": "fb66426bf5bb47e38d75932a99cf1f8c"
    },
    "pubKeyCredParams": [
        {
            "type": "public-key",
            "alg": -7
        },
        {
            "type": "public-key",
            "alg": -257
        }
    ],
    "attestation": "none",
    "excludeCredentials": [],
    "timeout": 10000000,
    "challenge": "FCRcwVcUm_8kAhJVOc08CQ"
}

Reproduction

const createPasskey = async () => {
    if (await Webpass.isUnsupported()) {
        return alert("Your browser doesn't support WebAuthn.")
    }

    const {credential, success, error, id} = await Webpass.attest("/webauthn/register/options", {
        path: "/webauthn/register",
        body: {
            alias: passKeyAlias.value
        }
    })

    if (success) {
        console.log("Credential", credential)
        console.log("ID", id)
        successfullyCreated.value = true
    } else {
        console.error(error)
    }
}

Stack trace & logs

passkeys:64 AttestationCancelled: The credentials creation was cancelled by the user or a timeout.
    at newError (https://langsys-nova.test/nova-api/scripts/passkeys:1135:19)
    at attestRaw (https://langsys-nova.test/nova-api/scripts/passkeys:1191:19)
    at async Object.attest (https://langsys-nova.test/nova-api/scripts/passkeys:1159:48)
    at async Object.attest (https://langsys-nova.test/nova-api/scripts/passkeys:1264:42)
DarkGhostHunter commented 7 months ago

Well, if you see the source code, it will return an error when the navigator returns no credentials at all.

// Let the browser sign the challenge with a credential, returning a response
const credentials: Credential | null = await navigator.credentials.get({
    publicKey: parseServerRequestOptions(assertionOptions)
})

// If the user denied the permission, return null
if (!credentials) {
    throw newError("AssertionCancelled", "The credentials request was cancelled by the user or timeout.")
}

I'll add debug lines on 1.3.0 on a while, hold on.

Thotsuya commented 7 months ago

Using 1.3.0 This is the log:

Attestations Options Received { "rp": { "name": "langsys" }, "authenticatorSelection": { "userVerification": "discouraged" }, "user": { "id": "62d4839e46264793b696d65601bb66fe", "name": "jarliev2@gmail.com", "displayName": null }, "pubKeyCredParams": [ { "type": "public-key", "alg": -7 }, { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -8 } ], "attestation": "none", "excludeCredentials": [], "timeout": 40000000, "challenge": "0h3_nnVCTaVIxxXLvcoYew" }

Attestations Credentials Created

image

Probably not the place to post this, but I'm lost here @DarkGhostHunter

DarkGhostHunter commented 7 months ago

Which is weird because either the credentials are not null nor empty.

Going to make some tests locally and see if I can get the same error.

DarkGhostHunter commented 7 months ago

@Thotsuya Try v1.3.1. It makes a less strict check for the credentials from the navigator. If it's falsy, it will return the error. Otherwise, it should proceed with attestation normally.

Thotsuya commented 7 months ago

@DarkGhostHunter

[Error] InvalidCharacterError: The string contains invalid characters.
btoa
arrayToBase64UrlString — passkeys:1820
parseOutgoingCredentials — passkeys:1795
(anonymous function) — passkeys:1893:147

    _callee$ (passkeys:65)
    tryCatch (passkeys:19:1067)
    (anonymous function) (passkeys:19:3021)
    asyncGeneratorStep (passkeys:20:104)
    _next (passkeys:21:213)
image
Thotsuya commented 7 months ago

Seems to be an error here with the ArrayBuffer

// Maintain future compatibility for WebAuthn 3.0 if some credential properties are already strings
    if ("rawId" in credentials) {
        response.rawId = isArrayBuffer(credentials.rawId)
            ? arrayToBase64UrlString(credentials.rawId)
            : credentials.rawId
    }

and


function arrayToBase64UrlString(arrayBuffer: ArrayBuffer): string {
    return btoa(new TextDecoder().decode(arrayBuffer))
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=/g, "")
}
danielmwaveges commented 7 months ago

Yes I get the same issues and after bypassing the arraybuffer decoding using a custom function, this assignment fails due to Credentials object having non enumerable keys. https://github.com/Laragear/webpass/blob/361b6f9be116c8a9593ea1182061d84d094f3c88/src/webpass.ts#L47-L53

DarkGhostHunter commented 7 months ago

Since this is a breaking bug existing since the first version, I'll take the liberty to fix it and rollback to 1.0 entirely.

DarkGhostHunter commented 7 months ago

Figured out you can't "replace" a version. I will post this as 2.0.

DarkGhostHunter commented 7 months ago

@Thotsuya @danielmwaveges Try v2.0 and report back.

The main difference is that I'm using another library to interact with WebAuthn instead of doing it directly, which parses the credentials automatically. Also, shoved in some goodies while at it like Conditional UI support.

Thotsuya commented 7 months ago

@DarkGhostHunter Ok, It seems to work and it is sending the credentials to the webauthn/register endpoint, however, the default webauthn/register endpoint from the Webauthn package throws the following:

{
    "status": false,
    "data": [],
    "errors": {
        "line": 71,
        "file": "/Users/jarliev/Herd/langsys-nova/vendor/laravel/framework/src/Illuminate/Validation/ValidationException.php",
        "message": [
            "Attestation Error: ByteBuffer: Invalid offset or length."
        ]
    }
}

Credentials:

[Debug] Attestation Credentials Created – {id: "AZif0HxM24R4UTZfQg3GlysdL6VVod-3B1bE4rr3FZeRciis92GelrnqMkW4EanZ4K4P1eWa4ExLs1oJ8YA8g3U", rawId: "AZif0HxM24R4UTZfQg3GlysdL6VVod-3B1bE4rr3FZeRciis92GelrnqMkW4EanZ4K4P1eWa4ExLs1oJ8YA8g3U", response: Object, …} (passkeys, line 1171)
{id: "AZif0HxM24R4UTZfQg3GlysdL6VVod-3B1bE4rr3FZeRciis92GelrnqMkW4EanZ4K4P1eWa4ExLs1oJ8YA8g3U", rawId: "AZif0HxM24R4UTZfQg3GlysdL6VVod-3B1bE4rr3FZeRciis92GelrnqMkW4EanZ4K4P1eWa4ExLs1oJ8YA8g3U", response: Object, type: "public-key", clientExtensionResults: {}, …}ObjectauthenticatorAttachment: "cross-platform"clientExtensionResults: {}Objectid: "AZif0HxM24R4UTZfQg3GlysdL6VVod-3B1bE4rr3FZeRciis92GelrnqMkW4EanZ4K4P1eWa4ExLs1oJ8YA8g3U"rawId: "AZif0HxM24R4UTZfQg3GlysdL6VVod-3B1bE4rr3FZeRciis92GelrnqMkW4EanZ4K4P1eWa4ExLs1oJ8YA8g3U"response: {attestationObject: "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjFKNr5wj2wmE…hkIlggCM4Lc3QXFZXeGO-7s_cgyjFKoD-nS8QhQT37NeGMag4", clientDataJSON: "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIj…m9yaWdpbiI6Imh0dHBzOi8vbGFuZ3N5cy1ub3ZhLnRlc3QifQ", transports: ["internal", "hybrid"], publicKeyAlgorithm: -7, publicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEaxo73Gqr6FaKZm…3lVWGQIzgtzdBcVld4Y77uz9yDKMUqgP6dLxCFBPfs14YxqDg", …}Objecttype: "public-key"Object Prototype

However, since this is for the frontend package, not sure if this should be posted here.

DarkGhostHunter commented 7 months ago

@Thotsuya Got it. It's because an attestation part tries to use base64_decode, instead of the ByteBuffer::decodeBase64Url.

DarkGhostHunter commented 7 months ago

Should be fixed in v2.0.2