Using Signalr on Angular 5 and Asp.net WebApi - asp.net

I am actually creating a chat with angular 5 and signalR on an ASP.NET Framework API. I followed the documentation but it's still not work. Here is my hub:
public class ChatHub : Hub
{
public void Hello()
{
Clients.All.hello();
}
}
Here is my startup class:
app.Map("/signalr", map =>
{
// Setup the CORS middleware to run before SignalR.
// By default this will allow all origins. You can
// configure the set of origins and/or http verbs by
// providing a cors options with a different policy.
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
EnableJSONP = true
};
// Run the SignalR pipeline. We're not using MapSignalR
// since this branch already runs under the "/signalr"
// path.
map.RunSignalR(hubConfiguration);
});
and here is my angular part which create the hubconnection:
ngOnInit() {
this._hubConnection = new HubConnection('http://localhost:58525/signalr/hubs');
this._hubConnection
.start()
.then(() => console.log('Connection started!'))
.catch(err => console.log('Error while establishing connection :( : ' + err));
this._hubConnection.on('send', data => {
console.log(data);
});
}
I get this error:

If your ASP.NET page runs on another server, then your URL looks not correct.
https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-javascript-client#crossdomain
You have to connect to:
this._hubConnection = new HubConnection('http://localhost:58525/signalr');

Related

Keep getting 401 on authorization with SignalR

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

Getting lost configuring IdentityServer4 for SPA

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

Cannot connect to signalr hub from ionic app: Access to XMLHttpRequest ... has been blocked by CORS policy

I'm trying to develop a simple proof of concept for an ionic app sending and receiving signalr messages. I have a very simple signalr web app built in .net 4.5 that is succesfully sending and receiving messages from connected clients within the app host.
I am now trying to connect to this from an ionic app but I get the message
Access to XMLHttpRequest at 'http://localhost:59621/signalr/negotiate'
from origin 'http://localhost:8100' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: The
value of the 'Access-Control-Allow-Origin' header in the response must
not be the wildcard '*' when the request's credentials mode is
'include'.
when attempting to establish a connection to the signalr hub.
Any assistance is much appreciated.
.Net Code
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
// Any connection or hub wire up and configuration should go here
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration
{
EnableDetailedErrors = true,
};
map.RunSignalR(hubConfiguration);
});
}
}
ChatHub.cs
[HubName("ChatHub")]
public class ChatHub : Hub
{
public void Send(string name, string message)
{
// Call the addNewMessageToPage method to update clients.
Clients.All.addNewMessageToPage(name, message);
}
}
Ionic Code
import { Component } from '#angular/core';
import { NavController, DateTime, AlertController } from 'ionic-angular';
import { HubConnection, HubConnectionBuilder } from '#aspnet/signalr';
#Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
hubConnection: HubConnection;
name: string;
message: string;
messages: string[] = [];
constructor(public navCtrl: NavController, public alertCtrl: AlertController) {
}
ionViewDidLoad() {
}
ionViewWillEnter() {
let alert = this.alertCtrl.create({
title: 'Demo',
message: 'What is your name?',
inputs: [
{
name: 'Name',
placeholder: 'Name'
}
],
buttons: [
{
text: 'Enter',
handler: data => {
this.name = data.Name;
}
}
]
});
alert.present();
this.hubConnection = new HubConnectionBuilder().withUrl('http://localhost:59621/signalr').build();
this.hubConnection
.start()
.then(() => console.log('Connection started!'))
.catch(err => {
debugger;
console.log('Error while establishing connection :(')
});
this.hubConnection.on('addNewMessageToPage', (name: string, receivedMessage: string) => {
const text = `${name}: ${receivedMessage}`;
this.messages.push(text);
});
}
sendMessage() {
this.hubConnection
.invoke('send', this.name, this.message)
.then(() => this.message = '')
.catch(err => console.error(err));
}
}
Ionic Info
Ionic:
ionic (Ionic CLI) : 4.0.6
Ionic Framework : ionic-angular 3.9.3
#ionic/app-scripts : 3.2.1
Cordova:
cordova (Cordova CLI) : 8.1.0
Cordova Platforms : none
System:
NodeJS : v8.11.0 (C:\Program Files\nodejs\node.exe)
npm : 5.6.0
OS : Windows 10
Environment:
ANDROID_HOME : not set
The problem was that the signalr plugin for ionic required a .Net Core backend. I had been attempting to use a .Net Framework backend.
Your code is fine. issue of CORS related. So You should run as below steps
Create shortcut chrome on desktop and rename no-cors (Whatever name)
Right click on icon and goes to property
Next change target to "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir="D:/Chrome" and save.
Then run on this breowser definitely it works. thanks

How to enable CORS in SignalR only for a certain domain?

What I want to do is to allow connections from my example.com to signalr.example.com .
This works, but allows for all:
public void Configuration(IAppBuilder app)
{
app.Map("/signalr", map =>
{
map.UseCors(CorsOptions.AllowAll);
var hubConfiguration = new HubConfiguration { };
map.RunSignalR(hubConfiguration);
});
}
I found this suggestion:
app.UseCors(builder =>
{
builder.WithOrigins("https://example.com")
.AllowAnyHeader()
.WithMethods("GET", "POST")
.AllowCredentials();
});
But apparently it is for ASP.NET Core, my app.UseCors() accepts only a parameter of type CorsOptions.
How to enable CORS for a specific address and not all? Thanks.

Sending requests from spa to .netcore web api using adaljs always returns 401

I have a spa that is sending a get request to a web api.
When I dont have the authorize attribute, I am able to get the values (of course!). Adding the authorize attribute always returns a 401 response.
After scratching my head on this for 2 weeks straight, I feel like only SO gods can help.
I have the following questions:
What am I doing wrong?
Is there a better way to do this?
How can I log the incoming token on the server side?
(Just so I can validate it at jwt.io)
Assuming my keys, tenant, client(id) etc are properly set up,
My code on spa is like this:
'use strict';
angular.module('todoApp')
.controller('homeCtrl', ['$scope', '$http', 'adalAuthenticationService', '$location', function ($scope, $http, adalService, $location) {
$scope.apiData = [];
$scope.login = function () {
adalService.login().then(function () {
console.log('yay');
});
};
$scope.logout = function () {
adalService.logOut();
};
$scope.isActive = function (viewLocation) {
return viewLocation === $location.path();
};
$scope.getData = function () {
// #1: Set up ADAL
var authContext = new AuthenticationContext({
clientId: 'myclientid',
postLogoutRedirectUri: window.location
});
var user = authContext.getCachedUser();
if (user) {
console.log(user);
console.log('Signed in as: ' + user.userName);
} else {
console.log('Not signed in');
}
var tokenStored;
authContext.acquireToken(
'https://graph.windows.net',
function (error, token) {
// TODO: Handle error obtaining access token
if (error || !token) {
console.log('Error no token');
return;
}
console.log("token is:" + token);
tokenStored = token;
$http.get('https://localhost:44301/api/values', {
headers: { 'Authorization': 'Bearer ' + tokenStored, }
}).then(function (response) {
$scope.apiData = response.data;
console.log(response);
alert('Data recieved');
});
});
};
}]);
My Api Startup.cs looks like this:
// 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)
{
//ToDo: Implement Logger Factory
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// Shows UseCors with CorsPolicyBuilder.
// global policy - assign here or on each controller
app.UseCors("CorsPolicy");
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
// TokenValidationParameters = tokenValidationParameters
});
app.UseMvc();
}
My controller method looks like this
[Route("api/values")]
[Authorize]
[EnableCors("CorsPolicy")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IActionResult Get()
{
if (!HttpContext.User.Identity.IsAuthenticated)
{
var results = _interconnectCodesRepository.GetCodes();
return Ok(results);
}
else
{
return BadRequest();
}
}
}
}
Any suggestions or hints will be much appreciated.
Thanks
To get the token from .net core web API project, we can add AuthenticationFailed event like below:
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]),
Audience = Configuration["AzureAd:Audience"],
Events = new JwtBearerEvents
{
OnAuthenticationFailed= AuthenticationFailed
}
});
private Task AuthenticationFailed(AuthenticationFailedContext authenticationFailedContext)
{
Debug.WriteLine(authenticationFailedContext.Request.Headers["authorization"]);
return Task.FromResult(0);
}
The token you were acquiring using the resrouce https://graph.windows.net in the code is for the Azure Graph REST instead of your API. There is no need to acquire the token manually in the client for the SPA application. The ADAL library will acquire and append the token based on the resource automatically. We just only need to init which endpoints we want to request. Here is the js code for your reference:
var myApp = angular.module('myApp', ['AdalAngular']).config(['$httpProvider', 'adalAuthenticationServiceProvider', function ($httpProvider, adalProvider) {
//{Array} endpoints - Collection of {Endpoint-ResourceId} used for automatically attaching tokens in webApi calls.
var endpoints = {
"https://localhost:44327/": "https://adfei.onmicrosoft.com/ToGoAPI",
};
adalProvider.init(
{
instance: 'https://login.microsoftonline.com/',
tenant: 'adfei.onmicrosoft.com',
clientId: 'e2354bba-e915-4cb8-a48d-bcda101b8603',
extraQueryParameter: 'nux=1',
endpoints: endpoints,
},
$httpProvider
);
}])
myApp.controller('homeCtrl', ['$scope', '$http', 'adalAuthenticationService', '$location', 'toGoListSvc', function ($scope, $http, adalService, $location, toGoListSvc) {
$scope.double = function (value) { return value * 2; };
$scope.login = function () {
adalService.login();
};
$scope.logout = function () {
adalService.logOut();
};
$scope.getData = function () {
$http.defaults.useXDomain = true;
delete $http.defaults.headers.common['X-Requested-With'];
$http.get('https://localhost:44327/api/ToGoList').success(function (results) {
console.log(results)
$scope.toGoList = results;
});
}
}]);
For the web API side, we need to specify the the Authority and Audience or other parameters as you wanted( refer the first piece of code).
On the Azure side, we need to register two web applications. One presents the client and the other presents the resource protected by Azure AD. For example, in my test scenario, I registered ToDoSPA and ToGoAPI and grant the permission as figure below:
And to make the ToDoSPA application integrate with Azure AD with implicit flow for the SPA app, we also need to modify its manifest to set the oauth2AllowImplicitFlow to true.
In addition, here are some helpful links about protecting the web API with Azure AD:
https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore
https://github.com/Azure-Samples/active-directory-angularjs-singlepageapp-dotnet-webapi
https://github.com/AzureAD/azure-activedirectory-library-for-js
Update( custom AudienceValidator)
// Configure the app to use Jwt Bearer Authentication
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
TokenValidationParameters=new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
{
AudienceValidator = (audiences, securityToken, validationParameters) =>
{
string[] allowedAudiences = { "https://adfei.onmicrosoft.com/TodoListService", "https://graph.windows.net" };
return allowedAudiences.Contains<string>(audiences.First<string>());
},
},
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Authority = String.Format(Configuration["AzureAd:AadInstance"], Configuration["AzureAD:Tenant"]),
//Audience = Configuration["AzureAd:Audience"],
});

Resources