diff --git a/authenticate/authenticate.go b/authenticate/authenticate.go index 7ae42dfd1..67f4acb54 100644 --- a/authenticate/authenticate.go +++ b/authenticate/authenticate.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" + "github.com/pomerium/pomerium/authenticate/handlers/webauthn" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/pkg/cryptutil" @@ -33,9 +34,10 @@ func ValidateOptions(o *config.Options) error { // Authenticate contains data required to run the authenticate service. type Authenticate struct { - cfg *authenticateConfig - options *config.AtomicOptions - state *atomicAuthenticateState + cfg *authenticateConfig + options *config.AtomicOptions + state *atomicAuthenticateState + webauthn *webauthn.Handler } // New validates and creates a new authenticate service from a set of Options. @@ -45,6 +47,7 @@ func New(cfg *config.Config, options ...Option) (*Authenticate, error) { options: config.NewAtomicOptions(), state: newAtomicAuthenticateState(newAuthenticateState()), } + a.webauthn = webauthn.New(a.getWebauthnState) state, err := newAuthenticateStateFromConfig(cfg) if err != nil { diff --git a/authenticate/handlers.go b/authenticate/handlers.go index 330769209..0ac77db15 100644 --- a/authenticate/handlers.go +++ b/authenticate/handlers.go @@ -17,6 +17,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "github.com/pomerium/csrf" + "github.com/pomerium/pomerium/authenticate/handlers" "github.com/pomerium/pomerium/authenticate/handlers/webauthn" "github.com/pomerium/pomerium/internal/httputil" @@ -96,7 +97,7 @@ func (a *Authenticate) mountDashboard(r *mux.Router) { sr.Path("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo)) sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn)) sr.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut)) - sr.Path("/webauthn").Handler(webauthn.New(a.getWebauthnState)) + sr.Path("/webauthn").Handler(a.webauthn) sr.Path("/device-enrolled").Handler(httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error { handlers.DeviceEnrolled().ServeHTTP(w, r) return nil @@ -561,6 +562,8 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error { groups = append(groups, pbDirectoryGroup) } + creationOptions, requestOptions, _ := a.webauthn.GetOptions(ctx) + handlers.UserInfo(handlers.UserInfoData{ CSRFToken: csrf.Token(r), DirectoryGroups: groups, @@ -568,7 +571,10 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error { IsImpersonated: isImpersonated, Session: pbSession, User: pbUser, - WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()), + + WebAuthnCreationOptions: creationOptions, + WebAuthnRequestOptions: requestOptions, + WebAuthnURL: urlutil.WebAuthnURL(r, authenticateURL, state.sharedKey, r.URL.Query()), }).ServeHTTP(w, r) return nil } diff --git a/authenticate/handlers/userinfo.go b/authenticate/handlers/userinfo.go index 5ec61e6c4..99d227076 100644 --- a/authenticate/handlers/userinfo.go +++ b/authenticate/handlers/userinfo.go @@ -11,6 +11,7 @@ import ( "github.com/pomerium/pomerium/pkg/grpc/session" "github.com/pomerium/pomerium/pkg/grpc/user" "github.com/pomerium/pomerium/ui" + "github.com/pomerium/webauthn" ) // UserInfoData is the data for the UserInfo page. @@ -21,7 +22,10 @@ type UserInfoData struct { IsImpersonated bool Session *session.Session User *user.User - WebAuthnURL string + + WebAuthnCreationOptions *webauthn.PublicKeyCredentialCreationOptions + WebAuthnRequestOptions *webauthn.PublicKeyCredentialRequestOptions + WebAuthnURL string } // ToJSON converts the data into a JSON map. @@ -45,6 +49,8 @@ func (data UserInfoData) ToJSON() map[string]interface{} { if bs, err := protojson.Marshal(data.User); err == nil { m["user"] = json.RawMessage(bs) } + m["webAuthnCreationOptions"] = data.WebAuthnCreationOptions + m["webAuthnRequestOptions"] = data.WebAuthnRequestOptions m["webAuthnUrl"] = data.WebAuthnURL return m } diff --git a/authenticate/handlers/webauthn/webauthn.go b/authenticate/handlers/webauthn/webauthn.go index 00655321e..4afc804b7 100644 --- a/authenticate/handlers/webauthn/webauthn.go +++ b/authenticate/handlers/webauthn/webauthn.go @@ -72,11 +72,50 @@ func New(getState StateProvider) *Handler { } } +// GetOptions returns the creation and request options for WebAuthn. +func (h *Handler) GetOptions(ctx context.Context) ( + creationOptions *webauthn.PublicKeyCredentialCreationOptions, + requestOptions *webauthn.PublicKeyCredentialRequestOptions, + err error, +) { + state, err := h.getState(ctx) + if err != nil { + return nil, nil, err + } + + return h.getOptions(ctx, state, webauthnutil.DefaultDeviceType) +} + // ServeHTTP serves the HTTP handler. func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { httputil.HandlerFunc(h.handle).ServeHTTP(w, r) } +func (h *Handler) getOptions(ctx context.Context, state *State, deviceTypeParam string) ( + creationOptions *webauthn.PublicKeyCredentialCreationOptions, + requestOptions *webauthn.PublicKeyCredentialRequestOptions, + err error, +) { + // get the user information + u, err := user.Get(ctx, state.Client, state.Session.GetUserId()) + if err != nil { + return nil, nil, err + } + + // get the device credentials + knownDeviceCredentials, err := getKnownDeviceCredentials(ctx, state.Client, u.GetDeviceCredentialIds()...) + if err != nil { + return nil, nil, err + } + + // get the stored device type + deviceType := webauthnutil.GetDeviceType(ctx, state.Client, deviceTypeParam) + + creationOptions = webauthnutil.GenerateCreationOptions(state.SharedKey, deviceType, u) + requestOptions = webauthnutil.GenerateRequestOptions(state.SharedKey, deviceType, knownDeviceCredentials) + return creationOptions, requestOptions, nil +} + func (h *Handler) handle(w http.ResponseWriter, r *http.Request) error { s, err := h.getState(r.Context()) if err != nil { @@ -351,24 +390,11 @@ func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *Stat return errMissingDeviceType } - // get the user information - u, err := user.Get(ctx, state.Client, state.Session.GetUserId()) + creationOptions, requestOptions, err := h.getOptions(ctx, state, deviceTypeParam) if err != nil { return err } - // get the device credentials - knownDeviceCredentials, err := getKnownDeviceCredentials(ctx, state.Client, u.GetDeviceCredentialIds()...) - if err != nil { - return err - } - - // get the stored device type - deviceType := webauthnutil.GetDeviceType(ctx, state.Client, deviceTypeParam) - - creationOptions := webauthnutil.GenerateCreationOptions(state.SharedKey, deviceType, u) - requestOptions := webauthnutil.GenerateRequestOptions(state.SharedKey, deviceType, knownDeviceCredentials) - return ui.ServePage(w, r, "WebAuthnRegistration", map[string]interface{}{ "creationOptions": creationOptions, "requestOptions": requestOptions, diff --git a/authenticate/handlers_test.go b/authenticate/handlers_test.go index 4002e6bde..cf98b4ef8 100644 --- a/authenticate/handlers_test.go +++ b/authenticate/handlers_test.go @@ -24,6 +24,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/pomerium/pomerium/authenticate/handlers/webauthn" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/encoding" "github.com/pomerium/pomerium/internal/encoding/jws" @@ -748,6 +749,7 @@ func TestAuthenticate_userInfo(t *testing.T) { directoryClient: new(mockDirectoryServiceClient), }), } + a.webauthn = webauthn.New(a.getWebauthnState) r := httptest.NewRequest(tt.method, tt.url.String(), nil) state, err := tt.sessionStore.LoadSession(r) if err != nil { diff --git a/ui/package.json b/ui/package.json index c31ef1661..12f39ead6 100644 --- a/ui/package.json +++ b/ui/package.json @@ -49,5 +49,12 @@ "prettier": "^2.4.1", "ts-node": "^10.4.0", "typescript": "^4.4.4" + }, + "prettier": { + "importOrder": [ + "^[./]" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true } } diff --git a/ui/src/components/Avatar.tsx b/ui/src/components/Avatar.tsx index 700fc6b28..054619394 100644 --- a/ui/src/components/Avatar.tsx +++ b/ui/src/components/Avatar.tsx @@ -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 ? ( - - ) : ( - - ); + return url ? : ; }; diff --git a/ui/src/components/SessionDeviceCredentials.tsx b/ui/src/components/SessionDeviceCredentials.tsx index 119b90157..39f95b9f4 100644 --- a/ui/src/components/SessionDeviceCredentials.tsx +++ b/ui/src/components/SessionDeviceCredentials.tsx @@ -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 = ({ csrfToken, user, session, - webAuthnUrl + webAuthnCreationOptions, + webAuthnRequestOptions, + webAuthnUrl, }) => { const currentSessionDeviceCredentialIds = []; const otherDeviceCredentialIds = []; @@ -37,45 +42,62 @@ export const SessionDeviceCredentials: FC = ({ }); return ( - - - - - Current Session Device Credentials - - - - - - {otherDeviceCredentialIds?.length > 0 ? ( - <> - - - Other Device Credentials - - - - + + + + + Current Session Device Credentials + + + + - - - ) : ( - <> - )} - - - Register device with WebAuthn. - - - - + + + + + + + + + + {otherDeviceCredentialIds?.length > 0 ? ( + <> + + + + + Other Device Credentials + + + + + + + + + ) : ( + <> + )} + ); }; export default SessionDeviceCredentials; diff --git a/ui/src/components/UserInfoPage.tsx b/ui/src/components/UserInfoPage.tsx index 46372a83e..469d0111d 100644 --- a/ui/src/components/UserInfoPage.tsx +++ b/ui/src/components/UserInfoPage.tsx @@ -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 = ({ 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 ( @@ -31,37 +31,37 @@ const UserInfoPage: FC = ({ data }) => { open PaperProps={{ sx: { - backgroundColor: 'neutral.900', + backgroundColor: "neutral.900", width: 256, - height: '100vh', - } + height: "100vh", + }, }} variant="persistent" > - + )} + marginLeft: mdUp ? "256px" : "0px", + }} + > + {subpage === "User" && } - {subpage === 'User' && ( - - )} - - {subpage === 'Groups Info' && ( + {subpage === "Groups Info" && ( )} - {subpage === 'Devices Info' && ( + {subpage === "Devices Info" && ( )} diff --git a/ui/src/components/WebAuthnAuthenticateButton.tsx b/ui/src/components/WebAuthnAuthenticateButton.tsx new file mode 100644 index 000000000..fb9b547d9 --- /dev/null +++ b/ui/src/components/WebAuthnAuthenticateButton.tsx @@ -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 { + 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 { + 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 ( + 0} + onClick={authenticate} + text={"Authenticate Existing Device"} + {...props} + /> + ); +}; +export default WebAuthnAuthenticateButton; diff --git a/ui/src/components/WebAuthnButton.tsx b/ui/src/components/WebAuthnButton.tsx new file mode 100644 index 000000000..ff3fa420e --- /dev/null +++ b/ui/src/components/WebAuthnButton.tsx @@ -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 & { + action: string; + csrfToken: string; + enable: boolean; + onClick: () => Promise; + text: string; + url: string; +}; +export const WebAuthnButton: FC = ({ + action, + csrfToken, + enable, + onClick, + text, + url, + ...props +}) => { + const formRef = useRef(); + const responseRef = useRef(); + const [error, setError] = useState(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 ( + <> + +
+ + + +
+ OK} + > + {error} + + + ); +}; +export default WebAuthnButton; diff --git a/ui/src/components/WebAuthnRegisterButton.tsx b/ui/src/components/WebAuthnRegisterButton.tsx new file mode 100644 index 000000000..05f25dd2d --- /dev/null +++ b/ui/src/components/WebAuthnRegisterButton.tsx @@ -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 { + 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 = ({ + creationOptions, + ...props +}) => { + async function register(): Promise { + 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 ( + + ); +}; +export default WebAuthnRegisterButton; diff --git a/ui/src/components/WebAuthnRegistrationPage.tsx b/ui/src/components/WebAuthnRegistrationPage.tsx index e092affb4..c7d93e1a8 100644 --- a/ui/src/components/WebAuthnRegistrationPage.tsx +++ b/ui/src/components/WebAuthnRegistrationPage.tsx @@ -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 { - 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 { - 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 = ({ - data + data, }) => { - const authenticateFormRef = useRef(); - const authenticateResponseRef = useRef(); - const registerFormRef = useRef(); - const registerResponseRef = useRef(); - - const [error, setError] = useState(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 (
}> - - - - - - -
- - - + - -
- - - -
- OK} - > - {error} - +
); }; diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 96cd96642..755b4b827 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -112,6 +112,8 @@ export type UserInfoPageData = BasePageData & { directoryUser?: DirectoryUser; session?: Session; user?: User; + webAuthnCreationOptions?: WebAuthnCreationOptions; + webAuthnRequestOptions?: WebAuthnRequestOptions; webAuthnUrl?: string; };