Identity Server 4 JWT "access_token" is not valid for Identity Server Authorized endpoints - jwt

I have an Identity Server & API apps with ResourceOwnerPasswordAndClientCredentials flow.
After getting "access_token" from RequestResourceOwnerPasswordAsync method I can use my [Authorized] endpoints in my API project, but can't access [Authorized] endpoints in my Identity Server app.
May be I'm missing something in AllowedScopes?
Identity Server's Starup.cs:
public class Startup
{
public Startup(IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
builder.AddEnvironmentVariables();
Configuration = builder.Build();
var section = Configuration.GetSection("Logging");
loggerFactory.AddConsole(section);
loggerFactory.AddDebug();
loggerFactory.AddProvider(new FileLoggerProvider());
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<IdentityDataContext>(optionsAction =>
optionsAction.UseSqlServer(Configuration.GetConnectionString("DevelopersConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<IdentityDataContext>()
.AddDefaultTokenProviders();
services.AddMvc();
services.AddIdentityServer()
.AddDeveloperSigningCredential(filename: "tempkey.rsa")
.AddInMemoryClients(IdentityConfig.GetClients("http://localhost:7017"))
.AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources())
.AddInMemoryApiResources(IdentityConfig.GetApiResources())
.AddAspNetIdentity<ApplicationUser>();
services.AddCors();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
RolesData.SeedRoles(app).Wait();
app.UseCors(x =>
x.WithOrigins("http://localhost:7017")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
app.UseStaticFiles();
app.UseIdentityServer();
app.UseMvc();
}
}
Web Api Startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient(typeof(IDataRepository<>), typeof(DataRepository<>));
services.AddDbContext<DataContext>(optionsAction =>
optionsAction.UseSqlServer(Configuration.GetConnectionString("DevelopersConnection")));
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(
JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:7777"; // Auth Server
options.RequireHttpsMetadata = false; // only for development
options.ApiName = "api"; // API Resource Id
options.SupportedTokens = SupportedTokens.Jwt;
});
services.AddCors();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors(x =>
x.WithOrigins("http://localhost:7017")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
);
app.UseAuthentication();
app.UseMvc();
}
}
IdentityConfig.cs:
public class IdentityConfig
{
public static IEnumerable<Client> GetClients(string hostname) => new List<Client>
{
new Client
{
ClientId = "client",
ClientName = "application",
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AccessTokenType = AccessTokenType.Jwt,
AllowAccessTokensViaBrowser = true,
ClientSecrets = {new Secret("secret".Sha256())},
RequireConsent = false,
RedirectUris = { $"{hostname}/callback.html" },
PostLogoutRedirectUris = { $"{hostname}/index.html" },
AllowedCorsOrigins = { hostname },
AlwaysIncludeUserClaimsInIdToken = true,
AllowOfflineAccess = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.OfflineAccess,
JwtClaimTypes.Role,
"api"
}
},
};
public static IEnumerable<IdentityResource> GetIdentityResources() => new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Email(),
new IdentityResources.Profile(),
new IdentityResource("role", new []{ JwtClaimTypes.Role })
};
public static IEnumerable<ApiResource> GetApiResources() => new List<ApiResource>
{
new ApiResource("api")
{
UserClaims =
{
JwtClaimTypes.Email,
JwtClaimTypes.Role,
JwtClaimTypes.IdentityProvider,
IdentityServerConstants.StandardScopes.OpenId
}
}
};
}
Getting Jwt access_token:
var disco = await DiscoveryClient.GetAsync("http://localhost:7777");
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret");
var tokenResponse =
await tokenClient.RequestResourceOwnerPasswordAsync("username", "password", "openid api");
var access_token = tokenResponse.AccessToken;

Related

Authorization in .Net Framework 4.8 returns Unauthorized using OpenIdentity4

I'm trying to use Authorization in .Net Framework 4.8, but making a get request returns Unauthorized using OpenIdentity4
I have to use Framework and not Core!
This is my Startup.cs:
public void Configuration(IAppBuilder app)
{
var authority = "https://localhost:5001";
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
authority + "/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever());
var discoveryDocument = Task.Run(() => configurationManager.GetConfigurationAsync()).GetAwaiter().GetResult();
System.Console.WriteLine(discoveryDocument.AuthorizationEndpoint);
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = false,
}
}) ;
var config = new HttpConfiguration();
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "RestAPI",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.Remove(config.Formatters.XmlFormatter);
config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc;
app.UseWebApi(config);
}
}
}
OpenIdentity4 is running on https://localhost:5001
It's Startup:
public class Startup
{
public IWebHostEnvironment Environment { get; }
public Startup(IWebHostEnvironment environment)
{
Environment = environment;
}
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
services.AddControllers();
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy => {
policy.RequireAuthenticatedUser();
policy.RequireClaim("RestAPI", "APIRest");
});
});
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// uncomment if you want to add MVC
//app.UseStaticFiles();
//app.UseRouting();
app.UseIdentityServer();
app.UseRouting();
// uncomment, if you want to add MVC
app.UseAuthorization();
//app.UseEndpoints(endpoints =>
//{
// endpoints.MapDefaultControllerRoute();
//});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("ApiScope");
});
}
}
}
It's probably a stupid mistake, but I can't figure it out.

Change EF connection string when user logs in with Identity

My question is about extending this previous post using identity to calculate the connection string for each user: ASP.NET Core change EF connection string when user logs in
I tried the following approach :
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
var c = new SqlConnectionStringBuilder
{
-- the connection string to the users repository --
};
services.AddDbContextFactory<MasterDBContext>(options =>
options.UseSqlServer(c.ConnectionString));
services.AddScoped<MasterDBContext>(p => p.GetRequiredService<IDbContextFactory<MasterDBContext>>().CreateDbContext());
services.AddDefaultIdentity<MyUser>(options =>
options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<MasterDBContext>();
services.AddTransient<IMasterUserService, MasterUserService>();
services.AddDbContextFactory<UserDbContext>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
UserDbContext:
public MyContext(IServiceProvider provider)
{
_provider = provider;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var haccess = (IHttpContextAccessor)_provider.GetService(typeof(IHttpContextAccessor));
var scopefactory = haccess.HttpContext.RequestServices.GetService<IServiceScopeFactory>();
using (var scope = scopefactory.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<MyUser>>();
var user = userManager.GetUserAsync(haccess.HttpContext.User).Result;
var userServ = scope.ServiceProvider.GetRequiredService<IMasterUserService>();
optionsBuilder.UseSqlServer(userServ.GetConnectionString(user).Result);
}
base.OnConfiguring(optionsBuilder);
}
But, even in a scope, no way to get access to UserManager service (usermanager injection works fine from others services and controllers). I get an "invalid operation exception" at the usermanager connection point.
What is wrong with that code ?
Thanks in advance
I found the solution at the end... My code in MyContext.OnConfiguring is correct if you add services.TryAddScoped<UserManager>(); in the ConfigureServices function of statup.cs.
All together, I'm able to get a connection string depending of the current user from any service :
public class MyService : IMyService
{
private IDbContextFactory _dbfactory;
public MyService(IDbContextFactory<MyContext> dbfactory)
{
_dbfactory = dbfactory;
}
public async Task AnyAsync()
{
using (var dbtf = _dbfactory.CreateDbContext())
{
... your code ...
}
}
}

How to Query Database From Startup.CS

I am doing user authentication in my startup.cs. I need to query my database using the OpenIDConnect claims info. This is what I have done but don't know how to get the connection to work. I tried injecting the db query constructor at the top of the startup.cs like this and then calling the query as follows:
public class Startup
{
protected IAdoSqlService _adoSqlService;
public Startup(IConfiguration configuration, IAdoSqlService adoSqlService)
{
Configuration = configuration;
_adoSqlService = adoSqlService;
}
public void ConfigureServices(IServiceCollection services)
{
// do ConfigureServices stuff
options.Events = new OpenIdConnectEvents()
{
OnTokenValidated = async ctx =>
{
// This is the ClaimsIdentity created by OpenID Connect, you can add claims to it directly
ClaimsIdentity claimsIdentity = ctx.Principal.Identities.FirstOrDefault();
string userntid = claimsIdentity.Claims.FirstOrDefault(c => c.Type == "preferred_username").Value;
//How do I call the database to run the following query
int isUser = _adoSqlService.isUser(userntid);
if (isUser > 0)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "user"));
}
else
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "not authorized"));
}
}
}
//More stuff
}
}
When I run the above, it errors in program.cs before even running with the following error
System.InvalidOperationException: 'Unable to resolve service for type 'XXXX.Services.IAdoSqlService' while attempting to activate 'XXXX.Startup'.'
So how do I make the call _adoSqlService.isUser(userntid); to the database?
I am NOT using EF.
Solution
I figured this out by doing the following:
I moved most of my services to the top of the ConfigureServices section (based on something that #qudus said) before I performed my authentication.
I removed the database injection code from the top of the startup.cs.
Lastly I changed the OnTokenValidated to use the following:
ctx.HttpContext.RequestServices.GetRequiredService();
Here is the code:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
internal static IConfiguration Configuration { get; private set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var connectionSection = Configuration.GetSection("ConnectionStrings");
services.Configure<ConnectionStrings>(connectionSection);
services.AddScoped<IAdoSqlService, AdoSqlService>();
services.AddControllersWithViews();
services.AddHttpContextAccessor();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSession();
// Load the Federation configuration section from app settings
var federationConfig = Configuration.GetSection("Federation");
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromHours(2);//default is 14days
options.SlidingExpiration = true;// default
options.AccessDeniedPath = "/Error/AuthenticateError";// set a custom error access denied error page. this would need to be created/handled in your app.
})
.AddOpenIdConnect(options =>
{
//Set Options here......
//optional customizations to the auth and failure events
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = context =>
{
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
// handle an error response from Federation and redirect the user to a custom error page instead
context.Response.Redirect("/Error/401");
context.HandleResponse();
return Task.CompletedTask;
},
OnTokenValidated = async ctx =>
{
// This is the ClaimsIdentity created by OpenID Connect, you can add claims to it directly
ClaimsIdentity claimsIdentity = ctx.Principal.Identities.FirstOrDefault();
string userntid = claimsIdentity.Claims.FirstOrDefault(c => c.Type == "preferred_username").Value;
string username = claimsIdentity.Claims.FirstOrDefault(c => c.Type == "name").Value;
int isUser = 0;
int isAdmin = 0;
try
{
var db = ctx.HttpContext.RequestServices.GetRequiredService<IAdoSqlService>();
isUser = db.isUser(userntid);
isAdmin = db.isAdmin(userntid);
}
catch (Exception ex)
{
string error = ex.Message;
}
AppHttpContext.Current.Session.SetString("IsUser", "false");
if (isUser > 0)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "user"));
AppHttpContext.Current.Session.SetString("IsUser", "true");
}
AppHttpContext.Current.Session.SetString("IsUserAdmin", "false");
if (isAdmin > 0)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "admin"));
AppHttpContext.Current.Session.SetString("IsUserAdmin", "true");
}
if (isUser == 0 && isAdmin == 0)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "not authorized"));
}
}
};
});
Solution
I figured this out by doing the following:
I moved most of my services to the top of the ConfigureServices section (based on something that #qudus said) before I performed my authentication.
I removed the database injection code from the top of the startup.cs.
Lastly I changed the OnTokenValidated to use the following:
ctx.HttpContext.RequestServices.GetRequiredService();
Here is the code:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
internal static IConfiguration Configuration { get; private set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
var connectionSection = Configuration.GetSection("ConnectionStrings");
services.Configure<ConnectionStrings>(connectionSection);
services.AddScoped<IAdoSqlService, AdoSqlService>();
services.AddControllersWithViews();
services.AddHttpContextAccessor();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSession();
// Load the Federation configuration section from app settings
var federationConfig = Configuration.GetSection("Federation");
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromHours(2);//default is 14days
options.SlidingExpiration = true;// default
options.AccessDeniedPath = "/Error/AuthenticateError";// set a custom error access denied error page. this would need to be created/handled in your app.
})
.AddOpenIdConnect(options =>
{
//Set Options here......
//optional customizations to the auth and failure events
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = context =>
{
return Task.CompletedTask;
},
OnRemoteFailure = context =>
{
// handle an error response from Federation and redirect the user to a custom error page instead
context.Response.Redirect("/Error/401");
context.HandleResponse();
return Task.CompletedTask;
},
OnTokenValidated = async ctx =>
{
// This is the ClaimsIdentity created by OpenID Connect, you can add claims to it directly
ClaimsIdentity claimsIdentity = ctx.Principal.Identities.FirstOrDefault();
string userntid = claimsIdentity.Claims.FirstOrDefault(c => c.Type == "preferred_username").Value;
string username = claimsIdentity.Claims.FirstOrDefault(c => c.Type == "name").Value;
int isUser = 0;
int isAdmin = 0;
try
{
var db = ctx.HttpContext.RequestServices.GetRequiredService<IAdoSqlService>();
isUser = db.isUser(userntid);
isAdmin = db.isAdmin(userntid);
}
catch (Exception ex)
{
string error = ex.Message;
}
AppHttpContext.Current.Session.SetString("IsUser", "false");
if (isUser > 0)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "user"));
AppHttpContext.Current.Session.SetString("IsUser", "true");
}
AppHttpContext.Current.Session.SetString("IsUserAdmin", "false");
if (isAdmin > 0)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "admin"));
AppHttpContext.Current.Session.SetString("IsUserAdmin", "true");
}
if (isUser == 0 && isAdmin == 0)
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "not authorized"));
}
}
};
});

EntityFrameworkCore Task.WhenAll() A second operation started on this context before a previous operation completed

I want to read data from database. For this I create a query and queryhandler classes
QueryHandler
public class OrderGetQueryHandler: IQueryHandler<OrderGetQuery, OrderDTO>
{
private readonly GoodWillWebDbContext _context;
private readonly IQueryDispatcher _queryDispatcher;
public OrderGetQueryHandler(GoodWillWebDbContext context, IQueryDispatcher queryDispatcher)
{
_context = context;
_queryDispatcher = queryDispatcher;
}
private bool CheckPartnerBlock(BlockTypes blockType, decimal debtOverdue, bool payOff)
{
if (blockType == BlockTypes.Block)
return true;
if (blockType == BlockTypes.NotBlock)
return false;
if (blockType == BlockTypes.PreliminaryPayment)
return payOff;
return debtOverdue <= 0;
}
public async Task<OrderDTO> HandleAsync(OrderGetQuery query)
{
var order = await _context.Orders.FindAsync(query.OrderID);
if (order != null)
{
var getCustomerTask = _context.Partners.FindAsync(order.CustomerID).AsTask();
var getCuratorTask = _context.Users.FindAsync(order.CuratorID).AsTask();
var getPaymentTask = _context.Payments.OrderByDescending(x => x.PaymentID).FirstOrDefaultAsync(x => x.CustomerID == order.CustomerID);
var getOrderLinesTask =
_queryDispatcher.HandleAsync<OrderLinesGetQuery, OrderLineDTO[]>(
new OrderLinesGetQuery(query.OrderID));
await Task.WhenAll(getCustomerTask, getCuratorTask, getOrderLinesTask, getPaymentTask);
var priceRange = await _context.PriceRanges.FindAsync(getCustomerTask.Result.PriceRangeID);
return new OrderDTO
(
order.OrderID,
getCustomerTask.Result.Name,
getOrderLinesTask.Result,
order.CustomerID,
order.OrderStateID,
order.CanDelete,
order.CreationDate,
getPaymentTask.Result.DebtBank,
getPaymentTask.Result.DebtOverdue,
this.CheckPartnerBlock(getCustomerTask.Result.BlockTypeID, getPaymentTask.Result.DebtOverdue, order.PayOff),
priceRange.Name,
order.ReservationDate,
Mapper.Convert<DeliveryInfoDTO, BaseEntities.Entities.Sales.Order>(order)
);
}
throw new NullReferenceException();
}
}
this queryhandler i use in ASP.NET WEB Application. My startup class is
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
string connection = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<GoodWillWebDbContext>(options =>
options.UseSqlServer(connection), ServiceLifetime.Transient);
services.AddScoped<IQueryHandler<OrdersGetQuery, BaseEntities.DTO.Sales.Order.OrderDTO[]>, OrdersGetQueryHandler>();
services.AddScoped<IQueryHandler<OrderGetQuery, Sales.Queries.DTO.Order.OrderDTO>, OrderGetQueryHandler>();
services.AddScoped<ICommandDispatcher, CommandDispatcher>();
services.AddScoped<IQueryDispatcher, QueryDispatcher>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I set ServiceLifetime.Transient for my context, but I still get an exception: InvalidOperationException A second operation started on this context before a previous operation completed.
What's wrong?
It seems you're running multiple operations on the context without waiting for the previous ones to end, which EF doesn't like:
var getCustomerTask = _context.Partners.FindAsync(order.CustomerID).AsTask();
var getCuratorTask = _context.Users.FindAsync(order.CuratorID).AsTask();
var getPaymentTask = _context.Payments.OrderByDescending(x => x.PaymentID).FirstOrDefaultAsync(x => x.CustomerID == order.CustomerID);
Either make these call sync or use the await keyword.

WSFederation ADFS login loop .Net Core 2.1

While logging into ADFS(on Premise), I am being redirected after I enter in my credentials and eventually it erros out with the error Exception details: Microsoft.IdentityServer.Web.InvalidRequestException: MSIS7042: The same client browser session has made '6' requests in the last '7' seconds. Contact your administrator for details in Event Viewer. I followed couple of stack overflow suggestion(link) but couldn't resolve the issue. I am running on https and made sure the certificate are right.
Here is my code
namespace TestApp
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
private static string HtmlEncode(string content) =>
string.IsNullOrEmpty(content) ? string.Empty : HtmlEncoder.Default.Encode(content);
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.Secure = CookieSecurePolicy.SameAsRequest;
});
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.Cookie.Name = ".AspNet.SharedCookie";
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.None;
})
.AddWsFederation(options =>
{
options.MetadataAddress =
$"https://adfsdomain/FederationMetadata/2007-06/FederationMetadata.xml";
options.Wtrealm = "urn:apptest";
options.Wreply = "https://appdomain/apptest";
options.CallbackPath = "/apptest";
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://adfsdomain/adfs/services/trust/"
};
options.SaveTokens = true;
options.RequireHttpsMetadata = false;
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddMvc();//.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseCors(policy => policy.SetIsOriginAllowed(origin => origin == "https://adfsdomain"));
app.UseHttpsRedirection();
app.UseStaticFiles();
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync(WsFederationDefaults.AuthenticationScheme);
}
else
{
await next();
}
});
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
I am using .Net Core 2.1 and ADFS 3.0. If I take ASP.NET MVC app and publish it with the same adfs setting my code works like charm, which tells me that the configuration on ADFS has been configured correctly.
Not sure if this can help you guys but I got my ADFS to working with the following:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = WsFederationDefaults.AuthenticationScheme;
})
.AddWsFederation(options =>
{
options.Wtrealm = this._Configuration["wsfed:realm"];
options.MetadataAddress = string.Format("{0}/federationmetadata/2007-06/federationmetadata.xml", this._Configuration["wsfed:metadata"]);
})
.AddCookie();
You need app.UseAuthorization() before the app.Use(async ...). That's the middleware that takes the headers from the redirect and populates the context.User.