OIDC client support for multi tenant - oidc-client throws "no matching state found in storage"

Viewed 2977

I am using .net core Identity server4 authenticaltion for my vue js client side app. I have support for multi tenant. I am using oidc client java script library in the client side for authenticaltion.

Client side config will be like this

authority: 'http://identity.identityserver.com/',
"redirect_uri": "http://app.clientapp.com/callback",
"client_id": "app1",
"grant_type": "authorization_code",
"client_secret": "secret_code",
"response_type": "code",
"scope": "openid profile web.api",
"post_logout_redirect_uri": "http://app.clientapp.com/logoutSuccess",

Identity server config will be like this

new Client
            {
                ClientId = "app1",
                ClientName = "Vue JS APP",
                ClientSecrets = new List<Secret> { new Secret("secret_code") },
                AllowedGrantTypes = GrantTypes.Code,
                AllowAccessTokensViaBrowser = true,
                AlwaysIncludeUserClaimsInIdToken = true,
                AlwaysSendClientClaims = true,
                RedirectUris = { "http://app.clientapp.com/callback" },
                RequireConsent = false,
                PostLogoutRedirectUris = { "http://app.clientapp.com/callback/logoutSuccess" },
                AllowedScopes =
                {
                    IdentityServerConstants.StandardScopes.OpenId,
                    IdentityServerConstants.StandardScopes.Profile,
                    "web.api",
                },
},

This works as expected without any issue . It redirect to identity server, login windows pops up and upon entering the credential ,it sucsessfully redirect to the to the http://app.clientapp.com/callback.

My requirement is , based on the user login , i have to redirect to the user to his sub domain, ie is support for the multi tenant. For example , if user "user1" tenant is tenant1 , even though the call back url is configured is http://app.clientapp.com/callback, on sucsessfull redirection ,user has to redirect to the http://tenant1.clientapp.com/callback . I have done the necessery changes in the identity server to override this using the IRedirectUriValidator . And it is redirecting back the http://tenant1.clientapp.com/callback . But issue is, the oidc-client in client side throws "no matching state found in storage" .

Can anyone help on this issue .

Thanks in advance .

1 Answers

This doesn't specifically apply to your situation, however feel this is worth posting for others scratching their heads on this particular oidc-client error.

We found two specific other reasons why this error was intermittently appearing in our logs. In our case, the "No matching state found in storage" error was thrown during the oidc-client-js method signinRedirectCallback(), inside the oicd signin callback.

Reasons:

  1. Users with system clocks set more than 5 minutes away from the current actual time (the clockSkew defaults to 5 minutes in oidc-client-js)

  2. Users were sharing and re-using the unique generated login URLs w/ security parameters generated by Identity Server

    /login?ReturnUrl=https%3A%2F%2Fae-xxxxxx%2Fauthorize%2Fcallback%3Fclient_id%3Dxxxxxx%26redirect_uri%3Dhttps%253A%252F%252Fxxxxxxxx.com%252Fsignin-oidc%26response_type%3Dcode%26scope%3Dopenid%2520profile%2520email%26state%3D3b6da3263b9e47c3a954c1d6b149337f%26code_challenge%asagasgsagas-Wf8VWWWRWiGvCPfcu3MdDFhA%26code_challenge_method%3DS256%26response_mode%3Dquery)

We ended up trapping and reporting the error by in our AuthContext that wraps oidc-client-js like so:

signinRedirectCallback = async () => {
  try {
    await this.UserManager.signinRedirectCallback();
  } catch (error) {
    Sentry.captureException(error, {
      level: "warning",
    });

    // clearing state and storage 
    // may not be necessary, 
    // but it seems reasonable
    this.UserManager.clearStaleState();
    localStorage.clear();

    throw new Error(error);
  }
};

Then in the signin-oidc callback, catch the error and route the user to an error page:

import { useEffect, useContext } from "react";
import { useHistory } from "react-router-dom";
import AuthContext from "contexts/auth";

export default () => {
  const auth = useContext(AuthContext);
  const history = useHistory();

  useEffect(() => {
    auth.signinRedirectCallback().catch((error) => {
      history.replace("/error?code=login_callback_error");
    });
  }, [auth, history]);

  return "";
};
Related