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.
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.
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, "PublicPC#example.org")
};
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.
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
Related
I am building an Identity server with the Duende Identity server software package.
The IdentityServer is .Net 6, and the client application is ASP.NET 4.6.2, which could complicate things.
In my Client app I have the following configuration:
` app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "SomeId",
Authority = "https://localhost:5001",
RedirectUri = "https://localhost:5002",
ClientSecret = "SomeSecret",
ResponseType = "Code",
Scope = "SomeScopes,
PostLogoutRedirectUri = "https://localhost:5002/some-custom-path",
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = false,
RedeemCode = true,
SaveTokens = true,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async context =>
{
var identity = context.AuthenticationTicket.Identity;
var claims = identity.Claims;
await Task.Yield();
}
}
});`
Assuming the OpenID configuration is correct (we can connect both apps together and login and logout through its login and logout pages), we cannot seem to get the PostLogoutRedirectUri in the LogoutContext:
` var context = await _interaction.GetLogoutContextAsync(LogoutId);`
The context contains a couple properties which I expected to be filled, which are:
ClientId
ClientName
PostLogoutRedirectUri
Somehow these values are null in my context. Could anyone explain why this is the case here?
We tried to pass the postLogoutRedirectUri through the RedirectToIdentityProvider in the client application, which also resulted in a null-result.
We have searched the internet, but most solutions that we come across are for .NET Core, which does not fit our client application.
We also tried the solution in the following post: How to redirect user to client app after logging out from identity server? , which also didn't work on our end.
UPDATE: Add logout method in client:
HttpCookie userCookie = new HttpCookie("UserCookie", "");
userCookie.Expires = DateTime.Now.AddYears(-1);
Response.Cookies.Add(userCookie);
HttpContext.GetOwinContext().Authentication.SignOut(
OpenIdConnectAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType);
return null;
First up, I am NOT going to be maintaining roles (claims) from within Azure AD, as I have to maintain it within SQL Server. So, for Authentication, I am using Azure AD. Once authenticated, I query my claims tables (aspnetmembership) and add it to the identity.
Right now, the below code seems to be working fine. But I don't feel confident at all due to these questions I have, as I just don't know if I have coded it right.
Here's my code and here are my questions:
Like we do with Forms auth, once authenticated, am I supposed to set the Thread.Currentprincipal, as well as Context.User even with Azure AD authentication or does this line of code automatically do that for me (I sign in once azure ad authenticates fine)
HttpContext.Current.GetOwinContext().Authentication.SignIn(identity);
If yes to the above question, I am really confused as to how I must sequence the above Signin line of code with setting the Principal (as well as Context.User) with the Azure AD authenticated identity?
I never knew this but does the [Authorize] attribute in MVC 5.0 automatically do the call to check if the request is 'authenticated' as well?
How do I access the custom claims that I added in Startup, within my controllers?
Can you please explain how I need to be handling the cookies with AZure AD authentication?
Thanks in advance!
Here's my code:
public void ConfigureAuth(IAppBuilder app)
{
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
// app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
//app.UseCookieAuthentication(new CookieAuthenticationOptions
//{
// CookieDomain = "localhost",
// SlidingExpiration = true,
// ExpireTimeSpan = TimeSpan.FromHours(2)
//});
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = clientId,
Authority = authority,
RedirectUri = null,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
ResponseType = OpenIdConnectResponseType.CodeIdToken,
// ValidateIssuer set to false to allow personal and work accounts from any organization to sign in to your application
// To only allow users from a single organizations, set ValidateIssuer to true and 'tenant' setting in web.config to the tenant name
// To allow users from only a list of specific organizations, set ValidateIssuer to true and use ValidIssuers parameter
TokenValidationParameters = new TokenValidationParameters()
{
//NameClaimType = "preferred_username",
ValidateIssuer = false // TODO: SET THIS TO TRUE EVENTUALLY.
},
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed,
SecurityTokenValidated = async (x) =>
{
var identity = x.AuthenticationTicket.Identity; //Check this.
await Task.FromResult(0);
var claims = identity.Claims;
var name = claims.First(claim => claim.Type == "name").Value;
var email = claims.First(claim => claim.Type == "preferred_username").Value;
var user = UserManager.FindByEmail(email);
var customClaims = UserManager.GetClaims(user.Id);
foreach (var claim in customClaims)
{
identity.AddClaim(new Claim(claim.Type, claim.Value));
}
HttpContext.Current.GetOwinContext().Authentication.SignIn(identity); //THis is the key here.
var principal = new ClaimsPrincipal(identity);
System.Threading.Thread.CurrentPrincipal = principal;
if (System.Web.HttpContext.Current != null)
System.Web.HttpContext.Current.User = principal;
}
}
}
);
}
And in my controller methods I am accessing my claims that I added above, using this method. Please confirm if this is correct or should I use the Thread.CurrentPrincipal somehow?
[Authorize]
public class HomeController : BaseController
{
private ApplicationUserManager _userManager;
public ActionResult Index()
{
//{
var identity = User.Identity as ClaimsIdentity;
var count = identity.Claims.Count(); //I get to see all the claims here that I set in startup
return View();
}
I have an ASP.NET MVC framework web application. I want to use both .net identity and OpenId Connect for authentication (Microsoft accounts).
It works and redirects to another controller as I want. In this target controller I get information from the claims (which are returned from Azure AD).
What I want is to add more claims to this collection from Azure, or create a new set of claims as I want. I set claims as following but when debugger hits to another controller I see default claims returned by Azure AD only; my modifications are not reflected in the claims collection.
How can I add claims which is usable for both OpenId Connect (Microsoft) and .NET identity authentication?
This is how I set example claims in the controller:
var identity = new ClaimsIdentity("ApplicationCookie", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
identity.AddClaim(new Claim("test", "test"));
IAuthenticationManager authenticationManager = HttpContext.GetOwinContext().Authentication;
authenticationManager.SignOut("ApplicationCookie");
authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, identity);
This is how I configured in Startup:
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = clientId,
Authority = authority,
// PostLogoutRedirectUri is the page that users will be redirected to after sign-out. In this case, it is using the home page
PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
// ResponseType is set to request the code id_token - which contains basic information about the signed-in user
ResponseType = OpenIdConnectResponseType.CodeIdToken,
AuthenticationMode = AuthenticationMode.Passive,
// OpenIdConnectAuthenticationNotifications configures OWIN to send notification of failed authentications to OnAuthenticationFailed method
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
},
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
RoleClaimType = System.Security.Claims.ClaimTypes.Role,
ValidateIssuer = false
}
});
You have two options. Either hook into the various events provided by AddCookie and AddOpenIDConnect. Or add a custom claims transformation, like:
public class BonusLevelClaimTransformation : IClaimsTransformation
{
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
if (!principal.HasClaim(c => c.Type == "bonuslevel"))
{
//Lookup bonus level.....
principal.Identities.First().AddClaim(new Claim("bonuslevel", "12345"));
}
return Task.FromResult(principal);
}
}
You also need to register it in Startup.cs like
services.AddTransient<IClaimsTransformation, BonusLevelClaimTransformation>();
We're using IdentityServer4 for our IdentityServer and IdentityServer3 for the client (ASP.NET MVC 5).
Everything works (the User/Claimsprincipal is set correctly through OWIN) except I cannot get the access token from the User.
We're using a implicit client which has access to these scopes: openid, profile, testapi
Startup.cs:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = identityServerUrl,
RequiredScopes = new[] { "testapi" },
});
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = identityServerUrl,
ClientId = "testclient",
Scope = "openid profile testapi",
RedirectUri = "http://localhost:49000/signin-oidc",
ResponseType = "id_token token",
SignInAsAuthenticationType = "Cookies",
});
Code to retrieve Access Token (inside one of the controllers):
var user = User as ClaimsPrincipal;
var token = user.FindFirst("access_token");
User is set correctly, but the token is null. I am guessing it is some kind of option that I am missing in the startup.cs, but which?
I think a simpler solution is to use what is allready made availible:
var options = new IdentityServerBearerTokenAuthenticationOptions
{
Authority = authorityUrl,
PreserveAccessToken = true,
};
Then the access token is availible as a claim (named 'token') on the User principle.
I found a solution that does exactly what I want - I'm putting it here for anyone else running into the problem. It costs a dependency on IdentityModel, but that is acceptable in my case:
In Startup.cs, I added:
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
var tokenClient = new TokenClient(identityServerUrl + "/connect/token", clientId, secret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
HttpContext.Current.Session[HttpUserContext.ACCESS_TOKEN] = tokenResponse.AccessToken;
}
}
To the call to .UseOpenIdConnectAuthentication
I have a web forms app currently using either forms authentication (or LDAP which then sets a FormsAuthenticationTicket cookie). I need to add SSO to this project and I'm currently using OpenID/Azure AD to authenticate with. I have the following Startup.cs configured.
public void Configuration(IAppBuilder app)
{
string appId = "<id here>";
string aadInstance = "https://login.microsoftonline.com/{0}";
string tenant = "<tenant here>";
string postLogoutRedirectUri = "https://localhost:21770/";
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenReceived = context =>
{
System.Diagnostics.Debug.WriteLine("SecurityTokenReceived");
return Task.FromResult(0);
},
SecurityTokenValidated = async n =>
{
var claims_to_exclude = new[]
{
"aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
};
var claims_to_keep =
n.AuthenticationTicket.Identity.Claims
.Where(x => false == claims_to_exclude.Contains(x.Type)).ToList();
claims_to_keep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
if (n.ProtocolMessage.AccessToken != null)
{
claims_to_keep.Add(new Claim("access_token", n.ProtocolMessage.AccessToken));
//var userInfoClient = new UserInfoClient(new Uri("https://localhost:44333/core/connect/userinfo"), n.ProtocolMessage.AccessToken);
//var userInfoResponse = await userInfoClient.GetAsync();
//var userInfoClaims = userInfoResponse.Claims
// .Where(x => x.Item1 != "sub") // filter sub since we're already getting it from id_token
// .Select(x => new Claim(x.Item1, x.Item2));
//claims_to_keep.AddRange(userInfoClaims);
}
var ci = new ClaimsIdentity(
n.AuthenticationTicket.Identity.AuthenticationType,
"name", "role");
ci.AddClaims(claims_to_keep);
n.AuthenticationTicket = new AuthenticationTicket(
ci, n.AuthenticationTicket.Properties
);
},
MessageReceived = context =>
{
System.Diagnostics.Debug.WriteLine("MessageReceived");
return Task.FromResult(0);
},
AuthorizationCodeReceived = context =>
{
System.Diagnostics.Debug.WriteLine("AuthorizationCodeReceived");
return Task.FromResult(0);
},
AuthenticationFailed = context =>
{
System.Diagnostics.Debug.WriteLine("AuthenticationFailed");
context.HandleResponse();
context.Response.Write( context.Exception.Message);
return Task.FromResult(0);
}
,
RedirectToIdentityProvider = (context) =>
{
System.Diagnostics.Debug.WriteLine("RedirectToIdentityProvider");
//string currentUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.Path;
//context.ProtocolMessage.RedirectUri = currentUrl;
return Task.FromResult(0);
}
}
});
app.UseStageMarker(PipelineStage.Authenticate);
}
I have placed this in page Load event of my master (although it never seems to be getting hit - something else must be causing the authentication process to kick off when I navigate to a page requiring authentication.)
if (!Request.IsAuthenticated)
{
HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
My Azure settings are all correct because I am hitting SecurityTokenValidated and AuthorizationCodeReceived functions - I can see my email I am logged in with in the claims information, but I am not sure what to do next. As is I have a never ending loop of authentication requests. I am assuming this is because I have not translated the claim information I have received back into forms authentication ? I attempted to add a dummy auth ticket to the response in AuthorizationCodeReceived but that didn't appear to change anything - I am still getting the looping authentication requests.
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);
This is not a clear cut answer but it's too big for a comment.
I'm using "Organizational Accounts" (i.e. O365 email logins) and I had two big problems (both solved).
First Issue
intermittently, when logging in it would go into an endless redirect loop back and forth between two pages (This didn't happen all the time - only after half an hour testing and logging in and out).
If I left it long enough it would say "query string too long". There is a lot of long winded explanation around cookies and stuff but I had difficulties solving it. In the end it was solved simply by forcing https instead of http
I don't think that's your issue as it seems like it does it everytime. Perhaps have a read through this
New Asp.Net MVC5 project produces an infinite loop to login page
One answer says:
Do not call a protected web API (any web API which requires
Authorization) from an authorization page such as ~/Account/Login
(which, by itself, does NOT do this.). If you do you will enter into
an infinite redirect loop on the server-side.
Second Issue
So the next thing was: our existing authorisation system was sitting in a classic login/pwd table in our database (with an unencrypted password field >:| ). So I needed to pick up the login email and match that to a role defined in this table. Which I did thanks to the guy who answered my question:
Capturing login event so I can cache other user information
This answer meant that I could:
Go pick up the users role from the database once upon initial login
Save this role inside the existing native C# security object
Best of all: use the native authorisation annotations in my controller methods without any custom code in the method
I think thats what you are after but the question really is: how are you currently storing roles? In a database table? In Active Directory? In Azure active directory?
So in the hope that it help someone else - this is what I ended up with. In the web.config, the authentication mode is set to 'Forms'. I added the following Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
var appId = ConfigurationCache.GetConfigurationString(TOS_Configuration.KEY_SSO_APPID);
var authority = ConfigurationCache.GetConfigurationString(TOS_Configuration.KEY_SSO_AUTHORITY);
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = authority,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = context =>
{
string username = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.Name).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);
},
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Write(context.Exception.Message);
return Task.FromResult(0);
}
}
});
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
}
I did not add any challenge to my site master pages and instead added the following to my login page to trigger the authentication challenge:
if (!Request.IsAuthenticated && AttemptSSO)
{
ReturnURL = Request.QueryString["ReturnUrl"];
HttpContext.Current.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Login.aspx" }, OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
else if (Request.IsAuthenticated && AttemptSSO)
{
if (!string.IsNullOrEmpty(ReturnURL))
{
var url = ReturnURL;
ReturnURL = "";
Response.Redirect(ResolveUrl(url));
}
else
{
Response.Redirect(ResolveUrl("~/Default.aspx"));
}
}
This means that if a user arrives at a authenticated page without a valid forms authentication token they get redirected to the login page. The login page takes care of deciding if SSO is set up and handling it appropriately. If anyone has any thoughts as to how to improve it - I'd love to hear them, but for the moment this does work.