Skip to content

Commit 92f643e

Browse files
committed
Added authentication
1 parent d95a68d commit 92f643e

File tree

65 files changed

+783
-112
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+783
-112
lines changed
Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,143 @@
11
# Authentication
22

3-
This page is under construction.
3+
SmooSense supports optional authentication using Auth0. When configured, all pages require users to log in before accessing the application.
4+
5+
## Overview
6+
7+
Authentication is **optional** by default. If no Auth0 credentials are configured, SmooSense runs without authentication and all pages are publicly accessible. This is suitable for local development or private deployments.
8+
9+
When Auth0 is configured, users must authenticate before accessing any page. Unauthenticated users are redirected to Auth0's login page.
10+
11+
## Setting Up Auth0
12+
13+
### 1. Create an Auth0 Application
14+
15+
1. Go to [Auth0 Dashboard](https://manage.auth0.com/)
16+
2. Navigate to **Applications > Applications**
17+
3. Click **Create Application**
18+
4. Choose **Regular Web Application**
19+
5. Note your **Domain**, **Client ID**, and **Client Secret**
20+
21+
### 2. Configure Callback URLs
22+
23+
In your Auth0 application settings, configure:
24+
25+
- **Allowed Callback URLs**: `http://localhost:8000/auth/callback`
26+
- **Allowed Logout URLs**: `http://localhost:8000`
27+
28+
For production, replace `localhost:8000` with your actual domain.
29+
30+
### 3. Set Environment Variables
31+
32+
Set the following environment variables before starting SmooSense:
33+
34+
```bash
35+
export AUTH0_DOMAIN="your-tenant.auth0.com"
36+
export AUTH0_CLIENT_ID="your-client-id"
37+
export AUTH0_CLIENT_SECRET="your-client-secret"
38+
export APP_SECRET_KEY="your-random-secret-key" # Optional, auto-generated if not set
39+
```
40+
41+
You can also create a `.env` file in your project directory with these values.
42+
43+
### 4. Start SmooSense
44+
45+
```bash
46+
sense
47+
```
48+
49+
When Auth0 is properly configured, you'll see a log message:
50+
```
51+
Auth0 authentication enabled
52+
```
53+
54+
## Authentication Flow
55+
56+
1. User visits any protected page (e.g., `/`, `/FolderBrowser`, `/Table`)
57+
2. If not authenticated, user is redirected to `/auth/login`
58+
3. Auth0 handles the login (username/password, SSO, etc.)
59+
4. After successful login, user is redirected back to the application
60+
5. User session is stored and maintained until logout
61+
62+
## API Endpoints
63+
64+
SmooSense provides the following authentication endpoints:
65+
66+
| Endpoint | Description |
67+
|----------|-------------|
68+
| `/auth/login` | Initiates Auth0 login flow |
69+
| `/auth/logout` | Clears session and logs out from Auth0 |
70+
| `/auth/callback` | Handles OAuth callback from Auth0 |
71+
| `/auth/me` | Returns current user info as JSON |
72+
73+
### Check Authentication Status
74+
75+
```bash
76+
curl http://localhost:8000/auth/me
77+
```
78+
79+
Returns:
80+
```json
81+
{
82+
"authenticated": true,
83+
"email": "[email protected]",
84+
"name": "John Doe",
85+
"picture": "https://..."
86+
}
87+
```
88+
89+
Or if not authenticated:
90+
```json
91+
{
92+
"authenticated": false
93+
}
94+
```
95+
96+
## Restricting Access by Email Domain
97+
98+
You can restrict access to users from specific email domains using Auth0 Actions. In your Auth0 Dashboard:
99+
100+
1. Go to **Actions > Flows > Login**
101+
2. Create a new Action with this code:
102+
103+
```javascript
104+
exports.onExecutePostLogin = async (event, api) => {
105+
if (!event.user.email) {
106+
api.access.deny('access_denied', 'Email required');
107+
return;
108+
}
109+
110+
const allowList = ['mycorp.com', 'partner.com']; // Your allowed domains
111+
112+
const parts = event.user.email.split('@');
113+
const domain = parts[parts.length - 1].toLowerCase();
114+
115+
if (!allowList.includes(domain)) {
116+
api.access.deny(
117+
'access_denied',
118+
'Only specific email domains are allowed to access this app.'
119+
);
120+
}
121+
};
122+
```
123+
124+
3. Deploy the Action and add it to your Login flow
125+
126+
## Troubleshooting
127+
128+
### "Auth0 not configured, running without authentication"
129+
130+
This message appears when environment variables are not set. Verify:
131+
- `AUTH0_DOMAIN` is set
132+
- `AUTH0_CLIENT_ID` is set
133+
- `AUTH0_CLIENT_SECRET` is set
134+
135+
### Callback URL Mismatch
136+
137+
Ensure your Auth0 application's "Allowed Callback URLs" exactly matches `http://your-host:port/auth/callback`.
138+
139+
### Session Not Persisting
140+
141+
If sessions aren't persisting across requests, ensure:
142+
1. `APP_SECRET_KEY` is set consistently (not auto-generated each restart)
143+
2. Cookies are enabled in the browser

smoosense-gui/src/components/settings/GlobalSettings.tsx

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ import FolderBrowserSection from './FolderBrowserSection'
99
import RowDetailSection from './RowDetailSection'
1010
import MediaSection from './MediaSection'
1111
import Logo from '@/components/common/Logo'
12+
import { useAuth, logout } from '@/lib/hooks/useAuth'
13+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
14+
import { Button } from '@/components/ui/button'
1215

1316
interface GlobalSettingsDropdownProps {
1417
context?: 'Table' | 'FolderBrowser' | 'LiteTable' | 'MiniTable'
1518
}
1619

1720
export default function GlobalSettingsDropdown({ context }: GlobalSettingsDropdownProps) {
21+
const { authenticated, user, loading } = useAuth()
22+
1823
const renderContextSpecificSection = () => {
1924
switch (context) {
2025
case 'Table':
@@ -47,14 +52,83 @@ export default function GlobalSettingsDropdown({ context }: GlobalSettingsDropdo
4752
}
4853
}
4954

55+
// Get user initials for avatar fallback
56+
const getInitials = (name: string | undefined, email: string | undefined) => {
57+
if (name) {
58+
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
59+
}
60+
if (email) {
61+
return email[0].toUpperCase()
62+
}
63+
return '?'
64+
}
65+
66+
// Render icon based on auth state
67+
const renderIcon = () => {
68+
if (loading || !authenticated || !user) {
69+
return <Settings />
70+
}
71+
return (
72+
<Avatar className="h-6 w-6">
73+
<AvatarImage
74+
src={user.picture || undefined}
75+
alt={user.name || user.email}
76+
referrerPolicy="no-referrer"
77+
/>
78+
<AvatarFallback className="text-xs">
79+
{getInitials(user.name, user.email)}
80+
</AvatarFallback>
81+
</Avatar>
82+
)
83+
}
84+
85+
// Render user info section
86+
const renderUserSection = () => {
87+
if (!authenticated || !user) {
88+
return null
89+
}
90+
return (
91+
<>
92+
<div className="flex items-center gap-3">
93+
<Avatar className="h-10 w-10">
94+
<AvatarImage
95+
src={user.picture || undefined}
96+
alt={user.name || user.email}
97+
referrerPolicy="no-referrer"
98+
/>
99+
<AvatarFallback>
100+
{getInitials(user.name, user.email)}
101+
</AvatarFallback>
102+
</Avatar>
103+
<div className="flex-1 min-w-0">
104+
{user.name && (
105+
<div className="text-sm font-medium truncate">{user.name}</div>
106+
)}
107+
<div className="text-xs text-muted-foreground truncate">{user.email}</div>
108+
</div>
109+
</div>
110+
<Button
111+
variant="outline"
112+
size="sm"
113+
onClick={logout}
114+
className="w-full mt-2"
115+
>
116+
Sign out
117+
</Button>
118+
<Separator className="my-4" />
119+
</>
120+
)
121+
}
122+
50123
return (
51124
<IconPopover
52-
icon={<Settings />}
53-
tooltip="Settings"
125+
icon={renderIcon()}
126+
tooltip={authenticated && user ? user.email : "Settings"}
54127
contentClassName="w-80 p-4"
55128
align="end"
56129
>
57130
<div className="space-y-4">
131+
{renderUserSection()}
58132
<CommonSettingSection />
59133

60134
{context && (
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useState, useEffect } from 'react'
2+
3+
export interface AuthUser {
4+
email: string
5+
name: string
6+
picture: string | null
7+
}
8+
9+
export interface AuthState {
10+
authenticated: boolean
11+
user: AuthUser | null
12+
loading: boolean
13+
error: string | null
14+
}
15+
16+
/**
17+
* Hook to get current authentication state.
18+
* Fetches user info from /auth/me endpoint.
19+
*/
20+
export function useAuth(): AuthState {
21+
const [state, setState] = useState<AuthState>({
22+
authenticated: false,
23+
user: null,
24+
loading: true,
25+
error: null,
26+
})
27+
28+
useEffect(() => {
29+
async function fetchAuthState() {
30+
try {
31+
const response = await fetch('/auth/me')
32+
if (!response.ok) {
33+
throw new Error('Failed to fetch auth state')
34+
}
35+
const data = await response.json()
36+
37+
if (data.authenticated) {
38+
setState({
39+
authenticated: true,
40+
user: {
41+
email: data.email,
42+
name: data.name,
43+
picture: data.picture || null,
44+
},
45+
loading: false,
46+
error: null,
47+
})
48+
} else {
49+
setState({
50+
authenticated: false,
51+
user: null,
52+
loading: false,
53+
error: null,
54+
})
55+
}
56+
} catch (error) {
57+
setState({
58+
authenticated: false,
59+
user: null,
60+
loading: false,
61+
error: error instanceof Error ? error.message : 'Unknown error',
62+
})
63+
}
64+
}
65+
66+
fetchAuthState()
67+
}, [])
68+
69+
return state
70+
}
71+
72+
/**
73+
* Redirect to login page
74+
*/
75+
export function login(): void {
76+
window.location.href = '/auth/login'
77+
}
78+
79+
/**
80+
* Redirect to logout
81+
*/
82+
export function logout(): void {
83+
window.location.href = '/auth/logout'
84+
}

smoosense-py/intests/base_integration_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,9 @@ def setUpClass(cls) -> None:
114114
cls.browser = cls.playwright.chromium.launch(headless=cls.headless)
115115

116116
# Ensure screenshots directory exists
117-
cls.screenshots_dir = Path(__file__).parent.parent.parent / "landing/public/images/screenshots"
117+
cls.screenshots_dir = (
118+
Path(__file__).parent.parent.parent / "landing/public/images/screenshots"
119+
)
118120
cls.screenshots_dir.mkdir(parents=True, exist_ok=True)
119121

120122
@classmethod

smoosense-py/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ classifiers = [
2424
"Topic :: Software Development :: Libraries :: Python Modules",
2525
]
2626
dependencies = [
27+
"authlib>=1.0",
2728
"boto3>=1.39.11",
2829
"click>=8.1.8",
2930
"duckdb>=1.2.1",
@@ -149,5 +150,5 @@ exclude = [
149150
]
150151

151152
[[tool.mypy.overrides]]
152-
module = ["boto3.*", "botocore.*", "pandas.*", "IPython.*", "daft.*", "lancedb.*", "pyarrow.*", "torch.*", "PIL.*", "transformers.*", "tqdm.*", "umap.*"]
153+
module = ["authlib.*", "boto3.*", "botocore.*", "pandas.*", "IPython.*", "daft.*", "lancedb.*", "pyarrow.*", "torch.*", "PIL.*", "transformers.*", "tqdm.*", "umap.*"]
153154
ignore_missing_imports = true

smoosense-py/smoosense/app.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from flask import Flask
99
from pydantic import ConfigDict, validate_call
1010

11+
from smoosense.handlers.auth import auth_bp, init_oauth
1112
from smoosense.handlers.fs import fs_bp
1213
from smoosense.handlers.lance import lance_bp
1314
from smoosense.handlers.pages import pages_bp
@@ -65,7 +66,16 @@ def create_app(self) -> Flask:
6566
app.config["DUCKDB_CONNECTION_MAKER"] = self.duckdb_connection_maker
6667
app.config["PASSOVER_CONFIG"] = self.passover_config
6768

69+
# Initialize Auth0 if configured
70+
oauth = init_oauth(app)
71+
if oauth is not None:
72+
app.config["OAUTH"] = oauth
73+
logger.info("Auth0 authentication enabled")
74+
else:
75+
logger.info("Auth0 not configured, running without authentication")
76+
6877
# Register blueprints
78+
app.register_blueprint(auth_bp, url_prefix="/auth")
6979
app.register_blueprint(query_bp, url_prefix="/api")
7080
app.register_blueprint(fs_bp, url_prefix="/api")
7181
app.register_blueprint(lance_bp, url_prefix="/api")

0 commit comments

Comments
 (0)