Supporting Core Identity Roles in IdentityServer4 - 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();
});
}
}

Related

Ocelot Identity Server: message: Request for authenticated route {api-path} by was unauthenticated

I am getting the following error in a docker container. I am trying to create api gateway using ocelot and authentication by identity server.
message: Client has NOT been authenticated for {api-path} and pipeline error set. Request for authenticated route {api-path} by was unauthenticated
Error Code: UnauthenticatedError Message: Request for authenticated route {api-path} by was unauthenticated errors found in ResponderMiddleware. Setting error response for request path:{api-path}, request method: GET
I can see that the client name is empty there but not sure why it is happening.
Below is the code in my api gateway
IdentityModelEventSource.ShowPII = true;
var authenticationProviderKey = "IdentityApiKey";
services.AddAuthentication().AddJwtBearer(authenticationProviderKey, x =>
{
x.Authority = "http://identityserver";
x.RequireHttpsMetadata = false;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
Ocelot-config.json //added the authentication parameters
"AuthenticationOptions": {
"AuthenticationProviderKey": "IdentityApiKey",
"AllowedScopes": [ "AdminService" ]
},
Code in my microservice
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("Bearer").AddJwtBearer("Bearer", options =>
{
options.Authority = "http://identityserver";
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
........
}
public void Configure(...)
{
....
app.UseAuthentication();
app.UseAuthorization();
....
}
My IdentityConfig in identity server
public class IdentityConfig
{
public static IEnumerable<Client> Clients => new Client[]
{
new Client
{
ClientId = "Consumer_01",
ClientName = "Consumer_01",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> { new Secret("Consumer01".Sha256()) },
AllowedScopes = new List<String> { "consumerservice" }
},
new Client
{
ClientId = "Consumer_02",
ClientName = "Consumer_02",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> { new Secret("Consumer02".Sha256()) },
AllowedScopes = new List<String> { "consumerservice" }
},
new Client
{
ClientId = "Provider_01",
ClientName = "Provider_01",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> { new Secret("Provider01".Sha256()) },
AllowedScopes = new List<String> { "providerservice" }
},
new Client
{
ClientId = "Provider_02",
ClientName = "Provider_02",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> { new Secret("Provider02".Sha256()) },
AllowedScopes = new List<String> { "providerservice" }
},
new Client
{
ClientId = "Provider_03",
ClientName = "Provider_03",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> { new Secret("Provider03".Sha256()) },
AllowedScopes = new List<String> { "providerservice" }
},
new Client
{
ClientId = "Provider_04",
ClientName = "Provider_04",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> { new Secret("Provider04".Sha256()) },
AllowedScopes = new List<String> { "providerservice" }
},
new Client
{
ClientId = "Admin_01",
ClientName = "Admin_01",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = new List<Secret> { new Secret("Admin01".Sha256()) },
AllowedScopes = new List<String> { "AdminService" }
}
};
public static IEnumerable<ApiScope> ApiScopes => new ApiScope[]
{
new ApiScope("consumerservice", "Consumer Service"),
new ApiScope("providerservice", "Provider Service"),
new ApiScope("AdminService", "AdminService")
};
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource
{
Name = "admin",
UserClaims = new List<string> {"admin"}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new[]
{
new ApiResource
{
}
};
}
public static List<TestUser> TestUsers()
{
return new List<TestUser> {
new TestUser {
}
};
}
}
IdentityServer startup
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
services.AddIdentityServer()
.AddInMemoryClients(IdentityConfig.Clients)
.AddInMemoryIdentityResources(IdentityConfig.GetIdentityResources())
.AddInMemoryApiResources(IdentityConfig.GetApiResources())
.AddInMemoryApiScopes(IdentityConfig.ApiScopes)
.AddTestUsers(IdentityConfig.TestUsers())
.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseIdentityServer();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
I have tried many things but nothing seems to be worked. I only get 401 error.
Not sure if I was clear, but if you have anything, please help. Thank You.
add "ValidateIssuer = fasle" to "TokenValidationParameters" and it will worke fine
I had the same error, and my code has the same structure.
I was rerouting from http (ocelot url) when the solution I guess, to me at least, was to reroute from https.
Example: https://localhost:5001/rerouteDestination
Hope it would solve someone else's problem

invalid_request 400 bad request

This is my ConfigureServices method of my Identity Server project.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddOperationalStore(options =>
{
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30; // interval in seconds
})
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryClients(Config.GetClients())
.AddTestUsers(Config.GetUsers())
.AddProfileService<DefaultProfileService>();
}
When I follow the tutorial, the tutorial uses .AddProfileService<ProfileService>() but I could not find ProfileService. So I use DefaultProfileService. Could the error due to this?
and in my config.cs
public class Config
{
public static IEnumerable GetApiResources()
{
return new List
{
new ApiResource("myresourceapi", "My Resource API")
{
Scopes = {new Scope("apiscope")}
}
};
}
public static IEnumerable<Client> GetClients()
{
return new[]
{
// for public api
new Client
{
ClientId = "secret_client_id",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "apiscope" }
},
new Client
{
ClientId = "secret_user_client_id",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "apiscope" }
}
};
}
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "user",
Password = "user",
Claims = new[]
{
new Claim("roleType", "CanReaddata")
}
},
new TestUser
{
SubjectId = "2",
Username = "admin",
Password = "admin",
Claims = new[]
{
new Claim("roleType", "CanUpdatedata")
}
}
};
}
}
When I test it at Postman, I got the error invalid_request
I have tested another client id ClientId = "secret_client_id" and it works.
If you are using AddTestUser then you don't need to add a profile service as it adds one for you that works with the test users:
public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users)
{
builder.Services.AddSingleton(new TestUserStore(users));
builder.AddProfileService<TestUserProfileService>();
builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>();
return builder;
}
Found out that my problem is I issued a GET request which should be a POST request.

Why can't I use role authorization in my API controller

I created a react project with the react template in asp.net core with individual user accounts. I have looked around and found out I needed to add roles in the startup file and add a profile service, which seems to have half worked.
I can see the roles in my authorization token like this:
"role": [
"admin",
"bookkeeping" ],
But when I add the [Authorize(Roles = "admin")] tag to my controller the requests are now forbidden, even when i can see my token includes the role "admin".
What am I missing or doing wrong here?
This is the Profile service:
public class ProfileService : IProfileService
{
protected UserManager<ApplicationUser> mUserManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
mUserManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
ApplicationUser user = await mUserManager.GetUserAsync(context.Subject);
IList<string> roles = await mUserManager.GetRolesAsync(user);
IList<Claim> roleClaims = new List<Claim>();
foreach (string role in roles)
{
roleClaims.Add(new Claim(JwtClaimTypes.Role, role));
}
context.IssuedClaims.Add(new Claim(JwtClaimTypes.Name, user.UserName));
context.IssuedClaims.AddRange(roleClaims);
}
public Task IsActiveAsync(IsActiveContext context)
{
return Task.CompletedTask;
}
}
and this is my startup file:
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.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdministratorRole",
policy => policy.RequireRole("admin"));
});
services.AddTransient<IProfileService, ProfileService>();
services.AddControllersWithViews()
.AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
services.AddRazorPages();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.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.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
}
A small version of my API controller:
[Authorize(Roles = "admin")]
[Route("api/[controller]")]
public class SampleDataController : ControllerBase
{
private readonly ApplicationDbContext _db;
public SampleDataController(ApplicationDbContext db)
{
_db = db;
}
[HttpGet("[action]")]
public IEnumerable<Order> GetOrderList()
{
return _db.Order.ToList();
}
}
And my fetch method
async populateOrderList() {
const token = await authService.getAccessToken();
const response = await fetch('api/SampleData/GetOrderList', {
headers: !token ? {} : { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
this.setState({ orderList: data });
}
As you can see in my startup file I have also tried using a policy, with the tag [Authorize(Policy = "RequireAdministratorRole")] instead. Also it works fine when i just use [Authorize]
Edit: My api controllers are in the same projekt as my identity server.
Thanks for the help in advance.
change your default claimtype for role in startup like this:
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.RoleClaimType = JwtClaimTypes.Role;
});

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.

IdentityServer 4 with ASP.net Identity not working

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.

Resources