mirror of https://github.com/jumpserver/jumpserver
192 lines
6.3 KiB
Python
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>
|