Reputation: 1292
I have an existing MVC project that uses FormsAuthentication for its authentication.
I need to incorporate the option of logging in with an OpenID IDP in addition to the regular login page already available.
The problem I'm having is challenging the IDP on demand and setting the authentication cookie once the claims are received, I can't find the reason why the cookie is not sticking. The flow seems to be working fine, and I can see the claims in the AuthorizationCodeReceived callback.
Here's the Startup.Auth.cs code:
var notificationHandlers = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = (context) =>
{
string username = context.AuthenticationTicket.Identity.FindFirst("preferred_username").Value;
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(60), true, "");
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
context.Response.Cookies.Append(FormsAuthentication.FormsCookieName, encryptedTicket);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
if (context.OwinContext.Request.Path.Value != "/Account/SignInWithOpenId")
{
context.OwinContext.Response.Redirect("/Account/Login");
context.HandleResponse();
}
return Task.FromResult(0);
}
};
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "Cookies",
Authority = "xxxxxxxxx",
ClientId = "MyClient",
ClientSecret = "xxxxxxxx",
RedirectUri = "http://localhost:52389/",
PostLogoutRedirectUri = "http://localhost:52389/",
ResponseType = "code id_token",
Scope = "openid profile email roles",
UseTokenLifetime = false,
TokenValidationParameters = new TokenValidationParameters()
{
NameClaimType = "preferred_username",
RoleClaimType = "role"
},
Notifications = notificationHandlers
});
app.SetDefaultSignInAsAuthenticationType("Cookies");
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationType = "Cookies",
AuthenticationMode = AuthenticationMode.Passive,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider()
});
app.UseStageMarker(PipelineStage.Authenticate);
And here's the AccountController SignInWithOpenId method:
public ActionResult SignInWithOpenId()
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType);
// If I don't have this line, reponse redirects to the forms authentication login... so maybe something is wrong here?
return new HttpUnauthorizedResult("IDP");
}
else
{
return RedirectToAction("Index", "Default");
}
}
Any pointers would be greatly appreciated. Thank you.
Upvotes: 7
Views: 2767
Reputation: 421
Coming along a number of years later to provide a complete, working solution, since I found this question again while trying to solve this problem in a different application.
OpenIDConnect and Forms Authentication can coexist within the same .NET Framework project, at least if you're ok with Forms Authentication to be mostly in-charge of limiting access to protected resources.
The problems you'll have are when Forms Authentication kicks in limiting access to the routes that OpenIDConnect needs. You need to do two things to let this work properly:
Update your web.config to allow anonymous access to the OpenIDConnect URLs, for example:
<location path="Account/SSOLogIn">
<system.web>
<authorization>
<allow users="?" />
</authorization>
</system.web>
</location>
<location path="signin-oidc">
<system.web>
<authorization>
<allow users="?" />
</authorization>
</system.web>
</location>
In your Global.asax.cs, tell Forms Authentication middleware to ignore all OpenIDConnect-related routes:
// For small quantities, HashSet isn't really that much of an improvement over List,
// but using it makes the intention of the collection clear...
protected readonly HashSet<string> surpressedEndpoints =
[
"ssologin", "signin-oidc"
];
protected void Application_BeginRequest(object sender, EventArgs e)
{
// To support Forms authentication and OpenIDConnect, we have to prevent
// Forms authentication from interfering with OpenIDConnect URLs.
var lastSegmentLowercase = Context.Request.Url.Segments[Context.Request.Url.Segments.Length - 1].ToLower();
if (surpressedEndpoints.Contains(lastSegmentLowercase))
{
Context.Response.SuppressFormsAuthenticationRedirect = true;
}
}
Our application is now using OpenIDConnect with multiple identity providers, and Forms Authentication. We handle the OpenIDConnect SecurityTokenValidated
event to identity the authenticated user, and then call FormsAuthentication.SetAuthCookie
within that event.
Our signout routine also brute-force removes existing auth cookies:
// Remove any SSO cookies which may exist by sending new, expired, cookies.
Response.Cookies.Add(new HttpCookie(".AspNet.provider") { Expires = DateTime.Now.AddDays(-1) });
Upvotes: 1
Reputation: 101
solved by adding these code to Global.asax:
protected void Application_BeginRequest()
{
Context.Response.SuppressFormsAuthenticationRedirect = true;
}
according to Prevent ASP.NET from redirecting to login.aspx
Upvotes: 3
Reputation: 421
I was able to get this, or at least an equivalent, working in .NET 4.7. My use case is that most subscribers are being upgraded to log in via Azure AD B2C, but we have public PCs that we want to authenticate with a manual claim via an obscured URL.
I'm using Microsoft.Owin.Security.OpenIDConnect
and related packages, and the Owin startup is standard, although I will point out this line:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
I had to disable Forms authentication entirely; I could not get this working when anything other than Anonymous Authentication was enabled in IIS.
The core of the solution was actually an example I found here: How to use OWIN forms authentication without aspnet identity
/* URL validated, add authenticated claim */
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, "PublicPC"),
new Claim(ClaimTypes.Email, "[email protected]")
};
var id = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType);
var ctx = HttpContext.Current.GetOwinContext();
var authenticationManager = ctx.Authentication;
authenticationManager.SignIn(id);
But critically, I needed to specify CookieAuthenticationDefaults.AuthenticationType
, which is what I'm using in the Owin startup.
Upvotes: 0
Reputation: 25
This is the exact thing I'm trying to do at the moment. I will let you know if I find anything useful.
Update: I ended up disabling Forms Authentication in the MVC web app. I was doing a proof of concept so it wasn't a hard requirement. I know this was not really what you were getting at. I successfully used my IdP to login and redirect back to the web app. Where the proof of concept ended was the HttpContext.User object was needed to be populated.
Upvotes: 1