Trust Authentik email verification
This commit is contained in:
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user