I am trying to use IdentityServer4 with resource owner flow + aspnet identity and embed the api in the same project.
I tested the Sample here on github and it's working fine. I am able to retrieve a token for a registered user in the database and use this token to get protected resources from the api.
The sample the api is separated from the identity server, once both are merged into one project, im still able to get a token, BUT I get 401 Unauthorized while trying to access the protected resource. somehow the embedded api is no longer validating the token.
here's the Startup.cs code :
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
//(1)
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryPersistedGrants()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
//(2)
.AddAspNetIdentity<ApplicationUser>();
//.AddTestUsers(Config.GetUsers());
var corsBuilder = new CorsPolicyBuilder();
corsBuilder.AllowAnyHeader();
corsBuilder.AllowAnyMethod();
corsBuilder.AllowAnyOrigin();
corsBuilder.AllowCredentials();
corsBuilder.WithExposedHeaders("Location");
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", corsBuilder.Build());
});
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:51318";
options.RequireHttpsMetadata = false;
options.ApiName = "api";
});
}
// 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();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseCors("CorsPolicy");
app.UseIdentityServer();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Note that if we swith to in memory TestUser instead of persisted ApplicationUser by commenting the code in (1) and changing the code in (2) to :
//(2)
//.AddAspNetIdentity<ApplicationUser>();
.AddTestUsers(Config.GetUsers());
the whole system works and the embedded api is authenticating the user normally.
Is there something missing in this code ? In real life scenarios the api will almost always be embedded with the identity server because of cost efficiency, is there any example I can use to make it work ?
Thank you.
After digging into AspNet Identity source code, I realized that the AddIdentity extension was doing some extra work that prevents from validating the token, but without it and the AddEntityFrameworkStores method the identity managers were not set by dependency injection.
So we need to replace :
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
by a piece of code that does only dependency injection like that :
services.TryAddScoped<IUserValidator<ApplicationUser>, UserValidator<ApplicationUser>>();
services.TryAddScoped<IPasswordValidator<ApplicationUser>, PasswordValidator<ApplicationUser>>();
services.TryAddScoped<IPasswordHasher<ApplicationUser>, PasswordHasher<ApplicationUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<IdentityRole>, RoleValidator<IdentityRole>>();
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<ApplicationUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>>();
services.TryAddScoped<UserManager<ApplicationUser>, AspNetUserManager<ApplicationUser>>();
services.TryAddScoped<SignInManager<ApplicationUser>, SignInManager<ApplicationUser>>();
services.TryAddScoped<RoleManager<IdentityRole>, AspNetRoleManager<IdentityRole>>();
services.TryAddScoped<IRoleStore<IdentityRole>, RoleStore<IdentityRole>>();
services.TryAddScoped<DbContext, ApplicationDbContext>();
services.TryAddScoped<IUserStore<ApplicationUser>, UserStore<ApplicationUser>>();
by doing this, the final result is a working identity server embedded in the api with AspNet Identity.
Related
I'm stuck with deploying a SPA (ASP.NET Core + identity + ReactJS, all built from 'ASP.NET Core With React.js' template in VS) to production. It works fine in the development environment, but when deploying to prod (non-Azure), the API starts to return 401s.
I went through Authentication and authorization for SPAs article, it says the following:
To deploy the app to production, the following resources need to be provisioned:
...
A production certificate to use for signing tokens.
I have a link to the .pfx certificate on the hosting, but I'm a bit lost on how to implement it (and whether I actually need it?). Most samples I see assume running from the local/development machine (i.e. generate a self-signed certificate and load it from the file, etc).
Below is the code (skipping some irrelevant service configuration). I'm looking for a link to a resource that explains the changes needed to be done to deploy this to production (or a code sample).
Code:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddControllersWithViews();
services.AddRazorPages();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
// 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.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
You need to provide a signing-key to IdentityServer, so that the keys issued by it are signed by the same key each time, even across re-deployments.
The answers to this question will show you how to add it to IdentitySever.
See also this link that provides this piece of code to add the key:
services.AddIdentityServer()
// .AddTemporarySigningCredential() // Can be used for testing until a real cert is available
.AddSigningCredential(new X509Certificate2(Path.Combine(".", "certs", "IdentityServer4Auth.pfx")))
I have several .NET core API and I use IdentityServer 4 as a seperate service for authentication.
The problem is that in "debug" I also wish to run my API without authentication (without launching the IdentityServer).
So, I try to bypass it... I have try several solutions, but none work:
- With a AuthorizationHandler: Bypass Authorize Attribute in .Net Core for Release Version
- With a Middleware : Simple token based authentication/authorization in asp.net core for Mongodb datastore
- With a filter : ASP.NET Core with optional authentication/authorization
- With AllowAnonymousFilter : Bypass Authorize Attribute in .Net Core for Release Version
But no way, none of theses solutions work, I still got a "401 Undocumented Error: Unauthorized" !
Here is some parts of my code:
public void ConfigureServices(IServiceCollection services)
{
// JSON - setup serialization
services.AddControllers().
AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(new TargetSpot.Core.Json.SnakeCaseNamingStrategy()));
options.JsonSerializerOptions.IgnoreNullValues = true;
});
// Force lowercase naming
services.AddRouting(options => options.LowercaseUrls = true);
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Setup the connection to the IdentityServer to request a token to access our API
services.AddAuthentication(IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetSection("APISettings")["AuthorityURL"];
options.RequireHttpsMetadata = false;
options.ApiName = Configuration.GetSection("APISettings")["APIName"];
});
// Add swagger
services.AddSwaggerGen(options =>
{
//options.DescribeAllEnumsAsStrings();
options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
Title = "HTTP API",
Version = "v1",
Description = "The Service HTTP API",
TermsOfService = new Uri("http://www.myurl.com/tos")
});
// XML Documentation
var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = System.IO.Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSwagger().UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Winamp API v1");
});
}
I had similar problem.
AllowAnonymousFilter works in ASP.NET Core 2.2 but not in ASP.NET Core 3.x.
After day of investigation I have found out that switching from UseEndpoints to UseMvc solved it and I can now disable authentication without commenting out [Authorize] attributes.
It seems that UseEndpoints does not use filter when registered by AddMvc but how to correctly register it when using UseEndpoints I do not know.
My solution
Startup.ConfigureServices:
services.AddMvc(o =>
{
o.EnableEndpointRouting = false;
o.Filters.Add(new AllowAnonymousFilter());
});
Startup.Configure:
// anonymous filter works with UseMvc but not with UseEndpoints
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
//app.UseEndpoints(endpoints =>
//{
// endpoints.MapControllerRoute(
// name: "default",
// pattern: "{controller=Home}/{action=Index}/{id?}");
//});
I found the solution in this link: https://docs.identityserver.io/_/downloads/en/latest/pdf/. Obviously I had to remove the Authorize attributes I added manually in my controllers.
app.UseEndpoints(endpoints =>
{
// Allowing Anonymous access to all controllers but only in Local environment
if (env.IsEnvironment(Constants.ApplicationConstants.LocalEnvironment))
endpoints.MapControllers();
else
endpoints.MapControllers().RequireAuthorization();
});
I have an ASP.NET Core Web App which successfully uses Azure AD Authentication. I would like to run a process immediately after a user logs in. I thought I might somehow handle the Redirect URI specified in the Azure app registration but I couldn't figure it out as much of the login process is nicely handled by the .AddAzureAd() method in my Startup.cs.
Can anyone suggest an easy way to call a method or redirect to a razor page after authentication? Preferably something which would not be circumvented by specifying a returnUrl in the initial request.
Update
Between posting the question and seeing the answers I found what might be considered a hack:
Basically I created a service and injected it into my _LoginPartial.cshtml page and then call a method on the service.
...
#inject MyService myService
...
#if (User.Identity.IsAuthenticated)
{
await MyService.MyCustomMethod();
...
}
For running code or changing the redirect url, you could configure OpenIdConnectOptions.
services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
.AddAzureAD(options => Configuration.Bind("AzureAd", options));
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = ctx =>
{
ctx.Properties.RedirectUri = "/Home/Privacy";
return Task.CompletedTask;
},
};
});
If you want to run code after authentication, you could place your code in the OnTokenValidated.
If you want to change the uri, you could replace /Home/Privacy.
You can define the route in Startup.cs file. I used the sample here.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();
services.AddMvc();
}
// 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.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Contact}/{id?}");
});
}
I've setup IdentityServer4, a protected API (Core) project, and various clients. Anonymously accessed client pages use the Resource Owner flow to access the APIs and user credentials are used from client pages where login is required. This is all working. My problem is now I want to add registration API methods that are protected.
The new registration methods require the API project to use AspNetIdentity. Specifically they use Identity's UserManager object which is failing to instantiate unless I add this code to my Startup.ConfigureServices:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
But adding this code breaks the normal IDServer4 Bearer authentication. The Authorize tag on the API controller now sends the requesters to the login page. Is there a way to create a good userManager component without the chunk of code above so Identity authentication does not come into play?
Without the code above I get the following error:
Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UserManager`1[TestAPICore.Models.ApplicationUser]' while attempting to activate *controller*
Here is my ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
services.AddCors(o => o.AddPolicy("MyPolicy", builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
//// ADDING THIS CAUSES API requests to send back the login screen
//services.AddIdentity<ApplicationUser, IdentityRole>()
// .AddEntityFrameworkStores<ApplicationDbContext>()
// .AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
}
Ideas for making this work?
Update:
Reading more, it looks like calling services.AddIdentityCore<ApplicationUser>(cfg => {}); is the way to go. I've tried it both before and after the .AddAuthentication code but I still get nearly the same error:
System.InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.IUserStore`1[TestAPICore.Models.ApplicationUser]' while attempting to activate 'Microsoft.AspNetCore.Identity.UserManager`1[TestAPICore.Models.ApplicationUser]'.
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateArgumentCallSites...
...which is slightly different since it no longer references my controller.
Solved!
Here's what worked...
IdentityBuilder builder = services.AddIdentityCore<ApplicationUser>(options => { });
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>();
Thanks,Kirk, for pointing me in the right direction!
So that others may benefit, here's my whole ConfigureServices method for my API project which can manage users but still authenticates against IdentityServer:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
services.AddCors(o => o.AddPolicy("MyPolicy", bld =>
{
bld.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
// *** The Fix ***
IdentityBuilder builder = services.AddIdentityCore<ApplicationUser>(options => { });
builder = new IdentityBuilder(builder.UserType, typeof(IdentityRole), builder.Services);
builder.AddEntityFrameworkStores<ApplicationDbContext>();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
}
I'm setting up social login in an ASP.NET Core 2.0 application without using Identity.
I simply want to authenticate the user through Facebook, Google and LinkedIn and receive their info. I handle storing user info myself.
Here's what I've done so far which is giving me the following error:
No authentication handler is configured to handle the scheme: facebook
Here's the Startup.cs file changes:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
// Added these lines for cookie and Facebook authentication
services.AddAuthentication("MyCookieAuthenticationScheme")
.AddCookie(options => {
options.AccessDeniedPath = "/Account/Forbidden/";
options.LoginPath = "/Account/Login/";
})
.AddFacebook(facebookOptions =>
{
facebookOptions.AppId = "1234567890";
facebookOptions.AppSecret = "1234567890";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
// Added this line
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
I then have this action method where I send the user to determine the provider we're using for authenticaiton e.g. Facebook, Google, etc. This code came from my ASP.NET Core 1.1 app which is working fine.
[AllowAnonymous]
public async Task ExternalLogin(string provider, string returnUrl)
{
var properties = new AuthenticationProperties
{
RedirectUri = "Login/Callback"
};
// Add returnUrl to properties -- if applicable
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
properties.Items.Add("returnUrl", returnUrl);
// The ASP.NET Core 1.1 version of this line was
// await HttpContext.Authentication.ChallengeAsync(provider, properties);
await HttpContext.ChallengeAsync(provider, properties);
return;
}
I'm getting the error message when I hit the ChallangeAsync line.
What am I doing wrong?
No authentication handler is configured to handle the scheme: facebook
Scheme names are case-sensitive. Use provider=Facebook instead of provider=facebook and it should work.