Skip to content
6 changes: 6 additions & 0 deletions .changeset/violet-badgers-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Adding iframeContext to SignIn and SignUp params when a CHIPS build is running in an iframe context
19 changes: 16 additions & 3 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import type {
} from '@clerk/types';

import { debugLogger } from '@/utils/debug';
import { inIframe } from '@/utils/runtime';

import {
generateSignatureWithBase,
Expand Down Expand Up @@ -298,12 +299,18 @@ export class SignIn extends BaseResource implements SignInResource {
const redirectUrl = SignIn.clerk.buildUrlWithAuth(params.redirectUrl);

if (!this.id || !continueSignIn) {
await this.create({
const createParams: SignInCreateParams = {
strategy,
identifier,
redirectUrl,
actionCompleteRedirectUrl,
});
};

if (__BUILD_VARIANT_CHIPS__ && inIframe()) {
createParams.iframeContext = true;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the core change. When in CHIPS build and running in an iframe we want to send extra params to FAPI that indicate we are using partitioned cookies in an iframe. FAPI will skip validating the client_id in this scenario


await this.create(createParams);
}

if (strategy === 'saml' || strategy === 'enterprise_sso') {
Expand Down Expand Up @@ -627,9 +634,15 @@ class SignInFuture implements SignInFutureResource {

async create(params: SignInFutureCreateParams): Promise<{ error: unknown }> {
return runAsyncResourceTask(this.resource, async () => {
const createParams: SignInFutureCreateParams = { ...params };

if (__BUILD_VARIANT_CHIPS__ && inIframe()) {
createParams.iframeContext = true;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, this is the core logical change


await this.resource.__internal_basePost({
path: this.resource.pathRoot,
body: params,
body: createParams,
});
});
}
Expand Down
13 changes: 12 additions & 1 deletion packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge';
import { createValidatePassword } from '../../utils/passwords/password';
import { normalizeUnsafeMetadata } from '../../utils/resourceParams';
import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask';
import { inIframe } from '../../utils/runtime';
import {
clerkInvalidFAPIResponse,
clerkMissingOptionError,
Expand Down Expand Up @@ -142,6 +143,10 @@ export class SignUp extends BaseResource implements SignUpResource {

let finalParams = { ...params };

if (__BUILD_VARIANT_CHIPS__ && inIframe()) {
finalParams.iframeContext = true;
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the SignIn change


if (!__BUILD_DISABLE_RHC__ && !this.clientBypass() && !this.shouldBypassCaptchaForAttempt(params)) {
const captchaChallenge = new CaptchaChallenge(SignUp.clerk);
const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signup' });
Expand Down Expand Up @@ -426,8 +431,14 @@ export class SignUp extends BaseResource implements SignUpResource {
};

update = (params: SignUpUpdateParams): Promise<SignUpResource> => {
const finalParams = { ...params };

if (__BUILD_VARIANT_CHIPS__ && inIframe()) {
finalParams.iframeContext = true;
}

return this._basePatch({
body: normalizeUnsafeMetadata(params),
body: normalizeUnsafeMetadata(finalParams),
});
};

Expand Down
224 changes: 224 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/SignIn.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { inIframe } from '../../../utils/runtime';
import { SignIn } from '../SignIn';

vi.mock('../../../utils/runtime', () => ({
inIframe: vi.fn(),
}));

const originalBuildVariant = global.__BUILD_VARIANT_CHIPS__;
global.__BUILD_VARIANT_CHIPS__ = true;

describe('SignIn', () => {
let signIn: SignIn;
let mockCreate: any;
let mockBuildUrlWithAuth: any;

beforeEach(() => {
vi.clearAllMocks();

const mockClerk = {
buildUrlWithAuth: vi.fn((url: string) => `https://clerk.example.com${url}`),
};

const mockFapiClient = {
buildEmailAddress: vi.fn(() => '[email protected]'),
};

SignIn.clerk = mockClerk as any;

Object.defineProperty(SignIn, 'fapiClient', {
get: () => mockFapiClient,
configurable: true,
});

signIn = new SignIn({
id: 'test-signin-id',
status: 'needs_first_factor',
first_factor_verification: {
status: 'unverified',
external_verification_redirect_url: 'https://oauth.provider.com/auth',
},
} as any);

mockCreate = vi.fn().mockResolvedValue({});
signIn.create = mockCreate;

mockBuildUrlWithAuth = vi.fn((url: string) => `https://clerk.example.com${url}`);
SignIn.clerk.buildUrlWithAuth = mockBuildUrlWithAuth;
});

afterEach(() => {
global.__BUILD_VARIANT_CHIPS__ = originalBuildVariant;
});

describe('authenticateWithRedirectOrPopup', () => {
it('should set iframeContext to true when CHIPS build and in iframe', async () => {
vi.mocked(inIframe).mockReturnValue(true);

const params = {
strategy: 'oauth_google' as const,
redirectUrl: '/callback',
identifier: '[email protected]',
};

const mockNavigate = vi.fn();

Object.defineProperty(signIn, 'firstFactorVerification', {
value: {
status: 'unverified',
externalVerificationRedirectURL: 'https://oauth.provider.com/auth',
},
writable: true,
});

await signIn.authenticateWithRedirectOrPopup(params, mockNavigate);

expect(mockCreate).toHaveBeenCalledWith({
strategy: 'oauth_google',
identifier: '[email protected]',
redirectUrl: 'https://clerk.example.com/callback',
actionCompleteRedirectUrl: undefined,
iframeContext: true,
});
});

it('should not set iframeContext when not in iframe', async () => {
vi.mocked(inIframe).mockReturnValue(false);

const params = {
strategy: 'oauth_google' as const,
redirectUrl: '/callback',
identifier: '[email protected]',
};

const mockNavigate = vi.fn();

Object.defineProperty(signIn, 'firstFactorVerification', {
value: {
status: 'unverified',
externalVerificationRedirectURL: 'https://oauth.provider.com/auth',
},
writable: true,
});

await signIn.authenticateWithRedirectOrPopup(params, mockNavigate);

expect(mockCreate).toHaveBeenCalledWith({
strategy: 'oauth_google',
identifier: '[email protected]',
redirectUrl: 'https://clerk.example.com/callback',
actionCompleteRedirectUrl: undefined,
});
expect(mockCreate).toHaveBeenCalledWith(expect.not.objectContaining({ iframeContext: true }));
});

it('should not set iframeContext when not CHIPS build', async () => {
global.__BUILD_VARIANT_CHIPS__ = false;

vi.mocked(inIframe).mockReturnValue(true);

const params = {
strategy: 'oauth_google' as const,
redirectUrl: '/callback',
identifier: '[email protected]',
};

const mockNavigate = vi.fn();

Object.defineProperty(signIn, 'firstFactorVerification', {
value: {
status: 'unverified',
externalVerificationRedirectURL: 'https://oauth.provider.com/auth',
},
writable: true,
});

await signIn.authenticateWithRedirectOrPopup(params, mockNavigate);

expect(mockCreate).toHaveBeenCalledWith(expect.not.objectContaining({ iframeContext: true }));
});

it('should not set iframeContext when continueSignIn is true', async () => {
vi.mocked(inIframe).mockReturnValue(true);

const params = {
strategy: 'oauth_google' as const,
redirectUrl: '/callback',
identifier: '[email protected]',
continueSignIn: true,
};

const mockNavigate = vi.fn();

Object.defineProperty(signIn, 'firstFactorVerification', {
value: {
status: 'unverified',
externalVerificationRedirectURL: 'https://oauth.provider.com/auth',
},
writable: true,
});

await signIn.authenticateWithRedirectOrPopup(params, mockNavigate);

expect(mockCreate).not.toHaveBeenCalled();
});
});

describe('SignInFuture.create', () => {
it('should set iframeContext to true when CHIPS build and in iframe', async () => {
global.__BUILD_VARIANT_CHIPS__ = true;
vi.mocked(inIframe).mockReturnValue(true);

const params = {
strategy: 'oauth_google' as const,
redirectUrl: '/callback',
identifier: '[email protected]',
};

const signInFuture = signIn.__internal_future;

const mockBasePost = vi.fn().mockResolvedValue({});
signInFuture.resource.__internal_basePost = mockBasePost;

await signInFuture.create(params);

expect(mockBasePost).toHaveBeenCalledWith({
path: '/client/sign_ins',
body: {
strategy: 'oauth_google',
redirectUrl: '/callback',
identifier: '[email protected]',
iframeContext: true,
},
});
});

it('should not set iframeContext when not in iframe', async () => {
vi.mocked(inIframe).mockReturnValue(false);

const params = {
strategy: 'oauth_google' as const,
redirectUrl: '/callback',
identifier: '[email protected]',
};

const signInFuture = signIn.__internal_future;

const mockBasePost = vi.fn().mockResolvedValue({});
signInFuture.resource.__internal_basePost = mockBasePost;

await signInFuture.create(params);

expect(mockBasePost).toHaveBeenCalledWith({
path: '/client/sign_ins',
body: {
strategy: 'oauth_google',
redirectUrl: '/callback',
identifier: '[email protected]',
},
});
});
});
});
Loading
Loading