mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-29 08:57:18 +02:00
userinfo: add webauthn buttons to user info page (#3075)
* userinfo: add webauthn buttons to user info page * use new buttons on original page * fix test
This commit is contained in:
parent
38c7089642
commit
35f697e491
14 changed files with 423 additions and 288 deletions
|
@ -1,20 +1,21 @@
|
|||
import React from 'react';
|
||||
import {User} from "react-feather";
|
||||
import MuiAvatar from "@mui/material/Avatar";
|
||||
import { isArray } from "lodash";
|
||||
import React from "react";
|
||||
import { User } from "react-feather";
|
||||
|
||||
type AvatarProps = {
|
||||
name: string;
|
||||
url?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export const Avatar = ({url, name}:AvatarProps): JSX.Element => {
|
||||
if (url === 'https://graph.microsoft.com/v1.0/me/photo/$value') {
|
||||
export const Avatar = ({ url, name }: AvatarProps): JSX.Element => {
|
||||
if (isArray(url)) {
|
||||
url = url?.pop();
|
||||
}
|
||||
|
||||
if (url === "https://graph.microsoft.com/v1.0/me/photo/$value") {
|
||||
url = null;
|
||||
}
|
||||
|
||||
return url ? (
|
||||
<MuiAvatar alt={name} src={url} />
|
||||
) : (
|
||||
<User />
|
||||
);
|
||||
return url ? <MuiAvatar alt={name} src={url} /> : <User />;
|
||||
};
|
||||
|
|
|
@ -1,30 +1,35 @@
|
|||
import DeviceCredentialsTable from "../components/DeviceCredentialsTable";
|
||||
import SectionFooter from "../components/SectionFooter";
|
||||
import { Session, User } from "../types";
|
||||
import Box from "@mui/material/Box";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import React, { FC } from "react";
|
||||
|
||||
import DeviceCredentialsTable from "../components/DeviceCredentialsTable";
|
||||
import {
|
||||
Session,
|
||||
User,
|
||||
WebAuthnCreationOptions,
|
||||
WebAuthnRequestOptions,
|
||||
} from "../types";
|
||||
import WebAuthnAuthenticateButton from "./WebAuthnAuthenticateButton";
|
||||
import WebAuthnRegisterButton from "./WebAuthnRegisterButton";
|
||||
|
||||
export type SessionDeviceCredentialsProps = {
|
||||
csrfToken: string;
|
||||
user: User;
|
||||
session: Session;
|
||||
webAuthnCreationOptions: WebAuthnCreationOptions;
|
||||
webAuthnRequestOptions: WebAuthnRequestOptions;
|
||||
webAuthnUrl: string;
|
||||
};
|
||||
export const SessionDeviceCredentials: FC<SessionDeviceCredentialsProps> = ({
|
||||
csrfToken,
|
||||
user,
|
||||
session,
|
||||
webAuthnUrl
|
||||
webAuthnCreationOptions,
|
||||
webAuthnRequestOptions,
|
||||
webAuthnUrl,
|
||||
}) => {
|
||||
const currentSessionDeviceCredentialIds = [];
|
||||
const otherDeviceCredentialIds = [];
|
||||
|
@ -37,45 +42,62 @@ export const SessionDeviceCredentials: FC<SessionDeviceCredentialsProps> = ({
|
|||
});
|
||||
|
||||
return (
|
||||
<Paper sx={{ overflow: "hidden" }}>
|
||||
<Stack>
|
||||
<Toolbar>
|
||||
<Typography variant="h4" flexGrow={1}>
|
||||
Current Session Device Credentials
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
||||
<DeviceCredentialsTable
|
||||
csrfToken={csrfToken}
|
||||
ids={currentSessionDeviceCredentialIds}
|
||||
webAuthnUrl={webAuthnUrl}
|
||||
/>
|
||||
</Box>
|
||||
{otherDeviceCredentialIds?.length > 0 ? (
|
||||
<>
|
||||
<Toolbar>
|
||||
<Typography variant="h4" flexGrow={1}>
|
||||
Other Device Credentials
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
||||
<DeviceCredentialsTable
|
||||
<>
|
||||
<Paper sx={{ overflow: "hidden" }}>
|
||||
<Stack>
|
||||
<Toolbar>
|
||||
<Typography variant="h4" flexGrow={1}>
|
||||
Current Session Device Credentials
|
||||
</Typography>
|
||||
|
||||
<Stack direction="row" justifyContent="center" spacing={1}>
|
||||
<WebAuthnRegisterButton
|
||||
creationOptions={webAuthnCreationOptions}
|
||||
csrfToken={csrfToken}
|
||||
ids={otherDeviceCredentialIds}
|
||||
webAuthnUrl={webAuthnUrl}
|
||||
url={webAuthnUrl}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<SectionFooter>
|
||||
<Typography variant="caption">
|
||||
Register device with <a href={webAuthnUrl}>WebAuthn</a>.
|
||||
</Typography>
|
||||
</SectionFooter>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<WebAuthnAuthenticateButton
|
||||
requestOptions={webAuthnRequestOptions}
|
||||
csrfToken={csrfToken}
|
||||
url={webAuthnUrl}
|
||||
size="small"
|
||||
/>
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
||||
<DeviceCredentialsTable
|
||||
csrfToken={csrfToken}
|
||||
ids={currentSessionDeviceCredentialIds}
|
||||
webAuthnUrl={webAuthnUrl}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{otherDeviceCredentialIds?.length > 0 ? (
|
||||
<>
|
||||
<Paper sx={{ overflow: "hidden" }}>
|
||||
<Stack>
|
||||
<Toolbar>
|
||||
<Typography variant="h4" flexGrow={1}>
|
||||
Other Device Credentials
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
||||
<DeviceCredentialsTable
|
||||
csrfToken={csrfToken}
|
||||
ids={otherDeviceCredentialIds}
|
||||
webAuthnUrl={webAuthnUrl}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default SessionDeviceCredentials;
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import { Drawer, useMediaQuery } from "@mui/material";
|
||||
import Container from "@mui/material/Container";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import React, { FC, useContext } from "react";
|
||||
import { UserInfoPageData } from "src/types";
|
||||
|
||||
import { SubpageContext } from "../context/Subpage";
|
||||
import GroupDetails from "./GroupDetails";
|
||||
import SessionDetails from "./SessionDetails";
|
||||
import SessionDeviceCredentials from "./SessionDeviceCredentials";
|
||||
import Container from "@mui/material/Container";
|
||||
import React, {FC, useContext} from "react";
|
||||
import { UserInfoPageData } from "src/types";
|
||||
import {Drawer, useMediaQuery} from "@mui/material";
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { ToolbarOffset } from "./ToolbarOffset";
|
||||
import {UserSidebarContent} from "./UserSidebarContent";
|
||||
import {SubpageContext} from "../context/Subpage";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import { UserSidebarContent } from "./UserSidebarContent";
|
||||
|
||||
type UserInfoPageProps = {
|
||||
data: UserInfoPageData;
|
||||
};
|
||||
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||
const theme = useTheme();
|
||||
const mdUp = useMediaQuery(() => theme.breakpoints.up('md'), {
|
||||
const mdUp = useMediaQuery(() => theme.breakpoints.up("md"), {
|
||||
defaultMatches: true,
|
||||
noSsr: false
|
||||
noSsr: false,
|
||||
});
|
||||
const {subpage} = useContext(SubpageContext);
|
||||
|
||||
const { subpage } = useContext(SubpageContext);
|
||||
|
||||
return (
|
||||
<Container maxWidth={false}>
|
||||
|
@ -31,37 +31,37 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
|||
open
|
||||
PaperProps={{
|
||||
sx: {
|
||||
backgroundColor: 'neutral.900',
|
||||
backgroundColor: "neutral.900",
|
||||
width: 256,
|
||||
height: '100vh',
|
||||
}
|
||||
height: "100vh",
|
||||
},
|
||||
}}
|
||||
variant="persistent"
|
||||
>
|
||||
<ToolbarOffset />
|
||||
<UserSidebarContent close={null}/>
|
||||
<UserSidebarContent close={null} />
|
||||
<ToolbarOffset />
|
||||
</Drawer>
|
||||
)}
|
||||
<Stack
|
||||
spacing={3}
|
||||
sx={{
|
||||
marginLeft: mdUp ? '256px' : '0px',
|
||||
}}>
|
||||
marginLeft: mdUp ? "256px" : "0px",
|
||||
}}
|
||||
>
|
||||
{subpage === "User" && <SessionDetails session={data?.session} />}
|
||||
|
||||
{subpage === 'User' && (
|
||||
<SessionDetails session={data?.session} />
|
||||
)}
|
||||
|
||||
{subpage === 'Groups Info' && (
|
||||
{subpage === "Groups Info" && (
|
||||
<GroupDetails groups={data?.directoryGroups} />
|
||||
)}
|
||||
|
||||
{subpage === 'Devices Info' && (
|
||||
{subpage === "Devices Info" && (
|
||||
<SessionDeviceCredentials
|
||||
csrfToken={data?.csrfToken}
|
||||
session={data?.session}
|
||||
user={data?.user}
|
||||
webAuthnCreationOptions={data?.webAuthnCreationOptions}
|
||||
webAuthnRequestOptions={data?.webAuthnRequestOptions}
|
||||
webAuthnUrl={data?.webAuthnUrl}
|
||||
/>
|
||||
)}
|
||||
|
|
70
ui/src/components/WebAuthnAuthenticateButton.tsx
Normal file
70
ui/src/components/WebAuthnAuthenticateButton.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import React, { FC, useRef, useState } from "react";
|
||||
|
||||
import { WebAuthnRequestOptions } from "../types";
|
||||
import { decode, encodeUrl } from "../util/base64";
|
||||
import WebAuthnButton, { WebAuthnButtonProps } from "./WebAuthnButton";
|
||||
|
||||
type CredentialForAuthenticate = {
|
||||
id: string;
|
||||
type: string;
|
||||
rawId: ArrayBuffer;
|
||||
response: {
|
||||
authenticatorData: ArrayBuffer;
|
||||
clientDataJSON: ArrayBuffer;
|
||||
signature: ArrayBuffer;
|
||||
userHandle: ArrayBuffer;
|
||||
};
|
||||
};
|
||||
|
||||
async function authenticateCredential(
|
||||
requestOptions: WebAuthnRequestOptions
|
||||
): Promise<CredentialForAuthenticate> {
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: {
|
||||
allowCredentials: requestOptions?.allowCredentials?.map((c) => ({
|
||||
type: c.type,
|
||||
id: decode(c.id),
|
||||
})),
|
||||
challenge: decode(requestOptions?.challenge),
|
||||
timeout: requestOptions?.timeout,
|
||||
userVerification: requestOptions?.userVerification,
|
||||
},
|
||||
});
|
||||
return credential as CredentialForAuthenticate;
|
||||
}
|
||||
|
||||
export type WebAuthnAuthenticateButtonProps = Omit<
|
||||
WebAuthnButtonProps,
|
||||
"action" | "enable" | "onClick" | "text"
|
||||
> & {
|
||||
requestOptions: WebAuthnRequestOptions;
|
||||
};
|
||||
export const WebAuthnAuthenticateButton: FC<
|
||||
WebAuthnAuthenticateButtonProps
|
||||
> = ({ requestOptions, ...props }) => {
|
||||
async function authenticate(): Promise<unknown> {
|
||||
const credential = await authenticateCredential(requestOptions);
|
||||
return {
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: encodeUrl(credential.rawId),
|
||||
response: {
|
||||
authenticatorData: encodeUrl(credential.response.authenticatorData),
|
||||
clientDataJSON: encodeUrl(credential.response.clientDataJSON),
|
||||
signature: encodeUrl(credential.response.signature),
|
||||
userHandle: encodeUrl(credential.response.userHandle),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<WebAuthnButton
|
||||
action="authenticate"
|
||||
enable={requestOptions?.allowCredentials?.length > 0}
|
||||
onClick={authenticate}
|
||||
text={"Authenticate Existing Device"}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default WebAuthnAuthenticateButton;
|
71
ui/src/components/WebAuthnButton.tsx
Normal file
71
ui/src/components/WebAuthnButton.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import Button, { ButtonProps } from "@mui/material/Button";
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
|
||||
import AlertDialog from "./AlertDialog";
|
||||
|
||||
export type WebAuthnButtonProps = Omit<ButtonProps, "action"> & {
|
||||
action: string;
|
||||
csrfToken: string;
|
||||
enable: boolean;
|
||||
onClick: () => Promise<unknown>;
|
||||
text: string;
|
||||
url: string;
|
||||
};
|
||||
export const WebAuthnButton: FC<WebAuthnButtonProps> = ({
|
||||
action,
|
||||
csrfToken,
|
||||
enable,
|
||||
onClick,
|
||||
text,
|
||||
url,
|
||||
...props
|
||||
}) => {
|
||||
const formRef = useRef<HTMLFormElement>();
|
||||
const responseRef = useRef<HTMLInputElement>();
|
||||
const [error, setError] = useState<string>(null);
|
||||
|
||||
function handleClickButton(evt: React.MouseEvent): void {
|
||||
evt.preventDefault();
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await onClick();
|
||||
responseRef.current.value = JSON.stringify(response);
|
||||
formRef.current.submit();
|
||||
} catch (e) {
|
||||
setError(`${e}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
function handleClickDialogOK(evt: React.MouseEvent): void {
|
||||
evt.preventDefault();
|
||||
setError(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleClickButton}
|
||||
variant="contained"
|
||||
disabled={!enable}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
<form ref={formRef} method="post" action={url}>
|
||||
<input type="hidden" name="_pomerium_csrf" value={csrfToken} />
|
||||
<input type="hidden" name="action" value={action} />
|
||||
<input type="hidden" name={action + "_response"} ref={responseRef} />
|
||||
</form>
|
||||
<AlertDialog
|
||||
title="Error"
|
||||
severity="error"
|
||||
open={!!error}
|
||||
actions={<Button onClick={handleClickDialogOK}>OK</Button>}
|
||||
>
|
||||
{error}
|
||||
</AlertDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default WebAuthnButton;
|
89
ui/src/components/WebAuthnRegisterButton.tsx
Normal file
89
ui/src/components/WebAuthnRegisterButton.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import React, { FC } from "react";
|
||||
|
||||
import { WebAuthnCreationOptions } from "../types";
|
||||
import { decode, encodeUrl } from "../util/base64";
|
||||
import WebAuthnButton, { WebAuthnButtonProps } from "./WebAuthnButton";
|
||||
|
||||
type CredentialForCreate = {
|
||||
id: string;
|
||||
type: string;
|
||||
rawId: ArrayBuffer;
|
||||
response: {
|
||||
attestationObject: ArrayBuffer;
|
||||
clientDataJSON: ArrayBuffer;
|
||||
};
|
||||
};
|
||||
|
||||
async function createCredential(
|
||||
creationOptions: WebAuthnCreationOptions
|
||||
): Promise<CredentialForCreate> {
|
||||
const credential = await navigator.credentials.create({
|
||||
publicKey: {
|
||||
attestation: creationOptions?.attestation || undefined,
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment:
|
||||
creationOptions?.authenticatorSelection?.authenticatorAttachment ||
|
||||
undefined,
|
||||
requireResidentKey:
|
||||
creationOptions?.authenticatorSelection?.requireResidentKey ||
|
||||
undefined,
|
||||
residentKey: creationOptions?.authenticatorSelection?.residentKey,
|
||||
userVerification:
|
||||
creationOptions?.authenticatorSelection?.userVerification ||
|
||||
undefined,
|
||||
},
|
||||
challenge: decode(creationOptions?.challenge),
|
||||
pubKeyCredParams: creationOptions?.pubKeyCredParams?.map((p) => ({
|
||||
type: p.type,
|
||||
alg: p.alg,
|
||||
})),
|
||||
rp: {
|
||||
name: creationOptions?.rp?.name,
|
||||
},
|
||||
timeout: creationOptions?.timeout,
|
||||
user: {
|
||||
id: decode(creationOptions?.user?.id),
|
||||
name: creationOptions?.user?.name,
|
||||
displayName: creationOptions?.user?.displayName,
|
||||
},
|
||||
},
|
||||
});
|
||||
return credential as CredentialForCreate;
|
||||
}
|
||||
|
||||
export type WebAuthnRegisterButtonProps = Omit<
|
||||
WebAuthnButtonProps,
|
||||
"action" | "enable" | "onClick" | "text"
|
||||
> & {
|
||||
creationOptions: WebAuthnCreationOptions;
|
||||
csrfToken: string;
|
||||
url: string;
|
||||
};
|
||||
export const WebAuthnRegisterButton: FC<WebAuthnRegisterButtonProps> = ({
|
||||
creationOptions,
|
||||
...props
|
||||
}) => {
|
||||
async function register(): Promise<unknown> {
|
||||
const credential = await createCredential(creationOptions);
|
||||
return {
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: encodeUrl(credential.rawId),
|
||||
response: {
|
||||
attestationObject: encodeUrl(credential.response.attestationObject),
|
||||
clientDataJSON: encodeUrl(credential.response.clientDataJSON),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<WebAuthnButton
|
||||
action="register"
|
||||
enable={!!creationOptions}
|
||||
onClick={register}
|
||||
text={"Register New Device"}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export default WebAuthnRegisterButton;
|
|
@ -1,202 +1,32 @@
|
|||
import { decode, encodeUrl } from "../util/base64";
|
||||
import AlertDialog from "./AlertDialog";
|
||||
import ExperimentalIcon from "./ExperimentalIcon";
|
||||
import HeroSection from "./HeroSection";
|
||||
import Button from "@mui/material/Button";
|
||||
import Container from "@mui/material/Container";
|
||||
import Paper from "@mui/material/Paper";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import React, { FC, useRef, useState } from "react";
|
||||
import {
|
||||
WebAuthnCreationOptions,
|
||||
WebAuthnRegistrationPageData,
|
||||
WebAuthnRequestOptions
|
||||
} from "src/types";
|
||||
import JwtIcon from "./JwtIcon";
|
||||
import ClaimsTable from "./ClaimsTable";
|
||||
import React, { FC } from "react";
|
||||
import { WebAuthnRegistrationPageData } from "src/types";
|
||||
|
||||
import ExperimentalIcon from "./ExperimentalIcon";
|
||||
import Section from "./Section";
|
||||
|
||||
type CredentialForAuthenticate = {
|
||||
id: string;
|
||||
type: string;
|
||||
rawId: ArrayBuffer;
|
||||
response: {
|
||||
authenticatorData: ArrayBuffer;
|
||||
clientDataJSON: ArrayBuffer;
|
||||
signature: ArrayBuffer;
|
||||
userHandle: ArrayBuffer;
|
||||
};
|
||||
};
|
||||
|
||||
async function authenticateCredential(
|
||||
requestOptions: WebAuthnRequestOptions
|
||||
): Promise<CredentialForAuthenticate> {
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: {
|
||||
allowCredentials: requestOptions?.allowCredentials?.map((c) => ({
|
||||
type: c.type,
|
||||
id: decode(c.id)
|
||||
})),
|
||||
challenge: decode(requestOptions?.challenge),
|
||||
timeout: requestOptions?.timeout,
|
||||
userVerification: requestOptions?.userVerification
|
||||
}
|
||||
});
|
||||
return credential as CredentialForAuthenticate;
|
||||
}
|
||||
|
||||
type CredentialForCreate = {
|
||||
id: string;
|
||||
type: string;
|
||||
rawId: ArrayBuffer;
|
||||
response: {
|
||||
attestationObject: ArrayBuffer;
|
||||
clientDataJSON: ArrayBuffer;
|
||||
};
|
||||
};
|
||||
|
||||
async function createCredential(
|
||||
creationOptions: WebAuthnCreationOptions
|
||||
): Promise<CredentialForCreate> {
|
||||
const credential = await navigator.credentials.create({
|
||||
publicKey: {
|
||||
attestation: creationOptions?.attestation || undefined,
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment:
|
||||
creationOptions?.authenticatorSelection?.authenticatorAttachment ||
|
||||
undefined,
|
||||
requireResidentKey:
|
||||
creationOptions?.authenticatorSelection?.requireResidentKey ||
|
||||
undefined,
|
||||
residentKey: creationOptions?.authenticatorSelection?.residentKey,
|
||||
userVerification:
|
||||
creationOptions?.authenticatorSelection?.userVerification || undefined
|
||||
},
|
||||
challenge: decode(creationOptions?.challenge),
|
||||
pubKeyCredParams: creationOptions?.pubKeyCredParams?.map((p) => ({
|
||||
type: p.type,
|
||||
alg: p.alg
|
||||
})),
|
||||
rp: {
|
||||
name: creationOptions?.rp?.name
|
||||
},
|
||||
timeout: creationOptions?.timeout,
|
||||
user: {
|
||||
id: decode(creationOptions?.user?.id),
|
||||
name: creationOptions?.user?.name,
|
||||
displayName: creationOptions?.user?.displayName
|
||||
}
|
||||
}
|
||||
});
|
||||
return credential as CredentialForCreate;
|
||||
}
|
||||
import WebAuthnAuthenticateButton from "./WebAuthnAuthenticateButton";
|
||||
import WebAuthnRegisterButton from "./WebAuthnRegisterButton";
|
||||
|
||||
type WebAuthnRegistrationPageProps = {
|
||||
data: WebAuthnRegistrationPageData;
|
||||
};
|
||||
const WebAuthnRegistrationPage: FC<WebAuthnRegistrationPageProps> = ({
|
||||
data
|
||||
data,
|
||||
}) => {
|
||||
const authenticateFormRef = useRef<HTMLFormElement>();
|
||||
const authenticateResponseRef = useRef<HTMLInputElement>();
|
||||
const registerFormRef = useRef<HTMLFormElement>();
|
||||
const registerResponseRef = useRef<HTMLInputElement>();
|
||||
|
||||
const [error, setError] = useState<string>(null);
|
||||
|
||||
const enableAuthenticate = data?.requestOptions?.allowCredentials?.length > 0;
|
||||
|
||||
function handleClickAuthenticate(evt: React.MouseEvent): void {
|
||||
evt.preventDefault();
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const credential = await authenticateCredential(data?.requestOptions);
|
||||
authenticateResponseRef.current.value = JSON.stringify({
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: encodeUrl(credential.rawId),
|
||||
response: {
|
||||
authenticatorData: encodeUrl(credential.response.authenticatorData),
|
||||
clientDataJSON: encodeUrl(credential.response.clientDataJSON),
|
||||
signature: encodeUrl(credential.response.signature),
|
||||
userHandle: encodeUrl(credential.response.userHandle)
|
||||
}
|
||||
});
|
||||
authenticateFormRef.current.submit();
|
||||
} catch (e) {
|
||||
setError(`${e}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
function handleClickDialogOK(evt: React.MouseEvent): void {
|
||||
evt.preventDefault();
|
||||
setError(null);
|
||||
}
|
||||
function handleClickRegister(evt: React.MouseEvent): void {
|
||||
evt.preventDefault();
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const credential = await createCredential(data?.creationOptions);
|
||||
registerResponseRef.current.value = JSON.stringify({
|
||||
id: credential.id,
|
||||
type: credential.type,
|
||||
rawId: encodeUrl(credential.rawId),
|
||||
response: {
|
||||
attestationObject: encodeUrl(credential.response.attestationObject),
|
||||
clientDataJSON: encodeUrl(credential.response.clientDataJSON)
|
||||
}
|
||||
});
|
||||
registerFormRef.current.submit();
|
||||
} catch (e) {
|
||||
setError(`${e}`);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
return (
|
||||
<Section title="WebAuthn Registration" icon={<ExperimentalIcon />}>
|
||||
<Paper sx={{ padding: "16px" }}>
|
||||
<Stack direction="row" justifyContent="center" spacing={3}>
|
||||
<Button onClick={handleClickRegister} variant="contained">
|
||||
Register New Device
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleClickAuthenticate}
|
||||
variant="contained"
|
||||
disabled={!enableAuthenticate}
|
||||
>
|
||||
Authenticate Existing Device
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<form ref={authenticateFormRef} method="post" action={data?.selfUrl}>
|
||||
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
|
||||
<input type="hidden" name="action" value="authenticate" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticate_response"
|
||||
ref={authenticateResponseRef}
|
||||
<Stack direction="row" justifyContent="center" spacing={1}>
|
||||
<WebAuthnRegisterButton
|
||||
creationOptions={data?.creationOptions}
|
||||
csrfToken={data?.csrfToken}
|
||||
url={data?.selfUrl}
|
||||
/>
|
||||
</form>
|
||||
<form ref={registerFormRef} method="POST" action={data?.selfUrl}>
|
||||
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
|
||||
<input type="hidden" name="action" value="register" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="register_response"
|
||||
ref={registerResponseRef}
|
||||
<WebAuthnAuthenticateButton
|
||||
requestOptions={data?.requestOptions}
|
||||
csrfToken={data?.csrfToken}
|
||||
url={data?.selfUrl}
|
||||
/>
|
||||
</form>
|
||||
<AlertDialog
|
||||
title="Error"
|
||||
severity="error"
|
||||
open={!!error}
|
||||
actions={<Button onClick={handleClickDialogOK}>OK</Button>}
|
||||
>
|
||||
{error}
|
||||
</AlertDialog>
|
||||
</Stack>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue