mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
fix(oauth): userinfo reports email_verified only for an address the account did not set itself
The userinfo endpoint answered email_verified: true for every account, although nobody had confirmed the address. It now follows users.email_self_asserted, the same mark the SSO login uses before it links an account by email.
This commit is contained in:
@@ -41,6 +41,7 @@ export interface OauthTokenWithUserRow {
|
||||
username: string;
|
||||
email: string;
|
||||
role: string;
|
||||
email_self_asserted: number;
|
||||
}
|
||||
|
||||
/** OA17 (`findChainRoot`'s upward-walk read). */
|
||||
@@ -268,6 +269,7 @@ export class OauthTokensRepository extends TrekRepository<OauthTokens> {
|
||||
'u.username',
|
||||
'u.email',
|
||||
'u.role',
|
||||
'u.email_self_asserted',
|
||||
])
|
||||
.where({ 'ot.access_token_hash': hash })
|
||||
.execute<OauthTokenWithUserRow | undefined>('get', false);
|
||||
|
||||
@@ -220,7 +220,7 @@ export class OauthPublicController {
|
||||
res.json({
|
||||
sub: String(info.user.id),
|
||||
email: info.user.email,
|
||||
email_verified: true,
|
||||
email_verified: info.emailVerified,
|
||||
preferred_username: info.user.username,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,6 +73,8 @@ function expiriesAsIso(row: { access_token_expires_at: string; refresh_token_exp
|
||||
|
||||
export interface OAuthTokenInfo {
|
||||
user: User;
|
||||
/** False when the account typed its email in itself; nobody has confirmed that address. */
|
||||
emailVerified: boolean;
|
||||
scopes: string[];
|
||||
clientId: string;
|
||||
audience: string | null;
|
||||
@@ -450,6 +452,7 @@ export class OauthService {
|
||||
|
||||
return {
|
||||
user: { id: row.user_id, username: row.username, email: row.email, role: row.role as 'admin' | 'user' },
|
||||
emailVerified: !row.email_self_asserted,
|
||||
scopes: decodeJson(OAUTH_TOKEN_SCOPES, row.scopes, `client ${row.client_id} user ${row.user_id}`),
|
||||
clientId: row.client_id,
|
||||
audience: row.audience ?? null,
|
||||
|
||||
@@ -428,12 +428,29 @@ describe('OauthPublicController /userinfo + /revoke', () => {
|
||||
expect(r1.headers['WWW-Authenticate']).toBe('Bearer realm="TREK MCP"');
|
||||
const r2 = makeRes();
|
||||
await opc(
|
||||
osvc({ getUserByAccessToken: vi.fn().mockReturnValue({ user: { id: 1, email: 'a@b.c', username: 'u' } }) }),
|
||||
osvc({
|
||||
getUserByAccessToken: vi
|
||||
.fn()
|
||||
.mockReturnValue({ user: { id: 1, email: 'a@b.c', username: 'u' }, emailVerified: true }),
|
||||
}),
|
||||
rl(),
|
||||
).userinfo('Bearer tok', r2);
|
||||
expect(r2.body).toEqual({ sub: '1', email: 'a@b.c', email_verified: true, preferred_username: 'u' });
|
||||
});
|
||||
|
||||
it('userinfo: an email the account typed in itself is not reported as verified', async () => {
|
||||
const res = makeRes();
|
||||
await opc(
|
||||
osvc({
|
||||
getUserByAccessToken: vi
|
||||
.fn()
|
||||
.mockReturnValue({ user: { id: 2, email: 'x@y.z', username: 'v' }, emailVerified: false }),
|
||||
}),
|
||||
rl(),
|
||||
).userinfo('Bearer tok', res);
|
||||
expect(res.body).toEqual({ sub: '2', email: 'x@y.z', email_verified: false, preferred_username: 'v' });
|
||||
});
|
||||
|
||||
it('userinfo: 404 empty when MCP is disabled', async () => {
|
||||
const res = makeRes();
|
||||
await opc(osvc({ mcpEnabled: vi.fn().mockReturnValue(false) }), rl()).userinfo('Bearer tok', res);
|
||||
|
||||
@@ -954,6 +954,19 @@ describe('getUserByAccessToken — includes clientId (C2)', () => {
|
||||
expect(info).not.toBeNull();
|
||||
expect(info!.clientId).toBe(clientId);
|
||||
});
|
||||
|
||||
it('reports the email as verified unless the account typed it in itself', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const created = await makeClient(user.id);
|
||||
const clientId = created.client!.client_id as string;
|
||||
|
||||
const first = await issueTokens(clientId, user.id, ['trips:read']);
|
||||
expect((await getUserByAccessToken(first.access_token))!.emailVerified).toBe(true);
|
||||
|
||||
await updateRows(t, Users, { id: user.id }, { email_self_asserted: 1 });
|
||||
const second = await issueTokens(clientId, user.id, ['trips:read']);
|
||||
expect((await getUserByAccessToken(second.access_token))!.emailVerified).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user