IdentityServer 4 with ASP.net Identity not working - identityserver4

I am trying to create a Auth Server with IdentityServer 4 and ASP.net Core Identity backed by Entity Framework.
I have Users & Claims being stored in ASP.net identity tables on startup and Client, Resources stored in Identity Server tables.
When I am trying to get a token, I am getting the error attached in the screenshot.
Startup.cs
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
var connectionString = #"server=localhost;database=IdentityServer;trusted_connection=yes";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddScoped<ApplicationUser>();
//services.AddScoped<SignInManager<ApplicationUser>>();
services.AddScoped<UserManager<ApplicationUser>>();
services.AddScoped<UserStore<ApplicationUser>>();
services.AddEntityFrameworkSqlServer();
services.AddDbContext<ApplicationDbContext>(builder =>
{
builder.UseSqlServer(connectionString, options => options.MigrationsAssembly(migrationsAssembly));
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services
.AddIdentityServer()
.AddProfileService<ProfileService>()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
.AddTemporarySigningCredential()
.AddConfigurationStore(builder =>
builder.UseSqlServer(connectionString, options =>
options.MigrationsAssembly(migrationsAssembly)))
.AddOperationalStore(builder =>
builder.UseSqlServer(connectionString, options =>
options.MigrationsAssembly(migrationsAssembly)))
.AddAspNetIdentity<ApplicationUser>();
services
.AddMvcCore()
.AddJsonFormatters();
}
//This method gets called by the runtime.Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// this will do the initial DB population
InitializeDatabase(app);
loggerFactory.AddConsole();
app.UseIdentity();
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
private static void InitializeDatabase(IApplicationBuilder app)
{
using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
scope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
var configContext = scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
configContext.Database.Migrate();
if (!configContext.Clients.Any())
{
foreach (var client in Config.GetClients())
{
configContext.Clients.Add(client.ToEntity());
}
configContext.SaveChanges();
}
if (!configContext.IdentityResources.Any())
{
foreach (var resource in Config.GetIdentityResources())
{
configContext.IdentityResources.Add(resource.ToEntity());
}
configContext.SaveChanges();
}
var appContext = app.ApplicationServices.GetRequiredService<ApplicationDbContext>();
if (!appContext.Users.Any())
{
foreach (var user in Config.GetUsers())
{
var identityUser = new ApplicationUser();
var hash = new PasswordHasher<IdentityUser>().HashPassword(identityUser, user.Password);
identityUser.PasswordHash = hash;
identityUser.UserName = user.Username;
identityUser.NormalizedUserName = user.Username;
identityUser.Email = user.Username;
identityUser.NormalizedEmail = user.Username;
identityUser.EmailConfirmed = true;
foreach (var claim in user.Claims)
{
identityUser.Claims.Add(new IdentityUserClaim<string> { UserId = user.SubjectId, ClaimType = claim.Type, ClaimValue = claim.Value });
}
appContext.Users.Add(identityUser);
appContext.SaveChanges();
}
}
if (configContext.ApiResources.Any()) return;
foreach (var resource in Config.GetApiResources())
{
configContext.ApiResources.Add(resource.ToEntity());
}
configContext.SaveChanges();
}
}
}
ResourceOwnerPasswordValidator.cs
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly IUserStore<ApplicationUser> _userStore;
public ResourceOwnerPasswordValidator(IUserStore<ApplicationUser> userStore, UserManager<ApplicationUser> userManager)
{
_userStore = userStore;
_userManager = userManager;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
var user = await _userStore.FindByNameAsync(context.UserName, CancellationToken.None);
if (user != null && await _userManager.CheckPasswordAsync(user, context.Password))
{
context.Result = new GrantValidationResult(
subject: user.Id,
authenticationMethod: context.Request.GrantType,
claims: user.Claims.Select(c=>new Claim(c.ClaimType, c.ClaimValue)));
}
context.Result = new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
"invalid custom credential");
}
}
I can't figure out why ResourceOwnerPasswordValidator is not being invoked.
Thanks for your help.

Related

Supporting Core Identity Roles in IdentityServer4

I'm creating a Single-Sign-on server using IdentiyServer4. I've looked at their QuickStarts showing how to integrate MS Core Identity with ASP.NET Core 3.1 apps. But there's no examples showing whether ASP.NET roles are natively supported in MVC controllers. A few experiments seemed to indicate that they aren't. But when I discovered that role data can be returned in the Access Token, I wrote my own action filter that authorises users.
However, looking at the documentation for IdentityServer3, they do briefly show roles being used in MVC controllers. So now I'm completely confused. But apart from that, there's no documentation that I can find, and the only mention online I could find about roles with IdentityServer were about a different issue - using roles to control access to remote APIs.
My filter isn't working that well, and I'm worried it's the wrong approach and unnecessary. Can anyone either enlighten me, or point me to any resources that would help.
One gotcha, is that you need to configure and tell ASP.NET Core what the name of the roles claim is in the incoming token.
Out of the box IdentityServer and Microsoft does not agree on the name of the roles claim.
So, you need to set the RoleClaimType.
.AddOpenIdConnect(options =>
{
// other options...
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "email",
RoleClaimType = "role"
};
});
I hope these codes will be useful for you.
I added ASP.NET Core Identity in the IdentityServer project.
Statup.cs in API Client
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers()
.RequireAuthorization("ApiScope");
});
}
}
Startup.cs in MVC Client
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.Scope.Add("email");
options.Scope.Add("roles");
options.ClaimActions.DeleteClaim("sid");
options.ClaimActions.DeleteClaim("idp");
options.ClaimActions.DeleteClaim("s_hash");
options.ClaimActions.DeleteClaim("auth_time");
options.ClaimActions.MapJsonKey("role", "role");
options.Scope.Add("api1");
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
});
services.AddTransient<AuthenticationDelegatingHandler>();
services.AddHttpClient("ApplicationAPI", client =>
{
client.BaseAddress = new Uri("https://localhost:5002/");
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
}).AddHttpMessageHandler<AuthenticationDelegatingHandler>();
services.AddHttpClient("ApplicationIdentityServer", client =>
{
client.BaseAddress = new Uri("https://localhost:5001/");
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/json");
});
services.AddHttpContextAccessor();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{area=Admin}/{controller=Home}/{action=Index}/{id?}");
});
}
}
AuthenticationDelegatingHandler in MVC Application
To prevent getting token again.
public class AuthenticationDelegatingHandler : DelegatingHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
public AuthenticationDelegatingHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
if (!string.IsNullOrWhiteSpace(accessToken))
{
request.SetBearerToken(accessToken);
}
return await base.SendAsync(request, cancellationToken);
}
}
Config.cs in IdentityServer
public static class Config
{
public static IEnumerable<IdentityResource> IdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource("roles", "Your role(s)", new List<string>() { "role" })
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("api1", "My API")
};
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientId = "client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "api1" }
},
new Client
{
ClientId = "mvc",
ClientName = "Application Web",
AllowedGrantTypes = GrantTypes.Hybrid,
ClientSecrets = { new Secret("secret".Sha256()) },
RequirePkce = false,
AllowRememberConsent = false,
RedirectUris = { "https://localhost:5003/signin-oidc" },
PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"api1",
"roles"
}
}
};
}
Startup.cs in IdentityServer
public class Startup
{
public IWebHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
Environment = environment;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.SignIn.RequireConfirmedEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.EmitStaticAudienceClaim = true;
options.UserInteraction.LoginUrl = "/Account/Login";
options.UserInteraction.LogoutUrl = "/Account/Logout";
options.Authentication = new AuthenticationOptions()
{
CookieLifetime = TimeSpan.FromHours(10),
CookieSlidingExpiration = true
};
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddAspNetIdentity<ApplicationUser>();
if (Environment.IsDevelopment())
{
builder.AddDeveloperSigningCredential();
}
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "copy client ID from Google here";
options.ClientSecret = "copy client secret from Google here";
});
services.AddTransient<IEmailSender, EmailSender>();
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
}
}

return user role in token response using CustomTokenRequestValidationContext in identity server 4

I am using Identity Server 4.0 with Identity Core, I need user role along with token response.
below attached image for reference
It is not the correct concept due to OAuth2 protocol violation and requires modifying the internals of IS4.
But you could implement getting role from:
- User info endpoint (http://docs.identityserver.io/en/3.1.0/endpoints/userinfo.html)
- Access token
For the last variant, you should override profile service (http://docs.identityserver.io/en/3.1.0/reference/profileservice.html)|
For example:
public class MyProfileService : IProfileService
{
private readonly UserManager<User> _userManager;
public MyProfileService(UserManager<User> userManager)
{
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
var roles = await _userManager.GetRolesAsync(user);
var claims = new List<Claim>
{
new Claim(JwtClaimTypes.Email, user.Email),
new Claim(JwtClaimTypes.GivenName, user.FirstName),
new Claim(JwtClaimTypes.FamilyName, user.LastName),
new Claim(JwtClaimTypes.Name, $"{user.FirstName} {user.LastName}"),
new Claim(JwtClaimTypes.Locale, user.PreferredLanguage)
};
claims.AddRange(roles.Select(role => new Claim(JwtClaimTypes.Role, role)));
context.IssuedClaims.AddRange(claims);
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.CompletedTask;
}
}
And don't forget to register it
services.AddIdentityServer(options =>
{
...
})
.AddProfileService<MyProfileService>()

IdentityServer4 All Client Scopes in Token

I have a multitenant app secure with an IdentityServer4 implementation. I recently updated it to the latest ID4 and the behavior seems to have changed. Previously, I could make a request with the TokenClient inside of the IdentityModel package:
var parameters = new Dictionary<string, string>();
parameters.Add("username", loginModel.UserName);
parameters.Add("password", loginModel.Password);
var tokenClient = new TokenClient(new Uri(new Uri(accountsConfig.EndpointUrl), "/connect/token").ToString(), accountsConfig.ClientId, accountsConfig.Secret, null, AuthenticationStyle.PostValues);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("AgentLogin", extra: parameters);
It would return all of the scopes defined for the client in the token. That is no longer the case. How do I configure ID4 to do that without explicitly requesting them inside of the TokenClient?
public class AgentLoginCustomGrantValidator : IExtensionGrantValidator
{
private readonly ILogger<AgentLoginCustomGrantValidator> _logger;
private readonly IAdminUserService _adminUserService;
public AgentLoginCustomGrantValidator(ILogger<AgentLoginCustomGrantValidator> logger, IAdminUserService adminUserService)
{
_logger = logger;
_adminUserService = adminUserService;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
try
{
var username = context.Request.Raw.Get("username");
var password = context.Request.Raw.Get("password");
var userId = _adminUserService.AuthenticateUser(username, password);
if (userId != null)
{
var agencyUser = _adminUserService.GetUser(userId.Value);
context.Result = new GrantValidationResult($"{userId}", GrantType, agencyUser.Roles.Select(x => new Claim(JwtClaimTypes.Role, x.Name)).Concat(new List<Claim>() { new Claim(JwtClaimTypes.Name, agencyUser.UserName) { } }));
}
else
{
_logger.LogWarning($"Bum creds: {username} ");
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidClient, "Invalid credentials");
}
}
catch (Exception ex)
{
_logger.LogError(ex.ToString());
context.Result = new GrantValidationResult(TokenRequestErrors.InvalidClient, ex.Message);
}
}
public string GrantType => "AgentLogin";
}
Looks like Identity Server 4 by default only returns the requested identity or api resources for each client. However, this behaviour can be easily overridden to return all the scopes regardless whether they were requested in the token request or not. You can create a CustomClaimsService which inherits from the DefaultClaimsService.
public class CustomClaimsService : DefaultClaimsService
{
public CustomClaimsService(IProfileService profile, ILogger<DefaultClaimsService> logger) : base(profile, logger)
{
}
public override async Task<IEnumerable<Claim>> GetAccessTokenClaimsAsync(ClaimsPrincipal subject,
Resources resources, ValidatedRequest request)
{
var baseResult = await base.GetAccessTokenClaimsAsync(subject, resources, request);
var outputClaims = baseResult.ToList();
//If there are any allowed scope claims that are not yet in the output claims - add them
foreach (var allowedClientScope in request.Client.AllowedScopes)
{
if (!outputClaims.Any(x => x.Type == JwtClaimTypes.Scope && x.Value == allowedClientScope))
{
outputClaims.Add(new Claim(JwtClaimTypes.Scope, allowedClientScope));
}
}
return outputClaims;
}
}
Then just register it with the IdentityServerBuilder service container.
var builder = services.AddIdentityServer(options =>
{
//Your identity server options
});
//Register the custom claims service with the service container
builder.Services.AddTransient<IClaimsService, CustomClaimsService>();
Each access token will now contain all the scopes that the given client is allowed.

Adding Azure AD's policies to Startup after Auth 2.0 migration

I recently asked a similar question, but it was with AAD B2C in regard. Now I'm wondering how to properly add policies to Azure Active Directory authentication in my app. Currently, my Startup class looks like this :
namespace Auth
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
private IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(opts =>
{
opts.Filters.Add(typeof(AdalTokenAcquisitionExceptionFilter));
});
services.AddAuthorization(o =>
{
});
services.AddAuthentication(auth =>
{
auth.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
auth.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(opts =>
{
Configuration.GetSection("Authentication").Bind(opts);
opts.Events = new OpenIdConnectEvents
{
OnAuthorizationCodeReceived = async ctx =>
{
HttpRequest request = ctx.HttpContext.Request;
string currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path);
var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret);
IDistributedCache distributedCache = ctx.HttpContext.RequestServices.GetRequiredService<IDistributedCache>();
string userId = ctx.Principal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var cache = new AdalDistributedTokenCache(distributedCache, userId);
var authContext = new AuthenticationContext(ctx.Options.Authority, cache);
AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync(
ctx.ProtocolMessage.Code, new Uri(currentUri), credential, ctx.Options.Resource);
ctx.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
};
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvcWithDefaultRoute();
}
}
}
I manage to acquire all needed tokens (for Azure Graph) succesfully later on, but right now the app uses some kind of default microsoft policy and I'm forced to use Microsoft authentication, while I'd also want to authenticate local tenant users. I have a sign up policy in my tenant called B2C_1_SignInPolicy, but I can't figure out how to pass it to my app's authentication. App is using a MVC-like model and .Net Core 2.0.
My best guess was adding a line similar to opts.AddPolicyUrl("https://...policyName); but I can't find a way to do that.
Instead of adding the AddOpenIdConnect directly, you can refer the code below for the Asp.net Core 2.0 to interact with Azure AD B2C:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddAzureAdB2C(options => Configuration.Bind("Authentication:AzureAdB2C", options))
.AddCookie();
// Add framework services.
services.AddMvc();
// Adds a default in-memory implementation of IDistributedCache.
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromHours(1);
options.CookieHttpOnly = true;
});
}
public static class AzureAdB2CAuthenticationBuilderExtensions
{
public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder)
=> builder.AddAzureAdB2C(_ =>
{
});
public static AuthenticationBuilder AddAzureAdB2C(this AuthenticationBuilder builder, Action<AzureAdB2COptions> configureOptions)
{
builder.Services.Configure(configureOptions);
builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsSetup>();
builder.AddOpenIdConnect();
return builder;
}
public class OpenIdConnectOptionsSetup : IConfigureNamedOptions<OpenIdConnectOptions>
{
public OpenIdConnectOptionsSetup(IOptions<AzureAdB2COptions> b2cOptions)
{
AzureAdB2COptions = b2cOptions.Value;
}
public AzureAdB2COptions AzureAdB2COptions { get; set; }
public void Configure(string name, OpenIdConnectOptions options)
{
options.ClientId = AzureAdB2COptions.ClientId;
options.Authority = AzureAdB2COptions.Authority;
options.UseTokenLifetime = true;
options.TokenValidationParameters = new TokenValidationParameters() { NameClaimType = "name" };
options.Events = new OpenIdConnectEvents()
{
OnRedirectToIdentityProvider = OnRedirectToIdentityProvider,
OnRemoteFailure = OnRemoteFailure,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived
};
}
public void Configure(OpenIdConnectOptions options)
{
Configure(Options.DefaultName, options);
}
public Task OnRedirectToIdentityProvider(RedirectContext context)
{
var defaultPolicy = AzureAdB2COptions.DefaultPolicy;
if (context.Properties.Items.TryGetValue(AzureAdB2COptions.PolicyAuthenticationProperty, out var policy) &&
!policy.Equals(defaultPolicy))
{
context.ProtocolMessage.Scope = OpenIdConnectScope.OpenIdProfile;
context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.IdToken;
context.ProtocolMessage.IssuerAddress = context.ProtocolMessage.IssuerAddress.ToLower().Replace(defaultPolicy.ToLower(), policy.ToLower());
context.Properties.Items.Remove(AzureAdB2COptions.PolicyAuthenticationProperty);
}
else if (!string.IsNullOrEmpty(AzureAdB2COptions.ApiUrl))
{
context.ProtocolMessage.Scope += $" offline_access {AzureAdB2COptions.ApiScopes}";
context.ProtocolMessage.ResponseType = OpenIdConnectResponseType.CodeIdToken;
}
return Task.FromResult(0);
}
public Task OnRemoteFailure(RemoteFailureContext context)
{
context.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
context.Response.Redirect("/Session/ResetPassword");
}
else if (context.Failure is OpenIdConnectProtocolException && context.Failure.Message.Contains("access_denied"))
{
context.Response.Redirect("/");
}
else
{
context.Response.Redirect("/Home/Error?message=" + context.Failure.Message);
}
return Task.FromResult(0);
}
public async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
// Use MSAL to swap the code for an access token
// Extract the code from the response notification
var code = context.ProtocolMessage.Code;
string signedInUserID = context.Principal.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, context.HttpContext).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(AzureAdB2COptions.ClientId, AzureAdB2COptions.Authority, AzureAdB2COptions.RedirectUri, new ClientCredential(AzureAdB2COptions.ClientSecret), userTokenCache, null);
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, AzureAdB2COptions.ApiScopes.Split(' '));
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
catch (Exception ex)
{
//TODO: Handle
throw;
}
}
}
}
And for the full code sample, you can refer the core2.0 branch of active-directory-b2c-dotnetcore-webapp.

No 'Access-Control-Allow-Origin' header is present on the requested resource error when [Authorize] is placed on any method in a controller

I have been following the tutorials here to build my application. Everything works well at the authentication side. Now, the problem I am having is when I move to the resource server to retrieve data. If I place the [Authorize] on any method in the resource server I get the error message "No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin is therefore not allowed access. The response had HTTP status code 500.". If I remove it everything works fine but I am unable access any claims or roles associated with the user
Excerpts of the startup.cs code of my AuthServer is as follows
public class Startup
{
string PublicHostUri { get { return "https://localhost:44354"; } }
private readonly IHostingEnvironment _environment;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsEnvironment("Development"))
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
_environment = env;
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var connectionString = Configuration["Data:UserAccConnection:ConnectionString"];
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "reportbook.auth.pfx"), "");
var reportbookConnnectionString = Configuration["Data:ReportBookDbConnection:connectionString"];
services.AddDbContext<ReportBookDbContext>(options =>
options.UseSqlServer(reportbookConnnectionString));
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
services.AddDbContext<UserDbContext>(options =>
options.UseSqlServer(connectionString, b => b.MigrationsAssembly(migrationsAssembly)));
// Register the Identity services.
services.AddIdentity<ApplicationUser, UserRole>()
.AddEntityFrameworkStores<UserDbContext, Guid>()
.AddDefaultTokenProviders();
services.AddCors();
services.AddMvc();
services.AddIdentityServer()
.AddDefaultEndpoints()
.AddOperationalStore(builder =>
builder.UseSqlServer(connectionString,
options => options.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStore(builder =>
builder.UseSqlServer(connectionString,
options => options.MigrationsAssembly(migrationsAssembly)))
.SetSigningCredential(cert)
.AddAspNetIdentity<ApplicationUser>()
.AddProfileService<IdentityWithAdditionalClaimsProfileService>();
services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});
services.AddTransient<IProfileService, IdentityWithAdditionalClaimsProfileService>();
services.AddTransient<IUnitOfWorkAsync, UnitOfWork>();
services.AddScoped<IDataContextAsync, ReportBookDbContext>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
InitializeDbTestData(app);
}else
{
app.UseExceptionHandler(
builder =>
{
builder.Run(
async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false);
}
});
});
}
app.UseApplicationInsightsRequestTelemetry();
app.UseApplicationInsightsExceptionTelemetry();
app.UseStaticFiles();
app.UseCors(builder =>
builder.AllowAnyOrigin()
.AllowCredentials()
.AllowAnyHeader()
.AllowAnyMethod());
app.UseCsp(options => options.DefaultSources(directive => directive.Self())
.ImageSources(directive => directive.Self()
.CustomSources("*"))
.ScriptSources(directive => directive.Self()
.UnsafeInline())
.StyleSources(directive => directive.Self()
.UnsafeInline()));
app.UseXContentTypeOptions();
app.UseXfo(options => options.Deny());
app.UseXXssProtection(options => options.EnabledWithBlockMode());
app.UseIdentity();
app.UseIdentityServer();
//app.UseMvc(routes =>
//{
// routes.MapRoute(
// name: "default",
// template: "{controller=Home}/{action=Index}/{id?}");
//});
app.UseMvcWithDefaultRoute();
app.UseMvc();
//databaseInitializer.Seed(app).GetAwaiter().GetResult();
}
private static void InitializeDbTestData(IApplicationBuilder app)
{
using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
scope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();
scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>().Database.Migrate();
scope.ServiceProvider.GetRequiredService<UserDbContext>().Database.Migrate();
var context = scope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
if (!context.Clients.Any())
{
foreach (var client in Clients.Get())
{
context.Clients.Add(client.ToEntity());
}
context.SaveChanges();
}
if (!context.Scopes.Any())
{
foreach (var clientSope in Scopes.Get())
{
context.Scopes.Add(clientSope.ToEntity());
}
context.SaveChanges();
}
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<UserRole>>();
if (!userManager.Users.Any())
{
foreach (var newUser in Users.Get())
{
ApplicationUser user = new ApplicationUser();
user.Id = new Guid();
user.EmailConfirmed = true;
user.UserName = newUser.Email;
user.UserNo = newUser.UserNo;
user.FirstName = newUser.FirstName;
user.LastName = newUser.LastName;
user.Gender = newUser.Gender;
user.UserCategory = newUser.UserCategory;
user.ZoneInfo = newUser.ZoneInfo;
userManager.CreateAsync(user, "Password123!").Wait();
userManager.AddClaimAsync(user, new Claim("UserCategory", user.UserCategory)).Wait();
foreach (var role in newUser.UserRoles)
{
if (!roleManager.RoleExistsAsync(role).GetAwaiter().GetResult())
{
UserRole userRole = new UserRole();
userRole.Id = new Guid();
userRole.Name = role;
roleManager.CreateAsync(userRole).Wait();
}
userManager.AddToRoleAsync(user, role).Wait();
userManager.AddClaimAsync(user, new Claim(JwtClaimTypes.Role, role)).Wait();
}
}
}
}
}
}
Excerpts of startup.cs file of the resource server is as follows
public class Startup
{
private IHostingEnvironment _env { get; set; }
public Startup(IHostingEnvironment env)
{
_env = env;
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsEnvironment("Development"))
{
// This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
builder.AddApplicationInsightsSettings(developerMode: true);
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
//private static void InitializeDbTestData(IApplicationBuilder app)
//{
// using (var scope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
// {
// scope.ServiceProvider.GetRequiredService<ReportBookDbContext>().Database.Migrate();
// }
//}
// This method gets called by the runtime. Use this method to add services to the container
public void ConfigureServices(IServiceCollection services)
{
var folderForKeyStore = Configuration["Data:keystore:KeyStoreFolderWhichIsBacked"];
var cert = new X509Certificate2(Path.Combine(_env.ContentRootPath, "reportbook.auth.pfx"), "");
services.AddDataProtection()
.SetApplicationName("ReportBook")
.ProtectKeysWithDpapiNG("CERTIFICATE=Hashid:" + cert.Thumbprint,flags: DpapiNGProtectionDescriptorFlags.None);
services.AddDbContext<ReportBookDbContext>(options =>
options.UseSqlServer(Configuration["Data:ReportBookDbConnection:connectionString"],
b => b.MigrationsAssembly("ReportBook.Resource")));
// Add framework services.
services.AddCors();
services.AddApplicationInsightsTelemetry(Configuration);
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
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, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseDeveloperExceptionPage();
app.UseStatusCodePagesWithReExecute("/error");
if (env.IsDevelopment())
{
//InitializeDbTestData(app);
}
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
IdentityServerAuthenticationOptions identityServerValidationOptions = new IdentityServerAuthenticationOptions
{
Authority = "https://localhost:44354/",
ScopeName = "resource_server",
ScopeSecret = new IdentityServer4.Models.Secret("scopeSecret".Sha256()).ToString(),
AutomaticAuthenticate = true,
SupportedTokens = SupportedTokens.Both,
AutomaticChallenge = true
};
app.UseIdentityServerAuthentication(identityServerValidationOptions);
app.UseApplicationInsightsRequestTelemetry();
app.UseApplicationInsightsExceptionTelemetry();
app.UseExceptionHandler(
builder =>
{
builder.Run(
async context =>
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false);
}
});
});
app.UseCors(builder =>
builder.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
app.UseMvc();
}
}
Below is the an excerpt of the the controller whose method I am trying to reach
[HttpGet("GetInstitutions")]
//[Authorize]
public IActionResult GetInstitutions([FromQuery]InstitutionSearchQry model)
{
var authorisation = Request.Headers["Authorization"];
bool auth = User.Identity.IsAuthenticated;
IEnumerable<Institution> _institutions = null;
string userCategory = User.Claims.Where(a => a.Type == "UserCategory").Select(a => a.Value).FirstOrDefault().ToString();
string zoneInfo = User.Claims.Where(a => a.Type == "ZoneInfo").Select(a => a.Value).FirstOrDefault().ToString();
string userNo = User.Claims.Where(a => a.Type == "UserNo").Select(a => a.Value).FirstOrDefault().ToString();
bool admin = User.IsInRole("Admin");
List<Student> students = new List<Student>();
//Institution institution = _institutionService.Find(a => a.InstitutionID == zoneInfo);
var pagination = Request.Headers["Pagination"];
if (!string.IsNullOrEmpty(pagination))
{
string[] vals = pagination.ToString().Split(',');
int.TryParse(vals[0], out page);
int.TryParse(vals[1], out pageSize);
}
switch (userCategory)
{
case "Guardian":
{
students = _guardianService.GetStudents(userNo).ToList();
_institutions = _admissionService.GetInstitutions(students.Select(a => a.StudentID).ToList(),model.StartYear,model.EndYear, s => s.Term.AcademicYear.Institution.UniversityInstitutes.Select(a => a.University));
}
break;
case "Student":
{
_institutions = _admissionService.GetInstitution(userNo,s=>s.Term.AcademicYear.Institution.UniversityInstitutes.Select(a=>a.University));
}
break;
default:
{
_institutions = _institutionService.GetInstitutions(a => a.AdministrativeStructure.ZoneInfo == zoneInfo && a.Level.LevelName==model.Level, page, pageSize, out totalCount, s => s.AdministrativeStructure, s => s.Level,s=>s.UniversityInstitutes.Select(a=>a.University));
}
break;
}
if (!String.IsNullOrEmpty(model.Level) && model.Level != "myschool")
{
_institutions = _institutions.Where(a => a.Level.LevelName == model.Level);
}
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
Response.AddPagination(page, pageSize, totalCount,totalPages);
Response.AddIdentityInfo(userCategory, admin, userNo, zoneInfo);
IEnumerable<InstitutionDataViewModel> _instDataModel = Mapper.Map<IEnumerable<Institution>, IEnumerable<InstitutionDataViewModel>>(_institutions);
return new OkObjectResult(_instDataModel);
}
The following is the angular 2 code from where the call is made to the resource server
#Injectable()
export class InstitutionService {
private resourceApiUrl: string;
private headers: Headers;
private storage: any;
private actionUrl: string;
public totalItems: number;
constructor(private _http: Http,
private itemsService: ItemsService,
private _configuration: Configuration,
private _router: Router,
private _authService: AuthService) {
this.resourceApiUrl = `${_configuration.resourceServer}api/Institution/`;
}
private SetHeaders(page?: number, itemsPerPage?: number) {
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
this.headers.append('Accept', 'application/json');
if (page != null && itemsPerPage != null) {
this.headers.append('Pagination', page + ',' + itemsPerPage);
}
var token = this._authService.GetToken();
if (token !== "") {
let tokenValue = 'Bearer ' + token;
console.log("tokenValue:" + tokenValue);
this.headers.append('Authorization', tokenValue);
}
}
public GetInstitutions = (InstitutionSearchQry?: any, page?: number, itemsPerPage?: number): Observable<PaginatedResult<IInstitution[]>> => {
this.SetHeaders(page, itemsPerPage);
var paginatedResult: PaginatedResult<IInstitution[]> = new PaginatedResult<IInstitution[]>();
let options = new RequestOptions({ headers: this.headers, body: '' });
if (!InstitutionSearchQry.level) {
this.actionUrl = "GetInstitutions";
} else {
this.actionUrl = "GetInstitutions/", InstitutionSearchQry;
}
return this._http.get(this.resourceApiUrl + this.actionUrl, options)
.map((res: Response) => {
//console.log(res.headers.keys());
paginatedResult.result = res.json();
if (res.headers.get("Pagination") != null) {
//var pagination = JSON.parse(res.headers.get("Pagination"));
var paginationHeader: Pagination = this.itemsService.getSerialized<Pagination>(JSON.parse(res.headers.get("Pagination")));
paginatedResult.pagination = paginationHeader;
}
if (res.headers.get("IdentityInfo") != null) {
var identityInfo: IdentityInfo = this.itemsService.getSerialized<IdentityInfo>(JSON.parse(res.headers.get("IdentityInfo")));
paginatedResult.identityInfo = identityInfo;
}
this.totalItems = paginatedResult.pagination.TotalItems;
return paginatedResult;
}).catch(this.handleError);
};
}
So basically the authorisation information provided at the AuthServer side is not reaching the resource server.
As one can see I have added the 'CORS' service in both files.
Use this plugin for chrome browser. get from here

Resources