Skip to content
This repository has been archived by the owner on Dec 18, 2018. It is now read-only.

Commit

Permalink
Adding support for JWT in the TS client
Browse files Browse the repository at this point in the history
  • Loading branch information
Pawel Kadluczka authored and moozzyk committed Nov 22, 2017
1 parent 0bafb30 commit 18a6549
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 13 deletions.
15 changes: 11 additions & 4 deletions client-ts/Microsoft.AspNetCore.SignalR.Client.TS/HttpConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>();
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;

Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export interface IHttpConnectionOptions {
httpClient?: IHttpClient;
transport?: TransportType | ITransport;
logging?: ILogger | LogLevel;
jwtBearer?: () => string;
}
40 changes: 31 additions & 9 deletions client-ts/Microsoft.AspNetCore.SignalR.Client.TS/Transports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TransferMode> {

return new Promise<TransferMode>((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) {
Expand Down Expand Up @@ -96,23 +102,30 @@ 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;
}

connect(url: string, requestedTransferMode: TransferMode): Promise<TransferMode> {
if (typeof (EventSource) === "undefined") {
Promise.reject("EventSource not supported by the browser.");
}
this.url = url;

this.url = url;
return new Promise<TransferMode>((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) => {
Expand Down Expand Up @@ -152,7 +165,7 @@ export class ServerSentEventsTransport implements ITransport {
}

async send(data: any): Promise<void> {
return send(this.httpClient, this.url, data);
return send(this.httpClient, this.url, this.jwtBearer, data);
}

stop(): void {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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";
}
Expand All @@ -259,7 +277,7 @@ export class LongPollingTransport implements ITransport {
}

async send(data: any): Promise<void> {
return send(this.httpClient, this.url, data);
return send(this.httpClient, this.url, this.jwtBearer, data);
}

stop(): void {
Expand All @@ -274,8 +292,12 @@ export class LongPollingTransport implements ITransport {
onclose: TransportClosed;
}

const headers = new Map<string, string>();
async function send(httpClient: IHttpClient, url: string, jwtBearer: () => string, data: any): Promise<void> {
let headers;
if (jwtBearer) {
headers = new Map<string, string>();
headers.set("Authorization", `Bearer ${jwtBearer()}`)
}

async function send(httpClient: IHttpClient, url: string, data: any): Promise<void> {
await httpClient.post(url, data, headers);
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="$(MicrosoftAspNetCoreAuthenticationJwtBearerPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics" Version="$(MicrosoftAspNetCoreDiagnosticsPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
Expand Down
66 changes: 66 additions & 0 deletions client-ts/Microsoft.AspNetCore.SignalR.Test.Server/Startup.cs
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -18,6 +28,44 @@ public void ConfigureServices(IServiceCollection services)
// 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<EchoEndPoint>();
}

Expand All @@ -32,6 +80,24 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env)
app.UseSockets(options => options.MapEndPoint<EchoEndPoint>("echo"));
app.UseSignalR(options => options.MapHub<TestHub>("testhub"));
app.UseSignalR(options => options.MapHub<UncreatableHub>("uncreatable"));
app.UseSignalR(options => options.MapHub<HubWithAuthorization>("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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
});
}
});

0 comments on commit 18a6549

Please sign in to comment.