• R/O
  • SSH
  • HTTPS

grpcdotnet: Commit


Commit MetaInfo

Revision9 (tree)
Time2020-10-05 14:02:06
Authorsebastiandotnet

Log Message

add extended jwt poc

Change Summary

Incremental Difference

--- ServerInterceptor/ReadMe.md (revision 8)
+++ ServerInterceptor/ReadMe.md (revision 9)
@@ -1,8 +1,7 @@
11 Motivation)
22
3-This is a proof of concept to proof a service-side interceptor is able to manage call level security.
4-To simplify this poc, i dont use JWT as session token here. This poc dont mean its a good idea
5-to use an interceptor for security.
3+This is a concept to proof a service-side interceptor is able to manage call level security.
4+This poc doesnt mean its a good idea to use an interceptor for security. Mostly standard policies works well.
65
76
87 Ho it works)
@@ -20,7 +19,7 @@
2019 (You can add credentials permanently to a channel of course.)
2120
2221 The Common project in the middle is not directly referenced. Its just a shared place
23-for ouer proto files. The client console app is a slightly cheap integration test.
22+for our proto files. The client console app is a slightly cheap integration test.
2423
2524
2625
--- SessionJwt/SL.SessionJwt.ConsoleClient/Constants.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.ConsoleClient/Constants.cs (revision 9)
@@ -0,0 +1,15 @@
1+using System;
2+using System.Collections.Generic;
3+using System.Text;
4+
5+namespace SL.SessionJwt.ConsoleClient
6+{
7+ public static class Constants
8+ {
9+ public static readonly string TokenName = "Authorization";
10+
11+ public static readonly string BearerPrefix = "Bearer ";
12+
13+ public static readonly string RemoteEndPoint = "https://localhost:5001";
14+ }
15+}
--- SessionJwt/SL.SessionJwt.ConsoleClient/OutdatedTookie.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.ConsoleClient/OutdatedTookie.cs (revision 9)
@@ -0,0 +1,163 @@
1+using Grpc.Core;
2+using SL.SessionJwt.Service;
3+using System;
4+using System.Runtime.CompilerServices;
5+using System.Threading.Tasks;
6+
7+namespace SL.SessionJwt.ConsoleClient
8+{
9+ public class OutdatedTookie
10+ {
11+ public async Task Start()
12+ {
13+ WriteLine($"=== {nameof(OutdatedTookie)} starts. ===");
14+
15+ var user = await LogonAndSayHello();
16+ await Wait1MinuteForTokenTimeout();
17+ await SayHelloAndFail(user);
18+ await RenewTokenAndSayHello(user);
19+ await Logout(user, true);
20+ await SayHelloAndFail(user);
21+ await RenewAndFailBecauseExplicitLogout(user);
22+
23+ user = await LogonAndSayHello();
24+ await Wait2MinutesForTokenTimeoutNotRenewable();
25+ await RenewAndFailBecauseNotRenewable(user);
26+ await Logout(user);
27+
28+ WriteLine($"=== {nameof(OutdatedTookie)} passed. ==={Environment.NewLine}");
29+ }
30+
31+ public async Task<UserProxy> LogonAndSayHello()
32+ {
33+ var user = new UserProxy("User1");
34+ try
35+ {
36+ await user.LogonAsync();
37+ await user.Client<Greeter.GreeterClient>().SayHelloAsync(new HelloRequest { Name = nameof(OutdatedTookie) });
38+ WriteLine("passed.");
39+ return user;
40+ }
41+ catch (RpcException exception)
42+ {
43+ if (exception.StatusCode != StatusCode.InvalidArgument)
44+ WriteLine($"RpcException {exception.StatusCode}");
45+ throw;
46+ }
47+ catch (Exception exception)
48+ {
49+ WriteLine($"Exception {exception.Message}");
50+ throw;
51+ }
52+ }
53+
54+ public static async Task Wait1MinuteForTokenTimeout()
55+ {
56+ Console.WriteLine("Wait a minute.");
57+ await Task.Delay(61000);
58+ }
59+
60+ public static async Task Wait2MinutesForTokenTimeoutNotRenewable()
61+ {
62+ Console.WriteLine("Wait 2 minutes.");
63+ await Task.Delay(121000);
64+ }
65+
66+ public static async Task SayHelloAndFail(UserProxy user)
67+ {
68+ try
69+ {
70+ await user.Client<Greeter.GreeterClient>().SayHelloAsync(new HelloRequest { Name = nameof(OutdatedTookie) });
71+ }
72+ catch (RpcException)
73+ {
74+ WriteLine("passed.");
75+ return;
76+ }
77+ catch (Exception exception)
78+ {
79+ WriteLine($"{nameof(SayHelloAndFail)} Unexpected Exception {exception.Message}");
80+ throw;
81+ }
82+ throw new Exception("This should go wrong.");
83+ }
84+
85+ public static async Task RenewTokenAndSayHello(UserProxy user)
86+ {
87+ try
88+ {
89+ await user.RenewAsync();
90+ await user.Client<Greeter.GreeterClient>().SayHelloAsync(new HelloRequest { Name = nameof(OutdatedTookie) });
91+ WriteLine("passed.");
92+ }
93+ catch (RpcException exception)
94+ {
95+ if (exception.StatusCode != StatusCode.InvalidArgument)
96+ WriteLine($"RpcException {exception.StatusCode}");
97+ throw;
98+ }
99+ catch (Exception exception)
100+ {
101+ WriteLine($"Exception {exception.Message}");
102+ throw;
103+ }
104+ }
105+
106+ public static async Task Logout(UserProxy user, bool keepToken = false)
107+ {
108+ try
109+ {
110+ await user.LogoutAsync(keepToken);
111+ WriteLine("passed.");
112+ }
113+ catch (Exception exception)
114+ {
115+ Console.WriteLine($"Logout failed. {exception.Message}");
116+ throw;
117+ }
118+ }
119+
120+ public static async Task RenewAndFailBecauseExplicitLogout(UserProxy user)
121+ {
122+ try
123+ {
124+ await user.RenewAsync();
125+ }
126+ catch (RpcException)
127+ {
128+ WriteLine("passed.");
129+ return;
130+ }
131+ catch (Exception exception)
132+ {
133+ WriteLine($"{nameof(RenewAndFailBecauseExplicitLogout)} Unexpected Exception {exception.Message}");
134+ throw;
135+ }
136+ throw new Exception("This should go wrong.");
137+ }
138+
139+ public static async Task RenewAndFailBecauseNotRenewable(UserProxy user)
140+ {
141+ try
142+ {
143+ await user.RenewAsync();
144+ }
145+ catch (RpcException)
146+ {
147+ WriteLine("passed.");
148+ return;
149+ }
150+ catch (Exception exception)
151+ {
152+ WriteLine($"{nameof(RenewAndFailBecauseNotRenewable)} Unexpected Exception {exception.Message}");
153+ throw;
154+ }
155+ throw new Exception("This should go wrong.");
156+ }
157+
158+ static void WriteLine(string message, [CallerMemberName] string caller = "")
159+ {
160+ Console.WriteLine($"{caller}: {message}");
161+ }
162+ }
163+}
--- SessionJwt/SL.SessionJwt.ConsoleClient/Program.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.ConsoleClient/Program.cs (revision 9)
@@ -0,0 +1,18 @@
1+using System;
2+using System.Threading.Tasks;
3+
4+namespace SL.SessionJwt.ConsoleClient
5+{
6+ static class Program
7+ {
8+ static async Task Main()
9+ {
10+ await new TestSayHello().Start();
11+ await new TestLogonLogout().Start();
12+ await new OutdatedTookie().Start();
13+
14+ Console.WriteLine("Press any key.");
15+ Console.ReadKey();
16+ }
17+ }
18+}
--- SessionJwt/SL.SessionJwt.ConsoleClient/TestLogonLogout.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.ConsoleClient/TestLogonLogout.cs (revision 9)
@@ -0,0 +1,82 @@
1+using Grpc.Core;
2+using System;
3+using System.Runtime.CompilerServices;
4+using System.Threading.Tasks;
5+
6+namespace SL.SessionJwt.ConsoleClient
7+{
8+ public class TestLogonLogout
9+ {
10+ public async Task Start()
11+ {
12+ WriteLine($"=== {nameof(TestLogonLogout)} starts. ===");
13+
14+ await LogonByMissingUser();
15+ await LogonTwice();
16+ await LogoutTwice();
17+
18+ WriteLine($"=== {nameof(TestLogonLogout)} passed. ==={Environment.NewLine}");
19+ }
20+
21+ private async Task LogonByMissingUser()
22+ {
23+ var user = new UserProxy("UserXYZ");
24+ try
25+ {
26+ await user.LogonAsync();
27+ }
28+ catch (RpcException exception)
29+ {
30+ if (exception.StatusCode == StatusCode.InvalidArgument)
31+ WriteLine("passed");
32+ else
33+ WriteLine($"Exception {exception.Message}");
34+ }
35+ catch (Exception exception)
36+ {
37+ WriteLine($"Exception {exception.Message}");
38+ }
39+ }
40+
41+ private async Task LogonTwice()
42+ {
43+ try
44+ {
45+ // second logon overrides old session
46+ // and should not throw an error
47+ var user = new UserProxy("User1");
48+ await user.LogonAsync();
49+ await user.LogonAsync();
50+ WriteLine("passed");
51+ }
52+ catch (Exception exception)
53+ {
54+ WriteLine($"Exception {exception.Message}");
55+ }
56+ }
57+
58+ private async Task LogoutTwice()
59+ {
60+ try
61+ {
62+ // second logout should does just nothing
63+ var user = new UserProxy("User1");
64+ await user.LogonAsync();
65+ var logout1Result = await user.LogoutAsync();
66+ var logout2Result = await user.LogoutAsync();
67+ if (!logout1Result || logout2Result)
68+ throw new Exception($"logout1Result is{logout1Result} and logout2Result is {logout2Result}.");
69+ WriteLine("passed");
70+ }
71+ catch (Exception exception)
72+ {
73+ WriteLine($"Exception {exception.Message}");
74+ }
75+ }
76+
77+ static void WriteLine(string message, [CallerMemberName] string caller = "")
78+ {
79+ Console.WriteLine($"{caller}: {message}");
80+ }
81+ }
82+}
--- SessionJwt/SL.SessionJwt.ConsoleClient/TestSayHello.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.ConsoleClient/TestSayHello.cs (revision 9)
@@ -0,0 +1,43 @@
1+using SL.SessionJwt.Service;
2+using System;
3+using System.Runtime.CompilerServices;
4+using System.Threading.Tasks;
5+
6+namespace SL.SessionJwt.ConsoleClient
7+{
8+ public class TestSayHello
9+ {
10+ public async Task Start()
11+ {
12+ WriteLine($"=== {nameof(TestSayHello)} starts. ===");
13+
14+ await SayHelloAsync();
15+
16+ WriteLine($"=== {nameof(TestSayHello)} passed. ==={Environment.NewLine}");
17+ }
18+
19+ private async Task SayHelloAsync()
20+ {
21+ try
22+ {
23+ var user = new UserProxy("User1");
24+ user.Invoker.WriteTokenToConsole = true;
25+ await user.LogonAsync();
26+ await user.Client<Greeter.GreeterClient>().SayHelloAsync(new HelloRequest { Name = nameof(TestSayHello) });
27+ await user.Client<Greeter.GreeterClient>().SayHelloAsync(new HelloRequest { Name = nameof(TestSayHello) });
28+ await user.Client<Greeter.GreeterClient>().SayHelloAsync(new HelloRequest { Name = nameof(TestSayHello) });
29+ await user.LogoutAsync();
30+ WriteLine("passed");
31+ }
32+ catch (Exception exception)
33+ {
34+ WriteLine($"{nameof(SayHelloAsync)} Exception {exception.Message}");
35+ }
36+ }
37+
38+ static void WriteLine(string message, [CallerMemberName] string caller = "")
39+ {
40+ Console.WriteLine($"{caller}: {message}");
41+ }
42+ }
43+}
--- SessionJwt/SL.SessionJwt.ConsoleClient/TokenCallInvoker.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.ConsoleClient/TokenCallInvoker.cs (revision 9)
@@ -0,0 +1,135 @@
1+using Grpc.Core;
2+using Grpc.Net.Client;
3+using System;
4+using System.Linq;
5+using System.Threading.Tasks;
6+
7+namespace SL.SessionJwt.ConsoleClient
8+{
9+ public class TokenCallInvoker : CallInvoker, IDisposable
10+ {
11+ public TokenCallInvoker(GrpcChannel channel)
12+ {
13+ Channel = channel;
14+ Invoker = Channel.CreateCallInvoker();
15+ MetaData = new Metadata();
16+ }
17+
18+ public GrpcChannel Channel { get; private set; }
19+
20+ public CallInvoker Invoker { get; private set; }
21+
22+ public Metadata MetaData { get; private set; }
23+
24+ public string Token { get; set; }
25+
26+ public bool WriteTokenToConsole { get; set; } // for testing
27+
28+ public bool ValidateNewTokenIsDifferent { get; set; } = true; // for testing
29+
30+ public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
31+ Method<TRequest, TResponse> method,
32+ string host,
33+ CallOptions options,
34+ TRequest request)
35+ {
36+ options = HandleCallOptions(options, method);
37+ var call = Invoker.AsyncUnaryCall(method, host, options, request);
38+ var result = new AsyncUnaryCall<TResponse>(
39+ AsyncUnaryResponseAsync(call),
40+ call.ResponseHeadersAsync,
41+ call.GetStatus,
42+ call.GetTrailers,
43+ call.Dispose);
44+ return result;
45+ }
46+
47+
48+ public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request)
49+ {
50+ throw new NotImplementedException();
51+ }
52+
53+ public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options)
54+ {
55+ throw new NotImplementedException();
56+ }
57+
58+ public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options)
59+ {
60+ throw new NotImplementedException();
61+ }
62+
63+ public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request)
64+ {
65+ throw new NotImplementedException();
66+ }
67+
68+ internal bool AddBearer(string token)
69+ {
70+ if (ValidateNewTokenIsDifferent &&
71+ token.Equals(Token, StringComparison.CurrentCultureIgnoreCase))
72+ throw new ArgumentOutOfRangeException(nameof(token));
73+
74+ var result = RemoveBearer();
75+ MetaData.Add(Constants.TokenName, Constants.BearerPrefix + token);
76+ Token = token;
77+ return result;
78+ }
79+
80+ internal bool RemoveBearer()
81+ {
82+ var result = default(bool);
83+ var item = MetaData.FirstOrDefault(e => e.Key.Equals(Constants.TokenName, StringComparison.InvariantCultureIgnoreCase));
84+ if (null != item)
85+ result = MetaData.Remove(item);
86+ Token = String.Empty;
87+ return result;
88+ }
89+
90+ private CallOptions HandleCallOptions<TRequest, TResponse>(CallOptions options, Method<TRequest, TResponse> method)
91+ {
92+ if (null == options.Headers)
93+ options = new CallOptions(new Metadata(),
94+ options.Deadline,
95+ options.CancellationToken,
96+ options.WriteOptions,
97+ options.PropagationToken);
98+ foreach (var item in MetaData)
99+ options.Headers.Add(item.Key, item.Value);
100+
101+ if (WriteTokenToConsole && MetaData.Count > 0)
102+ {
103+ var token = MetaData[0].Value.Substring(Constants.BearerPrefix.Length, 20) + " " + MetaData[0].Value.Substring(MetaData[0].Value.Length - 20);
104+ Console.WriteLine($"{method.ServiceName} call with {token}");
105+ }
106+ return options;
107+ }
108+
109+ private async Task<TResponse> AsyncUnaryResponseAsync<TResponse>(AsyncUnaryCall<TResponse> underlying) where TResponse : class
110+ {
111+ var result = await underlying.ResponseAsync;
112+ var trailers = underlying.GetTrailers();
113+ var test = underlying.ResponseHeadersAsync;
114+ RefreshToken(trailers);
115+ return result;
116+ }
117+
118+ private void RefreshToken(Metadata trailers)
119+ {
120+ var tokenPair = trailers.FirstOrDefault(e => Constants.TokenName.Equals(e.Key, StringComparison.InvariantCultureIgnoreCase));
121+ if (null != tokenPair)
122+ {
123+ var removed = AddBearer(tokenPair.Value);
124+ if(WriteTokenToConsole)
125+ Console.WriteLine($"Token refreshed." + (true == removed ? "(Old Token removed.)" : ""));
126+ }
127+ }
128+
129+ public void Dispose()
130+ {
131+ Channel?.Dispose();
132+ Channel = null;
133+ }
134+ }
135+}
--- SessionJwt/SL.SessionJwt.ConsoleClient/UserProxy.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.ConsoleClient/UserProxy.cs (revision 9)
@@ -0,0 +1,76 @@
1+using Grpc.Core;
2+using Grpc.Net.Client;
3+using System;
4+using System.Linq;
5+using System.Collections.Generic;
6+using System.Threading.Tasks;
7+using static SL.SessionJwt.Service.Authentication;
8+using SL.SessionJwt.Service;
9+
10+namespace SL.SessionJwt.ConsoleClient
11+{
12+ public class UserProxy : IDisposable
13+ {
14+ public UserProxy(string userName)
15+ {
16+ UserName = userName;
17+ Invoker = new TokenCallInvoker(GrpcChannel.ForAddress(Constants.RemoteEndPoint));
18+ }
19+
20+ public string UserName { get; private set; }
21+
22+ public TokenCallInvoker Invoker { get; private set; }
23+
24+ internal List<ClientBase> Clients { get; private set; } = new List<ClientBase>();
25+
26+ public virtual T Client<T>() where T : ClientBase
27+ {
28+ var client = Clients.FirstOrDefault(e => e.GetType() == typeof(T)) as T;
29+ if (null == client)
30+ {
31+ client = (T)Activator.CreateInstance(typeof(T), Invoker);
32+ Clients.Add(client);
33+ }
34+ return client;
35+ }
36+
37+ public virtual async Task RenewAsync()
38+ {
39+ using (var channel = GrpcChannel.ForAddress(Constants.RemoteEndPoint))
40+ {
41+ var client = new AuthenticationClient(channel);
42+ var response = await client.RenewAsync(new RenewRequest { Token = Invoker.Token });
43+ Invoker.AddBearer(response.Token);
44+ }
45+ }
46+
47+ public virtual async Task LogonAsync()
48+ {
49+ using (var channel = GrpcChannel.ForAddress(Constants.RemoteEndPoint))
50+ {
51+ var client = new AuthenticationClient(channel);
52+ var response = await client.LogonAsync(new LogonRequest { Name = UserName });
53+ Invoker.AddBearer(response.Token);
54+ }
55+ }
56+
57+ public virtual async Task<bool> LogoutAsync(bool keepToken = false) // keepToken for greybox testing purpose
58+ {
59+ using (var channel = GrpcChannel.ForAddress(Constants.RemoteEndPoint))
60+ {
61+ var client = new AuthenticationClient(channel);
62+ var result = await client.LogoutAsync(new Google.Protobuf.WellKnownTypes.Empty(), Invoker.MetaData);
63+ if(!keepToken)
64+ Invoker.RemoveBearer();
65+ return await Task.FromResult(result.HasLoggedOut);
66+ }
67+ }
68+
69+ public virtual void Dispose()
70+ {
71+ Clients.Clear();
72+ Invoker?.Dispose();
73+ Invoker = null;
74+ }
75+ }
76+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtClaimReader.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtClaimReader.cs (revision 9)
@@ -0,0 +1,33 @@
1+using System;
2+using System.Collections.Generic;
3+using System.IdentityModel.Tokens.Jwt;
4+using System.Linq;
5+using System.Security.Claims;
6+
7+namespace SL.SessionJwt.Service
8+{
9+ public class JwtClaimReader
10+ {
11+ public JwtClaimReader(IEnumerable<Claim> claims)
12+ {
13+ Claims = claims ?? throw new ArgumentNullException(nameof(claims));
14+ }
15+
16+ public IEnumerable<Claim> Claims { get; private set; }
17+
18+ public virtual string SessionId()
19+ {
20+ return Claims.First(e => e.Type == JwtRegisteredClaimNames.Jti).Value.ToString();
21+ }
22+
23+ public virtual string Userid()
24+ {
25+ return Claims.First(e => e.Type == JwtRegisteredClaimNames.NameId).Value.ToString();
26+ }
27+
28+ public virtual string Aud()
29+ {
30+ return Claims.First(e => e.Type == JwtRegisteredClaimNames.Aud).Value.ToString();
31+ }
32+ }
33+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtClaimWriter.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtClaimWriter.cs (revision 9)
@@ -0,0 +1,49 @@
1+using SL.SessionJwt.Service.Model;
2+using System;
3+using System.Collections.Generic;
4+using System.IdentityModel.Tokens.Jwt;
5+using System.Linq;
6+using System.Security.Claims;
7+using System.Threading.Tasks;
8+
9+namespace SL.SessionJwt.Service
10+{
11+ public class JwtClaimWriter
12+ {
13+ public JwtClaimWriter(string userAgent, string userId, string userName, string sessionId, IEnumerable<Role> roles)
14+ {
15+ UserAgent = userAgent ?? throw new ArgumentNullException(nameof(userAgent));
16+ UserId = userId ?? throw new ArgumentNullException(nameof(userId));
17+ UserName = userName ?? throw new ArgumentNullException(nameof(userName));
18+ SessionId = sessionId ?? throw new ArgumentNullException(nameof(sessionId));
19+ Roles = roles ?? throw new ArgumentNullException(nameof(roles));
20+ }
21+
22+ public string UserAgent { get; private set; }
23+
24+ public string UserId { get; private set; }
25+
26+ public string UserName { get; private set; }
27+
28+ public string SessionId { get; private set; }
29+
30+ public IEnumerable<Role> Roles { get; private set; }
31+
32+ public virtual async Task<List<Claim>> WriteClaimsForSession()
33+ {
34+ var claims = await Task.Run<List<Claim>>(() =>
35+ {
36+ var claims = new List<Claim>()
37+ {
38+ new Claim(JwtRegisteredClaimNames.Aud, UserAgent),
39+ new Claim(ClaimTypes.NameIdentifier, UserId),
40+ new Claim(ClaimTypes.Name, UserName),
41+ new Claim(JwtRegisteredClaimNames.Jti, SessionId)
42+ };
43+ Roles.ToList().ForEach(e => claims.Add(new Claim(ClaimTypes.Role, e.ToString())));
44+ return claims;
45+ });
46+ return claims;
47+ }
48+ }
49+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtMetadataTokenReader.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtMetadataTokenReader.cs (revision 9)
@@ -0,0 +1,43 @@
1+using Grpc.Core;
2+using System;
3+using System.Linq;
4+using System.Threading.Tasks;
5+
6+namespace SL.SessionJwt.Service
7+{
8+ public class JwtMetadataTokenReader
9+ {
10+ public JwtMetadataTokenReader(Metadata meta)
11+ {
12+ Meta = meta ?? throw new ArgumentNullException(nameof(meta));
13+ }
14+
15+ public Metadata Meta { get; private set; }
16+
17+ public virtual async Task<string> Read()
18+ {
19+ return await Task.Run(() =>
20+ {
21+ var result = RecieveBearerToken(Meta);
22+ result = ValidateBearerToken(result);
23+ return result;
24+ });
25+ }
26+
27+ internal virtual string RecieveBearerToken(Metadata meta)
28+ {
29+ var result = default(string);
30+ var pair = meta.FirstOrDefault(e => Constants.AuthorizationHeader.Equals(e.Key, StringComparison.InvariantCultureIgnoreCase));
31+ if (null != pair)
32+ result = pair.Value;
33+ return result;
34+ }
35+
36+ internal virtual string ValidateBearerToken(string token)
37+ {
38+ return true == token?.StartsWith(Constants.BearerPrefix, StringComparison.InvariantCultureIgnoreCase)
39+ ? token.Substring(Constants.BearerPrefix.Length)
40+ : token;
41+ }
42+ }
43+}
\ No newline at end of file
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtMetadataUserAgentReader.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtMetadataUserAgentReader.cs (revision 9)
@@ -0,0 +1,30 @@
1+using Grpc.Core;
2+using System;
3+using System.Collections.Generic;
4+using System.Linq;
5+using System.Threading.Tasks;
6+
7+namespace SL.SessionJwt.Service
8+{
9+ public class JwtMetadataUserAgentReader
10+ {
11+ public JwtMetadataUserAgentReader(Metadata meta)
12+ {
13+ Meta = meta ?? throw new ArgumentNullException(nameof(meta));
14+ }
15+
16+ public Metadata Meta { get; private set; }
17+
18+ public virtual async Task<string> Read()
19+ {
20+ return await Task.Run(() =>
21+ {
22+ var result = default(string);
23+ var pair = Meta.FirstOrDefault(e => Constants.UserAgentHeader.Equals(e.Key, StringComparison.InvariantCultureIgnoreCase));
24+ if (null != pair)
25+ result = pair.Value;
26+ return result;
27+ });
28+ }
29+ }
30+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtSecurityTokenDecoder.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtSecurityTokenDecoder.cs (revision 9)
@@ -0,0 +1,32 @@
1+using System;
2+using System.IdentityModel.Tokens.Jwt;
3+using System.Threading.Tasks;
4+
5+namespace SL.SessionJwt.Service
6+{
7+ public class JwtSecurityTokenDecoder
8+ {
9+ public JwtSecurityTokenDecoder(string token)
10+ {
11+ if(String.IsNullOrWhiteSpace(token))
12+ throw new ArgumentNullException(nameof(token));
13+ Token = token;
14+ }
15+
16+ public string Token { get; private set; }
17+
18+ public virtual async Task<JwtSecurityToken> Decode()
19+ {
20+ return await Task.Run(() =>
21+ {
22+ var result = default(JwtSecurityToken);
23+ if (!String.IsNullOrWhiteSpace(Token))
24+ {
25+ var jwtHandler = new JwtSecurityTokenHandler();
26+ result = jwtHandler.ReadJwtToken(Token);
27+ }
28+ return result;
29+ });
30+ }
31+ }
32+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtSessionValidationResult.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtSessionValidationResult.cs (revision 9)
@@ -0,0 +1,12 @@
1+using System;
2+
3+namespace SL.SessionJwt.Service
4+{
5+ public enum JwtSessionValidationResult
6+ {
7+ Fine = 0,
8+ NoSession = 1,
9+ RolesChanged = 2,
10+ Error = 3
11+ }
12+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtSessionValidator.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtSessionValidator.cs (revision 9)
@@ -0,0 +1,59 @@
1+using Microsoft.Extensions.Logging;
2+using System;
3+using System.IdentityModel.Tokens.Jwt;
4+using System.Linq;
5+using System.Threading.Tasks;
6+
7+namespace SL.SessionJwt.Service
8+{
9+ public class JwtSessionValidator
10+ {
11+ public JwtSessionValidator(ILogger<JwtSessionValidator> logger, FakeUserRepository users)
12+ {
13+ Logger = logger;
14+ Users = users ?? throw new ArgumentNullException(nameof(users));
15+ }
16+
17+ public ILogger<JwtSessionValidator> Logger { get; private set; }
18+
19+ public FakeUserRepository Users { get; private set; }
20+
21+ public async Task<JwtSessionValidationResult> IsValidSession(JwtSecurityToken token)
22+ {
23+ try
24+ {
25+ var claims = new JwtClaimReader(token.Claims);
26+ var sessionId = claims.SessionId();
27+ var userId = claims.Userid();
28+
29+ var user = await Task.Run(() =>
30+ {
31+ return Users.FirstOrDefault(e => e.Id.Equals(userId));
32+ });
33+ if(null == user)
34+ return JwtSessionValidationResult.Error;
35+
36+ if (user.SessionId != sessionId)
37+ return JwtSessionValidationResult.NoSession;
38+
39+ var roleClaims = await Task.Run(() =>
40+ {
41+ return token.Claims.
42+ Where(e => e.Type == nameof(Model.Role).ToLower()).
43+ Select(e => e.Value.ToLower());
44+ });
45+
46+ if (roleClaims.Count() != user.Roles.Length
47+ && !user.Roles.Select(e => e.ToString().ToLower()).All(e => roleClaims.Contains(e)))
48+ return JwtSessionValidationResult.RolesChanged;
49+
50+ return JwtSessionValidationResult.Fine;
51+ }
52+ catch (Exception exception)
53+ {
54+ Logger.LogError(exception, nameof(IsValidSession));
55+ return JwtSessionValidationResult.Error;
56+ }
57+ }
58+ }
59+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtSigningTokenHandler.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtSigningTokenHandler.cs (revision 9)
@@ -0,0 +1,120 @@
1+using Microsoft.Extensions.Logging;
2+using Microsoft.IdentityModel.Tokens;
3+using System;
4+using System.Collections.Generic;
5+using System.IdentityModel.Tokens.Jwt;
6+using System.Linq;
7+using System.Security.Claims;
8+using System.Threading.Tasks;
9+
10+namespace SL.SessionJwt.Service
11+{
12+ public class JwtSigningTokenHandler : JwtSecurityTokenHandler
13+ {
14+ public JwtSigningTokenHandler(string token, TokenValidationParameters validationParameters)
15+ {
16+ Token = token ?? throw new ArgumentNullException(nameof(token));
17+ ValidationParameters = validationParameters ?? throw new ArgumentNullException(nameof(validationParameters));
18+ }
19+
20+ public string Token { get; private set; }
21+
22+ public TokenValidationParameters ValidationParameters { get; private set; }
23+
24+ public virtual async Task<JwtSecurityToken> ValidateWitoutLifetime()
25+ {
26+ return await Task.Run(() =>
27+ {
28+ var result = default(JwtSecurityToken);
29+ var jwtToken = SafeValidateSignature(Token, ValidationParameters);
30+ if (null != jwtToken)
31+ {
32+ if (SafeValidateTokenPayloadWithoutLifetime(jwtToken, ValidationParameters)
33+ && IsValidLifetimeOrExpiresWithinRenewPeriod(jwtToken))
34+ result = jwtToken;
35+ }
36+ return result;
37+ });
38+ }
39+
40+ internal JwtSecurityToken SafeValidateSignature(string token, TokenValidationParameters validationParameters)
41+ {
42+ var result = default(JwtSecurityToken);
43+ try
44+ {
45+ result = ValidateSignature(token, validationParameters);
46+ }
47+ catch
48+ {
49+ ;
50+ }
51+ return result;
52+ }
53+
54+ internal virtual bool SafeValidateTokenPayloadWithoutLifetime(JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
55+ {
56+ var result = default(bool);
57+ try
58+ {
59+ var expires = (!jwtToken.Payload.Exp.HasValue) ? null : new DateTime?(jwtToken.ValidTo);
60+ ValidateAudience(jwtToken.Audiences, jwtToken, validationParameters);
61+ string issuer = ValidateIssuer(jwtToken.Issuer, jwtToken, validationParameters);
62+ ValidateTokenReplay(expires, jwtToken.RawData, validationParameters);
63+ if (validationParameters.ValidateActor && !String.IsNullOrWhiteSpace(jwtToken.Actor))
64+ ValidateToken(jwtToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters, out SecurityToken _);
65+ ValidateIssuerSecurityKey(jwtToken.SigningKey, jwtToken, validationParameters);
66+ result = true;
67+ }
68+ catch
69+ {
70+ ;
71+ }
72+ return result;
73+ }
74+
75+ internal virtual bool ValidateLifetime(DateTime? notBefore, DateTime? expires,
76+ SecurityToken securityToken,
77+ TokenValidationParameters validationParameters)
78+ {
79+ if (validationParameters == null)
80+ return false;
81+ if (!validationParameters.ValidateLifetime)
82+ return false;
83+
84+ if (validationParameters.LifetimeValidator != null)
85+ {
86+ if (validationParameters.LifetimeValidator(notBefore, expires, securityToken, validationParameters))
87+ return false;
88+
89+ }
90+ if (!expires.HasValue && validationParameters.RequireExpirationTime)
91+ return false;
92+ if (notBefore.HasValue && expires.HasValue && notBefore.Value > expires.Value)
93+ return false;
94+
95+ DateTime utcNow = DateTime.UtcNow;
96+ if (notBefore.HasValue && notBefore.Value > DateTimeUtil.Add(utcNow, validationParameters.ClockSkew))
97+ return false;
98+
99+ if (expires.HasValue && expires.Value < DateTimeUtil.Add(utcNow, validationParameters.ClockSkew.Negate()))
100+ return false;
101+
102+ return true;
103+ }
104+ internal virtual bool IsValidLifetimeOrExpiresWithinRenewPeriod(JwtSecurityToken jwtToken)
105+ {
106+ var result = default(bool);
107+
108+ var notBefore = (!jwtToken.Payload.Nbf.HasValue) ? null : new DateTime?(jwtToken.ValidFrom);
109+ var expires = (!jwtToken.Payload.Exp.HasValue) ? null : new DateTime?(jwtToken.ValidTo);
110+
111+ if (!ValidateLifetime(notBefore, expires, jwtToken, ValidationParameters)
112+ && expires.HasValue && notBefore.HasValue)
113+ result = ((DateTime.UtcNow - expires.Value).TotalSeconds <= Constants.RenewPeriodSeconds);
114+ else
115+ result = true;
116+
117+ return result;
118+ }
119+ }
120+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtTokenBuilder.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtTokenBuilder.cs (revision 9)
@@ -0,0 +1,59 @@
1+using Microsoft.IdentityModel.Tokens;
2+using System;
3+using System.Collections.Generic;
4+using System.IdentityModel.Tokens.Jwt;
5+using System.Linq;
6+using System.Security.Claims;
7+using System.Threading.Tasks;
8+
9+namespace SL.SessionJwt.Service
10+{
11+ public class JwtTokenBuilder
12+ {
13+ public JwtTokenBuilder(IEnumerable<Claim> claims)
14+ {
15+ Claims = claims ?? throw new ArgumentNullException(nameof(claims));
16+ AddByteMagicToClaims();
17+ }
18+
19+ public IEnumerable<Claim> Claims { get; private set; }
20+
21+ public virtual async Task<string> BuildTokenString()
22+ {
23+ var descriptor = await CreateTokenDescriptor();
24+ return await Task.Run(() =>
25+ {
26+ var tokenHandler = new JwtSecurityTokenHandler();
27+ var token = tokenHandler.CreateToken(descriptor);
28+ return tokenHandler.WriteToken(token);
29+ });
30+ }
31+
32+ internal virtual async Task<SecurityTokenDescriptor> CreateTokenDescriptor()
33+ {
34+ return await Task.Run(() =>
35+ {
36+ var tokenDescriptor = new SecurityTokenDescriptor()
37+ {
38+ Subject = new ClaimsIdentity(Claims),
39+ NotBefore = DateTime.UtcNow,
40+ IssuedAt = DateTime.UtcNow,
41+ Issuer = typeof(JwtTokenBuilder).Assembly.FullName,
42+ Expires = DateTime.UtcNow.AddSeconds(Constants.SessionTimeoutSeconds),
43+ SigningCredentials = new SigningCredentials(
44+ new SymmetricSecurityKey(Constants.SigningKey),
45+ SecurityAlgorithms.HmacSha256)
46+ };
47+ return tokenDescriptor;
48+ });
49+ }
50+
51+ // see Constants.cs why this is useful
52+ private void AddByteMagicToClaims()
53+ {
54+ var list = Claims.ToList();
55+ list.Add(new Claim("Magic", new Guid().ToString().Replace("-", "")));
56+ Claims = list;
57+ }
58+ }
59+}
--- SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtTokenEvents.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/JwtExtensions/JwtTokenEvents.cs (revision 9)
@@ -0,0 +1,44 @@
1+using System;
2+using System.Collections.Generic;
3+using System.IdentityModel.Tokens.Jwt;
4+using System.Linq;
5+using System.Threading.Tasks;
6+using Grpc.Core;
7+using Microsoft.AspNetCore.Authentication.JwtBearer;
8+using Microsoft.AspNetCore.Http;
9+
10+namespace SL.SessionJwt.Service.JwtExtensions
11+{
12+ public class JwtTokenEvents : JwtBearerEvents
13+ {
14+ public JwtTokenEvents(FakeUserRepository users, JwtSessionValidator validator)
15+ {
16+ Users = users ?? throw new ArgumentNullException(nameof(users));
17+ Validator = validator ?? throw new ArgumentNullException(nameof(validator));
18+ }
19+
20+ public JwtSessionValidator Validator { get; private set; }
21+
22+ public FakeUserRepository Users { get; private set; }
23+
24+ public override async Task TokenValidated(TokenValidatedContext context)
25+ {
26+ var jwtToken = context.SecurityToken as JwtSecurityToken;
27+ if (null != jwtToken)
28+ {
29+ var result = await Validator?.IsValidSession(jwtToken);
30+ if (result == JwtSessionValidationResult.NoSession)
31+ throw new RpcException(new Status(StatusCode.Aborted, Constants.SessionValidateLoggedOutMessage));
32+ else if (result == JwtSessionValidationResult.RolesChanged)
33+ throw new RpcException(new Status(StatusCode.Aborted, Constants.SessionValidateRolesChangedMessage));
34+ else if (result == JwtSessionValidationResult.Error)
35+ throw new RpcException(new Status(StatusCode.Internal, Constants.SessionValidateErrorMessage));
36+
37+ var tokenString = await new JwtTokenBuilder(
38+ jwtToken.Claims.Where(e => !ClaimTlas.TimeTlas.Contains(e.Type))).BuildTokenString();
39+
40+ context.Response.AppendTrailer(Constants.AuthorizationHeader, tokenString);
41+ }
42+ }
43+ }
44+}
\ No newline at end of file
--- SessionJwt/SL.SessionJwt.Service/Model/Role.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/Model/Role.cs (revision 9)
@@ -0,0 +1,14 @@
1+using System;
2+using System.Collections.Generic;
3+using System.Linq;
4+using System.Threading.Tasks;
5+
6+namespace SL.SessionJwt.Service.Model
7+{
8+ public enum Role
9+ {
10+ Customer = 0,
11+ Employee = 1,
12+ Administrator = 3
13+ }
14+}
--- SessionJwt/SL.SessionJwt.Service/Model/User.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/Model/User.cs (revision 9)
@@ -0,0 +1,25 @@
1+using System;
2+using System.Collections.Generic;
3+using System.Linq;
4+using System.Threading.Tasks;
5+
6+namespace SL.SessionJwt.Service.Model
7+{
8+ public class User
9+ {
10+ public User(Guid id, string name, params Role[] roles)
11+ {
12+ Id = id.ToString().ToLower();
13+ Name = name ?? throw new ArgumentNullException(nameof(name));
14+ Roles = roles;
15+ }
16+
17+ public string Id { get; private set; }
18+
19+ public string Name { get; private set; }
20+
21+ public Role[] Roles { get; private set; }
22+
23+ public string SessionId { get; internal set; }
24+ }
25+}
--- SessionJwt/SL.SessionJwt.Service/Properties/launchSettings.json (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/Properties/launchSettings.json (revision 9)
@@ -0,0 +1,12 @@
1+{
2+ "profiles": {
3+ "SL.JwtLifetime.Service": {
4+ "commandName": "Project",
5+ "launchBrowser": false,
6+ "applicationUrl": "https://localhost:5001",
7+ "environmentVariables": {
8+ "ASPNETCORE_ENVIRONMENT": "Development"
9+ }
10+ }
11+ }
12+}
--- SessionJwt/SL.SessionJwt.Service/Services/AuthenticationService.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/Services/AuthenticationService.cs (revision 9)
@@ -0,0 +1,80 @@
1+using System;
2+using System.Linq;
3+using System.Threading.Tasks;
4+using Google.Protobuf.WellKnownTypes;
5+using Grpc.Core;
6+
7+namespace SL.SessionJwt.Service.Services
8+{
9+ public class AuthenticationService : Authentication.AuthenticationBase
10+ {
11+ public AuthenticationService(FakeUserRepository users)
12+ {
13+ Users = users;
14+ }
15+
16+ public FakeUserRepository Users { get; private set; }
17+
18+ public override async Task<LogonReply> Logon(LogonRequest request, ServerCallContext context)
19+ {
20+ return await Task.Run(async () =>
21+ {
22+ var user = await Users.FindUserByName<RpcException>(request.Name, new Status(StatusCode.InvalidArgument, String.Empty));
23+ var sessionId = Guid.NewGuid().ToString();
24+ var userAgent = await new JwtMetadataUserAgentReader(context.RequestHeaders).Read();
25+ var claims = await new JwtClaimWriter(userAgent, user.Id, user.Name, sessionId, user.Roles).
26+ WriteClaimsForSession();
27+ var tokenString = await new JwtTokenBuilder(claims).BuildTokenString();
28+ user.SessionId = sessionId;
29+ return new LogonReply { Token = tokenString };
30+ });
31+ }
32+
33+ public override async Task<LogoutReply> Logout(Empty request, ServerCallContext context)
34+ {
35+ return await Task.Run(async() =>
36+ {
37+ var result = new LogoutReply();
38+ var bearerToken = await new JwtMetadataTokenReader(context.RequestHeaders).Read();
39+ var jwtToken = null != bearerToken ? await new JwtSecurityTokenDecoder(bearerToken).Decode() : null;
40+ if (null != jwtToken)
41+ {
42+ var claims = new JwtClaimReader(jwtToken.Claims);
43+ var sessionId = claims.SessionId();
44+ var user = Users.FirstOrDefault(e => e.SessionId.ToString() == sessionId);
45+ if (null != user)
46+ {
47+ user.SessionId = null;
48+ result.HasLoggedOut = true;
49+ }
50+ }
51+ return result;
52+ });
53+ }
54+
55+ public override async Task<RenewReply> Renew(RenewRequest request, ServerCallContext context)
56+ {
57+ return await Task.Run(async () =>
58+ {
59+ var handler = new JwtSigningTokenHandler(request.Token, Constants.TokenValidation);
60+ var token = await handler.ValidateWitoutLifetime();
61+ if (null != token)
62+ {
63+ var claimReader = new JwtClaimReader(token.Claims);
64+ var sessionId = claimReader.SessionId();
65+ var aud = claimReader.Aud();
66+ var user = Users.FirstOrDefault(e => e.SessionId == sessionId);
67+ if (null != user)
68+ {
69+ var claims = await new JwtClaimWriter(aud, user.Id, user.Name, sessionId, user.Roles).
70+ WriteClaimsForSession();
71+ var tokenString = await new JwtTokenBuilder(claims).BuildTokenString();
72+ return new RenewReply { Token = tokenString };
73+ }
74+ }
75+
76+ throw new RpcException(new Status(StatusCode.Unauthenticated, Constants.RenewErrorMessage));
77+ });
78+ }
79+ }
80+}
--- SessionJwt/SL.SessionJwt.Service/Services/GreeterService.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/Services/GreeterService.cs (revision 9)
@@ -0,0 +1,23 @@
1+using System;
2+using System.Collections.Generic;
3+using System.Linq;
4+using System.Threading.Tasks;
5+using Grpc.Core;
6+using Microsoft.AspNetCore.Authorization;
7+using Microsoft.Extensions.Logging;
8+using SL.SessionJwt.Service.Model;
9+
10+namespace SL.SessionJwt.Service.Services
11+{
12+ public class GreeterService : Greeter.GreeterBase
13+ {
14+ [Authorize(Roles = "Administrator,Employee,Customer")]
15+ public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
16+ {
17+ return Task.FromResult(new HelloReply
18+ {
19+ Message = "Hello " + request.Name
20+ });
21+ }
22+ }
23+}
--- SessionJwt/SL.SessionJwt.Service/Constants.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/Constants.cs (revision 9)
@@ -0,0 +1,61 @@
1+using Microsoft.IdentityModel.Tokens;
2+using System;
3+using System.Text;
4+
5+namespace SL.SessionJwt.Service
6+{
7+ public static class Constants
8+ {
9+ public static readonly int SessionTimeoutSeconds = 60;
10+
11+ public static readonly int RenewPeriodSeconds = 60;
12+
13+ public static readonly byte[] SigningKey = Encoding.UTF8.GetBytes("the password is swordfish"); // dont store here in real scenario
14+
15+ public static readonly TokenValidationParameters TokenValidation = new TokenValidationParameters
16+ {
17+ RequireSignedTokens = true,
18+ ValidateIssuerSigningKey = true,
19+ IssuerSigningKey = new SymmetricSecurityKey(SigningKey),
20+ ValidateIssuer = false,
21+ ValidateAudience = false,
22+ ValidateLifetime = true,
23+ ValidateActor = true,
24+ RequireExpirationTime = true,
25+ RequireAudience = true,
26+ ClockSkew = TimeSpan.FromSeconds(1)
27+ };
28+
29+ public static readonly string SessionValidateLoggedOutMessage = "Specified session not found.";
30+
31+ public static readonly string SessionValidateRolesChangedMessage = "Specified roles has been changed during session. Please reauthenticate.";
32+
33+ public static readonly string SessionValidateErrorMessage = "An error has occured. This indicates user is already deleted.";
34+
35+ public static readonly string RenewErrorMessage = $"Session is may over or user is already deleted. Moreover you can renew an outdated tokie only within {RenewPeriodSeconds} seconds.";
36+
37+ public static readonly string BearerPrefix = "Bearer ";
38+
39+ public static readonly string AuthorizationHeader = "Authorization";
40+
41+ public static readonly string UserAgentHeader = "user-agent";
42+
43+ /*
44+ This is a hotfix for testing purpose to make sure a regenerated token from JwtTokenEvents
45+ is different from each other because IssuedAt/Expires claim Utc time accuracy is only seconds.
46+ So if we ran the tests localy very fast, we got the same token which is an issue for testing.
47+ */
48+ public static readonly string MagicClaim = "Magic ";
49+ }
50+
51+ public static class ClaimTlas
52+ {
53+ public static readonly string Nbf = "nbf";
54+
55+ public static readonly string Exp = "exp";
56+
57+ public static readonly string Iat = "iat";
58+
59+ public static readonly string[] TimeTlas = { Nbf, Exp, Iat };
60+ }
61+}
--- SessionJwt/SL.SessionJwt.Service/FakeUserRepository.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/FakeUserRepository.cs (revision 9)
@@ -0,0 +1,44 @@
1+using System;
2+using System.Collections;
3+using System.Collections.Generic;
4+using System.Linq;
5+using System.Threading.Tasks;
6+using SL.SessionJwt.Service.Model;
7+
8+namespace SL.SessionJwt.Service
9+{
10+ public class FakeUserRepository : IEnumerable<User>
11+ {
12+ private List<User> _users = new List<User>();
13+
14+ public FakeUserRepository()
15+ {
16+ _users.Add(new User(Guid.NewGuid(), "User1", Role.Customer));
17+ _users.Add(new User(Guid.NewGuid(), "User2", Role.Employee));
18+ _users.Add(new User(Guid.NewGuid(), "User3", Role.Administrator));
19+ _users.Add(new User(Guid.NewGuid(), "User4", Role.Customer, Role.Employee));
20+ _users.Add(new User(Guid.NewGuid(), "User5", Role.Customer, Role.Employee, Role.Administrator));
21+ }
22+
23+ public async Task<User> FindUserByName<T>(string name, params object[] exceptionArguments) where T:Exception
24+ {
25+ return await Task<User>.Run(() =>
26+ {
27+ var result = _users.FirstOrDefault(e => e.Name == name);
28+ if (null == result)
29+ throw (Exception)Activator.CreateInstance(typeof(T), exceptionArguments);
30+ return result;
31+ });
32+ }
33+
34+ public IEnumerator<User> GetEnumerator()
35+ {
36+ return _users.GetEnumerator();
37+ }
38+
39+ IEnumerator IEnumerable.GetEnumerator()
40+ {
41+ return _users.GetEnumerator();
42+ }
43+ }
44+}
--- SessionJwt/SL.SessionJwt.Service/Program.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/Program.cs (revision 9)
@@ -0,0 +1,25 @@
1+using System;
2+using System.Collections.Generic;
3+using System.IO;
4+using System.Linq;
5+using System.Threading.Tasks;
6+using Microsoft.AspNetCore.Hosting;
7+using Microsoft.Extensions.Hosting;
8+
9+namespace SL.SessionJwt.Service
10+{
11+ public class Program
12+ {
13+ public static void Main(string[] args)
14+ {
15+ CreateHostBuilder(args).Build().Run();
16+ }
17+
18+ public static IHostBuilder CreateHostBuilder(string[] args) =>
19+ Host.CreateDefaultBuilder(args)
20+ .ConfigureWebHostDefaults(webBuilder =>
21+ {
22+ webBuilder.UseStartup<Startup>();
23+ });
24+ }
25+}
--- SessionJwt/SL.SessionJwt.Service/Startup.cs (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/Startup.cs (revision 9)
@@ -0,0 +1,86 @@
1+using System;
2+using Auth = Microsoft.AspNetCore.Authentication;
3+using Microsoft.AspNetCore.Authentication.JwtBearer;
4+using Microsoft.AspNetCore.Builder;
5+using Microsoft.AspNetCore.Hosting;
6+using Microsoft.AspNetCore.Http;
7+using Microsoft.Extensions.DependencyInjection;
8+using Microsoft.Extensions.Hosting;
9+using Microsoft.IdentityModel.Tokens;
10+using SL.SessionJwt.Service.Services;
11+using Microsoft.AspNetCore.Authorization;
12+using Microsoft.IdentityModel.JsonWebTokens;
13+using System.Security.Claims;
14+using System.Linq;
15+using SL.SessionJwt.Service.JwtExtensions;
16+
17+namespace SL.SessionJwt.Service
18+{
19+ public class Startup
20+ {
21+ public void ConfigureServices(IServiceCollection services)
22+ {
23+ services.AddSingleton<FakeUserRepository>();
24+ services.AddSingleton<JwtSessionValidator>();
25+ services.AddSingleton<JwtTokenEvents>();
26+
27+ //services.AddCors(o => o.AddPolicy("MyPolicy", builder =>
28+ //{
29+ // builder.AllowAnyOrigin()
30+ // .AllowAnyMethod()
31+ // .AllowAnyHeader()
32+ // .WithExposedHeaders("Token-Expired");
33+ //}));
34+
35+ services.AddGrpc();
36+
37+ services.AddAuthentication(x =>
38+ {
39+ x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
40+ x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
41+ })
42+ .AddJwtBearer(x =>
43+ {
44+ x.EventsType = typeof(JwtTokenEvents);
45+ x.RequireHttpsMetadata = false;
46+ x.SaveToken = false;
47+ x.TokenValidationParameters = Constants.TokenValidation;
48+ });
49+
50+ services.AddAuthorization(options =>
51+ {
52+ options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
53+ {
54+ policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
55+ policy.RequireClaim(ClaimTypes.Name);
56+ policy.RequireClaim(ClaimTypes.NameIdentifier);
57+ policy.RequireClaim(JwtRegisteredClaimNames.Jti);
58+ });
59+ });
60+ }
61+
62+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
63+ {
64+ if (env.IsDevelopment())
65+ {
66+ app.UseDeveloperExceptionPage();
67+ }
68+
69+ //app.UseCors("MyPolicy");
70+ app.UseRouting();
71+ app.UseAuthentication();
72+ app.UseAuthorization();
73+
74+
75+ app.UseEndpoints(endpoints =>
76+ {
77+ endpoints.MapGrpcService<AuthenticationService>();
78+ endpoints.MapGrpcService<GreeterService>();
79+ endpoints.MapGet("/", async context =>
80+ {
81+ await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
82+ });
83+ });
84+ }
85+ }
86+}
--- SessionJwt/SL.SessionJwt.Service/appsettings.Development.json (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/appsettings.Development.json (revision 9)
@@ -0,0 +1,10 @@
1+{
2+ "Logging": {
3+ "LogLevel": {
4+ "Default": "Debug",
5+ "System": "Information",
6+ "Grpc": "Information",
7+ "Microsoft": "Information"
8+ }
9+ }
10+}
--- SessionJwt/SL.SessionJwt.Service/appsettings.json (nonexistent)
+++ SessionJwt/SL.SessionJwt.Service/appsettings.json (revision 9)
@@ -0,0 +1,15 @@
1+{
2+ "Logging": {
3+ "LogLevel": {
4+ "Default": "Information",
5+ "Microsoft": "Warning",
6+ "Microsoft.Hosting.Lifetime": "Information"
7+ }
8+ },
9+ "AllowedHosts": "*",
10+ "Kestrel": {
11+ "EndpointDefaults": {
12+ "Protocols": "Http2"
13+ }
14+ }
15+}
--- SessionJwt/ReadMe.md (nonexistent)
+++ SessionJwt/ReadMe.md (revision 9)
@@ -0,0 +1,52 @@
1+What & Why)
2+
3+This poc is an extented usage of jwt
4+to proof a jwt is usable as session cookie too.
5+I call that a tookie.
6+
7+I use a 1 minute session timeout by inactivity here and i send an
8+updated tookie back to the client at each call in the response header.
9+
10+Moreover a client can use an outdated tookie to
11+recreate a session if tookie is valid/signed and not older than 2 minutes,
12+we dont use a so called refresh token for that technique.
13+This recreation is not possible if client does a regular logout before,
14+only in case of session timeout.
15+
16+
17+Please note:
18+1) Jwt's are litmited in size, think about before u plan to use it as cookie too.
19+2) Blocking unary calls in Grpc client does not return any response headers
20+so we can only use async unary here. (not realy an issue i guess)
21+3.) I do not handle the other kind of calls in the client invoker:
22+AsyncServerStreamingCall, AsyncClientStreamingCall, AsyncDuplexStreamingCall
23+in this poc but its pretty easy to implement with shown AsyncUnaryCall as role model.
24+
25+
26+Service)
27+
28+The main job is performed in JwtExtensions by AuthenticationService & JwtTokenEvents.
29+We use an attached event class to JwtBearerHandler here instead of an interceptor to
30+avoid decode the token twice.
31+
32+
33+Client)
34+
35+The client use a custom invoker to encapsulate AsyncUnaryCall for
36+send and recieve tokens trough request and response header.
37+
38+
39+Optional)
40+
41+1) Change solution start settings to multiple and select server and client in this order.
42+2) Change session timeout values in service Constants.cs and wait time in client OutdatedTookie.cs
43+if testing tooks too long 4u.
44+
45+
46+Issues)
47+
48+1.) AuthenticationService.cs should look more fluent but its okay for a poc so far.
49+
50+
51+Thats all folks
52+If u see issues or found and error, please tell me.
\ No newline at end of file
--- SimpleJwt/SL.JwtTokens.Service/Services/GreeterService.cs (revision 8)
+++ SimpleJwt/SL.JwtTokens.Service/Services/GreeterService.cs (revision 9)
@@ -11,12 +11,13 @@
1111 public class GreeterService : Greeter.GreeterBase
1212 {
1313 private readonly ILogger<GreeterService> _logger;
14+
1415 public GreeterService(ILogger<GreeterService> logger)
1516 {
1617 _logger = logger;
1718 }
1819
19- [Authorize(Roles = "Administrator, Employee")]
20+ [Authorize(Roles = "Administrator, Employee, Customer")]
2021 public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
2122 {
2223 return Task.FromResult(new HelloReply
--- SimpleJwt/ReadMe.md (revision 8)
+++ SimpleJwt/ReadMe.md (revision 9)
@@ -16,7 +16,7 @@
1616 Service)
1717
1818 The Authentication service adds id, name and then user roles as role claims to the token.
19-Greeter service use the Authorize attribute so the jwtbearer can verify one of the roles in our token match
19+Greeter service use the Authorize attribute so the authorization can verify one of the roles in our token match
2020 and its allowed for the token to call SayHello. Remove 'Customer' Role from authorize attribute
2121 in SayHello to see it fails by permission denied. We also say id and name are required token claims.
2222
Show on old repository browser