@startsimpli/auth 0.4.27 → 0.4.29
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/package.json
CHANGED
|
@@ -119,6 +119,54 @@ describe('CSRF not required for signin/register (endpoints are @csrf_exempt)', (
|
|
|
119
119
|
expect(headers['X-CSRFToken']).toBeUndefined();
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
it('signInWithCredentials threads the login target (app) into the body when provided (qxv2.5)', async () => {
|
|
123
|
+
mockFetch.mockImplementation(() =>
|
|
124
|
+
Promise.resolve({
|
|
125
|
+
ok: true,
|
|
126
|
+
status: 200,
|
|
127
|
+
json: async () => ({ access: VALID_TOKEN, user: { id: '1', email: 'a@b.com' } }),
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
await signInWithCredentials('test@test.com', 'password', 'foundry');
|
|
132
|
+
|
|
133
|
+
const tokenCall = mockFetch.mock.calls.find(
|
|
134
|
+
(c: unknown[]) =>
|
|
135
|
+
typeof c[0] === 'string' &&
|
|
136
|
+
(c[0] as string).includes('/auth/token/') &&
|
|
137
|
+
!(c[0] as string).includes('refresh'),
|
|
138
|
+
);
|
|
139
|
+
expect(tokenCall).toBeDefined();
|
|
140
|
+
expect(JSON.parse(tokenCall![1]!.body as string)).toEqual({
|
|
141
|
+
email: 'test@test.com',
|
|
142
|
+
password: 'password',
|
|
143
|
+
app: 'foundry',
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('signInWithCredentials omits app from the body when not provided (qxv2.5)', async () => {
|
|
148
|
+
mockFetch.mockImplementation(() =>
|
|
149
|
+
Promise.resolve({
|
|
150
|
+
ok: true,
|
|
151
|
+
status: 200,
|
|
152
|
+
json: async () => ({ access: VALID_TOKEN, user: { id: '1', email: 'a@b.com' } }),
|
|
153
|
+
}),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
await signInWithCredentials('test@test.com', 'password');
|
|
157
|
+
|
|
158
|
+
const tokenCall = mockFetch.mock.calls.find(
|
|
159
|
+
(c: unknown[]) =>
|
|
160
|
+
typeof c[0] === 'string' &&
|
|
161
|
+
(c[0] as string).includes('/auth/token/') &&
|
|
162
|
+
!(c[0] as string).includes('refresh'),
|
|
163
|
+
);
|
|
164
|
+
expect(JSON.parse(tokenCall![1]!.body as string)).toEqual({
|
|
165
|
+
email: 'test@test.com',
|
|
166
|
+
password: 'password',
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
122
170
|
it('registerAccount does not fetch or send CSRF token', async () => {
|
|
123
171
|
mockFetch.mockImplementation(() => {
|
|
124
172
|
return Promise.resolve({
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* loginAppHint() reads the signin target (?app=<slug>) from the URL so the
|
|
3
|
+
* login token POST can carry it — central auth scopes the minted token's
|
|
4
|
+
* audience by it (startsim-qxv2.5).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
7
|
+
import { loginAppHint } from '../client/functions';
|
|
8
|
+
|
|
9
|
+
function setUrl(search: string) {
|
|
10
|
+
window.history.pushState({}, '', `/signin${search}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('loginAppHint', () => {
|
|
14
|
+
afterEach(() => setUrl(''));
|
|
15
|
+
|
|
16
|
+
it('returns the app slug from ?app=', () => {
|
|
17
|
+
setUrl('?app=foundry&return_to=https%3A%2F%2Ffoundry.startsimpli.com%2F');
|
|
18
|
+
expect(loginAppHint()).toBe('foundry');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns the tenant slug for a tenant-app signin', () => {
|
|
22
|
+
setUrl('?app=mcr');
|
|
23
|
+
expect(loginAppHint()).toBe('mcr');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns undefined when no app param is present', () => {
|
|
27
|
+
setUrl('?return_to=https%3A%2F%2Fexample.com');
|
|
28
|
+
expect(loginAppHint()).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('treats a blank app as absent', () => {
|
|
32
|
+
setUrl('?app=');
|
|
33
|
+
expect(loginAppHint()).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
AuthUser,
|
|
12
12
|
} from '../types';
|
|
13
13
|
import { isTokenExpired, getTokenExpiresAt, shouldRefreshToken } from '../utils';
|
|
14
|
-
import { extractApiError, setAccessToken as setModuleAccessToken } from './functions';
|
|
14
|
+
import { extractApiError, loginAppHint, setAccessToken as setModuleAccessToken } from './functions';
|
|
15
15
|
import { deleteCookie } from '../utils/cookies';
|
|
16
16
|
import type { AuthBackend, RegisterPayload } from './backend';
|
|
17
17
|
|
|
@@ -68,11 +68,12 @@ export class AuthClient implements AuthBackend {
|
|
|
68
68
|
* Login with email and password
|
|
69
69
|
*/
|
|
70
70
|
async login(email: string, password: string): Promise<Session> {
|
|
71
|
+
const app = loginAppHint(); // scope the token audience to the login target (qxv2.5)
|
|
71
72
|
const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/token/`, {
|
|
72
73
|
method: 'POST',
|
|
73
74
|
headers: { 'Content-Type': 'application/json' },
|
|
74
75
|
credentials: 'include', // Include cookies for refresh token
|
|
75
|
-
body: JSON.stringify({ email, password }),
|
|
76
|
+
body: JSON.stringify({ email, password, ...(app ? { app } : {}) }),
|
|
76
77
|
});
|
|
77
78
|
|
|
78
79
|
if (!response.ok) {
|
package/src/client/functions.ts
CHANGED
|
@@ -59,6 +59,19 @@ export function notifySessionExpired(): void {
|
|
|
59
59
|
_onSessionExpired();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The app/target the user is signing in FOR, read from the signin URL
|
|
64
|
+
* (?app=<slug>). Central auth uses it to scope the minted token's audience:
|
|
65
|
+
* a central/platform login (app=foundry) yields a first-party token; a tenant
|
|
66
|
+
* login (app=<tenant>) yields the tenant audience. Browser-only (undefined
|
|
67
|
+
* off-browser, e.g. SSR). (startsim-qxv2.5)
|
|
68
|
+
*/
|
|
69
|
+
export function loginAppHint(): string | undefined {
|
|
70
|
+
if (typeof window === 'undefined') return undefined;
|
|
71
|
+
const app = new URLSearchParams(window.location.search).get('app');
|
|
72
|
+
return app?.trim() || undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
// --- Endpoint paths (Django backend defaults) ---
|
|
63
76
|
|
|
64
77
|
const API_BASE = '/api/v1';
|
|
@@ -317,7 +330,10 @@ function parseAuthResponse(data: unknown): { access?: string; user?: AuthUser }
|
|
|
317
330
|
|
|
318
331
|
// --- Auth functions ---
|
|
319
332
|
|
|
320
|
-
export async function signInWithCredentials(email: string, password: string) {
|
|
333
|
+
export async function signInWithCredentials(email: string, password: string, app?: string) {
|
|
334
|
+
// `app` is the login target (qxv2.5): the central mint scopes the token's
|
|
335
|
+
// audience to it (control plane => first-party for a foundry owner; tenant =>
|
|
336
|
+
// tenant aud). Server-validated; omitted => current home-org behavior.
|
|
321
337
|
// No CSRF needed — Django's /auth/token/ is @csrf_exempt (JWT endpoint)
|
|
322
338
|
const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.TOKEN), {
|
|
323
339
|
method: 'POST',
|
|
@@ -325,7 +341,7 @@ export async function signInWithCredentials(email: string, password: string) {
|
|
|
325
341
|
'Content-Type': 'application/json',
|
|
326
342
|
},
|
|
327
343
|
credentials: 'include',
|
|
328
|
-
body: JSON.stringify({ email, password }),
|
|
344
|
+
body: JSON.stringify(app ? { email, password, app } : { email, password }),
|
|
329
345
|
});
|
|
330
346
|
|
|
331
347
|
const data = await response.json().catch(() => ({}));
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import type { AuthUser, Session } from '../types';
|
|
21
21
|
import { getTokenExpiresAt } from '../utils/token';
|
|
22
22
|
import { extractApiError } from '../utils/api-error';
|
|
23
|
+
import { loginAppHint } from './functions';
|
|
23
24
|
|
|
24
25
|
/** Persistence for the refresh token. Web uses cookies (and never needs this);
|
|
25
26
|
* native provides a SecureStore-backed implementation. */
|
|
@@ -89,10 +90,11 @@ export class TokenAuthClient {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
async login(email: string, password: string): Promise<Session> {
|
|
93
|
+
const app = loginAppHint(); // scope the token audience to the login target (qxv2.5)
|
|
92
94
|
const response = await this.fetchImpl(`${this.apiBaseUrl}/api/v1/auth/token/`, {
|
|
93
95
|
method: 'POST',
|
|
94
96
|
headers: { 'Content-Type': 'application/json', 'X-Auth-Mode': 'token' },
|
|
95
|
-
body: JSON.stringify({ email, password }),
|
|
97
|
+
body: JSON.stringify({ email, password, ...(app ? { app } : {}) }),
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
if (!response.ok) {
|