the issue is that i have a project with .net core 3.1 and reactjs called tv-participants-lookup and after implementing and configuring serveridentity4 and when trying to register or login it returns an Invalid Scope error, hence the problem is as shown bellow in the .well-known/openid-configuration:
{
"issuer": "https://localhost:5001",
"jwks_uri": "https://localhost:5001/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://localhost:5001/connect/authorize",
"token_endpoint": "https://localhost:5001/connect/token",
"userinfo_endpoint": "https://localhost:5001/connect/userinfo",
"end_session_endpoint": "https://localhost:5001/connect/endsession",
"check_session_iframe": "https://localhost:5001/connect/checksession",
"revocation_endpoint": "https://localhost:5001/connect/revocation",
"introspection_endpoint": "https://localhost:5001/connect/introspect",
"device_authorization_endpoint": "https://localhost:5001/connect/deviceauthorization",
"frontchannel_logout_supported": true,
"frontchannel_logout_session_supported": true,
"backchannel_logout_supported": true,
"backchannel_logout_session_supported": true,
"scopes_supported": [
"openid",
"profile",
"API.ClientId",
"TV Participants lookupAPI", **<=======** you may notice the space between the words causing it to appear like three scopes when validating the scopes
"offline_access"
],
and this is my appsettings.json:
"IdentityServer": {
"Clients": {
"client1": {
"Profile": "IdentityServerSPA",
"AlowedScopes":"openid profile"
}
},
"Resources": {
"API.ClientId": {
"Profile": "API"
}
}
},
startup.cs as follows:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
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.AddAuthorization();
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.RoleClaimType = JwtClaimTypes.Role;
});
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddTransient<IProfileService, ProfileService>();
services.AddControllersWithViews();
services.AddRazorPages();
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
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");
}
});
}
So the question is from where is the openid-connect is getting that scope -"TV Participants lookupAPI"-from ?? and is there a way to remove or alter the scopes_supported ?
Figured it out as I had to change the project name and remove the spaces. Once the .csproj is named TV_Participants_lookupAPI the problem has been resolved.
Related
Technology Stack
Using .NET CORE React Template
1 IIS Website
Application Pool (v4 Integrated)
Port 80
Clicking on a Register Button, calls the Register Component.
Component within a useEffect(), calls "/login URL using Axios
C# Map("/login") is called a Challenge to Authenticate using LinkedIn
The CORS Error is then returned
Error Snapshot 1 of 5
Snapshot 2 of 5; Snapshot 3 of 5; Snapshot 4 of 5; Snapshot 5 of 5
React Code
const url = `/login`;
const headers = {
'Content-Type': 'text/html'
}
axios({
method: 'get',
url: url,
headers: headers
})
.then((response) => {...})
.catch((error: Error | AxiosError) => {...});
C# Code - Linked Authentication, Cookies, CORS Middleware
Start.cs - ConfigureServices()
public void ConfigureServices(IServiceCollection services)
{
#region AddAuthentication, AddLinkedIn, AddCookie
services.AddAuthentication()
.AddLinkedIn(o =>
{
IConfigurationSection linkedinAuthNSection =
Configuration.GetSection("Authentication:Linkedin");
o.ClientId = linkedinAuthNSection["ClientId"];
o.ClientSecret = linkedinAuthNSection["ClientSecret"];
})
.AddCookie(o =>
{
o.LoginPath = "/login";
o.LogoutPath = "/logout";
});
#endregion
#region Global CORS Policy Declaration
services.AddCors(o =>
{
o.AddDefaultPolicy(builder =>
builder.AllowAnyMethod()
.AllowAnyHeader()
.AllowAnyOrigin()
);
});
#endregion
services.AddControllersWithViews();
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "client-app/build";
});
}
Start.cs - Configure()
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
#region Map/login
app.Map("/login", builder =>
{
builder.Run(async context =>
{
var properties = new AuthenticationProperties() { RedirectUri = "/" };
await context.ChallengeAsync("LinkedIn", properties);
});
});
#endregion
#region Map/logout
app.Map("/logout", builder =>
{
builder.Run(async context =>
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
context.Response.Redirect("/");
});
});
#endregion
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = Path.Join(env.ContentRootPath, "client-app");
if (env.IsDevelopment())
{
spa.Options.StartupTimeout = TimeSpan.FromSeconds(240);
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
Finally, I got it working!
I have made 2 major changes, after trying to fathom, understand internalizing. Thanks to the yellow warning symbols ⚠ of Chrome Dev Tools, that let me to this article and change 1 of the solution.
Change 1
Applied the major snippets of the above example code to my React SPA .NET Core project
Dropped the Map Middleware (app.Map("/login") that allows branching of the pipeline path.
In favor for a .NET Controller/Action.
However more specifically, just Action since the "/login" is added to the path, of the URL, which makes it difficult to accept the successful sign-in.
Change 2
Dropped Axios call only for the Authentication UI Interaction, since LinkedIn, does not support it. LinkedIn OAuth redirect login returning "No 'Access-Control-Allow-Origin' header is present on the requested resource" error
In favour of using HTML href.
Authentication.cs
//[Route("[controller]/[action]")]
[Route("[action]")]
public class AuthenticationController : Controller
{
[HttpGet]
public IActionResult Register(string authType = "LinkedIn")
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/" });
}
[HttpGet]
public IActionResult Login(string authType = "LinkedIn")
{
return Challenge(new AuthenticationProperties() { RedirectUri = "/" });
}
[HttpGet]
public IActionResult Logout()
{
return SignOut();
}
Start.cs ConfigureServices()
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = "LinkedIn";
})
.AddCookie()
.AddOAuth("LinkedIn", o =>
{
o.CorrelationCookie.HttpOnly = true;
o.CorrelationCookie.SameSite = SameSiteMode.Lax;
var linkedInSection = Configuration.GetSection("Authentication:LinkedIn");
o.ClientId = linkedInSection.GetSection("ClientId").Get<string>();
o.ClientSecret = linkedInSection.GetSection("ClientSecret").Get<string>();
o.CallbackPath = new PathString(linkedInSection.GetSection("CallbackPath").Get<string>());
o.AuthorizationEndpoint = linkedInSection.GetSection("AuthorizationEndpoint").Get<string>();
o.TokenEndpoint = linkedInSection.GetSection("TokenEndpoint").Get<string>();
o.UserInformationEndpoint = linkedInSection.GetSection("UserInformationEndpoint").Get<string>();
o.Scope.Add("r_liteprofile");
o.Scope.Add("r_liteprofile");
o.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var json = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
context.RunClaimActions(json.RootElement);
}
};
});
}
Start.cs Configure()
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
}
In your Chrome browser, try installing an extension called Access control Allow Origin from the Chrome web store . And Open the Options or settings page of that extension and type your localhost address in the textbox like this : https://localhost:80 in ur case
I had faced this issue once and this extension worked for me..
When i'm trying to sign-in with google sign-in from blazor webassembly, the requested claims and the claims returned from identity server is not matching (see output)
BackEnd/Config.cs
namespace BackEnd
{
public static class ServerConfiguration
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource("roles", "User roles", new List<string> { "role" })
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("protectedScope", "Protected Scope")
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client()
{
ClientId = <confidential>,
ClientName = "client 1",
RequireClientSecret = false,
RequirePkce = true,
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
AllowOfflineAccess = true,
AllowedScopes = new List<string>{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"protectedScope"
}
},
new Client()
{
ClientId = "blazor",
ClientName = "oidcUser",
RequireClientSecret = false,
RequirePkce = true,
RequireConsent = true,
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
AllowOfflineAccess = true,
AlwaysIncludeUserClaimsInIdToken = true,
AllowedScopes = new List<string>{
"openid",
"email",
"profile",
"protectedScope"
}
}
};
public static List<TestUser> TestUsers {
get
{
TestUser user1 = new TestUser()
{
SubjectId = "2f47f8f0-bea1-4f0e-ade1-88533a0eaf57",
Username = "John",
Claims = new List<Claim>()
{
new Claim("role", "SignedInUser"),
new Claim("email", "johnsmith#gmail.com"),
new Claim("picture", "https://www.google.com/url?sa=i&url=https%3A%2F%2Fwww.business2community.com%2Fsocial-media%2Fimportance-profile-picture-career-01899604&psig=AOvVaw2LC5T-WZMYnHD9I7PeK7lT&ust=1615219065948000&source=images&cd=vfe&ved=2ahUKEwip1caGxp7vAhV1NbcAHd_2BFwQjRx6BAgAEAc")
}
};
List<TestUser> testUsers = new List<TestUser>();
testUsers.Add(user1);
return testUsers;
}
}
}
}
BackEnd/Startup.cs
namespace BackEnd
{
public class Startup
{
public IWebHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
private string _clientId = null;
private string _clientSecret = null;
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
Environment = environment;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
var cert = new X509Certificate2(Path.Combine(".", "IdsvCertificate.pfx"), "YouShallNotPass123");
_clientId = Configuration["OAuth:ClientId"];
_clientSecret = Configuration["OAuth:ClientSecret"];
services.AddControllersWithViews();
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = false)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddTransient<IProfileService, ProfileService>();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
options.UserInteraction = new UserInteractionOptions() { LoginUrl = "/Account/Login", LogoutUrl = "/Account/Logout" };
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddProfileService<ProfileService>()
.AddAspNetIdentity<ApplicationUser>();
builder.AddSigningCredential(cert);
// builder.AddDeveloperSigningCredential();
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = _clientId;
options.ClientSecret = _clientSecret;
options.ClaimActions.MapJsonKey("picture", "picture", "url");
options.SaveTokens = true;
})
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.Authority = "https://accounts.google.com";
options.RequireHttpsMetadata = true;
options.ResponseType = "code";
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("openid");
options.ClientId = _clientId;
options.ClientSecret = _clientSecret;
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.ClaimActions.MapJsonKey("picture", "picture", "url");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
ValidateIssuer = true
};
});
services.AddAuthorization();
services.AddGrpc();
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true });
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<UserService>();
endpoints.MapDefaultControllerRoute().RequireAuthorization();
});
}
}
}
BackEnd/ProfileService.cs
public class ProfileService : IProfileService
{
public ProfileService()
{
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var roleClaims = context.Subject.FindAll(JwtClaimTypes.Role);
context.IssuedClaims.AddRange(roleClaims);
await Task.CompletedTask;
}
public async Task IsActiveAsync(IsActiveContext context)
{
await Task.CompletedTask;
}
}
FrontEnd/Program.cs
namespace FrontEnd
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient()
{ BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped(services =>
{
var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
var channel = GrpcChannel.ForAddress("https://localhost:5000", new GrpcChannelOptions
{
HttpHandler = httpHandler
});
return new Greeter.GreeterClient(channel);
});
builder.Services.AddScoped(services =>
{
var baseAddressMessageHandler = services.GetRequiredService<BaseAddressAuthorizationMessageHandler>();
baseAddressMessageHandler.InnerHandler = new HttpClientHandler();
var httpHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, new HttpClientHandler());
var channel = GrpcChannel.ForAddress("https://localhost:5000", new GrpcChannelOptions
{
HttpHandler = httpHandler
});
return new User.UserClient(channel);
});
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("Authentication:Google", options.ProviderOptions);
options.UserOptions.RoleClaim = "SignedInUser";
}).AddAccountClaimsPrincipalFactory<CustomUserFactory>();
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
await builder.Build().RunAsync();
}
}
FrontEnd/wwwroot/appsettings.json
{
"Authentication":{
"Google": {
"Authority": "https://localhost:5000",
"ClientId": <confidential>,
"ClientSecret": "2fxc9srOe8QsRBnhzLIa1pF0",
"DefaultScopes": [
"email",
"profile",
"openid"
],
"PostLogoutRedirectUri": "https://localhost:5001/",
"RedirectUri": "https://localhost:5001/authentication/login-callback",
"ResponseType": "code"
},
}
This is the output from IdentityServer when i'm trying to sign-in
[18:34:28 Debug] IdentityServer4.Validation.TokenValidator
Calling into custom token validator: IdentityServer4.Validation.DefaultCustomTokenValidator
[18:34:28 Debug] IdentityServer4.Validation.TokenValidator
Token validation success
{"ClientId": null, "ClientName": null, "ValidateLifetime": true, "AccessTokenType": "Jwt", "ExpectedScope": "openid", "TokenHandle": null, "JwtId": "8E6167D64F8FEA2FF6D12D17A1CEEBFE", "Claims": {"nbf": 1615548868, "exp": 1615552468, "iss": "https://localhost:5000", "aud": "https://localhost:5000/resources", "client_id": "499675830263-ldcg4fm7kcbjlt48tpaffqdbfnskmi8v.apps.googleusercontent.com", "sub": "81c306df-c1f0-4714-964f-2459b670429e", "auth_time": 1615548849, "idp": "oidc", "jti": "8E6167D64F8FEA2FF6D12D17A1CEEBFE", "sid": "0BF0BA4CA6BD3DE8D158A426A70A91E0", "iat": 1615548868, "scope": ["openid", "profile", "email", "role"], "amr": "external"}, "$type": "TokenValidationLog"}
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Creating userinfo response
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Scopes in access token: openid profile email role
[18:34:28 Debug] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Requested claim types: sub name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at email email_verified role
[18:34:28 Information] IdentityServer4.ResponseHandling.UserInfoResponseGenerator
Profile service returned the following claim types: sub name preferred_username
[18:34:28 Debug] IdentityServer4.Endpoints.UserInfoEndpoint
End userinfo request
[18:34:29 Debug] IdentityServer4.Hosting.EndpointRouter
Request path /connect/checksession matched to endpoint type Checksession
[18:34:29 Debug] IdentityServer4.Hosting.EndpointRouter
Endpoint enabled: Checksession, successfully created handler: IdentityServer4.Endpoints.CheckSessionEndpoint
[18:34:29 Information] IdentityServer4.Hosting.IdentityServerMiddleware
Invoking IdentityServer endpoint: IdentityServer4.Endpoints.CheckSessionEndpoint for /connect/checksession
[18:34:29 Debug] IdentityServer4.Endpoints.CheckSessionEndpoint
Rendering check session result
From the output, the requested claims and the claims that's returned by the profile service is not matching
From the image, there should be a name after the "hello"
The image is similar except now it shows the name. IdentityServer4 is working fine, it shows everything as i expected
By default, only these claims are mapped from the ID-Token:
options.ClaimActions.MapUniqueJsonKey("sub", "sub");
options.ClaimActions.MapUniqueJsonKey("name", "name");
options.ClaimActions.MapUniqueJsonKey("given_name", "given_name");
options.ClaimActions.MapUniqueJsonKey("family_name", "family_name");
options.ClaimActions.MapUniqueJsonKey("profile", "profile");
options.ClaimActions.MapUniqueJsonKey("email", "email");
To get additional claims mapped, we have to write like:
options.ClaimActions.MapUniqueJsonKey("website", "website");
options.ClaimActions.MapUniqueJsonKey("gender", "gender");
options.ClaimActions.MapUniqueJsonKey("birthdate", "birthdate");
In AddOpenIDConnect. This is called Claims transformation if you want to google for it.
Getting the subjected error , I have no clue on the reason for this, did lot of research in google but
answers not working out for my case. Service is in Springboot and it is giving correct result with postman where as my react application it is failing. Any help will be appreciated. see my code..
getSites = () => {
fetch("http://localhost:8080/xyz/sites", {
"method": "GET",
"headers": {
'Content-Type':'application/json',
'Access-Control-Allow-Origin:':'http://localhost:8080',
'accept':'application/json',
'content-type':'application/json'
}
})
.then(response => response.json())
.then(response => {
this.setState({
sites: response
})
})
.catch(err => {
console.log(err);
});
return this.state.sites;
}
And the response json in postman is..
[
{
"parkDimKey": "12345",
"parkName": "Site 1"
},
{
"parkDimKey": "67890",
"parkName": "Site 2"
}
]
Spring boot Service is..
#RestController
public class SiteDataController {
#Autowired
SiteDataDao siteDataDao;
#RequestMapping("/sites")
public List<SiteData> getSiteData(){
return siteDataDao.getSiteData();
}
}
And the main application..
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/xyz/sites").allowedOrigins("http://localhost:8080");
}
};
}
}
I had a similar problem, turns out to be a simple typo. I removed a colon symbol from header name and it worked:
'Access-Control-Allow-Origin:':'http://localhost:8080',
'Access-Control-Allow-Origin':'http://localhost:8080',
after searching for two days to trying and identify where I am going wrong, I have accepted that I need some help to point me in the right direction.
I'm at the really early stages of working with Identity server, still simply using inMemory clients and scopes, just to get my head around what is happening and how it all links together.
I am trying to return a list of custom claims to my angular application from Identity server, but I am failing. I've tried extending IProfileService, which successfully adds the custom claim but it removes the other claims, that I defined in my TestUser
With MyProfileService registered
Without MyProfileService registered
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(Resources.GetApiResources())
.AddInMemoryIdentityResources(Resources.GetIdentityResources())
.AddInMemoryClients(Clients.Get())
.AddTestUsers(Users.Get())
.AddDeveloperSigningCredential();
//services.AddTransient<IProfileService, MyProfileService>();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
#if DEBUG
app.UseDeveloperExceptionPage();
#endif
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
MyProfileService.cs
public class MyProfileService : IProfileService
{
public MyProfileService()
{
}
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// Issue custom claim
context.IssuedClaims.Add(new System.Security.Claims.Claim("TenantId", "123456"));
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.CompletedTask;
}
}
Resources.cs
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource> {
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email(),
new IdentityResource {
Name = "role",
UserClaims = new List<string> {"role"}
},
new IdentityResource
{
Name = "tenant.info",
DisplayName = "Tenant Information",
UserClaims = new List<string>
{
"tenantid",
"subscriptionid"
}
}
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource> {
new ApiResource("api1", "api1")
};
}
Users.cs
public static List<TestUser> Get()
{
return new List<TestUser> {
new TestUser {
SubjectId = "5BE86359-073C-434B-AD2D-A3932222DABE",
Username = "scott",
Password = "password",
Claims = new List<Claim>
{
new Claim("tenantid", "123456"),
new Claim(JwtClaimTypes.Name, "Scott xxxxx"),
new Claim(JwtClaimTypes.GivenName, "Scott"),
new Claim(JwtClaimTypes.FamilyName, "xxxxx"),
new Claim(JwtClaimTypes.Email, "Scottxxxxx#email.com"),
new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
new Claim(JwtClaimTypes.Address, #"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json)
}
}
};
}
Clients.cs
public static IEnumerable<Client> Get()
{
return new List<Client> {
new Client {
ClientId = "angular_spa",
ClientName = "Angular 4 Client",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowedScopes = new List<string> {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
RedirectUris = new List<string> { "http://localhost:4200/admin/loggedin" },
PostLogoutRedirectUris = new List<string> { "http://localhost:4200/admin/loggedout" },
AllowedCorsOrigins = new List<string> { "http://localhost:4200" },
AllowAccessTokensViaBrowser = true
}
};
}
EDIT:
Additional failed solutions
Add default behaviour to MyProfileService (as suggested by the answer from Ruard van Elburg)
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.AddRequestedClaims(context.Subject.Claims);
context.IssuedClaims.Add(new System.Security.Claims.Claim("tenantId", "123456"));
}
Result in client: shows the tenantId but no other claims that I set on my TestUser
profile:
amr: ["pwd"]
auth_time: 1553024858
idp: "local"
sid: "34f36d1c0056ad3d65d1671e339e73aa"
sub: "5BE86359-073C-434B-AD2D-A3932222DABE"
tenantId: "123456"
__proto__: Object
Add subject.claims to issedClaims
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
context.IssuedClaims.Add(new System.Security.Claims.Claim("tenantId", "123456"));
context.IssuedClaims.AddRange(context.Subject.Claims);
}
Result in client: shows the tenantId and name (which is referring to the username) but no claims that I set on my TestUser
profile:
amr: ["pwd"]
auth_time: 1553025311
idp: "local"
name: "scott"
sid: "831a89053b54f3df7c9ca1bca92e1e10"
sub: "5BE86359-073C-434B-AD2D-A3932222DABE"
tenantId: "123456"
Define custom identity resources (resources docs)
I removed MyProfileService and added
public static IEnumerable<IdentityResource> GetIdentityResources()
{
var customProfile = new IdentityResource(
name: "custom.profile",
displayName: "Custom profile",
claimTypes: new[] {
"name",
"given_name",
"family_name",
"email",
"email_verified",
"website",
"address",
"status",
"tenantid" });
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
customProfile
};
}
Result in client I do not see all the claim types
profile:
amr: ["pwd"]
auth_time: 1553026892
family_name: "FamilyName"
given_name: "Scott givenName"
idp: "local"
name: "Scott name"
sid: "47ae7f9b5240742e2b2b94a739bed5fa"
sub: "5BE86359-073C-434B-AD2D-A3932222DABE"
website: "http://scott.com"
The problem is that you've removed default behaviour. So you'll need to restore that by adding the following line to your profile service (which is present in the DefaultProfileService):
context.AddRequestedClaims(context.Subject.Claims);
But it is not necessary to implement your own IProfileService. In this case you can suffice by configuring the scope for the client:
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"tenant.info",
"api1"
},
And requesting the scope in the client:
options.Scope.Add("tenant.info");
This should be enough to include the tenantId claim.
I'm setting up an oauth2 server with .net core, and I have it working as long as I use postman. But when I try to make the api call from a web app (in this instance, angularJS), I get an error "Invalid object name 'Clients'
Here's my Startup.cs
using System.Linq; using System.Reflection; using System.Security.Claims; using System.Threading.Tasks; using AuthCore.Models; using IdentityServer4.EntityFramework.DbContexts; using IdentityServer4.EntityFramework.Mappers; using IdentityServer4.EntityFramework; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Mvc; using AuthCore.Repositories; using AuthCore.Services; using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace AuthCore {
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
const string connectionString = #"redacted";
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddCors(options =>
{
options.AddPolicy("mysite",
builder =>
{
builder.AllowAnyHeader();
builder.AllowAnyOrigin();
builder.AllowAnyMethod();
builder.AllowCredentials();
});
});
services.AddScoped<AuthRepository>();
services.AddScoped<UserRepository>();
services.AddScoped<EmailCodeRepository>();
services.AddScoped<EmailRepository>();
services.AddScoped<TokenRepository>();
services.AddScoped<PasswordResetRepository>();
services.AddScoped<EmailCodeService>();
services.AddScoped<EmailService>();
services.AddScoped<UserService>();
services.AddScoped<TokenService>();
services.AddDbContext<ApplicationDbContext>(builder =>
builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)));
services.AddIdentity<ApplicationUser, IdentityRole>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
}).AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
services.AddIdentityServer()
.AddOperationalStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
.AddAspNetIdentity<ApplicationUser>()
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryClients(Config.GetClients())
.AddDeveloperSigningCredential();
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001/";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
// app.UseDeveloperExceptionPage();
}
// InitializeDbTestData(app);
app.UseCors("mysite");
app.UseAuthentication();
app.UseIdentityServer();
app.UseMvc();
}
} }
AngularJS Service call
login(loginData) {
this.deleteLoginInfo();
var data = {
client_id: 'client',
client_secret: 'secret',
grant_type: 'password',
scope: 'api1',
password: loginData.password,
username: loginData.userName
};
var deferred = this.$q.defer();
let loginEndpoint = ENV.authDomain;
let headers = {
headers:
{
'Content-Type': 'application/x-www-form-urlencoded'
}
}
this.$http.post(loginEndpoint + '/connect/token', this.$httpParamSerializer(data), headers).then((response) => {
this.localStorageService.set('authorizationData', { token: response.data.access_token });
this._authentication.isAuth = true;
this._authentication.userName = loginData.userName;
deferred.resolve(response.data);
}).catch((err, status) => {
deferred.reject(err);
});
return deferred.promise;
};
I've also tried
var data = `grant_type=password&username=${data.username}&password=${data.password}&client_id=${data.client_id}&client_secret=secret&scope=api1`;
Considering neither work, and postman does, I'm lead to believe something is up with the server side. It feels like a cors type issue, but I have cors enabled for all and the error isn't cors. I've tried searching around for the error described above but nothing seems relevant to my issue.