Skip to content

Commit e423582

Browse files
committed
Implement external properties/soft-deleting features
1 parent 38c5855 commit e423582

File tree

11 files changed

+145
-11
lines changed

11 files changed

+145
-11
lines changed

src/Core/Token.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using Newtonsoft.Json;
34

45
namespace Bewit
@@ -22,6 +23,12 @@ protected Token(string nonce, DateTime expirationDate)
2223

2324
public DateTime ExpirationDate { get; private set; }
2425

26+
[JsonIgnore]
27+
public bool? IsDeleted { get; set; } = false;
28+
29+
[JsonIgnore]
30+
public Dictionary<string, string> ExtraProperties { get; set; }
31+
2532
public static Token Create(string nonce, DateTime expirationDate)
2633
{
2734
if (string.IsNullOrWhiteSpace(nonce))

src/Extensions.HotChocolate/Generation/BewitMiddleware.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public async Task InvokeAsync(
2323
if (context.Result is TPayload result)
2424
{
2525
BewitToken<TPayload> bewit
26-
= await tokenGenerator.GenerateBewitTokenAsync(result, context.RequestAborted);
26+
= await tokenGenerator.GenerateBewitTokenAsync(
27+
result, context.RequestAborted, context.GetBewitTokenExtraProperties());
2728

2829
context.Result = (string)bewit;
2930
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using HotChocolate.Resolvers;
4+
5+
namespace Bewit.Extensions.HotChocolate.Generation;
6+
7+
public static class BewitTokenExtraPropertiesHelper
8+
9+
{
10+
private const string ExtraPropertyPrefix = "BewitTokenExtraProperty:";
11+
12+
public static void AddBewitTokenExtraProperties(
13+
this IResolverContext resolverContext, Dictionary<string, string> extraProperties)
14+
{
15+
if (extraProperties == null)
16+
{
17+
return;
18+
}
19+
20+
resolverContext.ScopedContextData =
21+
resolverContext.ScopedContextData.SetItems(
22+
extraProperties.ToDictionary(
23+
ctx => $"{ExtraPropertyPrefix}{ctx.Key}",
24+
ctx => (object)ctx.Value));
25+
}
26+
27+
public static Dictionary<string, string> GetBewitTokenExtraProperties(this IMiddlewareContext context)
28+
{
29+
Dictionary<string, string> extraProperties = new Dictionary<string, string>();
30+
31+
foreach (var key in context.ScopedContextData.Keys)
32+
{
33+
if (!key.StartsWith(ExtraPropertyPrefix))
34+
{
35+
continue;
36+
}
37+
38+
object extraPropertyValue = context.ScopedContextData.GetValueOrDefault(key);
39+
40+
if (extraPropertyValue != null)
41+
{
42+
extraProperties.Add(
43+
key.Substring(ExtraPropertyPrefix.Length),
44+
extraPropertyValue.ToString());
45+
}
46+
}
47+
48+
return extraProperties;
49+
}
50+
51+
}

src/Extensions.HotChocolate/Generation/BewitUrlMiddleware.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task InvokeAsync(
2929

3030
BewitToken<string> bewit =
3131
await tokenGenerator.GenerateBewitTokenAsync(
32-
uri.PathAndQuery, context.RequestAborted);
32+
uri.PathAndQuery, context.RequestAborted, context.GetBewitTokenExtraProperties());
3333

3434
var parametersToAdd = new Dictionary<string, string>
3535
{

src/Generation/BewitTokenGenerator.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Globalization;
34
using System.Text;
45
using System.Threading;
@@ -43,19 +44,23 @@ public BewitTokenGenerator(
4344

4445
public Task<BewitToken<T>> GenerateBewitTokenAsync(
4546
T payload,
46-
CancellationToken cancellationToken)
47+
CancellationToken cancellationToken,
48+
Dictionary<string, string> extraProperties = null)
4749
{
4850
var token = Token.Create(CreateNextToken(), CreateExpirationDate());
51+
token.ExtraProperties = extraProperties;
4952
return GenerateBewitTokenImplAsync(payload, token, cancellationToken);
5053

5154
}
5255

5356
public Task<BewitToken<T>> GenerateIdentifiableBewitTokenAsync(
5457
T payload,
5558
string identifier,
56-
CancellationToken cancellationToken)
59+
CancellationToken cancellationToken,
60+
Dictionary<string, string> extraProperties = null)
5761
{
5862
var token = new IdentifiableToken(identifier, CreateNextToken(), CreateExpirationDate());
63+
token.ExtraProperties = extraProperties;
5964
return GenerateBewitTokenImplAsync(payload, token, cancellationToken);
6065
}
6166

src/Generation/IBewitTokenGenerator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Threading;
23
using System.Threading.Tasks;
34

@@ -7,6 +8,7 @@ public interface IBewitTokenGenerator<T>
78
{
89
Task<BewitToken<T>> GenerateBewitTokenAsync(
910
T payload,
10-
CancellationToken cancellationToken);
11+
CancellationToken cancellationToken,
12+
Dictionary<string, string> extraProperties = null);
1113
}
1214
}

src/Generation/IIdentifiableBewitTokenGenerator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Generic;
12
using System.Threading;
23
using System.Threading.Tasks;
34

@@ -8,7 +9,8 @@ public interface IIdentifiableBewitTokenGenerator<T>
89
Task<BewitToken<T>> GenerateIdentifiableBewitTokenAsync(
910
T payload,
1011
string identifier,
11-
CancellationToken cancellationToken);
12+
CancellationToken cancellationToken,
13+
Dictionary<string, string> extraProperties = null);
1214

1315
Task InvalidateIdentifier(
1416
string identifier,

src/Storage.MongoDB/MongoNonceOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@ public class MongoNonceOptions
2626
/// ReUse will keep it in the storage.
2727
/// </summary>
2828
public NonceUsage NonceUsage { get; set; } = NonceUsage.OneTime;
29+
30+
31+
public int RecordExpireAfterDays { get; set; } = 365 * 2;
2932
}
3033
}

src/Storage.MongoDB/MongoNonceRepository.cs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Threading;
34
using System.Threading.Tasks;
45
using MongoDB.Bson.Serialization;
@@ -27,6 +28,8 @@ static MongoNonceRepository()
2728
{
2829
cm.MapIdMember(c => c.Nonce);
2930
cm.MapField(c => c.ExpirationDate);
31+
cm.MapField(c => c.IsDeleted);
32+
cm.MapField(c => c.ExtraProperties);
3033
cm.SetIgnoreExtraElements(true);
3134
});
3235
}
@@ -38,24 +41,51 @@ public MongoNonceRepository(IMongoDatabase database, MongoNonceOptions options)
3841

3942
_collection.Indexes.CreateOne(new CreateIndexModel<Token>(
4043
Builders<Token>.IndexKeys.Ascending(nameof(IdentifiableToken.Identifier))));
44+
45+
_collection.Indexes.CreateOne(new CreateIndexModel<Token>(
46+
Builders<Token>.IndexKeys.Combine(
47+
Builders<Token>.IndexKeys.Ascending(nameof(Token.Nonce)),
48+
Builders<Token>.IndexKeys.Ascending(nameof(Token.IsDeleted)))));
49+
50+
_collection.Indexes.CreateOne(new CreateIndexModel<Token>(
51+
Builders<Token>.IndexKeys.Ascending(nameof(Token.ExpirationDate)),
52+
new CreateIndexOptions
53+
{
54+
ExpireAfter = TimeSpan.FromDays(options.RecordExpireAfterDays)
55+
}));
4156
}
4257

4358
public async ValueTask InsertOneAsync(
4459
Token token, CancellationToken cancellationToken)
4560
{
61+
if (token.ExtraProperties != null)
62+
{
63+
foreach (KeyValuePair<string, string> searchAttribute in token.ExtraProperties)
64+
{
65+
_collection.Indexes.CreateOne(new CreateIndexModel<Token>(
66+
Builders<Token>.IndexKeys.Ascending($"{nameof(token.ExtraProperties)}.{searchAttribute.Key}")));
67+
}
68+
}
69+
4670
await _collection.InsertOneAsync(token, cancellationToken: cancellationToken);
4771
}
4872

4973
public async ValueTask<Token?> TakeOneAsync(
5074
string token,
5175
CancellationToken cancellationToken)
5276
{
53-
FilterDefinition<Token> findFilter = Builders<Token>.Filter.Eq(n => n.Nonce, token);
77+
FilterDefinition<Token> findFilter =
78+
Builders<Token>.Filter.Eq(n => n.Nonce, token) &
79+
(Builders<Token>.Filter.Not(Builders<Token>.Filter.Exists(n => n.IsDeleted)) |
80+
Builders<Token>.Filter.Eq(n => n.IsDeleted, false));
81+
82+
UpdateDefinition<Token> updateDefinition =
83+
Builders<Token>.Update.Set(x => x.IsDeleted, true);
5484

5585
if (_options.NonceUsage == NonceUsage.OneTime)
5686
{
5787
return await _collection
58-
.FindOneAndDeleteAsync(findFilter, cancellationToken: cancellationToken);
88+
.FindOneAndUpdateAsync(findFilter, updateDefinition, cancellationToken: cancellationToken);
5989
}
6090

6191
return await _collection

test/Extensions.HotChocolate.Tests/Integration/BewitUrlMiddlewareTests.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using System.Net.Http;
45
using System.Threading;
56
using System.Threading.Tasks;
@@ -59,7 +60,34 @@ await gqlClient.QueryAsync<GiveMeAccessResult>(query,
5960
res.Data.RequestAccess.Should().Be("https://www.google.com/a/b/?c=d&bewit=eyJUb2tlbiI6eyJOb25jZSI6IjcyNGU3YWNjLWJlNTctNDlhMS04MTk1LTQ2YTAzYzYyNzFjNiIsIkV4cGlyYXRpb25EYXRlIjoiMjAxNy0wMS0wMVQwMTowMjowMS4wMDFaIn0sIlBheWxvYWQiOiIvYS9iLz9jPWQiLCJIYXNoIjoiOWxaak9tTlFqVG0xbUlUVjZ2LzNtU1NBTFBXRndmNXNaQ3pqc3J6eXhwQT0ifQ%253D%253D");
6061
}
6162

62-
private static TestServer CreateTestServer()
63+
[Fact]
64+
public async Task InvokeAsync_WithExtraProperties_ShouldNotImpactBewitToken()
65+
{
66+
//Arrange
67+
TestServer testServer = CreateTestServer(
68+
new Dictionary<string, string> { ["foo"] = "bar" });
69+
HttpClient client = testServer.CreateClient();
70+
GraphQLClient gqlClient = new GraphQLClient(client);
71+
QueryRequest query = new QueryRequest(
72+
string.Empty,
73+
@"mutation giveMeAccess {
74+
RequestAccess
75+
}",
76+
"giveMeAccess",
77+
new Dictionary<string, object>());
78+
79+
//Act
80+
QueryResponse<GiveMeAccessResult> res =
81+
await gqlClient.QueryAsync<GiveMeAccessResult>(query,
82+
CancellationToken.None);
83+
84+
//Assert
85+
res.Data.Should().NotBeNull();
86+
res.Data.RequestAccess.Should().Be("https://www.google.com/a/b/?c=d&bewit=eyJUb2tlbiI6eyJOb25jZSI6IjcyNGU3YWNjLWJlNTctNDlhMS04MTk1LTQ2YTAzYzYyNzFjNiIsIkV4cGlyYXRpb25EYXRlIjoiMjAxNy0wMS0wMVQwMTowMjowMS4wMDFaIn0sIlBheWxvYWQiOiIvYS9iLz9jPWQiLCJIYXNoIjoiOWxaak9tTlFqVG0xbUlUVjZ2LzNtU1NBTFBXRndmNXNaQ3pqc3J6eXhwQT0ifQ%253D%253D");
87+
}
88+
89+
private static TestServer CreateTestServer(
90+
Dictionary<string, string>? extraProperties = null)
6391
{
6492
IWebHostBuilder hostBuilder = new WebHostBuilder()
6593
.ConfigureServices(services =>
@@ -79,7 +107,12 @@ private static TestServer CreateTestServer()
79107
d.Name("Mutation");
80108
d.Field("RequestAccess")
81109
.Type<NonNullType<StringType>>()
82-
.Resolve(ctx => "https://www.google.com/a/b/?c=d")
110+
.Resolve(ctx =>
111+
{
112+
ctx.AddBewitTokenExtraProperties(extraProperties);
113+
114+
return "https://www.google.com/a/b/?c=d";
115+
})
83116
.UseBewitUrlProtection();
84117
});
85118
})

0 commit comments

Comments
 (0)