feat(thalos-web): harden production auth experience
This commit is contained in:
parent
24bcf71048
commit
39f5af23e4
@ -6,6 +6,7 @@ window.__APP_CONFIG__ = {
|
|||||||
API_BASE_URL: "${API_BASE_URL:-http://localhost:8080}",
|
API_BASE_URL: "${API_BASE_URL:-http://localhost:8080}",
|
||||||
THALOS_AUTH_BASE_URL: "${THALOS_AUTH_BASE_URL:-${API_BASE_URL:-http://localhost:8080}}",
|
THALOS_AUTH_BASE_URL: "${THALOS_AUTH_BASE_URL:-${API_BASE_URL:-http://localhost:8080}}",
|
||||||
THALOS_DEFAULT_RETURN_URL: "${THALOS_DEFAULT_RETURN_URL:-http://localhost:22080/callback}",
|
THALOS_DEFAULT_RETURN_URL: "${THALOS_DEFAULT_RETURN_URL:-http://localhost:22080/callback}",
|
||||||
THALOS_DEFAULT_TENANT_ID: "${THALOS_DEFAULT_TENANT_ID:-demo-tenant}"
|
THALOS_DEFAULT_TENANT_ID: "${THALOS_DEFAULT_TENANT_ID:-demo-tenant}",
|
||||||
|
THALOS_ENABLE_MANUAL_LOGIN: "${THALOS_ENABLE_MANUAL_LOGIN:-false}"
|
||||||
};
|
};
|
||||||
EOT
|
EOT
|
||||||
|
|||||||
@ -12,6 +12,7 @@
|
|||||||
- `THALOS_AUTH_BASE_URL` for session and OIDC endpoints.
|
- `THALOS_AUTH_BASE_URL` for session and OIDC endpoints.
|
||||||
- `THALOS_DEFAULT_RETURN_URL` for callback fallback.
|
- `THALOS_DEFAULT_RETURN_URL` for callback fallback.
|
||||||
- `THALOS_DEFAULT_TENANT_ID` for OIDC tenant defaults.
|
- `THALOS_DEFAULT_TENANT_ID` for OIDC tenant defaults.
|
||||||
|
- `THALOS_ENABLE_MANUAL_LOGIN` for explicitly enabling the dev/test fallback form.
|
||||||
|
|
||||||
## Protected Workflow Endpoints
|
## Protected Workflow Endpoints
|
||||||
|
|
||||||
@ -27,4 +28,4 @@
|
|||||||
- Central login launch (Google OIDC start)
|
- Central login launch (Google OIDC start)
|
||||||
- Callback processing and error rendering
|
- Callback processing and error rendering
|
||||||
- Session workspace verification and snapshot reload
|
- Session workspace verification and snapshot reload
|
||||||
- Manual dev/test session login fallback
|
- Manual dev/test session login fallback gated by environment/runtime config
|
||||||
|
|||||||
@ -14,6 +14,7 @@ docker run --rm -p 8080:8080 \
|
|||||||
-e THALOS_AUTH_BASE_URL=http://host.docker.internal:22080 \
|
-e THALOS_AUTH_BASE_URL=http://host.docker.internal:22080 \
|
||||||
-e THALOS_DEFAULT_RETURN_URL=http://localhost:22080/callback \
|
-e THALOS_DEFAULT_RETURN_URL=http://localhost:22080/callback \
|
||||||
-e THALOS_DEFAULT_TENANT_ID=demo-tenant \
|
-e THALOS_DEFAULT_TENANT_ID=demo-tenant \
|
||||||
|
-e THALOS_ENABLE_MANUAL_LOGIN=false \
|
||||||
--name thalos-web agilewebs/thalos-web:dev
|
--name thalos-web agilewebs/thalos-web:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ docker run --rm -p 8080:8080 \
|
|||||||
- Runtime override: container env `API_BASE_URL`
|
- Runtime override: container env `API_BASE_URL`
|
||||||
- Runtime file generated at startup: `/runtime-config.js`
|
- Runtime file generated at startup: `/runtime-config.js`
|
||||||
- OIDC callback defaults are injected through runtime env vars, not hardcoded per build.
|
- OIDC callback defaults are injected through runtime env vars, not hardcoded per build.
|
||||||
|
- Manual fallback login should remain disabled for deployed auth hosts unless an explicit dev/test override is required.
|
||||||
|
|
||||||
## Health Check
|
## Health Check
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ VITE_API_BASE_URL=http://localhost:8080 \
|
|||||||
VITE_THALOS_AUTH_BASE_URL=http://localhost:20080 \
|
VITE_THALOS_AUTH_BASE_URL=http://localhost:20080 \
|
||||||
VITE_THALOS_DEFAULT_RETURN_URL=http://localhost:22080/callback \
|
VITE_THALOS_DEFAULT_RETURN_URL=http://localhost:22080/callback \
|
||||||
VITE_THALOS_DEFAULT_TENANT_ID=demo-tenant \
|
VITE_THALOS_DEFAULT_TENANT_ID=demo-tenant \
|
||||||
|
VITE_THALOS_ENABLE_MANUAL_LOGIN=true \
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ npm run dev
|
|||||||
- Central login starts via `GET /api/identity/oidc/google/start`.
|
- Central login starts via `GET /api/identity/oidc/google/start`.
|
||||||
- Callback route validates query parameters and resolves session by calling refresh/me endpoints.
|
- Callback route validates query parameters and resolves session by calling refresh/me endpoints.
|
||||||
- Session cookies are sent with `credentials: include`.
|
- Session cookies are sent with `credentials: include`.
|
||||||
|
- Manual session login is available locally by setting `VITE_THALOS_ENABLE_MANUAL_LOGIN=true`.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
|||||||
@ -2,5 +2,6 @@ window.__APP_CONFIG__ = {
|
|||||||
API_BASE_URL: "http://localhost:8080",
|
API_BASE_URL: "http://localhost:8080",
|
||||||
THALOS_AUTH_BASE_URL: "http://localhost:20080",
|
THALOS_AUTH_BASE_URL: "http://localhost:20080",
|
||||||
THALOS_DEFAULT_RETURN_URL: "http://localhost:22080/callback",
|
THALOS_DEFAULT_RETURN_URL: "http://localhost:22080/callback",
|
||||||
THALOS_DEFAULT_TENANT_ID: "demo-tenant"
|
THALOS_DEFAULT_TENANT_ID: "demo-tenant",
|
||||||
|
THALOS_ENABLE_MANUAL_LOGIN: "true"
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,7 +25,8 @@ describe('Thalos App', () => {
|
|||||||
API_BASE_URL: 'http://localhost:8080',
|
API_BASE_URL: 'http://localhost:8080',
|
||||||
THALOS_AUTH_BASE_URL: 'https://auth.dream-views.com',
|
THALOS_AUTH_BASE_URL: 'https://auth.dream-views.com',
|
||||||
THALOS_DEFAULT_RETURN_URL: `${window.location.origin}/callback`,
|
THALOS_DEFAULT_RETURN_URL: `${window.location.origin}/callback`,
|
||||||
THALOS_DEFAULT_TENANT_ID: 'demo-tenant'
|
THALOS_DEFAULT_TENANT_ID: 'demo-tenant',
|
||||||
|
THALOS_ENABLE_MANUAL_LOGIN: 'false'
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -96,4 +97,25 @@ describe('Thalos App', () => {
|
|||||||
await waitFor(() => expect(screen.getByText('User cancelled')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('User cancelled')).toBeInTheDocument());
|
||||||
expect(refreshSession).not.toHaveBeenCalled();
|
expect(refreshSession).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides manual fallback on production-style auth host config', async () => {
|
||||||
|
vi.mocked(getSessionMe).mockResolvedValue({ isAuthenticated: false, subjectId: '', tenantId: '', provider: 0 });
|
||||||
|
window.history.pushState({}, '', '/login');
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText('Sign in required')).toBeInTheDocument());
|
||||||
|
expect(screen.queryByText('Manual Session Login (Dev/Test)')).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Manual session login is disabled in this environment. Use the Google sign-in path above.')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows correlation id when callback returns bff auth error context', async () => {
|
||||||
|
vi.mocked(getSessionMe).mockResolvedValue({ isAuthenticated: false, subjectId: '', tenantId: '', provider: 0 });
|
||||||
|
window.history.pushState({}, '', '/callback?authError=oidc_exchange_failed&correlationId=corr-123');
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(screen.getByText('The sign-in code could not be exchanged. Please retry the login flow.')).toBeInTheDocument());
|
||||||
|
expect(screen.getByText('Correlation ID: corr-123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
81
src/App.tsx
81
src/App.tsx
@ -18,7 +18,7 @@ import {
|
|||||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||||
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { loadDashboard } from './api/dashboardApi';
|
import { loadDashboard } from './api/dashboardApi';
|
||||||
import { getThalosDefaultReturnUrl } from './api/client';
|
import { getThalosDefaultReturnUrl, isThalosManualLoginEnabled } from './api/client';
|
||||||
import type { IdentityProvider } from './api/sessionApi';
|
import type { IdentityProvider } from './api/sessionApi';
|
||||||
import { parseOidcCallbackState } from './auth/callbackState';
|
import { parseOidcCallbackState } from './auth/callbackState';
|
||||||
import { buildGoogleOidcStartUrl } from './auth/oidcLogin';
|
import { buildGoogleOidcStartUrl } from './auth/oidcLogin';
|
||||||
@ -99,6 +99,7 @@ function ThalosShell() {
|
|||||||
|
|
||||||
function LoginRoute() {
|
function LoginRoute() {
|
||||||
const session = useSessionContext();
|
const session = useSessionContext();
|
||||||
|
const manualLoginEnabled = useMemo(() => isThalosManualLoginEnabled(), []);
|
||||||
const [subjectId, setSubjectId] = useState('demo-user');
|
const [subjectId, setSubjectId] = useState('demo-user');
|
||||||
const [tenantId, setTenantId] = useState('demo-tenant');
|
const [tenantId, setTenantId] = useState('demo-tenant');
|
||||||
const [provider, setProvider] = useState<IdentityProvider>(0);
|
const [provider, setProvider] = useState<IdentityProvider>(0);
|
||||||
@ -129,7 +130,7 @@ function LoginRoute() {
|
|||||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||||
<Typography.Title level={3}>Thalos Authentication</Typography.Title>
|
<Typography.Title level={3}>Thalos Authentication</Typography.Title>
|
||||||
<Typography.Paragraph type="secondary">
|
<Typography.Paragraph type="secondary">
|
||||||
Use the central OIDC journey for browser login. Manual form login remains available for local simulated-provider testing.
|
Use the central OIDC journey for browser login. Manual form login is only exposed for local and explicitly enabled dev/test environments.
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
@ -145,36 +146,49 @@ function LoginRoute() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card title="Manual Session Login (Dev/Test)">
|
{manualLoginEnabled ? (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Card title="Manual Session Login (Dev/Test)">
|
||||||
<Form layout="vertical" onFinish={() => void onSubmit()}>
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
<Form.Item label="Subject Id" required>
|
<Alert
|
||||||
<Input value={subjectId} onChange={(event) => setSubjectId(event.target.value)} />
|
type="info"
|
||||||
</Form.Item>
|
showIcon
|
||||||
<Form.Item label="Tenant Id" required>
|
message="This fallback is intended for local development, simulated-provider testing, and operator troubleshooting only."
|
||||||
<Input value={tenantId} onChange={(event) => setTenantId(event.target.value)} />
|
/>
|
||||||
</Form.Item>
|
<Form layout="vertical" onFinish={() => void onSubmit()}>
|
||||||
<Form.Item label="Provider" required>
|
<Form.Item label="Subject Id" required>
|
||||||
<Select
|
<Input value={subjectId} onChange={(event) => setSubjectId(event.target.value)} />
|
||||||
value={provider}
|
</Form.Item>
|
||||||
onChange={(value) => setProvider(value as IdentityProvider)}
|
<Form.Item label="Tenant Id" required>
|
||||||
options={[
|
<Input value={tenantId} onChange={(event) => setTenantId(event.target.value)} />
|
||||||
{ value: 0, label: 'Internal JWT' },
|
</Form.Item>
|
||||||
{ value: 1, label: 'Azure AD (simulated)' },
|
<Form.Item label="Provider" required>
|
||||||
{ value: 2, label: 'Google (simulated)' }
|
<Select
|
||||||
]}
|
value={provider}
|
||||||
/>
|
onChange={(value) => setProvider(value as IdentityProvider)}
|
||||||
</Form.Item>
|
options={[
|
||||||
<Form.Item label="External Token (optional)">
|
{ value: 0, label: 'Internal JWT' },
|
||||||
<Input value={externalToken} onChange={(event) => setExternalToken(event.target.value)} />
|
{ value: 1, label: 'Azure AD (simulated)' },
|
||||||
</Form.Item>
|
{ value: 2, label: 'Google (simulated)' }
|
||||||
<Button type="primary" htmlType="submit" loading={submitting}>
|
]}
|
||||||
Sign In
|
/>
|
||||||
</Button>
|
</Form.Item>
|
||||||
</Form>
|
<Form.Item label="External Token (optional)">
|
||||||
{error && <Alert type="error" showIcon message={error} />}
|
<Input value={externalToken} onChange={(event) => setExternalToken(event.target.value)} />
|
||||||
</Space>
|
</Form.Item>
|
||||||
</Card>
|
<Button type="primary" htmlType="submit" loading={submitting}>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
{error && <Alert type="error" showIcon message={error} />}
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Alert
|
||||||
|
type="info"
|
||||||
|
showIcon
|
||||||
|
message="Manual session login is disabled in this environment. Use the Google sign-in path above."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -237,6 +251,9 @@ function CallbackRoute() {
|
|||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
<Alert type="error" showIcon message={message ?? 'Callback failed.'} />
|
<Alert type="error" showIcon message={message ?? 'Callback failed.'} />
|
||||||
|
{callback.kind === 'error' && callback.correlationId ? (
|
||||||
|
<Typography.Text type="secondary">Correlation ID: {callback.correlationId}</Typography.Text>
|
||||||
|
) : null}
|
||||||
<Button type="primary" onClick={() => navigate('/login', { replace: true })}>
|
<Button type="primary" onClick={() => navigate('/login', { replace: true })}>
|
||||||
Back to login
|
Back to login
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { getApiBaseUrl, getThalosAuthBaseUrl, getThalosDefaultReturnUrl, getThalosDefaultTenantId } from './client';
|
import {
|
||||||
|
getApiBaseUrl,
|
||||||
|
getThalosAuthBaseUrl,
|
||||||
|
getThalosDefaultReturnUrl,
|
||||||
|
getThalosDefaultTenantId,
|
||||||
|
isThalosManualLoginEnabled
|
||||||
|
} from './client';
|
||||||
|
|
||||||
describe('client runtime base URLs', () => {
|
describe('client runtime base URLs', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -62,4 +68,18 @@ describe('client runtime base URLs', () => {
|
|||||||
it('falls back to demo tenant when no tenant is configured', () => {
|
it('falls back to demo tenant when no tenant is configured', () => {
|
||||||
expect(getThalosDefaultTenantId()).toBe('demo-tenant');
|
expect(getThalosDefaultTenantId()).toBe('demo-tenant');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses runtime-config manual-login toggle when present', () => {
|
||||||
|
window.__APP_CONFIG__ = {
|
||||||
|
THALOS_ENABLE_MANUAL_LOGIN: 'true'
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(isThalosManualLoginEnabled()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to env manual-login toggle when runtime config is missing', () => {
|
||||||
|
vi.stubEnv('VITE_THALOS_ENABLE_MANUAL_LOGIN', 'false');
|
||||||
|
|
||||||
|
expect(isThalosManualLoginEnabled()).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,6 +5,7 @@ declare global {
|
|||||||
THALOS_AUTH_BASE_URL?: string;
|
THALOS_AUTH_BASE_URL?: string;
|
||||||
THALOS_DEFAULT_RETURN_URL?: string;
|
THALOS_DEFAULT_RETURN_URL?: string;
|
||||||
THALOS_DEFAULT_TENANT_ID?: string;
|
THALOS_DEFAULT_TENANT_ID?: string;
|
||||||
|
THALOS_ENABLE_MANUAL_LOGIN?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,6 +68,20 @@ export function getThalosDefaultTenantId(): string {
|
|||||||
return import.meta.env.VITE_THALOS_DEFAULT_TENANT_ID ?? 'demo-tenant';
|
return import.meta.env.VITE_THALOS_DEFAULT_TENANT_ID ?? 'demo-tenant';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isThalosManualLoginEnabled(): boolean {
|
||||||
|
const runtimeValue = parseBoolean(window.__APP_CONFIG__?.THALOS_ENABLE_MANUAL_LOGIN);
|
||||||
|
if (runtimeValue !== undefined) {
|
||||||
|
return runtimeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const envValue = parseBoolean(import.meta.env.VITE_THALOS_ENABLE_MANUAL_LOGIN);
|
||||||
|
if (envValue !== undefined) {
|
||||||
|
return envValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||||||
|
}
|
||||||
|
|
||||||
export async function getJson<T>(path: string, baseUrl = getApiBaseUrl()): Promise<T> {
|
export async function getJson<T>(path: string, baseUrl = getApiBaseUrl()): Promise<T> {
|
||||||
return requestJson<T>(baseUrl, path, { method: 'GET' });
|
return requestJson<T>(baseUrl, path, { method: 'GET' });
|
||||||
}
|
}
|
||||||
@ -142,3 +157,24 @@ function createCorrelationId(): string {
|
|||||||
|
|
||||||
return `corr-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
return `corr-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBoolean(value: string | boolean | undefined): boolean | undefined {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (normalized === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized === 'false') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,16 @@ describe('callback state parser', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns auth error state when bff redirects with authError query values', () => {
|
||||||
|
const result = parseOidcCallbackState('?authError=oidc_exchange_failed&correlationId=corr-123');
|
||||||
|
|
||||||
|
expect(result.kind).toBe('error');
|
||||||
|
if (result.kind === 'error') {
|
||||||
|
expect(result.message).toBe('The sign-in code could not be exchanged. Please retry the login flow.');
|
||||||
|
expect(result.correlationId).toBe('corr-123');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('returns success state with sanitized same-origin return path', () => {
|
it('returns success state with sanitized same-origin return path', () => {
|
||||||
const result = parseOidcCallbackState(`?returnUrl=${encodeURIComponent(`${window.location.origin}/session?tab=profile`)}`);
|
const result = parseOidcCallbackState(`?returnUrl=${encodeURIComponent(`${window.location.origin}/session?tab=profile`)}`);
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ type OidcCallbackSuccess = {
|
|||||||
type OidcCallbackError = {
|
type OidcCallbackError = {
|
||||||
kind: 'error';
|
kind: 'error';
|
||||||
message: string;
|
message: string;
|
||||||
|
correlationId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OidcCallbackState = OidcCallbackSuccess | OidcCallbackError;
|
export type OidcCallbackState = OidcCallbackSuccess | OidcCallbackError;
|
||||||
@ -16,11 +17,22 @@ export function parseOidcCallbackState(search: string): OidcCallbackState {
|
|||||||
const query = new URLSearchParams(search);
|
const query = new URLSearchParams(search);
|
||||||
const error = query.get('error');
|
const error = query.get('error');
|
||||||
const errorDescription = query.get('error_description');
|
const errorDescription = query.get('error_description');
|
||||||
|
const authError = query.get('authError');
|
||||||
|
const correlationId = query.get('correlationId') ?? undefined;
|
||||||
|
|
||||||
if (error && error.length > 0) {
|
if (error && error.length > 0) {
|
||||||
return {
|
return {
|
||||||
kind: 'error',
|
kind: 'error',
|
||||||
message: errorDescription && errorDescription.length > 0 ? errorDescription : `OIDC callback failed: ${error}`
|
message: errorDescription && errorDescription.length > 0 ? errorDescription : `OIDC callback failed: ${error}`,
|
||||||
|
correlationId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authError && authError.length > 0) {
|
||||||
|
return {
|
||||||
|
kind: 'error',
|
||||||
|
message: getAuthErrorMessage(authError),
|
||||||
|
correlationId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,6 +43,23 @@ export function parseOidcCallbackState(search: string): OidcCallbackState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAuthErrorMessage(authError: string): string {
|
||||||
|
switch (authError) {
|
||||||
|
case 'oidc_provider_error':
|
||||||
|
return 'The identity provider rejected the sign-in request.';
|
||||||
|
case 'oidc_state_invalid':
|
||||||
|
return 'The sign-in session is no longer valid. Please start again.';
|
||||||
|
case 'oidc_code_missing':
|
||||||
|
return 'The identity provider did not return an authorization code.';
|
||||||
|
case 'oidc_exchange_failed':
|
||||||
|
return 'The sign-in code could not be exchanged. Please retry the login flow.';
|
||||||
|
case 'session_login_failed':
|
||||||
|
return 'The session could not be created after authentication.';
|
||||||
|
default:
|
||||||
|
return `Authentication failed: ${authError}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeReturnPath(rawReturnUrl: string | null): string {
|
function sanitizeReturnPath(rawReturnUrl: string | null): string {
|
||||||
if (!rawReturnUrl || rawReturnUrl.length === 0) {
|
if (!rawReturnUrl || rawReturnUrl.length === 0) {
|
||||||
return '/session';
|
return '/session';
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
{"root":["./src/App.test.tsx","./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.test.ts","./src/api/client.ts","./src/api/dashboardApi.test.ts","./src/api/dashboardApi.ts","./src/test/setup.ts"],"version":"5.9.3"}
|
{"root":["./src/App.test.tsx","./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.test.ts","./src/api/client.ts","./src/api/dashboardApi.test.ts","./src/api/dashboardApi.ts","./src/api/sessionApi.ts","./src/auth/callbackState.test.ts","./src/auth/callbackState.ts","./src/auth/oidcLogin.test.ts","./src/auth/oidcLogin.ts","./src/auth/sessionContext.tsx","./src/test/setup.ts"],"version":"5.9.3"}
|
||||||
Loading…
Reference in New Issue
Block a user