I have Three application viz(IdentityServer4 App, .Net Core2.0 WebApp, .NetCore2.0 WebAPI)
When I open the webapp if its un-authenticated, It gets navigated to identity server where I supply the credentials. After successful authentication it navigates back to webapp with the required cookies in place. Things are fine till here.
Now within webapp I am making calls to webapi (with cookies set by identity server in webapp) but each time it returns as 401 unauthorized.
Code sample in webapp:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
{
o.Cookie.Name = Config.CookieName;
o.Cookie.SameSite = SameSiteMode.None;
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Config.IdentityUrl;
options.RequireHttpsMetadata = false;
options.ClientId = Config.ClientId;
options.SaveTokens = true;
});
And Code sample used in WebAPI in configure service method ConfigureServices:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o => {
o.Cookie.Name = Config.CookieName;
o.Cookie.SameSite = SameSiteMode.None;
o.Events = new CookieAuthenticationEvents()
{
OnRedirectToLogin = redirectContext =>
{
redirectContext.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
}
};
})
.AddIdentityServerAuthentication(options =>
{
options.Authority = Config.IdentityUrl;
options.RequireHttpsMetadata = false;
options.ApiName = Config.ApiName;
});
also I have app.UseAuthentication() method in Configure method
What I get a feeling of it has to do with something session-id may be. If so it the case please help if not then what you could make out as not doing right please help.
I traced log it shows just following thing in there:
Cookie was not authenticated. Failure Message: Unprotect ticket failed.
Authentication Cookie was chanllenged.
Any help would be appreciated.
Here is the magical line of code.Added in
ConfigureServices
method before
services.AddAuthentication
This was reason because of which cookie was not getting validated.
services.AddDataProtection().PersistKeysToFileSystem(PersistKeysLocation.GetKeyRingDirInfo())
.SetApplicationName(Config.ApplicationName);
Related
Here is what I did: using my local keycloak server (thru docker), I created a realm, users, role and client with this setup :
I set up credentials and got secret key and stuff and that's it, I haven't set anything, no mappers, client scope, etc.
I did this as our other applications that is using other languages such as PHP or nodejs have similar settings.
services.AddAuthentication(options =>
{
//Sets cookie authentication scheme
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(cookie =>
{
//Sets the cookie name and maxage, so the cookie is invalidated.
cookie.Cookie.Name = "keycloak.cookie";
cookie.Cookie.MaxAge = TimeSpan.FromMinutes(60);
cookie.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
cookie.SlidingExpiration = true;
})
.AddOpenIdConnect(options =>
{
//Use default signin scheme
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
//Keycloak server
options.Authority = Configuration.GetSection("Keycloak")["ServerRealm"];
//Keycloak client ID
options.ClientId = Configuration.GetSection("Keycloak")["ClientId"];
//Keycloak client secret
options.ClientSecret = Configuration.GetSection("Keycloak")["ClientSecret"];
//Keycloak .wellknown config origin to fetch config
// options.MetadataAddress = Configuration.GetSection("Keycloak")["Metadata"];
//Require keycloak to use SSL
options.RequireHttpsMetadata = false;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
//Save the token
options.SaveTokens = true;
//Token response type, will sometimes need to be changed to IdToken, depending on config.
options.ResponseType = OpenIdConnectResponseType.Code;
//SameSite is needed for Chrome/Firefox, as they will give http error 500 back, if not set to unspecified.
options.NonceCookie.SameSite = SameSiteMode.None;
options.CorrelationCookie.SameSite = SameSiteMode.None;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "https://schemas.scopic.com/roles"
};
Configuration.Bind("<Json Config Filter>", options);
options.Events.OnRedirectToIdentityProvider = async context =>
{
context.ProtocolMessage.RedirectUri = "http://localhost:13636/home";
await Task.FromResult(0);
};
});
Then I created a fresh ASP.NET Core MVC application and setup the OpenId options like so
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
My HomeController looks like this:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
public IActionResult Index()
{
bool value = User.Identity.IsAuthenticated;
return View();
}
[Authorize]
public IActionResult Privacy()
{
return View();
}
}
When I access localhost:13636/Privacy to test, the Keycloak login page is triggered which is correct, but after successful login and a redirect to /home, User.Identity.IsAuthenticated is false and it seems like the application doesn't know that authentication has been successful.
What needs to be done after this?
Or am I missing some configuration/settings/options?
Summary of what I did
Setup keycloak dashboard (created Realm, client, user and roles)
Setup a simple ASP.NET Core MVC application, pass openid options and controller.
The keycloak login page is triggered but authentication fail during test
Try to add to the AddCookie handler the following setting:
options.Cookie.SameSite = SameSiteMode.None;
To make sure the cookies are set with SameSite=none.
So I'm adding JWT authentication for a webapi. Everything works fine until I messed around with the scheme settings. Somehow the following settings dont work any more
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
//TODO
var userMachine = context.HttpContext.RequestServices.GetRequiredService<UserManager<User>>();
var user = userMachine.GetUserAsync(context.HttpContext.User);
if (user == null)
context.Fail("Unauthorized");
return Task.CompletedTask;
}
};
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
The problem is this scheme line of code
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
when I try to call a api method it somehow returns 404 instead of 401
But if I overwrite this scheme in the controller like this
[Route("api/[controller]")]
[ApiController]
[Authorize(AuthenticationSchemes = AuthSchemes)]
public class ValuesController : ControllerBase
{
private const string AuthSchemes = JwtBearerDefaults.AuthenticationScheme;
Wether I commented the previous scheme line of code or not the program runs.
And if I call a api with out the JWT is returns 401 now and everything works.
Anybody knows why? Thank you!
PS. I didn't add an identity. Instead I added identity manually from scratch. I noticed that the problem happens after I create a new custom user inheriting from IdentityUser and add an migration. So basically it happens after I add these code below:
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireNonAlphanumeric = false;
}).AddEntityFrameworkStores<ApplicationContext>();
in program.cs
and
public class ApplicationContext : IdentityDbContext<ApplicationUser>
{
public ApplicationContext(DbContextOptions<ApplicationContext> options):base(options)
{
}
}
in Dbcontext.
Seems like for it to work I have to use the built-in identity. But the question is why it doesnt work if I do it manually?
Good morning,
I need to have in same project both web api and web app mvc.
Web api has to be protected via bearer token and web app mvc has to be authenticated via identity server.
Is it possible protecting a scope and a client in same project?
I think I have to do something like this in startup
//this to protect scope api1
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000/";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
//this to authenticate mvc client
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies", options =>
{
options.AccessDeniedPath = "/account/denied";
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000",
options.RequireHttpsMetadata = false;
options.ResponseType = "id_token token";
options.ClientId = "mvc-implicit";
options.SaveTokens = true;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("api1");
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("role", "role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
Now, I have to call my Api1 using client_credential with an external client.
But it returns me at login page.
Is it possible to do what I want?
Protected WebApi and Authenticated MVC client in same project?
Now, I have to call my Api1 using client_credential with an external client. But it returns me at login page.
That seems you misunderstand the scenario . Your MVC application is client also is a resource application which protected by Identity Server (in Config.cs):
public static IEnumerable<ApiResource> GetApis()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
I assume you have api controller in your MVC application :
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET: api/Values
[HttpGet]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
And you have config to protect the api actions by using AddJwtBearer :
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000/";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
That means any request to access the Get action should have an authentication bearer header with access token append , the access token is issued by your Identity Server(endpoint is http://localhost:5000/) and the audience is api1 .
Now your another client could use client credential flow to acquire access token to access your web application :
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
And call your protected actions :
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:64146/api/values");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
So it won't redirect to login page , since client credential in fact is sending HTTP POST request to get access token with app's credential . There is no login page in this scenario .
I'm trying to create an ASP.Net Core app which contains both MVC and API controllers in single project. For authenticating I use IdentityServer4.
Currently when the user is not authorized for a request he is always redirected to Account/AccessDenied path regardless of authentication scheme. But I want to keep this behavior only for MVC controllers. For API requests I just want to return 403 status code.
Configuration:
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(ApiResourceProvider.GetAllResources())
.AddAspNetIdentity<ApplicationUser>()
.AddInMemoryClients(clientStore.AllClients);
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = tokenAuth.Issuer,
ValidateAudience = true,
ValidAudience = tokenAuth.Audience,
ValidateLifetime = true,
IssuerSigningKey = tokenAuth.SecurityKey,
ValidateIssuerSigningKey = true
};
});
How can I achieve that?
If you're using cookies you can override the AccessDeniedPath like the following
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
}).AddCookie("Cookies", (options) =>
{
options.AccessDeniedPath = "/Authorization/AccessDenied";
})
Actually it was quite simple but not obvious: it's needed to explicitly specify authentication scheme in [Authorize] attribute.
I tried to specify [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] on a controller level but it seems that setting [Authorize(Roles = RoleHelper.MobileWorker)] on the action level overrides the auth schema.
So I created a custom attribute which is derived from Authorize but with properly set auth scheme.
Note : After resolving the redirection issue i had an another issue that is getting an error "Cannot cast Newtonsoft.Json.Linq.JArray to Newtonsoft.Json.Linq.JToken". So in my answer I have provided the correct solution for both.
I have identity server project and the client project, everything works up to authentication without any issues and even it redirects to the correct client url but the url ex : "https://localhost:44309/signin-oidc" gives the blank page.
Note : SSl enabled for Identity Server and Client application.
It is authenticating the user as expected as you can see below in the screen shot.
My Identity server contains the following config values for client.
// OpenID Connect hybrid flow and client credentials client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { /*"http://localhost:5002/signin-oidc",*/"https://localhost:44309/signin-oidc" },
PostLogoutRedirectUris = { /*"http://localhost:5002/signout-callback-oidc",*/"https://localhost:44309/signout-callback-oidc" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
//"api1"
},
AllowOfflineAccess = true
}
The startup.cs is as follows.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// configure identity server with in-memory stores, keys, clients and scopes
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers());
services.AddAuthentication()
//.AddGoogle("Google", options =>
//{
// options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
// options.ClientId = "434483408261-55tc8n0cs4ff1fe21ea8df2o443v2iuc.apps.googleusercontent.com";
// options.ClientSecret = "3gcoTrEDPPJ0ukn_aYYT6PWo";
//})
.AddOpenIdConnect("oidc", "dataVail Login", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.SignOutScheme = IdentityServerConstants.SignoutScheme;
options.Authority = "https://login.microsoftonline.com/d0e2ebcc-0961-45b2-afae-b9ed6728ead7";//"https://demo.identityserver.io/";
options.ClientId = "f08cc131-72da-4831-b19d-e008024645e4";
options.UseTokenLifetime = true;
options.CallbackPath = "/signin-oidc";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.Use(async (context, next) =>
{
context.Request.Scheme = "https";
await next.Invoke();
});
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
Here is the startup.cs for my client app
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "https://localhost:44392/";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
//options.Scope.Add("api1");
options.Scope.Add("offline_access");
});
}
Can anyone please try helping me to sort this out.
I could resolved this with the help of Identity Server 4 folks.
If any one come across this problem here is the solution.
I missed adding "UseAuthentication" in Configure the client MVC pipeline. So after adding that i was redirected as expected and then I had another issue as shown below.
System.InvalidCastException: Cannot cast Newtonsoft.Json.Linq.JArray to Newtonsoft.Json.Linq.JToken. at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler1.d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.<Invoke>d__6.MoveNext() --- End of stack trace from previous location where exception was thrown --- at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.<Invoke>d__7.MoveNext()
I'm getting this exception while connecting my application to IdentityServer4 with AzureAD as external authentication provider. My application is using Hybrid flow to connect to IdentityServer4. I get properly redirected to Azure, login, and code and id_tokens are properly issued. This exception is raised in my application when userInfo endpoint is invoked.
In order resolve this I had to remove the claim which has the name twice.
I confirmed that AAD sends two name claims. Removing one of them resolved the problem.
var namesClaim = externalUser.FindFirst(ClaimTypes.Name) ??
throw new Exception("Unknown names");
if (namesClaim!=null)
{
claims.Remove(namesClaim);
}
Hope this may help someone.
I had the same problem with having multiple roles. Here is the solution for it:
.AddOpenIdConnect("oidc", options =>
{
// ...
options.Scope.Add("roles");
// ... using MapJsonKey instead of MapUniqueJsonKey for having 2 or more roles
options.ClaimActions.MapJsonKey(claimType: "role", jsonKey: "role");
});