Initial commit
This commit is contained in:
commit
ff5a958e26
28 changed files with 7460 additions and 0 deletions
202
src/views/webauthn-authenticate/index.vue
Normal file
202
src/views/webauthn-authenticate/index.vue
Normal file
|
@ -0,0 +1,202 @@
|
|||
<template>
|
||||
<layout>
|
||||
<h1
|
||||
v-if="context.isUserIdentified"
|
||||
class="text-3xl font-semibold text-center text-gray-700">
|
||||
Welcome {{ context.auth?.attemptedUsername }}!
|
||||
</h1>
|
||||
<h1 v-else class="text-3xl font-semibold text-center text-gray-700">
|
||||
Welcome!
|
||||
</h1>
|
||||
<div v-if="webauthnSupported && !error">
|
||||
<p class="mt-5 mb-5 text-center">
|
||||
Please use one of your registered devices to continue:
|
||||
</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">
|
||||
Registered: {{ new Date(authenticator.createdAt).toLocaleString() }}
|
||||
</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">
|
||||
Authenticate
|
||||
</button>
|
||||
<p
|
||||
class="mt-5 text-sm text-center"
|
||||
v-if="context.auth?.showTryAnotherWayLink">
|
||||
Don't have your device at hand?
|
||||
<a
|
||||
:onclick="tryAnotherWay"
|
||||
class="text-blue-500 hover:underline cursor-pointer"
|
||||
>Try another way.</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="!webauthnSupported">
|
||||
<p class="mt-5 text-center">
|
||||
It seems that your browser doesn't support WebAuthn. Please try logging
|
||||
in with a different browser<span
|
||||
v-if="!context.auth?.showTryAnotherWayLink"
|
||||
>.</span
|
||||
><span v-else
|
||||
>, or try a different login method using the button below.</span
|
||||
>
|
||||
</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">
|
||||
Try another method
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="error" class="mt-5 flex flex-col">
|
||||
<ErrorBox
|
||||
>Something went wrong during authentication using your device.</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">
|
||||
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";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Login",
|
||||
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: {
|
||||
tryAnotherWay(e: Event) {
|
||||
e.preventDefault();
|
||||
this.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 }
|
||||
),
|
||||
};
|
||||
|
||||
this.formPost(this.context.url.loginAction, postData);
|
||||
} catch (err) {
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
retryAuth() {
|
||||
this.error = false;
|
||||
},
|
||||
formPost(url: string, data: object) {
|
||||
const form = document.createElement("form");
|
||||
form.method = "post";
|
||||
form.action = url;
|
||||
|
||||
for (const key in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
||||
const hiddenField = document.createElement("input");
|
||||
hiddenField.type = "hidden";
|
||||
hiddenField.name = key;
|
||||
hiddenField.value = data[key];
|
||||
|
||||
form.appendChild(hiddenField);
|
||||
}
|
||||
}
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue