ITicketStore AuthenticationTicket ExpiresUtc value always 14 days - c#

I'm creating a custom ticket store that inherits from ITicketStore in order to store user tokens in a database since cookies were getting far too large due to saving tokens.
I've got everything setup and working, however, I cannot get the value of ExpiresUtc to be anything other than 14 days in the AuthenticationTicket that is passed into the ITicketStore StoreAsync method.
Looking at my ADFS instance, I've got refresh token lifetime down to 5 minutes and access token lifetime down to 2 minutes. I am still unable to get the correct value stuffed into the AuthenticationTicket.
I've found I can manually change it once I'm in the ITicketStore StoreAsync method, however this isn't ideal since if the refresh token lifetime were to change on the ADFS, it would not be respected here.
Does ASP .net core set its own value for this? Is ADFS setting the value itself and overriding my values? What could be happening?
STARTUP.CS
// Authentication / Authorization
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = Configuration["OpenIdConnect:Authority"];
options.Resource = Configuration["OpenIdConnect:Resource"];
options.ClientId = Configuration["OpenIdConnect:ClientId"];
options.ClientSecret = Configuration["OpenIdConnect:ClientSecret"];
options.Scope.Clear();
options.Scope.Add(Configuration["OpenIdConnect:Scopes"]);
options.SaveTokens = true;
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.GetClaimsFromUserInfoEndpoint = false;
})
.AddCookie(options =>
{
options.SessionStore = new CustomTicketStore(services);
});
CustomTicketStore.CS
public async Task<string> StoreAsync(AuthenticationTicket ticket)
The CustomTicketStore was based around the following implementation:
https://ml-software.ch/posts/implementing-a-custom-iticketstore-for-asp-net-core-identity-update

The ExpiresUtc value is controlled by CookieAuthenticationOptions.ExpireTimeSpan which defaults to 14 days.
OpenIdConnectOptions.UseTokenLifetime can override this to make the expiration match the auth token.

Related

Blazor Server Side and Azure B2C Login using invite, how to authenticate with returned token?

I have followed a few different tutorials on how to get Signup by invite working and I am very close to getting it working in blazor server side but I am having issues with the final returned token.
I have 2 authentications setup, one which is the default Microsoft Identity and my custom one which is used for sign ups via an email link.
Everything seems to work until the final step.
When you click the link, it takes you to Azure signup pages asking for Name, email etc and then it returns back to my site with the returned "id_token".
When this happens I get the following error.
InvalidOperationException: The authentication handler registered for scheme 'OpenIdConnect' is 'OpenIdConnectHandler' which cannot be used for SignInAsync. The registered sign-in schemes are: Cookies.
I set breakpoints in the OpenIdConnectEvents on event TicketReceived and I can see that the TicketReceivedContext object has a valid ClaimsPrinciple with correct claims and IsAuthenticated is true.
My return page never gets hit because of the error.
Any ideas on how to fix this?
Edit
My startup registration for the 2 authentications.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>{
builder.Configuration.Bind("AzureAd", options);
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.Events = new CustomOpenIdConnectEvents();
options.DataProtectionProvider = protector;
}, subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true);
and
string invite_policy = builder.Configuration.GetSection("AzureAdB2C")["SignUpSignInPolicyId"]; builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddOpenIdConnect(invite_policy, GetOpenIdSignUpOptions(invite_policy, builder.Configuration));
Action<OpenIdConnectOptions> GetOpenIdSignUpOptions(string policy, Microsoft.Extensions.Configuration.ConfigurationManager Configuration)
=> options =>
{
builder.Configuration.Bind("AzureAdB2C", options);
options.ResponseType = OpenIdConnectResponseType.IdToken;
string B2CDomain = Configuration.GetSection("AzureAdB2C")["B2CDomain"];
string Domain = Configuration.GetSection("AzureAdB2C")["Domain"];
options.MetadataAddress = $"https://{B2CDomain}/{Domain}/{policy}/v2.0/.well-known/openid-configuration";
options.ResponseMode = OpenIdConnectResponseMode.FormPost;
options.CallbackPath = "/LoginRedirect";
options.Events = new CustomOpenIdConnectEvents();
options.DataProtectionProvider = protector;
};
Update:
So thanks to Wolfspirit's answer I am now closer to solving the issue.
My start up registration has now changed to this.
string invite_policy = builder.Configuration.GetSection("AzureAdB2C")["SignUpSignInPolicyId"];
builder.Services.AddAuthentication(options => {
options.DefaultScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(invite_policy, GetOpenIdSignUpOptions(invite_policy, builder.Configuration))
.AddMicrosoftIdentityWebApp(options =>{
builder.Configuration.Bind("AzureAd", options);
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.Events = new CustomOpenIdConnectEvents();
options.DataProtectionProvider = protector;
}, subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true);
My issue now is that User.Identity.Name is returning a null if you login through the signup method. If you login the normal default way then everything is fine. I checked the claims and they are correct so not sure why Name is no being populated.
According to the error you got, you've set "OpenIdConnect" as the SignIn scheme inside the "AddAuthentication" call. It would be helpful to know how you've set up your Program.cs/Startup.cs but it should look similar to this for example:
https://github.com/onelogin/openid-connect-dotnet-core-sample/blob/master/Startup.cs#L32
You need to use both schemes cause one manages the "where to store the authentication after login" while the other manages the "how to log in" way. If you set both to OpenIdConnect then you can log in but asp.net core will not know how to actually "sign" you in by storing the login result into a cookie.
ok so I have figured out the answer, with credit to Wolfspirit for pointing me in the right direction.
string invite_policy = builder.Configuration.GetSection("AzureAdB2C")["SignUpSignInPolicyId"];
builder.Services.AddAuthentication(options => {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(invite_policy, GetOpenIdSignUpOptions(invite_policy, builder.Configuration))
.AddMicrosoftIdentityWebApp(options =>{
builder.Configuration.Bind("AzureAd", options);
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.Events = new CustomOpenIdConnectEvents();
options.DataProtectionProvider = protector;
// Thanks to Nan Yu for the folowing to fix the null name after login
//https://stackoverflow.com/questions/54444747/user-identity-name-is-null-after-federated-azure-ad-login-with-aspnetcore-2-2
options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" };
}, subscribeToOpenIdConnectMiddlewareDiagnosticsEvents: true); // don't need this, for debugging.

.Net Core 3.1 OpenIdConnect with AWS Cognito

I've been trying to get AWS Incognito working with ASP.NET Core 3.1 all day and am missing sime essential piece.
The code below is close because:
It Will
Let the user go through the built-in ASP.NET Identity user registration and login pages
That will result in records for the user in both the AspNetUsers table and the AspNetUserLogins table.
Appear to log the user in when they pass through the ASP.Net Identity UI ExternalLogin.cshml page's Callback method:
But in reality ASP.Net Identity is unaware of this user
In addition it will result in an infinite loop if you try to access an authorized page, bouncing between the requested page, the AWS Cognito server, and the "signin-oidc" built-in route.
The line I suspect the most is:
options.SignInScheme = IdentityConstants.ExternalScheme;
As the code comments mention, commentng out this line will break the built-in registration, but solve the infiniteloop problem (as well as let the openid claims cookie stay populated).
It's pretty clear that it's close, but there's something not quite linked up to bring it all together.
var awsCognitoRegion = "us-east-1";
var awsCognitoPoolId = "******";
var metaDataAddress = $"https://cognito-idp.{awsCognitoRegion}.amazonaws.com/{awsCognitoPoolId}/.well-known/openid-configuration";
//var awsCognitoResponseType = "code";
var awsCognitoMetaAddress = metaDataAddress;
var awsCognitoClientId = "*******************";
var awsCognitoSecret = "**********************";
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
//fixes "Error loading external login information" error on Identity Login page / callback
//after external login...but breaks identity/cookie/claims?
//
//Also Causes infinite redirect problem via signin-oidc
options.SignInScheme = IdentityConstants.ExternalScheme;
//show all claims since MS filters some out...
options.ClaimActions.Clear();
//Tell .Net Core identity where to find the "name"
//options.TokenValidationParameters.NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress";
options.TokenValidationParameters.NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
options.ResponseType = "code";
options.MetadataAddress = metaDataAddress;
options.ClientId = awsCognitoClientId;
options.ClientSecret = awsCognitoSecret;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
//I think all I need here is email
options.Scope.Add("email");
options.Scope.Add("profile");
options.Scope.Add("openid");
//options.Scope.Add("aws.cognito.signin.user.admin");
});
As these authentication things always are, it ended up being one property missing/set wrong.
In this case it was that the SigninManager was checking if the Identity had principal.Identities.Any(i => i.AuthenticationType == IdentityConstants.ApplicationScheme); (https://github.com/aspnet/Identity/blob/master/src/Identity/SignInManager.cs). This is why even though the Identity would have IsAuthentictated = true, the built-in ASP.Net Identity pages were broken, because that value was set to AuthenticationTypes.Federation.
After more digging around I discovered that in the options of the .AddOpenIdConnect method there is a way to set that:
options.TokenValidationParameters.AuthenticationType = IdentityConstants.ApplicationScheme;
That finally made ASP.Net Identity aware that I was logged in (through OpenId).

How to disable External Logins in Identity Core?

I am in the process of integrating a simplefied authentication process into a asp.net core 2.1 application, where users are logging in via the UI by default, but there is also the possibility to aquire a token and call some secured api endpoints to retrieve some data needed for reporting.
The issue I am facing is, that with the default configuration everything works, but adding the token config throws some weird errors.
If I do not add AddCookie("Identity.External"), call to the onGet method at /Identity/Account/Login throws the exception
InvalidOperationException: No sign-out authentication handler is registered for the scheme 'Identity.External'. The registered sign-out schemes are: Identity.Application. Did you forget to call AddAuthentication().AddCookies("Identity.External",...)?
If I do not specify options.DefaultScheme = "Identity.Application"; the user is not successfully signed in.
If I do not add .AddCookie("Identity.External") and .AddCookie("Identity.TwoFactorUserId") the logout process throws the same exception as above.
For the login process, this is simply rectified by removing the line await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);. If I do not use external schemes I do not need to sign out of them, right?
This brings me to my problem: How can I disable external logins and multi factor authentication in Identity Core, so I do not have to add those cookies in the first place? Furthermore, why do I have to specifiy a cookie named "Identity.Application", which is not the case in the default configuration? I'm pretty sure this is just another issue of me not thoroughly understanding the problem at hand, so I am grateful for any clarification on this.
This is my Identity config from the Startup.cs I have also scaffolded out the complete Identity UI with a custom IdentityUser class.
var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));
services.Configure<JwtIssuerOptions>(options =>
{
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});
var tokenValidationParameters = new TokenValidationParameters
{
/*...*/
};
services.AddAuthentication(options =>
{
options.DefaultScheme = "Identity.Application";
})
//.AddCookie("Identity.External")
//.AddCookie("Identity.TwoFactorUserId")
.AddCookie("Identity.Application", opt =>
{
opt.SlidingExpiration = true;
})
.AddJwtBearer(options =>
{
options.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.TokenValidationParameters = tokenValidationParameters;
options.SaveToken = true;
});
var builder = services.AddIdentityCore<AppUser>(o =>
{
//removed
});
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();

Cookies with a SameSite policy enforced are blocked in iOS 12 for SSO flows involving cross-origin requests

Summary: Third party login breaks in iOS / OS 12!
We have a common login that works across multiple websites. This is working fine in Firefox, Chrome and Safari on Windows, macOS and iOS. But with iOS 12 and macOS 12, it seems cookies are no longer working from auth0 login window to our login API.
It has stopped working not just in Safari, but on iOS 12 also in Chrome and Firefox (it still works in Chrome on Mac OS 12). I suspect this has to do with Intelligent Tracking Prevention 2.0, but I'm struggling to find many technical details.
Our login flow is as follows:
User clicks login which sets window.location.href to login url on the universal (different) login domain.
This calls ChallengeAsync which sends user to auth0 domain for login.
User is then sent back to the login domain, but at this point the cookies from auth0 and session cookies set in the controller are missing.
I use the following in startup:
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.Cookie.Path = "/";
options.SlidingExpiration = false;
})
.AddOpenIdConnect("Auth0", options => {
// Set the authority to your Auth0 domain
options.Authority = $"https://{Configuration["Auth0:Domain"]}";
// Configure the Auth0 Client ID and Client Secret
options.ClientId = Configuration["Auth0:ClientId"];
options.ClientSecret = Configuration["Auth0:ClientSecret"];
// Set response type to code
options.ResponseType = "code";
// Configure the scope
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("offline_access");
options.CallbackPath = new PathString("/signin-auth0");
options.ClaimsIssuer = "Auth0";
options.SaveTokens = true;
options.Events = new OpenIdConnectEvents
{
OnRemoteFailure = context => {
<not relevant error redirects>
},
OnRedirectToIdentityProvider = context =>
{
context.ProtocolMessage.SetParameter("audience", $"{ Configuration["Auth0:ApiIdentifier"]}");
return Task.FromResult(0);
},
OnRedirectToIdentityProviderForSignOut = (context) =>
{
<not relevant logout handling>
}
};
});
In the login controller I have a login action which just sets a session value and calls ChallengeAsync to open the Auth0 login:
await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { IsPersistent = true, ExpiresUtc = DateTime.UtcNow.AddMinutes(Global.MAX_LOGIN_DURATION_MINUTES), RedirectUri = returnUri });
The "returnUri" parameter is the full path back to this same controller, but different action. It is when this action is hit that both cookies from the auth0 login (i.e. https://ourcompany.eu.auth0.com) and session data I set in the login action are missing in iOS 12.
Can I do it in some other way that will work on iOS 12? All help appreciated.
I have finally figured it out. The cookie set by default uses SameSiteMode.Lax. This has worked fine everywhere up until iOS 12, where it now needs to be set to SameSiteMode.None.
This is the modification I use that has got it working again:
.AddCookie(options =>
{
options.Cookie.Path = "/";
options.SlidingExpiration = false;
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.Expiration = TimeSpan.FromMinutes(Global.MAX_LOGIN_DURATION_MINUTES);
})

Add ClaimsIdentity to ClaimsPrincipal - JwtBearer

My system consists of two authorization steps (but not the standard way).
My client application first connects with server passing login and password (it's just something like ApiSecret and ApiKey).
Next, after authentication, server returns bearer token with basic info (username, roles etc). But notice that this user is like ApiClient not a living person :)
Next, application shows login form. And this is time for a living person to login. So he passes his credentials to API which checks whether this user can be logged in.
And this is the place that I have problem with. Until now I thought it would be work like that:
If the user can be logged to the application, I create new ClaimsIdentity and ADD it to ClaimsPrincipal Identities.
The idea is great but it doesn't work :/ It turns out that next requests don't send this second identity information. I even know why. Becase ClaimsPrincipal is created based on received bearer token. But this knowledge doesn't solve my problem.
What should I do to add new ClaimsIdentity to existing ClaimsPrincipal and store this value between requests? (until user logges out of the application)
After a lot of digging and research I was able to create a solution (.Net Core 2).
You have to add Cookie Authentication in configure services and all of this should look like that:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(cfg =>
{
//standard settings
})
.AddCookie(AuthTypes.CLIENT_AUTHENTICATION_TYPE, cfg =>
{
//cookie settings; the most important is following event:
cfg.Events.OnValidatePrincipal = (CookieValidatePrincipalContext ctx) =>
{
ClaimsPrincipal mainUser = ctx.HttpContext.User; //get ClaimsPrincipal from JwtBearer
ClaimsPrincipal cookieUser = ctx.Principal; //get ClaimsPrincipal read from Cookie
Debug.Assert(mainUser.Identities.Count() == 1);
//now we have to add ClaimsIdentity to main ClaimsPrincipal (from JwtBearer). We add only those absent in main ClaimsPrincipal (here is simplified solution)
var claimsToAdd = cookieUser.Identities.Where(id => id.AuthenticationType != mainUser.Identities.ElementAt(0).AuthenticationType);
mainUser.AddIdentities(claimsToAdd);
return Task.CompletedTask;
};
}
);
AuthTypes.CLIENT_AUTHENTICATION_TYPE - it's just string with your authentication type name.
Next we have to configure default policy filter later on in ConfigureServices (basic configuration):
services.AddMvc(config =>
{
var defaultPolicy = new AuthorizationPolicyBuilder(new[] { JwtBearerDefaults.AuthenticationScheme, AuthTypes.CLIENT_AUTHENTICATION_TYPE })
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(defaultPolicy));
});
What's important here is to pass this array in AuthorizationPolicyBuilder.
Now Authorization will take under consideration JwtBearer, but cookie will also be read.
And now how to set the cookie at all. This can be additional login process (you do it at the controller level):
var authProps = new AuthenticationProperties
{
IsPersistent = true,
IssuedUtc = DateTimeOffset.Now
};
await HttpContext.SignInAsync(AuthTypes.CLIENT_AUTHENTICATION_TYPE, User, authProps);
User here is just ClaimsPrincipal with additional ClaimsIdentities.
And that's all folks :)

Categories

Resources