From a2e73946fa5ef42d920360ba03db87492bbdf326 Mon Sep 17 00:00:00 2001 From: Bill Tallitsch Date: Thu, 12 Sep 2024 10:22:54 -0400 Subject: [PATCH] DAPP1-35-auth0-webauthn-integration --- src/App.tsx | 62 +++++++++++++---------- src/api/webauthn.ts | 41 +++++++++++++++ src/components/AuthButton.tsx | 22 ++++++++ src/context/AuthContext.tsx | 94 +++++++++++++++++++++++++++++++++++ src/types/webauthn.d.ts | 74 +++++++++++++++++++++++++++ tsconfig.json | 1 + 6 files changed, 267 insertions(+), 27 deletions(-) create mode 100644 src/api/webauthn.ts create mode 100644 src/components/AuthButton.tsx create mode 100644 src/context/AuthContext.tsx create mode 100644 src/types/webauthn.d.ts diff --git a/src/App.tsx b/src/App.tsx index c9562c9..3ad3fc4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { useAuth0 } from '@auth0/auth0-react'; +import { Auth0Provider, useAuth0 } from '@auth0/auth0-react'; import { Header, Sidebar, SearchBar, PropertyListCard } from '@3um-group/atomic-sdk'; +import AuthButton from './components/AuthButton'; // import { useThemeContext } from './context/ThemeContext'; const App: React.FC = () => { @@ -214,35 +215,42 @@ const App: React.FC = () => { // console.log("yey",useAuth().loginWithRedirect()) return ( -
-
-
-
- { console.log(event); }} - onSearch={() => {}} - placeholder="Enter to search" - value="" - /> + +
+
+
+
+ { console.log(event); }} + onSearch={() => {}} + placeholder="Enter to search" + value="" + /> + +
+
+ +
-
- +
+
-
- -
-
+
); }; diff --git a/src/api/webauthn.ts b/src/api/webauthn.ts new file mode 100644 index 0000000..ee9ecaf --- /dev/null +++ b/src/api/webauthn.ts @@ -0,0 +1,41 @@ +import { PublicKeyCredentialCreationOptions, PublicKeyCredential } from 'src/types/webauthn'; + +export const startRegistration = async (): Promise => { + const response = await fetch('/api/webauthn/register/challenge', { + method: 'POST', + credentials: 'include' + }); + return await response.json(); +}; + +export const completeRegistration = async (credential: PublicKeyCredential) => { + const response = await fetch('/api/webauthn/register', { + method: 'POST', + body: JSON.stringify(credential), + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include' + }); + return await response.json(); +}; + +export const startAuthentication = async (): Promise => { + const response = await fetch('/api/webauthn/authenticate/challenge', { + method: 'POST', + credentials: 'include' + }); + return await response.json(); +}; + +export const completeAuthentication = async (assertion: PublicKeyCredential) => { + const response = await fetch('/api/webauthn/authenticate', { + method: 'POST', + body: JSON.stringify(assertion), + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include' + }); + return await response.json(); +}; diff --git a/src/components/AuthButton.tsx b/src/components/AuthButton.tsx new file mode 100644 index 0000000..8ca2799 --- /dev/null +++ b/src/components/AuthButton.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { useAuth0 } from '@auth0/auth0-react'; + +const AuthButton: React.FC = () => { + const { loginWithRedirect, logout, isAuthenticated } = useAuth0(); + + return ( +
+ {isAuthenticated ? ( + + ) : ( + + )} +
+ ); +}; + +export default AuthButton; diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 0000000..e9ea40b --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,94 @@ +import React, { createContext, useState, useContext, ReactNode } from 'react'; +import { completeAuthentication, completeRegistration, startAuthentication, startRegistration } from 'src/api/webauthn'; +import { PublicKeyCredentialCreationOptions, PublicKeyCredentialRequestOptions, PublicKeyCredential } from 'src/types/webauthn'; + +interface AuthContextType { + isAuthenticated: boolean; + authenticate: () => Promise; + register: () => Promise; +} + +interface AuthProviderProps { + children: ReactNode; +} + +const AuthContext = createContext(undefined); + +// Utility to convert base64 to Uint8Array +const base64ToUint8Array = (base64String: string): Uint8Array => { + const decodedString = atob(base64String); + const bytes = new Uint8Array(decodedString.length); + for (let i = 0; i < decodedString.length; i++) { + bytes[i] = decodedString.charCodeAt(i); + } + return bytes; +}; + +export const AuthProvider: React.FC = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + + // Register function + const register = async () => { + try { + const registrationOptions: PublicKeyCredentialCreationOptions = await startRegistration(); + + if (!navigator.credentials) { + throw new Error('WebAuthn is not supported in this browser.'); + } + + const credential = await navigator.credentials.create({ publicKey: registrationOptions }); + + if (credential) { + await completeRegistration(credential as PublicKeyCredential); + } else { + throw new Error('Registration failed. Please try again.'); + } + } catch (error) { + console.error('Error during registration:', error); + alert('Registration failed. Please try again.'); + } + }; + + // Updated authenticate function + const authenticate = async () => { + try { + // Start authentication process + const authenticationOptions: PublicKeyCredentialRequestOptions = await startAuthentication(); + + // Convert challenge from base64 to Uint8Array + authenticationOptions.challenge = base64ToUint8Array(authenticationOptions.challenge as unknown as string); + + if (!navigator.credentials) { + throw new Error('WebAuthn is not supported in this browser.'); + } + + // Start WebAuthn authentication + const assertion = await navigator.credentials.get({ publicKey: authenticationOptions }); + + if (assertion) { + const response = await completeAuthentication(assertion as PublicKeyCredential); + setIsAuthenticated(response.success); + } else { + throw new Error('Authentication failed. Please try again.'); + } + + } catch (error) { + console.error('Error during authentication:', error); + alert('Authentication failed. Please try again.'); + } + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; diff --git a/src/types/webauthn.d.ts b/src/types/webauthn.d.ts new file mode 100644 index 0000000..fe7312d --- /dev/null +++ b/src/types/webauthn.d.ts @@ -0,0 +1,74 @@ +// src/types/webauthn.d.ts + +// PublicKeyCredentialCreationOptions for registration +interface PublicKeyCredentialCreationOptions { + challenge: Uint8Array | ArrayBuffer; + rp: { + name: string; + id?: string; + }; + user: { + id: Uint8Array; + name: string; + displayName: string; + }; + pubKeyCredParams: Array<{ + type: 'public-key'; + alg: number; + }>; + authenticatorSelection?: { + authenticatorAttachment?: 'platform' | 'cross-platform'; + residentKey?: 'required' | 'preferred' | 'discouraged'; + userVerification?: 'required' | 'preferred' | 'discouraged'; + }; + attestation?: 'none' | 'indirect' | 'direct' | 'enterprise'; + timeout?: number; + excludeCredentials?: Array<{ + id: Uint8Array; + type: 'public-key'; + }>; + } + + // PublicKeyCredentialRequestOptions for authentication + interface PublicKeyCredentialRequestOptions { + challenge: Uint8Array | ArrayBuffer; + timeout?: number; + rpId?: string; + allowCredentials?: Array<{ + id: Uint8Array; + type: 'public-key'; + transports?: Array<'usb' | 'nfc' | 'ble' | 'internal'>; + }>; + userVerification?: 'required' | 'preferred' | 'discouraged'; + } + + // Extend the Credential interface to include public-key credentials + interface PublicKeyCredential extends Credential { + rawId: ArrayBuffer; + response: AuthenticatorAttestationResponse | AuthenticatorAssertionResponse; + clientExtensionResults: () => AuthenticationExtensionsClientOutputs; + } + + // AuthenticatorAttestationResponse used during registration + interface AuthenticatorAttestationResponse { + clientDataJSON: ArrayBuffer; + attestationObject: ArrayBuffer; + } + + // AuthenticatorAssertionResponse used during authentication + interface AuthenticatorAssertionResponse { + clientDataJSON: ArrayBuffer; + authenticatorData: ArrayBuffer; + signature: ArrayBuffer; + userHandle?: ArrayBuffer; + } + + // Exported types for use in other parts of the application + export { + PublicKeyCredentialCreationOptions, + PublicKeyCredentialRequestOptions, + PublicKeyCredential, + AuthenticatorAttestationResponse, + AuthenticatorAssertionResponse, + }; + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 744f1d8..a9a9bb1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ "outDir": "dist", "sourceMap": true, "rootDir": "src", + "typeRoots": ["./node_modules/@types", "./src/types"], "noImplicitReturns": true, "noImplicitThis": true, "noImplicitAny": true,