CORS Error: LinkedIn Authentication; .NET Core 5 REST Api - reactjs

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..

Related

SignalR and React - Invocation canceled due to the underlying connection being closed

Good evening.
I am building a chat component using SignalR(5.0.1, I also tried an early version 3.0.0) and dotnet core 3.1.
The comment that any of the users makes can be sent to all the connected clients, and all the connected clients can see the comment. However, the client which sent the comment will disconnect, and an error is caught addComment() in the mobx store.
A client will lose its connection after sending a comment (the sender and the other clients can get the comment which has been processed from ChatHub). I don't know why the server decides to drop the connection and raise an error. I
The error:
[2021-01-10T22:38:14.314Z] Information: Connection disconnected.
adventureStore.ts:73 Error: Invocation canceled due to the underlying connection being closed.
at HubConnection.connectionClosed (HubConnection.ts:654)
at HttpConnection.HubConnection.connection.onclose (HubConnection.ts:103)
at HttpConnection.stopConnection (HttpConnection.ts:488)
at WebSocketTransport.transport.onclose (HttpConnection.ts:410)
at WebSocketTransport.close (WebSocketTransport.ts:135)
at WebSocket.webSocket.onclose (WebSocketTransport.ts:97)
Here are the codes:
startup:
public class Startup
{
// only showing the codes related to problem
services.AddSignalR();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ValidateAudience = false,
ValidateIssuer = false
};
opt.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/chat")))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseMiddleware<ErrorHandlingMiddleware>();
if (env.IsDevelopment())
{
// app.UseDeveloperExceptionPage();
}
// app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<ChatHub>("/chat");
});
}
}
ChatHub.cs:
public class ChatHub : Hub
{
private readonly IMediator _mediator;
public ChatHub(IMediator mediator)
{
_mediator = mediator;
}
public async Task SendComment(Create.Command command)
{
var username = Context.User?.Claims?.FirstOrDefault(x => x.Type ==
ClaimTypes.NameIdentifier)?.Value;
command.Username = username;
var comment = await _mediator.Send(command);
// clients can successfully receive the comment and load it to their mobx store
await Clients.All.SendAsync("ReceiveComment", comment);
}
}
the mobx store where the connection is built:
#observable comments: IComment[] | null = null;
#observable.ref hubConnection: HubConnection | null = null;
#action createHubConnection = () => {
this.hubConnection = new HubConnectionBuilder()
.withUrl("http://localhost:5000/chat", {
accessTokenFactory: () => this.rootStore.commonStore.token!,
})
.configureLogging(LogLevel.Information)
// i have to do this for now. Need to fix that disconnecting right
// after Clients.All.SendAsync("ReceiveComment", comment); in ChatHub.cs
// .withAutomaticReconnect([0, 0, 10000])
.build();
this.hubConnection
.start()
.then(() => console.log(this.hubConnection!.state))
.catch((error) => console.log("Error establishing connection: ", error));
this.hubConnection.on("ReceiveComment", (comment) => {
runInAction(() => {
if (this.comments == null) {
this.comments = new Array<IComment>();
}
this.comments.push(comment);
});
});
};
#action stopHubConnection = () => {
this.hubConnection!.stop();
};
#action addComment = async (values: any) => {
try {
await this.hubConnection!.invoke("SendComment", values);
} catch (error) {
console.log(error);
}
};
I have an Angular client facing same issues when connect to SignalR Hub in Asp.net Core 3.1. The applied NuGet package is "Microsoft.AspNetCore.SignalR.Core" version 1.1.0. After upgrade Asp.net Web API portal to .Net 5.0. The problem is resolved.
I've faced the same issue. I've use System.Text.Json as default json serializer.
To fix this issue i upgrade 'Microsoft.AspNetCore.SignalR.Protocols.Json' nuget to the valid version.
Who is using nginx for reverse proxy please update your nginx.config:
microsoft docs
These steps help me to resolve the same issue.

Getting "No authentication handler is registered for the scheme 'Windows'" error when trying Windows Authetication

I want to enable Windows Authentication for my IdentityServer hosted by IIS (in process). The doc from IdentityServer4 stated that "You trigger Windows authentication by calling ChallengeAsync on the Windows scheme" but it does say where and how. I was assuming it is in the Login function of the AccountController but it doesn't seem to work for me.
Here is the error I am getting when running my IdentityServer
But to my knowledge, I had registered the authentication for Windows scheme. Here is the ConfigurtionServices and Confiuration functions of the Startup.cs for the identityserver:
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true;
services.AddControllersWithViews();
var connstr = Configuration.GetConnectionString("DBConnection");
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connstr));
services.AddIdentity<ApplicationUser, IdentityRole>()
.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;
})
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connstr,
sql => sql.MigrationsAssembly(migrationsAssembly));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseSqlServer(connstr,
sql => sql.MigrationsAssembly(migrationsAssembly));
});
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
//services.AddIdentityServer().AddSigningCredential(
// new X509Certificate2(Path.Combine(_environment.ContentRootPath, "certs", "IdentityServer4Auth.pfx")));
// configures IIS in-proc settings
services.Configure<IISServerOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
// for Windows authentication
services.AddAuthentication(IISDefaults.AuthenticationScheme);
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
// use MVC
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
// use MVC
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
As you can see the very last line in ConfigurationServices function, I registered authentication handler for Windows by calling services.AddAuthentication(IISDefaults.AuthenticationScheme).
Here is the Login function in the AccountController.cs, in which I call ChallengeWindowsAsync - I thought this is how the IdentityServer4 Doc suggested to do but I am not sure. If it is not the right way, how should I correct it?
[HttpGet]
public async Task<IActionResult> Login(string returnUrl)
{
// trigger Windows authentication by calling ChallengeAsync
await ChallengeWindowsAsync(returnUrl);
// build a model so we know what to show on the login page
var vm = await BuildLoginViewModelAsync(returnUrl);
if (vm.IsExternalLoginOnly)
{
// we only have one option for logging in and it's an external provider
return RedirectToAction("Challenge", "External", new { scheme = vm.ExternalLoginScheme, returnUrl });
}
return View(vm);
}
And here is the ChallengeWindowsAsync function
private async Task<IActionResult> ChallengeWindowsAsync(string returnUrl)
{
// see if windows auth has already been requested and succeeded
var result = await HttpContext.AuthenticateAsync("Windows");
if (result?.Principal is WindowsPrincipal wp)
{
// we will issue the external cookie and then redirect the
// user back to the external callback, in essence, treating windows
// auth the same as any other external authentication mechanism
var props = new AuthenticationProperties()
{
RedirectUri = Url.Action("Callback"),
Items =
{
{ "returnUrl", returnUrl },
{ "scheme", "Windows" },
}
};
var id = new ClaimsIdentity("Windows");
// the sid is a good sub value
id.AddClaim(new Claim(JwtClaimTypes.Subject, wp.FindFirst(ClaimTypes.PrimarySid).Value));
// the account name is the closest we have to a display name
id.AddClaim(new Claim(JwtClaimTypes.Name, wp.Identity.Name));
// add the groups as claims -- be careful if the number of groups is too large
var wi = wp.Identity as WindowsIdentity;
// translate group SIDs to display names
var groups = wi.Groups.Translate(typeof(NTAccount));
var roles = groups.Select(x => new Claim(JwtClaimTypes.Role, x.Value));
id.AddClaims(roles);
await HttpContext.SignInAsync(
IdentityServerConstants.ExternalCookieAuthenticationScheme,
new ClaimsPrincipal(id),
props);
return Redirect(props.RedirectUri);
}
else
{
// trigger windows auth
// since windows auth don't support the redirect uri,
// this URL is re-triggered when we call challenge
return Challenge("Windows");
}
}

.net core identity token endpoint failing with api call from website but not from postman

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.

How to access [Authorize] controller actions using HttpClient with Bearer token? Getting 401 "The audience is invalid"

There are four clients within the application:
angular.application - resource owner
identity_ms.client - webapi app (.net core 2.1)
IdentityServer4 with AspNetIdentity
AccountController with shared actions to register users, reset password etc.
UserController with secured actions.
The Data action of the UserController has an [Authorize(Policy = "user.data")] attribute
ms_1.client - webapi app (.net core 2.1)
request.client - added specially to send requests from ms_1.client to identity_ms.client's UserController to get some user data.
I'm requesting clients using Postman:
http://localhost:identity_ms_port/connect/token to get access_token
http://localhost:ms_1_port/api/secured/action to get some secured data from ms_1
http://localhost:identity_ms_port/api/user/data to get some secured user data from identity_ms
Everything is working fine.
Also, ms_1 service has a secured action requesting http://localhost:identity_ms_port/api/user/data using System.Net.Http.HttpClient.
// identity_ms configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(/*cors options*/);
services
.AddMvc()
.AddApplicationPart(/*Assembly*/)
.AddJsonOptions(/*SerializerSettings*/)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<IISOptions>(iis =>
{
iis.AuthenticationDisplayName = "Windows";
iis.AutomaticAuthentication = false;
});
var clients = new List<Client>
{
new Client
{
ClientId = "angular.application",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "user.data.scope", "ms_1.scope", "identity_ms.scope" },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword
},
new Client
{
ClientId = "ms_1.client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "user.data.scope", "ms_1.scope" },
AllowedGrantTypes = GrantTypes.ClientCredentials
},
new Client
{
ClientId = "identity_ms.client",
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes =
{
"user.data.scope",
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
AllowedGrantTypes = GrantTypes.Implicit
},
new Client
{
ClientId = "request.client",
AllowedScopes = { "user.data.scope" },
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets =
{
new Secret("secret".Sha256())
}
}
};
var apiResources = new List<ApiResource>
{
new ApiResource("ms_1.scope", "MS1 microservice scope"),
new ApiResource("identity_ms.scope", "Identity microservice scope"),
new ApiResource("user.data.scope", "Requests between microservices scope")
};
var identityResources = new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
services
.AddAuthorization(options => options.AddPolicy("user.data", policy => policy.RequireScope("user.data.scope")))
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(identityResources)
.AddInMemoryApiResources(apiResources)
.AddInMemoryClients(clients);
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = "identity_ms.scope";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:identity_ms_port";
});
services.AddSwaggerGen(/*swagger options*/);
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<CustomMiddleware>();
app.UseIdentityServer();
app.UseAuthentication();
app.UseCors("Policy");
app.UseHttpsRedirection();
app.UseMvc(/*routes*/);
app.UseSwagger();
}
// ms_1.client configuration
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(/*cors options*/);
services
.AddMvc()
.AddJsonOptions(/*SerializerSettings*/)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Audience = "ms_1.scope";
options.RequireHttpsMetadata = false;
options.Authority = "http://localhost:identity_ms_port";
});
}
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<CustomMiddleware>();
app.UseAuthentication();
app.UseCors("Policy");
app.UseStaticFiles();
app.UseHttpsRedirection();
app.UseMvc(/*routes*/);
app.UseSwagger();
}
// ms_1.client action using HttpClient
[HttpPost]
public async Task<IActionResult> Post(ViewModel model)
{
//...
using (var client = new TokenClient("http://localhost:identity_ms_port/connect/token", "ms_1.client", "secret"))
{
var response = await client.RequestClientCredentialsAsync("user.data.scope");
if (response.IsError)
{
throw new Exception($"{response.Error}{(string.IsNullOrEmpty(response.ErrorDescription) ? string.Empty : $": {response.ErrorDescription}")}", response.Exception);
}
if (string.IsNullOrWhiteSpace(response.AccessToken))
{
throw new Exception("Access token is empty");
}
var udClient = new HttpClient();
udClient.SetBearerToken(response.AccessToken);
udClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var result = await udClient.GetAsync("http://localhost:identity_ms_port/api/user/data");
}
//...
}
I've tried the following:
To retrieve access_token from the request to ms_1 Authorization header and use it to access user/data.
To get new access_token to access user/data with it.
See public async Task<IActionResult> Post(ViewModel model) code within the code block.
In both cases, I've got the correct token which I can use to request both secured/action and user/data actions from Postman, but HttpClient is getting Unauthorized response (401).
Response headers screenshot
What am I doing wrong?
In your client code with HttpClient you are not requesting any scopes for the API therefore the token that is issued by Identity Server 4 will not contain the API as one of the audiences and then subsequently you will get 401 from API.
Change your token request to ask for the API scope as well.
var response = await client.RequestClientCredentialsAsync("user.data.scope ms_1.scope");

IdentityServer 4 - invaid_client error

I am new to Identity Server. I haven't configured it before. But I need it for a Project I am working on.
The API will be serving an Angular JS Client, iOS App and an Android App. We need to implement authentication and authorisation and custmer grant
Note: I am trying to configure Identity Server and my API in the same Web API project.
I have followed the documentation and configured Identity Server as the following:
In startup.cs, in ConfigureServices()
private readonly IConfiguration config;
private const string DEFAULT_CORS_POLICY = "localhost";
public Startup (IConfiguration config) => this.config = config;
public void ConfigureServices (IServiceCollection services) {
services.AddIdentityServer ()
.AddDeveloperSigningCredential ()
//.AddInMemoryApiResources(config.GetSection("ApiResources"))
.AddInMemoryApiResources (Config.GetApis ())
//.AddInMemoryClients(config.GetSection("Clients"))
.AddInMemoryClients (Config.GetClients ())
.AddInMemoryIdentityResources (Config.GetIdentityResources ())
//.AddInMemoryIdentityResources(config.GetSection("IdentityResources"))
.AddExtensionGrantValidator<WechatGrantValidator> ();
services.AddTransient<IUserCodeValidator, UserCodeValidator> ();
services.AddCors (options => {
options.AddPolicy (DEFAULT_CORS_POLICY, builder => {
builder.WithOrigins ("http://localhost:5202");
builder.AllowAnyHeader ();
builder.AllowAnyMethod ();
});
});
}
I implemented the interface IExtensionGrantValidator and register the extension grant
public class WechatGrantValidator : IExtensionGrantValidator {
private IUserCodeValidator validator;
public WechatGrantValidator (IUserCodeValidator validator) {
this.validator = validator;
}
public string GrantType => "wechat_grant";
public async Task ValidateAsync (ExtensionGrantValidationContext context) {
string userCode = context.Request.Raw.Get ("userCode");
var result = await validator.ValidateAsync (userCode);
if (result.IsError) {
context.Result = new GrantValidationResult (TokenRequestErrors.InvalidGrant);
return;
}
context.Result = new GrantValidationResult (result.UserId, GrantType);
return;
}
}
I have followed the documentation and configured client infos as the following
public static IEnumerable<Client> GetClients () {
return new Client[] {
new Client {
ClientId = "javascritpClient",
ClientName = "JavaScript Client",
AllowedGrantTypes = { "wechat_grant" },
AllowAccessTokensViaBrowser = true,
AllowedCorsOrigins = { "http://localhost:5202" },
AllowedScopes = { "api1" },
ClientSecrets = { new Secret ("secret".Sha256 ()) }
}
};
}
Now because I want to use it Angular JS, iOS and Android I want to just get the Access Token from the IdentityServer, and then use the Access Token for Authentication and Authorisation.
for this I am trying to access the /connect/token from a JS client
But I am getting an invalid_client error.
#Injectable()
export class OauthService {
private http: Http;
public constructor(http: Http) {
this.http = http;
}
public async getDiscoveryInfos(issuer: string): Promise<DiscoveryInfos> {
if (!issuer.endsWith('/')) {
issuer += '/';
}
issuer += '.well-known/openid-configuration';
return this.http.get(issuer).map(response => {
return response.json();
}).toPromise();
}
public async getToken(): Promise<any> {
const headers = new Headers({ "Content-Type": "application/x-www-form-urlencoded" });
const discovery = await this.getDiscoveryInfos('http://localhost:5200');
return this.http.post(discovery.token_endpoint, {
grant_type: 'wechat_grant',
userCode: 'userCodeAA',
client_id: 'javascritpClient',
client_secret: 'secret',
scope:'api1'
}, { headers: headers }).map(response => response.json()).toPromise();
}
}
http response infos
The server response "error":"invalid_client"
log infos
The error I get on the server side is 'No client identifier found':
1 - Why am I getting this error?
2 - As I need to get the Token programmatically in JS, I need to use /connect/token, am I correct on this? Am I on the correct path?
in ng2 use a method like bellow:
public Token(data: SigninModel): Observable<any> {
this.options = new RequestOptions({ headers: this.headers });
this.headers.append('Content-Type', 'application/x-www-form-urlencoded');
const url = this.urlBase + `connect/token`;
const param = new URLSearchParams();
param.set('grant_type', 'password');
param.set('client_Id', 'javascritpClient');
param.set('client_secret', 'secret');
param.set('scope', 'offline_access');
param.set('username', data.username);
param.set('password', data.password);
return this.http.post(url, `${param.toString()}`, this.options)
.map((response: Response) => {
return (response.json());
})
.catch(this.handleError);
}

Resources