Skip to content

Commit

Permalink
fix: Fix devices display error; store webauthn origin; remove webauth…
Browse files Browse the repository at this point in the history
…n register popup
  • Loading branch information
wangjf8090 committed Aug 19, 2024
1 parent 76f873c commit 60c3f45
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 97 deletions.
16 changes: 10 additions & 6 deletions packages/itmat-apis/src/trpc/webauthnProcedure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,30 @@ export class WebAuthnRouter {
}),

webauthnRegister: this.baseProcedure.mutation(async ({ ctx }) => {
return await this.webAuthnCore.getWebauthnRegistrationOptions(ctx.user);
const {rpID} = await this.webAuthnCore.getCurrentOriginAndRpID(ctx);
return await this.webAuthnCore.getWebauthnRegistrationOptions(ctx.user, rpID);
}),

webauthnRegisterVerify: this.baseProcedure.input(z.object({
attestationResponse: z.any()
})).mutation(async ({ input, ctx }) => {
return await this.webAuthnCore.handleRegistrationVerify(ctx.req.user, input.attestationResponse);
const {origin, rpID} = await this.webAuthnCore.getCurrentOriginAndRpID(ctx);
return await this.webAuthnCore.handleRegistrationVerify(ctx.req.user, input.attestationResponse,origin, rpID);
}),

webauthnAuthenticate: this.baseProcedure.input(z.object({
userId: z.string()
})).mutation(async ({ input }) => {
return await this.webAuthnCore.getWebauthnAuthenticationOptions(input.userId);
})).mutation(async ({ input, ctx}) => {
const {rpID} = await this.webAuthnCore.getCurrentOriginAndRpID(ctx);
return await this.webAuthnCore.getWebauthnAuthenticationOptions(input.userId, rpID);
}),

webauthnAuthenticateVerify: this.baseProcedure.input(z.object({
userId: z.string(),
assertionResponse: z.any()
})).mutation(async ({ input }) => {
return await this.webAuthnCore.handleAuthenticationVerify(input.userId, input.assertionResponse);
})).mutation(async ({ input, ctx }) => {
const {origin, rpID} = await this.webAuthnCore.getCurrentOriginAndRpID(ctx);
return await this.webAuthnCore.handleAuthenticationVerify(input.userId, input.assertionResponse, origin, rpID);
}),

deleteWebauthnRegisteredDevices: this.baseProcedure.input(z.object({
Expand Down
3 changes: 1 addition & 2 deletions packages/itmat-cores/config/config.sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,5 @@
"adminEmail": "[email protected]",
"aeEndpoint": "http://localhost:9090",
"useWebdav": true,
"webdavPort": 1900,
"webauthnOrigin": "http://localhost:4200"
"webdavPort": 1900
}
43 changes: 26 additions & 17 deletions packages/itmat-cores/src/coreFunc/webauthnCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,14 @@ export class WebauthnCore {
objStore: ObjectStore;
configCore: ConfigCore;
rpName: string;
origin: string;
rpID: string;
constructor(db: DBType, mailer: Mailer, config: IConfiguration, objStore: ObjectStore) {
this.db = db;
this.mailer = mailer;
this.config = config;
this.objStore = objStore;
this.configCore = new ConfigCore(db);
// get the rpName, origin and rpID from the config
// get the rpName,from the config
this.rpName = config.appName;
this.origin = process.env['NX_WEBAUTHN_ORIGIN'] ?? config.webauthnOrigin;
this.rpID = new URL(this.origin).hostname;
}
/**
* webauthn functions
Expand Down Expand Up @@ -122,7 +118,7 @@ export class WebauthnCore {
* @param user
* @returns - The registration options.
*/
public async getWebauthnRegistrationOptions(user: IUser) {
public async getWebauthnRegistrationOptions(user: IUser, rpID: string) {
let webauthn_id;
const challenge = this.generate_challenge();
const webauthnStore = await this.db.collections.webauthn_collection.findOne({
Expand Down Expand Up @@ -163,7 +159,7 @@ export class WebauthnCore {
try {
const options = await generateRegistrationOptions({
rpName: this.rpName,
rpID: this.rpID,
rpID: rpID,
userID: Buffer.from(user.id) as Uint8Array,
userName: user.username,
timeout: 60000,
Expand Down Expand Up @@ -198,7 +194,7 @@ export class WebauthnCore {
* @param attestationResponse - The attestation response.
* @returns - The response.
*/
public async handleRegistrationVerify(user: IUser, attestationResponse: RegistrationResponseJSON) {
public async handleRegistrationVerify(user: IUser, attestationResponse: RegistrationResponseJSON, origin: string, rpID: string) {
let device_id;
const webauthn = await this.db.collections.webauthn_collection.findOne({
userId: user.id
Expand All @@ -216,11 +212,12 @@ export class WebauthnCore {
const decodedString = isoBase64URL.fromBuffer(challenge.buffer as Uint8Array);

try{
// verify the registration response
const {verified, registrationInfo} = await verifyRegistrationResponse({
response: attestationResponse,
expectedChallenge: decodedString,
expectedOrigin: this.origin,
expectedRPID: this.rpID,
expectedOrigin: origin,
expectedRPID: rpID,
requireUserVerification: true
});

Expand All @@ -233,7 +230,8 @@ export class WebauthnCore {
credentialID,
counter,
transports: attestationResponse.response.transports,
id: device_id
id: device_id,
origin: origin
};
devices.push(newDevice);

Expand Down Expand Up @@ -264,7 +262,7 @@ export class WebauthnCore {
* @param userId - The user id.
* @returns - The authentication options.
*/
public async getWebauthnAuthenticationOptions(userId: string): Promise<PublicKeyCredentialRequestOptionsJSON>{
public async getWebauthnAuthenticationOptions(userId: string, rpID: string): Promise<PublicKeyCredentialRequestOptionsJSON>{

const webauthn = await this.db.collections.webauthn_collection.findOne({
userId: userId
Expand All @@ -279,7 +277,6 @@ export class WebauthnCore {
const {devices, challenge} = webauthn;

try{

const options = await generateAuthenticationOptions({
challenge: challenge.buffer as Uint8Array,
timeout: 60000,
Expand All @@ -289,7 +286,7 @@ export class WebauthnCore {
transports: authenticator.transports
})),
userVerification: 'required',
rpID: this.rpID
rpID: rpID
});
return options;
}
Expand All @@ -307,7 +304,7 @@ export class WebauthnCore {
* @param assertionResponse - The assertion response.
* @returns - The response.
*/
public async handleAuthenticationVerify(userId: string, assertionResponse: AuthenticationResponseJSON) {
public async handleAuthenticationVerify(userId: string, assertionResponse: AuthenticationResponseJSON, origin: string, rpID: string) {


const webauthn = await this.db.collections.webauthn_collection.findOne({
Expand Down Expand Up @@ -344,8 +341,8 @@ export class WebauthnCore {
const verification = await verifyAuthenticationResponse({
response: assertionResponse,
expectedChallenge: decodedChallengeString,
expectedOrigin: this.origin,
expectedRPID: this.rpID,
expectedOrigin: origin,
expectedRPID: rpID,
authenticator: {
credentialPublicKey: device.credentialPublicKey.buffer as Uint8Array,
credentialID: device.credentialID,
Expand Down Expand Up @@ -453,5 +450,17 @@ export class WebauthnCore {

return result.devices;
}
/**
* get the current origin and rpID.
* @param ctx - The context.
* @returns - The origin and rpID.
*/

public async getCurrentOriginAndRpID(ctx) {
const req = ctx.req;
const origin = req.headers.origin;
const rpID = (new URL(origin)).hostname;
return {origin, rpID};
}

}
1 change: 0 additions & 1 deletion packages/itmat-cores/src/utils/configManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export interface IConfiguration extends IServerConfig {
aeEndpoint: string;
useWebdav: boolean;
webdavPort: number;
webauthnOrigin: string;
}

export class ConfigurationManager {
Expand Down
1 change: 1 addition & 0 deletions packages/itmat-types/src/types/webauthn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IBase } from './base';
export type AuthenticatorDevice = Omit<OriginalAuthenticatorDevice, 'id' | 'name'> & {
id: string; // the ID of the authenticator credential
name?: string; // the name of the authenticator credential
origin?: string; // the origin of the authenticator credential
};

// include the credential
Expand Down
69 changes: 13 additions & 56 deletions packages/itmat-ui-react/src/Fence.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,17 @@ import { WebAuthnAuthenticationComponent } from './utils/dmpWebauthn/webauthn.au
import { DeviceNicknameModal } from './utils/dmpWebauthn/webuathn.nickname';
import { useAuth } from './utils/dmpWebauthn/webauthn.context';

import {
browserSupportsWebAuthn,
platformAuthenticatorIsAvailable
} from '@simplewebauthn/browser';
import { message } from 'antd';

export const Fence: FunctionComponent = () => {
const {
showRegistrationDialog,
setShowRegistrationDialog,
credentials,
setCredentials,
isUserLogin,
setIsUserLogin,
isWebauthAvailable,
setIsWebauthAvailable,
showRegistrationDialog,
setShowRegistrationDialog,
handleCancelRegistration,
useWebauthn,
setUseWebauthn,
showNicknameModal
} = useAuth();

Expand All @@ -42,9 +36,6 @@ export const Fence: FunctionComponent = () => {
});

const [component, setComponent] = useState<JSX.Element | null>(null);
const [windowComponent, setwindowComponent] = useState<JSX.Element | null>(null);

const [useWebauthn, setUseWebauthn] = useState<'register' | 'authenticate' | 'close'>('close');

const isAnyLoading = whoAmI.isLoading;

Expand All @@ -66,19 +57,15 @@ export const Fence: FunctionComponent = () => {
}, [whoAmI.data, credentials]);

useEffect(() => {
if (isWebauthAvailable === null) {
setIsWebauthAvailable(false);
Promise.all([
browserSupportsWebAuthn(),
platformAuthenticatorIsAvailable()
])
.then(statuses => statuses.reduce((prev, curr) => curr && prev, true))
.then((result) => {
setIsWebauthAvailable(result);
})
.catch(() => setIsWebauthAvailable(false));
if (!isWebauthAvailable) {
setShowRegistrationDialog(false);
} else if (useWebauthn === 'authenticate' && !isUserLogin) {
setShowRegistrationDialog(true); // Show the WebAuthn dialog
} else {
setShowRegistrationDialog(false);
}
}, [isWebauthAvailable]);
}, [isWebauthAvailable, isUserLogin, useWebauthn, setShowRegistrationDialog]);


useEffect(() => {
if (isAnyLoading) {
Expand All @@ -98,36 +85,6 @@ export const Fence: FunctionComponent = () => {
}
}, [isAnyLoading, hasError, isUserLogin, errorMessage]);

useEffect(() => {
if (isWebauthAvailable && !showRegistrationDialog) {
try {
if (isUserLogin && useWebauthn === 'register') {
setShowRegistrationDialog(true);
} else if (!isUserLogin && useWebauthn === 'authenticate') {
setShowRegistrationDialog(true);
} else {
setShowRegistrationDialog(false);
}
} catch (error) {
void message.error('WebAuthn authentication failed:');
}
}
}, [isUserLogin, isWebauthAvailable, useWebauthn]);

useEffect(() => {
if (isUserLogin && showRegistrationDialog && useWebauthn === 'register') {
setwindowComponent(
<WebAuthnRegistrationComponent />
);
} else if (!isUserLogin && showRegistrationDialog && useWebauthn === 'authenticate') {
setwindowComponent(
<WebAuthnAuthenticationComponent />
);
} else {
setwindowComponent(null);
}
}, [showRegistrationDialog, handleCancelRegistration, isUserLogin, useWebauthn, credentials, setCredentials]);

return (
<Routes>
<Route path='/reset/:encryptedEmail/:token' element={<ResetPasswordPage />} />
Expand All @@ -138,7 +95,7 @@ export const Fence: FunctionComponent = () => {
<Route path='*' element={
<>
{component}
{windowComponent}
{showRegistrationDialog && <WebAuthnAuthenticationComponent />}
{showNicknameModal && <DeviceNicknameModal />}
</>
} />
Expand Down
32 changes: 21 additions & 11 deletions packages/itmat-ui-react/src/components/profile/webauthn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ export const MyWebauthn: FunctionComponent = () => {
key: 'transports',
render: (transports: string[]) => transports.join(', ')
},
{
title: 'Origin',
dataIndex: 'origin',
key: 'origin',
render: (origin: string | undefined) => (origin ? origin : 'N/A') // Show N/A if origin is null or undefined
},
{
title: 'Set Name',
dataIndex: '',
Expand All @@ -174,33 +180,37 @@ export const MyWebauthn: FunctionComponent = () => {

if (isLoading) return <LoadSpinner />;
if (isError) return <Alert type="error" message={'Error fetching data.'} />;
if (devices.length === 0) return <p>No WebAuthn devices found for this user.</p>;

return (
<div className={css.group_wrapper}>
<List
header={
<div className={css['overview-header']} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div className={css['overview-header']} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div className={css['overview-icon']}></div>
<div>WebAuthn Devices</div>
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div className={css['overview-icon']}></div>
<div>WebAuthn Devices</div>
</div>
<div>
<Button type="primary" onClick={handleNavigateToRegister}>
Register New Device
Register New Device
</Button>
</div>
</div>
}
>
<List.Item>
<div className={css.shared_container}>
{/* Conditional rendering for devices */}
{devices.length > 0 ? (
<Table style={{width: '100%'}} dataSource={devices} columns={columns} rowKey={(record: AuthenticatorDevice) => record.credentialID} />
<Table
style={{ width: '100%' }}
dataSource={devices}
columns={columns}
rowKey={(record: AuthenticatorDevice) => record.credentialID}
/>
) : (
<p>No WebAuthn devices found for this user.</p>
<div style={{ width: '100%' }}>
<p style={{ textAlign: 'left' }}>No WebAuthn devices found for this user.</p>
</div>
)}
</div>
</List.Item>
Expand All @@ -216,4 +226,4 @@ export const MyWebauthn: FunctionComponent = () => {
</Modal>
</div>
);
};
};
Loading

0 comments on commit 60c3f45

Please sign in to comment.