Skip to content

Commit f5e7e10

Browse files
authored
Merge pull request #427 from objectstack-ai/copilot/implement-authentication-interface
2 parents 1136abe + cad19b6 commit f5e7e10

8 files changed

Lines changed: 621 additions & 6 deletions

File tree

CONDITIONAL_AUTH.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
# Conditional Authentication
2+
3+
ObjectUI now supports conditional authentication based on server discovery information. This allows the console application to automatically detect whether the backend has authentication enabled and adapt accordingly.
4+
5+
## Overview
6+
7+
When the ObjectStack server discovery endpoint (`/.well-known/objectstack` or `/api`) indicates that authentication is disabled (`auth.enabled === false`), the console application will bypass the authentication flow and operate in "Guest Mode".
8+
9+
This is useful for:
10+
- **Development environments** where authentication is not configured
11+
- **Demo environments** where users should access the system without credentials
12+
- **Embedded scenarios** where authentication is handled externally
13+
14+
## Implementation
15+
16+
### 1. Discovery Response Structure
17+
18+
The server's discovery endpoint should return a response with the following structure:
19+
20+
```json
21+
{
22+
"name": "ObjectStack API",
23+
"version": "2.0.0",
24+
"services": {
25+
"auth": {
26+
"enabled": false,
27+
"status": "unavailable",
28+
"message": "Authentication service not installed. Install an auth plugin to enable authentication."
29+
},
30+
"data": {
31+
"enabled": true,
32+
"status": "available"
33+
},
34+
"metadata": {
35+
"enabled": true,
36+
"status": "available"
37+
}
38+
}
39+
}
40+
```
41+
42+
### 2. Client-Side Detection
43+
44+
The console application uses the `ConditionalAuthWrapper` component which:
45+
46+
1. Connects to the server and fetches discovery information
47+
2. Checks the `services.auth.enabled` flag
48+
3. If `false`, wraps the application with `<AuthProvider enabled={false}>` (Guest Mode)
49+
4. If `true` or undefined, wraps with normal `<AuthProvider>` (Standard Auth Mode)
50+
51+
### 3. Guest Mode Behavior
52+
53+
When authentication is disabled:
54+
55+
- A virtual "Guest User" is automatically authenticated with:
56+
- ID: `guest`
57+
- Name: `Guest User`
58+
- Email: `guest@local`
59+
- Token: `guest-token`
60+
61+
- All protected routes become accessible without login
62+
- Login/Register pages are still accessible but not required
63+
- The user menu shows the guest user
64+
65+
## Usage
66+
67+
### For Application Developers
68+
69+
Simply wrap your application with `ConditionalAuthWrapper`:
70+
71+
```tsx
72+
import { ConditionalAuthWrapper } from './components/ConditionalAuthWrapper';
73+
74+
function App() {
75+
return (
76+
<ConditionalAuthWrapper authUrl="/api/auth">
77+
<BrowserRouter>
78+
<Routes>
79+
{/* Your routes */}
80+
</Routes>
81+
</BrowserRouter>
82+
</ConditionalAuthWrapper>
83+
);
84+
}
85+
```
86+
87+
### For Backend Developers
88+
89+
Ensure your discovery endpoint returns the correct `services.auth.enabled` flag:
90+
91+
**With Auth Enabled:**
92+
```typescript
93+
{
94+
services: {
95+
auth: {
96+
enabled: true,
97+
status: "available"
98+
}
99+
}
100+
}
101+
```
102+
103+
**With Auth Disabled:**
104+
```typescript
105+
{
106+
services: {
107+
auth: {
108+
enabled: false,
109+
status: "unavailable",
110+
message: "Auth service not configured"
111+
}
112+
}
113+
}
114+
```
115+
116+
### Using the useDiscovery Hook
117+
118+
Components can also directly access discovery information:
119+
120+
```tsx
121+
import { useDiscovery } from '@object-ui/react';
122+
123+
function MyComponent() {
124+
const { discovery, isLoading, isAuthEnabled } = useDiscovery();
125+
126+
if (isLoading) return <LoadingSpinner />;
127+
128+
return (
129+
<div>
130+
{isAuthEnabled
131+
? <LoginPrompt />
132+
: <GuestWelcome />
133+
}
134+
</div>
135+
);
136+
}
137+
```
138+
139+
## Security Considerations
140+
141+
⚠️ **Important Security Note:**
142+
143+
- The default behavior (when discovery fails or auth status is unknown) is to **enable authentication** for security.
144+
- Guest mode should only be used in controlled environments (development, demos).
145+
- In production, always configure proper authentication.
146+
147+
## API Reference
148+
149+
### ConditionalAuthWrapper
150+
151+
**Props:**
152+
- `authUrl` (string): The authentication endpoint URL
153+
- `children` (ReactNode): Application content to wrap
154+
155+
**Behavior:**
156+
- Fetches discovery on mount
157+
- Shows loading screen while checking auth status
158+
- Wraps children with appropriate AuthProvider configuration
159+
160+
### AuthProvider
161+
162+
**New Props:**
163+
- `enabled` (boolean, default: `true`): Whether authentication is enabled
164+
- When `false`: Automatically authenticates as guest user
165+
- When `true`: Normal authentication flow
166+
167+
### useDiscovery Hook
168+
169+
**Returns:**
170+
- `discovery` (DiscoveryInfo | null): Raw discovery data from server
171+
- `isLoading` (boolean): Whether discovery is being fetched
172+
- `error` (Error | null): Any error that occurred during fetch
173+
- `isAuthEnabled` (boolean): Convenience flag for `discovery?.services?.auth?.enabled ?? true`
174+
175+
## Examples
176+
177+
### Example 1: Development Server Without Auth
178+
179+
**Discovery Response:**
180+
```json
181+
{
182+
"name": "Dev Server",
183+
"services": {
184+
"auth": { "enabled": false }
185+
}
186+
}
187+
```
188+
189+
**Result:** Console loads without requiring login
190+
191+
### Example 2: Production Server With Auth
192+
193+
**Discovery Response:**
194+
```json
195+
{
196+
"name": "Production Server",
197+
"services": {
198+
"auth": { "enabled": true }
199+
}
200+
}
201+
```
202+
203+
**Result:** Console requires user authentication
204+
205+
### Example 3: Legacy Server (No Discovery)
206+
207+
**Discovery Response:** 404 or connection error
208+
209+
**Result:** Defaults to authentication enabled (secure by default)
210+
211+
## Testing
212+
213+
Tests are included in:
214+
- `packages/auth/src/__tests__/AuthProvider.disabled.test.tsx`
215+
- Tests verify guest mode behavior
216+
- Tests verify normal auth mode behavior
217+
- Tests verify session token creation in guest mode
218+
219+
Run tests with:
220+
```bash
221+
pnpm test packages/auth/src/__tests__/AuthProvider.disabled.test.tsx
222+
```

apps/console/src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { SchemaRendererProvider } from '@object-ui/react';
66
import { ObjectStackAdapter } from './dataSource';
77
import type { ConnectionState } from './dataSource';
88
import appConfig from '../objectstack.shared';
9-
import { AuthProvider, AuthGuard, useAuth } from '@object-ui/auth';
9+
import { AuthGuard, useAuth } from '@object-ui/auth';
1010

1111
// Components
1212
import { ConsoleLayout } from './components/ConsoleLayout';
@@ -19,6 +19,7 @@ import { DashboardView } from './components/DashboardView';
1919
import { PageView } from './components/PageView';
2020
import { ReportView } from './components/ReportView';
2121
import { ExpressionProvider } from './context/ExpressionProvider';
22+
import { ConditionalAuthWrapper } from './components/ConditionalAuthWrapper';
2223

2324
// Auth Pages
2425
import { LoginPage } from './pages/LoginPage';
@@ -291,7 +292,7 @@ function RootRedirect() {
291292
export function App() {
292293
return (
293294
<ThemeProvider defaultTheme="system" storageKey="object-ui-theme">
294-
<AuthProvider authUrl="/api/auth">
295+
<ConditionalAuthWrapper authUrl="/api/auth">
295296
<BrowserRouter basename="/">
296297
<Routes>
297298
<Route path="/login" element={<LoginPage />} />
@@ -305,7 +306,7 @@ export function App() {
305306
<Route path="/" element={<RootRedirect />} />
306307
</Routes>
307308
</BrowserRouter>
308-
</AuthProvider>
309+
</ConditionalAuthWrapper>
309310
</ThemeProvider>
310311
);
311312
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* ObjectUI Console - Conditional Auth Wrapper
3+
*
4+
* This component fetches discovery information from the server and conditionally
5+
* enables/disables authentication based on the server's auth service status.
6+
*/
7+
8+
import { useState, useEffect, ReactNode } from 'react';
9+
import { ObjectStackAdapter } from './dataSource';
10+
import { AuthProvider } from '@object-ui/auth';
11+
import { LoadingScreen } from './components/LoadingScreen';
12+
import type { DiscoveryInfo } from '@object-ui/react';
13+
14+
interface ConditionalAuthWrapperProps {
15+
children: ReactNode;
16+
authUrl: string;
17+
}
18+
19+
/**
20+
* Wrapper component that conditionally enables authentication based on server discovery.
21+
*
22+
* This component:
23+
* 1. Creates a temporary data source connection
24+
* 2. Fetches discovery information from the server
25+
* 3. Checks if auth.enabled is true in the discovery response
26+
* 4. Conditionally wraps children with AuthProvider if auth is enabled
27+
* 5. Bypasses auth if discovery indicates auth is disabled (development/demo mode)
28+
*/
29+
export function ConditionalAuthWrapper({ children, authUrl }: ConditionalAuthWrapperProps) {
30+
const [authEnabled, setAuthEnabled] = useState<boolean | null>(null);
31+
const [isLoading, setIsLoading] = useState(true);
32+
33+
useEffect(() => {
34+
let cancelled = false;
35+
36+
async function checkAuthStatus() {
37+
try {
38+
// Create a temporary adapter to fetch discovery
39+
// Empty baseUrl allows the adapter to use browser-relative paths
40+
// This works because the console app is served from the same origin as the API
41+
const adapter = new ObjectStackAdapter({
42+
baseUrl: '',
43+
autoReconnect: false,
44+
});
45+
46+
await adapter.connect();
47+
const discovery = await adapter.getDiscovery() as DiscoveryInfo | null;
48+
49+
if (!cancelled) {
50+
// Check if auth is enabled in discovery
51+
// Default to true if discovery doesn't provide this information
52+
const isAuthEnabled = discovery?.services?.auth?.enabled ?? true;
53+
setAuthEnabled(isAuthEnabled);
54+
}
55+
} catch (error) {
56+
if (!cancelled) {
57+
// If discovery fails, default to auth enabled for security
58+
console.warn('[ConditionalAuthWrapper] Failed to fetch discovery, defaulting to auth enabled:', error);
59+
setAuthEnabled(true);
60+
}
61+
} finally {
62+
if (!cancelled) {
63+
setIsLoading(false);
64+
}
65+
}
66+
}
67+
68+
checkAuthStatus();
69+
70+
return () => {
71+
cancelled = true;
72+
};
73+
}, []);
74+
75+
if (isLoading) {
76+
return <LoadingScreen />;
77+
}
78+
79+
// If auth is enabled, wrap with AuthProvider
80+
if (authEnabled) {
81+
return (
82+
<AuthProvider authUrl={authUrl}>
83+
{children}
84+
</AuthProvider>
85+
);
86+
}
87+
88+
// If auth is disabled, wrap with a disabled AuthProvider (guest mode)
89+
return (
90+
<AuthProvider authUrl={authUrl} enabled={false}>
91+
{children}
92+
</AuthProvider>
93+
);
94+
}

0 commit comments

Comments
 (0)