Run a custom method immediately after Azure AD Authentication - asp.net-core

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?}");
});
}

Related

IdentityServer - How to bypass authorization for simple debug

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();
});

ASP Core 3 react template, HttpContext.User.IsAuthenticated() returns False after login

After working on my project for a while, I released the HttpContext.User.IsAuthenticated() returns False after login and I need to know where I should look for the mistake I made that cause this problem.
This is the Login, OnPost method.
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
var user = _userManager.Users.FirstOrDefault(u => u.StudentNumber == Input.StudentNumber.ToString());
if (!(user is null) && await _userManager.CheckPasswordAsync(user, Input.Password))
await _signInManager.SignInAsync(user, Input.RememberMe);
var isUserAuthenticated = HttpContext.User.IsAuthenticated();
return Redirect(returnUrl);
}
// If we got this far, something failed, redisplay form
return Page();
}
The ConfigureServices method.
public void ConfigureServices(IServiceCollection services)
{
services.AddAutoMapper();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(option=>option.Password.RequireNonAlphanumeric=false)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddMvc(options => options.EnableEndpointRouting = false)
.AddNewtonsoftJson();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
The Configure method.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
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.UseAuthentication();
app.UseIdentityServer();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
SignInManager.SignInAsync() only creates the cookie for the given user. This method would not set HttpContext.User.
But in the next request which has the cookie you can access HttpContext.User after AuthenticationMiddleware and HttpContext.User.IsAuthenticated() should be true.
AuthenticationMiddleware always try to authenticate user with the default scheme and since you have AddIdentityServer after AddDefaultIdentity, identity server is becoming your default scheme, but when you call SignInManager.SignInAsync the Identity scheme is triggered.
To sum up, with this configuration your AuthenticationMiddleware always tries to authenticate request for IdentityServer and if you want other scheme for you apis you should use [Authorize(AuthenticationSchemes = "Identity.Application")].
P.S. Identity.Application is authenticatio scheme for ASP.NET Identity

How to access Session in OpenIdConnect TokenValidated even handler

I have an ASP.NET Core 2.1 MVC application in which I have configured OpenIdConnect provider for authentication. The Startup class looks like below:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => false;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
// Set a short timeout for easy testing.
options.IdleTimeout = TimeSpan.FromSeconds(1200);
options.Cookie.HttpOnly = true;
});
services.AddHttpContextAccessor();
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IClientDataHandler, ClientDataHandler>();
services.AddAuthentication(options => .AddOpenIdConnect("oidc", options =>
{
...
options.Events.OnTokenValidated = async x =>
{
var serviceScopeFactory = services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>();
...
await x.HttpContext.Session.LoadAsync(new CancellationToken()); --does NOT work
x.HttpContext.Session.Set("clients", Utils.ObjectToByteArray(someData)); --does NOT work
};}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSession();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Though this lets me use HttpContext.Session (by injecting IHttpContextAccessor) in any controller or service, I can't use the Session in TokenValidated event handler. Any help?
Thanks in advance.
You should not be building the service provider in your event handler. This is not executed during startup. It's executed on each request by your authentication handler long after the service provider has been built.
options.Events.OnTokenValidated = async context =>
{
// don't do this...service provider is already built
var serviceScopeFactory = services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>();
};
Instead, you can access the built service provider from the HttpContext.RequestServices.
options.Events.OnTokenValidated = async context =>
{
var serviceScopeFactory = context.HttpContext.RequestServices.GetRequiredService<IServiceScopeFactory>();
};

Embedded IdentityServer 4 with Aspnet Identity and resource owner

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.

Setting Up Social Authentication in ASP.NET Core 2.0

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.