Getting lost configuring IdentityServer4 for SPA - asp.net

I have been through MS docs and IdentityServer docs. I am still stuck.
I have tried every config option and I've reached breaking point.
I started with the MS SPA template, added AspNet Core Identity, and fiddled with these setting. I'm able to call the Api's using Postman, and get a token, it has authority, and scope, and name set, but the API's show null in User.Identity when I debug.
Currently my error is this:
WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
Here is my configureServices:
public void ConfigureServices(IServiceCollection services)
{
// Add ASP.Net Core Identity
// this handles user authentication as well as user registration and management
services.AddDefaultIdentity<User>(options =>
{
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireDigit = true;
options.Password.RequireLowercase = false;
options.User.RequireUniqueEmail = true;
options.SignIn.RequireConfirmedEmail = true;
options.SignIn.RequireConfirmedAccount = true;
})
.AddEntityFrameworkStores<AccountingDbContext>();
services.AddRazorPages();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Handle authentication and authorization using Open ID Connect.
// The components for authentication and authorization are:
// - user
// - client (the Angular app)
// - identity provider (ASP.Net Core Identity with IdentityServer4), and
// - resource server (the api)
// identity provider and resource server are hosted together.
services.AddIdentityServer()
.AddApiAuthorization<User, AccountingDbContext>(options =>
{
options.ApiResources
.First() // represents the configured default Api
.UserClaims
.Add(JwtClaimTypes.Name);
})
.AddDeveloperSigningCredential();
// Add Controllers with API
services.AddControllers();
.AddMvcOptions(options =>
{
// Adding an empty Authorise filter means that any Authenticated user is valid.
options.Filters.Add(new AuthorizeFilter());
});
// For Azure App Service deployments on Linux
if (!Environment.IsDevelopment())
{
services.Configure<JwtBearerOptions>(
IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
options.Authority = Configuration.GetValue<string>("Authority");
});
}
// Authenticate requests to the Api
// accepts any access token issued by identity server
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = Configuration.GetValue<string>("Authority");
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
})
.AddIdentityServerJwt();
// https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html
// Adds Authorization policy to make sure the token has scope 'Accounting.ApplicationAPI openid profile'
services.AddAuthorization(options =>
{
// Registers the Policy with Authorization middleware
options.AddPolicy("Accounting.ApplicationAPI", policy =>
{
// Policy specifies that any Authenticated user with Accounting.ApplicationAPI scope is valid.
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "Accounting.ApplicationAPI openid profile");
});
});
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "./Accounting.Ng/dist";
});
}
and configure:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("Accounting.ApplicationAPI");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "../Accounting.Ng";
if (env.IsDevelopment())
{
spa.UseProxyToSpaDevelopmentServer(Configuration["AngularServer"]);
}
});
}

Related

Keep getting 401 on authorization with SignalR

I have been trying to create a sample Test app with SignalR but I have been extremely unsuccessful with authentication.
I am on .NET 6, and my Program.cs code looks like this.
Program.cs
using HubTestApp.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddSignalR();
builder.Services
.AddAuthentication(options =>
{
// Identity made Cookie authentication the default.
// However, we want JWT Bearer Auth to be the default.
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{TenantId}/";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateLifetime = true,
ValidateAudience = false,
ValidateIssuerSigningKey = false,
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required due to
// a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://docs.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/Test")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<SimpleHub>("/Test");
app.Run();
My hub code is pretty simple:
namespace HubTestApp.Hubs
{
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
internal class SimpleHub : Hub<ISimpleClient>
{
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task EchoMessage(string message) => await Clients.All.ReceiveMessage(message);
}
}
And this is my client code:
namespace HubTestClient
{
using Microsoft.AspNetCore.SignalR.Client;
public class MockClient
{
private const string Token = "Bearer <JWT from AAD>";
private readonly HubConnection hubConnection;
public MockClient()
{
// Notice here I have tried to pass the token in various ways, all to no avail.
this.hubConnection = new HubConnectionBuilder()
.WithUrl($"http://localhost:5110/Test?access_token={MockClient.Token}", options =>
{
options.AccessTokenProvider = () => Task.FromResult(MockClient.Token);
options.Headers.Add("Authorization", MockClient.Token);
})
.Build();
this.hubConnection.On<string>("ReceiveMessage", (message) =>
{
Console.WriteLine($"Received message: '{message}'");
});
}
public async Task StartClient()
{
await hubConnection.StartAsync();
Random rng = new Random();
while (true)
{
string messageToEcho = $"Sending random number '{rng.Next()}'";
Console.WriteLine(messageToEcho);
try
{
await hubConnection.InvokeAsync("EchoMessage", messageToEcho);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
// Delay 5 seconds between hitting the hub.
await Task.Delay(5000);
}
}
}
}
I continuously get the message "Failed to invoke 'X' because user is unauthorized." I have made sure the token I got is valid. So, I'm bashing my head over this, completely confused where I am going wrong. Thank you in advance!

Authorization Policy Attribute returns 403 and User.IsInRole returns false but claims are present WEB API Azure AD (Single Tenant)

Setup
I have an ASP.NET Core MVC web app configured with Azure AD. I have an ASP.NET Core Web API configured Azure AD.
I obtain the token from Azure AD in the client web app and use it when requesting resources from the Web API, this works fine.
Issue
The '[Authorize(Policy = 'policyName')]' attribute returns 403 and User.IsInRole() returns false in the WEB API controller, however when I remove the authorize policy attribute and check the user claims object, I can see the role present. Please check the image below
Claims Image
RoleClaimType Image
In my startup file I have configured token validation parameters of roleClaimType to be roles
`services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>{
// Use the groups claim for populating roles
options.TokenValidationParameters.RoleClaimType = "roles";
});`
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions();
services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// Use the groups claim for populating roles
options.TokenValidationParameters.RoleClaimType = "roles";
});
services.AddAuthorization(options =>
{
options.AddPolicy(Constants.AssignmentToAccountCreatorsRoleRequired, policy => policy.RequireRole(Constants.CAN_CREATE_ACCOUNT));
});
services.AddDbContext<DbContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]));
//services.BuildServiceProvider().GetService<DbContext>().Database.Migrate();
services.AddAutoMapper(typeof(Startup));
services.AddControllers().AddNewtonsoftJson(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
//TODO: Replace CORS configuration
services.AddCors(c => c.AddDefaultPolicy(policy => {
policy.WithOrigins("https://localhost:44385", "https://someweb.net/")
.AllowAnyHeader()
.AllowAnyMethod();
}));
services.AddControllers();
services.AddScoped<ConfigurationModel>();
//services.AddScoped<GraphProfileClient>();
//services.AddScoped<GraphEmailClient>();
services.AddMemoryCache();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "NAPI", Version = "v1" });
});
}
// 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.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"));
}
else
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
I noticed the options for AddAuthorization() policy in the startup.cs contains a RequireClaim method which takes in the claimType and the allowedValue(s). RequireClaim(String, IEnumerable)
services.AddAuthorization(options =>
{
options.AddPolicy("AssignmentRoleRequired", policy => policy.RequireClaim("roles", "canDoSomething"));
}); //Startup.cs
[Authorize(Policy = "AssignmentToRoleRequired")] //For Controllers
Similarly the User Object also contains a method called HasClaim(), that takes in the claimType and the allowed value.
Within a controller HasClaim(String, String)
var result = User.HasClaim("roles", "canDoSomething"); //returns true

Getting Fetch errorNot Found /swagger/v1/swagger.json in swashbuckle Asp.netcore

I have a basic asp.net core 3.1 app. I installed SwashBuckle.ASPNetCore nuget package.
here is my startup.cs. When I run this as a separate application, swagger is working fine. But Once I deploy this to IIS, I am getting 404 not found. Do I need to add a Path somewhere?
I looked almost everything but things are not working for me.
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddProtectedWebApi(Configuration);
// Additional configuration
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.RoleClaimType = "roles";
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Channel.API", Version = "v1" });
/*var security = new Dictionary<string, IEnumerable<string>>
{
{"Bearer", new string[] { }},
};
c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme()
{
Description =
"JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
In = ParameterLocation.Header,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme{
Reference = new OpenApiReference{
Id = "Bearer", //The name of the previously defined security scheme.
Type = ReferenceType.SecurityScheme
}
},new List<string>()
}
});*/
});
services.AddApplicationInsightsTelemetry();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// Enable middleware to serve generated Swagger as a JSON endpoint.
//app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
}
else
{
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("v1/swagger.json", "My API V1");
});
}
app.UseHsts();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
endpoints.MapControllers();
});
app.UseElmah();
}
Update
Resolved this by adding ..
c.SwaggerEndpoint("../swagger/v1/swagger.json", "My API V1");

User is not authenticated in Middleware, but authenticated in Controller

I am using OpenIddict package to handle authentication in my .net core Api app. Inside my controllers, I can see User, its claims, roles, etc. However in Middleware, I don't have access to the user, as if the user is not authenticated.
I think I am missing something in ConfigureServices or Configure methods in Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc(
config =>
{
//We are adding a default policy required for all requests
var policyBuilder =
new AuthorizationPolicyBuilder(OpenIddictValidationDefaults.AuthenticationScheme);
var policy = policyBuilder.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}
);
services.AddDbContext<TGDbContext>((ctx, options) =>
{
options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"]);
options.UseOpenIddict();
}, ServiceLifetime.Scoped, ServiceLifetime.Scoped);
// Register the OpenIddict services.
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<TGDbContext>();
})
.AddServer(options =>
{
options.UseMvc();
options.EnableTokenEndpoint( "/connect/token");
options.AllowPasswordFlow();
options.AllowRefreshTokenFlow();
options.AcceptAnonymousClients();
options.AllowCustomFlow("GuestLogin");
options.RegisterScopes("Booking");
if (this._env.IsDevelopment())
options.DisableHttpsRequirement(); // Note: Requires Https in production
options.RegisterScopes(
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Phone,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles);
})
.AddValidation();
// add identity
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<TGDbContext>()
.AddDefaultTokenProviders();
// Configure Identity options and password complexity here
services.Configure<IdentityOptions>(options =>
{
// User settings
options.User.RequireUniqueEmail = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 5;
options.ClaimsIdentity.UserNameClaimType = OpenIdConnectConstants.Claims.Name;
options.ClaimsIdentity.UserIdClaimType = OpenIdConnectConstants.Claims.Subject;
options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role;
});
services.AddAuthorization(options =>
{
options.AddPolicy(.....);
....
....
....
});
.....
.....
.....
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
....
....
....
app.UseAuthentication();
app.Use(async (context, next) =>
{
var isAuthenticated = context.User.Identity.IsAuthenticated;
// isAuthenticated is false here
// also context.User.Claims is empty
Console.WriteLine(isAuthenticated);
await next.Invoke();
});
....
....
....
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
}
I am targeting .Net Core 2.2 using OpenIddict 2.0.1 and OpenIddict.EntityFramework 2.0.1.
The behavior you're seeing is expected (and not specific to OpenIddict): by registering an AuthorizeFilter pointing to the OpenIddict validation handler, you're only configuring MVC to validate bearer tokens, but not the rest of your app, including middleware.
To configure OpenIddict as the default authentication scheme, call
services.AddAuthentication(options =>
{
// When targeting OpenIddict 3.0, OpenIddictValidationAspNetCoreDefaults
// must be used instead of OpenIddictValidationDefaults.
options.DefaultAuthenticateScheme = OpenIddictValidationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIddictValidationDefaults.AuthenticationScheme;
});

.NET Core Angular client SPA has issue redirecting on logout to IdentityServer4 IDP

I've an Angular Client which needs to be authentcated. This client is hosted in .NetCore SPA. Now using IdentityServer4, I've setup the IDP to authenticate the Client.
Everything works fine for login. Here the client automatically redirects to IDP project to login. Once credentials are entered it will redirect back to the client app.
However, for logout it's not working. On the Angular client which is hosted as a SPA in .netcore, I've a logout button. This logout button event will call the API controller in the same client. In this controller there is code to Signout from the httpContext.
My expectation was, when the logout action is called in the controller it will logout and redirect me back to the IDP. On a positive note, I've implemented the same in MVC client(no SPA and angular) and it works for the logout by redirecting me back to the IDP.
Also another positive, on the angular client on logout it actually tries to logout, but the CORS policy is blocking it. The error message in the browser console has the redirect url as part of the error message. This url works when i click on it by redirecting me to the IDP and logging me out.
I'm not able to figure out what CORS policy I have to add to make this redirect work. I've tried adding CORS to the startup class, but did not work. Help please
Browser error message:
Access to XMLHttpRequest at 'https://localhost:44336/connect/endsession?post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A44374%2Fsignout-callback-oidc&id_token_hint=eyJhbGciOiJSUzI1NiIsImtpZCI6IlMwbFpqUi1QazItS0dLc2xxaFlQQ2ciLCJ0eXAiOiJKV1QifQ.eyJuYmYiOjE1NzkzNDc3NDcsImV4cCI6MTU3OTM0ODA0NywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NDQzMzYiLCJhdWQiOiJBY3Rpdml0eVRyYWNrZXJOZ0NsaWVudF9DbGllbnRJZCIsIm5vbmNlIjoiNjM3MTQ5NDQ1NDMyMDYwODM5LllUVmpaV1UyTURjdE5ERmhOeTAwTW1SbExXRm1NVGd0TWpKaVl6VTFOMlJoWkRkaE9EYzNPV1EwTm1VdFpEUmxaUzAwT1RNMkxUZ3hOV0l0T0RJeU5HWTVaR1l4T0RsaSIsImlhdCI6MTU3OTM0Nzc0NywiYXRfaGFzaCI6ImJwc3hLLThSU2Nwb2hKMnJPSlViQlEiLCJzX2hhc2giOiJ0M2R1RmljZDR1VTRTQlQ3S253cG1nIiwic2lkIjoiYTFIWnE5YXFMdnR1WHB6S1FUZUVvUSIsInN1YiI6ImQ4NjBlZmNhLTIyZDktNDdmZC04MjQ5LTc5MWJhNjFiMDdjNyIsImF1dGhfdGltZSI6MTU3OTM0Nzc0NywiaWRwIjoibG9jYWwiLCJhbXIiOlsicHdkIl19.bd35dk-lcolUxgoNAzzc4kKIORQIsmeSu5JaARpyqj1I6cv5P6LSHrcdw3YmZ80q_tF8WLi7ywIml-enEP4JAe-nbYw7gSlFt9qHtw5eSF37dMdBZq7UUXt6EoK29xs9lp6TyIB11pzgRZ8tPVAPw0Y8rNpGSGYtjfWjp7t4FdKthvUchAo_SNh6l40S5oV0Yo_YIWfHtjxM-nLZXia0YCvjNEQChmTmkzSMCIdGnVqawhIzQ_O7jv0c1T7kCwaF5YGyer3ZUyj1UM53JTBbbGpKDrDh2DV-kd4tvhoaLnWQAoUqCQ1Ofl_kHc8vffqE7RRPGmQLQYOM48186hIe0g&state=CfDJ8DRPXADjz9hKioMAFvg6DCP1P37ODZ4R81EV3uFXBpxiOLWoJY6GDEcbYNZzB--zZjv-Z94PSfMJkcoJhQcmHAvmM_9yKL9hPaGqmucpJrO_wv74Fj8bmdm8C7l_MJZ3VaNahF5Bqvi9tWFUikbr-HJ_uI0GiGX6qsj5mkrp8K4x&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0' (redirected from 'https://localhost:44374/api/Authorization/Logout') from origin 'https://localhost:44374' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource
And the API controller logout code:
[HttpGet]
public async Task Logout()
{
var httpclient = new HttpClient();
var disco = await httpclient.GetDiscoveryDocumentAsync("https://localhost:44336/");
// get the access token to revoke
var accessToken = await HttpContext
.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (!string.IsNullOrWhiteSpace(accessToken))
{
var revokeAccessTokenResponse =
await httpclient.RevokeTokenAsync(new TokenRevocationRequest
{
Address = disco.RevocationEndpoint,
ClientId = "App_ClientId",
ClientSecret = "someSecret",
Token = accessToken
});
if (revokeAccessTokenResponse.IsError)
{
throw new Exception("Problem encountered while revoking the access token."
, revokeAccessTokenResponse.Exception);
}
}
// Clears the local cookie ("Cookies" must match name from scheme)
await HttpContext.SignOutAsync("Cookies");
await HttpContext.SignOutAsync("oidc");
}
Startup.cs with entire client configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<ActivityTrackerAPIHttpClient>();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc";
}).AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = Configuration["IdentityServer:Authority"];
options.ClientId = Configuration["IdentityServer:ClientId"];
options.ClientSecret = Configuration["IdentityServer:ClientSecret"];
options.ResponseType = "code id_token";
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("roles");
options.Scope.Add(Configuration["IdentityServer:ApiName"]);
options.Scope.Add("offline_access");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.Remove("amr");
options.ClaimActions.DeleteClaim("sid");
options.ClaimActions.DeleteClaim("idp");
options.ClaimActions.MapJsonKey("role", "role");
options.TokenValidationParameters = new TokenValidationParameters
{
RoleClaimType = JwtClaimTypes.Role,
};
});
services.ConfigureLoggerService();
}
// 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();
}
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.UseAuthentication();
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.Use(async (context, next) =>
{
if (!context.User.Identity.IsAuthenticated)
{
await context.ChallengeAsync("oidc");
}
else
{
await next();
}
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
I came up with a temporary solution at that point. However I realized, it is ideal to do the client security in the angular side.
My temp solution below.
Startup.cs of client
options.Events = new Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var protocolMessage = context.ProtocolMessage;
var param = "";
foreach (var parameter in protocolMessage.Parameters)
{
param += $"{parameter.Key}={parameter.Value}$";
}
var url = $"{protocolMessage.IssuerAddress}?{param}x-client-SKU={protocolMessage.SkuTelemetryValue}&x-client-ver=5.5.0.0";
context.HttpContext.Session.SetString("LogoutUrl", url);
return Task.FromResult(0);
}
};
Also enable session in the client startup.cs by adding services.AddSession(); in ConfigureServices method and then add app.UseSession(); in the Configure method.
In the controller
[HttpGet]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
var url = HttpContext.Session.GetString("LogoutUrl");
return Ok(url);
}

Resources