Adding support for JWT in the TS client

This commit is contained in:
Pawel Kadluczka 2017-11-13 14:41:22 -08:00 committed by Pawel Kadluczka
parent 0bafb304c2
commit 18a65496b7
7 changed files with 184 additions and 13 deletions

View File

@ -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;
@ -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)) {

View File

@ -9,4 +9,5 @@ export interface IHttpConnectionOptions {
httpClient?: IHttpClient;
transport?: TransportType | ITransport;
logging?: ILogger | LogLevel;
jwtBearer?: () => string;
}

View File

@ -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) {
@ -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<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) => {
@ -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 {
@ -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<void> {
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<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);
}

View File

@ -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;
}
}

View File

@ -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)" />

View File

@ -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<EchoEndPoint>();
}
@ -32,6 +80,24 @@ namespace Microsoft.AspNetCore.SignalR.Test.Server
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);
}
}
}

View File

@ -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));
}
});
}
});