This commit is contained in:
parent
a4a79e45b7
commit
a0de7d4da6
11606
package-lock.json
generated
11606
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,7 @@
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"axios": "^0.25.0",
|
||||
"axios": "^0.28.0",
|
||||
"dayjs": "^1.11.9",
|
||||
"mui-chips-input": "^2.1.3",
|
||||
"nth-check": "^2.0.1",
|
||||
|
@ -23,6 +23,25 @@
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Вход</title>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #004fa1;
|
||||
--background-color: #ffffff;
|
||||
--text-color: #000000;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-color: #000000;
|
||||
--text-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body data-externalurl="">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Grid } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import Webauthn from "./Webauthn.tsx";
|
||||
import Webauthn from "./Webauthn";
|
||||
|
||||
interface Props {
|
||||
LoggedIn: boolean;
|
||||
|
@ -3,14 +3,15 @@ import {
|
||||
performAssertionCeremony,
|
||||
performAttestationCeremony,
|
||||
performAssertionCeremonyConditional,
|
||||
} from "../services/WebauthnService.ts";
|
||||
import { AssertionResult, AttestationResult } from "../models/Webauthn.ts";
|
||||
} from "../services/WebauthnService";
|
||||
|
||||
import { AssertionResult, AttestationResult } from "../models/Webauthn";
|
||||
import { Button, Grid, Box } from "@mui/material";
|
||||
import KeyIcon from "@mui/icons-material/Key";
|
||||
import { getInfo } from "../services/APIService.ts";
|
||||
import { getInfo } from "../services/APIService";
|
||||
//import axios from "axios";
|
||||
//import { hexMD5 } from "../utils/MD5.js";
|
||||
import { radiusLogin } from "../services/ClientService.ts";
|
||||
import { radiusLogin } from "../services/ClientService";
|
||||
interface Props {
|
||||
setDebugMessage: React.Dispatch<React.SetStateAction<string>>;
|
||||
Discoverable: boolean;
|
||||
@ -21,17 +22,39 @@ interface Props {
|
||||
}
|
||||
|
||||
const Webauthn = function (props: Props) {
|
||||
const [abortController, setabortController] =
|
||||
React.useState<AbortController>();
|
||||
const [abortSignal, setabortSignal] = React.useState<AbortSignal>();
|
||||
const [req, setReq] = React.useState<PublicKeyCredentialRequestOptions>();
|
||||
React.useEffect(() => {
|
||||
console.log("got first time");
|
||||
performAssertionCeremonyConditional(true, props.mac).then(res => {
|
||||
if (res == AssertionResult.Success) {
|
||||
handleDiscoverableLoginSuccess();
|
||||
}
|
||||
});
|
||||
abortController?.abort();
|
||||
if (!props.loggedIn) {
|
||||
var abortControllerlocal = new AbortController();
|
||||
setabortController(abortControllerlocal);
|
||||
setabortSignal(abortControllerlocal.signal);
|
||||
console.log(abortControllerlocal);
|
||||
console.log("got first time");
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [props.loggedIn]);
|
||||
React.useEffect(() => {
|
||||
if (!props.loggedIn && abortController) {
|
||||
console.log("running conditional attestation");
|
||||
performAssertionCeremonyConditional(
|
||||
true,
|
||||
props.mac,
|
||||
abortController,
|
||||
setReq
|
||||
).then(res => {
|
||||
if (res === AssertionResult.Success) {
|
||||
handleDiscoverableLoginSuccess();
|
||||
}
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [abortController,abortSignal,props.loggedIn, props.mac]);
|
||||
const handleDiscoverableLoginSuccess = async () => {
|
||||
const info = await getInfo();
|
||||
|
||||
if (info != null) {
|
||||
props.setBothUsername(info.username);
|
||||
}
|
||||
@ -39,7 +62,7 @@ const Webauthn = function (props: Props) {
|
||||
|
||||
const handleAttestationClick = async (discoverable: boolean = false) => {
|
||||
props.setDebugMessage("Attempting Webauthn Attestation");
|
||||
|
||||
abortController?.abort();
|
||||
const result = await performAttestationCeremony(discoverable);
|
||||
|
||||
switch (result) {
|
||||
@ -79,43 +102,6 @@ const Webauthn = function (props: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResult = (result:AttestationResult) => {
|
||||
switch (result) {
|
||||
case AttestationResult.Success:
|
||||
props.setDebugMessage("Successful attestation.");
|
||||
handleDiscoverableLoginSuccess();
|
||||
break;
|
||||
case AttestationResult.FailureSupport:
|
||||
props.setDebugMessage(
|
||||
"Your browser does not appear to support the configuration."
|
||||
);
|
||||
break;
|
||||
case AttestationResult.FailureSyntax:
|
||||
props.setDebugMessage(
|
||||
"The attestation challenge was rejected as malformed or incompatible by your browser."
|
||||
);
|
||||
break;
|
||||
case AttestationResult.FailureWebauthnNotSupported:
|
||||
props.setDebugMessage(
|
||||
"Your browser does not support the WebAuthN protocol."
|
||||
);
|
||||
break;
|
||||
case AttestationResult.FailureUserConsent:
|
||||
props.setDebugMessage("You cancelled the attestation request.");
|
||||
break;
|
||||
case AttestationResult.FailureUserVerificationOrResidentKey:
|
||||
props.setDebugMessage(
|
||||
"Your device does not support user verification or resident keys but this was required."
|
||||
);
|
||||
break;
|
||||
case AttestationResult.FailureExcluded:
|
||||
props.setDebugMessage("You have registered this device already.");
|
||||
break;
|
||||
case AttestationResult.FailureUnknown:
|
||||
props.setDebugMessage("An unknown error occurred.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
const radiusAuth = () => {
|
||||
console.log("auth");
|
||||
radiusLogin().then(ret => {
|
||||
@ -154,9 +140,14 @@ const Webauthn = function (props: Props) {
|
||||
const handleAssertionClick = async () => {
|
||||
props.setDebugMessage("Attempting Webauthn Assertion");
|
||||
//alert(searchParams.get('to'));
|
||||
console.log(abortController);
|
||||
abortController?.abort();
|
||||
console.log(abortController);
|
||||
const result = await performAssertionCeremony(
|
||||
props.Discoverable,
|
||||
props.mac
|
||||
props.mac,
|
||||
req,
|
||||
setReq
|
||||
);
|
||||
|
||||
switch (result) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getEmbeddedVariable } from "../utils/Configuration.ts";
|
||||
import { getEmbeddedVariable } from "../utils/Configuration";
|
||||
|
||||
const ExternalURL = getEmbeddedVariable("externalurl")
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import Login from "./pages/LoginPage.tsx";
|
||||
import Login from "./pages/LoginPage";
|
||||
import "@fontsource/roboto/300.css"; // Import the font for font-weight: 300
|
||||
import "@fontsource/roboto/400.css"; // Import the font for font-weight: 400
|
||||
import "@fontsource/roboto/500.css"; // Import the font for font-weight: 500
|
||||
|
@ -46,10 +46,10 @@ export interface AuthenticatorAssertionResponseJSON
|
||||
}
|
||||
|
||||
export interface AuthenticatorAttestationResponseFuture extends AuthenticatorAttestationResponse {
|
||||
getTransports?: () => AuthenticatorTransport[];
|
||||
getAuthenticatorData?: () => ArrayBuffer;
|
||||
getPublicKey?: () => ArrayBuffer;
|
||||
getPublicKeyAlgorithm?: () => COSEAlgorithmIdentifier[];
|
||||
getTransports(): AuthenticatorTransport[];
|
||||
getAuthenticatorData(): ArrayBuffer;
|
||||
getPublicKey(): ArrayBuffer | null;
|
||||
getPublicKeyAlgorithm(): COSEAlgorithmIdentifier;
|
||||
}
|
||||
|
||||
export interface AttestationPublicKeyCredential extends PublicKeyCredential {
|
||||
@ -57,7 +57,7 @@ export interface AttestationPublicKeyCredential extends PublicKeyCredential {
|
||||
}
|
||||
|
||||
export interface AuthenticatorAttestationResponseJSON
|
||||
extends Omit<AuthenticatorAttestationResponseFuture, "clientDataJSON" | "attestationObject"> {
|
||||
extends Omit<AuthenticatorAttestationResponseFuture, "clientDataJSON" | "attestationObject" | "getTransports" | "getAuthenticatorData" | "getPublicKey" | "getPublicKey" | "getPublicKeyAlgorithm"> {
|
||||
clientDataJSON: string;
|
||||
attestationObject: string;
|
||||
}
|
||||
|
@ -25,13 +25,13 @@ import Brightness7Icon from "@mui/icons-material/Brightness7";
|
||||
import LogoutIcon from "@mui/icons-material/Logout";
|
||||
|
||||
import u2fApi from "u2f-api";
|
||||
import { logout, login, radiusLogin } from "../services/ClientService.ts";
|
||||
import { logout, login, radiusLogin } from "../services/ClientService";
|
||||
import {
|
||||
isWebauthnSecure,
|
||||
isWebauthnSupported,
|
||||
} from "../services/WebauthnService.ts";
|
||||
import { getInfo } from "../services/APIService.ts";
|
||||
import SecurityKey from "../components/SecurityKey.tsx";
|
||||
} from "../services/WebauthnService";
|
||||
import { getInfo } from "../services/APIService";
|
||||
import SecurityKey from "../components/SecurityKey";
|
||||
const totalTime = 5;
|
||||
export default function Login() {
|
||||
const searchParams = React.useMemo(
|
||||
@ -140,7 +140,7 @@ export default function Login() {
|
||||
elem.href="intent://networkcheck.kde.org#Intent;scheme=http;end"
|
||||
elem.click();
|
||||
}
|
||||
function findNextTabStop(el) {
|
||||
function findNextTabStop(el: EventTarget) {
|
||||
var universe = document.querySelectorAll('input, button, select, textarea, a[href]');
|
||||
var list = Array.prototype.filter.call(universe, function(item) {return item.tabIndex >= "0"});
|
||||
var index = list.indexOf(el);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import axios, {AxiosResponse} from "axios";
|
||||
import {ErrorResponse, ServiceResponse} from "../models/API.ts";
|
||||
import {Info} from "../models/Info.ts";
|
||||
import {InfoPath} from "../constants/API.ts";
|
||||
import {ErrorResponse, ServiceResponse} from "../models/API";
|
||||
import {Info} from "../models/Info";
|
||||
import {InfoPath} from "../constants/API";
|
||||
|
||||
function toErrorResponse<T>(resp: AxiosResponse<ServiceResponse<T>>): ErrorResponse | undefined {
|
||||
if (resp.data && "status" in resp.data && resp.data["status"] === "KO") {
|
||||
@ -10,8 +10,8 @@ function toErrorResponse<T>(resp: AxiosResponse<ServiceResponse<T>>): ErrorRespo
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function toData<T>(resp: AxiosResponse<ServiceResponse<T>>): T | undefined {
|
||||
if (resp.data && "status" in resp.data && resp.data["status"] === "OK") {
|
||||
export function toData<T>(resp: void | AxiosResponse<ServiceResponse<T>>): T | undefined {
|
||||
if (resp?.data && "status" in resp.data && resp.data["status"] === "OK") {
|
||||
return resp.data.data as T;
|
||||
}
|
||||
return undefined;
|
||||
@ -27,7 +27,7 @@ export function hasServiceError<T>(resp: AxiosResponse<ServiceResponse<T>>) {
|
||||
|
||||
export async function getInfo() : Promise<Info | undefined> {
|
||||
try {
|
||||
const response = await axios.get<ServiceResponse<Info>>(InfoPath);
|
||||
const response = await axios.get<ServiceResponse<Info>>(InfoPath).catch((err)=>{console.log(err)}).then((res)=>(res));
|
||||
return toData<Info>(response);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import axios from "axios";
|
||||
import { LoginBody } from "../models/API.ts";
|
||||
import { LoginPath, LogoutPath, ManualLogin } from "../constants/API.ts";
|
||||
import { LoginBody } from "../models/API";
|
||||
import { LoginPath, LogoutPath, ManualLogin } from "../constants/API";
|
||||
|
||||
export async function login(username: string, password: string, mac: string): Promise<boolean> {
|
||||
const body: LoginBody = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getBase64WebEncodingFromBytes, getBytesFromBase64 } from '../utils/Base64.ts';
|
||||
import { getBase64WebEncodingFromBytes, getBytesFromBase64 } from '../utils/Base64';
|
||||
import {
|
||||
AssertionPublicKeyCredentialResult,
|
||||
AssertionResult,
|
||||
@ -13,10 +13,10 @@ import {
|
||||
PublicKeyCredentialJSON,
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
PublicKeyCredentialRequestOptionsStatus
|
||||
} from '../models/Webauthn.ts';
|
||||
} from '../models/Webauthn';
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { OptionalDataServiceResponse, ServiceResponse, SignInResponse } from "../models/API.ts";
|
||||
import { AssertionPath, AttestationPath, DiscoverableAssertionPath, DiscoverableAttestationPath } from "../constants/API.ts";
|
||||
import { OptionalDataServiceResponse, ServiceResponse, SignInResponse } from "../models/API";
|
||||
import { AssertionPath, AttestationPath, DiscoverableAssertionPath, DiscoverableAttestationPath } from "../constants/API";
|
||||
|
||||
export function isWebauthnSecure(): boolean {
|
||||
if (window.isSecureContext) {
|
||||
@ -105,8 +105,10 @@ function encodeAttestationPublicKeyCredential(credential: AttestationPublicKeyCr
|
||||
response: {
|
||||
attestationObject: arrayBufferEncode(response.attestationObject),
|
||||
clientDataJSON: arrayBufferEncode(response.clientDataJSON),
|
||||
|
||||
},
|
||||
transports: transports,
|
||||
authenticatorAttachment: credential.authenticatorAttachment
|
||||
};
|
||||
}
|
||||
|
||||
@ -285,18 +287,24 @@ async function getAssertionPublicKeyCredentialResult(requestOptions: PublicKeyCr
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getAssertionPublicKeyCredentialResultConditional(requestOptions: PublicKeyCredentialRequestOptions): Promise<AssertionPublicKeyCredentialResult> {
|
||||
async function getAssertionPublicKeyCredentialResultConditional(requestOptions: PublicKeyCredentialRequestOptions,abortSignal:AbortController): Promise<AssertionPublicKeyCredentialResult> {
|
||||
const result: AssertionPublicKeyCredentialResult = {
|
||||
result: AssertionResult.Success,
|
||||
};
|
||||
|
||||
console.log(abortSignal)
|
||||
try {
|
||||
result.credential = (await navigator.credentials.get({mediation: "conditional",publicKey: requestOptions})) as PublicKeyCredential;
|
||||
result.credential = (await navigator.credentials.get({signal: abortSignal.signal,
|
||||
mediation: "conditional" as CredentialMediationRequirement,
|
||||
publicKey: requestOptions})) as PublicKeyCredential;
|
||||
} catch(e) {
|
||||
result.result = AssertionResult.Failure;
|
||||
|
||||
const exception = e as DOMException;
|
||||
if (exception !== undefined) {
|
||||
if (exception.name === "AbortError") {
|
||||
console.log("request aborted");
|
||||
return result;
|
||||
}
|
||||
else if (exception !== undefined) {
|
||||
result.result = getAssertionResultFromDOMException(exception, requestOptions);
|
||||
|
||||
return result;
|
||||
@ -354,8 +362,9 @@ export async function performAttestationCeremony(discoverable: boolean = false):
|
||||
return AttestationResult.Failure;
|
||||
}
|
||||
|
||||
export async function performAssertionCeremony(discoverable: boolean = false, mac:string): Promise<AssertionResult> {
|
||||
const assertionRequestOpts = await getAssertionRequestOptions(discoverable);
|
||||
export async function performAssertionCeremony(discoverable: boolean = false, mac:string, req:PublicKeyCredentialRequestOptions|undefined,setReq:React.Dispatch<React.SetStateAction<PublicKeyCredentialRequestOptions | undefined>>): Promise<AssertionResult> {
|
||||
|
||||
const assertionRequestOpts = req === undefined ? await getAssertionRequestOptions(discoverable) : {options: req, status: 200};
|
||||
|
||||
if (assertionRequestOpts.status !== 200 || assertionRequestOpts.options == null) {
|
||||
return AssertionResult.Failure;
|
||||
@ -370,7 +379,7 @@ export async function performAssertionCeremony(discoverable: boolean = false, ma
|
||||
}
|
||||
|
||||
const response = await postAssertionPublicKeyCredentialResult(assertionResult.credential, discoverable, mac);
|
||||
|
||||
setReq(undefined)
|
||||
if (response.data.status === "OK" && response.status === 200) {
|
||||
return AssertionResult.Success;
|
||||
}
|
||||
@ -378,14 +387,13 @@ export async function performAssertionCeremony(discoverable: boolean = false, ma
|
||||
return AssertionResult.Failure;
|
||||
}
|
||||
|
||||
export async function performAssertionCeremonyConditional(discoverable: boolean = false, mac:string): Promise<AssertionResult> {
|
||||
export async function performAssertionCeremonyConditional(discoverable: boolean = false, mac:string,abortSignal:AbortController, setReq:React.Dispatch<React.SetStateAction<PublicKeyCredentialRequestOptions | undefined>>): Promise<AssertionResult> {
|
||||
const assertionRequestOpts = await getAssertionRequestOptions(discoverable);
|
||||
|
||||
if (assertionRequestOpts.status !== 200 || assertionRequestOpts.options == null) {
|
||||
return AssertionResult.Failure;
|
||||
}
|
||||
|
||||
const assertionResult = await getAssertionPublicKeyCredentialResultConditional(assertionRequestOpts.options);
|
||||
setReq(assertionRequestOpts.options)
|
||||
const assertionResult = await getAssertionPublicKeyCredentialResultConditional(assertionRequestOpts.options,abortSignal);
|
||||
|
||||
if (assertionResult.result !== AssertionResult.Success) {
|
||||
return assertionResult.result;
|
||||
@ -395,7 +403,7 @@ export async function performAssertionCeremonyConditional(discoverable: boolean
|
||||
console.log("navigator exited");
|
||||
|
||||
const response = await postAssertionPublicKeyCredentialResult(assertionResult.credential, discoverable, mac);
|
||||
|
||||
setReq(undefined)
|
||||
if (response.data.status === "OK" && response.status === 200) {
|
||||
return AssertionResult.Success;
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ export function getBase64FromBytes(bytes : number[] | Uint8Array) : string {
|
||||
return getBase64WebEncodingFromBytes(bytes);
|
||||
}
|
||||
|
||||
export function getBytesFromBase64(value) {
|
||||
export function getBytesFromBase64(value: string) {
|
||||
value = value.replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (value.length % 4) {
|
||||
value += "=";
|
||||
@ -139,8 +139,15 @@ export function getBytesFromBase64(value) {
|
||||
return Uint8Array.from(atob(value), c => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
export function getBase64WebEncodingFromBytes(value) {
|
||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
|
||||
export function getBase64WebEncodingFromBytes(value: number[] | Uint8Array) {
|
||||
let byteArray: Uint8Array;
|
||||
|
||||
if (value instanceof Uint8Array) {
|
||||
byteArray = value;
|
||||
} else {
|
||||
byteArray = new Uint8Array(value);
|
||||
}
|
||||
return btoa(String.fromCharCode.apply(null, Array.from(byteArray)))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=/g, "");
|
||||
|
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user