jumpserver/apps/authentication/templates/authentication/passkey.html

192 lines
6.3 KiB
Python

{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login passkey</title>
<script src="{% static "js/jquery-3.6.1.min.js" %}?_=9"></script>
</head>
<body>
<form action='{% url 'api-auth:passkey-auth' %}' method="post" id="loginForm">
<input type="hidden" name="passkeys" id="passkeys"/>
</form>
</body>
<script>
const loginUrl = "/core/auth/login/";
window.conditionalUI = false;
window.conditionUIAbortController = new AbortController();
window.conditionUIAbortSignal = conditionUIAbortController.signal;
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
// Use a lookup table to find the index.
const lookup = new Uint8Array(256)
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i
}
const encode = function (arraybuffer) {
const bytes = new Uint8Array(arraybuffer)
let i;
const len = bytes.length;
let base64url = ''
for (i = 0; i < len; i += 3) {
base64url += chars[bytes[i] >> 2]
base64url += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]
base64url += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]
base64url += chars[bytes[i + 2] & 63]
}
if ((len % 3) === 2) {
base64url = base64url.substring(0, base64url.length - 1)
} else if (len % 3 === 1) {
base64url = base64url.substring(0, base64url.length - 2)
}
return base64url
}
const decode = function (base64string) {
const bufferLength = base64string.length * 0.75
const len = base64string.length;
let i;
let p = 0
let encoded1;
let encoded2;
let encoded3;
let encoded4
const bytes = new Uint8Array(bufferLength)
for (i = 0; i < len; i += 4) {
encoded1 = lookup[base64string.charCodeAt(i)]
encoded2 = lookup[base64string.charCodeAt(i + 1)]
encoded3 = lookup[base64string.charCodeAt(i + 2)]
encoded4 = lookup[base64string.charCodeAt(i + 3)]
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4)
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2)
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63)
}
return bytes.buffer
}
function checkConditionalUI(form) {
if (!navigator.credentials) {
alert('WebAuthn is not supported in this browser')
return
}
if (window.PublicKeyCredential && PublicKeyCredential.isConditionalMediationAvailable) {
// Check if conditional mediation is available.
PublicKeyCredential.isConditionalMediationAvailable().then((result) => {
window.conditionalUI = result;
if (!window.conditionalUI) {
alert("Conditional UI is not available. Please use the legacy UI.");
} else {
return true
}
});
}
}
const publicKeyCredentialToJSON = (pubKeyCred) => {
if (pubKeyCred instanceof Array) {
const arr = []
for (const i of pubKeyCred) {
arr.push(publicKeyCredentialToJSON(i))
}
return arr
}
if (pubKeyCred instanceof ArrayBuffer) {
return encode(pubKeyCred)
}
if (pubKeyCred instanceof Object) {
const obj = {}
for (const key in pubKeyCred) {
obj[key] = publicKeyCredentialToJSON(pubKeyCred[key])
}
return obj
}
return pubKeyCred
}
function GetAssertReq(getAssert) {
getAssert.publicKey.challenge = decode(getAssert.publicKey.challenge)
for (const allowCred of getAssert.publicKey.allowCredentials) {
allowCred.id = decode(allowCred.id)
}
return getAssert
}
function startAuthn(form, conditionalUI = false) {
window.loginForm = form
fetch('/api/v1/authentication/passkeys/auth/', {method: 'GET'}).then(function (response) {
if (response.ok) {
return response.json().then(function (req) {
return GetAssertReq(req)
})
}
throw new Error('No credential available to authenticate!')
}).then(function (options) {
if (conditionalUI) {
options.mediation = 'conditional'
options.signal = window.conditionUIAbortSignal
} else {
window.conditionUIAbortController.abort()
}
return navigator.credentials.get(options)
}).then(function (assertion) {
const pk = $('#passkeys')
if (pk.length === 0) {
retry("Did you add the 'passkeys' hidden input field")
return
}
pk.val(JSON.stringify(publicKeyCredentialToJSON(assertion)))
const x = document.getElementById(window.loginForm)
if (x === null || x === undefined) {
console.error('Did you pass the correct form id to auth function')
return
}
x.submit()
}).catch(function (err) {
retry(err)
})
}
function safeStartAuthn(form) {
checkConditionalUI('loginForm')
const errorMsg = "{% trans 'This page is not served over HTTPS. Please use HTTPS to ensure security of your credentials.' %}"
const isSafe = window.location.protocol === 'https:'
if (!isSafe && location.hostname !== 'localhost') {
alert(errorMsg)
window.location.href = loginUrl
} else {
setTimeout(() => startAuthn('loginForm'), 100)
}
}
function retry(error) {
const fullError = "{% trans 'Error' %}" + ': ' + error + "\n\n " + "{% trans 'Do you want to retry ?' %}"
const result = confirm(fullError)
if (result) {
safeStartAuthn()
} else {
window.location.href = loginUrl
}
}
{% if not error %}
window.onload = function () {
safeStartAuthn()
}
{% else %}
const error = "{{ error }}"
retry(error)
{% endif %}
</script>
</html>