diff --git a/.changeset/console-noise-fixes.md b/.changeset/console-noise-fixes.md
new file mode 100644
index 000000000..052782f97
--- /dev/null
+++ b/.changeset/console-noise-fixes.md
@@ -0,0 +1,9 @@
+---
+'@asgardeo/nextjs': patch
+'@asgardeo/react': patch
+---
+
+- Next.js: cross-origin redirects (the identity server's hosted sign-in, sign-up and logout endpoints) are now performed with a full browser navigation instead of the Next.js app router. Handing those URLs to the router made it request them as a React Server Components payload first, which the browser blocked with a CORS error before the router fell back to a normal navigation, leaving "Failed to fetch RSC payload" errors in the console on every sign-out.
+- React: dialogs (for example the popup mode of `` opened from the user dropdown) now move focus onto the dialog when they open. Previously focus stayed on the trigger, which the focus manager had just hidden from assistive technology with `aria-hidden`, and browsers reported "Blocked aria-hidden on an element because its descendant retained focus".
+- Next.js: the OAuth callback handler no longer runs inside the popup opened by the embedded sign-in/sign-up flows, which logged a spurious "Authentication failed" on every social login even though the flow completed.
+- Next.js: the server-side log level can be raised with `ASGARDEO_LOG_LEVEL` (`debug`, `info`, `warn`, `error`; default `error`), for example to see why a profile fell back to the ID token claims.
diff --git a/packages/nextjs/README.md b/packages/nextjs/README.md
index 6249fb398..704b75b88 100644
--- a/packages/nextjs/README.md
+++ b/packages/nextjs/README.md
@@ -32,6 +32,15 @@ A missing entry surfaces as Google's `Error 400: redirect_uri_mismatch`.
`afterSignOutUrl` (default: the app origin) is sent as the post-logout redirect URI and must be registered as well.
+## Logging
+
+The SDK logs at `error` level by default. Set `ASGARDEO_LOG_LEVEL` to `warn`, `info` or `debug` to see more,
+for example why a user's profile fell back to the ID token claims:
+
+```bash
+ASGARDEO_LOG_LEVEL=warn
+```
+
## API Documentation
For complete API documentation including all components, hooks, and customization options, see the [Next.js SDK Documentation](https://wso2.com/asgardeo/docs/sdks/nextjs/overview).
diff --git a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx
index e6640ca3d..de77065f8 100644
--- a/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx
+++ b/packages/nextjs/src/client/contexts/Asgardeo/AsgardeoProvider.tsx
@@ -54,6 +54,7 @@ import AsgardeoContext, {AsgardeoContextProps} from './AsgardeoContext';
import {HttpRequestActionResult} from '../../../server/actions/httpRequestAction';
import {RefreshResult} from '../../../server/actions/refreshToken';
import logger from '../../../utils/logger';
+import navigateTo from '../../../utils/navigateTo';
/**
* Props interface of {@link AsgardeoClientProvider}
@@ -150,6 +151,11 @@ const AsgardeoClientProvider: FC>
// Don't handle callback if already signed in
if (isSignedIn) return;
+ // The embedded sign-in/sign-up flows open the identity provider in a popup named `oauth_popup` and
+ // read the `code`/`state` from this window's URL themselves. Handling the callback here as well would
+ // try to exchange a code that belongs to the embedded flow and log a spurious "Authentication failed".
+ if (typeof window !== 'undefined' && window.opener && window.name === 'oauth_popup') return;
+
(async (): Promise => {
try {
const code: string | null = searchParams.get('code');
@@ -177,7 +183,7 @@ const AsgardeoClientProvider: FC>
if (result.success) {
// Redirect to the success URL
if (result.redirectUrl) {
- router.push(result.redirectUrl);
+ navigateTo(router, result.redirectUrl);
} else {
// Refresh the page to update authentication state
window.location.reload();
@@ -215,14 +221,14 @@ const AsgardeoClientProvider: FC>
// Redirect based flow URL is sent as `signInUrl` in the response.
if (result?.data?.signInUrl) {
- router.push(result.data.signInUrl);
+ navigateTo(router, result.data.signInUrl);
return undefined;
}
// After the Embedded flow is successful, the URL to navigate next is sent as `afterSignInUrl` in the response.
if (result?.data?.afterSignInUrl) {
- router.push(result.data.afterSignInUrl);
+ navigateTo(router, result.data.afterSignInUrl);
return undefined;
}
@@ -251,7 +257,7 @@ const AsgardeoClientProvider: FC>
// Redirect based flow URL is sent as `signUpUrl` in the response.
if (result?.data?.signUpUrl) {
- router.push(result.data.signUpUrl);
+ navigateTo(router, result.data.signUpUrl);
return undefined;
}
@@ -261,7 +267,7 @@ const AsgardeoClientProvider: FC>
const {afterSignUpUrl, autoSignInSkippedReason, signedIn, ...flowResponse}: any = result.data;
// A URL passed by the caller (e.g. the `afterSignUpUrl` prop of ``) wins over the configured one.
- router.push(options?.afterSignUpUrl || afterSignUpUrl);
+ navigateTo(router, options?.afterSignUpUrl || afterSignUpUrl);
if (signedIn) {
// A session cookie was set during sign-up; re-render server components so the signed-in state is picked up.
@@ -295,7 +301,7 @@ const AsgardeoClientProvider: FC>
logger.debug('[AsgardeoClientProvider][handleSignOut] Sign out result:', result);
if (result?.data?.afterSignOutUrl) {
- router.push(result.data.afterSignOutUrl);
+ navigateTo(router, result.data.afterSignOutUrl);
return {location: result.data.afterSignOutUrl, redirected: true};
}
diff --git a/packages/nextjs/src/utils/__tests__/navigateTo.test.ts b/packages/nextjs/src/utils/__tests__/navigateTo.test.ts
new file mode 100644
index 000000000..89133ba5e
--- /dev/null
+++ b/packages/nextjs/src/utils/__tests__/navigateTo.test.ts
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2026, 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 {afterEach, beforeEach, describe, expect, it, vi, Mock} from 'vitest';
+import navigateTo, {isCrossOriginUrl} from '../navigateTo';
+
+describe('navigateTo', () => {
+ let assign: Mock;
+ let push: Mock;
+
+ beforeEach(() => {
+ assign = vi.fn();
+ push = vi.fn();
+ vi.stubGlobal('window', {location: {assign, origin: 'http://localhost:3000'}});
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it('uses the app router for relative and same-origin URLs', () => {
+ navigateTo({push}, '/dashboard');
+ navigateTo({push}, 'http://localhost:3000/profile');
+
+ expect(push).toHaveBeenCalledTimes(2);
+ expect(push).toHaveBeenNthCalledWith(1, '/dashboard');
+ expect(push).toHaveBeenNthCalledWith(2, 'http://localhost:3000/profile');
+ expect(assign).not.toHaveBeenCalled();
+ });
+
+ it('uses a full browser navigation for cross-origin URLs such as the hosted logout endpoint', () => {
+ const logoutUrl: string =
+ 'https://api.asgardeo.io/t/acme/oidc/logout?post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A3000&state=sign_out_success';
+
+ navigateTo({push}, logoutUrl);
+
+ expect(assign).toHaveBeenCalledWith(logoutUrl);
+ expect(push).not.toHaveBeenCalled();
+ });
+
+ it('treats a different port on the same host as cross-origin', () => {
+ expect(isCrossOriginUrl('http://localhost:3001/callback')).toBe(true);
+ });
+
+ it('falls back to the router for unparsable values', () => {
+ navigateTo({push}, 'http://[invalid');
+
+ expect(push).toHaveBeenCalledWith('http://[invalid');
+ expect(assign).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/nextjs/src/utils/logger.ts b/packages/nextjs/src/utils/logger.ts
index 22cc01aeb..330ca227d 100644
--- a/packages/nextjs/src/utils/logger.ts
+++ b/packages/nextjs/src/utils/logger.ts
@@ -16,10 +16,23 @@
* under the License.
*/
-import {createLogger} from '@asgardeo/node';
+import {createLogger, LogLevel} from '@asgardeo/node';
+
+const LOG_LEVELS: LogLevel[] = ['debug', 'info', 'warn', 'error'];
+
+/**
+ * Resolves the server-side log level from `ASGARDEO_LOG_LEVEL`. Defaults to `error`, so that
+ * degraded-but-working situations the SDK reports at `warn` (for example falling back to the ID token
+ * when the SCIM2 profile cannot be loaded) can be surfaced by setting `ASGARDEO_LOG_LEVEL=warn`.
+ */
+const resolveLogLevel = (): LogLevel => {
+ const configured: string | undefined = process.env['ASGARDEO_LOG_LEVEL']?.toLowerCase();
+
+ return LOG_LEVELS.includes(configured as LogLevel) ? (configured as LogLevel) : 'error';
+};
const logger: any = createLogger({
- level: 'error',
+ level: resolveLogLevel(),
});
export default logger;
diff --git a/packages/nextjs/src/utils/navigateTo.ts b/packages/nextjs/src/utils/navigateTo.ts
new file mode 100644
index 000000000..fd13c32b2
--- /dev/null
+++ b/packages/nextjs/src/utils/navigateTo.ts
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2026, 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.
+ */
+
+/**
+ * The subset of the Next.js app router used for in-app navigation.
+ */
+export interface InAppRouter {
+ push: (href: string) => void;
+}
+
+/**
+ * Whether `url` points outside the current origin (e.g. the identity server's hosted
+ * login or logout endpoint). Relative URLs and same-origin URLs return `false`.
+ */
+export const isCrossOriginUrl = (url: string): boolean => {
+ if (typeof window === 'undefined') {
+ return false;
+ }
+
+ try {
+ return new URL(url, window.location.origin).origin !== window.location.origin;
+ } catch {
+ return false;
+ }
+};
+
+/**
+ * Navigates to `url`, using the Next.js router for in-app URLs and a full browser
+ * navigation for cross-origin ones.
+ *
+ * The app router can only render routes of this application: handing it an external
+ * URL makes it request that URL as a React Server Components payload first, which the
+ * browser blocks (CORS) before the router falls back to a normal navigation, leaving
+ * "Failed to fetch RSC payload" errors in the console on every hosted sign-in or sign-out.
+ */
+const navigateTo = (router: InAppRouter, url: string): void => {
+ if (isCrossOriginUrl(url)) {
+ window.location.assign(url);
+
+ return;
+ }
+
+ router.push(url);
+};
+
+export default navigateTo;
diff --git a/packages/react/src/components/primitives/Dialog/Dialog.tsx b/packages/react/src/components/primitives/Dialog/Dialog.tsx
index db65685e5..dd2338d96 100644
--- a/packages/react/src/components/primitives/Dialog/Dialog.tsx
+++ b/packages/react/src/components/primitives/Dialog/Dialog.tsx
@@ -188,9 +188,17 @@ export const DialogContent: ForwardRefExoticComponent
className={cx(withVendorCSSClassPrefix(bem('dialog', 'overlay')), styles['overlay'])}
lockScroll
>
-
+ {/*
+ Move focus onto the dialog itself when it opens. The focus manager marks everything outside the
+ dialog `aria-hidden`, so leaving focus on the trigger (e.g. the user dropdown in a page header)
+ hides a focused element from assistive technology, which browsers flag as an accessibility error.
+ Focusing the container rather than the first field keeps the previous behaviour of not
+ auto-focusing an input.
+ */}
+