Trust Authentik email verification
Create PR containing updated CHANGELOG.md and release packages to NPM once PR is merged / Release (push) Has been cancelled
Run i18n AI automation / Run i18n (push) Has been cancelled
Next.js Bundle Analysis / analyze (push) Has been cancelled

This commit is contained in:
2026-06-08 09:49:31 -06:00
parent 8eae26472e
commit 7673a170b1
2 changed files with 81 additions and 3 deletions
@@ -299,6 +299,80 @@ describe("Authentik OIDC provider", () => {
);
expect(result).toBe(true);
});
it("creates a Cal user from a trusted Authentik OIDC profile without email_verified", async () => {
const result = await signInCallback({
user: {
id: "authentik-user-id",
email: "new@example.com",
name: "New Authentik User",
image: "https://auth.example.com/avatar.png",
emailVerified: null,
},
account: {
provider: "authentik",
providerAccountId: "authentik-user-id",
type: "oauth" as const,
},
profile: {} as any,
credentials: undefined,
email: undefined,
} as any);
expect(mockPrismaUserCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
email: "new@example.com",
emailVerified: expect.any(Date),
identityProvider: "SAML",
identityProviderId: "authentik-user-id",
}),
})
);
expect(result).toBe(true);
});
it("links an existing unverified Cal account when Authentik authenticates the same email", async () => {
mockPrismaUserFindFirst
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({
id: 42,
email: "existing@example.com",
emailVerified: null,
identityProvider: "CAL",
password: { hash: "hashed" },
twoFactorEnabled: false,
});
const result = await signInCallback({
user: {
id: "authentik-user-id",
email: "existing@example.com",
name: "Existing Authentik User",
emailVerified: null,
},
account: {
provider: "authentik",
providerAccountId: "authentik-user-id",
type: "oauth" as const,
},
profile: {} as any,
credentials: undefined,
email: undefined,
} as any);
expect(mockPrismaUserUpdate).toHaveBeenCalledWith({
where: { email: "existing@example.com" },
data: {
email: "existing@example.com",
emailVerified: expect.any(Date),
identityProvider: "SAML",
identityProviderId: "authentik-user-id",
},
});
expect(result).toBe(true);
});
});
describe("CredentialsProvider authorize", () => {
@@ -815,6 +815,7 @@ export const getOptions = ({
});
return "/auth/error?error=unknown-provider";
}
const isAuthentikProvider = account.provider === "authentik";
const shouldUseCalTwoFactor = account.provider !== "authentik";
// Use optional chaining for safety, especially with AdapterUser potentially having different structure initially.
const isEmailVerified = user.emailVerified || (profile as ExtendedOAuthProfile)?.email_verified;
@@ -829,7 +830,9 @@ export const getOptions = ({
// falsy for AZUREAD logins. Use isAzureEmailDomainVerified (xms_edov) as the equivalent
// proof of ownership so the auto-merge path treats Azure AD the same as other verified IdPs.
const isVerified =
isEmailVerified || (idP === IdentityProvider.AZUREAD && isAzureEmailDomainVerified);
isAuthentikProvider ||
isEmailVerified ||
(idP === IdentityProvider.AZUREAD && isAzureEmailDomainVerified);
if (idP === IdentityProvider.AZUREAD && !isAzureEmailDomainVerified) {
log.error(
@@ -839,7 +842,7 @@ export const getOptions = ({
return "/auth/error?error=unverified-email";
}
if (!isEmailVerified && idP !== IdentityProvider.AZUREAD) {
if (!isVerified && idP !== IdentityProvider.AZUREAD) {
log.error("Attention: SAML/Google User email is not verified in the IdP", safeStringify({ user }));
return "/auth/error?error=unverified-email";
}
@@ -1015,7 +1018,7 @@ export const getOptions = ({
idP === IdentityProvider.AZUREAD)
) {
// Prevent account pre-hijacking: block OAuth linking for unverified accounts
if (!existingUserWithEmail.emailVerified) {
if (!existingUserWithEmail.emailVerified && !isAuthentikProvider) {
return "/auth/error?error=unverified-email";
}
@@ -1023,6 +1026,7 @@ export const getOptions = ({
where: { email: existingUserWithEmail.email },
data: {
email: user.email.toLowerCase(),
emailVerified: existingUserWithEmail.emailVerified || new Date(Date.now()),
identityProvider: idP,
identityProviderId: account.providerAccountId,
},