mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-12 16:47:41 +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
|
@ -7,6 +7,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/log"
|
"github.com/pomerium/pomerium/internal/log"
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
"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.
|
// Authenticate contains data required to run the authenticate service.
|
||||||
type Authenticate struct {
|
type Authenticate struct {
|
||||||
cfg *authenticateConfig
|
cfg *authenticateConfig
|
||||||
options *config.AtomicOptions
|
options *config.AtomicOptions
|
||||||
state *atomicAuthenticateState
|
state *atomicAuthenticateState
|
||||||
|
webauthn *webauthn.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
// New validates and creates a new authenticate service from a set of Options.
|
// 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(),
|
options: config.NewAtomicOptions(),
|
||||||
state: newAtomicAuthenticateState(newAuthenticateState()),
|
state: newAtomicAuthenticateState(newAuthenticateState()),
|
||||||
}
|
}
|
||||||
|
a.webauthn = webauthn.New(a.getWebauthnState)
|
||||||
|
|
||||||
state, err := newAuthenticateStateFromConfig(cfg)
|
state, err := newAuthenticateStateFromConfig(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/pomerium/csrf"
|
"github.com/pomerium/csrf"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/authenticate/handlers"
|
"github.com/pomerium/pomerium/authenticate/handlers"
|
||||||
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
|
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
|
||||||
"github.com/pomerium/pomerium/internal/httputil"
|
"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("/").Handler(a.requireValidSignatureOnRedirect(a.userInfo))
|
||||||
sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn))
|
sr.Path("/sign_in").Handler(a.requireValidSignature(a.SignIn))
|
||||||
sr.Path("/sign_out").Handler(httputil.HandlerFunc(a.SignOut))
|
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 {
|
sr.Path("/device-enrolled").Handler(httputil.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
||||||
handlers.DeviceEnrolled().ServeHTTP(w, r)
|
handlers.DeviceEnrolled().ServeHTTP(w, r)
|
||||||
return nil
|
return nil
|
||||||
|
@ -561,6 +562,8 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
||||||
groups = append(groups, pbDirectoryGroup)
|
groups = append(groups, pbDirectoryGroup)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
creationOptions, requestOptions, _ := a.webauthn.GetOptions(ctx)
|
||||||
|
|
||||||
handlers.UserInfo(handlers.UserInfoData{
|
handlers.UserInfo(handlers.UserInfoData{
|
||||||
CSRFToken: csrf.Token(r),
|
CSRFToken: csrf.Token(r),
|
||||||
DirectoryGroups: groups,
|
DirectoryGroups: groups,
|
||||||
|
@ -568,7 +571,10 @@ func (a *Authenticate) userInfo(w http.ResponseWriter, r *http.Request) error {
|
||||||
IsImpersonated: isImpersonated,
|
IsImpersonated: isImpersonated,
|
||||||
Session: pbSession,
|
Session: pbSession,
|
||||||
User: pbUser,
|
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)
|
}).ServeHTTP(w, r)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/session"
|
"github.com/pomerium/pomerium/pkg/grpc/session"
|
||||||
"github.com/pomerium/pomerium/pkg/grpc/user"
|
"github.com/pomerium/pomerium/pkg/grpc/user"
|
||||||
"github.com/pomerium/pomerium/ui"
|
"github.com/pomerium/pomerium/ui"
|
||||||
|
"github.com/pomerium/webauthn"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserInfoData is the data for the UserInfo page.
|
// UserInfoData is the data for the UserInfo page.
|
||||||
|
@ -21,7 +22,10 @@ type UserInfoData struct {
|
||||||
IsImpersonated bool
|
IsImpersonated bool
|
||||||
Session *session.Session
|
Session *session.Session
|
||||||
User *user.User
|
User *user.User
|
||||||
WebAuthnURL string
|
|
||||||
|
WebAuthnCreationOptions *webauthn.PublicKeyCredentialCreationOptions
|
||||||
|
WebAuthnRequestOptions *webauthn.PublicKeyCredentialRequestOptions
|
||||||
|
WebAuthnURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToJSON converts the data into a JSON map.
|
// 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 {
|
if bs, err := protojson.Marshal(data.User); err == nil {
|
||||||
m["user"] = json.RawMessage(bs)
|
m["user"] = json.RawMessage(bs)
|
||||||
}
|
}
|
||||||
|
m["webAuthnCreationOptions"] = data.WebAuthnCreationOptions
|
||||||
|
m["webAuthnRequestOptions"] = data.WebAuthnRequestOptions
|
||||||
m["webAuthnUrl"] = data.WebAuthnURL
|
m["webAuthnUrl"] = data.WebAuthnURL
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
// ServeHTTP serves the HTTP handler.
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
httputil.HandlerFunc(h.handle).ServeHTTP(w, r)
|
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 {
|
func (h *Handler) handle(w http.ResponseWriter, r *http.Request) error {
|
||||||
s, err := h.getState(r.Context())
|
s, err := h.getState(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -351,24 +390,11 @@ func (h *Handler) handleView(w http.ResponseWriter, r *http.Request, state *Stat
|
||||||
return errMissingDeviceType
|
return errMissingDeviceType
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the user information
|
creationOptions, requestOptions, err := h.getOptions(ctx, state, deviceTypeParam)
|
||||||
u, err := user.Get(ctx, state.Client, state.Session.GetUserId())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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{}{
|
return ui.ServePage(w, r, "WebAuthnRegistration", map[string]interface{}{
|
||||||
"creationOptions": creationOptions,
|
"creationOptions": creationOptions,
|
||||||
"requestOptions": requestOptions,
|
"requestOptions": requestOptions,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/pomerium/pomerium/authenticate/handlers/webauthn"
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/internal/encoding"
|
"github.com/pomerium/pomerium/internal/encoding"
|
||||||
"github.com/pomerium/pomerium/internal/encoding/jws"
|
"github.com/pomerium/pomerium/internal/encoding/jws"
|
||||||
|
@ -748,6 +749,7 @@ func TestAuthenticate_userInfo(t *testing.T) {
|
||||||
directoryClient: new(mockDirectoryServiceClient),
|
directoryClient: new(mockDirectoryServiceClient),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
a.webauthn = webauthn.New(a.getWebauthnState)
|
||||||
r := httptest.NewRequest(tt.method, tt.url.String(), nil)
|
r := httptest.NewRequest(tt.method, tt.url.String(), nil)
|
||||||
state, err := tt.sessionStore.LoadSession(r)
|
state, err := tt.sessionStore.LoadSession(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -49,5 +49,12 @@
|
||||||
"prettier": "^2.4.1",
|
"prettier": "^2.4.1",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^4.4.4"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"importOrder": [
|
||||||
|
"^[./]"
|
||||||
|
],
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"importOrderSortSpecifiers": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import React from 'react';
|
|
||||||
import {User} from "react-feather";
|
|
||||||
import MuiAvatar from "@mui/material/Avatar";
|
import MuiAvatar from "@mui/material/Avatar";
|
||||||
|
import { isArray } from "lodash";
|
||||||
|
import React from "react";
|
||||||
|
import { User } from "react-feather";
|
||||||
|
|
||||||
type AvatarProps = {
|
type AvatarProps = {
|
||||||
name: string;
|
name: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const Avatar = ({url, name}:AvatarProps): JSX.Element => {
|
export const Avatar = ({ url, name }: AvatarProps): JSX.Element => {
|
||||||
if (url === 'https://graph.microsoft.com/v1.0/me/photo/$value') {
|
if (isArray(url)) {
|
||||||
|
url = url?.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === "https://graph.microsoft.com/v1.0/me/photo/$value") {
|
||||||
url = null;
|
url = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return url ? (
|
return url ? <MuiAvatar alt={name} src={url} /> : <User />;
|
||||||
<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 Box from "@mui/material/Box";
|
||||||
import Paper from "@mui/material/Paper";
|
import Paper from "@mui/material/Paper";
|
||||||
import Stack from "@mui/material/Stack";
|
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 Toolbar from "@mui/material/Toolbar";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import React, { FC } from "react";
|
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 = {
|
export type SessionDeviceCredentialsProps = {
|
||||||
csrfToken: string;
|
csrfToken: string;
|
||||||
user: User;
|
user: User;
|
||||||
session: Session;
|
session: Session;
|
||||||
|
webAuthnCreationOptions: WebAuthnCreationOptions;
|
||||||
|
webAuthnRequestOptions: WebAuthnRequestOptions;
|
||||||
webAuthnUrl: string;
|
webAuthnUrl: string;
|
||||||
};
|
};
|
||||||
export const SessionDeviceCredentials: FC<SessionDeviceCredentialsProps> = ({
|
export const SessionDeviceCredentials: FC<SessionDeviceCredentialsProps> = ({
|
||||||
csrfToken,
|
csrfToken,
|
||||||
user,
|
user,
|
||||||
session,
|
session,
|
||||||
webAuthnUrl
|
webAuthnCreationOptions,
|
||||||
|
webAuthnRequestOptions,
|
||||||
|
webAuthnUrl,
|
||||||
}) => {
|
}) => {
|
||||||
const currentSessionDeviceCredentialIds = [];
|
const currentSessionDeviceCredentialIds = [];
|
||||||
const otherDeviceCredentialIds = [];
|
const otherDeviceCredentialIds = [];
|
||||||
|
@ -37,45 +42,62 @@ export const SessionDeviceCredentials: FC<SessionDeviceCredentialsProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ overflow: "hidden" }}>
|
<>
|
||||||
<Stack>
|
<Paper sx={{ overflow: "hidden" }}>
|
||||||
<Toolbar>
|
<Stack>
|
||||||
<Typography variant="h4" flexGrow={1}>
|
<Toolbar>
|
||||||
Current Session Device Credentials
|
<Typography variant="h4" flexGrow={1}>
|
||||||
</Typography>
|
Current Session Device Credentials
|
||||||
</Toolbar>
|
</Typography>
|
||||||
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
|
||||||
<DeviceCredentialsTable
|
<Stack direction="row" justifyContent="center" spacing={1}>
|
||||||
csrfToken={csrfToken}
|
<WebAuthnRegisterButton
|
||||||
ids={currentSessionDeviceCredentialIds}
|
creationOptions={webAuthnCreationOptions}
|
||||||
webAuthnUrl={webAuthnUrl}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{otherDeviceCredentialIds?.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<Toolbar>
|
|
||||||
<Typography variant="h4" flexGrow={1}>
|
|
||||||
Other Device Credentials
|
|
||||||
</Typography>
|
|
||||||
</Toolbar>
|
|
||||||
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
|
||||||
<DeviceCredentialsTable
|
|
||||||
csrfToken={csrfToken}
|
csrfToken={csrfToken}
|
||||||
ids={otherDeviceCredentialIds}
|
url={webAuthnUrl}
|
||||||
webAuthnUrl={webAuthnUrl}
|
size="small"
|
||||||
/>
|
/>
|
||||||
</Box>
|
<WebAuthnAuthenticateButton
|
||||||
</>
|
requestOptions={webAuthnRequestOptions}
|
||||||
) : (
|
csrfToken={csrfToken}
|
||||||
<></>
|
url={webAuthnUrl}
|
||||||
)}
|
size="small"
|
||||||
<SectionFooter>
|
/>
|
||||||
<Typography variant="caption">
|
</Stack>
|
||||||
Register device with <a href={webAuthnUrl}>WebAuthn</a>.
|
</Toolbar>
|
||||||
</Typography>
|
<Box sx={{ padding: 3, paddingTop: 0 }}>
|
||||||
</SectionFooter>
|
<DeviceCredentialsTable
|
||||||
</Stack>
|
csrfToken={csrfToken}
|
||||||
</Paper>
|
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;
|
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 GroupDetails from "./GroupDetails";
|
||||||
import SessionDetails from "./SessionDetails";
|
import SessionDetails from "./SessionDetails";
|
||||||
import SessionDeviceCredentials from "./SessionDeviceCredentials";
|
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 { ToolbarOffset } from "./ToolbarOffset";
|
||||||
import {UserSidebarContent} from "./UserSidebarContent";
|
import { UserSidebarContent } from "./UserSidebarContent";
|
||||||
import {SubpageContext} from "../context/Subpage";
|
|
||||||
import Stack from "@mui/material/Stack";
|
|
||||||
|
|
||||||
type UserInfoPageProps = {
|
type UserInfoPageProps = {
|
||||||
data: UserInfoPageData;
|
data: UserInfoPageData;
|
||||||
};
|
};
|
||||||
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const mdUp = useMediaQuery(() => theme.breakpoints.up('md'), {
|
const mdUp = useMediaQuery(() => theme.breakpoints.up("md"), {
|
||||||
defaultMatches: true,
|
defaultMatches: true,
|
||||||
noSsr: false
|
noSsr: false,
|
||||||
});
|
});
|
||||||
const {subpage} = useContext(SubpageContext);
|
const { subpage } = useContext(SubpageContext);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth={false}>
|
<Container maxWidth={false}>
|
||||||
|
@ -31,37 +31,37 @@ const UserInfoPage: FC<UserInfoPageProps> = ({ data }) => {
|
||||||
open
|
open
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
sx: {
|
sx: {
|
||||||
backgroundColor: 'neutral.900',
|
backgroundColor: "neutral.900",
|
||||||
width: 256,
|
width: 256,
|
||||||
height: '100vh',
|
height: "100vh",
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
variant="persistent"
|
variant="persistent"
|
||||||
>
|
>
|
||||||
<ToolbarOffset />
|
<ToolbarOffset />
|
||||||
<UserSidebarContent close={null}/>
|
<UserSidebarContent close={null} />
|
||||||
<ToolbarOffset />
|
<ToolbarOffset />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)}
|
)}
|
||||||
<Stack
|
<Stack
|
||||||
spacing={3}
|
spacing={3}
|
||||||
sx={{
|
sx={{
|
||||||
marginLeft: mdUp ? '256px' : '0px',
|
marginLeft: mdUp ? "256px" : "0px",
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
|
{subpage === "User" && <SessionDetails session={data?.session} />}
|
||||||
|
|
||||||
{subpage === 'User' && (
|
{subpage === "Groups Info" && (
|
||||||
<SessionDetails session={data?.session} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{subpage === 'Groups Info' && (
|
|
||||||
<GroupDetails groups={data?.directoryGroups} />
|
<GroupDetails groups={data?.directoryGroups} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{subpage === 'Devices Info' && (
|
{subpage === "Devices Info" && (
|
||||||
<SessionDeviceCredentials
|
<SessionDeviceCredentials
|
||||||
csrfToken={data?.csrfToken}
|
csrfToken={data?.csrfToken}
|
||||||
session={data?.session}
|
session={data?.session}
|
||||||
user={data?.user}
|
user={data?.user}
|
||||||
|
webAuthnCreationOptions={data?.webAuthnCreationOptions}
|
||||||
|
webAuthnRequestOptions={data?.webAuthnRequestOptions}
|
||||||
webAuthnUrl={data?.webAuthnUrl}
|
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 Stack from "@mui/material/Stack";
|
||||||
import React, { FC, useRef, useState } from "react";
|
import React, { FC } from "react";
|
||||||
import {
|
import { WebAuthnRegistrationPageData } from "src/types";
|
||||||
WebAuthnCreationOptions,
|
|
||||||
WebAuthnRegistrationPageData,
|
import ExperimentalIcon from "./ExperimentalIcon";
|
||||||
WebAuthnRequestOptions
|
|
||||||
} from "src/types";
|
|
||||||
import JwtIcon from "./JwtIcon";
|
|
||||||
import ClaimsTable from "./ClaimsTable";
|
|
||||||
import Section from "./Section";
|
import Section from "./Section";
|
||||||
|
import WebAuthnAuthenticateButton from "./WebAuthnAuthenticateButton";
|
||||||
type CredentialForAuthenticate = {
|
import WebAuthnRegisterButton from "./WebAuthnRegisterButton";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebAuthnRegistrationPageProps = {
|
type WebAuthnRegistrationPageProps = {
|
||||||
data: WebAuthnRegistrationPageData;
|
data: WebAuthnRegistrationPageData;
|
||||||
};
|
};
|
||||||
const WebAuthnRegistrationPage: FC<WebAuthnRegistrationPageProps> = ({
|
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 (
|
return (
|
||||||
<Section title="WebAuthn Registration" icon={<ExperimentalIcon />}>
|
<Section title="WebAuthn Registration" icon={<ExperimentalIcon />}>
|
||||||
<Paper sx={{ padding: "16px" }}>
|
<Stack direction="row" justifyContent="center" spacing={1}>
|
||||||
<Stack direction="row" justifyContent="center" spacing={3}>
|
<WebAuthnRegisterButton
|
||||||
<Button onClick={handleClickRegister} variant="contained">
|
creationOptions={data?.creationOptions}
|
||||||
Register New Device
|
csrfToken={data?.csrfToken}
|
||||||
</Button>
|
url={data?.selfUrl}
|
||||||
<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}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
<WebAuthnAuthenticateButton
|
||||||
<form ref={registerFormRef} method="POST" action={data?.selfUrl}>
|
requestOptions={data?.requestOptions}
|
||||||
<input type="hidden" name="_pomerium_csrf" value={data?.csrfToken} />
|
csrfToken={data?.csrfToken}
|
||||||
<input type="hidden" name="action" value="register" />
|
url={data?.selfUrl}
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="register_response"
|
|
||||||
ref={registerResponseRef}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
</Stack>
|
||||||
<AlertDialog
|
|
||||||
title="Error"
|
|
||||||
severity="error"
|
|
||||||
open={!!error}
|
|
||||||
actions={<Button onClick={handleClickDialogOK}>OK</Button>}
|
|
||||||
>
|
|
||||||
{error}
|
|
||||||
</AlertDialog>
|
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -112,6 +112,8 @@ export type UserInfoPageData = BasePageData & {
|
||||||
directoryUser?: DirectoryUser;
|
directoryUser?: DirectoryUser;
|
||||||
session?: Session;
|
session?: Session;
|
||||||
user?: User;
|
user?: User;
|
||||||
|
webAuthnCreationOptions?: WebAuthnCreationOptions;
|
||||||
|
webAuthnRequestOptions?: WebAuthnRequestOptions;
|
||||||
webAuthnUrl?: string;
|
webAuthnUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue