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
6 changes: 6 additions & 0 deletions .changeset/hot-pets-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@clerk/astro": patch
---

Support `Astro.locals.auth().redirectToSignIn()`
This allows for redirectingToSignIn at the page level
5 changes: 4 additions & 1 deletion integration/templates/astro-node/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const unautorized = () =>
/**
* 3. Support handler
*/
const isProtectedPage = createRouteMatcher(['/user(.*)', '/discover(.*)', /^\/organization/]);
const isProtectedPage = createRouteMatcher(['/user(.*)', '/discover(.*)']);

const isProtectedApiRoute = createRouteMatcher(['/api/protected(.*)']);

Expand All @@ -27,6 +27,9 @@ export const onRequest = clerkMiddleware((auth, context, next) => {
}

if (!auth().orgId && requestURL.pathname !== '/discover' && requestURL.pathname === '/organization') {
if (!auth().userId) {
return next();
}
const searchParams = new URLSearchParams({
redirectUrl: requestURL.href,
});
Expand Down
6 changes: 6 additions & 0 deletions integration/templates/astro-node/src/pages/organization.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import {
} from "@clerk/astro/components";
import Layout from "../layouts/Layout.astro";
import Streaming from "../layouts/Streaming.astro";

const { userId, redirectToSignIn } = Astro.locals.auth()

if(!userId) {
return redirectToSignIn()
}
---

<Layout title="Welcome to Astro.">
Expand Down
36 changes: 36 additions & 0 deletions integration/tests/astro/redirect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { test } from '@playwright/test';

import type { FakeUser } from '../../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../../testUtils';

testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('redirectToSignIn for @astro', ({ app }) => {
test.describe.configure({ mode: 'parallel' });
let fakeUser: FakeUser;

test.beforeAll(async () => {
const m = createTestUtils({ app });
fakeUser = m.services.users.createFakeUser();
await m.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('redirects to sign-in when unauthenticated (middleware)', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToStart();
await u.page.getByRole('link', { name: 'User', exact: true }).click();
await u.page.waitForURL(`${app.serverUrl}/sign-in?redirect_url=${encodeURIComponent(`${app.serverUrl}/user`)}`);
});

test('redirects to sign-in when unauthenticated (page)', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToStart();
await u.page.getByRole('link', { name: 'Organization', exact: true }).click();
await u.page.waitForURL(
`${app.serverUrl}/sign-in?redirect_url=${encodeURIComponent(`${app.serverUrl}/organization`)}`,
);
});
});
4 changes: 3 additions & 1 deletion packages/astro/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ declare namespace App {
authStatus: string;
authMessage: string | null;
authReason: string | null;
auth: () => import('@clerk/astro/server').GetAuthReturn;
auth: () => import('@clerk/astro/server').GetAuthReturn & {
redirectToSignIn: import('@clerk/backend/internal').RedirectFun<Response>;
};
currentUser: () => Promise<import('@clerk/astro/server').User | null>;
}
}
40 changes: 35 additions & 5 deletions packages/astro/src/server/clerk-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { ClerkClient } from '@clerk/backend';
import type { AuthenticateRequestOptions, AuthObject, ClerkRequest, RequestState } from '@clerk/backend/internal';
import type {
AuthenticateRequestOptions,
AuthObject,
ClerkRequest,
RedirectFun,
RequestState,
} from '@clerk/backend/internal';
import { AuthStatus, constants, createClerkRequest, createRedirect } from '@clerk/backend/internal';
import { handleValueOrFn, isDevelopmentFromSecretKey, isHttpOrHttps } from '@clerk/shared';
import { eventMethodCalled } from '@clerk/shared/telemetry';
Expand Down Expand Up @@ -87,7 +93,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {
const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest);
const authObjWithMethods: ClerkMiddlewareAuthObject = Object.assign(authObject, { redirectToSignIn });

decorateAstroLocal(context.request, context, requestState);
decorateAstroLocal(clerkRequest, context, requestState);

/**
* ALS is crucial for guaranteeing SSR in UI frameworks like React.
Expand Down Expand Up @@ -218,14 +224,38 @@ Check if signInUrl is missing from your configuration or if it is not an absolut
PUBLIC_CLERK_SIGN_IN_URL='SOME_URL'
PUBLIC_CLERK_IS_SATELLITE='true'`;

function decorateAstroLocal(req: Request, context: APIContext, requestState: RequestState) {
function decorateAstroLocal(clerkRequest: ClerkRequest, context: APIContext, requestState: RequestState) {
const { reason, message, status, token } = requestState;
context.locals.authToken = token;
context.locals.authStatus = status;
context.locals.authMessage = message;
context.locals.authReason = reason;
context.locals.auth = () => getAuth(req, context.locals);
context.locals.currentUser = createCurrentUser(req, context);
context.locals.auth = () => {
const authObject = getAuth(clerkRequest, context.locals);

const clerkUrl = clerkRequest.clerkUrl;

const redirectToSignIn: RedirectFun<Response> = (opts = {}) => {
const devBrowserToken =
clerkRequest.clerkUrl.searchParams.get(constants.QueryParameters.DevBrowser) ||
clerkRequest.cookies.get(constants.Cookies.DevBrowser);

return createRedirect({
redirectAdapter,
devBrowserToken: devBrowserToken,
baseUrl: clerkUrl.toString(),
publishableKey: getSafeEnv(context).pk!,
signInUrl: requestState.signInUrl,
signUpUrl: requestState.signUpUrl,
}).redirectToSignIn({
returnBackUrl: opts.returnBackUrl === null ? '' : opts.returnBackUrl || clerkUrl.toString(),
Comment on lines +250 to +251

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.

Does the redirectToSignUp function commonly used?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We've build it, but no public API consumes it. and this implementation aims to mirror the API from @clerk/nextjs.

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.

Thanks for answering!

});
};

return Object.assign(authObject, { redirectToSignIn });
};

context.locals.currentUser = createCurrentUser(clerkRequest, context);
}

/**
Expand Down
23 changes: 4 additions & 19 deletions packages/astro/src/server/get-auth.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
import {
type AuthObject,
AuthStatus,
type SignedInAuthObject,
signedInAuthObject,
type SignedOutAuthObject,
signedOutAuthObject,
} from '@clerk/backend/internal';
import { type AuthObject, AuthStatus, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal';
import { decodeJwt } from '@clerk/backend/jwt';
import type { APIContext } from 'astro';

import { getSafeEnv } from './get-safe-env';
import { getAuthKeyFromRequest } from './utils';

type AuthObjectWithoutResources<T extends AuthObject> = Omit<T, 'user' | 'organization' | 'session'>;

export type GetAuthReturn =
| AuthObjectWithoutResources<SignedInAuthObject>
| AuthObjectWithoutResources<SignedOutAuthObject>;
export type GetAuthReturn = AuthObject;

export const createGetAuth = ({ noAuthStatusMessage }: { noAuthStatusMessage: string }) => {
return (
req: Request,
locals: APIContext['locals'],
opts?: { secretKey?: string },
): AuthObjectWithoutResources<SignedInAuthObject> | AuthObjectWithoutResources<SignedOutAuthObject> => {
return (req: Request, locals: APIContext['locals'], opts?: { secretKey?: string }): GetAuthReturn => {
// When the auth status is set, we trust that the middleware has already run
// Then, we don't have to re-verify the JWT here,
// we can just strip out the claims manually.
Expand Down Expand Up @@ -62,5 +47,5 @@ const authAuthHeaderMissing = (helperName = 'auth') =>
`;

export const getAuth = createGetAuth({
noAuthStatusMessage: authAuthHeaderMissing('getAuth'),
noAuthStatusMessage: authAuthHeaderMissing(),
});