How can I create a new instance of GraphServiceClient when my BootstrapContext is null .net6 - c#

I'm currently trying to get a token from the bootstrap context in a test application that I have put together to help understand a separate issue. This is the first time that I have configured a .net 6 web application (generally I work in Angular) authentication pipeline so I'm not sure if I am missing something simple.
The code that I have configured in program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/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.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
When I access HttpContext.User.Identities.First() - I have access to a valid object and the user is shown as successfully authenticated. However, the BootstrapContext is always null...I did some digging and found that I might need to configure the auth service to save tokens, I tried that using the following alternative code but still no luck:
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(x =>
{
var configurationSection = builder.Configuration.GetSection("AzureAd");
x.ClientId = configurationSection["ClientId"];
x.TenantId = configurationSection["TenantId"];
x.ClientSecret = configurationSection["ClientSecret"];
x.Domain = configurationSection["Domain"];
x.CallbackPath = configurationSection["CallbackPath"];
x.Instance = configurationSection["Instance"];
x.SaveTokens = true;
});
Does anyone have any ideas as to how I can get the BootstrapContext populated?
Alternatively, given the fact that I have an authenticated user, is there a way to create a new instance of GraphServiceClient using the HttpContext.User object? - that is ultimately what I'm trying to achieve here.

Related

How do i add a web api to an existing asp.net mvc core 6 web application

I am trying to add a web API endpoint to an existing web application. How do I go about doing this?
Here is the GitHub link https://github.com/GrindingLife/BHFunctioning
I have tried adding a scaffold of an API controller in the controller folder. When launched accessing the localhost/api does not work. I do not know what to add to the Program.cs file. All the examples that I've seen are for startup.cs file instead of program.cs
Here is my program.cs file
using BHFunctioning.Controllers;
using BHFunctioning.Data;
using BHFunctioning.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole",
policy => policy.RequireRole("Administrator"));
});
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = false)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddRoles<IdentityRole>()
.AddDefaultUI()
.AddDefaultTokenProviders();
builder.Services.AddSingleton<IFileProvider>(
new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")));
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Home/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.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
app.Run();
I have solved it, i just needed to add the following into Program.cs and call it using localhost:2444/api/{controller name}
builder.Services.AddRazorPages();
builder.Services.AddControllers();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});

Having issues with IAntiforgery in ASP.NET Core 6.0

Please look at my long answer at the end about how I resolved this. I had gotten too frustrated and after another day with a fresh perspective and more sleep, I got to a solution.
I did this in 5.0 with no issues in the Startup.Configure method.
Basically I created a header for the request on a protected route. I'm using React as the front end. I'm finding when I place everything in Program.cs the dependency injection, authorization doesn't work right so I split up into separate Program and Startup files.
But I can't use the following signature in 6.0 like I did in 5.0:
example that worked in 5.0:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IAntiforgery antiforgery)
{
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("antiforgery/token", context =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Headers.Append("XYZ", tokens.RequestToken!);
return Task.FromResult(StatusCodes.Status200OK);
});
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
}
Program.cs (my attempt to split up program and startup - 6.0)
var startup = new dolpassword.Startup(builder.Configuration);
startup.ConfigureServices(builder.Services);
var app = builder.Build();
startup.Configure(app,app.Environment);
Saw this example on Microsoft website:
app.UseRouting();
app.UseAuthorization();
// app.Services syntax error in Configure for 6.0
var antiforgery = **app.Services.GetRequiredService<IAntiforgery>();**
app.Use((context, next) =>
{
var requestPath = context.Request.Path.Value;
if (string.Equals(requestPath, "/",
StringComparison.OrdinalIgnoreCase)
|| string.Equals(requestPath, "/index.html",
StringComparison.OrdinalIgnoreCase))
{
var tokenSet = antiforgery.GetAndStoreTokens(context);
context.Response.Cookies.Append("XSRF-TOKEN",
tokenSet.RequestToken!,
new CookieOptions { HttpOnly = false });
}
return next(context);
});
I was able to successfully do this in 6.0 so I will share some of the code and how I resolved it. I also had Windows authentication baked in with a policy-based authorization. The reason I'm putting all the authentication/authorization wireup in this post is because the entire solution relies on authentication, authorization and antiforgery.
First I set up my services. I get IAntiforgery by default by adding ControllersWithViews but I want to use my own header name, which is X-XSRF-TOKEN instead of the .AspNet.Antiforgery.xxxx or whatever the default is. I also needed options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme; to get Windows auth working.
string CorsPolicy = "CorsPolicy";
//===================================formerly Configure Services
WebApplicationBuilder? builder = WebApplication.CreateBuilder(args);
ConfigurationManager _configuration = builder.Configuration;
// Add services to the container.
**IServiceCollection? services = builder.Services;
services.AddAntiforgery(options => { options.HeaderName = "X-XSRF-TOKEN";
options.Cookie.HttpOnly = false; });**
services.AddTransient<IActiveDirectoryUserService, ActiveDirectoryUserService>();
services.AddControllersWithViews();
services.AddAuthentication(options => {//needed for Windows authentication
options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme;
});
Adding more...
I'm using Windows auth so I'm using the Negotiate provider. Then I set up my Authorization. I insert my own authorization policy and also add my claims transformers to get the authenticated user into a claim. The fallback policy in Authorization was causing an Authentication exception.
services.AddAuthorization(options =>
{
// options.FallbackPolicy = options.DefaultPolicy;//authorization bombs if you include this line
options.AddPolicy("AuthenticatedOnly", policy => {
policy.Requirements.Add(new AuthenticatedRequirement(true));
});
});
services.AddTransient<IClaimsTransformation, MyClaimsTransformer>();
services.AddTransient<IAuthorizationHandler, AppUserRoleHandler>();
services.AddTransient<IAuthorizationHandler, AuthenticatedRoleHandler>();
services.AddCors(options =>
{
options.AddPolicy(CorsPolicy,
builder => builder
.WithOrigins("https://localhost:7021","https://localhost:44414")
//Note: The URL must be specified without a trailing slash (/).
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
Now I'm in the middleware territory...as you know, order matters in your middleware! In 5.0 you could add IAntiforgery to your constructor and DI would handle the rest. In program.cs you don't have that luxury. Fortunately you can just grab it out of your services collection and you see that in the following code.
//==============formerly Startup.Configure=====================
WebApplication app = builder.Build();
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseCookiePolicy();
app.UseCors(CorsPolicy);
IAntiforgery? antiforgery = app.Services.GetRequiredService<IAntiforgery>();
Now when I'm setting up my endpoint routing. Found out that UseRouting and Use.Endpoints are married at the hip and need to be paired.
I also create a protected route "/auth" (protected by my authorization policy) to grab the antiforgery request token generated when we added it in the services collection. So this header won't be persisted from request to request like a cookie would. The minimal API allows me to create a route without creating the controller and action in a separate controller class.
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/auth", context =>
{
var tokens = antiforgery.GetAndStoreTokens(context);
context.Response.Headers.Append("XYZ", tokens.RequestToken!);
return Task.FromResult(Results.Ok());
}).RequireAuthorization("AuthenticatedOnly");
endpoints.MapControllerRoute(
name: "default"
pattern: "{controller}/{action=Index}/{id?}");
});
My React front end will use a fetch get request to get the token from the headers collection and then stick into a second post request and voila it works.
BTW, React doesn't provide Antiforgery functionality out of the box like Angular, in step with it's minimalist API ethos.
The action I'm posting to looks like this:
[HttpPost]
[Authorize(Policy="AuthenticatedOnly")]
[ValidateAntiForgeryToken]
public string Update()
I fully realize there are other ways to do this.

ASP.NET Core authentication and authorization with OIDC without creating local copies of the user

I've just created a new ASP.NET Core application using Visual Studio, it has the authentication type set to "Individual Accounts":
I've added some code to handle OIDC:
builder.Services.AddAuthentication().AddOpenIdConnect(openIdOptions =>
{
openIdOptions.ClientId = "myClientId";
openIdOptions.Authority = "myAuthority";
openIdOptions.ResponseType = OpenIdConnectResponseType.Code;
openIdOptions.GetClaimsFromUserInfoEndpoint = false;
openIdOptions.CallbackPath = "/signin-oidc";
openIdOptions.SaveTokens = true;
Which is all well and good, I can click to login with OpenIdConnect:
When I log in with the provided credentials it pops up asking me to Associate my OpenIdConnect account:
I don't want to have to do this. Retrieving the access token and id token is sufficient for what I want to do, I don't require a local copy of the user. Being new to ASP.net and having not used Razor before it's not immediately obvious what code I should delete — if that's even the correct approach. I've played around with deleting various bits such as the Db setup as I don't really require it, although I suspect Razor does in some regard.
The Program.cs looks as follows:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
})
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
builder.Services.AddAuthentication().AddGoogle(googleOptions =>
{
googleOptions.ClientId = builder.Configuration["Authentication:Google:ClientId"];
googleOptions.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
});
builder.Services.AddAuthentication().AddOpenIdConnect(openIdOptions =>
{
openIdOptions.ClientId = "clientId";
openIdOptions.Authority = "authority";
openIdOptions.ResponseType = OpenIdConnectResponseType.Code;
openIdOptions.GetClaimsFromUserInfoEndpoint = false;
openIdOptions.CallbackPath = "/signin-oidc";
openIdOptions.SaveTokens = true;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseMigrationsEndPoint();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.UseEndpoints(endpoints => { endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}"); });
app.Run();

How to use Refresh Token with ASP.NET Core 6 MVC and Azure AD Microsoft.Identity.Web?

What else do I need to program so that my web app responds to a refresh token?
I access the Azure-AD via Powershell and execute this command
Revoke-AzureADUserAllRefreshToken -ObjectId "ONJECTID".
Now, since I have 2 web apps and an already logged in user from the 1st web app calling the 2nd web app, it works out immediately that this user has to log in again with the password. But if I am already on a page in the 1st web app and just refresh the page nothing happens.
Here is my source code Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration, "AzureAd");
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("XYZ", p =>
{
p.RequireClaim("roles", "XYZ");
});
});
builder.Services.AddRazorPages().AddMvcOptions(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();
builder.Services.Configure<MicrosoftIdentityOptions>(options => {
options.Events = new OpenIdConnectEvents
{
//When Correlation Error, back to Startpage (Browserbackbutton after login)
OnRemoteFailure = context =>
{
context.Response.Redirect("/Home/Index");
context.HandleResponse();
return Task.CompletedTask;
}
};
});
//Configure the HTTP request pipeline.
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.Run();

Trouble authorizing .NET 5 Web API reference tokens to IdentityServer3 using IdentityServer4.AccessTokenValidation package

Server
Using IdentityServer3 for client/application authorization.
Using IdentityAdmin to edit clients/scopes via GUI.
Created a new Client for the API, added a SharedSecret and api scope.
API / Client
Has 2 GET endpoints.
Uses the IdentityServer4.AccessTokenValidation NuGet package.
Configuration should be simple:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(c => {
var policy = ScopePolicy.Create("api");
c.Filters.Add(new AuthorizeFilter(policy));
});
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options => {
options.Authority = "{base url of identity server}";
options.ApiName = ""; // not sure what this is? client id from identity server?
options.ApiSecret = ""; // should this be the hashed password?
options.LegacyAudienceValidation = true;
});
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MarvalAPI", Version = "v1" });
});
RegisterServices(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MarvalAPI v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication(); //this is added, everything else is by default
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
Testing:
GET client reference token from identity "/connect/token" endpoint
GET API's endpoint with added header "Authorization: Bearer {token}"
Receive 401 Unauthorized
Things I have tried:
Different Startup.cs configurations
Tried validating token via identity "/connect/accesstokenvalidation" endpoint, token is valid.
Different apiname/apisecret values, because not 100% sure what they have to be.
Googled to no avail
I am at a loss here, am I doing something totally wrong? Is this just a compatibility issue? Or am I just not understanding anything at all? Seems like clear documentation is scarce and users have to draw out information.
Sources used
https://github.com/IdentityServer/CrossVersionIntegrationTests/blob/main/src/CoreApiIdSrv3/Startup.cs
https://github.com/IdentityServer/IdentityServer4.AccessTokenValidation
IdentityServer3 documentation
SO / github/identityserver3 threads.
Well, some time after making this post I figured it out.
options.ApiName = "";
options.ApiSecret = "";
ApiName is the name of the scope which the client uses, so it this case the value should be api.
ApiSecret is the PRE-HASHED value of the scope secret.
e.g.
if secret value is "test" and it's SHA256 value is 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08, then ApiSecret value should be test
So, after figuring this out, the above options config should look like this:
options.ApiName = "api";
options.ApiSecret = "test";
Note: SHA512 works as well.
To me this seems like a major naming issue.
I solved this after analysing this VS solution:
https://github.com/IdentityServer/CrossVersionIntegrationTests

Categories

Resources