Skip to content

Commit

Permalink
DAPP1-35-auth0-webauthn-integration (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
BillTallitsch authored Sep 13, 2024
2 parents cf2bfcf + 1de0b56 commit 02a4b99
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 32 deletions.
70 changes: 38 additions & 32 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 { Routes, Route } from 'react-router-dom';
import Wallet from './pages/Wallet';
// import { useThemeContext } from './context/ThemeContext';
Expand Down Expand Up @@ -216,40 +217,45 @@ const App: React.FC = () => {
// console.log("yey",useAuth().loginWithRedirect())

return (
<div className='h-screen'>
<Header
logoProps={{
alt: 'Company Logo',
customLightSrc:'/assets/3UM-dark-logo.png',
customDarkSrc:'/assets/3UM-white-logo.png',
height: 50,
width: 50,
}}
useAuth={useAuth}
showNavItems
/>
<div className="flex flex-row">
<div className="basis-3/4">
<SearchBar data-test-id="search-bar"
onChange={(event) => { console.log(event); }}
onSearch={() => {}}
placeholder="Enter to search"
value=""
/>
<Auth0Provider
domain={process.env.REACT_APP_AUTH0_DOMAIN!}
clientId={process.env.REACT_APP_AUTH0_CLIENT_ID!}
// redirectUri={window.location.origin}
>
<div className='h-screen'>
<Header
logoProps={{
alt: 'Company Logo',
customLightSrc:'/assets/3UM-dark-logo.png',
customDarkSrc:'/assets/3UM-white-logo.png',
height: 50,
width: 50,
}}
useAuth={useAuth}
showNavItems
/>
<div className="flex flex-row">
<div className="basis-3/4">
<SearchBar data-test-id="search-bar"
onChange={(event) => { console.log(event); }}
onSearch={() => {}}
placeholder="Enter to search"
value=""
/>
<AuthButton />
</div>
<div className="basis-1/4">
<Sidebar children={sidebarItems()} />
</div>
</div>
<div className="basis-1/4">
<Sidebar children={sidebarItems()} />
<div className="viewContainer">
<Routes>
<Route path="/" element={<PropertyList />} />
<Route path="/wallet" element={<Wallet />} />
</Routes>
</div>
</div>
<div className="viewContainer">

<Routes>
<Route path="/" element={<PropertyList />} />
<Route path="/wallet" element={<Wallet />} />
{/* Add more routes as needed */}
</Routes>
</div>
</div>
</Auth0Provider>
);
};

Expand Down
41 changes: 41 additions & 0 deletions src/api/webauthn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { PublicKeyCredentialCreationOptions, PublicKeyCredential } from 'src/types/webauthn';

export const startRegistration = async (): Promise<PublicKeyCredentialCreationOptions> => {
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<PublicKeyCredentialCreationOptions> => {
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();
};
22 changes: 22 additions & 0 deletions src/components/AuthButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import { useAuth0 } from '@auth0/auth0-react';

const AuthButton: React.FC = () => {
const { loginWithRedirect, logout, isAuthenticated } = useAuth0();

return (
<div>
{isAuthenticated ? (
<button onClick={() => logout()}>
Logout
</button>
) : (
<button onClick={() => loginWithRedirect()}>
Login with WebAuthn (Auth0)
</button>
)}
</div>
);
};

export default AuthButton;
94 changes: 94 additions & 0 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -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<void>;
register: () => Promise<void>;
}

interface AuthProviderProps {
children: ReactNode;
}

const AuthContext = createContext<AuthContextType | undefined>(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<AuthProviderProps> = ({ 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 (
<AuthContext.Provider value={{ isAuthenticated, authenticate, register }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
74 changes: 74 additions & 0 deletions src/types/webauthn.d.ts
Original file line number Diff line number Diff line change
@@ -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,
};

1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"outDir": "dist",
"sourceMap": true,
"rootDir": "src",
"typeRoots": ["./node_modules/@types", "./src/types"],
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
Expand Down

0 comments on commit 02a4b99

Please sign in to comment.