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
5 changes: 5 additions & 0 deletions .changeset/soft-kiwis-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@asgardeo/nextjs': patch
---

Enhance route protection with createRouteMatcher and session validation
4 changes: 4 additions & 0 deletions packages/nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ export type {UserProfileProps} from './client/components/presentation/UserProfil
export {default as AsgardeoNext} from './AsgardeoNextClient';

export {default as asgardeoMiddleware} from './middleware/asgardeoMiddleware';
export * from './middleware/asgardeoMiddleware';

export {default as createRouteMatcher} from './middleware/createRouteMatcher';
export * from './middleware/createRouteMatcher';
201 changes: 116 additions & 85 deletions packages/nextjs/src/middleware/asgardeoMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,55 @@
*/

import {NextRequest, NextResponse} from 'next/server';
import AsgardeoNextClient from '../AsgardeoNextClient';
import {CookieConfig} from '@asgardeo/node';
import {AsgardeoNextConfig} from '../models/config';

export interface AsgardeoMiddlewareOptions extends Partial<AsgardeoNextConfig> {
debug?: boolean;
}

type AsgardeoAuth = {
protect: (options?: {redirect?: string}) => Promise<NextResponse | void>;
isSignedIn: () => Promise<boolean>;
getUser: () => Promise<any | null>;
redirectToSignIn: (afterSignInUrl?: string) => NextResponse;
export type AsgardeoMiddlewareOptions = Partial<AsgardeoNextConfig>;

export type AsgardeoMiddlewareContext = {
/**
* Protect a route by redirecting unauthenticated users.
* Redirect URL fallback order:
* 1. options.redirect
* 2. resolvedOptions.signInUrl
* 3. resolvedOptions.defaultRedirect
* 4. referer (if from same origin)
* If none are available, throws an error.
*/
protectRoute: (options?: {redirect?: string}) => Promise<NextResponse | void>;
/** Check if the current request has a valid Asgardeo session */
isSignedIn: () => boolean;
/** Get the session ID from the current request */
getSessionId: () => string | undefined;
};

type AsgardeoMiddlewareHandler = (
auth: AsgardeoAuth,
asgardeo: AsgardeoMiddlewareContext,
req: NextRequest,
) => Promise<NextResponse | void> | NextResponse | void;

/**
* Checks if a request has a valid session ID in cookies.
* This is a lightweight check that can be used in middleware.
*
* @param request - The Next.js request object
* @returns True if a session ID exists, false otherwise
*/
const hasValidSession = (request: NextRequest): boolean => {
const sessionId = request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value;
return Boolean(sessionId && sessionId.trim().length > 0);
};

/**
* Gets the session ID from the request cookies.
*
* @param request - The Next.js request object
* @returns The session ID if it exists, undefined otherwise
*/
const getSessionIdFromRequest = (request: NextRequest): string | undefined => {
return request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value;
};

/**
* Asgardeo middleware that integrates authentication into your Next.js application.
* Similar to Clerk's clerkMiddleware pattern.
Expand All @@ -46,23 +76,49 @@ type AsgardeoMiddlewareHandler = (
*
* @example
* ```typescript
* // middleware.ts
* // middleware.ts - Basic usage
* import { asgardeoMiddleware } from '@asgardeo/nextjs';
*
* export default asgardeoMiddleware();
* ```
*
* @example
* ```typescript
* // With protection
* // With route protection
* import { asgardeoMiddleware, createRouteMatcher } from '@asgardeo/nextjs';
*
* const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);
*
* export default asgardeoMiddleware(async (auth, req) => {
* export default asgardeoMiddleware(async (asgardeo, req) => {
* if (isProtectedRoute(req)) {
* await auth.protect();
* await asgardeo.protectRoute();
* }
* });
* ```
*
* @example
* ```typescript
* // Advanced usage with custom logic
* import { asgardeoMiddleware, createRouteMatcher } from '@asgardeo/nextjs';
*
* const isProtectedRoute = createRouteMatcher(['/dashboard(.*)']);
* const isAuthRoute = createRouteMatcher(['/sign-in', '/sign-up']);
*
* export default asgardeoMiddleware(async (asgardeo, req) => {
* // Skip protection for auth routes
* if (isAuthRoute(req)) return;
*
* // Protect specified routes
* if (isProtectedRoute(req)) {
* await asgardeo.protectRoute({ redirect: '/sign-in' });
* }
*
* // Check authentication status
* if (asgardeo.isSignedIn()) {
* console.log('User is authenticated with session:', asgardeo.getSessionId());
* }
* }, {
* defaultRedirect: '/sign-in'
* });
* ```
*/
Expand All @@ -71,78 +127,53 @@ const asgardeoMiddleware = (
options?: AsgardeoMiddlewareOptions | ((req: NextRequest) => AsgardeoMiddlewareOptions),
): ((request: NextRequest) => Promise<NextResponse>) => {
return async (request: NextRequest): Promise<NextResponse> => {
// Resolve options - can be static or dynamic based on request
const resolvedOptions = typeof options === 'function' ? options(request) : options || {};

const asgardeoClient = AsgardeoNextClient.getInstance();

// // Initialize client if not already done
// if (!asgardeoClient.isInitialized && resolvedOptions) {
// asgardeoClient.initialize(resolvedOptions);
// }

// // Debug logging
// if (resolvedOptions.debug) {
// console.log(`[Asgardeo Middleware] Processing request: ${request.nextUrl.pathname}`);
// }

// // Handle auth API routes automatically
// if (request.nextUrl.pathname.startsWith('/api/auth/asgardeo')) {
// if (resolvedOptions.debug) {
// console.log(`[Asgardeo Middleware] Handling auth route: ${request.nextUrl.pathname}`);
// }
// return await asgardeoClient.handleAuthRequest(request);
// }

// // Create auth object for the handler
// const auth: AsgardeoAuth = {
// protect: async (options?: {redirect?: string}) => {
// const isSignedIn = await asgardeoClient.isSignedIn(request);
// if (!isSignedIn) {
// const afterSignInUrl = options?.redirect || '/api/auth/asgardeo/signin';
// return NextResponse.redirect(new URL(afterSignInUrl, request.url));
// }
// },

// isSignedIn: async () => {
// return await asgardeoClient.isSignedIn(request);
// },

// getUser: async () => {
// return await asgardeoClient.getUser(request);
// },

// redirectToSignIn: (afterSignInUrl?: string) => {
// const signInUrl = afterSignInUrl || '/api/auth/asgardeo/signin';
// return NextResponse.redirect(new URL(signInUrl, request.url));
// },
// };

// // Execute user-provided handler if present
// let handlerResponse: NextResponse | void;
// if (handler) {
// handlerResponse = await handler(auth, request);
// }

// // If handler returned a response, use it
// if (handlerResponse) {
// return handlerResponse;
// }

// // Otherwise, continue with default behavior
// const response = NextResponse.next();

// // Add authentication context to response headers
// const isSignedIn = await asgardeoClient.isSignedIn(request);
// if (isSignedIn) {
// response.headers.set('x-asgardeo-authenticated', 'true');
// const user = await asgardeoClient.getUser(request);
// if (user?.sub) {
// response.headers.set('x-asgardeo-user-id', user.sub);
// }
// }

// return response;
const sessionId = request.cookies.get(CookieConfig.SESSION_COOKIE_NAME)?.value;
const isAuthenticated = hasValidSession(request);

const asgardeo: AsgardeoMiddlewareContext = {
protectRoute: async (options?: {redirect?: string}): Promise<NextResponse | void> => {
if (!isAuthenticated) {
const referer = request.headers.get('referer');
// TODO: Make this configurable or call the signIn() from here.
let fallbackRedirect: string = '/';

// If referer exists and is from the same origin, use it as fallback
if (referer) {
try {
const refererUrl = new URL(referer);
const requestUrl = new URL(request.url);

if (refererUrl.origin === requestUrl.origin) {
fallbackRedirect = refererUrl.pathname + refererUrl.search;
}
} catch (error) {
// Invalid referer URL, ignore it
}
}

// Fallback chain: options.redirect -> resolvedOptions.signInUrl -> resolvedOptions.defaultRedirect -> referer (same origin only)
const redirectUrl: string = (resolvedOptions?.signInUrl as string) || fallbackRedirect;

const signInUrl = new URL(redirectUrl, request.url);

return NextResponse.redirect(signInUrl);
}

// Session exists, allow access
return;
},
isSignedIn: () => isAuthenticated,
getSessionId: () => sessionId,
};

if (handler) {
const result = await handler(asgardeo, request);
if (result) {
return result;
}
}

return NextResponse.next();
};
Expand Down
57 changes: 57 additions & 0 deletions packages/nextjs/src/middleware/createRouteMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {NextRequest} from 'next/server';

/**
* Creates a route matcher function that tests if a request matches any of the given patterns.
*
* @param patterns - Array of route patterns to match. Supports glob-like patterns.
* @returns Function that tests if a request matches any of the patterns
*
* @example
* ```typescript
* const isProtectedRoute = createRouteMatcher([
* '/dashboard(.*)',
* '/admin(.*)',
* '/profile'
* ]);
*
* if (isProtectedRoute(req)) {
* // Route is protected
* }
* ```
*/
const createRouteMatcher = (patterns: string[]) => {
const regexPatterns = patterns.map(pattern => {
// Convert glob-like patterns to regex
const regexPattern = pattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*') // Convert * to .*
.replace(/\(\.\*\)/g, '(.*)'); // Handle explicit (.*) patterns

return new RegExp(`^${regexPattern}$`);
});

return (req: NextRequest): boolean => {
const pathname = req.nextUrl.pathname;
return regexPatterns.some(regex => regex.test(pathname));
};
};

export default createRouteMatcher;
55 changes: 55 additions & 0 deletions packages/nextjs/src/utils/createRouteMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {NextRequest} from 'next/server';

/**
* Creates a route matcher function that tests if a request matches any of the given patterns.
*
* @param patterns - Array of route patterns to match. Supports glob-like patterns.
* @returns Function that tests if a request matches any of the patterns
*
* @example
* ```typescript
* const isProtectedRoute = createRouteMatcher([
* '/dashboard(.*)',
* '/admin(.*)',
* '/profile'
* ]);
*
* if (isProtectedRoute(req)) {
* // Route is protected
* }
* ```
*/
export const createRouteMatcher = (patterns: string[]) => {
const regexPatterns = patterns.map(pattern => {
// Convert glob-like patterns to regex
const regexPattern = pattern
.replace(/\./g, '\\.') // Escape dots
.replace(/\*/g, '.*') // Convert * to .*
.replace(/\(\.\*\)/g, '(.*)'); // Handle explicit (.*) patterns

return new RegExp(`^${regexPattern}$`);
});

return (req: NextRequest): boolean => {
const pathname = req.nextUrl.pathname;
return regexPatterns.some(regex => regex.test(pathname));
};
};
Loading
Loading