How do I issue the corresponding Bearer and Cookie identity in ASP.NET with multiple Authorization schemes? - asp.net

This documentation describes in part how to use more than one authentication scheme:
In some scenarios, such as Single Page Applications it is possible to end up with multiple authentication methods. For example, your application may use cookie-based authentication to log in and bearer authentication for JavaScript requests. In some cases you may have multiple instances of an authentication middleware. For example, two cookie middlewares where one contains a basic identity and one is created when a multi-factor authentication has triggered because the user requested an operation that requires extra security.
Example:
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "Cookie",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = false
});
app.UseBearerAuthentication(options =>
{
options.AuthenticationScheme = "Bearer";
options.AutomaticAuthenticate = false;
});
However it only describes how to use Bearer or Cookie auth. What isn't clear is what other combinations are valid, or how to properly issue bearer or cookies to the client.
How can that be accomplished?

One common use case for this which large sites like Facebook, Google etc. use is to use multiple cookie authentication middleware's and set one of them to be the default using AutomaticAuthenticate
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "InsecureLongLived",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = true
});
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "SecureAndShortLived",
LoginPath = new PathString("/Account/Unauthorized/"),
AccessDeniedPath = new PathString("/Account/Forbidden/"),
AutomaticAuthenticate = false
});
The default one is long lived and used for non-critical auth scenarios e.g. on Facebook, this may be to view your profile page.
The more secure and short lived on is used for security critical user actions like changing your password or profile information.
This gives you the convenience of not having to login all the time with a long lived cookie but as soon as you need to do something potentially dangerous, you switch to doing auth with a much shorter lived and thus more secure cookie which requires the user to login again.

Related

Microsoft Graph API userInfo endpoint UnknownError: Token must contain sub claim

I'm trying to execute the userinfo endpoint at https://graph.microsoft.com/oidc/userinfo using an access token received through Open ID Connect.
The response received is:
400 Bad Request
{
"error": {
"code": "UnknownError",
"message": "Token must contain sub claim.",
"innerError": {
"date": "2021-02-22T07:14:37",
"request-id": "650a2928-b0e7-49ae-9e6d-ecb569ee69e6",
"client-request-id": "650a2928-b0e7-49ae-9e6d-ecb569ee69e6"
}
}
}
The access token is valid and does contain a sub claim.
If I sign-in to https://developer.microsoft.com/en-us/graph/graph-explorer, and use the access token it automatically retrieves, it works - for the same user. The sub claim is different though and there are two of them.
It seems the token from OIDC doesn't have a correct sub claim - how come might this be?
Access token from directly from the /authorize endpoint [WORKING]:
Access token from OIDC [NOT WORKING]:
OIDC configuration:
options.Authority = authority;
options.ClientId = Configuration[ConfigKeys.IdentityProvider.ClientID];
options.ClientSecret = Configuration[ConfigKeys.IdentityProvider.ClientSecret];
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.CallbackPath = Configuration[ConfigKeys.IdentityProvider.CallbackPath];
options.SignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.CorrelationCookie.Expiration
= options.NonceCookie.Expiration
= options.ProtocolValidator.NonceLifetime
= options.RemoteAuthenticationTimeout
= TimeSpan.FromHours(8);
options.Resource = "https://graph.microsoft.com";
options.GetClaimsFromUserInfoEndpoint = false;
options.UseTokenLifetime = true;
options.RequireHttpsMetadata = true;
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access");
options.Scope.Add("groups");
options.RemoteAuthenticationTimeout = TimeSpan.FromHours(10);
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = authority,
//NameClaimType = "name"
};
The access token is valid and does contain a sub claim.
I suppose you didn't get the token correctly, please follow the steps below.
1.Register an application with Azure AD
2.In the API permissions of the AD App, add the following permission in Microsoft Graph
3.In the Authentication, choose the options below.
4.Hit the URL below in the browser, replace the <tenant-id>, <client-id> of yours, login your user account, then you will get an access_token and an id_token.
https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize?client_id=<client-id>&response_type=token+id_token&redirect_uri=http://localhost&scope=user.read+openid+profile+email&response_mode=fragment&state=12345&nonce=678910
5.Use the access_token to call the https://graph.microsoft.com/oidc/userinfo endpoint, it works fine, the sub value is EY4uO7uc1IG2n8EboEalB4LDxJ1NU8nuc2JXZgkisN4 in my sample.
6.Decode the id_token got in step 4 in https://jwt.io/, the sub is also EY4uO7uc1IG2n8EboEalB4LDxJ1NU8nuc2JXZgkisN4, so it means the sub got from https://graph.microsoft.com/oidc/userinfo endpoint is correct.
If I sign-in to https://developer.microsoft.com/en-us/graph/graph-explorer, and use the access token it automatically retrieves, it works - for the same user. The sub claim is different though and there are two of them.
The token you got from Microsoft Graph Explorer is an access_token, the first sub is the value for access_token, the second one is that you want i.e. sub of id_token.
It seems the token from OIDC doesn't have a correct sub claim - how come might this be?
It is correct, as I mentioned above, the sub you got from the OIDC is the same as the sub got from the id_token.
Reference - https://learn.microsoft.com/en-us/azure/active-directory/develop/userinfo#userinfo-response
These are the same values that the app would see in the ID token issued to the app.
Note: You may find the sub got manually is different from the second sub got from the MS Graph Explorer, this is because your user account logged in two different clients, one is the client of Graph Explorer, another one is your custom AD App.
Reference - https://learn.microsoft.com/en-us/azure/active-directory/develop/id-tokens
Update:
OIDC does not use the v2.0 endpoint, to solve this issue, we need to configure OIDC to make it use the v2.0 endpoint, just add v2.0 in the authority of the configuration.

ASP.Net External Cookie with Sliding Expiration appearing as a Session cookie

I am trying to configure a sliding expiration cookie in Asp.Net. I am expecting the cookie to appear in the Google Chrome developer tools cookie manager with an expiration date 5 minutes after authentication, but it shows as "Session" and never expires until the sign-out button is clicked. It does go away if the browser is closed.
Below is the code as it currently stands. The website uses Saml based Single-Sign-On authentication with Kentor.AuthServices nuget package (now known as SustainSys.Saml2, we are behind in versions).
app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/signin"),
CookieSecure = CookieSecureOption.SameAsRequest,
ExpireTimeSpan = TimeSpan.FromMinutes(5),
SlidingExpiration = true,
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ctx => { },
OnResponseSignIn = context =>
{
context.Properties.AllowRefresh = true;
context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(5);
}
}
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
Kentor.AuthServices.Configuration.Options.GlobalEnableSha256XmlSignatures();
The OnResponseSignIn block was recently added based on this MSDN answer:
https://forums.asp.net/t/2121970.aspx?OWIN+Authentication+ExpireTimeSpan+not+working
I want the cookies to expire in a 30-minute inactive period. The above code is set to 5 for ease of testing.
The developer tools show the cookie expiration time. This is not directly related to the authentication token expiration time, which should in fact be correct for your code too.
As indicated by this comment "The expiration information is stored in the protected cookie ticket". The token expiration time should take effect properly, even if you cannot see it in the developer tools as it's encrypted inside the cookie itself.

Why is context.Request.Context.Authentication.SignIn not generating a cookie?

We have an MVC 5 application that we have added Web Api Controllers to in order to provide REST API functionality. We have successfully implemented OAuth authentication through the OWIN pipeline using a custom OAuth Provider class.
Now we want to implement authentication cookies as well to protect static resources on the server. I'm sure there's a million other ways to do this, but the request for the resource is a link directly to that resource so I can't use my OAuth token or any other mechanism which is why we want to use cookies...the browser sends them already, no need to change anything.
From everything I've read it is possible to do both Bearer Token authentication and Cookie authentication with the OWIN Pipeline. Basically Web API will use Bearer Tokens cause that's all the client will supply and requests for certain static resources on the server will use Cookies which are sent on all requests.
Our problem is that with the code below an auth cookie is never generated. Throughout the pipeline I never see a set-cookie header on the response, which is why I added the Kentor Cookie Saver to the pipeline...it's supposed to help.
WebApiConfig.cs
...
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
...
Startup.Auth.cs
...
app.UseOAuthBearerTokens(OAuthOptions);
// I was told this might help with my cookie problem...something to do with System.Web stripping Set-Cookie headers
app.UseKentorOwinCookieSaver();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = Microsoft.Owin.Security.Cookies.CookieAuthenticationDefaults.AuthenticationType,
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
ExpireTimeSpan = TimeSpan.FromHours(4)
});
...
Custom OAuth Provider
...
// Creates our claims and properties...keep in mind that token based authentication is working
CreatePropertiesAndClaims(acct, out properties, out claims);
if (IsAccountAuthorized(claims))
{
AuthenticationProperties authProps = new AuthenticationProperties(properties);
ClaimsIdentity claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
claimsIdentity.AddClaims(claims);
AuthenticationTicket ticket = new AuthenticationTicket(claimsIdentity, authProps);
context.Validated(ticket);
ClaimsIdentity cookieIdentity = new ClaimsIdentity(claims, Microsoft.Owin.Security.Cookies.CookieAuthenticationDefaults.AuthenticationType);
context.Request.Context.Authentication.SignIn(cookieIdentity); // This should create the auth cookie!??!
}
else
{
context.SetError("Unauthorized", "You don't currently have authorization. Please contact support.");
}
...
Keep in mind that Token based authentication is working so I assume it's a configuration setting missing or misconfigured, or a pipeline ordering issue.
THANK YOU!
I know it is a late answer, but we came across exactly the same problem. My colleague and I spent 4 hours trying to figure out why. Here is the answer that hopefully can save somebody else from bang their head against the wall.
Inspect the response, you can see there is Set-Cookie:.AspNet.Cookies=LqP1uH-3UZE-ySj4aUAyGa8gt .... But the cookies it is not saved. What you need to do is, in your ajax call, include credentials.
$.ajax({
url: url,
type: 'POST',
xhrFields: {
withCredentials: true
}
)
we are using the Fetch API, it looks like the following
fetch(url, {
method: 'post',
headers: {
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8"
},
credentials: 'include'
})

Invalidate user credentials when password changes

I have an Asp.net MVC website. When the users change their password, do the logins from all of the browsers invalidate? I mean will the user require to login on all browsers with the new password? If not, is there a way to do this?
Not immediately, it will take 30 minutes by default for old cookies to invalidate in asp.net Identity 2, asp.net identity doesn't check the database on every request for that, it has an interval, use SecurityStamp to change it, you can set it in Startup.Auth.cs, default is 30 minutes, set the validateInterval to 0, this is not the most efficient approach because on every request the database will be hit to check if the cookies are still valid, but it will do the job if you want to see the effects immediately, also take a look at this and this.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromSeconds(0),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});

Extend forms authentication to use a custom http header for ticket

I have a wcf webhttp service which uses forms authentication to authenticate users. This works fine if the ticket comes in the cookie collection or in the url.
But now I want to send the string of the forms auth ticket in a custom http header and change the forms auth module to check for that header instead of the cookie.
I think it should be easy to extend forms auth to achive this, but could not find any resources of how to. Can you point me in the right direction ?
here's how my authentication flow would work,
A client calls the authenticate method with the username and pwd
Service returns the encrypted ticket string
Client send the received ticket string in a http header with every subsequent request
Service checks for auth header and validates the auth ticket
FormAuthentication module is not extendible, but you could write your own authentication.
It is very simple:
Authentication(2):
var formsTicket = new FormsAuthenticationTicket(
1, login, DateTime.Now, DateTime.Now.AddYears(1), persistent, String.Empty);
var encryptedFormsTicket = FormsAuthentication.Encrypt(formsTicket);
//return encryptedFormsTicket string to client
Service call with attached ticket(4):
var ticket = FormsAuthentication.Decrypt(encryptedFormsTicket)
//extract authentication info from ticket: ticket.Name
I am not sure this is the way to go (elegance-wise), but what about adding an event in global.asax.cs for Application BeginRequest and taking the string from the header and injecting a cookie into the Request yourself (Forms authentication should then pick that up).
Something like:
protected void Application_BeginRequest()
{
// Your code here to read request header into cookieText variable
string cookieText = ReadCookieFromHeader();
var cookieData = FormsAuthentication.Decrypt(cookieText);
if (!cookieData.Expired)
{
HttpContext.Current.Request.Cookies.Add(new HttpCookie(cookieData.Name, cookieText));
}
}
DISCLAIMER: Please note that I didn't test this, just throwing a possible approach your way!

Resources