Skip to content

Commit

Permalink
feat: allow renaming webauthn authenticators, closes #1293
Browse files Browse the repository at this point in the history
  • Loading branch information
MiniDigger committed Dec 9, 2023
1 parent a65cd19 commit 184ecef
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ public void unregisterWebauthnDevice(@RequestBody final String id) {
this.credentialsService.checkRemoveBackupCodes();
}

@Privileged
@RequireAal(1)
@PostMapping(value = "/webauthn/rename", consumes = MediaType.APPLICATION_JSON_VALUE)
public void renameWebauthn(@RequestBody final RenameRequest renameRequest) {
this.webAuthNService.renameDevice(this.getHangarPrincipal().getUserId(), renameRequest.id(), renameRequest.displayName());
}

public record RenameRequest(String id, String displayName) {}

/*
* TOTP
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ public void updateCredential(final long userId, final String credentialId, final

}

public void renameDevice(final long userId, final String credentialId, final String newName) {
final WebAuthNCredential webAuthNCredential = this.getWebAuthNCredential(userId);
final var any = webAuthNCredential.credentials().stream().filter(c -> c.id().equals(credentialId)).findAny();
if (any.isPresent()) {
final WebAuthNCredential.WebAuthNDevice oldDevice = any.get();
final WebAuthNCredential.WebAuthNDevice patchedDevice = new WebAuthNCredential.WebAuthNDevice(oldDevice.id(), oldDevice.addedAt(), oldDevice.publicKey(), newName, oldDevice.authenticator(), oldDevice.isPasswordLess(), oldDevice.attestationType());
webAuthNCredential.credentials().remove(oldDevice);
webAuthNCredential.credentials().add(patchedDevice);
this.credentialsService.updateCredential(userId, webAuthNCredential);
}
}

private WebAuthNCredential getWebAuthNCredential(final long userId) {
final UserCredentialTable credential = this.credentialsService.getCredential(userId, CredentialType.WEBAUTHN);
if (credential == null) {
Expand Down
50 changes: 48 additions & 2 deletions frontend/src/pages/auth/settings/security.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import PageTitle from "~/components/design/PageTitle.vue";
import Modal from "~/components/modals/Modal.vue";
import PrettyTime from "~/components/design/PrettyTime.vue";
const props = defineProps<{
defineProps<{
settings?: AuthSettings;
}>();
const emit = defineEmits<{
Expand Down Expand Up @@ -81,6 +81,40 @@ async function unregisterAuthenticator(authenticator: AuthSettings["authenticato
loading.value = false;
}
const newAuthenticatorName = ref<string>();
const currentlyRenamingAuthenticator = ref<AuthSettings["authenticators"][0]>();
const authenticatorRenameModal = ref();
function renameAuthenticatorModal(authenticator: AuthSettings["authenticators"][0]) {
newAuthenticatorName.value = authenticator.displayName;
currentlyRenamingAuthenticator.value = authenticator;
authenticatorRenameModal.value.isOpen = true;
v.value.$reset();
}
async function renameAuthenticator() {
if (!(await v.value.$validate())) return;
if (!currentlyRenamingAuthenticator.value) {
notification.error("Something went wrong, please try again");
return;
}
loading.value = true;
try {
await useInternalApi("auth/webauthn/rename", "POST", { id: currentlyRenamingAuthenticator.value.id, displayName: newAuthenticatorName.value });
authenticatorRenameModal.value.isOpen = false;
emit("refreshSettings");
} catch (e) {
if (e.response?.data?.message === "error.privileged") {
await router.push(useAuth.loginUrl(route.path) + "&privileged=true");
} else if (e?.toString()?.startsWith("NotAllowedError")) {
notification.error("Security Key Authentication failed!");
} else {
notification.fromError(i18n, e);
}
}
loading.value = false;
}
const totpData = ref<{ secret: string; qrCode: string } | undefined>();
async function setupTotp() {
Expand Down Expand Up @@ -237,10 +271,22 @@ async function generateNewCodes() {
<li v-for="authenticator in settings.authenticators" :key="authenticator.id" class="my-1">
{{ authenticator.displayName }} <small class="mr-2">(added at <PrettyTime :time="authenticator.addedAt" long />)</small>
<Button size="small" :disabled="loading" @click.prevent="unregisterAuthenticator(authenticator)">Unregister</Button>
<Button class="ml-2" size="small" :disabled="loading" @click.prevent="renameAuthenticatorModal(authenticator)">Rename</Button>
</li>
<Modal
ref="authenticatorRenameModal"
title="Rename authenticator"
@close="
authenticatorRenameModal.isOpen = false;
v.$reset();
"
>
<InputText v-model="newAuthenticatorName" label="Name" :rules="[requiredIf()(() => authenticatorRenameModal.isOpen)]" />
<Button class="mt-2" size="small" :disabled="loading" @click.prevent="renameAuthenticator">Rename</Button>
</Modal>
</ul>
<div class="my-2">
<InputText v-model="authenticatorName" label="Name" :rules="[requiredIf()(() => totpData == undefined)]" />
<InputText v-model="authenticatorName" label="Name" :rules="[requiredIf()(() => totpData == undefined && !authenticatorRenameModal.isOpen)]" />
</div>
<Button :disabled="loading" @click="addAuthenticator">Setup 2FA via security key</Button>

Expand Down

0 comments on commit 184ecef

Please sign in to comment.