Close the login loop end-to-end: client_id is the app actor uri, actor path serves the OIDC client document via a boot-safe service, startup heals the access-need-group link, stack aligned on pod provider v2.3.12
This commit is contained in:
@@ -13,23 +13,23 @@ test('clientId uses explicit backendClientId when set', () => {
|
||||
expect(cfg.redirectUri).toBe('http://localhost:3000/login');
|
||||
});
|
||||
|
||||
test('clientId derives from backendUrl when backendClientId is empty', () => {
|
||||
test('clientId derives the actor uri from backendUrl when backendClientId is empty', () => {
|
||||
setRuntimeConfig({ backendUrl: 'http://localhost:3003', backendClientId: '' });
|
||||
const cfg = resolveActivityPodsAuthConfig('http://localhost:5000');
|
||||
expect(cfg.clientId).toBe('http://localhost:3003/app');
|
||||
expect(cfg.clientId).toBe('http://localhost:3003/as/actor/app');
|
||||
expect(cfg.redirectUri).toBe('http://localhost:5000/login');
|
||||
});
|
||||
|
||||
test('clientId falls back to origin/app when nothing is configured', () => {
|
||||
test('clientId falls back to the origin actor uri when nothing is configured', () => {
|
||||
setRuntimeConfig({});
|
||||
const cfg = resolveActivityPodsAuthConfig('http://localhost:5000');
|
||||
expect(cfg.clientId).toBe('http://localhost:5000/app');
|
||||
expect(cfg.clientId).toBe('http://localhost:5000/as/actor/app');
|
||||
});
|
||||
|
||||
test('clientId does not double the slash on a trailing-slash backendUrl', () => {
|
||||
setRuntimeConfig({ backendUrl: 'http://localhost:3003/' });
|
||||
const cfg = resolveActivityPodsAuthConfig('http://localhost:5000');
|
||||
expect(cfg.clientId).toBe('http://localhost:3003/app');
|
||||
expect(cfg.clientId).toBe('http://localhost:3003/as/actor/app');
|
||||
});
|
||||
|
||||
test('podProviderUrl is passed through when set', () => {
|
||||
@@ -45,7 +45,7 @@ test('whitespace-only runtime values are treated as empty', () => {
|
||||
podProviderUrl: ' ',
|
||||
});
|
||||
const cfg = resolveActivityPodsAuthConfig('http://localhost:5000');
|
||||
expect(cfg.clientId).toBe('http://localhost:5000/app');
|
||||
expect(cfg.clientId).toBe('http://localhost:5000/as/actor/app');
|
||||
expect(cfg.podProviderUrl).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -109,17 +109,34 @@ export async function urlStatus(url: string): Promise<number> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Sign in to the local Pod provider and read the actor's outbox/storage. */
|
||||
/** Sign in to the local Pod provider and read the actor's outbox/storage.
|
||||
* On a fresh stack the account does not exist yet: `account.not-found` falls back
|
||||
* to the cookbook signup, then logs in — the smoke stays reproducible from a wipe. */
|
||||
export async function podLogin(
|
||||
config: ApodsStackConfig,
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<ApodsSession> {
|
||||
const login = await request(`${config.podProviderUrl}/auth/login`, {
|
||||
let login = await request(`${config.podProviderUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (login.status === 401 && login.text.includes('account.not-found')) {
|
||||
const signup = await request(`${config.podProviderUrl}/auth/signup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, email: `${username}@test.com`, password, name: username, 'schema:knowsLanguage': 'en' }),
|
||||
});
|
||||
if (signup.status !== 200) {
|
||||
throw new Error(`Pod signup failed: HTTP ${signup.status} ${signup.text.slice(0, 200)}`);
|
||||
}
|
||||
login = await request(`${config.podProviderUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
}
|
||||
if (login.status !== 200) {
|
||||
throw new Error(`Pod login failed: HTTP ${login.status} ${login.text.slice(0, 200)}`);
|
||||
}
|
||||
|
||||
@@ -39,17 +39,22 @@ export interface ResolvedAuthConfig {
|
||||
* Resolve the auth configuration from runtime values.
|
||||
*
|
||||
* - `backendClientId` wins when explicitly set.
|
||||
* - Otherwise `backendUrl` derives `${backendUrl}/app`.
|
||||
* - Otherwise fall back to `${origin}/app` so a page without a configured
|
||||
* - Otherwise `backendUrl` derives `${backendUrl}/as/actor/app` — the app ACTOR uri, and
|
||||
* this is not negotiable: the pod provider registers the app under its actor uri, and
|
||||
* the auth provider's `registerApp` discovers the registration through the pod
|
||||
* auth-agent's `Link: rel="interop:registeredAgent"` header, which the pod only returns
|
||||
* when the requesting app's identity (the token's azp, i.e. this client_id) matches the
|
||||
* registration. client_id and actor uri MUST be one and the same.
|
||||
* - Otherwise fall back to `${origin}/as/actor/app` so a page without a configured
|
||||
* backend still typechecks and can be exercised.
|
||||
*/
|
||||
export function resolveActivityPodsAuthConfig(origin: string): ResolvedAuthConfig {
|
||||
const backendClientId = runtimeConfig.backendClientId?.trim();
|
||||
// Strip trailing slashes first: appending `/app` to `http://host/` would double
|
||||
// the slash (`http://host//app`), and the client id must be the exact URI the
|
||||
// Strip trailing slashes first: appending a path to `http://host/` would double
|
||||
// the slash (`http://host//as/...`), and the client id must be the exact URI the
|
||||
// backend serves.
|
||||
const backendUrl = runtimeConfig.backendUrl?.trim().replace(/\/+$/, '');
|
||||
const clientId = backendClientId || (backendUrl ? `${backendUrl}/app` : `${origin}/app`);
|
||||
const clientId = backendClientId || (backendUrl ? `${backendUrl}/as/actor/app` : `${origin}/as/actor/app`);
|
||||
return {
|
||||
clientId,
|
||||
redirectUri: `${origin}/login`,
|
||||
|
||||
Reference in New Issue
Block a user