dotnet / dotnet/aspnetcore

Silent sign-in with oidc-client.js causes timeout issues with some IdPs

Open
#60,115 0 comments 6 reactions 0 assignees View on GitHub
area-auth
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

Original posted by @Herdo:

https://github.com/dotnet/aspnetcore/issues/40764#issuecomment-1567061660

The current implementation with iframes and silent signin based on _oidc-client-js_ is causing some timeout issues with third-party IdPs. I've described the cause of this issue in this Stackoverflow question: [Blazor WASM - Spending a long time initially in Authorizing component](https://stackoverflow.com/questions/74959385/blazor-wasm-spending-a-long-time-initially-in-authorizing-component). I'll add my analysis results below.

IMHO, the new solution should allow more freedom for cases like this, where you cannot influence IdP configuration like `X-Frame-Options` header.

## Source of issue
The issue is caused by a timeout in the underlying implementation of the authentication services. I traced down the source, but there's no easy solution to this issue.

If you enable Debug tracing for your WASM client, you should see this log message in the console:

> dbug: Microsoft.AspNetCore.Components.WebAssembly.Authentication.RemoteAuthenticationService[0]
Initial silent sign in failed 'Frame window timed out'

For me - using Keycloak (instead of Auth0), and Discord as IdP behind Keycloak - the Discord login cannot be framed in the hidden iframe:

> Refused to frame 'https://discord.com/' because it violates the following Content Security Policy directive: "frame-src 'self' my.domain.com".

Of course this policy can be modified to include `discord.com`, but Discord denies being embedded that way with `X-Frame-Options` header.

## What's happening

1. The app gets loaded
2. `AuthorizeViewCore` is being rendered, entering [`OnParametersSetAsync`][1]:
```cs
// Clear the previous result of authorization
// This will cause the Authorizing state to be displayed until the authorization has been completed
isAuthorized = null;

currentAuthenticationState = await AuthenticationState;
isAuthorized = await IsAuthorizedAsync(currentAuthenticationState.User);
```
3. The `AuthenticationState` is initialized by [`RemoteAuthenticationService.GetAuthenticationStateAsync`][2]:
```cs
new AuthenticationState(await GetUser(useCache: true));
```
4. This will invoke `GetAuthenticatedUser`:
```cs
///
/// Gets the current authenticated used using JavaScript interop.
///
/// A that will return the current authenticated user when completes.
protected internal virtual async ValueTask GetAuthenticatedUser()
{
await EnsureAuthService();
var account = await JsRuntime.InvokeAsync("AuthenticationService.getUser");
var user = await AccountClaimsPrincipalFactory.CreateUserAsync(account, Options.UserOptions);

return user;
}
```
5. [`AuthenticationService.getUser`][3] will invoke `trySilentSignIn`:
```js
async trySilentSignIn() {
if (!this._intialSilentSignIn) {
this._intialSilentSignIn = (async () => {
try {
this.debug('Beginning initial silent sign in.');
await this._userManager.signinSilent();
this.debug('Initial silent sign in succeeded.');
} catch (e) {
if (e instanceof Error) {
this.debug(`Initial silent sign in failed '${e.message}'`);
}
// It is ok to swallow the exception here.
// The user might not be logged in and in that case it
// is expected for signinSilent to fail and throw
}
})();
}

return this._intialSilentSignIn;
}
```
6. The `await this._userManager.signinSilent();` will invoke the **oidc-client-js** [UserManager][4] `signinSilent` and then `_signinSilentIframe`:
```js
_signinSilentIframe(args = {}) {
let url = args.redirect_uri || this.settings.silent_redirect_uri || this.settings.redirect_uri;
if (!url) {
Log.error("UserManager.signinSilent: No silent_redirect_uri configured");
return Promise.reject(new Error("No silent_redirect_uri configured"));
}

args.redirect_uri = url;
args.prompt = args.prompt || "none";

return this._signin(args, this._iframeNavigator, {
startUrl: url,
silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout
}).then(user => {
if (user) {
if (user.profile && user.profile.sub) {
Log.info("UserManager.signinSilent: successful, signed in sub: ", user.profile.sub);
}
else {
Log.info("UserManager.signinSilent: no sub");
}
}

return user;
});
}
```
7. Finally, this will end up at [`IFrameWindow.js`][5], which has **a timeout of 10000 ms** configured:
```js
const DefaultTimeout = 10000;
```
8. The initially logged **timeout error** is thrown:
```js
_timeout() {
Log.debug("IFrameWindow.timeout");
this._error("Frame window timed out");
}
```

[1]: https://github.com/dotnet/aspnetcore/blob/d0e94423f0ef587b8fe262667c8168e14e4f5ac7/src/Components/Authorization/src/AuthorizeViewCore.cs#LL71C5-L93C1
[2]: https://github.com/dotnet/aspnetcore/blob/d0e94423f0ef587b8fe262667c8168e14e4f5ac7/src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs#LL104C1-L104C141
[3]: https://github.com/dotnet/aspnetcore/blob/d0e94423f0ef587b8fe262667c8168e14e4f5ac7/src/Components/WebAssembly/WebAssembly.Authentication/src/Interop/AuthenticationService.ts#LL141C5-L150C6
[4]: https://github.com/IdentityModel/oidc-client-js/blob/dev/src/UserManager.js
[5]: https://github.com/IdentityModel/oidc-client-js/blob/dev/src/IFrameWindow.js

Contributor guide

Open the contributing guide

Research direction

Start with AuthorizeViewCore.cs, RemoteAuthenticationService.cs, and AuthenticationService.ts, then review the referenced oidc-client-js UserManager.js and IFrameWindow.js behavior. Define how silent sign-in should work when an IdP rejects iframe embedding, and verify that initial authorization no longer waits for the frame timeout.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, javascript, typescript
Domain
authentication, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.