Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changeset/silent-sign-in-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@asgardeo/javascript': patch
'@asgardeo/react': patch
---

Fix `signInSilently()` always resolving `false`.

The authorize request replaced the caller's `state` with the `instance_<id>` prefix, so the `sign-in-silently` marker never reached the hidden iframe and the identity provider's response was never handed back to the parent window. The state now keeps both parts (for example `instance_0_sign-in-silently_request_0`).

`AsgardeoProvider` also hands that response back to the parent before resuming a session. The iframe shares the parent's session storage, so it previously saw an active session and returned early, leaving the parent waiting until its silent sign-in timed out.
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,35 @@ describe('getAuthorizeRequestUrlParams', (): void => {
expect(params.get(OIDCRequestConstants.Params.STATE)).toBe('customState_request_1');
});

it('should prefix the state with the instance ID', (): void => {
const params: Map<string, string> = getAuthorizeRequestUrlParams(
{
clientId: 'client123',
instanceId: '0',
redirectUri: 'https://app/callback',
},
{key: pkceKey},
{},
);

expect(params.get(OIDCRequestConstants.Params.STATE)).toBe('instance_0_request_1');
});

it('should keep the custom state when an instance ID is also provided', (): void => {
const params: Map<string, string> = getAuthorizeRequestUrlParams(
{
clientId: 'client123',
instanceId: '0',
prompt: 'none',
redirectUri: 'https://app/callback',
},
{key: pkceKey},
{[OIDCRequestConstants.Params.STATE]: 'sign-in-silently'},
);

expect(params.get(OIDCRequestConstants.Params.STATE)).toBe('instance_0_sign-in-silently_request_1');
});

it('should set scope to undefined if none provided', (): void => {
const params: Map<string, string> = getAuthorizeRequestUrlParams(
{
Expand Down
16 changes: 12 additions & 4 deletions packages/javascript/src/utils/getAuthorizeRequestUrlParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,22 @@ const getAuthorizeRequestUrlParams = (
}

const AUTH_INSTANCE_PREFIX: string = 'instance_';
let customStateValue: string = '';
// Keep the caller's state alongside the instance prefix rather than replacing it: silent sign-in and
// check-session recognise their own responses by a marker in the state (e.g. `sign-in-silently`).
const stateParts: string[] = [];

if (options.instanceId) {
customStateValue = AUTH_INSTANCE_PREFIX + options.instanceId;
} else if (customParams) {
customStateValue = customParams[OIDCRequestConstants.Params.STATE]?.toString() ?? '';
stateParts.push(AUTH_INSTANCE_PREFIX + options.instanceId);
}

const callerState: string = customParams?.[OIDCRequestConstants.Params.STATE]?.toString() ?? '';

if (callerState) {
stateParts.push(callerState);
}

const customStateValue: string = stateParts.join('_');

authorizeRequestParams.set(
OIDCRequestConstants.Params.STATE,
generateStateParamForRequestCorrelation(pkceKey, customStateValue),
Expand Down
14 changes: 14 additions & 0 deletions packages/react/src/contexts/Asgardeo/AsgardeoProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
EmbeddedSignInFlowResponseV2,
TokenResponse,
createPackageComponentLogger,
SPAUtils,
} from '@asgardeo/browser';
import {FC, RefObject, PropsWithChildren, ReactElement, useEffect, useMemo, useRef, useState, useCallback} from 'react';
import AsgardeoContext from './AsgardeoContext';
Expand Down Expand Up @@ -275,6 +276,19 @@ const AsgardeoProvider: FC<PropsWithChildren<AsgardeoProviderProps>> = ({
reRenderCheckRef.current = true;

(async (): Promise<void> => {
// Inside the hidden iframe opened by `signInSilently()`, hand the authorization response back to the
// parent window before anything else. This iframe shares the parent's session storage, so the
// `isSignedIn()` short-circuit below would otherwise resume the session and leave the parent waiting
// until its silent sign-in times out.
if (
SPAUtils.isInitializedSilentSignIn() &&
hasCalledForThisInstance(new URL(window.location.href), instanceId ?? 0)
) {
await asgardeo.signInSilently();

return;
}

// User is already authenticated. Skip...
const isAlreadySignedIn: boolean = await asgardeo.isSignedIn();

Expand Down