Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Access Token Factory/Provider proper support #4

Merged
merged 1 commit into from
Jul 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions src/Blazor.Extensions.SignalR.JS/src/HubConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,47 @@ export class HubConnectionManager {
const Blazor: BlazorType = window["Blazor"];
window["BlazorExtensions"].HubConnectionManager = new HubConnectionManager();

Blazor.registerFunction('Blazor.Extensions.SignalR.CreateConnection', (connectionId: string, url: string, httpConnectionOptions: any, addMessagePack: boolean) => {
window["BlazorExtensions"].HubConnectionManager.createConnection(connectionId, url,
{
logger: httpConnectionOptions.LogLevel,
transport: httpConnectionOptions.Transport,
logMessageContent: httpConnectionOptions.LogMessageContent,
skipNegotiation: httpConnectionOptions.SkipNegotiation,
accessTokenFactory: () => httpConnectionOptions.AccessToken
}, addMessagePack);
return true;
});
Blazor.registerFunction('Blazor.Extensions.SignalR.CreateConnection',
(connectionId: string, httpConnectionOptions: any) => {
let options: any = {
logger: httpConnectionOptions.logLevel,
transport: httpConnectionOptions.transport,
logMessageContent: httpConnectionOptions.logMessageContent,
skipNegotiation: httpConnectionOptions.skipNegotiation
};

if (httpConnectionOptions.hasAccessTokenFactory) {
options.accessTokenFactory = () => {
return new Promise<string>(async (resolve, reject) => {
const token = await Blazor.invokeDotNetMethodAsync<string>(
{
type: {
assembly: 'Blazor.Extensions.SignalR',
name: 'Blazor.Extensions.HubConnectionManager'
},
method: {
name: 'GetAccessToken'
}
}, connectionId);

if (token) {
resolve(token);
} else {
reject();
}
})
}
}

window["BlazorExtensions"].HubConnectionManager.createConnection(
connectionId,
httpConnectionOptions.url,
options,
httpConnectionOptions.enableMessagePack
);
return true;
}
);

Blazor.registerFunction('Blazor.Extensions.SignalR.RemoveConnection', (connectionId: string) => {
return window["BlazorExtensions"].HubConnectionManager.removeConnection(connectionId);
Expand Down
30 changes: 29 additions & 1 deletion src/Blazor.Extensions.SignalR/HttpConnectionOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using System;
using System.Runtime.Serialization;
using System.Threading.Tasks;

namespace Blazor.Extensions
{
public class HttpConnectionOptions
Expand All @@ -6,6 +10,30 @@ public class HttpConnectionOptions
public SignalRLogLevel LogLevel { get; set; }
public bool LogMessageContent { get; set; }
public bool SkipNegotiation { get; set; }
public string AccessToken { get; set; }
internal bool EnableMessagePack { get; set; }
internal string Url { get; set; }
public Func<Task<string>> AccessTokenProvider { get; set; }
}

internal class InternalHttpConnectionOptions
{
public HttpTransportType Transport { get; set; }
public SignalRLogLevel LogLevel { get; set; }
public bool LogMessageContent { get; set; }
public bool SkipNegotiation { get; set; }
public bool EnableMessagePack { get; set; }
public string Url { get; set; }
public bool HasAccessTokenFactory { get; set; }

public InternalHttpConnectionOptions(HttpConnectionOptions options)
{
this.Transport = options.Transport;
this.LogLevel = options.LogLevel;
this.LogMessageContent = options.LogMessageContent;
this.SkipNegotiation = options.SkipNegotiation;
this.EnableMessagePack = options.EnableMessagePack;
this.Url = options.Url;
this.HasAccessTokenFactory = options.AccessTokenProvider != null;
}
}
}
7 changes: 3 additions & 4 deletions src/Blazor.Extensions.SignalR/HubConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,20 @@ public class HubConnection : IDisposable
private const string STOP_CONNECTION_METHOD = "Blazor.Extensions.SignalR.StopConnection";
private const string INVOKE_ASYNC_METHOD = "Blazor.Extensions.SignalR.InvokeAsync";
private const string INVOKE_WITH_RESULT_ASYNC_METHOD = "Blazor.Extensions.SignalR.InvokeWithResultAsync";
internal string Url { get; }
internal HttpConnectionOptions Options { get; }
internal string InternalConnectionId { get; }

private Dictionary<string, Func<object, Task>> _handlers = new Dictionary<string, Func<object, Task>>();
private Func<Exception, Task> _errorHandler;

public HubConnection(string url, HttpConnectionOptions options, bool addMessagePack)
public HubConnection(HttpConnectionOptions options)
{
this.Url = url;
this.Options = options;
this.InternalConnectionId = Guid.NewGuid().ToString();
HubConnectionManager.AddConnection(this, addMessagePack);
HubConnectionManager.AddConnection(this);
}

internal Task<string> GetAccessToken() => this.Options.AccessTokenProvider != null ? this.Options.AccessTokenProvider() : null;
internal Task OnClose(string error) => this._errorHandler != null ? this._errorHandler(new Exception(error)) : Task.CompletedTask;

public Task StartAsync() => RegisteredFunction.InvokeAsync<object>(START_CONNECTION_METHOD, this.InternalConnectionId);
Expand Down
14 changes: 5 additions & 9 deletions src/Blazor.Extensions.SignalR/HubConnectionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ public class HubConnectionBuilder
{
private bool _hubConnectionBuilt;

internal string Url { get; set; }
internal HttpConnectionOptions Options { get; set; }
internal bool EnableMessagePack { get; set; }
internal HttpConnectionOptions Options { get; set; } = new HttpConnectionOptions();

public HubConnection Build()
{
Expand All @@ -20,7 +18,7 @@ public HubConnection Build()

this._hubConnectionBuilt = true;

return new HubConnection(this.Url, this.Options, this.EnableMessagePack);
return new HubConnection(this.Options);
}
}

Expand All @@ -40,10 +38,8 @@ public static HubConnectionBuilder WithUrl(this HubConnectionBuilder hubConnecti
if (hubConnectionBuilder == null) throw new ArgumentNullException(nameof(hubConnectionBuilder));
if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url));

hubConnectionBuilder.Url = url;
var opt = new HttpConnectionOptions();
configureHttpOptions?.Invoke(opt);
hubConnectionBuilder.Options = opt;
hubConnectionBuilder.Options.Url = url;
configureHttpOptions?.Invoke(hubConnectionBuilder.Options);
return hubConnectionBuilder;
}

Expand All @@ -55,7 +51,7 @@ public static HubConnectionBuilder WithUrl(this HubConnectionBuilder hubConnecti
public static HubConnectionBuilder AddMessagePackProtocol(this HubConnectionBuilder hubConnectionBuilder)
{
if (hubConnectionBuilder == null) throw new ArgumentNullException(nameof(hubConnectionBuilder));
hubConnectionBuilder.EnableMessagePack = true;
hubConnectionBuilder.Options.EnableMessagePack = true;
return hubConnectionBuilder;
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/Blazor.Extensions.SignalR/HubConnectionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ internal static class HubConnectionManager

public static Task Dispatch(string connectionId, string methodName, object payload) => _connections[connectionId].Dispatch(methodName, payload);
public static Task OnClose(string connectionId, string error) => _connections[connectionId].OnClose(error);
public static Task<string> GetAccessToken(string connectionId) => _connections[connectionId].GetAccessToken();

public static void AddConnection(HubConnection connection, bool addMessagePack)
public static void AddConnection(HubConnection connection)
{
RegisteredFunction.Invoke<object>(CREATE_CONNECTION_METHOD, connection.InternalConnectionId, connection.Url, connection.Options, addMessagePack);
RegisteredFunction.Invoke<object>(CREATE_CONNECTION_METHOD,
connection.InternalConnectionId,
new InternalHttpConnectionOptions(connection.Options));
_connections[connection.InternalConnectionId] = connection;
}

Expand Down
15 changes: 15 additions & 0 deletions test/Blazor.Extensions.SignalR.Test.Client/Pages/ChatComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

namespace Blazor.Extensions.SignalR.Test.Client.Pages
{
public class ChatComponent : BlazorComponent
{
[Inject] private HttpClient _http { get; set; }
[Inject] private ILogger<ChatComponent> _logger { get; set; }
internal string _toEverybody { get; set; }
internal string _toConnection { get; set; }
Expand All @@ -27,13 +29,26 @@ protected override async Task OnInitAsync()
{
opt.LogLevel = SignalRLogLevel.Trace;
opt.Transport = HttpTransportType.WebSockets;
opt.AccessTokenProvider = async () =>
{
var token = await this.GetJwtToken("DemoUser");
this._logger.LogInformation($"Access Token: {token}");
return token;
};
})
.Build();

this._connection.On("Send", this.Handle);
await this._connection.StartAsync();
}

private async Task<string> GetJwtToken(string userId)
{
var httpResponse = await this._http.GetAsync($"/generatetoken?user={userId}");
httpResponse.EnsureSuccessStatusCode();
return await httpResponse.Content.ReadAsStringAsync();
}

private Task Handle(object msg)
{
this._logger.LogInformation(msg);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

namespace Blazor.Extensions.SignalR.Test.Server.Controllers
{
[Route("generatetoken")]
public class TokenController : Controller
{
[HttpGet]
public string GenerateToken()
{
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, this.Request.Query["user"]) };
var credentials = new SigningCredentials(Startup.SecurityKey, SecurityAlgorithms.HmacSha256); // Too lazy to inject the key as a service
var token = new JwtSecurityToken("SignalRTestServer", "SignalRTests", claims, expires: DateTime.UtcNow.AddSeconds(30), signingCredentials: credentials);
return Startup.JwtTokenHandler.WriteToken(token); // Even more lazy here
}
}
}
3 changes: 3 additions & 0 deletions test/Blazor.Extensions.SignalR.Test.Server/Hubs/ChatHub.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;

namespace Blazor.Extensions.SignalR.Test.Server.Hubs
{
[Authorize(JwtBearerDefaults.AuthenticationScheme)]
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
Expand Down
48 changes: 48 additions & 0 deletions test/Blazor.Extensions.SignalR.Test.Server/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using Blazor.Extensions.SignalR.Test.Server.Hubs;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Blazor.Server;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Serialization;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Mime;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Blazor.Extensions.SignalR.Test.Server
{
public class Startup
{
public static readonly SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());
public static readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();

// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
Expand All @@ -26,6 +35,45 @@ public void ConfigureServices(IServiceCollection services)
.AddSignalR(options => options.KeepAliveInterval = TimeSpan.FromSeconds(5))
.AddJsonProtocol();

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
{
LifetimeValidator = (before, expires, token, parameters) => expires > DateTime.UtcNow,
ValidateAudience = false,
ValidateIssuer = false,
ValidateActor = false,
ValidateLifetime = true,
IssuerSigningKey = SecurityKey
};

options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];

if (!string.IsNullOrEmpty(accessToken) &&
(context.HttpContext.WebSockets.IsWebSocketRequest || context.Request.Headers["Accept"] == "text/event-stream"))
{
context.Token = context.Request.Query["access_token"];
}
return Task.CompletedTask;
}
};
});

services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
Expand Down