Adding support for JWT in the TS client
This commit is contained in:
parent
0bafb304c2
commit
18a65496b7
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ export interface IHttpConnectionOptions {
|
|||
httpClient?: IHttpClient;
|
||||
transport?: TransportType | ITransport;
|
||||
logging?: ILogger | LogLevel;
|
||||
jwtBearer?: () => string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue