Go straight to the server's sign-in page, with no account-name step

INBUXA Admin talks to one known server whose own page signs in every account,
so it reads the server's OpenID configuration instead of looking endpoints up
per account. The login card now only shows while redirecting, or to retry
when that fails.
This commit is contained in:
2026-09-18 15:50:10 -07:00
parent 2399f5ce97
commit 14d708ecf8
3 changed files with 99 additions and 52 deletions
+47
View File
@@ -102,3 +102,50 @@ describe('generateCodeChallenge', () => {
}
});
});
describe('startAuthFlow (INBUXA: no account-name step)', () => {
const metadata = {
authorization_endpoint: '/login',
token_endpoint: '/auth/token',
scopes_supported: ['openid', 'offline_access'],
};
async function run(username: string | null) {
document.head.innerHTML = '<meta name="api-base-url" content="https://mail.example.org" />';
const requested: string[] = [];
const fetchMock = async (url: string) => {
requested.push(url);
return new Response(JSON.stringify(metadata), { status: 200 });
};
const originalFetch = globalThis.fetch;
globalThis.fetch = fetchMock as typeof fetch;
let href = '';
const originalLocation = window.location;
Object.defineProperty(window, 'location', {
configurable: true,
value: { ...originalLocation, origin: 'https://admin.example.org', pathname: '/', search: '', set href(v: string) { href = v; }, get href() { return href; } },
});
try {
const { startAuthFlow } = await import('./oauth');
await startAuthFlow(username);
} finally {
globalThis.fetch = originalFetch;
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
}
return { requested, target: new URL(href) };
}
it('reads the server-wide configuration and goes straight to its sign-in page', async () => {
const { requested, target } = await run(null);
expect(requested).toEqual(['https://mail.example.org/.well-known/openid-configuration']);
expect(`${target.origin}${target.pathname}`).toBe('https://mail.example.org/login');
expect(target.searchParams.has('login_hint')).toBe(false);
expect(target.searchParams.get('redirect_uri')).toBe('https://admin.example.org/oauth/callback');
});
it('still looks an account up, and passes the hint, when given one', async () => {
const { requested, target } = await run('[email protected]');
expect(requested).toEqual(['https://mail.example.org/api/discover/someone%40example.org']);
expect(target.searchParams.get('login_hint')).toBe('[email protected]');
});
});
+18 -5
View File
@@ -20,13 +20,24 @@ interface DiscoveryResponse {
scopes_supported?: string[];
}
export async function discover(username: string): Promise<DiscoveryResponse> {
const url = `${getApiBaseUrl()}/api/discover/${encodeURIComponent(username)}`;
/**
* The server's OAuth endpoints.
*
* Upstream asks for the account name first and looks the endpoints up per
* account, because a domain there can sign in at its own identity provider.
* INBUXA Admin talks to one known server whose own sign-in page handles every
* account, so it reads the server's published OpenID configuration instead,
* which needs no account name. Given one, it still asks per account.
*/
export async function discover(username?: string | null): Promise<DiscoveryResponse> {
const url = username
? `${getApiBaseUrl()}/api/discover/${encodeURIComponent(username)}`
: `${getApiBaseUrl()}/.well-known/openid-configuration`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(
i18n.t('oauth.discoveryFailed', 'Discovery failed for "{{username}}": {{status}} {{statusText}}', {
username,
username: username ?? getApiBaseUrl(),
status: response.status,
statusText: response.statusText,
}),
@@ -136,7 +147,7 @@ function getRedirectUri(): string {
return `${window.location.origin}${basePath}/oauth/callback`;
}
export async function startAuthFlow(username: string, returnUrl?: string | null): Promise<void> {
export async function startAuthFlow(username: string | null, returnUrl?: string | null): Promise<void> {
const { authorization_endpoint, token_endpoint, end_session_endpoint, scopes_supported } = await discover(username);
const codeVerifier = generateCodeVerifier();
@@ -170,9 +181,11 @@ export async function startAuthFlow(username: string, returnUrl?: string | null)
code_challenge: codeChallenge,
code_challenge_method: codeChallengeMethod,
state,
login_hint: username,
prompt: 'login',
});
if (username) {
params.set('login_hint', username);
}
let scope: string;
if (SCOPES && SCOPES.length > 0) {