keycloak-modern-login/src/views/webauthn-authenticate/index.vue

182 lines
6.1 KiB
Vue

<template>
<layout>
<h1 class="text-3xl font-semibold text-center text-gray-700">
{{ $t("login.welcome") }}
</h1>
<div v-if="webauthnSupported && !error">
<p class="mt-5 mb-5 text-center">
{{ $t("webauthn.title") }}
</p>
<div
v-for="authenticator in context.authenticators.authenticators"
:key="authenticator.credentialId"
class="w-full h-20 mb-5 bg-white shadow-md rounded-md flex flex-row items-center">
<svg
class="w-12 h-12 ml-4 text-gray-700"
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 16 16">
<path
d="M6 .5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4H6v-4ZM7 1v1h1V1H7Zm2 0v1h1V1H9ZM5.5 5a.5.5 0 0 0-.5.5V15a1 1 0 0 0 1 1h5a1 1 0 0 0 1-1V5.5a.5.5 0 0 0-.5-.5h-6Z" />
</svg>
<div class="w-full ml-5 flex flex-col">
<p>{{ authenticator.label }}</p>
<p class="text-sm">
{{ $t("webauthn.registered", {date: moment(authenticator.createdAt)}) }}
</p>
</div>
</div>
<button
:onclick="prepareAuthenticate"
class="w-full px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-blue-500 rounded-md hover:bg-blue-400 focus:outline-none focus:bg-blue-400">
{{ $t("webauthn.authenticate") }}
</button>
<p
class="mt-5 text-sm text-center"
v-if="context.auth?.showTryAnotherWayLink">
{{ $t("webauthn.noDevice") }}
<a
:onclick="tryAnotherWay"
class="text-blue-500 hover:underline cursor-pointer"
>{{ $t("2fa.tryAnotherWay") }}</a
>
</p>
</div>
<div v-if="!webauthnSupported">
<p v-if="context.auth?.showTryAnotherWayLink" class="mt-5 text-center">
{{ $t("webauthn.noSupportOtherMethod") }}
</p>
<p v-else class="mt-5 text-center">
{{ $t("webauthn.noSupport") }}
</p>
<button
v-if="context.auth?.showTryAnotherWayLink"
:onclick="tryAnotherWay"
class="w-full mt-5 px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-blue-500 rounded-md hover:bg-blue-400 focus:outline-none focus:bg-blue-400">
{{ $t("2fa.tryAnotherWay") }}
</button>
</div>
<div v-if="error" class="mt-5 flex flex-col">
<ErrorBox
>{{ $t("webauthn.error") }}</ErrorBox
>
<button
:onclick="retryAuth"
class="w-full mt-5 px-4 py-2 tracking-wide text-white transition-colors duration-200 transform bg-blue-500 rounded-md hover:bg-blue-400 focus:outline-none focus:bg-blue-400">
{{ $t("login.retry") }}
</button>
</div>
</layout>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import Layout from "~/components/Layout.vue";
import ErrorBox from "~/components/ErrorBox.vue";
import type { KcContextBase } from "~/types/context";
import { base64url } from "rfc4648";
import { formPost } from "~/functions/utils";
import moment from "moment";
export default defineComponent({
name: "WebAuthnAuthenticate",
components: {
Layout,
ErrorBox,
},
data() {
return {
context: (window as any).kcContext as KcContextBase.WebauthnAuthenticate,
webauthnSupported: true,
error: false,
};
},
mounted: function () {
if (typeof PublicKeyCredential === "undefined") {
this.webauthnSupported = false;
}
},
methods: {
moment(date: string) : string {
return moment(date).toDate().toLocaleString();
},
tryAnotherWay(e: Event) {
e.preventDefault();
formPost(this.context.url.loginAction, {
tryAnotherWay: "on",
});
},
prepareAuthenticate() {
if (this.context.isUserIdentified) {
const allowedCredentials = new Array<PublicKeyCredentialDescriptor>();
this.context.authenticators.authenticators.forEach(authenticator => {
allowedCredentials.push({
id: base64url.parse(authenticator.credentialId, { loose: true }),
type: "public-key",
});
});
this.authenticate(allowedCredentials);
} else {
this.authenticate([]);
}
},
async authenticate(
allowedAuthenticators: Array<PublicKeyCredentialDescriptor>
) {
const publicKey: PublicKeyCredentialRequestOptions = {
rpId: this.context.rpId,
challenge: base64url.parse(this.context.challenge, { loose: true }),
};
if (this.context.createTimeout !== "0")
publicKey.timeout = Number(this.context.createTimeout) * 1000;
if (allowedAuthenticators.length)
publicKey.allowCredentials = allowedAuthenticators;
if (this.context.userVerification !== "not specified")
publicKey.userVerification = this.context.userVerification;
try {
const resultRaw = await navigator.credentials.get({ publicKey });
if (!resultRaw || resultRaw.type !== "public-key") return;
const result = resultRaw as PublicKeyCredential;
if (!("authenticatorData" in result.response)) return;
const response = result.response as AuthenticatorAssertionResponse;
const clientDataJSON = response.clientDataJSON;
const authenticatorData = response.authenticatorData;
const signature = response.signature;
const postData = {
credentialId: result.id,
clientDataJSON: base64url.stringify(new Uint8Array(clientDataJSON), {
pad: false,
}),
authenticatorData: base64url.stringify(
new Uint8Array(authenticatorData),
{ pad: false }
),
signature: base64url.stringify(new Uint8Array(signature), {
pad: false,
}),
userHandle: base64url.stringify(
new Uint8Array(response.userHandle!),
{ pad: false }
),
};
formPost(this.context.url.loginAction, postData);
} catch (err) {
this.error = true;
}
},
retryAuth() {
this.error = false;
},
},
});
</script>
<style>
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>