Skip to content

Commit

Permalink
Merge pull request #4 from BlazorExtensions/access-token-factory
Browse files Browse the repository at this point in the history
Added Access Token Factory/Provider proper support
  • Loading branch information
galvesribeiro committed Jul 17, 2018
2 parents beb5c6a + c4268e9 commit 24bba66
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 27 deletions.
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

0 comments on commit 24bba66

Please sign in to comment.