@vertesia/ui 0.81.0 → 0.81.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/esm/core/components/shadcn/dialog.js +7 -3
- package/lib/esm/core/components/shadcn/dialog.js.map +1 -1
- package/lib/esm/core/components/shadcn/popover.js +3 -1
- package/lib/esm/core/components/shadcn/popover.js.map +1 -1
- package/lib/esm/core/components/shadcn/selectBox.js +1 -1
- package/lib/esm/core/components/shadcn/selectBox.js.map +1 -1
- package/lib/esm/core/components/shadcn/tooltip.js +5 -1
- package/lib/esm/core/components/shadcn/tooltip.js.map +1 -1
- package/lib/esm/features/agent/PayloadBuilder.js +22 -0
- package/lib/esm/features/agent/PayloadBuilder.js.map +1 -1
- package/lib/esm/features/agent/chat/ModernAgentConversation.js +6 -62
- package/lib/esm/features/agent/chat/ModernAgentConversation.js.map +1 -1
- package/lib/esm/features/agent/chat/ModernAgentOutput/Header.js +1 -1
- package/lib/esm/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
- package/lib/esm/features/agent/chat/ModernAgentOutput/MessageInput.js +1 -1
- package/lib/esm/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
- package/lib/esm/features/facets/AgentRunnerFacetsNav.js +86 -0
- package/lib/esm/features/facets/AgentRunnerFacetsNav.js.map +1 -0
- package/lib/esm/features/facets/index.js +1 -0
- package/lib/esm/features/facets/index.js.map +1 -1
- package/lib/esm/features/store/collections/CreateCollection.js +1 -1
- package/lib/esm/features/store/collections/CreateCollection.js.map +1 -1
- package/lib/esm/features/store/collections/EditCollectionView.js +4 -6
- package/lib/esm/features/store/collections/EditCollectionView.js.map +1 -1
- package/lib/esm/features/store/objects/components/PropertiesEditorModal.js +25 -39
- package/lib/esm/features/store/objects/components/PropertiesEditorModal.js.map +1 -1
- package/lib/esm/features/store/objects/components/useContentPanelHooks.js +3 -3
- package/lib/esm/features/store/objects/components/useContentPanelHooks.js.map +1 -1
- package/lib/esm/features/store/types/ObjectSchemaEditor.js +4 -6
- package/lib/esm/features/store/types/ObjectSchemaEditor.js.map +1 -1
- package/lib/esm/features/store/types/TableLayoutEditor.js +4 -6
- package/lib/esm/features/store/types/TableLayoutEditor.js.map +1 -1
- package/lib/esm/widgets/index.js +1 -1
- package/lib/esm/widgets/index.js.map +1 -1
- package/lib/esm/widgets/json-view/JSONCode.js +16 -151
- package/lib/esm/widgets/json-view/JSONCode.js.map +1 -1
- package/lib/esm/widgets/{codemirror → monacoEditor}/MonacoEditor.js +18 -6
- package/lib/esm/widgets/monacoEditor/MonacoEditor.js.map +1 -0
- package/lib/esm/widgets/monacoEditor/index.js +2 -0
- package/lib/esm/widgets/monacoEditor/index.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/core/components/shadcn/dialog.d.ts.map +1 -1
- package/lib/types/core/components/shadcn/popover.d.ts.map +1 -1
- package/lib/types/core/components/shadcn/tooltip.d.ts.map +1 -1
- package/lib/types/features/agent/PayloadBuilder.d.ts +1 -0
- package/lib/types/features/agent/PayloadBuilder.d.ts.map +1 -1
- package/lib/types/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
- package/lib/types/features/facets/AgentRunnerFacetsNav.d.ts +14 -0
- package/lib/types/features/facets/AgentRunnerFacetsNav.d.ts.map +1 -0
- package/lib/types/features/facets/index.d.ts +1 -0
- package/lib/types/features/facets/index.d.ts.map +1 -1
- package/lib/types/features/store/collections/EditCollectionView.d.ts.map +1 -1
- package/lib/types/features/store/objects/components/PropertiesEditorModal.d.ts.map +1 -1
- package/lib/types/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -1
- package/lib/types/features/store/types/ObjectSchemaEditor.d.ts.map +1 -1
- package/lib/types/features/store/types/TableLayoutEditor.d.ts.map +1 -1
- package/lib/types/widgets/index.d.ts +1 -1
- package/lib/types/widgets/index.d.ts.map +1 -1
- package/lib/types/widgets/json-view/JSONCode.d.ts +2 -19
- package/lib/types/widgets/json-view/JSONCode.d.ts.map +1 -1
- package/lib/types/widgets/{codemirror → monacoEditor}/MonacoEditor.d.ts +6 -3
- package/lib/types/widgets/monacoEditor/MonacoEditor.d.ts.map +1 -0
- package/lib/types/widgets/monacoEditor/index.d.ts +3 -0
- package/lib/types/widgets/monacoEditor/index.d.ts.map +1 -0
- package/lib/vertesia-ui-core.js +1 -1
- package/lib/vertesia-ui-core.js.map +1 -1
- package/lib/vertesia-ui-features.js +1 -1
- package/lib/vertesia-ui-features.js.map +1 -1
- package/lib/vertesia-ui-widgets.js +1 -1
- package/lib/vertesia-ui-widgets.js.map +1 -1
- package/package.json +6 -10
- package/src/core/components/shadcn/calendar.tsx +2 -2
- package/src/core/components/shadcn/dialog.tsx +23 -19
- package/src/core/components/shadcn/filters/index.ts +1 -1
- package/src/core/components/shadcn/popover.tsx +3 -1
- package/src/core/components/shadcn/selectBox.tsx +2 -2
- package/src/core/components/shadcn/tooltip.tsx +20 -16
- package/src/features/agent/PayloadBuilder.tsx +28 -0
- package/src/features/agent/chat/ModernAgentConversation.tsx +6 -104
- package/src/features/agent/chat/ModernAgentOutput/Header.tsx +1 -1
- package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +1 -1
- package/src/features/facets/AgentRunnerFacetsNav.tsx +125 -0
- package/src/features/facets/index.ts +1 -0
- package/src/features/store/collections/CreateCollection.tsx +1 -1
- package/src/features/store/collections/EditCollectionView.tsx +10 -8
- package/src/features/store/objects/components/PropertiesEditorModal.tsx +36 -51
- package/src/features/store/objects/components/useContentPanelHooks.ts +3 -3
- package/src/features/store/types/ObjectSchemaEditor.tsx +9 -7
- package/src/features/store/types/TableLayoutEditor.tsx +9 -7
- package/src/session/auth/auth-flow.md +1094 -0
- package/src/widgets/index.ts +1 -1
- package/src/widgets/json-view/JSONCode.tsx +30 -172
- package/src/widgets/{codemirror → monacoEditor}/MonacoEditor.tsx +30 -10
- package/src/widgets/monacoEditor/index.ts +3 -0
- package/lib/esm/widgets/codemirror/CodeMirrorEditor.js +0 -103
- package/lib/esm/widgets/codemirror/CodeMirrorEditor.js.map +0 -1
- package/lib/esm/widgets/codemirror/CodemirrorStateSingleton.js +0 -33
- package/lib/esm/widgets/codemirror/CodemirrorStateSingleton.js.map +0 -1
- package/lib/esm/widgets/codemirror/MonacoEditor.js.map +0 -1
- package/lib/esm/widgets/codemirror/index.js +0 -3
- package/lib/esm/widgets/codemirror/index.js.map +0 -1
- package/lib/types/widgets/codemirror/CodeMirrorEditor.d.ts +0 -24
- package/lib/types/widgets/codemirror/CodeMirrorEditor.d.ts.map +0 -1
- package/lib/types/widgets/codemirror/CodemirrorStateSingleton.d.ts +0 -15
- package/lib/types/widgets/codemirror/CodemirrorStateSingleton.d.ts.map +0 -1
- package/lib/types/widgets/codemirror/MonacoEditor.d.ts.map +0 -1
- package/lib/types/widgets/codemirror/index.d.ts +0 -5
- package/lib/types/widgets/codemirror/index.d.ts.map +0 -1
- package/src/widgets/codemirror/CodeMirrorEditor.tsx +0 -122
- package/src/widgets/codemirror/CodemirrorStateSingleton.tsx +0 -39
- package/src/widgets/codemirror/index.ts +0 -6
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
# User Authentication Flow
|
|
2
|
+
|
|
3
|
+
This document describes the authentication flow for the Vertesia user-facing applications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The authentication system uses:
|
|
8
|
+
- **Firebase** for identity management and OAuth provider integration
|
|
9
|
+
- **Central Auth** (internal-auth.vertesia.app) for centralized authentication flow
|
|
10
|
+
- **STS (Security Token Service)** (sts.vertesia.io) to validate Firebase tokens and generate Vertesia JWT tokens
|
|
11
|
+
- **Session state management** to validate redirects and prevent CSRF attacks
|
|
12
|
+
|
|
13
|
+
### STS Endpoints
|
|
14
|
+
- Production/Preview: `https://sts.vertesia.io`
|
|
15
|
+
- Staging: `https://sts-staging.vertesia.io`
|
|
16
|
+
|
|
17
|
+
**Important**: STS is the single source of truth for generating Vertesia JWT tokens. Both authentication paths (Central Auth and Direct Firebase) ultimately call STS to get the JWT.
|
|
18
|
+
|
|
19
|
+
## Key Components
|
|
20
|
+
|
|
21
|
+
- **UserSession** (UserSession.ts): Core session class managing auth state, client configuration, and user data
|
|
22
|
+
- **UserSessionProvider** (UserSessionProvider.tsx): React context provider orchestrating the auth flow
|
|
23
|
+
- **SigninScreen** (SigninScreen.tsx): UI component showing login options and handling signup
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Complete Authentication Flow
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
31
|
+
│ APP LOADS (NO AUTHENTICATION) │
|
|
32
|
+
└────────────────────────────────────────────┬────────────────────────────────┘
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
┌─────────────────────────────┐
|
|
36
|
+
│ UserSessionProvider mounts │
|
|
37
|
+
│ │
|
|
38
|
+
│ 1. Parse URL hash: │
|
|
39
|
+
│ token=..., state=... │
|
|
40
|
+
│ 2. Get account/project from │
|
|
41
|
+
│ URL (?a=...&p=...) or │
|
|
42
|
+
│ localStorage │
|
|
43
|
+
│ 3. Clear URL hash │
|
|
44
|
+
│ 4. Setup idempotency guard │
|
|
45
|
+
│ 5. Create new UserSession │
|
|
46
|
+
│ (authToken = undefined) │
|
|
47
|
+
└──────────┬──────────────────┘
|
|
48
|
+
│
|
|
49
|
+
▼
|
|
50
|
+
┌─────────────────────────────┐
|
|
51
|
+
│ Has token & state in URL? │
|
|
52
|
+
└─────┬───────────────┬───────┘
|
|
53
|
+
YES NO
|
|
54
|
+
│ │
|
|
55
|
+
┌───────────────────────────┘ └───────────────────────────┐
|
|
56
|
+
▼ ▼
|
|
57
|
+
┌────────────────────┐ ┌────────────────────┐
|
|
58
|
+
│ ═══════════════════│ │ ═══════════════════│
|
|
59
|
+
│ PATH A: TOKEN FLOW │ │ PATH B: NO TOKEN │
|
|
60
|
+
│ ═══════════════════│ │ ═══════════════════│
|
|
61
|
+
│ │ │ │
|
|
62
|
+
│ (Returning from │ │ (First load or │
|
|
63
|
+
│ central auth) │ │ no URL token) │
|
|
64
|
+
└────────┬───────────┘ └────────┬───────────┘
|
|
65
|
+
│ │
|
|
66
|
+
▼ ▼
|
|
67
|
+
┌────────────────────┐ ┌────────────────────┐
|
|
68
|
+
│ verifyState() │ │ Setup Firebase │
|
|
69
|
+
└────┬───────────┬───┘ │ onAuthStateChanged │
|
|
70
|
+
YES NO │ listener │
|
|
71
|
+
│ │ └────────┬───────────┘
|
|
72
|
+
│ ▼ │
|
|
73
|
+
│ ┌────────────────┐ │
|
|
74
|
+
│ │ Invalid state! │ │
|
|
75
|
+
│ │ Log error │ │
|
|
76
|
+
│ │ Show │ │
|
|
77
|
+
│ │ SigninScreen │ │
|
|
78
|
+
│ └────────────────┘ │
|
|
79
|
+
│ │
|
|
80
|
+
▼ ▼
|
|
81
|
+
┌────────────────────┐ ┌────────────────────────┐
|
|
82
|
+
│ clearState() │ │ Show SigninScreen │
|
|
83
|
+
└────────┬───────────┘ │ │
|
|
84
|
+
│ │ shouldRedirectTo │
|
|
85
|
+
▼ │ CentralAuth()? │
|
|
86
|
+
┌────────────────────┐ └──────┬─────────────────┘
|
|
87
|
+
│ getComposableToken │ YES
|
|
88
|
+
│ (with URL token) │ │
|
|
89
|
+
│ │ ▼
|
|
90
|
+
│ IMPORTANT: │ ┌──────────────────────────┐
|
|
91
|
+
│ This token is │ │ Show "Continue with │
|
|
92
|
+
│ already a │ │ Central Auth" button │
|
|
93
|
+
│ VERTESIA JWT! │ │ │
|
|
94
|
+
└────────┬───────────┘ │ OR │
|
|
95
|
+
│ │ │
|
|
96
|
+
▼ │ Show login options: │
|
|
97
|
+
┌────────────────────┐ │ - GoogleSignInButton │
|
|
98
|
+
│ SUCCESS? │ │ - GitHubSignInButton │
|
|
99
|
+
└────┬───────────┬───┘ │ - MicrosoftSignInButton │
|
|
100
|
+
YES NO │ - EnterpriseSigninButton │
|
|
101
|
+
│ │ └──────┬───────────────────┘
|
|
102
|
+
│ ▼ │
|
|
103
|
+
│ ┌──────────────────┐ │
|
|
104
|
+
│ │ Error Type? │ ┌───────────────┴───────────────┐
|
|
105
|
+
│ └──┬───────────┬───┘ │ │
|
|
106
|
+
│ │ │ ┌───────▼────────┐ ┌──────────────┐ │
|
|
107
|
+
│ UserNot Other │ OPTION 1: │ │ OPTION 2: │ │
|
|
108
|
+
│ Found Error │ CENTRAL AUTH │ │ STANDARD │ │
|
|
109
|
+
│ │ │ │ (OAuth-like) │ │ SIGNIN │ │
|
|
110
|
+
│ ▼ ▼ └───────┬────────┘ └──────┬───────┘ │
|
|
111
|
+
│ ┌───────┐ ┌──────────────────┐ │ │ │
|
|
112
|
+
│ │ Show │ │ Show SigninScreen│ │ │ │
|
|
113
|
+
│ │signup │ │ with error msg │ │ │ │
|
|
114
|
+
│ │ flow │ │ (authError set) │ │ ┌────▼──────────▼────┐
|
|
115
|
+
│ └───────┘ └──────────────────┘ │ │ OPTION 3: │
|
|
116
|
+
│ │ │ SSO SIGNIN (SAML) │
|
|
117
|
+
│ │ └────┬───────────────┘
|
|
118
|
+
▼ │ │
|
|
119
|
+
┌────────────────────┐ │ │
|
|
120
|
+
│ session.login(JWT) │ ◄──────────────────┼───────────────────┘
|
|
121
|
+
│ │ │
|
|
122
|
+
│ Inside login(): │ │
|
|
123
|
+
│ 1. authError=undef │ │
|
|
124
|
+
│ 2. isLoading=false │ │
|
|
125
|
+
│ 3. Decode JWT │ │
|
|
126
|
+
│ 4. Set auth │ │
|
|
127
|
+
│ callback │ │
|
|
128
|
+
│ 5. Save to │ │
|
|
129
|
+
│ localStorage │ │
|
|
130
|
+
│ 6. Notify │ │
|
|
131
|
+
│ Env.onLogin() │ │
|
|
132
|
+
│ 7. Promise.all([ │ │
|
|
133
|
+
│ _loadTypes(), │ │
|
|
134
|
+
│ fetchOnboard │ │
|
|
135
|
+
│ ingStatus() │ │
|
|
136
|
+
│ ]) │ │
|
|
137
|
+
└────────┬───────────┘ │
|
|
138
|
+
│ │
|
|
139
|
+
▼ │
|
|
140
|
+
┌────────────────────┐ │
|
|
141
|
+
│ setSession() │ │
|
|
142
|
+
│ Update state │ │
|
|
143
|
+
│ │ │
|
|
144
|
+
│ USER LOGGED IN! │ │
|
|
145
|
+
│ Show main app │ │
|
|
146
|
+
└────────────────────┘ │
|
|
147
|
+
│
|
|
148
|
+
│
|
|
149
|
+
┌──────────────────────────────────────────┼──────────────────────────────────┐
|
|
150
|
+
│ │ │
|
|
151
|
+
│ DETAILED: SIGNIN OPTIONS │ │
|
|
152
|
+
│ │ │
|
|
153
|
+
└──────────────────────────────────────────┼──────────────────────────────────┘
|
|
154
|
+
│
|
|
155
|
+
┌───────────────────────────────┴────────────────────────┐
|
|
156
|
+
│ │
|
|
157
|
+
▼ │
|
|
158
|
+
┌────────────────────────┐ ┌────────────────────────┐ ┌─────▼─────────────┐
|
|
159
|
+
│ OPTION 1: │ │ OPTION 2: │ │ OPTION 3: │
|
|
160
|
+
│ CENTRAL AUTH │ │ STANDARD SIGNIN │ │ SSO SIGNIN │
|
|
161
|
+
│ ═════════════ │ │ ═══════════════ │ │ ══════════ │
|
|
162
|
+
└────────┬───────────────┘ └────────┬───────────────┘ └─────┬─────────────┘
|
|
163
|
+
│ │ │
|
|
164
|
+
▼ ▼ ▼
|
|
165
|
+
┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
|
|
166
|
+
│ Generate state │ │ User clicks: │ │ User enters: │
|
|
167
|
+
│ Save to session │ │ - Google │ │ - Company email │
|
|
168
|
+
│ Storage (5 min) │ │ - GitHub │ │ or domain │
|
|
169
|
+
└──────────┬───────────┘ │ - Microsoft │ └──────────┬───────────┘
|
|
170
|
+
│ └──────────┬───────────┘ │
|
|
171
|
+
▼ │ ▼
|
|
172
|
+
┌──────────────────────┐ │ ┌──────────────────────┐
|
|
173
|
+
│ Get account/ │ ▼ │ Lookup SSO │
|
|
174
|
+
│ project from URL or │ ┌──────────────────────┐ │ provider config │
|
|
175
|
+
│ localStorage │ │ Call Firebase: │ │ for domain │
|
|
176
|
+
└──────────┬───────────┘ │ signInWith...() │ └──────────┬───────────┘
|
|
177
|
+
│ │ - Google │ │
|
|
178
|
+
▼ │ - GitHub │ ▼
|
|
179
|
+
┌──────────────────────┐ │ - Microsoft │ ┌──────────────────────┐
|
|
180
|
+
│ Build redirect URL: │ └──────────┬───────────┘ │ Redirect to SSO │
|
|
181
|
+
│ │ │ │ provider (Okta, │
|
|
182
|
+
│ internal-auth │ ▼ │ Azure AD, etc.) │
|
|
183
|
+
│ .vertesia.app │ ┌──────────────────────┐ └──────────┬───────────┘
|
|
184
|
+
│ ?sts=... │ │ Firebase OAuth │ │
|
|
185
|
+
│ &redirect_uri=... │ │ redirect & return │ ▼
|
|
186
|
+
│ &state=... │ └──────────┬───────────┘ ┌──────────────────────┐
|
|
187
|
+
└──────────┬───────────┘ │ │ User auth at SSO │
|
|
188
|
+
│ ▼ └──────────┬───────────┘
|
|
189
|
+
▼ ┌──────────────────────┐ │
|
|
190
|
+
┌──────────────────────┐ │ Firebase auth │ ▼
|
|
191
|
+
│ window.location │ │ success! │ ┌──────────────────────┐
|
|
192
|
+
│ .replace() to │ │ │ │ SAML response │
|
|
193
|
+
│ central auth │ │ Firebase creates │ │ back to app │
|
|
194
|
+
└──────────┬───────────┘ │ session │ └──────────┬───────────┘
|
|
195
|
+
│ └──────────┬───────────┘ │
|
|
196
|
+
▼ │ ▼
|
|
197
|
+
┌──────────────────────┐ │ ┌──────────────────────┐
|
|
198
|
+
│ Central Auth Page │ │ │ Exchange SAML for │
|
|
199
|
+
│ │ │ │ Firebase custom │
|
|
200
|
+
│ User authenticates │ │ │ token │
|
|
201
|
+
│ Gets Firebase token │ │ └──────────┬───────────┘
|
|
202
|
+
└──────────┬───────────┘ │ │
|
|
203
|
+
│ │ ▼
|
|
204
|
+
▼ │ ┌──────────────────────┐
|
|
205
|
+
┌──────────────────────┐ │ │ Sign in to Firebase │
|
|
206
|
+
│ Central auth calls │ │ │ with custom token │
|
|
207
|
+
│ STS server: │ │ └──────────┬───────────┘
|
|
208
|
+
│ │ │ │
|
|
209
|
+
│ POST │ │ │
|
|
210
|
+
│ sts.vertesia.io │ │ │
|
|
211
|
+
│ /token/issue │ │ │
|
|
212
|
+
│ │ │ │
|
|
213
|
+
│ Authorization: │ │ │
|
|
214
|
+
│ Bearer <Firebase> │ │ │
|
|
215
|
+
│ │ │ │
|
|
216
|
+
│ Body: { │ │ │
|
|
217
|
+
│ type: 'user', │ │ │
|
|
218
|
+
│ account_id, │ │ │
|
|
219
|
+
│ project_id │ │ │
|
|
220
|
+
│ } │ │ │
|
|
221
|
+
└──────────┬───────────┘ │ │
|
|
222
|
+
│ │ │
|
|
223
|
+
▼ │ │
|
|
224
|
+
┌──────────────────────┐ │ │
|
|
225
|
+
│ STS validates │ │ │
|
|
226
|
+
│ Firebase token and │ │ │
|
|
227
|
+
│ generates Vertesia │ │ │
|
|
228
|
+
│ JWT │ │ │
|
|
229
|
+
└──────────┬───────────┘ │ │
|
|
230
|
+
│ │ │
|
|
231
|
+
▼ │ │
|
|
232
|
+
┌──────────────────────┐ │ │
|
|
233
|
+
│ Redirect back to app │ │ │
|
|
234
|
+
│ with: │ │ │
|
|
235
|
+
│ │ │ │
|
|
236
|
+
│ #token=<VERTESIA_JWT>│ │ │
|
|
237
|
+
│ &state=<state> │ │ │
|
|
238
|
+
└──────────┬───────────┘ │ │
|
|
239
|
+
│ │ │
|
|
240
|
+
│ ▼ ▼
|
|
241
|
+
│ ┌────────────────────────────────────────────┐
|
|
242
|
+
│ │ ★ TRIGGERS onAuthStateChanged listener ★ │
|
|
243
|
+
│ │ │
|
|
244
|
+
│ │ This Firebase listener is triggered by: │
|
|
245
|
+
│ │ │
|
|
246
|
+
│ │ 1. Page load/mount (always) │
|
|
247
|
+
│ │ │
|
|
248
|
+
│ │ 2. Firebase sign-in from: │
|
|
249
|
+
│ │ - Google/GitHub/MS OAuth (Option 2) │
|
|
250
|
+
│ │ - Enterprise SSO/SAML (Option 3) │
|
|
251
|
+
│ │ │
|
|
252
|
+
│ │ 3. Firebase sign-out (session.logout()) │
|
|
253
|
+
│ │ │
|
|
254
|
+
│ │ 4. Token refresh/expiry (automatic) │
|
|
255
|
+
│ └────────────┬───────────────────────────────┘
|
|
256
|
+
│ │
|
|
257
|
+
│ ▼
|
|
258
|
+
│ ┌────────────────────────────────┐
|
|
259
|
+
│ │ Firebase user exists? │
|
|
260
|
+
│ └─────┬──────────────────┬───────┘
|
|
261
|
+
│ YES NO
|
|
262
|
+
│ │ │
|
|
263
|
+
│ ▼ ▼
|
|
264
|
+
│ ┌────────────────────┐ ┌────────────────────┐
|
|
265
|
+
│ │ RETURNING USER │ │ NEW/LOGGED OUT │
|
|
266
|
+
│ │ │ │ USER │
|
|
267
|
+
│ │ getComposableToken │ │ │
|
|
268
|
+
│ │ calls STS: │ │ session.authToken │
|
|
269
|
+
│ │ │ │ stays undefined │
|
|
270
|
+
│ │ POST │ │ │
|
|
271
|
+
│ │ sts.vertesia.io │ │ SigninScreen shows │
|
|
272
|
+
│ │ /token/issue │ │ (!session||!user) │
|
|
273
|
+
│ │ │ └────────────────────┘
|
|
274
|
+
│ │ Authorization: │
|
|
275
|
+
│ │ Bearer <Firebase> │
|
|
276
|
+
│ │ │
|
|
277
|
+
│ │ Body: { │
|
|
278
|
+
│ │ type: 'user', │
|
|
279
|
+
│ │ account_id, │
|
|
280
|
+
│ │ project_id │
|
|
281
|
+
│ │ } │
|
|
282
|
+
│ │ │
|
|
283
|
+
│ │ STS returns │
|
|
284
|
+
│ │ Vertesia JWT │
|
|
285
|
+
│ └────────┬───────────┘
|
|
286
|
+
│ │
|
|
287
|
+
│ ▼
|
|
288
|
+
│ ┌────────────────────┐
|
|
289
|
+
│ │ session.login(JWT) │
|
|
290
|
+
│ └────────┬───────────┘
|
|
291
|
+
│ │
|
|
292
|
+
│ ▼
|
|
293
|
+
│ ┌────────────────────┐
|
|
294
|
+
│ │ SUCCESS? │
|
|
295
|
+
│ └────┬───────────┬───┘
|
|
296
|
+
│ YES NO
|
|
297
|
+
│ │ │
|
|
298
|
+
│ │ ▼
|
|
299
|
+
│ │ ┌──────────────────┐
|
|
300
|
+
│ │ │ UserNotFoundError│
|
|
301
|
+
│ │ │ → Show signup │
|
|
302
|
+
│ │ │ │
|
|
303
|
+
│ │ │ Other Error │
|
|
304
|
+
│ │ │ → logout() │
|
|
305
|
+
│ │ │ → Show error │
|
|
306
|
+
│ │ └──────────────────┘
|
|
307
|
+
│ │
|
|
308
|
+
│ ▼
|
|
309
|
+
│ ┌────────────────────┐
|
|
310
|
+
│ │ Update state │
|
|
311
|
+
│ │ User logged in │
|
|
312
|
+
│ │ Show main app │
|
|
313
|
+
│ └────────────────────┘
|
|
314
|
+
│ │
|
|
315
|
+
└────────────────┘
|
|
316
|
+
│
|
|
317
|
+
▼
|
|
318
|
+
┌────────────────────┐
|
|
319
|
+
│ ═══════════════════│
|
|
320
|
+
│ ALL PATHS END HERE │
|
|
321
|
+
│ ═══════════════════│
|
|
322
|
+
│ │
|
|
323
|
+
│ User authenticated │
|
|
324
|
+
│ Main app shown │
|
|
325
|
+
└────────────────────┘
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Key Differences Between Authentication Paths
|
|
329
|
+
|
|
330
|
+
### Path A: Central Auth (Option 1) - Returns with JWT
|
|
331
|
+
1. User clicks "Continue with Central Auth"
|
|
332
|
+
2. Redirects to internal-auth.vertesia.app
|
|
333
|
+
3. Central auth authenticates user and gets Firebase token
|
|
334
|
+
4. **Central auth calls STS** (`POST sts.vertesia.io/token/issue` with Firebase token)
|
|
335
|
+
5. **STS validates Firebase token and generates Vertesia JWT**
|
|
336
|
+
6. Returns with **Vertesia JWT** in URL hash
|
|
337
|
+
7. UserSessionProvider processes JWT directly → `session.login(JWT)`
|
|
338
|
+
8. **onAuthStateChanged may fire separately but login already happened**
|
|
339
|
+
|
|
340
|
+
### Path B: Standard/SSO (Options 2 & 3) - Firebase First
|
|
341
|
+
1. User clicks provider button (Google/GitHub/Microsoft/Enterprise)
|
|
342
|
+
2. Firebase authentication flow (OAuth or SAML)
|
|
343
|
+
3. **Firebase creates session (NO token in URL)**
|
|
344
|
+
4. **onAuthStateChanged fires FIRST**
|
|
345
|
+
5. Listener calls `getComposableToken()` which **calls STS** (`POST sts.vertesia.io/token/issue` with Firebase token)
|
|
346
|
+
6. **STS validates Firebase token and generates Vertesia JWT**
|
|
347
|
+
7. Then calls `session.login(JWT)`
|
|
348
|
+
|
|
349
|
+
### Summary Table
|
|
350
|
+
|
|
351
|
+
| Aspect | Central Auth (Option 1) | Standard/SSO (Options 2 & 3) |
|
|
352
|
+
|--------|------------------------|------------------------------|
|
|
353
|
+
| **Token in URL?** | YES - Vertesia JWT | NO |
|
|
354
|
+
| **Initial trigger** | URL token processing | onAuthStateChanged listener |
|
|
355
|
+
| **Who calls STS?** | Central auth server | Your app (in onAuthStateChanged) |
|
|
356
|
+
| **STS call location** | Server-side (central auth) | Client-side (browser) |
|
|
357
|
+
| **Firebase session** | Created by central auth | Created directly by Firebase |
|
|
358
|
+
| **Login order** | 1. session.login()<br>2. onAuthStateChanged fires | 1. onAuthStateChanged fires<br>2. session.login() |
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Signup Flow (UserNotFoundError)
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
366
|
+
│ UserNotFoundError Handling │
|
|
367
|
+
└────────────────────────────────────────────┬────────────────────────────────┘
|
|
368
|
+
│
|
|
369
|
+
▼
|
|
370
|
+
┌─────────────────────────────┐
|
|
371
|
+
│ getComposableToken() │
|
|
372
|
+
│ returns UserNotFoundError │
|
|
373
|
+
│ │
|
|
374
|
+
│ This means: Firebase user │
|
|
375
|
+
│ exists but no Vertesia user │
|
|
376
|
+
└──────────┬──────────────────┘
|
|
377
|
+
│
|
|
378
|
+
▼
|
|
379
|
+
┌─────────────────────────────┐
|
|
380
|
+
│ Set state: │
|
|
381
|
+
│ - isLoading = false │
|
|
382
|
+
│ - authError = err │
|
|
383
|
+
│ - Do NOT logout() │
|
|
384
|
+
│ - Do NOT redirect │
|
|
385
|
+
└──────────┬──────────────────┘
|
|
386
|
+
│
|
|
387
|
+
▼
|
|
388
|
+
┌─────────────────────────────┐
|
|
389
|
+
│ SigninScreen detects │
|
|
390
|
+
│ UserNotFoundError via │
|
|
391
|
+
│ useEffect │
|
|
392
|
+
└──────────┬──────────────────┘
|
|
393
|
+
│
|
|
394
|
+
▼
|
|
395
|
+
┌─────────────────────────────┐
|
|
396
|
+
│ setCollectSignupData(true) │
|
|
397
|
+
│ │
|
|
398
|
+
│ Shows SignupForm component │
|
|
399
|
+
└──────────┬──────────────────┘
|
|
400
|
+
│
|
|
401
|
+
▼
|
|
402
|
+
┌─────────────────────────────┐
|
|
403
|
+
│ SignupForm Component │
|
|
404
|
+
│ │
|
|
405
|
+
│ User fills in: │
|
|
406
|
+
│ - Name │
|
|
407
|
+
│ - Organization │
|
|
408
|
+
│ - Other signup data │
|
|
409
|
+
└──────────┬──────────────────┘
|
|
410
|
+
│
|
|
411
|
+
▼
|
|
412
|
+
┌─────────────────────────────┐
|
|
413
|
+
│ onSignup() called │
|
|
414
|
+
│ │
|
|
415
|
+
│ Build SignupPayload: │
|
|
416
|
+
│ - signupData │
|
|
417
|
+
│ - firebaseToken │
|
|
418
|
+
└──────────┬──────────────────┘
|
|
419
|
+
│
|
|
420
|
+
▼
|
|
421
|
+
┌─────────────────────────────┐
|
|
422
|
+
│ POST /auth/signup │
|
|
423
|
+
│ │
|
|
424
|
+
│ Server creates: │
|
|
425
|
+
│ - Vertesia user │
|
|
426
|
+
│ - Default account │
|
|
427
|
+
│ - Default project │
|
|
428
|
+
└──────────┬──────────────────┘
|
|
429
|
+
│
|
|
430
|
+
▼
|
|
431
|
+
┌─────────────────────────────┐
|
|
432
|
+
│ trackEvent("sign_up") │
|
|
433
|
+
│ │
|
|
434
|
+
│ window.location.href = "/" │
|
|
435
|
+
│ │
|
|
436
|
+
│ (Full page reload triggers │
|
|
437
|
+
│ normal auth flow) │
|
|
438
|
+
└─────────────────────────────┘
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Logout Flow
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
447
|
+
│ User Logout │
|
|
448
|
+
└────────────────────────────────────────────┬────────────────────────────────┘
|
|
449
|
+
│
|
|
450
|
+
▼
|
|
451
|
+
┌─────────────────────────────┐
|
|
452
|
+
│ session.logout() or │
|
|
453
|
+
│ session.signOut() │
|
|
454
|
+
└──────────┬──────────────────┘
|
|
455
|
+
│
|
|
456
|
+
▼
|
|
457
|
+
┌─────────────────────────────┐
|
|
458
|
+
│ Check: authToken exists? │
|
|
459
|
+
└──────────┬──────────────────┘
|
|
460
|
+
YES
|
|
461
|
+
│
|
|
462
|
+
▼
|
|
463
|
+
┌─────────────────────────────┐
|
|
464
|
+
│ shouldRedirectToCentralAuth │
|
|
465
|
+
│ ()? │
|
|
466
|
+
└──────┬────────────┬─────────┘
|
|
467
|
+
YES NO
|
|
468
|
+
│ │
|
|
469
|
+
┌───────────┘ └──────────┐
|
|
470
|
+
▼ ▼
|
|
471
|
+
┌──────────────────────┐ ┌──────────────────────┐
|
|
472
|
+
│ Redirect to Central │ │ getFirebaseAuth() │
|
|
473
|
+
│ Auth for logout: │ │ .signOut() │
|
|
474
|
+
│ │ │ │
|
|
475
|
+
│ internal-auth │ │ (Triggers │
|
|
476
|
+
│ .vertesia.app │ │ onAuthStateChanged │
|
|
477
|
+
│ /logout │ │ with anonymous user)│
|
|
478
|
+
│ │ └──────────┬───────────┘
|
|
479
|
+
│ (Central auth │ │
|
|
480
|
+
│ handles Firebase │ │
|
|
481
|
+
│ logout) │ │
|
|
482
|
+
└──────────┬───────────┘ │
|
|
483
|
+
│ │
|
|
484
|
+
└────────────┬───────────────────┘
|
|
485
|
+
│
|
|
486
|
+
▼
|
|
487
|
+
┌─────────────────────────────┐
|
|
488
|
+
│ Clear session data: │
|
|
489
|
+
│ - authError = undefined │
|
|
490
|
+
│ - isLoading = false │
|
|
491
|
+
│ - authToken = undefined │
|
|
492
|
+
│ - typeRegistry = undefined │
|
|
493
|
+
└──────────┬──────────────────┘
|
|
494
|
+
│
|
|
495
|
+
▼
|
|
496
|
+
┌─────────────────────────────┐
|
|
497
|
+
│ client.withAuthCallback │
|
|
498
|
+
│ (undefined) │
|
|
499
|
+
│ │
|
|
500
|
+
│ (Clear client auth) │
|
|
501
|
+
└──────────┬──────────────────┘
|
|
502
|
+
│
|
|
503
|
+
▼
|
|
504
|
+
┌─────────────────────────────┐
|
|
505
|
+
│ setSession(this.clone()) │
|
|
506
|
+
│ │
|
|
507
|
+
│ React re-render triggers │
|
|
508
|
+
│ SigninScreen display │
|
|
509
|
+
└─────────────────────────────┘
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Account/Project Switching
|
|
515
|
+
|
|
516
|
+
```
|
|
517
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
518
|
+
│ Account/Project Switching │
|
|
519
|
+
└────────────────────────────────────────┬────────────────────────────────────┘
|
|
520
|
+
│
|
|
521
|
+
┌────────────────┴────────────────┐
|
|
522
|
+
│ │
|
|
523
|
+
▼ ▼
|
|
524
|
+
┌────────────────────┐ ┌────────────────────┐
|
|
525
|
+
│ switchAccount(id) │ │ switchProject(id) │
|
|
526
|
+
└──────────┬─────────┘ └──────────┬─────────┘
|
|
527
|
+
│ │
|
|
528
|
+
▼ ▼
|
|
529
|
+
┌────────────────────┐ ┌────────────────────┐
|
|
530
|
+
│ Save to │ │ Save to │
|
|
531
|
+
│ localStorage: │ │ localStorage: │
|
|
532
|
+
│ - LastSelected │ │ - LastSelected │
|
|
533
|
+
│ AccountId │ │ ProjectId │
|
|
534
|
+
│ - LastSelected │ │ (per account) │
|
|
535
|
+
│ ProjectId │ └──────────┬─────────┘
|
|
536
|
+
│ (for current) │ │
|
|
537
|
+
└──────────┬─────────┘ │
|
|
538
|
+
│ │
|
|
539
|
+
▼ ▼
|
|
540
|
+
┌────────────────────┐ ┌────────────────────┐
|
|
541
|
+
│ window.location │ │ window.location │
|
|
542
|
+
│ .replace( │ │ .replace( │
|
|
543
|
+
│ '/?a=' + id) │ │ '/?a=' + acct │
|
|
544
|
+
│ │ │ '&p=' + proj) │
|
|
545
|
+
│ Full page reload │ │ │
|
|
546
|
+
└──────────┬─────────┘ │ Full page reload │
|
|
547
|
+
│ └──────────┬─────────┘
|
|
548
|
+
│ │
|
|
549
|
+
└───────────┬───────────────────┘
|
|
550
|
+
│
|
|
551
|
+
▼
|
|
552
|
+
┌─────────────────────────────┐
|
|
553
|
+
│ UserSessionProvider mounts │
|
|
554
|
+
│ with new account/project │
|
|
555
|
+
│ from URL params │
|
|
556
|
+
│ │
|
|
557
|
+
│ Firebase listener triggers │
|
|
558
|
+
│ getComposableToken with │
|
|
559
|
+
│ new selection │
|
|
560
|
+
└─────────────────────────────┘
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
---
|
|
564
|
+
|
|
565
|
+
## Key Implementation Details
|
|
566
|
+
|
|
567
|
+
### State Validation (CSRF Protection)
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
// generateState() creates random string, stores in sessionStorage with 5 min TTL
|
|
571
|
+
const state = crypto.randomUUID();
|
|
572
|
+
sessionStorage.setItem('auth_state', JSON.stringify({
|
|
573
|
+
state,
|
|
574
|
+
timestamp: Date.now()
|
|
575
|
+
}));
|
|
576
|
+
|
|
577
|
+
// verifyState() checks:
|
|
578
|
+
// 1. State exists in sessionStorage
|
|
579
|
+
// 2. Matches URL parameter
|
|
580
|
+
// 3. Not expired (< 5 minutes old)
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### Account/Project Selection Priority
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
// 1. URL params (highest priority)
|
|
587
|
+
const accountId = searchParams.get('a')
|
|
588
|
+
// 2. localStorage (fallback)
|
|
589
|
+
?? localStorage.getItem(LastSelectedAccountId_KEY)
|
|
590
|
+
// 3. undefined (will use default)
|
|
591
|
+
?? undefined;
|
|
592
|
+
|
|
593
|
+
const projectId = searchParams.get('p')
|
|
594
|
+
?? localStorage.getItem(LastSelectedProjectId_KEY + '-' + accountId)
|
|
595
|
+
?? undefined;
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Session.login() Internal Flow
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
async login(token: string) {
|
|
602
|
+
// 1. Clear any previous errors
|
|
603
|
+
this.authError = undefined;
|
|
604
|
+
this.isLoading = false;
|
|
605
|
+
|
|
606
|
+
// 2. Setup auth callback for API client
|
|
607
|
+
this.client.withAuthCallback(() => this.authCallback);
|
|
608
|
+
|
|
609
|
+
// 3. Decode JWT to get user info
|
|
610
|
+
this.authToken = jwtDecode(token);
|
|
611
|
+
|
|
612
|
+
// 4. Save selections to localStorage
|
|
613
|
+
localStorage.setItem(LastSelectedAccountId_KEY, this.authToken.account.id);
|
|
614
|
+
localStorage.setItem(
|
|
615
|
+
LastSelectedProjectId_KEY + '-' + this.authToken.account.id,
|
|
616
|
+
this.authToken.project?.id ?? ''
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
// 5. Notify host app
|
|
620
|
+
Env.onLogin?.(this.authToken);
|
|
621
|
+
|
|
622
|
+
// 6. Load types and onboarding in parallel
|
|
623
|
+
await Promise.all([
|
|
624
|
+
this._loadTypes(),
|
|
625
|
+
this.fetchOnboardingStatus(),
|
|
626
|
+
]);
|
|
627
|
+
}
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Idempotency Guard
|
|
631
|
+
|
|
632
|
+
```typescript
|
|
633
|
+
// UserSessionProvider uses ref to prevent duplicate auth flow
|
|
634
|
+
const hasInitiatedAuthRef = useRef(false);
|
|
635
|
+
|
|
636
|
+
useEffect(() => {
|
|
637
|
+
if (hasInitiatedAuthRef.current) {
|
|
638
|
+
console.log("Auth: skipping duplicate auth flow initiation");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
hasInitiatedAuthRef.current = true;
|
|
642
|
+
// ... rest of auth flow
|
|
643
|
+
}, []);
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
---
|
|
647
|
+
|
|
648
|
+
## Environment Differences
|
|
649
|
+
|
|
650
|
+
The `shouldRedirectToCentralAuth()` function determines which signin UI to show:
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
// Currently hardcoded to return true
|
|
654
|
+
export function shouldRedirectToCentralAuth() {
|
|
655
|
+
return true;
|
|
656
|
+
|
|
657
|
+
// Has logic for:
|
|
658
|
+
// if (Env.isDocker) return true;
|
|
659
|
+
// return devDomains.some(domain =>
|
|
660
|
+
// window.location.hostname.endsWith(domain)
|
|
661
|
+
// );
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
When `true`: Shows "Continue with Central Auth" button
|
|
666
|
+
When `false`: Shows Google/GitHub/Microsoft/Enterprise sign-in buttons
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## Error Handling Summary
|
|
671
|
+
|
|
672
|
+
| Error Type | Flow | Action |
|
|
673
|
+
|------------|------|--------|
|
|
674
|
+
| **UserNotFoundError** | Token exchange | Show SignupForm, don't logout, don't redirect |
|
|
675
|
+
| **Invalid state** | State validation | Show SigninScreen with error |
|
|
676
|
+
| **Network/API error** | Token exchange | Show SigninScreen with error message |
|
|
677
|
+
| **Type loading error** | session.login() | Set authError, partial login (may show app with limitations) |
|
|
678
|
+
| **Onboarding fetch error** | session.login() | Log error, continue (onboardingComplete=false) |
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## Storage Keys
|
|
683
|
+
|
|
684
|
+
- `LastSelectedAccountId_KEY`: Last selected account ID
|
|
685
|
+
- `LastSelectedProjectId_KEY-{accountId}`: Last selected project per account
|
|
686
|
+
- `auth_state`: State parameter for CSRF protection (sessionStorage, 5 min TTL)
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Complete Flow Diagram
|
|
691
|
+
|
|
692
|
+
### Full Authentication Flow (Mermaid)
|
|
693
|
+
|
|
694
|
+
```mermaid
|
|
695
|
+
flowchart TB
|
|
696
|
+
Start([APP LOADS NO AUTHENTICATION]) --> Mount[UserSessionProvider mounts<br/>1. Parse URL hash<br/>2. Get account/project<br/>3. Clear URL hash<br/>4. Setup idempotency<br/>5. Create UserSession]
|
|
697
|
+
Mount --> HasToken{Has token &<br/>state in URL?}
|
|
698
|
+
|
|
699
|
+
%% PATH B: NO TOKEN FLOW
|
|
700
|
+
HasToken -->|NO| PathB[PATH B: NO TOKEN FLOW<br/>First load or no URL token]
|
|
701
|
+
PathB --> SetupFirebase[Setup Firebase<br/>onAuthStateChanged<br/>listener]
|
|
702
|
+
SetupFirebase --> ShowSignin[Show SigninScreen]
|
|
703
|
+
ShowSignin --> ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
|
|
704
|
+
ShouldRedirect -->|YES| ShowCentral[Show Continue with<br/>Central Auth button]
|
|
705
|
+
ShouldRedirect -->|NO| ShowOptions[Show login options:<br/>- Google<br/>- GitHub<br/>- Microsoft<br/>- Enterprise]
|
|
706
|
+
|
|
707
|
+
%% Three authentication options
|
|
708
|
+
ShowCentral --> Option1
|
|
709
|
+
ShowOptions --> Option2
|
|
710
|
+
ShowOptions --> Option3
|
|
711
|
+
|
|
712
|
+
subgraph Option1[OPTION 1: CENTRAL AUTH]
|
|
713
|
+
C1[Generate state<br/>Save to sessionStorage]
|
|
714
|
+
C2[Get account/project<br/>from URL or localStorage]
|
|
715
|
+
C3[Build redirect URL:<br/>internal-auth.vertesia.app<br/>?sts=...&redirect_uri=...&state=...]
|
|
716
|
+
C4[window.location.replace<br/>to central auth]
|
|
717
|
+
C5[Central Auth Page<br/>User authenticates<br/>Gets Firebase token]
|
|
718
|
+
C6[Central auth calls STS:<br/>POST sts.vertesia.io/token/issue<br/>Authorization: Bearer Firebase<br/>Body: type, account_id, project_id]
|
|
719
|
+
C7[STS validates Firebase token<br/>and generates Vertesia JWT]
|
|
720
|
+
C8[Redirect back to app with:<br/>#token=VERTESIA_JWT&state=state]
|
|
721
|
+
|
|
722
|
+
C1 --> C2 --> C3 --> C4 --> C5 --> C6 --> C7 --> C8
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
subgraph Option2[OPTION 2: STANDARD SIGNIN]
|
|
726
|
+
S1[User clicks:<br/>Google/GitHub/Microsoft]
|
|
727
|
+
S2[Call Firebase:<br/>signInWith... OAuth]
|
|
728
|
+
S3[Firebase OAuth<br/>redirect & return]
|
|
729
|
+
S4[Firebase auth success!<br/>Firebase creates session]
|
|
730
|
+
|
|
731
|
+
S1 --> S2 --> S3 --> S4
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
subgraph Option3[OPTION 3: SSO SIGNIN SAML]
|
|
735
|
+
E1[User enters:<br/>Company email or domain]
|
|
736
|
+
E2[Lookup SSO<br/>provider config]
|
|
737
|
+
E3[Redirect to SSO provider<br/>Okta, Azure AD, etc.]
|
|
738
|
+
E4[User auth at SSO provider]
|
|
739
|
+
E5[SAML response back to app]
|
|
740
|
+
E6[Exchange SAML for<br/>Firebase custom token]
|
|
741
|
+
E7[Sign in to Firebase<br/>with custom token]
|
|
742
|
+
|
|
743
|
+
E1 --> E2 --> E3 --> E4 --> E5 --> E6 --> E7
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
%% PATH A: TOKEN FLOW - After redirect back
|
|
747
|
+
C8 --> PathA[PATH A: TOKEN FLOW<br/>Returning from central auth]
|
|
748
|
+
HasToken -->|YES| PathA
|
|
749
|
+
PathA --> VerifyState{verifyState?}
|
|
750
|
+
VerifyState -->|NO| InvalidState[Invalid state!<br/>Log error<br/>Show SigninScreen]
|
|
751
|
+
VerifyState -->|YES| ReturningUser[RETURNING USER<br/>getComposableToken calls STS:<br/>POST sts.vertesia.io/token/issue<br/>Authorization: Bearer Firebase<br/>Body: type, account_id, project_id<br/><br/>STS returns Vertesia JWT]
|
|
752
|
+
ClearState --> GetToken[getComposableToken<br/>with URL token<br/>This is already a<br/>VERTESIA JWT from STS]
|
|
753
|
+
GetToken --> TokenSuccess{SUCCESS?}
|
|
754
|
+
TokenSuccess -->|YES| SessionLogin[session.login JWT]
|
|
755
|
+
TokenSuccess -->|NO| TokenError{Error Type?}
|
|
756
|
+
TokenError -->|UserNotFound| SignupFlow
|
|
757
|
+
TokenError -->|Other| ShowError[Show SigninScreen<br/>with error msg]
|
|
758
|
+
|
|
759
|
+
subgraph SignupFlow[SIGNUP FLOW - UserNotFoundError]
|
|
760
|
+
SU1[Set state:<br/>- isLoading = false<br/>- authError = UserNotFoundError<br/>- Do NOT logout]
|
|
761
|
+
SU2[SigninScreen detects<br/>UserNotFoundError via useEffect]
|
|
762
|
+
SU3[setCollectSignupData true<br/>Shows SignupForm component]
|
|
763
|
+
SU4[User fills in:<br/>- Name<br/>- Organization<br/>- Other signup data]
|
|
764
|
+
SU5[onSignup called<br/>Build SignupPayload]
|
|
765
|
+
SU6[POST /auth/signup<br/><br/>Server creates:<br/>- Vertesia user<br/>- Default account<br/>- Default project]
|
|
766
|
+
SU7[trackEvent sign_up<br/>window.location.href = /<br/>Full page reload]
|
|
767
|
+
|
|
768
|
+
SU1 --> SU2 --> SU3 --> SU4 --> SU5 --> SU6 --> SU7
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
Start([User Logout]) --> Trigger[session.logout or<br/>session.signOut]
|
|
772
|
+
|
|
773
|
+
Trigger --> Check{authToken<br/>exists?}
|
|
774
|
+
|
|
775
|
+
Check -->|YES| ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
|
|
776
|
+
|
|
777
|
+
ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
|
|
778
|
+
|
|
779
|
+
ShouldRedirect -->|NO| FirebaseSignout[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
|
|
780
|
+
|
|
781
|
+
RedirectLogout --> ClearData[Clear session data:<br/>- authError = undefined<br/>- isLoading = false<br/>- authToken = undefined<br/>- typeRegistry = undefined]
|
|
782
|
+
|
|
783
|
+
FirebaseSignout --> ClearData
|
|
784
|
+
|
|
785
|
+
ClearData --> ClearClient[client.withAuthCallback<br/>undefined<br/><br/>Clear client auth]
|
|
786
|
+
|
|
787
|
+
ClearClient --> UpdateState[setSession this.clone<br/><br/>React re-render triggers<br/>SigninScreen display]
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
SU7 --> Converge
|
|
791
|
+
|
|
792
|
+
%% Convergence point
|
|
793
|
+
S4 --> FirebaseListener
|
|
794
|
+
E7 --> FirebaseListener
|
|
795
|
+
|
|
796
|
+
FirebaseListener[★ TRIGGERS onAuthStateChanged ★<br/>Triggered by:<br/>1. Page load/mount always<br/>2. Firebase sign-in Options 2 & 3<br/>3. Firebase sign-out<br/>4. Token refresh/expiry]
|
|
797
|
+
|
|
798
|
+
FirebaseListener --> FirebaseUser{Firebase user exists?}
|
|
799
|
+
|
|
800
|
+
FirebaseUser -->|NO| LoggedOut[NEW/LOGGED OUT USER<br/>session.authToken stays undefined<br/>SigninScreen shows]
|
|
801
|
+
|
|
802
|
+
LoggedOut --> ShowSignin
|
|
803
|
+
|
|
804
|
+
FirebaseUser -->|YES| ReturningUser[RETURNING USER<br/>getComposableToken calls STS:<br/>POST sts.vertesia.io/token/issue<br/>Authorization: Bearer Firebase<br/>Body: type, account_id, project_id<br/><br/>STS returns Vertesia JWT]
|
|
805
|
+
|
|
806
|
+
ReturningUser --> ReturningLogin[session.login JWT]
|
|
807
|
+
ReturningLogin --> ReturningSuccess{SUCCESS?}
|
|
808
|
+
ReturningSuccess -->|YES| UpdateState[Update state<br/>User logged in<br/>Show main app]
|
|
809
|
+
ReturningSuccess -->|NO| ReturningError{Error Type?}
|
|
810
|
+
ReturningError -->|UserNotFound| SignupFlow
|
|
811
|
+
ReturningError -->|Other| LogoutError[logout<br/>Show error]
|
|
812
|
+
|
|
813
|
+
%% Session login flow
|
|
814
|
+
SessionLogin --> LoginSteps[Inside login:<br/>1. authError=undefined<br/>2. isLoading=false<br/>3. Decode JWT<br/>4. Set auth callback<br/>5. Save to localStorage<br/>6. Notify Env.onLogin<br/>7. Promise.all loadTypes, fetchOnboarding]
|
|
815
|
+
LoginSteps --> SetSession[setSession<br/>Update state]
|
|
816
|
+
SetSession --> LoggedIn[USER LOGGED IN!<br/>Show main app]
|
|
817
|
+
|
|
818
|
+
UpdateState --> Converge[ALL PATHS END HERE<br/>User authenticated<br/>Main app shown]
|
|
819
|
+
LoggedIn --> Converge
|
|
820
|
+
|
|
821
|
+
style PathA fill:#e1f5ff
|
|
822
|
+
style PathB fill:#fff4e1
|
|
823
|
+
style Option1 fill:#f0f0f0
|
|
824
|
+
style Option2 fill:#f0f0f0
|
|
825
|
+
style Option3 fill:#f0f0f0
|
|
826
|
+
style Converge fill:#d4edda
|
|
827
|
+
style FirebaseListener fill:#fff3cd
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## Sequence Diagrams
|
|
833
|
+
|
|
834
|
+
### Option 1: Central Auth Flow
|
|
835
|
+
|
|
836
|
+
```mermaid
|
|
837
|
+
sequenceDiagram
|
|
838
|
+
actor User
|
|
839
|
+
participant App as Composable UI
|
|
840
|
+
participant CentralAuth as Central Auth<br/>(internal-auth.vertesia.app)
|
|
841
|
+
participant Firebase as Firebase Auth
|
|
842
|
+
participant STS as STS<br/>(sts.vertesia.io)
|
|
843
|
+
|
|
844
|
+
User->>App: Navigate to app (no auth)
|
|
845
|
+
activate App
|
|
846
|
+
App->>App: Show SigninScreen
|
|
847
|
+
User->>App: Click 'Continue with Central Auth'
|
|
848
|
+
|
|
849
|
+
App->>App: generateState()<br/>(save to sessionStorage)
|
|
850
|
+
App->>CentralAuth: Redirect with state<br/>?sts=...&redirect_uri=...&state=...
|
|
851
|
+
activate CentralAuth
|
|
852
|
+
|
|
853
|
+
CentralAuth->>Firebase: Authenticate user
|
|
854
|
+
activate Firebase
|
|
855
|
+
Firebase-->>CentralAuth: Firebase ID token
|
|
856
|
+
deactivate Firebase
|
|
857
|
+
|
|
858
|
+
CentralAuth->>STS: POST /token/issue<br/>Authorization: Bearer {Firebase token}<br/>Body: {type:'user', account_id, project_id}
|
|
859
|
+
activate STS
|
|
860
|
+
STS->>STS: Validate Firebase token
|
|
861
|
+
STS->>STS: Generate Vertesia JWT
|
|
862
|
+
STS-->>CentralAuth: Vertesia JWT
|
|
863
|
+
deactivate STS
|
|
864
|
+
|
|
865
|
+
CentralAuth->>App: Redirect back<br/>#token={JWT}&state={state}
|
|
866
|
+
deactivate CentralAuth
|
|
867
|
+
|
|
868
|
+
App->>App: verifyState()
|
|
869
|
+
App->>App: clearState()
|
|
870
|
+
App->>App: getComposableToken(JWT from URL)
|
|
871
|
+
App->>App: session.login(JWT)
|
|
872
|
+
App->>App: Show main app
|
|
873
|
+
deactivate App
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
### Option 2: Standard Sign-in (Google/GitHub/Microsoft)
|
|
877
|
+
|
|
878
|
+
```mermaid
|
|
879
|
+
sequenceDiagram
|
|
880
|
+
actor User
|
|
881
|
+
participant App as Composable UI
|
|
882
|
+
participant Firebase as Firebase Auth
|
|
883
|
+
participant Provider as OAuth Provider<br/>(Google/GitHub/Microsoft)
|
|
884
|
+
participant STS as STS<br/>(sts.vertesia.io)
|
|
885
|
+
|
|
886
|
+
User->>App: Navigate to app (no auth)
|
|
887
|
+
activate App
|
|
888
|
+
App->>App: Show SigninScreen
|
|
889
|
+
App->>App: Setup onAuthStateChanged listener
|
|
890
|
+
User->>App: Click provider button (e.g., Google)
|
|
891
|
+
|
|
892
|
+
App->>Firebase: signInWithPopup(GoogleAuthProvider)
|
|
893
|
+
activate Firebase
|
|
894
|
+
Firebase->>Provider: OAuth redirect
|
|
895
|
+
activate Provider
|
|
896
|
+
Provider->>User: Show provider login page
|
|
897
|
+
User->>Provider: Authenticate & consent
|
|
898
|
+
Provider-->>Firebase: OAuth credentials
|
|
899
|
+
deactivate Provider
|
|
900
|
+
Firebase->>Firebase: Create Firebase session
|
|
901
|
+
Firebase-->>App: Firebase auth success
|
|
902
|
+
|
|
903
|
+
Note over App,Firebase: onAuthStateChanged fires
|
|
904
|
+
Firebase->>App: User object (with ID token)
|
|
905
|
+
deactivate Firebase
|
|
906
|
+
|
|
907
|
+
App->>App: getComposableToken()
|
|
908
|
+
App->>Firebase: Get ID token from current user
|
|
909
|
+
activate Firebase
|
|
910
|
+
Firebase-->>App: Firebase ID token
|
|
911
|
+
deactivate Firebase
|
|
912
|
+
|
|
913
|
+
App->>STS: POST /token/issue<br/>Authorization: Bearer {Firebase token}<br/>Body: {type:'user', account_id, project_id}
|
|
914
|
+
activate STS
|
|
915
|
+
STS->>STS: Validate Firebase token
|
|
916
|
+
STS->>STS: Generate Vertesia JWT
|
|
917
|
+
STS-->>App: Vertesia JWT
|
|
918
|
+
deactivate STS
|
|
919
|
+
|
|
920
|
+
App->>App: session.login(JWT)
|
|
921
|
+
App->>App: Show main app
|
|
922
|
+
deactivate App
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
### Option 3: Enterprise SSO/SAML Sign-in
|
|
926
|
+
|
|
927
|
+
```mermaid
|
|
928
|
+
sequenceDiagram
|
|
929
|
+
actor User
|
|
930
|
+
participant App as Composable UI
|
|
931
|
+
participant Firebase as Firebase Auth
|
|
932
|
+
participant SSO as SSO Provider<br/>(Okta/Azure AD/etc.)
|
|
933
|
+
participant STS as STS<br/>(sts.vertesia.io)
|
|
934
|
+
|
|
935
|
+
User->>App: Navigate to app (no auth)
|
|
936
|
+
activate App
|
|
937
|
+
App->>App: Show SigninScreen
|
|
938
|
+
App->>App: Setup onAuthStateChanged listener
|
|
939
|
+
User->>App: Click 'Enterprise Sign-in'
|
|
940
|
+
User->>App: Enter company email/domain
|
|
941
|
+
|
|
942
|
+
App->>App: Lookup SSO provider config
|
|
943
|
+
App->>SSO: Redirect to SSO provider
|
|
944
|
+
activate SSO
|
|
945
|
+
SSO->>User: Show SSO login page
|
|
946
|
+
User->>SSO: Authenticate
|
|
947
|
+
SSO-->>App: SAML response
|
|
948
|
+
deactivate SSO
|
|
949
|
+
|
|
950
|
+
App->>Firebase: Exchange SAML for Firebase custom token
|
|
951
|
+
activate Firebase
|
|
952
|
+
Firebase->>Firebase: Validate SAML & create session
|
|
953
|
+
Firebase-->>App: Firebase auth success
|
|
954
|
+
|
|
955
|
+
Note over App,Firebase: onAuthStateChanged fires
|
|
956
|
+
Firebase->>App: User object (with ID token)
|
|
957
|
+
deactivate Firebase
|
|
958
|
+
|
|
959
|
+
App->>App: getComposableToken()
|
|
960
|
+
App->>Firebase: Get ID token from current user
|
|
961
|
+
activate Firebase
|
|
962
|
+
Firebase-->>App: Firebase ID token
|
|
963
|
+
deactivate Firebase
|
|
964
|
+
|
|
965
|
+
App->>STS: POST /token/issue<br/>Authorization: Bearer {Firebase token}<br/>Body: {type:'user', account_id, project_id}
|
|
966
|
+
activate STS
|
|
967
|
+
STS->>STS: Validate Firebase token
|
|
968
|
+
STS->>STS: Generate Vertesia JWT
|
|
969
|
+
STS-->>App: Vertesia JWT
|
|
970
|
+
deactivate STS
|
|
971
|
+
|
|
972
|
+
App->>App: session.login(JWT)
|
|
973
|
+
App->>App: Show main app
|
|
974
|
+
deactivate App
|
|
975
|
+
```
|
|
976
|
+
|
|
977
|
+
### Session Restoration (Page Reload)
|
|
978
|
+
|
|
979
|
+
```mermaid
|
|
980
|
+
sequenceDiagram
|
|
981
|
+
actor User
|
|
982
|
+
participant App as Composable UI
|
|
983
|
+
participant Firebase as Firebase Auth
|
|
984
|
+
participant STS as STS<br/>(sts.vertesia.io)
|
|
985
|
+
|
|
986
|
+
User->>App: Reload page / Navigate back
|
|
987
|
+
activate App
|
|
988
|
+
App->>App: UserSessionProvider mounts
|
|
989
|
+
App->>App: Setup onAuthStateChanged listener
|
|
990
|
+
|
|
991
|
+
Note over App,Firebase: onAuthStateChanged fires immediately
|
|
992
|
+
|
|
993
|
+
alt Firebase session exists
|
|
994
|
+
Firebase->>App: User object (with ID token)
|
|
995
|
+
|
|
996
|
+
App->>App: getComposableToken()
|
|
997
|
+
App->>Firebase: Get ID token from current user
|
|
998
|
+
activate Firebase
|
|
999
|
+
Firebase-->>App: Firebase ID token
|
|
1000
|
+
deactivate Firebase
|
|
1001
|
+
|
|
1002
|
+
App->>STS: POST /token/issue<br/>Authorization: Bearer {Firebase token}<br/>Body: {type:'user', account_id, project_id}
|
|
1003
|
+
activate STS
|
|
1004
|
+
STS->>STS: Validate Firebase token
|
|
1005
|
+
STS->>STS: Generate Vertesia JWT
|
|
1006
|
+
STS-->>App: Vertesia JWT
|
|
1007
|
+
deactivate STS
|
|
1008
|
+
|
|
1009
|
+
App->>App: session.login(JWT)
|
|
1010
|
+
App->>App: Show main app (user logged in)
|
|
1011
|
+
else No Firebase session
|
|
1012
|
+
Firebase->>App: Anonymous user
|
|
1013
|
+
App->>App: Show SigninScreen (not logged in)
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
deactivate App
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
---
|
|
1020
|
+
|
|
1021
|
+
## Additional Flow Diagrams (Mermaid)
|
|
1022
|
+
|
|
1023
|
+
### Logout Flow
|
|
1024
|
+
|
|
1025
|
+
```mermaid
|
|
1026
|
+
flowchart TB
|
|
1027
|
+
Start([User Logout]) --> Trigger[session.logout or<br/>session.signOut]
|
|
1028
|
+
|
|
1029
|
+
Trigger --> Check{authToken<br/>exists?}
|
|
1030
|
+
|
|
1031
|
+
Check -->|YES| ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
|
|
1032
|
+
|
|
1033
|
+
ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
|
|
1034
|
+
|
|
1035
|
+
ShouldRedirect -->|NO| FirebaseSignout[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
|
|
1036
|
+
|
|
1037
|
+
RedirectLogout --> ClearData[Clear session data:<br/>- authError = undefined<br/>- isLoading = false<br/>- authToken = undefined<br/>- typeRegistry = undefined]
|
|
1038
|
+
|
|
1039
|
+
FirebaseSignout --> ClearData
|
|
1040
|
+
|
|
1041
|
+
ClearData --> ClearClient[client.withAuthCallback<br/>undefined<br/><br/>Clear client auth]
|
|
1042
|
+
|
|
1043
|
+
ClearClient --> UpdateState[setSession this.clone<br/><br/>React re-render triggers<br/>SigninScreen display]
|
|
1044
|
+
|
|
1045
|
+
style Start fill:#fff4e1
|
|
1046
|
+
style UpdateState fill:#d4edda
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
### Account/Project Switching Flow
|
|
1050
|
+
|
|
1051
|
+
```mermaid
|
|
1052
|
+
flowchart TB
|
|
1053
|
+
Start([Account/Project Switching]) --> Branch{Which action?}
|
|
1054
|
+
|
|
1055
|
+
Branch -->|Account| SwitchAccount[switchAccount id]
|
|
1056
|
+
Branch -->|Project| SwitchProject[switchProject id]
|
|
1057
|
+
|
|
1058
|
+
SwitchAccount --> SaveAccount[Save to localStorage:<br/>- LastSelectedAccountId<br/>- LastSelectedProjectId<br/> for current account]
|
|
1059
|
+
|
|
1060
|
+
SwitchProject --> SaveProject[Save to localStorage:<br/>- LastSelectedProjectId<br/> per account]
|
|
1061
|
+
|
|
1062
|
+
SaveAccount --> ReloadAccount[window.location.replace<br/>/?a= + id<br/><br/>Full page reload]
|
|
1063
|
+
|
|
1064
|
+
SaveProject --> ReloadProject[window.location.replace<br/>/?a= + acct & p= + proj<br/><br/>Full page reload]
|
|
1065
|
+
|
|
1066
|
+
ReloadAccount --> Converge[UserSessionProvider mounts<br/>with new account/project<br/>from URL params<br/><br/>Firebase listener triggers<br/>getComposableToken with<br/>new selection]
|
|
1067
|
+
|
|
1068
|
+
ReloadProject --> Converge
|
|
1069
|
+
|
|
1070
|
+
style Start fill:#fff4e1
|
|
1071
|
+
style Converge fill:#d4edda
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
subgraph LogoutFlow[LOGOUT FLOW]
|
|
1077
|
+
LO1([User Logout])
|
|
1078
|
+
LO2[session.logout or<br/>session.signOut]
|
|
1079
|
+
LO3{authToken<br/>exists?}
|
|
1080
|
+
LO4{shouldRedirectTo<br/>CentralAuth?}
|
|
1081
|
+
LO5[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
|
|
1082
|
+
LO6[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
|
|
1083
|
+
LO7[Clear session data:<br/>- authError = undefined<br/>- isLoading = false<br/>- authToken = undefined<br/>- typeRegistry = undefined]
|
|
1084
|
+
LO8[client.withAuthCallback<br/>undefined<br/><br/>Clear client auth]
|
|
1085
|
+
LO9[setSession this.clone<br/><br/>React re-render triggers<br/>SigninScreen display]
|
|
1086
|
+
|
|
1087
|
+
LO1 --> LO2 --> LO3
|
|
1088
|
+
LO3 -->|YES| LO4
|
|
1089
|
+
LO4 -->|YES| LO5
|
|
1090
|
+
LO4 -->|NO| LO6
|
|
1091
|
+
LO5 --> LO7
|
|
1092
|
+
LO6 --> LO7
|
|
1093
|
+
LO7 --> LO8 --> LO9
|
|
1094
|
+
end
|