diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts index 4c0f55a42b..9ff2ae2173 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts @@ -59,7 +59,14 @@ export class HttpConnection implements IConnection { this.transport = this.createTransport(this.options.transport, [TransportType[TransportType.WebSockets]]); } else { - let negotiatePayload = await this.httpClient.post(this.resolveNegotiateUrl(this.url), ""); + let headers; + if (this.options.jwtBearer) { + headers = new Map(); + headers.set("Authorization", `Bearer ${this.options.jwtBearer()}`); + } + + let negotiatePayload = await this.httpClient.post(this.resolveNegotiateUrl(this.url), "", headers); + let negotiateResponse: INegotiateResponse = JSON.parse(negotiatePayload); this.connectionId = negotiateResponse.connectionId; @@ -101,13 +108,13 @@ export class HttpConnection implements IConnection { transport = TransportType[availableTransports[0]]; } if (transport === TransportType.WebSockets && availableTransports.indexOf(TransportType[transport]) >= 0) { - return new WebSocketTransport(this.logger); + return new WebSocketTransport(this.options.jwtBearer, this.logger); } if (transport === TransportType.ServerSentEvents && availableTransports.indexOf(TransportType[transport]) >= 0) { - return new ServerSentEventsTransport(this.httpClient, this.logger); + return new ServerSentEventsTransport(this.httpClient, this.options.jwtBearer, this.logger); } if (transport === TransportType.LongPolling && availableTransports.indexOf(TransportType[transport]) >= 0) { - return new LongPollingTransport(this.httpClient, this.logger); + return new LongPollingTransport(this.httpClient, this.options.jwtBearer, this.logger); } if (this.isITransport(transport)) { diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHttpConnectionOptions.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHttpConnectionOptions.ts index aebb2a50a0..3bc380e124 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHttpConnectionOptions.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/IHttpConnectionOptions.ts @@ -9,4 +9,5 @@ export interface IHttpConnectionOptions { httpClient?: IHttpClient; transport?: TransportType | ITransport; logging?: ILogger | LogLevel; + jwtBearer?: () => string; } diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts index de891096b1..a6179a05ec 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts +++ b/client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts @@ -27,16 +27,22 @@ export interface ITransport { export class WebSocketTransport implements ITransport { private readonly logger: ILogger; + private readonly jwtBearer: () => string; private webSocket: WebSocket; - constructor(logger: ILogger) { + constructor(jwtBearer: () => string, logger: ILogger) { this.logger = logger; + this.jwtBearer = jwtBearer; } connect(url: string, requestedTransferMode: TransferMode): Promise { return new Promise((resolve, reject) => { url = url.replace(/^http/, "ws"); + if (this.jwtBearer) { + let token = this.jwtBearer(); + url += (url.indexOf("?") < 0 ? "?" : "&") + `signalRTokenHeader=${token}`; + } let webSocket = new WebSocket(url); if (requestedTransferMode == TransferMode.Binary) { @@ -96,12 +102,14 @@ export class WebSocketTransport implements ITransport { export class ServerSentEventsTransport implements ITransport { private readonly httpClient: IHttpClient; + private readonly jwtBearer: () => string; private readonly logger: ILogger; private eventSource: EventSource; private url: string; - constructor(httpClient: IHttpClient, logger: ILogger) { + constructor(httpClient: IHttpClient, jwtBearer: () => string, logger: ILogger) { this.httpClient = httpClient; + this.jwtBearer = jwtBearer; this.logger = logger; } @@ -109,10 +117,15 @@ export class ServerSentEventsTransport implements ITransport { if (typeof (EventSource) === "undefined") { Promise.reject("EventSource not supported by the browser."); } - this.url = url; + this.url = url; return new Promise((resolve, reject) => { - let eventSource = new EventSource(this.url); + if (this.jwtBearer) { + let token = this.jwtBearer(); + url += (url.indexOf("?") < 0 ? "?" : "&") + `signalRTokenHeader=${token}`; + } + + let eventSource = new EventSource(url); try { eventSource.onmessage = (e: MessageEvent) => { @@ -152,7 +165,7 @@ export class ServerSentEventsTransport implements ITransport { } async send(data: any): Promise { - return send(this.httpClient, this.url, data); + return send(this.httpClient, this.url, this.jwtBearer, data); } stop(): void { @@ -168,14 +181,16 @@ export class ServerSentEventsTransport implements ITransport { export class LongPollingTransport implements ITransport { private readonly httpClient: IHttpClient; + private readonly jwtBearer: () => string; private readonly logger: ILogger; private url: string; private pollXhr: XMLHttpRequest; private shouldPoll: boolean; - constructor(httpClient: IHttpClient, logger: ILogger) { + constructor(httpClient: IHttpClient, jwtBearer: () => string, logger: ILogger) { this.httpClient = httpClient; + this.jwtBearer = jwtBearer; this.logger = logger; } @@ -249,6 +264,9 @@ export class LongPollingTransport implements ITransport { this.pollXhr = pollXhr; this.pollXhr.open("GET", `${url}&_=${Date.now()}`, true); + if (this.jwtBearer) { + this.pollXhr.setRequestHeader("Authorization", `Bearer ${this.jwtBearer()}`); + } if (transferMode === TransferMode.Binary) { this.pollXhr.responseType = "arraybuffer"; } @@ -259,7 +277,7 @@ export class LongPollingTransport implements ITransport { } async send(data: any): Promise { - return send(this.httpClient, this.url, data); + return send(this.httpClient, this.url, this.jwtBearer, data); } stop(): void { @@ -274,8 +292,12 @@ export class LongPollingTransport implements ITransport { onclose: TransportClosed; } -const headers = new Map(); +async function send(httpClient: IHttpClient, url: string, jwtBearer: () => string, data: any): Promise { + let headers; + if (jwtBearer) { + headers = new Map(); + headers.set("Authorization", `Bearer ${jwtBearer()}`) + } -async function send(httpClient: IHttpClient, url: string, data: any): Promise { await httpClient.post(url, data, headers); } \ No newline at end of file diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/AuthorizedHub.cs b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/AuthorizedHub.cs new file mode 100644 index 0000000000..9674683671 --- /dev/null +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/AuthorizedHub.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Authorization; + +namespace Microsoft.AspNetCore.SignalR.Test.Server +{ + [Authorize(JwtBearerDefaults.AuthenticationScheme)] + public class HubWithAuthorization : Hub + { + public string Echo(string message) => message; + } +} diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Microsoft.AspNetCore.SignalR.Test.Server.csproj b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Microsoft.AspNetCore.SignalR.Test.Server.csproj index 7e28045704..6f82ce037e 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Microsoft.AspNetCore.SignalR.Test.Server.csproj +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Microsoft.AspNetCore.SignalR.Test.Server.csproj @@ -10,6 +10,7 @@ + diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Startup.cs b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Startup.cs index e8a37f9055..cdeeb89bb1 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Startup.cs +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Startup.cs @@ -1,15 +1,25 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.SignalR.Test.Server { public class Startup { + private readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray()); + private readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); + public void ConfigureServices(IServiceCollection services) { services.AddSockets(); @@ -18,6 +28,44 @@ namespace Microsoft.AspNetCore.SignalR.Test.Server // consistent casing makes it cleaner to verify results options.JsonSerializerSettings.ContractResolver = new DefaultContractResolver(); }); + + services.AddAuthorization(options => + { + options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy => + { + policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme); + policy.RequireClaim(ClaimTypes.NameIdentifier); + }); + }); + + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = + new TokenValidationParameters + { + ValidateAudience = false, + ValidateIssuer = false, + ValidateActor = false, + ValidateLifetime = true, + IssuerSigningKey = SecurityKey + }; + + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var signalRTokenHeader = context.Request.Query["signalRTokenHeader"]; + + if (!string.IsNullOrEmpty(signalRTokenHeader) && + (context.HttpContext.WebSockets.IsWebSocketRequest || context.Request.Headers["Accept"] == "text/event-stream")) + { + context.Token = context.Request.Query["signalRTokenHeader"]; + } + return Task.CompletedTask; + } + }; + }); services.AddEndPoint(); } @@ -32,6 +80,24 @@ namespace Microsoft.AspNetCore.SignalR.Test.Server app.UseSockets(options => options.MapEndPoint("echo")); app.UseSignalR(options => options.MapHub("testhub")); app.UseSignalR(options => options.MapHub("uncreatable")); + app.UseSignalR(options => options.MapHub("authorizedhub")); + + app.Use(next => async (context) => + { + if (context.Request.Path.StartsWithSegments("/generateJwtToken")) + { + await context.Response.WriteAsync(GenerateJwtToken()); + return; + } + }); + } + + private string GenerateJwtToken() + { + var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "testuser") }; + var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); + var token = new JwtSecurityToken("SignalRTestServer", "SignalRTests", claims, expires: DateTime.Now.AddSeconds(5), signingCredentials: credentials); + return JwtTokenHandler.WriteToken(token); } } } diff --git a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js index 44de69c404..de124e7d8c 100644 --- a/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js +++ b/client-ts/Microsoft.AspNetCore.SignalR.Test.Server/wwwroot/js/hubConnectionTests.js @@ -343,4 +343,64 @@ describe('hubConnection', function () { }); }); }); + + eachTransport(function (transportType) { + describe(' over ' + signalR.TransportType[transportType] + ' transport', function () { + + it('can connect to hub with authorization', function (done) { + var message = '你好,世界!'; + + var hubConnection; + getJwtToken('http://' + document.location.host + '/generateJwtToken') + .then(jwtToken => { + var options = { + transport: transportType, + logging: signalR.LogLevel.Trace, + jwtBearer: function () { + return jwtToken; + } + }; + hubConnection = new signalR.HubConnection('/authorizedhub', options); + hubConnection.onclose(function (error) { + expect(error).toBe(undefined); + done(); + }); + return hubConnection.start(); + }) + .then(function() { + return hubConnection.invoke('Echo', message); + }) + .then(function(response) { + expect(response).toEqual(message); + return hubConnection.stop(); + }) + .catch(function(e) { + fail(e); + done(); + }); + }); + }); + }); + + function getJwtToken(url) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + + xhr.open('GET', url, true); + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.send(); + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + resolve(xhr.response || xhr.responseText); + } + else { + reject(new Error(xhr.statusText)); + } + }; + + xhr.onerror = () => { + reject(new Error(xhr.statusText)); + } + }); + } });