mirror of
https://github.com/lkddi/nexusphp.git
synced 2026-04-23 19:37:23 +08:00
passkey support
This commit is contained in:
+46
-1
@@ -1,11 +1,14 @@
|
||||
<?php
|
||||
require "../include/bittorrent.php";
|
||||
dbconn();
|
||||
loggedinorreturn();
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$params = $_POST['params'] ?? [];
|
||||
|
||||
if ($action != 'getPasskeyGetArgs' && $action != 'processPasskeyGet') {
|
||||
loggedinorreturn();
|
||||
}
|
||||
|
||||
class AjaxInterface{
|
||||
|
||||
public static function toggleUserMedalStatus($params)
|
||||
@@ -179,6 +182,48 @@ class AjaxInterface{
|
||||
$user->tokens()->where('id', $params['id'])->delete();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getPasskeyCreateArgs($params)
|
||||
{
|
||||
global $CURUSER;
|
||||
$rep = new \App\Repositories\UserPasskeyRepository();
|
||||
return $rep->getCreateArgs($CURUSER['id'], $CURUSER['username']);
|
||||
}
|
||||
|
||||
public static function processPasskeyCreate($params)
|
||||
{
|
||||
global $CURUSER;
|
||||
$rep = new \App\Repositories\UserPasskeyRepository();
|
||||
return $rep->processCreate($CURUSER['id'], $params['clientDataJSON'], $params['attestationObject']);
|
||||
}
|
||||
|
||||
public static function deletePasskey($params)
|
||||
{
|
||||
global $CURUSER;
|
||||
$rep = new \App\Repositories\UserPasskeyRepository();
|
||||
return $rep->delete($CURUSER['id'], $params['credentialId']);
|
||||
}
|
||||
|
||||
public static function getPasskeyList($params)
|
||||
{
|
||||
global $CURUSER;
|
||||
$rep = new \App\Repositories\UserPasskeyRepository();
|
||||
return $rep->getList($CURUSER['id']);
|
||||
}
|
||||
|
||||
public static function getPasskeyGetArgs($params)
|
||||
{
|
||||
global $CURUSER;
|
||||
$rep = new \App\Repositories\UserPasskeyRepository();
|
||||
return $rep->getGetArgs();
|
||||
}
|
||||
|
||||
public static function processPasskeyGet($params)
|
||||
{
|
||||
global $CURUSER;
|
||||
$rep = new \App\Repositories\UserPasskeyRepository();
|
||||
return $rep->processGet($params['challenge'], $params['id'], $params['clientDataJSON'], $params['authenticatorData'], $params['signature'], $params['userHandle']);
|
||||
}
|
||||
}
|
||||
|
||||
$class = 'AjaxInterface';
|
||||
|
||||
Vendored
+169
@@ -0,0 +1,169 @@
|
||||
const Passkey = (() => {
|
||||
const apiUrl = '/ajax.php';
|
||||
|
||||
const supported = () => {
|
||||
return window.PublicKeyCredential;
|
||||
}
|
||||
|
||||
const conditionalSupported = () => {
|
||||
return supported() && PublicKeyCredential.isConditionalMediationAvailable;
|
||||
}
|
||||
|
||||
const isCMA = async () => {
|
||||
return await PublicKeyCredential.isConditionalMediationAvailable();
|
||||
}
|
||||
|
||||
const getCreateArgs = async () => {
|
||||
const getArgsParams = new URLSearchParams();
|
||||
getArgsParams.set('action', 'getPasskeyCreateArgs');
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: getArgsParams,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.ret !== 0) {
|
||||
throw new Error(data.msg);
|
||||
}
|
||||
|
||||
const createArgs = data.data;
|
||||
recursiveBase64StrToArrayBuffer(createArgs);
|
||||
return createArgs;
|
||||
}
|
||||
|
||||
const createRegistration = async () => {
|
||||
const createArgs = await getCreateArgs();
|
||||
|
||||
const cred = await navigator.credentials.create(createArgs);
|
||||
|
||||
const processCreateParams = new URLSearchParams();
|
||||
processCreateParams.set('action', 'processPasskeyCreate');
|
||||
processCreateParams.set('params[transports]', cred.response.getTransports ? cred.response.getTransports() : null)
|
||||
processCreateParams.set('params[clientDataJSON]', cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null);
|
||||
processCreateParams.set('params[attestationObject]', cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: processCreateParams,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.ret !== 0) {
|
||||
throw new Error(data.msg);
|
||||
}
|
||||
}
|
||||
|
||||
const getGetArgs = async () => {
|
||||
const getArgsParams = new URLSearchParams();
|
||||
getArgsParams.set('action', 'getPasskeyGetArgs');
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: getArgsParams,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.ret !== 0) {
|
||||
throw new Error(data.msg);
|
||||
}
|
||||
|
||||
const getArgs = data.data;
|
||||
recursiveBase64StrToArrayBuffer(getArgs);
|
||||
return getArgs;
|
||||
}
|
||||
|
||||
let abortController;
|
||||
|
||||
const checkRegistration = async (conditional, showLoading) => {
|
||||
if (abortController) {
|
||||
abortController.abort()
|
||||
abortController = null;
|
||||
}
|
||||
if (!conditional) showLoading();
|
||||
const getArgs = await getGetArgs();
|
||||
if (conditional) {
|
||||
abortController = new AbortController();
|
||||
getArgs.signal = abortController.signal;
|
||||
getArgs.mediation = 'conditional';
|
||||
}
|
||||
|
||||
const cred = await navigator.credentials.get(getArgs);
|
||||
|
||||
if (conditional) showLoading();
|
||||
|
||||
const processGetParams = new URLSearchParams();
|
||||
processGetParams.set('action', 'processPasskeyGet');
|
||||
processGetParams.set('params[challenge]', arrayBufferToBase64(getArgs['publicKey']['challenge']));
|
||||
processGetParams.set('params[id]', cred.rawId ? arrayBufferToBase64(cred.rawId) : null);
|
||||
processGetParams.set('params[clientDataJSON]', cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null);
|
||||
processGetParams.set('params[authenticatorData]', cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null);
|
||||
processGetParams.set('params[signature]', cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null);
|
||||
processGetParams.set('params[userHandle]', cred.response.userHandle ? arrayBufferToBase64(cred.response.userHandle) : null);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: processGetParams,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.ret !== 0) {
|
||||
throw new Error(data.msg);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteRegistration = async (credentialId) => {
|
||||
const deleteParams = new URLSearchParams();
|
||||
deleteParams.set('action', 'deletePasskey');
|
||||
deleteParams.set('params[credentialId]', credentialId);
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
body: deleteParams,
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.ret !== 0) {
|
||||
throw new Error(data.msg);
|
||||
}
|
||||
}
|
||||
|
||||
const recursiveBase64StrToArrayBuffer = (obj) => {
|
||||
let prefix = '=?BINARY?B?';
|
||||
let suffix = '?=';
|
||||
if (typeof obj === 'object') {
|
||||
for (let key in obj) {
|
||||
if (typeof obj[key] === 'string') {
|
||||
let str = obj[key];
|
||||
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
|
||||
str = str.substring(prefix.length, str.length - suffix.length);
|
||||
|
||||
let binary_string = window.atob(str);
|
||||
let len = binary_string.length;
|
||||
let bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary_string.charCodeAt(i);
|
||||
}
|
||||
obj[key] = bytes.buffer;
|
||||
}
|
||||
} else {
|
||||
recursiveBase64StrToArrayBuffer(obj[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const arrayBufferToBase64 = (buffer) => {
|
||||
let binary = '';
|
||||
let bytes = new Uint8Array(buffer);
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
return {
|
||||
supported: supported,
|
||||
conditionalSupported: conditionalSupported,
|
||||
createRegistration: createRegistration,
|
||||
checkRegistration: checkRegistration,
|
||||
deleteRegistration: deleteRegistration,
|
||||
isCMA: isCMA,
|
||||
}
|
||||
})();
|
||||
@@ -92,6 +92,7 @@ if (isset($returnto)) {
|
||||
if ($useChallengeResponseAuthentication) {
|
||||
print('<input type="hidden" name="response" />');
|
||||
}
|
||||
\App\Repositories\UserPasskeyRepository::renderLogin();
|
||||
?>
|
||||
</form>
|
||||
<?php
|
||||
@@ -134,4 +135,5 @@ print("</td></tr></table></form></td></tr></table>");
|
||||
?>
|
||||
<?php
|
||||
render_password_challenge_js("login-form", "username", "password");
|
||||
\Nexus\Nexus::js('js/passkey.js', 'footer', true);
|
||||
stdfoot();
|
||||
|
||||
@@ -936,6 +936,9 @@ EOD;
|
||||
$twoStepY .= '</div>';
|
||||
tr_small($lang_usercp['row_two_step_secret'], $twoStepY, 1);
|
||||
}
|
||||
printf('<tr><td class="rowhead" valign="top" align="right">%s</td><td class="rowfollow" valign="top" align="left">', nexus_trans('passkey.passkey'));
|
||||
\App\Repositories\UserPasskeyRepository::renderList($CURUSER['id']);
|
||||
printf('</td></tr>');
|
||||
|
||||
if ($disableemailchange != 'no' && $smtptype != 'none') //system-wide setting
|
||||
tr_small($lang_usercp['row_email_address'], "<input type=\"text\" name=\"email\" style=\"width: 200px\" value=\"" . htmlspecialchars($CURUSER["email"]) . "\" /> <br /><font class=small>".$lang_usercp['text_email_address_note']."</font>", 1);
|
||||
|
||||
Reference in New Issue
Block a user