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;
};