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>