5

I have a WebForms application (not MVC, not WebApi) which I'm porting to an OpenID Connect external authentication (.net 4.7.2, latest OWIN NuGet packages). This web app uses role-based authorization in order to prevent unauthorized users to access some parts of the application. Roles are supplied via OIDC claims and specified in the web application as web.config authorization entries.

The main flow works: the application correctly redirects users to the OIDC provider when the user is not authenticated and the user has the appropriate roles in his ClaimsIdentity.

The problem happens when a user is logged in but doesn't have the role required for accessing the role-limited areas. In this case, the UrlAuthorizationModule returns a 401 Unauthorized error (shouldn't it be a 403 Forbidden?!), which triggers the OIDC challenge, which goes to the OIDC provider, which returns the very same user with the same claims, which triggers another 401, and the user experiences an unlimited redirect loop.

I can detect the condition in the RedirectToIdentityProvider notification and use the Response.Redirect method to render an error message to the user, but this causes a 302 > 403 error sequence and changes the browser URL to the 403.aspx page. I'd rather rewrite the response keeping the current URL, but I can't figure out how (check the *-marked lines inside the Startup.cs snippet).

Do you have any ideas?

Relevant Startup.cs snippet:

app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions() { });

app.UseOpenIdConnectAuthentication(
    new OpenIdConnectAuthenticationOptions {
        ClientId = ClientId,
        ClientSecret = ClientSecret,
        Authority = Authority,
        Scope = Scopes,
        TokenValidationParameters = new TokenValidationParameters()
        {
            NameClaimType = ClaimTypes.NameIdentifier,
        },
        Notifications = new OpenIdConnectAuthenticationNotifications
        {
            SecurityTokenValidated = n =>
            {
                // Store the ID Token in the auth ticket to be able to remote logout
                if (n.ProtocolMessage.IdToken != null)
                {
                    var idTokenClaim = new Claim("id_token", n.ProtocolMessage.IdToken, ClaimValueTypes.String);
                    n.AuthenticationTicket.Identity.AddClaim(idTokenClaim);
                }

                return Task.FromResult(0);
            },
            RedirectToIdentityProvider = n =>
            {
                // If authenticating, compute the redirect URI from the current request
                if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
                {
                    n.ProtocolMessage.RedirectUri = new Uri(n.Request.Uri, "/authorization-code/callback").ToString();

                    // Make sure we're here because we're unauthenticated users and not
                    // just authenticated users without a valid role
                    if (n.OwinContext.Authentication.User.Identity.IsAuthenticated &&
                        n.OwinContext.Authentication.User.FindFirst(t => t.Type == ClaimTypes.Role && t.Value == "UserEmailValid") == null)
                    {
*                       // Pass control to the error message page
*                       // n.Response.Redirect(VirtualPathUtility.ToAbsolute("~/Errors/403.aspx"));
*                       n.OwinContext.Request.Path = new PathString(VirtualPathUtility.ToAbsolute("~/Errors/403.aspx"));
*** What here? I'm not in a middleware, this is a notification! ***
*                       n.HandleResponse();
                    }
                }

                // If signing out, add the id_token_hint
                if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
                {
                    var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
                    if (idTokenHint != null)
                        n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                }

                return Task.FromResult(0);
            }
        }
    })
    .UseStageMarker(PipelineStage.Authenticate);

web.config authorization example:

<configuration>
  <system.web>
    <authorization>
      <allow roles="UserEmailValid" />
      <deny users="*" />
    </authorization>
  </system.web>
</configuration>

0 Answers0