Skip to content

Commit 1ffbdf7

Browse files
added react as host
1 parent 5d11c85 commit 1ffbdf7

17 files changed

+213
-446
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
public record Connection : IDisposable
5+
{
6+
public required Task CancelTask { get; init; }
7+
public required CancellationTokenSource TokenSource { get; init; }
8+
9+
public void Dispose()
10+
{
11+
CancelTask.Dispose();
12+
TokenSource.Dispose();
13+
}
14+
}
Lines changed: 1 addition & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,6 @@
1-
using System.Collections.Concurrent;
2-
using System.IdentityModel.Tokens.Jwt;
3-
using System.Security.Claims;
4-
using System.Text;
5-
using HotChocolate.AspNetCore;
6-
using HotChocolate.AspNetCore.Subscriptions;
7-
using HotChocolate.AspNetCore.Subscriptions.Protocols;
8-
using HotChocolate.AspNetCore.Subscriptions.Protocols.Apollo;
9-
using HotChocolate.Subscriptions;
10-
using HotChocolate.Types.Pagination;
111
using Microsoft.AspNetCore.Authentication.JwtBearer;
122
using Microsoft.AspNetCore.HttpOverrides;
13-
using Microsoft.Extensions.Options;
14-
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
15-
using Microsoft.IdentityModel.Tokens;
16-
using WebSocket.GraphQLServer.Types;
3+
using Websocket.GraphQLServer.Types;
174

185
var builder = WebApplication.CreateBuilder(args);
196

@@ -56,156 +43,3 @@
5643

5744
app.RunWithGraphQLCommands(args);
5845
app.UseRouting();
59-
60-
61-
62-
public class Subscription
63-
{
64-
[Subscribe]
65-
public Book BookAdded([EventMessage] Book book) => book;
66-
67-
}
68-
69-
public class Mutation
70-
{
71-
public async Task<Book> AddBook(Book book, [Service] ITopicEventSender sender)
72-
{
73-
await sender.SendAsync("BookAdded", book);
74-
75-
// Omitted code for brevity
76-
return book;
77-
}
78-
79-
80-
}
81-
public class JwtBearerValidator(IOptionsFactory<JwtBearerOptions> jwtBearerOptions)
82-
{
83-
private readonly JwtBearerOptions _jwtOptions = jwtBearerOptions.Create(JwtBearerDefaults.AuthenticationScheme);
84-
85-
// Inject the validator and the configured options
86-
// Retrieve the TokenValidationParameters from the options
87-
// We use the scheme name "Bearer" to get the correct options.
88-
89-
public async Task<(ClaimsPrincipal Principal, SecurityToken Token)?> ValidateToken(string token)
90-
{
91-
// The token often comes with "Bearer " prefix, which needs to be removed.
92-
var tokenToValidate = token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
93-
? token.Substring(7)
94-
: token;
95-
96-
var openIdConfig = await _jwtOptions.ConfigurationManager.GetConfigurationAsync(CancellationToken.None);
97-
98-
99-
try
100-
{
101-
var tokenValidator = new JwtSecurityTokenHandler();
102-
// Validate the token using the injected validator and parameters.
103-
// This method throws an exception if the token is invalid.
104-
// 2. Clone the TokenValidationParameters from your options.
105-
var validationParameters = _jwtOptions.TokenValidationParameters.Clone();
106-
107-
// 3. Apply the discovered issuer and signing keys.
108-
validationParameters.ValidIssuer = openIdConfig.Issuer;
109-
validationParameters.IssuerSigningKeys = openIdConfig.SigningKeys;
110-
111-
// 4. Validate the token using the fully populated parameters.
112-
var claimsPrincipal = tokenValidator.ValidateToken(
113-
tokenToValidate,
114-
validationParameters,
115-
out SecurityToken validatedToken);
116-
117-
return (claimsPrincipal, validatedToken);
118-
}
119-
catch (SecurityTokenException ex)
120-
{
121-
// Token validation failed (e.g., expired, invalid signature, etc.)
122-
Console.WriteLine($"Token validation failed: {ex.Message}");
123-
return null;
124-
}
125-
catch (Exception ex)
126-
{
127-
// Some other error occurred
128-
Console.WriteLine($"An error occurred: {ex.Message}");
129-
return null;
130-
}
131-
}
132-
}
133-
134-
public class SubscriptionAuthMiddleware(JwtBearerValidator bearerValidator) : DefaultSocketSessionInterceptor
135-
{
136-
private ConcurrentDictionary<ISocketSession, Connection> _connections = new();
137-
138-
public override async ValueTask<ConnectionStatus> OnConnectAsync(ISocketSession session, IOperationMessagePayload message, CancellationToken cancellationToken)
139-
{
140-
try
141-
{
142-
var httpContext = session.Connection.HttpContext;
143-
var bearerToken = httpContext.Request.Headers.Authorization;
144-
if (string.IsNullOrEmpty(bearerToken))
145-
{
146-
return ConnectionStatus.Accept();
147-
}
148-
149-
var token = bearerToken.ToString().Replace("Bearer ", string.Empty, StringComparison.OrdinalIgnoreCase);
150-
var validatedToken = await bearerValidator.ValidateToken(token);
151-
152-
if (validatedToken == null)
153-
{
154-
return ConnectionStatus.Accept();
155-
}
156-
//{
157-
// return ConnectionStatus.Reject("provided token was invalid");
158-
//}
159-
160-
CancellationTokenSource ct = new CancellationTokenSource();
161-
162-
var validity = validatedToken.Value.Token.ValidTo - DateTime.UtcNow;
163-
164-
if (validity < TimeSpan.Zero)
165-
{
166-
return ConnectionStatus.Reject("token no longer valid");
167-
}
168-
169-
Console.WriteLine("Token valid for " + validity);
170-
171-
_connections.TryAdd(session, new Connection()
172-
{
173-
TokenSource = ct,
174-
CancelTask = Task.Delay(validity)
175-
.ContinueWith(async (_, __) =>
176-
{
177-
_connections.Remove(session, out var _);
178-
await session.Connection.CloseAsync("Refresh", ConnectionCloseReason.NormalClosure,
179-
ct.Token);
180-
}, null, ct.Token)
181-
});
182-
183-
return ConnectionStatus.Accept();
184-
}
185-
catch (Exception ex)
186-
{
187-
return ConnectionStatus.Reject(ex.Message);
188-
}
189-
}
190-
191-
public override async ValueTask OnCloseAsync(ISocketSession session, CancellationToken cancellationToken = new CancellationToken())
192-
{
193-
if (_connections.TryRemove(session, out var connection))
194-
{
195-
await connection.TokenSource.CancelAsync();
196-
connection.Dispose();
197-
}
198-
}
199-
}
200-
201-
public record Connection : IDisposable
202-
{
203-
public required Task CancelTask { get; init; }
204-
public required CancellationTokenSource TokenSource { get; init; }
205-
206-
public void Dispose()
207-
{
208-
CancelTask.Dispose();
209-
TokenSource.Dispose();
210-
}
211-
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.IdentityModel.Tokens.Jwt;
5+
using System.Security.Claims;
6+
using Microsoft.AspNetCore.Authentication.JwtBearer;
7+
using Microsoft.Extensions.Options;
8+
using Microsoft.IdentityModel.Tokens;
9+
10+
public class JwtBearerValidator(IOptionsFactory<JwtBearerOptions> jwtBearerOptions)
11+
{
12+
private readonly JwtBearerOptions _jwtOptions = jwtBearerOptions.Create(JwtBearerDefaults.AuthenticationScheme);
13+
14+
// Inject the validator and the configured options
15+
// Retrieve the TokenValidationParameters from the options
16+
// We use the scheme name "Bearer" to get the correct options.
17+
18+
public async Task<(ClaimsPrincipal Principal, SecurityToken Token)?> ValidateToken(string token)
19+
{
20+
// The token often comes with "Bearer " prefix, which needs to be removed.
21+
var tokenToValidate = token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
22+
? token.Substring(7)
23+
: token;
24+
25+
var openIdConfig = await _jwtOptions.ConfigurationManager.GetConfigurationAsync(CancellationToken.None);
26+
27+
28+
try
29+
{
30+
var tokenValidator = new JwtSecurityTokenHandler();
31+
// Validate the token using the injected validator and parameters.
32+
// This method throws an exception if the token is invalid.
33+
// 2. Clone the TokenValidationParameters from your options.
34+
var validationParameters = _jwtOptions.TokenValidationParameters.Clone();
35+
36+
// 3. Apply the discovered issuer and signing keys.
37+
validationParameters.ValidIssuer = openIdConfig.Issuer;
38+
validationParameters.IssuerSigningKeys = openIdConfig.SigningKeys;
39+
40+
// 4. Validate the token using the fully populated parameters.
41+
var claimsPrincipal = tokenValidator.ValidateToken(
42+
tokenToValidate,
43+
validationParameters,
44+
out SecurityToken validatedToken);
45+
46+
return (claimsPrincipal, validatedToken);
47+
}
48+
catch (SecurityTokenException ex)
49+
{
50+
// Token validation failed (e.g., expired, invalid signature, etc.)
51+
Console.WriteLine($"Token validation failed: {ex.Message}");
52+
return null;
53+
}
54+
catch (Exception ex)
55+
{
56+
// Some other error occurred
57+
Console.WriteLine($"An error occurred: {ex.Message}");
58+
return null;
59+
}
60+
}
61+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Collections.Concurrent;
5+
using HotChocolate.AspNetCore;
6+
using HotChocolate.AspNetCore.Subscriptions;
7+
using HotChocolate.AspNetCore.Subscriptions.Protocols;
8+
9+
public class SubscriptionAuthMiddleware(JwtBearerValidator bearerValidator) : DefaultSocketSessionInterceptor
10+
{
11+
private ConcurrentDictionary<ISocketSession, Connection> _connections = new();
12+
13+
public override async ValueTask<ConnectionStatus> OnConnectAsync(ISocketSession session, IOperationMessagePayload message, CancellationToken cancellationToken)
14+
{
15+
try
16+
{
17+
var httpContext = session.Connection.HttpContext;
18+
var bearerToken = httpContext.Request.Headers.Authorization;
19+
if (string.IsNullOrEmpty(bearerToken))
20+
{
21+
return ConnectionStatus.Accept();
22+
}
23+
24+
var token = bearerToken.ToString().Replace("Bearer ", string.Empty, StringComparison.OrdinalIgnoreCase);
25+
var validatedToken = await bearerValidator.ValidateToken(token);
26+
27+
if (validatedToken == null)
28+
{
29+
return ConnectionStatus.Accept();
30+
}
31+
//{
32+
// return ConnectionStatus.Reject("provided token was invalid");
33+
//}
34+
35+
CancellationTokenSource ct = new CancellationTokenSource();
36+
37+
var validity = validatedToken.Value.Token.ValidTo - DateTime.UtcNow;
38+
39+
if (validity < TimeSpan.Zero)
40+
{
41+
return ConnectionStatus.Reject("token no longer valid");
42+
}
43+
44+
Console.WriteLine("Token valid for " + validity);
45+
46+
_connections.TryAdd(session, new Connection()
47+
{
48+
TokenSource = ct,
49+
CancelTask = Task.Delay(validity)
50+
.ContinueWith(async (_, __) =>
51+
{
52+
_connections.Remove(session, out var _);
53+
await session.Connection.CloseAsync("Refresh", ConnectionCloseReason.NormalClosure,
54+
ct.Token);
55+
}, null, ct.Token)
56+
});
57+
58+
return ConnectionStatus.Accept();
59+
}
60+
catch (Exception ex)
61+
{
62+
return ConnectionStatus.Reject(ex.Message);
63+
}
64+
}
65+
66+
public override async ValueTask OnCloseAsync(ISocketSession session, CancellationToken cancellationToken = new CancellationToken())
67+
{
68+
if (_connections.TryRemove(session, out var connection))
69+
{
70+
await connection.TokenSource.CancelAsync();
71+
connection.Dispose();
72+
}
73+
}
74+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
namespace WebSocket.GraphQLServer.Types;
1+
namespace Websocket.GraphQLServer.Types;
22

33
public record Author(string Name);
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
namespace WebSocket.GraphQLServer.Types;
1+
namespace Websocket.GraphQLServer.Types;
22

33
public record Book(string Title, Author Author);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using HotChocolate.Subscriptions;
2+
3+
namespace Websocket.GraphQLServer.Types;
4+
5+
public class Mutation
6+
{
7+
public async Task<Book> AddBook(Book book, [Service] ITopicEventSender sender)
8+
{
9+
await sender.SendAsync("BookAdded", book);
10+
11+
// Omitted code for brevity
12+
return book;
13+
}
14+
15+
16+
}

BFF/v3/Websocket/WebSocket.GraphQLServer/Types/Query.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System.Security.Claims;
22

3-
namespace WebSocket.GraphQLServer.Types;
3+
namespace Websocket.GraphQLServer.Types;
44

55
[QueryType]
66
public static class Query
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Websocket.GraphQLServer.Types;
2+
3+
public class Subscription
4+
{
5+
[Subscribe]
6+
public Book BookAdded([EventMessage] Book book) => book;
7+
8+
}

BFF/v3/Websocket/WebSocket.GraphQLServer/Websocket.GraphQLServer.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup Condition="'$(ImplicitUsings)' == 'enable'">
11-
<Using Include="WebSocket.GraphQLServer" />
11+
<Using Include="Websocket.GraphQLServer" />
1212
</ItemGroup>
1313

1414
<ItemGroup>

0 commit comments

Comments
 (0)