-
Notifications
You must be signed in to change notification settings - Fork 434
feat(clerk-js): Add sign_up_if_missing for SignIn.create #7749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@clerk/clerk-js': minor | ||
| '@clerk/shared': minor | ||
| --- | ||
|
|
||
| Support `sign_up_if_missing` on SignIn.create, including captcha |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -80,6 +80,7 @@ import { | |||||||||||||||||||||||||||||||||
| _futureAuthenticateWithPopup, | ||||||||||||||||||||||||||||||||||
| wrapWithPopupRoutes, | ||||||||||||||||||||||||||||||||||
| } from '../../utils/authenticateWithPopup'; | ||||||||||||||||||||||||||||||||||
| import { CaptchaChallenge } from '../../utils/captcha/CaptchaChallenge'; | ||||||||||||||||||||||||||||||||||
| import { runAsyncResourceTask } from '../../utils/runAsyncResourceTask'; | ||||||||||||||||||||||||||||||||||
| import { loadZxcvbn } from '../../utils/zxcvbn'; | ||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||
|
|
@@ -162,12 +163,37 @@ export class SignIn extends BaseResource implements SignInResource { | |||||||||||||||||||||||||||||||||
| this.fromJSON(data); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| create = (params: SignInCreateParams): Promise<SignInResource> => { | ||||||||||||||||||||||||||||||||||
| create = async (params: SignInCreateParams): Promise<SignInResource> => { | ||||||||||||||||||||||||||||||||||
| debugLogger.debug('SignIn.create', { id: this.id, strategy: 'strategy' in params ? params.strategy : undefined }); | ||||||||||||||||||||||||||||||||||
| const locale = getBrowserLocale(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let finalParams = { ...params }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Inject browser locale | ||||||||||||||||||||||||||||||||||
| const browserLocale = getBrowserLocale(); | ||||||||||||||||||||||||||||||||||
| if (browserLocale) { | ||||||||||||||||||||||||||||||||||
| finalParams.locale = browserLocale; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Determine captcha requirement based on params | ||||||||||||||||||||||||||||||||||
| const requiresCaptcha = this.shouldRequireCaptcha(params); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (requiresCaptcha) { | ||||||||||||||||||||||||||||||||||
| const captchaParams = await this.getCaptchaToken(); | ||||||||||||||||||||||||||||||||||
| finalParams = { ...finalParams, ...captchaParams }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) { | ||||||||||||||||||||||||||||||||||
| const strategy = SignIn.clerk.client?.signUp.verifications.externalAccount.strategy; | ||||||||||||||||||||||||||||||||||
| if (strategy) { | ||||||||||||||||||||||||||||||||||
| // When transfer is true, we're in the OAuth/Enterprise SSO transfer case | ||||||||||||||||||||||||||||||||||
| type TransferParams = Extract<SignInCreateParams, { transfer: true }>; | ||||||||||||||||||||||||||||||||||
| (finalParams as TransferParams).strategy = strategy as TransferParams['strategy']; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+185
to
+192
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential null reference when accessing signUp properties during transfer. Line 186 uses optional chaining on Proposed fix if (params.transfer && this.shouldBypassCaptchaForAttempt(params)) {
- const strategy = SignIn.clerk.client?.signUp.verifications.externalAccount.strategy;
+ const strategy = SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy;
if (strategy) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's a sign in and a transfer and there's no signup or verification or externalaccount, that would be an error.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return this._basePost({ | ||||||||||||||||||||||||||||||||||
| path: this.pathRoot, | ||||||||||||||||||||||||||||||||||
| body: locale ? { locale, ...params } : params, | ||||||||||||||||||||||||||||||||||
| body: finalParams, | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -574,6 +600,76 @@ export class SignIn extends BaseResource implements SignInResource { | |||||||||||||||||||||||||||||||||
| return this; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * We delegate bot detection to the following providers, instead of relying on turnstile exclusively | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * This is almost identical to SignUp.shouldBypassCaptchaForAttempt, but they differ because on transfer | ||||||||||||||||||||||||||||||||||
| * sign up needs to check the sign in, and sign in needs to check the sign up. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| protected shouldBypassCaptchaForAttempt(params: { strategy?: string; transfer?: boolean }) { | ||||||||||||||||||||||||||||||||||
| if (!('strategy' in params) || !params.strategy) { | ||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||||||||||||||||||||||||||||||||||
| const captchaOauthBypass = SignIn.clerk.__internal_environment!.displayConfig.captchaOauthBypass; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (captchaOauthBypass.some(strategy => strategy === params.strategy)) { | ||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||
| params.transfer && | ||||||||||||||||||||||||||||||||||
| captchaOauthBypass.some( | ||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||||||||||||||||||||||||||||||||||
| strategy => strategy === SignIn.clerk.client!.signUp.verifications.externalAccount.strategy, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+621
to
+629
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same null reference risk in the transfer branch. Line 625 accesses Proposed fix if (
params.transfer &&
captchaOauthBypass.some(
// eslint-disable-next-line `@typescript-eslint/no-non-null-assertion`
- strategy => strategy === SignIn.clerk.client!.signUp.verifications.externalAccount.strategy,
+ strategy => strategy === SignIn.clerk.client?.signUp?.verifications?.externalAccount?.strategy,
)
) {🤖 Prompt for AI Agents
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As in the other case, a transfer with a sign up that's incomplete in the sense of missing an external account is an error.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Determines whether captcha is required based on the provided params. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| private shouldRequireCaptcha(params: { | ||||||||||||||||||||||||||||||||||
| strategy?: string; | ||||||||||||||||||||||||||||||||||
| transfer?: boolean; | ||||||||||||||||||||||||||||||||||
| sign_up_if_missing?: boolean; | ||||||||||||||||||||||||||||||||||
| }): boolean { | ||||||||||||||||||||||||||||||||||
| // Always bypass for these conditions | ||||||||||||||||||||||||||||||||||
| if (__BUILD_DISABLE_RHC__) { | ||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (SignIn.clerk.client?.captchaBypass) { | ||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Strategy-based bypass (OAuth, etc.) | ||||||||||||||||||||||||||||||||||
| if (this.shouldBypassCaptchaForAttempt(params)) { | ||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Require captcha if sign_up_if_missing is present | ||||||||||||||||||||||||||||||||||
| return !!params.sign_up_if_missing; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Gets captcha token and widget type from the captcha challenge. | ||||||||||||||||||||||||||||||||||
| * Throws if captcha is unavailable. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| private async getCaptchaToken() { | ||||||||||||||||||||||||||||||||||
| const captchaChallenge = new CaptchaChallenge(SignIn.clerk); | ||||||||||||||||||||||||||||||||||
| const captchaParams = await captchaChallenge.managedOrInvisible({ action: 'signin' }); | ||||||||||||||||||||||||||||||||||
| if (!captchaParams) { | ||||||||||||||||||||||||||||||||||
| throw new ClerkRuntimeError('', { code: 'captcha_unavailable' }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return captchaParams; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| public __internal_toSnapshot(): SignInJSONSnapshot { | ||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| object: 'sign_in', | ||||||||||||||||||||||||||||||||||
|
|
@@ -758,10 +854,24 @@ class SignInFuture implements SignInFutureResource { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private async _create(params: SignInFutureCreateParams): Promise<void> { | ||||||||||||||||||||||||||||||||||
| let body = { ...params }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const locale = getBrowserLocale(); | ||||||||||||||||||||||||||||||||||
| if (locale) { | ||||||||||||||||||||||||||||||||||
| body.locale = locale; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Determine captcha requirement based on params | ||||||||||||||||||||||||||||||||||
| const requiresCaptcha = this.#resource['shouldRequireCaptcha'](body); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (requiresCaptcha) { | ||||||||||||||||||||||||||||||||||
| const captchaParams = await this.#resource['getCaptchaToken'](); | ||||||||||||||||||||||||||||||||||
| body = { ...body, ...captchaParams }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| await this.#resource.__internal_basePost({ | ||||||||||||||||||||||||||||||||||
| path: this.#resource.pathRoot, | ||||||||||||||||||||||||||||||||||
| body: locale ? { locale, ...params } : params, | ||||||||||||||||||||||||||||||||||
| body, | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check guards against an attacker who tries to make a fake
transferand then changesstrategyat the same time, which would be an attempt to get around WAF rules and Captcha rules that exclude transfers. We already guard against this on the backend on both sign in and sign up, but in the SDK we were only checking it on sign up. So I took this opportunity to also add the check on sign in.