182 lines
6.1 KiB
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>
|