146 lines
4.6 KiB
JavaScript
146 lines
4.6 KiB
JavaScript
import {
|
|
bufferToBase64,
|
|
isWebauthnSupported,
|
|
stringToBuffer,
|
|
} from "discourse/lib/webauthn";
|
|
import Controller from "@ember/controller";
|
|
import I18n from "I18n";
|
|
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
|
|
|
// model for this controller is user
|
|
export default Controller.extend(ModalFunctionality, {
|
|
loading: false,
|
|
errorMessage: null,
|
|
|
|
onShow() {
|
|
// clear properties every time because the controller is a singleton
|
|
this.setProperties({
|
|
errorMessage: null,
|
|
loading: true,
|
|
securityKeyName: I18n.t("user.second_factor.security_key.default_name"),
|
|
webauthnUnsupported: !isWebauthnSupported(),
|
|
});
|
|
|
|
this.model
|
|
.requestSecurityKeyChallenge()
|
|
.then((response) => {
|
|
if (response.error) {
|
|
this.set("errorMessage", response.error);
|
|
return;
|
|
}
|
|
|
|
this.setProperties({
|
|
errorMessage: isWebauthnSupported()
|
|
? null
|
|
: I18n.t("login.security_key_support_missing_error"),
|
|
loading: false,
|
|
challenge: response.challenge,
|
|
relayingParty: {
|
|
id: response.rp_id,
|
|
name: response.rp_name,
|
|
},
|
|
supported_algorithms: response.supported_algorithms,
|
|
user_secure_id: response.user_secure_id,
|
|
existing_active_credential_ids:
|
|
response.existing_active_credential_ids,
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
this.send("closeModal");
|
|
this.onError(error);
|
|
})
|
|
.finally(() => this.set("loading", false));
|
|
},
|
|
|
|
actions: {
|
|
registerSecurityKey() {
|
|
if (!this.securityKeyName) {
|
|
this.set(
|
|
"errorMessage",
|
|
I18n.t("user.second_factor.security_key.name_required_error")
|
|
);
|
|
return;
|
|
}
|
|
const publicKeyCredentialCreationOptions = {
|
|
challenge: Uint8Array.from(this.challenge, (c) => c.charCodeAt(0)),
|
|
rp: {
|
|
name: this.relayingParty.name,
|
|
id: this.relayingParty.id,
|
|
},
|
|
user: {
|
|
id: Uint8Array.from(this.user_secure_id, (c) => c.charCodeAt(0)),
|
|
displayName: this.model.username_lower,
|
|
name: this.model.username_lower,
|
|
},
|
|
pubKeyCredParams: this.supported_algorithms.map((alg) => {
|
|
return { type: "public-key", alg };
|
|
}),
|
|
excludeCredentials: this.existing_active_credential_ids.map(
|
|
(credentialId) => {
|
|
return {
|
|
type: "public-key",
|
|
id: stringToBuffer(atob(credentialId)),
|
|
};
|
|
}
|
|
),
|
|
timeout: 20000,
|
|
attestation: "none",
|
|
authenticatorSelection: {
|
|
// see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why
|
|
// default value of preferred is not necessarily what we want, it limits webauthn to only devices that support
|
|
// user verification, which usually requires entering a PIN
|
|
userVerification: "discouraged",
|
|
},
|
|
};
|
|
|
|
navigator.credentials
|
|
.create({
|
|
publicKey: publicKeyCredentialCreationOptions,
|
|
})
|
|
.then(
|
|
(credential) => {
|
|
let serverData = {
|
|
id: credential.id,
|
|
rawId: bufferToBase64(credential.rawId),
|
|
type: credential.type,
|
|
attestation: bufferToBase64(
|
|
credential.response.attestationObject
|
|
),
|
|
clientData: bufferToBase64(credential.response.clientDataJSON),
|
|
name: this.securityKeyName,
|
|
};
|
|
|
|
this.model
|
|
.registerSecurityKey(serverData)
|
|
.then((response) => {
|
|
if (response.error) {
|
|
this.set("errorMessage", response.error);
|
|
return;
|
|
}
|
|
this.markDirty();
|
|
this.set("errorMessage", null);
|
|
this.send("closeModal");
|
|
})
|
|
.catch((error) => this.onError(error))
|
|
.finally(() => this.set("loading", false));
|
|
},
|
|
(err) => {
|
|
if (err.name === "InvalidStateError") {
|
|
return this.set(
|
|
"errorMessage",
|
|
I18n.t("user.second_factor.security_key.already_added_error")
|
|
);
|
|
}
|
|
if (err.name === "NotAllowedError") {
|
|
return this.set(
|
|
"errorMessage",
|
|
I18n.t("user.second_factor.security_key.not_allowed_error")
|
|
);
|
|
}
|
|
this.set("errorMessage", err.message);
|
|
}
|
|
);
|
|
},
|
|
},
|
|
});
|