Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/afraid-experts-eat.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@clerk/backend': major
---

Drop `user` / `organization` / `session` from auth object on **signed-out** state (current value was `null`). Eg

```diff
// Backend
import { createClerkClient } from '@clerk/backend';

const clerkClient = createClerkClient({...});
const requestState = clerkClient.authenticateRequest(request, {...});

- const { user, organization, session } = requestState.toAuth();
+ const { userId, organizationId, sessionId } = requestState.toAuth();

// Remix
import { getAuth } from '@clerk/remix/ssr.server';

- const { user, organization, session } = await getAuth(args);
+ const { userId, organizationId, sessionId } = await getAuth(args);

// or
rootAuthLoader(
args,
({ request }) => {
- const { user, organization, session } = request.auth;
+ const { userId, organizationId, sessionId } = request.auth;
// ...
},
{ loadUser: true },
);
Comment thread
dimkl marked this conversation as resolved.

// NextJS
import { getAuth } from '@clerk/nextjs/server';

- const { user, organization, session } = getAuth(args);
+ const { userId, organizationId, sessionId } = getAuth(req, opts);
Comment on lines +35 to +38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking, did we have getAuth.user before v5?
Nitpick, but this should probably be on its own changeset targeting nextjs (similar for remix and gatsby)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes we had them in v4.
signed-in:


signed-out:

The change is applied only in the backend package and the changeset will add this change in the changelog of all the packages depending on backend, that's why i didn't add both remix, nextjs, gatsby.
@nikosdouvlis Do we want for each breaking change in backend to also add the other packages?


// Gatsby
import { withServerAuth } from 'gatsby-plugin-clerk';

export const getServerData: GetServerData<any> = withServerAuth(
async props => {
- const { user, organization, session } = props;
+ const { userId, organizationId, sessionId } = props;
return { props: { data: '1', auth: props.auth, userId, organizationId, sessionId } };
},
{ loadUser: true },
);
```

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

cc: @LekoArts Could you handle adding these changes in clerk-docs?

5 changes: 5 additions & 0 deletions .changeset/strange-trains-bow.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/remix': patch
---

Fix adding `user`/`sessions`/`organization` resources into request.
3 changes: 0 additions & 3 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,10 @@ function assertSignedOutToAuth(assert, requestState: RequestState) {
assert.propContains(requestState.toAuth(), {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
orgId: null,
orgRole: null,
orgSlug: null,
organization: null,
getToken: {},
});
}
Expand Down
6 changes: 0 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,15 +47,12 @@ export type SignedInAuthObject = {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
session: null;
actor: null;
userId: null;
user: null;
orgId: null;
orgRole: null;
orgSlug: null;
orgPermissions: null;
organization: null;
getToken: ServerGetToken;
has: CheckAuthorizationWithCustomPermissions;
debug: AuthObjectDebug;
Expand DownExpand Up@@ -148,15 +145,12 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
return {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
organization: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
Expand Down
79 changes: 8 additions & 71 deletions packages/remix/src/ssr/authenticateRequest.ts
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,18 @@
import { createClerkClient } from '@clerk/backend';
import type { SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';
import type { LoaderFunctionArgs } from './types';

export async function authenticateRequest(
args: LoaderFunctionArgs,
opts: RootAuthLoaderOptions = {},
opts: AuthenticateRequestOptions,
): Promise<SignedInState | SignedOutState> {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);
const { request } = args;
const { audience, authorizedParties } = opts;

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = opts.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';

if (!secretKey) {
throw new Error(noSecretKeyError);
}

const publishableKey = opts.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';

const jwtKey = opts.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);

const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);

const domain = handleValueOrFn(opts.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';

const isSatellite =
handleValueOrFn(opts.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

const signInUrl = opts.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';

const signUpUrl = opts.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';

const afterSignInUrl = opts.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';

const afterSignUpUrl = opts.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}

if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = opts;

const requestState = await createClerkClient({
apiUrl,
Expand All@@ -79,16 +21,11 @@ export async function authenticateRequest(
proxyUrl,
isSatellite,
domain,
publishableKey,
userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
}).authenticateRequest(request, {
audience,
secretKey,
jwtKey,
publishableKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
afterSignInUrl,
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { stripPrivateDataFromObject } from '@clerk/backend/internal';

import { noLoaderArgsPassedInGetAuth } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { GetAuthReturn, LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

type GetAuthOptions = Pick<RootAuthLoaderOptions, 'secretKey'>;
Expand All@@ -11,8 +12,9 @@ export async function getAuth(args: LoaderFunctionArgs, opts?: GetAuthOptions):
throw new Error(noLoaderArgsPassedInGetAuth);
}

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts || {});
const requestState = await authenticateRequest(args, loadedOptions);

return stripPrivateDataFromObject(requestState.toAuth());
}
72 changes: 72 additions & 0 deletions packages/remix/src/ssr/loadOptions.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
import { createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderOptions = {}) => {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
const publishableKey = overrides.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
const domain =
handleValueOrFn(overrides.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';
const isSatellite =
handleValueOrFn(overrides.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context));
const relativeOrAbsoluteProxyUrl = handleValueOrFn(
overrides?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);
const signInUrl = overrides.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';
const signUpUrl = overrides.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';
const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';
const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

if (!secretKey) {
throw new Error(noSecretKeyError);
}
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}

return {
// used to append options that are not initialized from env
...overrides,
secretKey,
publishableKey,
jwtKey,
apiUrl,
domain,
isSatellite,
proxyUrl,
signInUrl,
signUpUrl,
afterSignInUrl,
afterSignUpUrl,
};
};
9 changes: 6 additions & 3 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { isDeferredData } from '@remix-run/server-runtime/dist/responses';

import { invalidRootLoaderCallbackReturn } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import {
assertValidHandlerResult,
Expand DownExpand Up@@ -46,17 +47,19 @@ export const rootAuthLoader: RootAuthLoader = async (
? handlerOrOptions
: {};

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts);
const requestState = await authenticateRequest(args, loadedOptions);

if (!handler) {
// if the user did not provide a handler, simply inject requestState into an empty response
return injectRequestStateIntoResponse(new Response(JSON.stringify({})), requestState, args.context);
}

const authObj = requestState.toAuth();
Object.assign(args.request, { auth: authObj });
const handlerResult = await handler(await decorateObjectWithResources(args, authObj, opts));
const requestWithAuth = Object.assign(args.request, { auth: authObj });
await decorateObjectWithResources(requestWithAuth, authObj, loadedOptions);
const handlerResult = await handler(args);
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);

// When using defer(), we need to inject the clerk auth state into its internal data object.
Expand Down
1 change: 0 additions & 1 deletion playground/remix-node/app/entry.server.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
Expand Down
8 changes: 3 additions & 5 deletions playground/remix-node/app/root.tsx
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
import { defer, type DataFunctionArgs } from '@remix-run/node';
import type { MetaFunction } from '@remix-run/react';
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
import { rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp } from '@clerk/remix';
import { Suspense } from 'react';

export const loader = (args: DataFunctionArgs) => {
Expand DownExpand Up@@ -46,8 +46,6 @@ export const meta: MetaFunction = () => {
];
};

export const ErrorBoundary = ClerkErrorBoundary();

function App() {
const loaderData = useLoaderData<typeof loader>();

Expand Down
1 change: 1 addition & 0 deletions playground/remix-node/app/routes/sign-in.$.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ export default function SignInPage() {
<div style={{ border: '2px solid blue', padding: '2rem' }}>
<h1>Sign In route</h1>
<SignIn
path='/sign-in'
signUpUrl='/sign-up'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/remix-node/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
"typescript": "^5"
},
"engines": {
"node": ">=18"
Expand Down
8 changes: 0 additions & 8 deletions playground/remix-node/remix.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,4 @@
module.exports = {
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
v2_headers: true,
v2_dev: true,
},
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/afraid-experts-eat.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@clerk/backend': major
---

Drop `user` / `organization` / `session` from auth object on **signed-out** state (current value was `null`). Eg

```diff
// Backend
import { createClerkClient } from '@clerk/backend';

const clerkClient = createClerkClient({...});
const requestState = clerkClient.authenticateRequest(request, {...});

- const { user, organization, session } = requestState.toAuth();
+ const { userId, organizationId, sessionId } = requestState.toAuth();

// Remix
import { getAuth } from '@clerk/remix/ssr.server';

- const { user, organization, session } = await getAuth(args);
+ const { userId, organizationId, sessionId } = await getAuth(args);

// or
rootAuthLoader(
args,
({ request }) => {
- const { user, organization, session } = request.auth;
+ const { userId, organizationId, sessionId } = request.auth;
// ...
},
{ loadUser: true },
);
Comment thread
dimkl marked this conversation as resolved.

// NextJS
import { getAuth } from '@clerk/nextjs/server';

- const { user, organization, session } = getAuth(args);
+ const { userId, organizationId, sessionId } = getAuth(req, opts);
Comment on lines +35 to +38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking, did we have getAuth.user before v5?
Nitpick, but this should probably be on its own changeset targeting nextjs (similar for remix and gatsby)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes we had them in v4.
signed-in:


signed-out:

The change is applied only in the backend package and the changeset will add this change in the changelog of all the packages depending on backend, that's why i didn't add both remix, nextjs, gatsby.
@nikosdouvlis Do we want for each breaking change in backend to also add the other packages?


// Gatsby
import { withServerAuth } from 'gatsby-plugin-clerk';

export const getServerData: GetServerData<any> = withServerAuth(
async props => {
- const { user, organization, session } = props;
+ const { userId, organizationId, sessionId } = props;
return { props: { data: '1', auth: props.auth, userId, organizationId, sessionId } };
},
{ loadUser: true },
);
```

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

cc: @LekoArts Could you handle adding these changes in clerk-docs?

5 changes: 5 additions & 0 deletions .changeset/strange-trains-bow.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/remix': patch
---

Fix adding `user`/`sessions`/`organization` resources into request.
3 changes: 0 additions & 3 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,10 @@ function assertSignedOutToAuth(assert, requestState: RequestState) {
assert.propContains(requestState.toAuth(), {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
orgId: null,
orgRole: null,
orgSlug: null,
organization: null,
getToken: {},
});
}
Expand Down
6 changes: 0 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,15 +47,12 @@ export type SignedInAuthObject = {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
session: null;
actor: null;
userId: null;
user: null;
orgId: null;
orgRole: null;
orgSlug: null;
orgPermissions: null;
organization: null;
getToken: ServerGetToken;
has: CheckAuthorizationWithCustomPermissions;
debug: AuthObjectDebug;
Expand DownExpand Up@@ -148,15 +145,12 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
return {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
organization: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
Expand Down
79 changes: 8 additions & 71 deletions packages/remix/src/ssr/authenticateRequest.ts
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,18 @@
import { createClerkClient } from '@clerk/backend';
import type { SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';
import type { LoaderFunctionArgs } from './types';

export async function authenticateRequest(
args: LoaderFunctionArgs,
opts: RootAuthLoaderOptions = {},
opts: AuthenticateRequestOptions,
): Promise<SignedInState | SignedOutState> {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);
const { request } = args;
const { audience, authorizedParties } = opts;

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = opts.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';

if (!secretKey) {
throw new Error(noSecretKeyError);
}

const publishableKey = opts.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';

const jwtKey = opts.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);

const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);

const domain = handleValueOrFn(opts.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';

const isSatellite =
handleValueOrFn(opts.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

const signInUrl = opts.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';

const signUpUrl = opts.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';

const afterSignInUrl = opts.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';

const afterSignUpUrl = opts.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}

if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = opts;

const requestState = await createClerkClient({
apiUrl,
Expand All@@ -79,16 +21,11 @@ export async function authenticateRequest(
proxyUrl,
isSatellite,
domain,
publishableKey,
userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
}).authenticateRequest(request, {
audience,
secretKey,
jwtKey,
publishableKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
afterSignInUrl,
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { stripPrivateDataFromObject } from '@clerk/backend/internal';

import { noLoaderArgsPassedInGetAuth } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { GetAuthReturn, LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

type GetAuthOptions = Pick<RootAuthLoaderOptions, 'secretKey'>;
Expand All@@ -11,8 +12,9 @@ export async function getAuth(args: LoaderFunctionArgs, opts?: GetAuthOptions):
throw new Error(noLoaderArgsPassedInGetAuth);
}

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts || {});
const requestState = await authenticateRequest(args, loadedOptions);

return stripPrivateDataFromObject(requestState.toAuth());
}
72 changes: 72 additions & 0 deletions packages/remix/src/ssr/loadOptions.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
import { createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderOptions = {}) => {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
const publishableKey = overrides.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
const domain =
handleValueOrFn(overrides.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';
const isSatellite =
handleValueOrFn(overrides.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context));
const relativeOrAbsoluteProxyUrl = handleValueOrFn(
overrides?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);
const signInUrl = overrides.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';
const signUpUrl = overrides.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';
const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';
const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

if (!secretKey) {
throw new Error(noSecretKeyError);
}
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}

return {
// used to append options that are not initialized from env
...overrides,
secretKey,
publishableKey,
jwtKey,
apiUrl,
domain,
isSatellite,
proxyUrl,
signInUrl,
signUpUrl,
afterSignInUrl,
afterSignUpUrl,
};
};
9 changes: 6 additions & 3 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { isDeferredData } from '@remix-run/server-runtime/dist/responses';

import { invalidRootLoaderCallbackReturn } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import {
assertValidHandlerResult,
Expand DownExpand Up@@ -46,17 +47,19 @@ export const rootAuthLoader: RootAuthLoader = async (
? handlerOrOptions
: {};

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts);
const requestState = await authenticateRequest(args, loadedOptions);

if (!handler) {
// if the user did not provide a handler, simply inject requestState into an empty response
return injectRequestStateIntoResponse(new Response(JSON.stringify({})), requestState, args.context);
}

const authObj = requestState.toAuth();
Object.assign(args.request, { auth: authObj });
const handlerResult = await handler(await decorateObjectWithResources(args, authObj, opts));
const requestWithAuth = Object.assign(args.request, { auth: authObj });
await decorateObjectWithResources(requestWithAuth, authObj, loadedOptions);
const handlerResult = await handler(args);
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);

// When using defer(), we need to inject the clerk auth state into its internal data object.
Expand Down
1 change: 0 additions & 1 deletion playground/remix-node/app/entry.server.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
Expand Down
8 changes: 3 additions & 5 deletions playground/remix-node/app/root.tsx
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
import { defer, type DataFunctionArgs } from '@remix-run/node';
import type { MetaFunction } from '@remix-run/react';
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
import { rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp } from '@clerk/remix';
import { Suspense } from 'react';

export const loader = (args: DataFunctionArgs) => {
Expand DownExpand Up@@ -46,8 +46,6 @@ export const meta: MetaFunction = () => {
];
};

export const ErrorBoundary = ClerkErrorBoundary();

function App() {
const loaderData = useLoaderData<typeof loader>();

Expand Down
1 change: 1 addition & 0 deletions playground/remix-node/app/routes/sign-in.$.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ export default function SignInPage() {
<div style={{ border: '2px solid blue', padding: '2rem' }}>
<h1>Sign In route</h1>
<SignIn
path='/sign-in'
signUpUrl='/sign-up'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/remix-node/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
"typescript": "^5"
},
"engines": {
"node": ">=18"
Expand Down
8 changes: 0 additions & 8 deletions playground/remix-node/remix.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,4 @@
module.exports = {
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
v2_headers: true,
v2_dev: true,
},
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/afraid-experts-eat.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@clerk/backend': major
---

Drop `user` / `organization` / `session` from auth object on **signed-out** state (current value was `null`). Eg

```diff
// Backend
import { createClerkClient } from '@clerk/backend';

const clerkClient = createClerkClient({...});
const requestState = clerkClient.authenticateRequest(request, {...});

- const { user, organization, session } = requestState.toAuth();
+ const { userId, organizationId, sessionId } = requestState.toAuth();

// Remix
import { getAuth } from '@clerk/remix/ssr.server';

- const { user, organization, session } = await getAuth(args);
+ const { userId, organizationId, sessionId } = await getAuth(args);

// or
rootAuthLoader(
args,
({ request }) => {
- const { user, organization, session } = request.auth;
+ const { userId, organizationId, sessionId } = request.auth;
// ...
},
{ loadUser: true },
);
Comment thread
dimkl marked this conversation as resolved.

// NextJS
import { getAuth } from '@clerk/nextjs/server';

- const { user, organization, session } = getAuth(args);
+ const { userId, organizationId, sessionId } = getAuth(req, opts);
Comment on lines +35 to +38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking, did we have getAuth.user before v5?
Nitpick, but this should probably be on its own changeset targeting nextjs (similar for remix and gatsby)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes we had them in v4.
signed-in:


signed-out:

The change is applied only in the backend package and the changeset will add this change in the changelog of all the packages depending on backend, that's why i didn't add both remix, nextjs, gatsby.
@nikosdouvlis Do we want for each breaking change in backend to also add the other packages?


// Gatsby
import { withServerAuth } from 'gatsby-plugin-clerk';

export const getServerData: GetServerData<any> = withServerAuth(
async props => {
- const { user, organization, session } = props;
+ const { userId, organizationId, sessionId } = props;
return { props: { data: '1', auth: props.auth, userId, organizationId, sessionId } };
},
{ loadUser: true },
);
```

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

cc: @LekoArts Could you handle adding these changes in clerk-docs?

5 changes: 5 additions & 0 deletions .changeset/strange-trains-bow.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/remix': patch
---

Fix adding `user`/`sessions`/`organization` resources into request.
3 changes: 0 additions & 3 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,10 @@ function assertSignedOutToAuth(assert, requestState: RequestState) {
assert.propContains(requestState.toAuth(), {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
orgId: null,
orgRole: null,
orgSlug: null,
organization: null,
getToken: {},
});
}
Expand Down
6 changes: 0 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,15 +47,12 @@ export type SignedInAuthObject = {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
session: null;
actor: null;
userId: null;
user: null;
orgId: null;
orgRole: null;
orgSlug: null;
orgPermissions: null;
organization: null;
getToken: ServerGetToken;
has: CheckAuthorizationWithCustomPermissions;
debug: AuthObjectDebug;
Expand DownExpand Up@@ -148,15 +145,12 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
return {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
organization: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
Expand Down
79 changes: 8 additions & 71 deletions packages/remix/src/ssr/authenticateRequest.ts
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,18 @@
import { createClerkClient } from '@clerk/backend';
import type { SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';
import type { LoaderFunctionArgs } from './types';

export async function authenticateRequest(
args: LoaderFunctionArgs,
opts: RootAuthLoaderOptions = {},
opts: AuthenticateRequestOptions,
): Promise<SignedInState | SignedOutState> {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);
const { request } = args;
const { audience, authorizedParties } = opts;

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = opts.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';

if (!secretKey) {
throw new Error(noSecretKeyError);
}

const publishableKey = opts.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';

const jwtKey = opts.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);

const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);

const domain = handleValueOrFn(opts.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';

const isSatellite =
handleValueOrFn(opts.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

const signInUrl = opts.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';

const signUpUrl = opts.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';

const afterSignInUrl = opts.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';

const afterSignUpUrl = opts.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}

if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = opts;

const requestState = await createClerkClient({
apiUrl,
Expand All@@ -79,16 +21,11 @@ export async function authenticateRequest(
proxyUrl,
isSatellite,
domain,
publishableKey,
userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
}).authenticateRequest(request, {
audience,
secretKey,
jwtKey,
publishableKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
afterSignInUrl,
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { stripPrivateDataFromObject } from '@clerk/backend/internal';

import { noLoaderArgsPassedInGetAuth } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { GetAuthReturn, LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

type GetAuthOptions = Pick<RootAuthLoaderOptions, 'secretKey'>;
Expand All@@ -11,8 +12,9 @@ export async function getAuth(args: LoaderFunctionArgs, opts?: GetAuthOptions):
throw new Error(noLoaderArgsPassedInGetAuth);
}

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts || {});
const requestState = await authenticateRequest(args, loadedOptions);

return stripPrivateDataFromObject(requestState.toAuth());
}
72 changes: 72 additions & 0 deletions packages/remix/src/ssr/loadOptions.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
import { createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderOptions = {}) => {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
const publishableKey = overrides.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
const domain =
handleValueOrFn(overrides.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';
const isSatellite =
handleValueOrFn(overrides.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context));
const relativeOrAbsoluteProxyUrl = handleValueOrFn(
overrides?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);
const signInUrl = overrides.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';
const signUpUrl = overrides.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';
const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';
const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

if (!secretKey) {
throw new Error(noSecretKeyError);
}
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}

return {
// used to append options that are not initialized from env
...overrides,
secretKey,
publishableKey,
jwtKey,
apiUrl,
domain,
isSatellite,
proxyUrl,
signInUrl,
signUpUrl,
afterSignInUrl,
afterSignUpUrl,
};
};
9 changes: 6 additions & 3 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { isDeferredData } from '@remix-run/server-runtime/dist/responses';

import { invalidRootLoaderCallbackReturn } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import {
assertValidHandlerResult,
Expand DownExpand Up@@ -46,17 +47,19 @@ export const rootAuthLoader: RootAuthLoader = async (
? handlerOrOptions
: {};

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts);
const requestState = await authenticateRequest(args, loadedOptions);

if (!handler) {
// if the user did not provide a handler, simply inject requestState into an empty response
return injectRequestStateIntoResponse(new Response(JSON.stringify({})), requestState, args.context);
}

const authObj = requestState.toAuth();
Object.assign(args.request, { auth: authObj });
const handlerResult = await handler(await decorateObjectWithResources(args, authObj, opts));
const requestWithAuth = Object.assign(args.request, { auth: authObj });
await decorateObjectWithResources(requestWithAuth, authObj, loadedOptions);
const handlerResult = await handler(args);
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);

// When using defer(), we need to inject the clerk auth state into its internal data object.
Expand Down
1 change: 0 additions & 1 deletion playground/remix-node/app/entry.server.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
Expand Down
8 changes: 3 additions & 5 deletions playground/remix-node/app/root.tsx
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
import { defer, type DataFunctionArgs } from '@remix-run/node';
import type { MetaFunction } from '@remix-run/react';
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
import { rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp } from '@clerk/remix';
import { Suspense } from 'react';

export const loader = (args: DataFunctionArgs) => {
Expand DownExpand Up@@ -46,8 +46,6 @@ export const meta: MetaFunction = () => {
];
};

export const ErrorBoundary = ClerkErrorBoundary();

function App() {
const loaderData = useLoaderData<typeof loader>();

Expand Down
1 change: 1 addition & 0 deletions playground/remix-node/app/routes/sign-in.$.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ export default function SignInPage() {
<div style={{ border: '2px solid blue', padding: '2rem' }}>
<h1>Sign In route</h1>
<SignIn
path='/sign-in'
signUpUrl='/sign-up'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/remix-node/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
"typescript": "^5"
},
"engines": {
"node": ">=18"
Expand Down
8 changes: 0 additions & 8 deletions playground/remix-node/remix.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,4 @@
module.exports = {
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
v2_headers: true,
v2_dev: true,
},
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/afraid-experts-eat.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@clerk/backend': major
---

Drop `user` / `organization` / `session` from auth object on **signed-out** state (current value was `null`). Eg

```diff
// Backend
import { createClerkClient } from '@clerk/backend';

const clerkClient = createClerkClient({...});
const requestState = clerkClient.authenticateRequest(request, {...});

- const { user, organization, session } = requestState.toAuth();
+ const { userId, organizationId, sessionId } = requestState.toAuth();

// Remix
import { getAuth } from '@clerk/remix/ssr.server';

- const { user, organization, session } = await getAuth(args);
+ const { userId, organizationId, sessionId } = await getAuth(args);

// or
rootAuthLoader(
args,
({ request }) => {
- const { user, organization, session } = request.auth;
+ const { userId, organizationId, sessionId } = request.auth;
// ...
},
{ loadUser: true },
);
Comment thread
dimkl marked this conversation as resolved.

// NextJS
import { getAuth } from '@clerk/nextjs/server';

- const { user, organization, session } = getAuth(args);
+ const { userId, organizationId, sessionId } = getAuth(req, opts);
Comment on lines +35 to +38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking, did we have getAuth.user before v5?
Nitpick, but this should probably be on its own changeset targeting nextjs (similar for remix and gatsby)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes we had them in v4.
signed-in:


signed-out:

The change is applied only in the backend package and the changeset will add this change in the changelog of all the packages depending on backend, that's why i didn't add both remix, nextjs, gatsby.
@nikosdouvlis Do we want for each breaking change in backend to also add the other packages?


// Gatsby
import { withServerAuth } from 'gatsby-plugin-clerk';

export const getServerData: GetServerData<any> = withServerAuth(
async props => {
- const { user, organization, session } = props;
+ const { userId, organizationId, sessionId } = props;
return { props: { data: '1', auth: props.auth, userId, organizationId, sessionId } };
},
{ loadUser: true },
);
```

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

cc: @LekoArts Could you handle adding these changes in clerk-docs?

5 changes: 5 additions & 0 deletions .changeset/strange-trains-bow.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/remix': patch
---

Fix adding `user`/`sessions`/`organization` resources into request.
3 changes: 0 additions & 3 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,10 @@ function assertSignedOutToAuth(assert, requestState: RequestState) {
assert.propContains(requestState.toAuth(), {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
orgId: null,
orgRole: null,
orgSlug: null,
organization: null,
getToken: {},
});
}
Expand Down
6 changes: 0 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,15 +47,12 @@ export type SignedInAuthObject = {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
session: null;
actor: null;
userId: null;
user: null;
orgId: null;
orgRole: null;
orgSlug: null;
orgPermissions: null;
organization: null;
getToken: ServerGetToken;
has: CheckAuthorizationWithCustomPermissions;
debug: AuthObjectDebug;
Expand DownExpand Up@@ -148,15 +145,12 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
return {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
organization: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
Expand Down
79 changes: 8 additions & 71 deletions packages/remix/src/ssr/authenticateRequest.ts
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,18 @@
import { createClerkClient } from '@clerk/backend';
import type { SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';
import type { LoaderFunctionArgs } from './types';

export async function authenticateRequest(
args: LoaderFunctionArgs,
opts: RootAuthLoaderOptions = {},
opts: AuthenticateRequestOptions,
): Promise<SignedInState | SignedOutState> {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);
const { request } = args;
const { audience, authorizedParties } = opts;

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = opts.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';

if (!secretKey) {
throw new Error(noSecretKeyError);
}

const publishableKey = opts.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';

const jwtKey = opts.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);

const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);

const domain = handleValueOrFn(opts.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';

const isSatellite =
handleValueOrFn(opts.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

const signInUrl = opts.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';

const signUpUrl = opts.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';

const afterSignInUrl = opts.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';

const afterSignUpUrl = opts.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}

if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = opts;

const requestState = await createClerkClient({
apiUrl,
Expand All@@ -79,16 +21,11 @@ export async function authenticateRequest(
proxyUrl,
isSatellite,
domain,
publishableKey,
userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
}).authenticateRequest(request, {
audience,
secretKey,
jwtKey,
publishableKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
afterSignInUrl,
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { stripPrivateDataFromObject } from '@clerk/backend/internal';

import { noLoaderArgsPassedInGetAuth } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { GetAuthReturn, LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

type GetAuthOptions = Pick<RootAuthLoaderOptions, 'secretKey'>;
Expand All@@ -11,8 +12,9 @@ export async function getAuth(args: LoaderFunctionArgs, opts?: GetAuthOptions):
throw new Error(noLoaderArgsPassedInGetAuth);
}

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts || {});
const requestState = await authenticateRequest(args, loadedOptions);

return stripPrivateDataFromObject(requestState.toAuth());
}
72 changes: 72 additions & 0 deletions packages/remix/src/ssr/loadOptions.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
import { createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderOptions = {}) => {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
const publishableKey = overrides.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
const domain =
handleValueOrFn(overrides.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';
const isSatellite =
handleValueOrFn(overrides.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context));
const relativeOrAbsoluteProxyUrl = handleValueOrFn(
overrides?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);
const signInUrl = overrides.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';
const signUpUrl = overrides.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';
const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';
const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

if (!secretKey) {
throw new Error(noSecretKeyError);
}
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}

return {
// used to append options that are not initialized from env
...overrides,
secretKey,
publishableKey,
jwtKey,
apiUrl,
domain,
isSatellite,
proxyUrl,
signInUrl,
signUpUrl,
afterSignInUrl,
afterSignUpUrl,
};
};
9 changes: 6 additions & 3 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { isDeferredData } from '@remix-run/server-runtime/dist/responses';

import { invalidRootLoaderCallbackReturn } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import {
assertValidHandlerResult,
Expand DownExpand Up@@ -46,17 +47,19 @@ export const rootAuthLoader: RootAuthLoader = async (
? handlerOrOptions
: {};

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts);
const requestState = await authenticateRequest(args, loadedOptions);

if (!handler) {
// if the user did not provide a handler, simply inject requestState into an empty response
return injectRequestStateIntoResponse(new Response(JSON.stringify({})), requestState, args.context);
}

const authObj = requestState.toAuth();
Object.assign(args.request, { auth: authObj });
const handlerResult = await handler(await decorateObjectWithResources(args, authObj, opts));
const requestWithAuth = Object.assign(args.request, { auth: authObj });
await decorateObjectWithResources(requestWithAuth, authObj, loadedOptions);
const handlerResult = await handler(args);
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);

// When using defer(), we need to inject the clerk auth state into its internal data object.
Expand Down
1 change: 0 additions & 1 deletion playground/remix-node/app/entry.server.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
Expand Down
8 changes: 3 additions & 5 deletions playground/remix-node/app/root.tsx
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
import { defer, type DataFunctionArgs } from '@remix-run/node';
import type { MetaFunction } from '@remix-run/react';
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
import { rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp } from '@clerk/remix';
import { Suspense } from 'react';

export const loader = (args: DataFunctionArgs) => {
Expand DownExpand Up@@ -46,8 +46,6 @@ export const meta: MetaFunction = () => {
];
};

export const ErrorBoundary = ClerkErrorBoundary();

function App() {
const loaderData = useLoaderData<typeof loader>();

Expand Down
1 change: 1 addition & 0 deletions playground/remix-node/app/routes/sign-in.$.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ export default function SignInPage() {
<div style={{ border: '2px solid blue', padding: '2rem' }}>
<h1>Sign In route</h1>
<SignIn
path='/sign-in'
signUpUrl='/sign-up'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/remix-node/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
"typescript": "^5"
},
"engines": {
"node": ">=18"
Expand Down
8 changes: 0 additions & 8 deletions playground/remix-node/remix.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,4 @@
module.exports = {
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
v2_headers: true,
v2_dev: true,
},
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/afraid-experts-eat.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@clerk/backend': major
---

Drop `user` / `organization` / `session` from auth object on **signed-out** state (current value was `null`). Eg

```diff
// Backend
import { createClerkClient } from '@clerk/backend';

const clerkClient = createClerkClient({...});
const requestState = clerkClient.authenticateRequest(request, {...});

- const { user, organization, session } = requestState.toAuth();
+ const { userId, organizationId, sessionId } = requestState.toAuth();

// Remix
import { getAuth } from '@clerk/remix/ssr.server';

- const { user, organization, session } = await getAuth(args);
+ const { userId, organizationId, sessionId } = await getAuth(args);

// or
rootAuthLoader(
args,
({ request }) => {
- const { user, organization, session } = request.auth;
+ const { userId, organizationId, sessionId } = request.auth;
// ...
},
{ loadUser: true },
);
Comment thread
dimkl marked this conversation as resolved.

// NextJS
import { getAuth } from '@clerk/nextjs/server';

- const { user, organization, session } = getAuth(args);
+ const { userId, organizationId, sessionId } = getAuth(req, opts);
Comment on lines +35 to +38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking, did we have getAuth.user before v5?
Nitpick, but this should probably be on its own changeset targeting nextjs (similar for remix and gatsby)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes we had them in v4.
signed-in:


signed-out:

The change is applied only in the backend package and the changeset will add this change in the changelog of all the packages depending on backend, that's why i didn't add both remix, nextjs, gatsby.
@nikosdouvlis Do we want for each breaking change in backend to also add the other packages?


// Gatsby
import { withServerAuth } from 'gatsby-plugin-clerk';

export const getServerData: GetServerData<any> = withServerAuth(
async props => {
- const { user, organization, session } = props;
+ const { userId, organizationId, sessionId } = props;
return { props: { data: '1', auth: props.auth, userId, organizationId, sessionId } };
},
{ loadUser: true },
);
```

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

cc: @LekoArts Could you handle adding these changes in clerk-docs?

5 changes: 5 additions & 0 deletions .changeset/strange-trains-bow.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/remix': patch
---

Fix adding `user`/`sessions`/`organization` resources into request.
3 changes: 0 additions & 3 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,10 @@ function assertSignedOutToAuth(assert, requestState: RequestState) {
assert.propContains(requestState.toAuth(), {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
orgId: null,
orgRole: null,
orgSlug: null,
organization: null,
getToken: {},
});
}
Expand Down
6 changes: 0 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,15 +47,12 @@ export type SignedInAuthObject = {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
session: null;
actor: null;
userId: null;
user: null;
orgId: null;
orgRole: null;
orgSlug: null;
orgPermissions: null;
organization: null;
getToken: ServerGetToken;
has: CheckAuthorizationWithCustomPermissions;
debug: AuthObjectDebug;
Expand DownExpand Up@@ -148,15 +145,12 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
return {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
organization: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
Expand Down
79 changes: 8 additions & 71 deletions packages/remix/src/ssr/authenticateRequest.ts
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,18 @@
import { createClerkClient } from '@clerk/backend';
import type { SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';
import type { LoaderFunctionArgs } from './types';

export async function authenticateRequest(
args: LoaderFunctionArgs,
opts: RootAuthLoaderOptions = {},
opts: AuthenticateRequestOptions,
): Promise<SignedInState | SignedOutState> {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);
const { request } = args;
const { audience, authorizedParties } = opts;

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = opts.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';

if (!secretKey) {
throw new Error(noSecretKeyError);
}

const publishableKey = opts.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';

const jwtKey = opts.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);

const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);

const domain = handleValueOrFn(opts.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';

const isSatellite =
handleValueOrFn(opts.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

const signInUrl = opts.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';

const signUpUrl = opts.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';

const afterSignInUrl = opts.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';

const afterSignUpUrl = opts.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}

if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = opts;

const requestState = await createClerkClient({
apiUrl,
Expand All@@ -79,16 +21,11 @@ export async function authenticateRequest(
proxyUrl,
isSatellite,
domain,
publishableKey,
userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
}).authenticateRequest(request, {
audience,
secretKey,
jwtKey,
publishableKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
afterSignInUrl,
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { stripPrivateDataFromObject } from '@clerk/backend/internal';

import { noLoaderArgsPassedInGetAuth } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { GetAuthReturn, LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

type GetAuthOptions = Pick<RootAuthLoaderOptions, 'secretKey'>;
Expand All@@ -11,8 +12,9 @@ export async function getAuth(args: LoaderFunctionArgs, opts?: GetAuthOptions):
throw new Error(noLoaderArgsPassedInGetAuth);
}

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts || {});
const requestState = await authenticateRequest(args, loadedOptions);

return stripPrivateDataFromObject(requestState.toAuth());
}
72 changes: 72 additions & 0 deletions packages/remix/src/ssr/loadOptions.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
import { createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderOptions = {}) => {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
const publishableKey = overrides.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
const domain =
handleValueOrFn(overrides.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';
const isSatellite =
handleValueOrFn(overrides.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context));
const relativeOrAbsoluteProxyUrl = handleValueOrFn(
overrides?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);
const signInUrl = overrides.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';
const signUpUrl = overrides.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';
const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';
const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

if (!secretKey) {
throw new Error(noSecretKeyError);
}
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}

return {
// used to append options that are not initialized from env
...overrides,
secretKey,
publishableKey,
jwtKey,
apiUrl,
domain,
isSatellite,
proxyUrl,
signInUrl,
signUpUrl,
afterSignInUrl,
afterSignUpUrl,
};
};
9 changes: 6 additions & 3 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { isDeferredData } from '@remix-run/server-runtime/dist/responses';

import { invalidRootLoaderCallbackReturn } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import {
assertValidHandlerResult,
Expand DownExpand Up@@ -46,17 +47,19 @@ export const rootAuthLoader: RootAuthLoader = async (
? handlerOrOptions
: {};

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts);
const requestState = await authenticateRequest(args, loadedOptions);

if (!handler) {
// if the user did not provide a handler, simply inject requestState into an empty response
return injectRequestStateIntoResponse(new Response(JSON.stringify({})), requestState, args.context);
}

const authObj = requestState.toAuth();
Object.assign(args.request, { auth: authObj });
const handlerResult = await handler(await decorateObjectWithResources(args, authObj, opts));
const requestWithAuth = Object.assign(args.request, { auth: authObj });
await decorateObjectWithResources(requestWithAuth, authObj, loadedOptions);
const handlerResult = await handler(args);
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);

// When using defer(), we need to inject the clerk auth state into its internal data object.
Expand Down
1 change: 0 additions & 1 deletion playground/remix-node/app/entry.server.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
Expand Down
8 changes: 3 additions & 5 deletions playground/remix-node/app/root.tsx
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
import { defer, type DataFunctionArgs } from '@remix-run/node';
import type { MetaFunction } from '@remix-run/react';
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
import { rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp } from '@clerk/remix';
import { Suspense } from 'react';

export const loader = (args: DataFunctionArgs) => {
Expand DownExpand Up@@ -46,8 +46,6 @@ export const meta: MetaFunction = () => {
];
};

export const ErrorBoundary = ClerkErrorBoundary();

function App() {
const loaderData = useLoaderData<typeof loader>();

Expand Down
1 change: 1 addition & 0 deletions playground/remix-node/app/routes/sign-in.$.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ export default function SignInPage() {
<div style={{ border: '2px solid blue', padding: '2rem' }}>
<h1>Sign In route</h1>
<SignIn
path='/sign-in'
signUpUrl='/sign-up'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/remix-node/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
"typescript": "^5"
},
"engines": {
"node": ">=18"
Expand Down
8 changes: 0 additions & 8 deletions playground/remix-node/remix.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,4 @@
module.exports = {
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
v2_headers: true,
v2_dev: true,
},
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/afraid-experts-eat.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@clerk/backend': major
---

Drop `user` / `organization` / `session` from auth object on **signed-out** state (current value was `null`). Eg

```diff
// Backend
import { createClerkClient } from '@clerk/backend';

const clerkClient = createClerkClient({...});
const requestState = clerkClient.authenticateRequest(request, {...});

- const { user, organization, session } = requestState.toAuth();
+ const { userId, organizationId, sessionId } = requestState.toAuth();

// Remix
import { getAuth } from '@clerk/remix/ssr.server';

- const { user, organization, session } = await getAuth(args);
+ const { userId, organizationId, sessionId } = await getAuth(args);

// or
rootAuthLoader(
args,
({ request }) => {
- const { user, organization, session } = request.auth;
+ const { userId, organizationId, sessionId } = request.auth;
// ...
},
{ loadUser: true },
);
Comment thread
dimkl marked this conversation as resolved.

// NextJS
import { getAuth } from '@clerk/nextjs/server';

- const { user, organization, session } = getAuth(args);
+ const { userId, organizationId, sessionId } = getAuth(req, opts);
Comment on lines +35 to +38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking, did we have getAuth.user before v5?
Nitpick, but this should probably be on its own changeset targeting nextjs (similar for remix and gatsby)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes we had them in v4.
signed-in:


signed-out:

The change is applied only in the backend package and the changeset will add this change in the changelog of all the packages depending on backend, that's why i didn't add both remix, nextjs, gatsby.
@nikosdouvlis Do we want for each breaking change in backend to also add the other packages?


// Gatsby
import { withServerAuth } from 'gatsby-plugin-clerk';

export const getServerData: GetServerData<any> = withServerAuth(
async props => {
- const { user, organization, session } = props;
+ const { userId, organizationId, sessionId } = props;
return { props: { data: '1', auth: props.auth, userId, organizationId, sessionId } };
},
{ loadUser: true },
);
```

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

cc: @LekoArts Could you handle adding these changes in clerk-docs?

5 changes: 5 additions & 0 deletions .changeset/strange-trains-bow.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/remix': patch
---

Fix adding `user`/`sessions`/`organization` resources into request.
3 changes: 0 additions & 3 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,10 @@ function assertSignedOutToAuth(assert, requestState: RequestState) {
assert.propContains(requestState.toAuth(), {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
orgId: null,
orgRole: null,
orgSlug: null,
organization: null,
getToken: {},
});
}
Expand Down
6 changes: 0 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,15 +47,12 @@ export type SignedInAuthObject = {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
session: null;
actor: null;
userId: null;
user: null;
orgId: null;
orgRole: null;
orgSlug: null;
orgPermissions: null;
organization: null;
getToken: ServerGetToken;
has: CheckAuthorizationWithCustomPermissions;
debug: AuthObjectDebug;
Expand DownExpand Up@@ -148,15 +145,12 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
return {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
organization: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
Expand Down
79 changes: 8 additions & 71 deletions packages/remix/src/ssr/authenticateRequest.ts
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,18 @@
import { createClerkClient } from '@clerk/backend';
import type { SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';
import type { LoaderFunctionArgs } from './types';

export async function authenticateRequest(
args: LoaderFunctionArgs,
opts: RootAuthLoaderOptions = {},
opts: AuthenticateRequestOptions,
): Promise<SignedInState | SignedOutState> {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);
const { request } = args;
const { audience, authorizedParties } = opts;

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = opts.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';

if (!secretKey) {
throw new Error(noSecretKeyError);
}

const publishableKey = opts.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';

const jwtKey = opts.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);

const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);

const domain = handleValueOrFn(opts.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';

const isSatellite =
handleValueOrFn(opts.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

const signInUrl = opts.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';

const signUpUrl = opts.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';

const afterSignInUrl = opts.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';

const afterSignUpUrl = opts.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}

if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = opts;

const requestState = await createClerkClient({
apiUrl,
Expand All@@ -79,16 +21,11 @@ export async function authenticateRequest(
proxyUrl,
isSatellite,
domain,
publishableKey,
userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
}).authenticateRequest(request, {
audience,
secretKey,
jwtKey,
publishableKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
afterSignInUrl,
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { stripPrivateDataFromObject } from '@clerk/backend/internal';

import { noLoaderArgsPassedInGetAuth } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { GetAuthReturn, LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

type GetAuthOptions = Pick<RootAuthLoaderOptions, 'secretKey'>;
Expand All@@ -11,8 +12,9 @@ export async function getAuth(args: LoaderFunctionArgs, opts?: GetAuthOptions):
throw new Error(noLoaderArgsPassedInGetAuth);
}

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts || {});
const requestState = await authenticateRequest(args, loadedOptions);

return stripPrivateDataFromObject(requestState.toAuth());
}
72 changes: 72 additions & 0 deletions packages/remix/src/ssr/loadOptions.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
import { createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderOptions = {}) => {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
const publishableKey = overrides.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
const domain =
handleValueOrFn(overrides.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';
const isSatellite =
handleValueOrFn(overrides.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context));
const relativeOrAbsoluteProxyUrl = handleValueOrFn(
overrides?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);
const signInUrl = overrides.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';
const signUpUrl = overrides.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';
const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';
const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

if (!secretKey) {
throw new Error(noSecretKeyError);
}
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}

return {
// used to append options that are not initialized from env
...overrides,
secretKey,
publishableKey,
jwtKey,
apiUrl,
domain,
isSatellite,
proxyUrl,
signInUrl,
signUpUrl,
afterSignInUrl,
afterSignUpUrl,
};
};
9 changes: 6 additions & 3 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { isDeferredData } from '@remix-run/server-runtime/dist/responses';

import { invalidRootLoaderCallbackReturn } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import {
assertValidHandlerResult,
Expand DownExpand Up@@ -46,17 +47,19 @@ export const rootAuthLoader: RootAuthLoader = async (
? handlerOrOptions
: {};

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts);
const requestState = await authenticateRequest(args, loadedOptions);

if (!handler) {
// if the user did not provide a handler, simply inject requestState into an empty response
return injectRequestStateIntoResponse(new Response(JSON.stringify({})), requestState, args.context);
}

const authObj = requestState.toAuth();
Object.assign(args.request, { auth: authObj });
const handlerResult = await handler(await decorateObjectWithResources(args, authObj, opts));
const requestWithAuth = Object.assign(args.request, { auth: authObj });
await decorateObjectWithResources(requestWithAuth, authObj, loadedOptions);
const handlerResult = await handler(args);
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);

// When using defer(), we need to inject the clerk auth state into its internal data object.
Expand Down
1 change: 0 additions & 1 deletion playground/remix-node/app/entry.server.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
Expand Down
8 changes: 3 additions & 5 deletions playground/remix-node/app/root.tsx
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
import { defer, type DataFunctionArgs } from '@remix-run/node';
import type { MetaFunction } from '@remix-run/react';
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
import { rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp } from '@clerk/remix';
import { Suspense } from 'react';

export const loader = (args: DataFunctionArgs) => {
Expand DownExpand Up@@ -46,8 +46,6 @@ export const meta: MetaFunction = () => {
];
};

export const ErrorBoundary = ClerkErrorBoundary();

function App() {
const loaderData = useLoaderData<typeof loader>();

Expand Down
1 change: 1 addition & 0 deletions playground/remix-node/app/routes/sign-in.$.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ export default function SignInPage() {
<div style={{ border: '2px solid blue', padding: '2rem' }}>
<h1>Sign In route</h1>
<SignIn
path='/sign-in'
signUpUrl='/sign-up'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/remix-node/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
"typescript": "^5"
},
"engines": {
"node": ">=18"
Expand Down
8 changes: 0 additions & 8 deletions playground/remix-node/remix.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,4 @@
module.exports = {
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
v2_headers: true,
v2_dev: true,
},
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/afraid-experts-eat.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@clerk/backend': major
---

Drop `user` / `organization` / `session` from auth object on **signed-out** state (current value was `null`). Eg

```diff
// Backend
import { createClerkClient } from '@clerk/backend';

const clerkClient = createClerkClient({...});
const requestState = clerkClient.authenticateRequest(request, {...});

- const { user, organization, session } = requestState.toAuth();
+ const { userId, organizationId, sessionId } = requestState.toAuth();

// Remix
import { getAuth } from '@clerk/remix/ssr.server';

- const { user, organization, session } = await getAuth(args);
+ const { userId, organizationId, sessionId } = await getAuth(args);

// or
rootAuthLoader(
args,
({ request }) => {
- const { user, organization, session } = request.auth;
+ const { userId, organizationId, sessionId } = request.auth;
// ...
},
{ loadUser: true },
);
Comment thread
dimkl marked this conversation as resolved.

// NextJS
import { getAuth } from '@clerk/nextjs/server';

- const { user, organization, session } = getAuth(args);
+ const { userId, organizationId, sessionId } = getAuth(req, opts);
Comment on lines +35 to +38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking, did we have getAuth.user before v5?
Nitpick, but this should probably be on its own changeset targeting nextjs (similar for remix and gatsby)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes we had them in v4.
signed-in:


signed-out:

The change is applied only in the backend package and the changeset will add this change in the changelog of all the packages depending on backend, that's why i didn't add both remix, nextjs, gatsby.
@nikosdouvlis Do we want for each breaking change in backend to also add the other packages?


// Gatsby
import { withServerAuth } from 'gatsby-plugin-clerk';

export const getServerData: GetServerData<any> = withServerAuth(
async props => {
- const { user, organization, session } = props;
+ const { userId, organizationId, sessionId } = props;
return { props: { data: '1', auth: props.auth, userId, organizationId, sessionId } };
},
{ loadUser: true },
);
```

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

cc: @LekoArts Could you handle adding these changes in clerk-docs?

5 changes: 5 additions & 0 deletions .changeset/strange-trains-bow.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/remix': patch
---

Fix adding `user`/`sessions`/`organization` resources into request.
3 changes: 0 additions & 3 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,10 @@ function assertSignedOutToAuth(assert, requestState: RequestState) {
assert.propContains(requestState.toAuth(), {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
orgId: null,
orgRole: null,
orgSlug: null,
organization: null,
getToken: {},
});
}
Expand Down
6 changes: 0 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,15 +47,12 @@ export type SignedInAuthObject = {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
session: null;
actor: null;
userId: null;
user: null;
orgId: null;
orgRole: null;
orgSlug: null;
orgPermissions: null;
organization: null;
getToken: ServerGetToken;
has: CheckAuthorizationWithCustomPermissions;
debug: AuthObjectDebug;
Expand DownExpand Up@@ -148,15 +145,12 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
return {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
organization: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
Expand Down
79 changes: 8 additions & 71 deletions packages/remix/src/ssr/authenticateRequest.ts
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,18 @@
import { createClerkClient } from '@clerk/backend';
import type { SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';
import type { LoaderFunctionArgs } from './types';

export async function authenticateRequest(
args: LoaderFunctionArgs,
opts: RootAuthLoaderOptions = {},
opts: AuthenticateRequestOptions,
): Promise<SignedInState | SignedOutState> {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);
const { request } = args;
const { audience, authorizedParties } = opts;

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = opts.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';

if (!secretKey) {
throw new Error(noSecretKeyError);
}

const publishableKey = opts.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';

const jwtKey = opts.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);

const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);

const domain = handleValueOrFn(opts.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';

const isSatellite =
handleValueOrFn(opts.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

const signInUrl = opts.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';

const signUpUrl = opts.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';

const afterSignInUrl = opts.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';

const afterSignUpUrl = opts.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}

if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = opts;

const requestState = await createClerkClient({
apiUrl,
Expand All@@ -79,16 +21,11 @@ export async function authenticateRequest(
proxyUrl,
isSatellite,
domain,
publishableKey,
userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
}).authenticateRequest(request, {
audience,
secretKey,
jwtKey,
publishableKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
afterSignInUrl,
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { stripPrivateDataFromObject } from '@clerk/backend/internal';

import { noLoaderArgsPassedInGetAuth } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { GetAuthReturn, LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

type GetAuthOptions = Pick<RootAuthLoaderOptions, 'secretKey'>;
Expand All@@ -11,8 +12,9 @@ export async function getAuth(args: LoaderFunctionArgs, opts?: GetAuthOptions):
throw new Error(noLoaderArgsPassedInGetAuth);
}

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts || {});
const requestState = await authenticateRequest(args, loadedOptions);

return stripPrivateDataFromObject(requestState.toAuth());
}
72 changes: 72 additions & 0 deletions packages/remix/src/ssr/loadOptions.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
import { createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderOptions = {}) => {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
const publishableKey = overrides.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
const domain =
handleValueOrFn(overrides.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';
const isSatellite =
handleValueOrFn(overrides.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context));
const relativeOrAbsoluteProxyUrl = handleValueOrFn(
overrides?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);
const signInUrl = overrides.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';
const signUpUrl = overrides.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';
const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';
const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

if (!secretKey) {
throw new Error(noSecretKeyError);
}
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}

return {
// used to append options that are not initialized from env
...overrides,
secretKey,
publishableKey,
jwtKey,
apiUrl,
domain,
isSatellite,
proxyUrl,
signInUrl,
signUpUrl,
afterSignInUrl,
afterSignUpUrl,
};
};
9 changes: 6 additions & 3 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { isDeferredData } from '@remix-run/server-runtime/dist/responses';

import { invalidRootLoaderCallbackReturn } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import {
assertValidHandlerResult,
Expand DownExpand Up@@ -46,17 +47,19 @@ export const rootAuthLoader: RootAuthLoader = async (
? handlerOrOptions
: {};

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts);
const requestState = await authenticateRequest(args, loadedOptions);

if (!handler) {
// if the user did not provide a handler, simply inject requestState into an empty response
return injectRequestStateIntoResponse(new Response(JSON.stringify({})), requestState, args.context);
}

const authObj = requestState.toAuth();
Object.assign(args.request, { auth: authObj });
const handlerResult = await handler(await decorateObjectWithResources(args, authObj, opts));
const requestWithAuth = Object.assign(args.request, { auth: authObj });
await decorateObjectWithResources(requestWithAuth, authObj, loadedOptions);
const handlerResult = await handler(args);
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);

// When using defer(), we need to inject the clerk auth state into its internal data object.
Expand Down
1 change: 0 additions & 1 deletion playground/remix-node/app/entry.server.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
Expand Down
8 changes: 3 additions & 5 deletions playground/remix-node/app/root.tsx
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
import { defer, type DataFunctionArgs } from '@remix-run/node';
import type { MetaFunction } from '@remix-run/react';
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
import { rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp } from '@clerk/remix';
import { Suspense } from 'react';

export const loader = (args: DataFunctionArgs) => {
Expand DownExpand Up@@ -46,8 +46,6 @@ export const meta: MetaFunction = () => {
];
};

export const ErrorBoundary = ClerkErrorBoundary();

function App() {
const loaderData = useLoaderData<typeof loader>();

Expand Down
1 change: 1 addition & 0 deletions playground/remix-node/app/routes/sign-in.$.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ export default function SignInPage() {
<div style={{ border: '2px solid blue', padding: '2rem' }}>
<h1>Sign In route</h1>
<SignIn
path='/sign-in'
signUpUrl='/sign-up'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/remix-node/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
"typescript": "^5"
},
"engines": {
"node": ">=18"
Expand Down
8 changes: 0 additions & 8 deletions playground/remix-node/remix.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,4 @@
module.exports = {
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
v2_headers: true,
v2_dev: true,
},
};
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .changeset/afraid-experts-eat.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
---
'@clerk/backend': major
---

Drop `user` / `organization` / `session` from auth object on **signed-out** state (current value was `null`). Eg

```diff
// Backend
import { createClerkClient } from '@clerk/backend';

const clerkClient = createClerkClient({...});
const requestState = clerkClient.authenticateRequest(request, {...});

- const { user, organization, session } = requestState.toAuth();
+ const { userId, organizationId, sessionId } = requestState.toAuth();

// Remix
import { getAuth } from '@clerk/remix/ssr.server';

- const { user, organization, session } = await getAuth(args);
+ const { userId, organizationId, sessionId } = await getAuth(args);

// or
rootAuthLoader(
args,
({ request }) => {
- const { user, organization, session } = request.auth;
+ const { userId, organizationId, sessionId } = request.auth;
// ...
},
{ loadUser: true },
);
Comment thread
dimkl marked this conversation as resolved.

// NextJS
import { getAuth } from '@clerk/nextjs/server';

- const { user, organization, session } = getAuth(args);
+ const { userId, organizationId, sessionId } = getAuth(req, opts);
Comment on lines +35 to +38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just checking, did we have getAuth.user before v5?
Nitpick, but this should probably be on its own changeset targeting nextjs (similar for remix and gatsby)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Yes we had them in v4.
signed-in:


signed-out:

The change is applied only in the backend package and the changeset will add this change in the changelog of all the packages depending on backend, that's why i didn't add both remix, nextjs, gatsby.
@nikosdouvlis Do we want for each breaking change in backend to also add the other packages?


// Gatsby
import { withServerAuth } from 'gatsby-plugin-clerk';

export const getServerData: GetServerData<any> = withServerAuth(
async props => {
- const { user, organization, session } = props;
+ const { userId, organizationId, sessionId } = props;
return { props: { data: '1', auth: props.auth, userId, organizationId, sessionId } };
},
{ loadUser: true },
);
```

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

cc: @LekoArts Could you handle adding these changes in clerk-docs?

5 changes: 5 additions & 0 deletions .changeset/strange-trains-bow.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
'@clerk/remix': patch
---

Fix adding `user`/`sessions`/`organization` resources into request.
3 changes: 0 additions & 3 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -41,13 +41,10 @@ function assertSignedOutToAuth(assert, requestState: RequestState) {
assert.propContains(requestState.toAuth(), {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
orgId: null,
orgRole: null,
orgSlug: null,
organization: null,
getToken: {},
});
}
Expand Down
6 changes: 0 additions & 6 deletions packages/backend/src/tokens/authObjects.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,15 +47,12 @@ export type SignedInAuthObject = {
export type SignedOutAuthObject = {
sessionClaims: null;
sessionId: null;
session: null;
actor: null;
userId: null;
user: null;
orgId: null;
orgRole: null;
orgSlug: null;
orgPermissions: null;
organization: null;
getToken: ServerGetToken;
has: CheckAuthorizationWithCustomPermissions;
debug: AuthObjectDebug;
Expand DownExpand Up@@ -148,15 +145,12 @@ export function signedOutAuthObject(debugData?: AuthObjectDebugData): SignedOutA
return {
sessionClaims: null,
sessionId: null,
session: null,
userId: null,
user: null,
actor: null,
orgId: null,
orgRole: null,
orgSlug: null,
orgPermissions: null,
organization: null,
getToken: () => Promise.resolve(null),
has: () => false,
debug: createDebug(debugData),
Expand Down
79 changes: 8 additions & 71 deletions packages/remix/src/ssr/authenticateRequest.ts
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,18 @@
import { createClerkClient } from '@clerk/backend';
import type { SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';
import type { AuthenticateRequestOptions, SignedInState, SignedOutState } from '@clerk/backend/internal';
import { AuthStatus } from '@clerk/backend/internal';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';
import type { LoaderFunctionArgs } from './types';

export async function authenticateRequest(
args: LoaderFunctionArgs,
opts: RootAuthLoaderOptions = {},
opts: AuthenticateRequestOptions,
): Promise<SignedInState | SignedOutState> {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);
const { request } = args;
const { audience, authorizedParties } = opts;

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = opts.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';

if (!secretKey) {
throw new Error(noSecretKeyError);
}

const publishableKey = opts.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';

const jwtKey = opts.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);

const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);

const domain = handleValueOrFn(opts.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';

const isSatellite =
handleValueOrFn(opts.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context)) ||
false;

const relativeOrAbsoluteProxyUrl = handleValueOrFn(
opts?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

const signInUrl = opts.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';

const signUpUrl = opts.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';

const afterSignInUrl = opts.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';

const afterSignUpUrl = opts.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}

if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}
const { apiUrl, secretKey, jwtKey, proxyUrl, isSatellite, domain, publishableKey } = opts;
const { signInUrl, signUpUrl, afterSignInUrl, afterSignUpUrl } = opts;

const requestState = await createClerkClient({
apiUrl,
Expand All@@ -79,16 +21,11 @@ export async function authenticateRequest(
proxyUrl,
isSatellite,
domain,
publishableKey,
userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
}).authenticateRequest(request, {
audience,
secretKey,
jwtKey,
publishableKey,
authorizedParties,
proxyUrl,
isSatellite,
domain,
signInUrl,
signUpUrl,
afterSignInUrl,
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuth.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { stripPrivateDataFromObject } from '@clerk/backend/internal';

import { noLoaderArgsPassedInGetAuth } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { GetAuthReturn, LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

type GetAuthOptions = Pick<RootAuthLoaderOptions, 'secretKey'>;
Expand All@@ -11,8 +12,9 @@ export async function getAuth(args: LoaderFunctionArgs, opts?: GetAuthOptions):
throw new Error(noLoaderArgsPassedInGetAuth);
}

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts || {});
const requestState = await authenticateRequest(args, loadedOptions);

return stripPrivateDataFromObject(requestState.toAuth());
}
72 changes: 72 additions & 0 deletions packages/remix/src/ssr/loadOptions.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
import { createClerkRequest } from '@clerk/backend/internal';
import { apiUrlFromPublishableKey } from '@clerk/shared/apiUrlFromPublishableKey';
import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
import { isHttpOrHttps, isProxyUrlRelative } from '@clerk/shared/proxy';
import { isTruthy } from '@clerk/shared/underscore';

import { noSecretKeyError, satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from '../errors';
import { getEnvVariable } from '../utils';
import type { LoaderFunctionArgs, RootAuthLoaderOptions } from './types';

export const loadOptions = (args: LoaderFunctionArgs, overrides: RootAuthLoaderOptions = {}) => {
const { request, context } = args;
const clerkRequest = createClerkRequest(request);

// Fetch environment variables across Remix runtime.
// 1. First check if the user passed the key in the getAuth function or the rootAuthLoader.
// 2. Then try from process.env if exists (Node).
// 3. Then try from globalThis (Cloudflare Workers).
// 4. Then from loader context (Cloudflare Pages).
const secretKey = overrides.secretKey || getEnvVariable('CLERK_SECRET_KEY', context) || '';
const publishableKey = overrides.publishableKey || getEnvVariable('CLERK_PUBLISHABLE_KEY', context) || '';
const jwtKey = overrides.jwtKey || getEnvVariable('CLERK_JWT_KEY', context);
const apiUrl = getEnvVariable('CLERK_API_URL', context) || apiUrlFromPublishableKey(publishableKey);
const domain =
handleValueOrFn(overrides.domain, new URL(request.url)) || getEnvVariable('CLERK_DOMAIN', context) || '';
const isSatellite =
handleValueOrFn(overrides.isSatellite, new URL(request.url)) ||
isTruthy(getEnvVariable('CLERK_IS_SATELLITE', context));
const relativeOrAbsoluteProxyUrl = handleValueOrFn(
overrides?.proxyUrl,
clerkRequest.clerkUrl,
getEnvVariable('CLERK_PROXY_URL', context),
);
const signInUrl = overrides.signInUrl || getEnvVariable('CLERK_SIGN_IN_URL', context) || '';
const signUpUrl = overrides.signUpUrl || getEnvVariable('CLERK_SIGN_UP_URL', context) || '';
const afterSignInUrl = overrides.afterSignInUrl || getEnvVariable('CLERK_AFTER_SIGN_IN_URL', context) || '';
const afterSignUpUrl = overrides.afterSignUpUrl || getEnvVariable('CLERK_AFTER_SIGN_UP_URL', context) || '';

let proxyUrl;
if (!!relativeOrAbsoluteProxyUrl && isProxyUrlRelative(relativeOrAbsoluteProxyUrl)) {
proxyUrl = new URL(relativeOrAbsoluteProxyUrl, clerkRequest.clerkUrl).toString();
} else {
proxyUrl = relativeOrAbsoluteProxyUrl;
}

if (!secretKey) {
throw new Error(noSecretKeyError);
}
if (isSatellite && !proxyUrl && !domain) {
throw new Error(satelliteAndMissingProxyUrlAndDomain);
}
if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey)) {
throw new Error(satelliteAndMissingSignInUrl);
}

return {
// used to append options that are not initialized from env
...overrides,
secretKey,
publishableKey,
jwtKey,
apiUrl,
domain,
isSatellite,
proxyUrl,
signInUrl,
signUpUrl,
afterSignInUrl,
afterSignUpUrl,
};
};
9 changes: 6 additions & 3 deletions packages/remix/src/ssr/rootAuthLoader.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ import { isDeferredData } from '@remix-run/server-runtime/dist/responses';

import { invalidRootLoaderCallbackReturn } from '../errors';
import { authenticateRequest } from './authenticateRequest';
import { loadOptions } from './loadOptions';
import type { LoaderFunctionArgs, LoaderFunctionReturn, RootAuthLoaderCallback, RootAuthLoaderOptions } from './types';
import {
assertValidHandlerResult,
Expand DownExpand Up@@ -46,17 +47,19 @@ export const rootAuthLoader: RootAuthLoader = async (
? handlerOrOptions
: {};

const loadedOptions = loadOptions(args, opts);
// Note: authenticateRequest() will throw a redirect if the auth state is determined to be handshake
const requestState = await authenticateRequest(args, opts);
const requestState = await authenticateRequest(args, loadedOptions);

if (!handler) {
// if the user did not provide a handler, simply inject requestState into an empty response
return injectRequestStateIntoResponse(new Response(JSON.stringify({})), requestState, args.context);
}

const authObj = requestState.toAuth();
Object.assign(args.request, { auth: authObj });
const handlerResult = await handler(await decorateObjectWithResources(args, authObj, opts));
const requestWithAuth = Object.assign(args.request, { auth: authObj });
await decorateObjectWithResources(requestWithAuth, authObj, loadedOptions);
const handlerResult = await handler(args);
assertValidHandlerResult(handlerResult, invalidRootLoaderCallbackReturn);

// When using defer(), we need to inject the clerk auth state into its internal data object.
Expand Down
1 change: 0 additions & 1 deletion playground/remix-node/app/entry.server.tsx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
import { PassThrough } from 'stream';
import type { EntryContext } from '@remix-run/node';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
Expand Down
8 changes: 3 additions & 5 deletions playground/remix-node/app/root.tsx
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import { defer, type DataFunctionArgs, type Headers } from '@remix-run/node';
import { defer, type DataFunctionArgs } from '@remix-run/node';
import type { MetaFunction } from '@remix-run/react';
import { Await, Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData } from '@remix-run/react';
import { getClerkDebugHeaders, rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp, ClerkErrorBoundary } from '@clerk/remix';
import { rootAuthLoader } from '@clerk/remix/ssr.server';
import { ClerkApp } from '@clerk/remix';
import { Suspense } from 'react';

export const loader = (args: DataFunctionArgs) => {
Expand DownExpand Up@@ -46,8 +46,6 @@ export const meta: MetaFunction = () => {
];
};

export const ErrorBoundary = ClerkErrorBoundary();

function App() {
const loaderData = useLoaderData<typeof loader>();

Expand Down
1 change: 1 addition & 0 deletions playground/remix-node/app/routes/sign-in.$.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ export default function SignInPage() {
<div style={{ border: '2px solid blue', padding: '2rem' }}>
<h1>Sign In route</h1>
<SignIn
path='/sign-in'
signUpUrl='/sign-up'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion playground/remix-node/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,7 @@
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"eslint": "^8.27.0",
"typescript": "^4.8.4"
"typescript": "^5"
},
"engines": {
"node": ">=18"
Expand Down
8 changes: 0 additions & 8 deletions playground/remix-node/remix.config.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,4 @@
module.exports = {
ignoredRouteFiles: ['**/.*'],
serverModuleFormat: 'cjs',
future: {
v2_errorBoundary: true,
v2_meta: true,
v2_normalizeFormMethod: true,
v2_routeConvention: true,
v2_headers: true,
v2_dev: true,
},
};