mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
The OIDC callback required `claims.email_verified === true`. Entra ID v2 does not send that claim at all, so every Microsoft login authenticated correctly against the tenant and was then refused with `email_unverified` on the way back. Nothing caught it: the SSO tests assert how the Microsoft issuer string is built but never put a Microsoft-shaped token through the policy. The strict check was itself a fix -- `=== false` had been accepting an omitted claim -- and it is right for a provider a CUSTOMER configured, since such a provider is chosen by the party it vouches for and its bare assertion is worth nothing. What was wrong is treating that as a question about the token when it is a question about who we trusted. `users.email_verified` is our own state; the claim is the IdP's. An instance-wide provider was chosen by the operator -- the same trust that already exempts it from domain confinement -- and Microsoft is additionally pinned to one tenant GUID, so only that directory can issue a token whose `iss` matches. So the policy now depends on the provider, in emailIsVerified(), next to the flag it reads so the two cannot drift: - explicit true -> believed, from anyone - claim absent, operator -> believed (Microsoft; opt-in for other IdPs) - claim absent, org -> refused - explicit false -> refused, always Org providers pin the flag false in rowToProvider and never read it from the row, so the takeover path the strict check existed to close stays closed. Google is left strict: it does send the claim. Also documents MICROSOFT_CLIENT_SECRET (supported in code, missing from the table), that the redirect URI must be registered under Web rather than SPA, and the email optional claim -- the other two ways an Entra setup fails. All three mutations of this policy fail the new tests: reinstating the strict check (3 failures), letting an org provider assume (1), and accepting an explicit false (2).
78 lines
4.1 KiB
JavaScript
78 lines
4.1 KiB
JavaScript
'use strict';
|
|
|
|
/*
|
|
* Who may be believed about an email address.
|
|
*
|
|
* Regression: requiring `claims.email_verified === true` made Microsoft sign-in impossible, because
|
|
* Entra ID v2 does not send the claim. Every Entra login authenticated and was then refused with
|
|
* `email_unverified`. The previous test suite asserted how the Microsoft ISSUER string is built but
|
|
* never pushed a Microsoft-shaped token through the policy, so nothing failed.
|
|
*
|
|
* The rule these tests pin down:
|
|
* - `email_verified: true` -> believed, always
|
|
* - claim ABSENT + operator-chosen -> believed (Microsoft, or an opted-in generic provider)
|
|
* - claim ABSENT + org-configured -> refused
|
|
* - `email_verified: false` -> refused, whoever asked
|
|
*/
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { emailIsVerified, list } = require('../lib/oidc-providers');
|
|
|
|
const MS_ENV = { MICROSOFT_CLIENT_ID: 'client-abc', MICROSOFT_TENANT_ID: 'ffffffff-1111-2222-3333-444444444444' };
|
|
const microsoft = () => list(MS_ENV).find((p) => p.slug === 'microsoft');
|
|
const google = () => list({ GOOGLE_CLIENT_ID: 'g-abc' }).find((p) => p.slug === 'google');
|
|
const orgProvider = { slug: 'acme7f3', source: 'org', organizationId: 'org-1', assumeEmailVerified: false };
|
|
|
|
test('an explicit true is believed from any provider', () => {
|
|
for (const p of [microsoft(), google(), orgProvider]) {
|
|
assert.equal(emailIsVerified({ email_verified: true }, p), true, `${p.slug} should accept an explicit true`);
|
|
}
|
|
});
|
|
|
|
test('Microsoft omits the claim and is still believed (the regression)', () => {
|
|
const ms = microsoft();
|
|
assert.equal(ms.assumeEmailVerified, true, 'the tenant-pinned Microsoft entry must assume verification');
|
|
assert.equal(emailIsVerified({ email: 'someone@example.com' }, ms), true);
|
|
});
|
|
|
|
test('Google stays strict — it does send the claim, so there is nothing to assume', () => {
|
|
const g = google();
|
|
assert.equal(g.assumeEmailVerified, false);
|
|
assert.equal(emailIsVerified({ email: 'someone@example.com' }, g), false);
|
|
});
|
|
|
|
test('an ORG-configured provider may never assume, even if the object claims it can', () => {
|
|
assert.equal(emailIsVerified({ email: 'a@b.c' }, orgProvider), false);
|
|
// Belt and braces: a tampered/hand-built org object must not be able to opt itself in through
|
|
// the database, which is why rowToProvider pins the field rather than reading a column.
|
|
const src = require('fs').readFileSync(require.resolve('../lib/oidc-providers'), 'utf8');
|
|
assert.match(src, /assumeEmailVerified: false,\s*\n\s*source: 'org'/,
|
|
'rowToProvider must hard-code assumeEmailVerified:false next to source:org');
|
|
assert.doesNotMatch(src, /assumeEmailVerified: *row\./, 'must never be read from the org row');
|
|
});
|
|
|
|
test('an EXPLICIT false is refused even where absence would be assumed', () => {
|
|
assert.equal(emailIsVerified({ email_verified: false }, microsoft()), false);
|
|
assert.equal(emailIsVerified({ email_verified: 'false' }, microsoft()), false, 'a string is not a true');
|
|
assert.equal(emailIsVerified({ email_verified: 0 }, microsoft()), false);
|
|
});
|
|
|
|
test('a generic provider can opt in by env, and is strict without it', () => {
|
|
const base = { OIDC_PROVIDERS: 'keycloak', OIDC_KEYCLOAK_ISSUER: 'https://kc.example.com', OIDC_KEYCLOAK_CLIENT_ID: 'kc' };
|
|
const strict = list(base).find((p) => p.slug === 'keycloak');
|
|
assert.equal(strict.assumeEmailVerified, false);
|
|
assert.equal(emailIsVerified({}, strict), false);
|
|
|
|
const opted = list({ ...base, OIDC_KEYCLOAK_ASSUME_EMAIL_VERIFIED: 'true' }).find((p) => p.slug === 'keycloak');
|
|
assert.equal(opted.assumeEmailVerified, true);
|
|
assert.equal(emailIsVerified({}, opted), true);
|
|
assert.equal(emailIsVerified({ email_verified: false }, opted), false, 'opting in never overrides an explicit false');
|
|
});
|
|
|
|
test('missing claims object or provider does not throw and does not pass', () => {
|
|
assert.equal(emailIsVerified(null, microsoft()), true, 'no claims at all still consults the provider policy');
|
|
assert.equal(emailIsVerified({}, null), false);
|
|
assert.equal(emailIsVerified({}, undefined), false);
|
|
});
|