Identity Server 3 - 401 on Ajax Calls instead of 302 - asp.net

I have a web api / mvc hybrid app and I have configured it to use cookie authentication. This works fine for the mvc portion of the application. The web api does enforce the authorization, but instead of returning a 401 - Unauthorised it returns a 302 - Found and redirects to the login page. I would rather it returns a 401. I have attempted to hook into the CookieAuthenticationProvider.OnApplyRedirect delegate, but this doesn't seem to be called. What have I missed? My current setup is below:
AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
ExpireTimeSpan = TimeSpan.FromMinutes(20),
SlidingExpiration = true,
CookieHttpOnly = true,
CookieSecure = CookieSecureOption.Never, //local non ssl-dev only
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = IdentityConfig.Authority,
ClientId = IdentityConfig.SoftwareClientId,
Scope = "openid profile roles",
RedirectUri = IdentityConfig.RedirectUri,
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies"
});

In your example the UseCookieAuthentication no longer controls this, instead the UseOpenIdConnectAuthentication does. This involves using the Notifications property and intercepting OpenID Connect authentication requests.
Try out the following for inspiration:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Authority = IdentityConfig.Authority,
ClientId = IdentityConfig.SoftwareClientId,
Scope = "openid profile roles",
RedirectUri = IdentityConfig.RedirectUri,
ResponseType = "id_token",
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = notification =>
{
if (notification.ProtocolMessage.RequestType == OpenIdConnectRequestType.AuthenticationRequest)
{
if (IsAjaxRequest(notification.Request) && notification.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
{
notification.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
notification.HandleResponse();
return Task.FromResult(0);
}
}
return Task.FromResult(0);
}
}
});

In my case the IsAjaxRequest did not do the trick. Instead I rely on all routes to the WebAPI being under "/api", so instead of the IsAjaxRequest I do:
RedirectToIdentityProvider = context => {
if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication){
if (context.Request.Path.StartsWithSegments(new PathString("/api")) && context.Response.StatusCode == (int)HttpStatusCode.Unauthorized){
context.HandleResponse();
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}

Related

ASP.NET MVC OpenIdConnect OnAuthorizationCodeReceived not triggering after app restart

I work in a corporate and we have got an AAD for all the users in our organisation.
I am working on creating an app that authenticates users with AAD using OpenIdConnect. Now, I have followed this sample from Azure AD B2C. it works fine when I run it locally for the first time (after an hour break) and OnAuthorizationCodeReceived is triggered, however, when I stop and restart the app in visual studio OnAuthorizationCodeReceived doesn't get triggered and as a result, I get a null User (IAccount) when trying to retrieve token silently in this code block
public async Task<string> GetAccessToken(string scopes)
{
//var userClaims = User.Identity as System.Security.Claims.ClaimsIdentity;
var userClaims2 = ClaimsPrincipal.Current.Claims;
IConfidentialClientApplication cc = MsalAppBuilder.BuildConfidentialClientApplication();
var userAccount = await cc.GetAccountAsync(ClaimsPrincipal.Current.GetMsalAccountId());
var userAccount2 = await cc.GetAccountsAsync(ClaimsPrincipal.Current.FindFirst(Globals.ObjectIdClaimType).Value);
var userAccount3 = userAccount2.FirstOrDefault();
AuthenticationResult result = await cc.AcquireTokenSilent(new string[] { scopes }, userAccount3).ExecuteAsync();
return result.AccessToken;
}
My startup.Auth file looks like this:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
// Configure OpenIDConnect, register callbacks for OpenIDConnect Notifications
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Sets the ClientId, authority, RedirectUri as obtained from web.config
ClientId = ConfigHelper.ClientId,
Authority = String.Format(CultureInfo.InvariantCulture, aadInstance, ConfigHelper.Tenant),
PostLogoutRedirectUri = ConfigHelper.PostLogoutRedirectUri,
RedirectUri = ConfigHelper.PostLogoutRedirectUri,
ResponseType = OpenIdConnectResponseTypes.CodeIdToken,
// ValidateIssuer set to false to allow 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 or Id (example: contoso.onmicrosoft.com)
// To allow users from only a list of specific organizations, set ValidateIssuer to true and use ValidIssuers parameter
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
ValidateIssuer = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = (context) =>
{
//string redirectURI = string.Format("{0}://{1}{2}/", context.Request.Scheme, context.Request.Host, context.Request.PathBase);
string redirectURI = string.Format("https://{0}{1}/", context.Request.Host, context.Request.PathBase);
context.ProtocolMessage.RedirectUri = redirectURI;
context.ProtocolMessage.DomainHint = domain_hint;
return Task.FromResult(0);
},
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = (context) =>
{
if (ConfigHelper.NonceExceptionHandler && (context.Exception.Message.StartsWith("OICE_20004") || context.Exception.Message.Contains("IDX10311") || context.Exception.Message.Contains("IDX21323")))
{
context.SkipToNextMiddleware();
return Task.FromResult(0);
}
return Task.FromResult(0);
}
}
});
app.Use<MsOfficeLinkPrefetchMiddleware>();
// add this function into the app pipeline to Call my function OnAuth and call the next in the pipeline
app.Use((context, next) =>
{
// The function to call...
OnAuth(context);
return next.Invoke();
});
// limit the calls to the above function to be in the PostAuthenticate part of the stage
app.UseStageMarker(PipelineStage.PostAuthenticate);
}
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
context.HandleResponse();
context.Response.Redirect("/?errormessage=" + context.Exception.Message);
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
string redirectURI = string.Format("{0}://{1}{2}/", notification.Request.Scheme, notification.Request.Host, notification.Request.PathBase);
IConfidentialClientApplication confidentialClient = MsalAppBuilder.BuildConfidentialClientApplication(new ClaimsPrincipal(notification.AuthenticationTicket.Identity));
// Upon successful sign in, get & cache a token using MSAL
//user.readbasic.all
AuthenticationResult result = await confidentialClient.AcquireTokenByAuthorizationCode(new[] { "user.readbasic.all" }, notification.Code).ExecuteAsync();
}
I have been trying for a few days to solve this problem but to no avail. Please Help.
Cheers

Azure SSO login with ApplicationId and Tenant Id not returning successful claim output

public partial class Startup
{
public void ConfigureAuth(IAppBuilder app) {
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions() {
CookieDomain = ".xxx.com"
});
var notifications = new OpenIdConnectAuthenticationNotifications {
AuthenticationFailed = OnAuthenticationFailed
};
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions {
ClientId = SystemSettings.ClientId, //This is the client Id of the central Multi-tenant Azure AD application
Authority = SystemSettings.Authority,
PostLogoutRedirectUri = SystemSettings.PostLogoutRedirectUri,
Notifications = notifications,
//ProtocolValidator = new OpenIdConnectProtocolValidator() { RequireNonce = false},
UseTokenLifetime = false,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters() {
ValidIssuers = SystemSettings.ValidIssuers
}
});
}
}
For SSO login we are calling OWIN context:
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = string.IsNullOrWhiteSpace(returnUrl) ? "/account/authenticated" : string.Format("/account/authenticated?companyCode={0}&returnUrl={1}", companyCode, HttpUtility.UrlEncode(returnUrl)) },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
return null;
After SSO succesfully login, I am redirecting to below route details:
[Route("account/authenticated")]
[AllowAnonymous]
public ActionResult Authenticated(string returnUrl, string companyCode) {
FileLogger.Log($"System.Web.HttpContext.Current.Request.IsAuthenticated: {System.Web.HttpContext.Current.Request.IsAuthenticated}");
var identity = (ClaimsIdentity)Thread.CurrentPrincipal.Identity;
var claims = JsonConvert.SerializeObject(identity?.Claims?.ToList(), new JsonSerializerSettings() {
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
FileLogger.Log($"claims: {claims}");
if (System.Web.HttpContext.Current.Request.IsAuthenticated) {
var token = AuthorizationService.AuthorizeUser();
FileLogger.Log($"AuthorizationService.AuthorizeUser() returns: {token}");
if (!string.IsNullOrWhiteSpace(token)) {
ViewBag.ClientCode = companyCode;
ViewBag.Token = token;
ViewBag.ReturnUrl = returnUrl;
return View();
}
return null;
}
var currentClaimsPrincipal = ClaimsPrincipal.Current;
if (currentClaimsPrincipal != null && currentClaimsPrincipal.Claims != null) {
var myClaimsPrincipal = new ClaimsIdentity(currentClaimsPrincipal.Claims);
}
return null;
}
But claim output is not coming and i am getting false authentication and no claim:
Identity:
{System.Security.Principal.GenericIdentity}
Actor: null
AuthenticationType: ""
BootstrapContext: null
Claims: {System.Security.Claims.ClaimsIdentity.<get_Claims>d__51}
CustomSerializationData: null
IsAuthenticated: false
Label: null
Name: ""
NameClaimType: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
RoleClaimType: "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
As far as I knew, we can use the following code to get the claims after we complete Azure AD auth
var userClaims = User.Identity as System.Security.Claims.ClaimsIdentity;
/*
The token's claim "aud" is the application's client ID. For more deatils, please refer to https://learn.microsoft.com/en-us/azure/architecture/multitenant-identity/claims.
*/
foreach (var claim in userClaims.Claims) {
// get app id
}
// TenantId is the unique Tenant Id - which represents an organization in Azure AD
ViewBag.TenantId = userClaims?.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
I solved it by updating OWIN packages and with below links:
https://dotnetcodetips.com/Tip/91/Azure-OWIN-website-login-gets-stuck-on-a-never-ending-redirect-loop.

No authentication handler is registered for the scheme 'Cookies'. The registered schemes are: Application, Bearer, ASOS

I am implementing Aspnet.security.openidconnect (ASOS) with .net core 2.1 application. Now the issue is when I am trying to execute this chunk in controller,
public async Task<IActionResult> Authorize()
{
if (Response.StatusCode != 200)
{
return View("AuthorizeError");
}
var ticket = await AuthenticationHttpContextExtensions.AuthenticateAsync(HttpContext, CookieAuthenticationDefaults.AuthenticationScheme);
var identity = ticket != null && ticket.Principal != null ? ticket.Ticket.Principal : null;
if (identity == null)
{
await AuthenticationHttpContextExtensions.ChallengeAsync(HttpContext, CookieAuthenticationDefaults.AuthenticationScheme, null);
return Unauthorized();
}
ViewData["Name"] = ticket.Principal.Identity.Name;
var scopes = (HttpContext.Request.Query["scope"].ToString() ?? "").Split(' ');
ViewData["Scopes"] = scopes;
//var claimsIdentity = new ClaimsIdentity(identity.Claims, "Bearer", identity.NameClaimType, identity.RoleClaimType);
var claimsIdentity = new ClaimsIdentity(identity.Claims, "Bearer");
foreach (var scope in scopes)
{
claimsIdentity.AddClaim(new Claim("urn:oauth:scope", scope));
}
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await AuthenticationHttpContextExtensions.SignInAsync(HttpContext, claimsPrincipal);
logger.Info("Authorize request received");
return View();
}
The error I am getting on this line:
var ticket = await AuthenticationHttpContextExtensions.AuthenticateAsync(HttpContext, CookieAuthenticationDefaults.AuthenticationScheme);
And here is the implementation of ASOS in startup:
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie("Application", options =>
{
options.LoginPath = new PathString(LoginPath);
options.LogoutPath = new PathString(LogoutPath);
options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
//options.AccessDeniedPath = new PathString();
});
//services.AddAuthentication("External")
// .AddCookie("Cookies", options =>
// {
// options.Cookie.Name = CookieAuthenticationDefaults.CookiePrefix + "External";
// options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
// });
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddAuthentication(OAuthValidationDefaults.AuthenticationScheme).AddOAuthValidation()
.AddOpenIdConnectServer(options =>
{
options.AuthorizationEndpointPath = new PathString(AuthorizePath);
// Enable the token endpoint.
options.TokenEndpointPath = new PathString(TokenPath);
options.ApplicationCanDisplayErrors = true;
options.AccessTokenLifetime = TimeSpan.FromMinutes(5);
#if DEBUG
options.AllowInsecureHttp = true;
#endif
options.Provider.OnValidateAuthorizationRequest = context =>
{
if (string.Equals(context.ClientId, Configuration["OpenIdServer:ClientId"], StringComparison.Ordinal))
{
context.Validate(context.RedirectUri);
}
return Task.CompletedTask;
};
// Implement OnValidateTokenRequest to support flows using the token endpoint.
options.Provider.OnValidateTokenRequest = context =>
{
// Reject token requests that don't use grant_type=password or grant_type=refresh_token.
if (!context.Request.IsClientCredentialsGrantType() && !context.Request.IsPasswordGrantType()
&& !context.Request.IsRefreshTokenGrantType())
{
context.Reject(
error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
description: "Only grant_type=password and refresh_token " +
"requests are accepted by this server.");
return Task.CompletedTask;
}
if (string.IsNullOrEmpty(context.ClientId))
{
context.Skip();
return Task.CompletedTask;
}
if (string.Equals(context.ClientId, Configuration["OpenIdServer:ClientId"], StringComparison.Ordinal) &&
string.Equals(context.ClientSecret, Configuration["OpenIdServer:ClientSecret"], StringComparison.Ordinal))
{
context.Validate();
}
return Task.CompletedTask;
};
// Implement OnHandleTokenRequest to support token requests.
options.Provider.OnHandleTokenRequest = context =>
{
// Only handle grant_type=password token requests and let
// the OpenID Connect server handle the other grant types.
if (context.Request.IsClientCredentialsGrantType() || context.Request.IsPasswordGrantType())
{
//var identity = new ClaimsIdentity(context.Scheme.Name,
// OpenIdConnectConstants.Claims.Name,
// OpenIdConnectConstants.Claims.Role);
ClaimsIdentity identity = null;
if (context.Request.IsClientCredentialsGrantType())
{
identity = new ClaimsIdentity(new GenericIdentity(context.Request.ClientId, "Bearer"), context.Request.GetScopes().Select(x => new Claim("urn:oauth:scope", x)));
}
else if (context.Request.IsPasswordGrantType())
{
identity = new ClaimsIdentity(new GenericIdentity(context.Request.Username, "Bearer"), context.Request.GetScopes().Select(x => new Claim("urn:oauth:scope", x)));
}
// Add the mandatory subject/user identifier claim.
// By default, claims are not serialized in the access/identity tokens.
// Use the overload taking a "destinations" parameter to make sure
// your claims are correctly inserted in the appropriate tokens.
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"), OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
var ticket = new Microsoft.AspNetCore.Authentication.AuthenticationTicket(
new ClaimsPrincipal(identity),
new Microsoft.AspNetCore.Authentication.AuthenticationProperties(),
context.Scheme.Name);
// Call SetScopes with the list of scopes you want to grant
// (specify offline_access to issue a refresh token).
ticket.SetScopes(
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess);
context.Validate(ticket);
}
return Task.CompletedTask;
};
Now the error I am getting is:
InvalidOperationException: No authentication handler is registered for
the scheme 'Cookies'. The registered schemes are: Application, Bearer,
ASOS. Did you forget to call
AddAuthentication().AddSomeAuthHandler?
What am I missing here. Any help?
So found the issue, actually I was using "Application" name for cookie scheme and in controller I was using default name "Cookies". So just had to remove the explicit "Application" name to default "Cookies" name
No authenticationScheme was specified, and there was no DefaultChallengeScheme found Cookies Authentication
In my case I was using "Cookies" when adding authentication and "Cookie" when calling the SiginOut method.
Changed both the places to use "Cookies"
Startup:
services.AddAuthentication(config => {
config.DefaultScheme = "Cookies";
config.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")<---- Change here.
.AddOpenIdConnect("oidc", config => {
config.Authority = "https://localhost:44392/";
config.ClientId = "client_id_mvc";
config.ClientSecret = "client_secret_mvc";
config.SaveTokens = true;
config.ResponseType = "code";
//config.SignedOutCallbackPath = "/Privacy";
});
Calling SignOut:
public async Task<IActionResult> OnPostAsync()
{
return SignOut("Cookies", "oidc");
}

Keycloak client for ASP.NET Core

Is there any existing Keycloak client for Asp.net Core? I have found a NuGet package for .net but it doesn't work with Core. Do you have any ideas how to easily integrate with this security server (or maybe using any other alternatives)?
I've played a bit with this today. The most straightforward way is too use OpenId standard.
In Startup.cs I used OpenIdConnect Authentication:
public void Configure(...)
{ (...)
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AutomaticAuthenticate = true,
CookieHttpOnly = true,
CookieSecure = CookieSecurePolicy.SameAsRequest
});
app.UseOpenIdConnectAuthentication(CreateKeycloakOpenIdConnectOptions());`(...)
}`
OpenIdConnectOptions method:
private OpenIdConnectOptions CreateKeycloakOpenIdConnectOptions()
{
var options = new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
Authority = Configuration["Authentication:KeycloakAuthentication:ServerAddress"]+"/auth/realms/"+ Configuration["Authentication:KeycloakAuthentication:Realm"],
RequireHttpsMetadata = false, //only in development
PostLogoutRedirectUri = Configuration["Authentication:KeycloakAuthentication:PostLogoutRedirectUri"],
ClientId = Configuration["Authentication:KeycloakAuthentication:ClientId"],
ClientSecret = Configuration["Authentication:KeycloakAuthentication:ClientSecret"],
ResponseType = OpenIdConnectResponseType.Code,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true
};
options.Scope.Add("openid");
return options;
}
In appsettings.json add configuration for Keycloak:
{
(...),
"Authentication": {
"KeycloakAuthentication": {
"ServerAddress": "http://localhost:8180",
"Realm": "demo",
"PostLogoutRedirectUri": "http://localhost:57630/",
"ClientId": "KeycloakASPNETCore",
"ClientSecret": "secret-get-it-in-keycloakConsole-client-credentials"
}
}
}
Keycloak client is configuerd as followed:
Client settings,
I've added 'accounting' role for test,
I added mapper 'member_of' of type 'User Client Role' for roles so that roles are added in the claims
If I want to Authorize user by role I do something like this:
Add authorization by claims in ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
(...)
services.AddAuthorization(options =>
{
options.AddPolicy("Accounting", policy =>
policy.RequireClaim("member_of", "[accounting]")); //this claim value is an array. Any suggestions how to extract just single role? This still works.
});
}
I've edited get method in ValuesController (Default Web API template):
[Authorize(Policy = "Accounting")]
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public Dictionary<string,string> Get()
{
var userPrinciple = User as ClaimsPrincipal;
var claims = new Dictionary<string, string>();
foreach (var claim in userPrinciple.Claims)
{
var key = claim.Type;
var value = claim.Value;
claims.Add(key, value);
}
return claims;
}
If I login with user that has accounting role or is in group that has accounting role, it should display my user claims on address localhost:57630/api/values.
I hope this works for you.
Edit: .NET Core 2
Hi everyone! The way my app works changed quite a bit and I have not fully tested .NET Core 2 yet, but you can still try connecting to Keycloak like this in ConfigureServices:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = Configuration["Authentication:KeycloakAuthentication:ServerAddress"] + "/auth/realms/" + Configuration["Authentication:KeycloakAuthentication:Realm"];
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidAudiences = new string[] { "curl", "financeApplication", "accountingApplication", "swagger"}
};
options.RequireHttpsMetadata = false; //for test only!
options.SaveToken = true;
options.Validate();
});
And in Configure:
app.UseAuthentication();
You can access your token later with IHttpContextAccessor httpContextAccessor, for example:
public KeycloakAuthorizationRequirementHandler(IConfiguration config,
IHttpContextAccessor httpContextAccessor,
IMemoryCache memoryCache)
{
_config = config;
_httpContextAccessor = httpContextAccessor;
_memoryCache = memoryCache;
}
//get accessToken
var accessToken = _httpContextAccessor.HttpContext.GetTokenAsync("access_token");
_httpContextAccessor.HttpContext.Items["username"] = username;
Tell me how it goes.
If you want to use standard .Net Role mappings with Keycloak Client Roles, setup like so:
Startup.cs:
services.AddAuthorization(options =>
{
options.AddPolicy("Users", policy =>
policy.RequireRole("Users"));
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = Configuration["Authentication:oidc:Authority"]
options.ClientId = Configuration["Authentication:oidc:ClientId"];
options.ClientSecret = Configuration["Authentication:oidc:ClientSecret"];
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.RemoteSignOutPath = "/SignOut";
options.SignedOutRedirectUri = "Redirect-here";
options.ResponseType = "code";
});
appsettings.json:
"Authentication": {
"oidc": {
"Authority":"http://your-keycloak-server/auth/realms/your-realm",
"ClientId":"Your-Client-Name",
"ClientSecret":"Your-client-secret"
}
}
Keycloak Client Settings:
Create new Token Mapper
Mapper-Values (enter your own client name)
Now you can use standard authorize role statements to apply your Keycloak Client Roles to your ASP.NET project:
[Authorize(Roles = "Users")]
The thing that worked for us was setting these things in Startup.cs (it's cookie based authentication):
public void Configure(...)
{
(...)
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
AutomaticAuthenticate = true,
CookieHttpOnly = true,
CookieSecure = CookieSecurePolicy.SameAsRequest
});
app.UseOpenIdConnectAuthentication(CreateOpenIdConnectOptions(_customConfig));
(...)
}
And setting up the options:
private OpenIdConnectOptions CreateOpenIdConnectOptions(CustomConfigurationFile configuration)
{
var options = new OpenIdConnectOptions
{
AuthenticationScheme = "oidc",
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
Authority = configuration.ServerAddress + "/auth/realms/" + configuration.Realm,
RequireHttpsMetadata = true,
PostLogoutRedirectUri = configuration.SystemAddress,
ClientId = configuration.ClientId,
ClientSecret = configuration.ClientSecret,
ResponseType = OpenIdConnectResponseType.Code,
GetClaimsFromUserInfoEndpoint = true,
SaveTokens = true
};
options.Scope.Clear();
options.Scope.Add("openid");
return options;
}
Could we get an up-to-date answer on this question with .net core 5+? I've recently installed keycloak version 13.0.0 & its working allowing me to use single sign on with a couple apps. Now for the real reason I installed keycloak, for webapi authentication. Based on the answers above I've installed Microsoft.AspNetCore.Authentication.OpenIdConnect & have been working to get this to work on both the webapi side and on the client side.
For those who building KeyCloak-based authentication for .Net 6 backend and looking for JWT-token based solution here is a code to add in ConfigureServices of your app with Delobytes.AspNetCore.Infrastructure client:
services.AddKeyCloakAuthentication("SchemeName", true, options =>
{
options.Authority = "https://mykeycloakinstallation.com/auth/realms/myrealm"; //"issuer" endpoint
options.Audience = "account";
options.OpenIdConfigurationEndpoint = "https://mykeycloakinstallation.com/auth/realms/myrealm/.well-known/openid-configuration";
options.TokenValidationParameters = new TokenValidationOptions
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateIssuer = true,
ValidIssuer = "https://mykeycloakinstallation.com/auth/realms/myrealm",
ValidateAudience = true,
ValidAudience = "account",
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(2),
};
});

Set Expires or Max-Age for .AspNet.Cookies with Owin OpenIdConnect Middleware

I am running this sample to create multitenant web app that connects using AzureAD with Owin OpenIDConnect middleware. The issued .AspNet.Cookies to authenticate between "my client" and "my server" is always a Session cookie. I would like to set it a Max-Age or an expiration date instead. I have tried several correction without success, for example, I tried to change the the ExpireTimeSpan (see code below) but in my browser cookie inspector I still see Expiration/ Max-Age: Session.
Also, why the SignOut method uses openidconnect and cookies for authentication types while the SignIn method only openidconnect?
AccountController
public void SignIn()
{
HttpContext.GetOwinContext()
.Authentication.Challenge(new AuthenticationProperties {RedirectUri = SettingsHelper.LoginRedirectRelativeUri},
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
public void SignOut()
{
HttpContext.GetOwinContext().Authentication.SignOut(
new AuthenticationProperties { RedirectUri = SettingsHelper.LogoutRedirectRelativeUri, },
OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
}
And in Start.Auth.cs
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
ExpireTimeSpan = TimeSpan.FromHours(1),
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
ClientId = SettingsHelper.ClientId,
Authority = SettingsHelper.Authority,
TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
// If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.
AuthorizationCodeReceived = (context) =>
{
var code = context.Code;
ClientCredential credential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.AppKey);
string tenantID = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string signInUserId = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
AuthenticationContext authContext = new AuthenticationContext(string.Format("{0}/{1}", SettingsHelper.AuthorizationUri, tenantID), new ADALTokenCache(signInUserId));
AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, SettingsHelper.AADGraphResourceId);
return Task.FromResult(0);
},
RedirectToIdentityProvider = (context) =>
{
string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
context.ProtocolMessage.RedirectUri = appBaseUrl + SettingsHelper.LoginRedirectRelativeUri;
context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl + SettingsHelper.LogoutRedirectRelativeUri;
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
context.HandleResponse();
return Task.FromResult(0);
}
}
});
}

Resources