From ebd5fbf00513ca51e729d30490e84e5c03fc3973 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Mon, 27 Mar 2023 10:42:05 +0200 Subject: [PATCH 01/26] Added doc to examples Slice interfaces --- examples/Authorization/Authorization.slice | 14 ++++++++------ examples/Compress/Hello.slice | 2 +- examples/Download/Downloader.slice | 3 +++ examples/HelloCore/Client/Program.cs | 4 ++-- examples/HelloLog/Server/Program.cs | 1 - examples/Interop/IceGrid/Hello.slice | 6 +++++- examples/Interop/Minimal/Hello.slice | 4 +++- examples/OpenTelemetry/CRM.slice | 4 ++++ examples/Secure/Client/Program.cs | 3 +-- examples/Stream/Generator.slice | 3 +++ examples/Stream/Server/Program.cs | 1 - examples/Upload/Uploader.slice | 3 +++ src/IceRpc/ConnectionCache.cs | 2 +- 13 files changed, 34 insertions(+), 16 deletions(-) diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index 8f61c6eee6..4501cfe062 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -5,6 +5,7 @@ module AuthorizationExample /// The authorization token typealias Token = sequence +/// Represents a service to create a session token. interface SessionManager { /// Creates a new session token. /// @param name: The user name. @@ -12,15 +13,16 @@ interface SessionManager { createSession(name: string) -> Token } -interface HelloAdmin { - /// Changes the greeting returned by the Hello service. - /// @param greeting: The new greeting. - changeGreeting(greeting: string) -} - /// Represents a simple greeter. interface Hello { /// Creates a personalized "hello" greeting. /// @returns: The greeting. sayHello() -> string } + +/// Represents a service to configure a simpler greeter. +interface HelloAdmin { + /// Changes the greeting returned by the Hello service. + /// @param greeting: The new greeting. + changeGreeting(greeting: string) +} diff --git a/examples/Compress/Hello.slice b/examples/Compress/Hello.slice index ce095d848b..207400da07 100644 --- a/examples/Compress/Hello.slice +++ b/examples/Compress/Hello.slice @@ -2,7 +2,7 @@ module CompressExample -/// Represents a simple greeter. +/// Represents a simple greeter that compresses the name argument and the greeting return value. interface Hello { /// Creates a personalized "hello" greeting. /// @param name: The name of the person to greet. diff --git a/examples/Download/Downloader.slice b/examples/Download/Downloader.slice index 3c103fd16a..8fb648b88c 100644 --- a/examples/Download/Downloader.slice +++ b/examples/Download/Downloader.slice @@ -2,6 +2,9 @@ module DownloadExample +/// Represents a downloader. interface Downloader { + /// Downloads an image. + /// @returns: The image as a byte stream. downloadImage() -> stream uint8 } diff --git a/examples/HelloCore/Client/Program.cs b/examples/HelloCore/Client/Program.cs index 1ad04e614b..5c4c21a638 100644 --- a/examples/HelloCore/Client/Program.cs +++ b/examples/HelloCore/Client/Program.cs @@ -12,8 +12,8 @@ Payload = StringCodec.EncodeString(Environment.UserName) }; -// Make the invocation: we send the request using the client connection and then wait for the response. -// Since the client connection is not connected yet, this call also connects the connection. +// Make the invocation: we send the request using the client connection and then wait for the response. Since the client +// connection is not connected yet, this call also connects the connection. IncomingResponse response = await connection.InvokeAsync(request); // When the response's status code is Success, we decode its payload. diff --git a/examples/HelloLog/Server/Program.cs b/examples/HelloLog/Server/Program.cs index 275e3ac622..6c9951de7a 100644 --- a/examples/HelloLog/Server/Program.cs +++ b/examples/HelloLog/Server/Program.cs @@ -13,7 +13,6 @@ // Create a server that logs messages using logger. await using var server = new Server(new Chatbot(), logger: logger); - server.Listen(); // Wait until the console receives a Ctrl+C. diff --git a/examples/Interop/IceGrid/Hello.slice b/examples/Interop/IceGrid/Hello.slice index 7fa9aad38f..f0b6151f8f 100644 --- a/examples/Interop/IceGrid/Hello.slice +++ b/examples/Interop/IceGrid/Hello.slice @@ -1,11 +1,15 @@ // Copyright (c) ZeroC, Inc. -// Use the Slice 1 encoding for compatibility with ZeroC Ice +// Use the Slice 1 encoding for compatibility with ZeroC Ice. encoding = 1 module Demo +/// Represents a simple greeter. interface Hello { + /// Requests the server to say hello. idempotent sayHello() + + /// Requests server shutdown. shutdown() } diff --git a/examples/Interop/Minimal/Hello.slice b/examples/Interop/Minimal/Hello.slice index 4e583a1a61..56379ce5dd 100644 --- a/examples/Interop/Minimal/Hello.slice +++ b/examples/Interop/Minimal/Hello.slice @@ -1,10 +1,12 @@ // Copyright (c) ZeroC, Inc. -// Use the Slice 1 encoding for compatibility with ZeroC Ice +// Use the Slice 1 encoding for compatibility with ZeroC Ice. encoding = 1 module Demo +/// Represents a simple greeter. interface Hello { + /// Requests the server to say hello. sayHello() } diff --git a/examples/OpenTelemetry/CRM.slice b/examples/OpenTelemetry/CRM.slice index 77d0884df7..5fdf54ad54 100644 --- a/examples/OpenTelemetry/CRM.slice +++ b/examples/OpenTelemetry/CRM.slice @@ -2,6 +2,10 @@ module OpenTelemetryExample +/// Represents a simple customer relationship management service. interface CRM { + /// Tries to add a customer. + /// @param name: The name of the customer. + /// @returns: true if the customer was added, false otherwise. tryAddCustomer(name: string) -> bool } diff --git a/examples/Secure/Client/Program.cs b/examples/Secure/Client/Program.cs index 4f7731b98c..fa1fb2c8c6 100644 --- a/examples/Secure/Client/Program.cs +++ b/examples/Secure/Client/Program.cs @@ -5,8 +5,7 @@ using System.Net.Security; using System.Security.Cryptography.X509Certificates; -// Create the authentication options with a custom certificate validation callback -// that uses our Root CA certificate. +// Create the authentication options with a custom certificate validation callback that uses our Root CA certificate. using var rootCA = new X509Certificate2("../../certs/cacert.der"); var clientAuthenticationOptions = new SslClientAuthenticationOptions() diff --git a/examples/Stream/Generator.slice b/examples/Stream/Generator.slice index bb58fd74b3..2f8f3457bf 100644 --- a/examples/Stream/Generator.slice +++ b/examples/Stream/Generator.slice @@ -2,6 +2,9 @@ module StreamExample +/// Represents a generator. interface Generator { + /// Generates numbers. + /// @returns: The generated number stream. generateNumbers() -> stream int32 } diff --git a/examples/Stream/Server/Program.cs b/examples/Stream/Server/Program.cs index d1dbe3aad3..fca26c5fc9 100644 --- a/examples/Stream/Server/Program.cs +++ b/examples/Stream/Server/Program.cs @@ -3,7 +3,6 @@ using IceRpc; using StreamExample; -using var cts = new CancellationTokenSource(); await using var server = new Server(new RandomGenerator()); server.Listen(); diff --git a/examples/Upload/Uploader.slice b/examples/Upload/Uploader.slice index ccc21a7afe..4bf7cb76a5 100644 --- a/examples/Upload/Uploader.slice +++ b/examples/Upload/Uploader.slice @@ -2,6 +2,9 @@ module UploadExample +/// Represents an uploader. interface Uploader { + /// Uploads an image. + /// @param image: The image byte stream. uploadImage(image: stream uint8) } diff --git a/src/IceRpc/ConnectionCache.cs b/src/IceRpc/ConnectionCache.cs index 4359c5a18a..9fd164b7da 100644 --- a/src/IceRpc/ConnectionCache.cs +++ b/src/IceRpc/ConnectionCache.cs @@ -605,7 +605,7 @@ private bool TryGetActiveConnection( } } - // A helper struct that implements and enumerator that allows iterating the addresses of a IServerAddressFeature + // A helper struct that implements an enumerator that allows iterating the addresses of an IServerAddressFeature // without allocations. private struct ServerAddressEnumerator { From 793b45a99f9f565e0b1dc4c9e6112539f2f180d3 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Mon, 27 Mar 2023 10:51:59 +0200 Subject: [PATCH 02/26] One more fix --- src/IceRpc/ConnectionCache.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IceRpc/ConnectionCache.cs b/src/IceRpc/ConnectionCache.cs index 9fd164b7da..b49416923d 100644 --- a/src/IceRpc/ConnectionCache.cs +++ b/src/IceRpc/ConnectionCache.cs @@ -605,8 +605,8 @@ private bool TryGetActiveConnection( } } - // A helper struct that implements an enumerator that allows iterating the addresses of an IServerAddressFeature - // without allocations. + /// A helper struct that implements an enumerator that allows iterating the addresses of an + /// without allocations. private struct ServerAddressEnumerator { internal ServerAddress Current From fc57cd5bb90fbb31dd23d7f420e5536506d61a80 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Mon, 27 Mar 2023 19:16:35 +0200 Subject: [PATCH 03/26] Better authorization demo --- examples/Authorization/Authorization.slice | 13 +++-- examples/Authorization/Client/Client.csproj | 2 +- .../Client/PipelineExtensions.cs | 10 ++-- examples/Authorization/Client/Program.cs | 58 +++++++++++-------- .../Client/SessionInterceptor.cs | 25 -------- examples/Authorization/Server/Chatbot.cs | 2 +- examples/Authorization/Server/ChatbotAdmin.cs | 2 + examples/Authorization/Server/Program.cs | 17 ++++-- .../Authorization/Server/RouterExtensions.cs | 9 +-- examples/Authorization/Server/Server.csproj | 2 +- examples/Authorization/Server/Session.cs | 49 ---------------- .../Authorization/Server/SessionMiddleware.cs | 48 --------------- examples/Authorization/SessionFieldKey.cs | 12 ---- 13 files changed, 68 insertions(+), 181 deletions(-) delete mode 100644 examples/Authorization/Client/SessionInterceptor.cs delete mode 100644 examples/Authorization/Server/Session.cs delete mode 100644 examples/Authorization/Server/SessionMiddleware.cs delete mode 100644 examples/Authorization/SessionFieldKey.cs diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index 4501cfe062..5549ed9908 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -2,15 +2,18 @@ module AuthorizationExample -/// The authorization token +/// The encrypted authentication token. typealias Token = sequence +exception AuthenticationException { } + /// Represents a service to create a session token. -interface SessionManager { - /// Creates a new session token. +interface Authenticator { + /// Authenticates a user. /// @param name: The user name. - /// @returns: The token. - createSession(name: string) -> Token + /// @param password: The password. + /// @returns: The encrypted authentication token. + authenticate(name: string, password: string) -> Token throws AuthenticationException } /// Represents a simple greeter. diff --git a/examples/Authorization/Client/Client.csproj b/examples/Authorization/Client/Client.csproj index 8b0394a2e6..7f2b715dab 100644 --- a/examples/Authorization/Client/Client.csproj +++ b/examples/Authorization/Client/Client.csproj @@ -4,6 +4,6 @@ - + diff --git a/examples/Authorization/Client/PipelineExtensions.cs b/examples/Authorization/Client/PipelineExtensions.cs index 8a15ce190a..44e2577ab7 100644 --- a/examples/Authorization/Client/PipelineExtensions.cs +++ b/examples/Authorization/Client/PipelineExtensions.cs @@ -4,14 +4,14 @@ namespace IceRpc; -/// This class provides an extension method to add a +/// This class provides an extension method to add an /// to a . internal static class PipelineExtensions { - /// Adds a to the pipeline. + /// Adds an to the pipeline. /// The pipeline being configured. - /// The session token. + /// The authentication token. /// The pipeline being configured. - internal static Pipeline UseSession(this Pipeline pipeline, Guid token) => - pipeline.Use(next => new SessionInterceptor(next, token)); + internal static Pipeline UseAuthenticationToken(this Pipeline pipeline, byte[] token) => + pipeline.Use(next => new AuthenticationTokenInterceptor(next, token)); } diff --git a/examples/Authorization/Client/Program.cs b/examples/Authorization/Client/Program.cs index 5ecbe17214..0be7fa7e9b 100644 --- a/examples/Authorization/Client/Program.cs +++ b/examples/Authorization/Client/Program.cs @@ -5,35 +5,45 @@ await using var connection = new ClientConnection(new Uri("icerpc://localhost")); -// A `Hello` proxy that doesn't use any authentication. An authentication token is not needed to call `SayHello`. +// A hello proxy that doesn't use any authentication will print a generic greeting. var unauthenticatedHelloProxy = new HelloProxy(connection, new Uri("icerpc:/hello")); - -// Unauthenticated hello; prints generic greeting. Console.WriteLine(await unauthenticatedHelloProxy.SayHelloAsync()); -// A `SessionManager` proxy that doesn't use any authentication. Used to create new session tokens. -var sessionManagerProxy = new SessionManagerProxy(connection, new Uri("icerpc:/sessionManager")); - -// Get an authentication token. The token is used to authenticate future requests. -var token = new Guid(await sessionManagerProxy.CreateSessionAsync("friend")); - -// Add an interceptor to the invocation pipeline that inserts the token into a request field. -Pipeline authenticatedPipeline = new Pipeline().UseSession(token).Into(connection); - -// A `Hello` proxy that uses the authentication pipeline. When an authentication token is used, `SayHello` -// will return a personalized greeting. -var helloProxy = new HelloProxy(authenticatedPipeline, new Uri("icerpc:/hello")); - -// A `HelloAdmin` proxy that uses the authentication pipeline. An authentication token is needed to change the greeting. -var helloAdminProxy = new HelloAdminProxy(authenticatedPipeline, new Uri("icerpc:/helloAdmin")); - -// Authenticated hello. -Console.WriteLine(await helloProxy.SayHelloAsync()); - -// Change the greeting using the authentication token. +// Authenticate the "friend" user to get its authentication token. +var authenticatorProxy = new AuthenticatorProxy(connection, new Uri("icerpc:/authenticator")); +byte[] friendToken = await authenticatorProxy.AuthenticateAsync("friend", "password"); + +// Setup a pipe line that inserts the "friend" token into a request field. +Pipeline friendPipeline = new Pipeline().UseAuthenticationToken(friendToken).Into(connection); + +// A hello proxy that uses "friend" pipeline will print a custom greeting for "friend". +var friendHelloProxy = new HelloProxy(friendPipeline, new Uri("icerpc:/hello")); +Console.WriteLine(await friendHelloProxy.SayHelloAsync()); + +// A hello admin proxy that uses the "friend" pipeline is not authorized to change the greeting message. +var helloAdminProxy = new HelloAdminProxy(friendPipeline, new Uri("icerpc:/helloAdmin")); +try +{ + await helloAdminProxy.ChangeGreetingAsync("Bonjour"); +} +catch (DispatchException exception) when (exception.StatusCode == StatusCode.Unauthorized) +{ + Console.WriteLine("The friend user is not authorized to change the greeting."); +} + +// Authenticate the "admin" user to get its authentication token and setup a pipeline with the "admin" token. +byte[] adminToken = await authenticatorProxy.AuthenticateAsync("admin", "password"); +helloAdminProxy = new HelloAdminProxy( + new Pipeline().UseAuthenticationToken(adminToken).Into(connection), + new Uri("icerpc:/helloAdmin")); + +// Change the greeting message. await helloAdminProxy.ChangeGreetingAsync("Bonjour"); // Authenticated hello with updated greeting. -Console.WriteLine(await helloProxy.SayHelloAsync()); +Console.WriteLine(await friendHelloProxy.SayHelloAsync()); + +// Change back the greeting to Hello. +await helloAdminProxy.ChangeGreetingAsync("Hello"); await connection.ShutdownAsync(); diff --git a/examples/Authorization/Client/SessionInterceptor.cs b/examples/Authorization/Client/SessionInterceptor.cs deleted file mode 100644 index 4176cc104d..0000000000 --- a/examples/Authorization/Client/SessionInterceptor.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using IceRpc; -using System.Buffers; - -namespace AuthorizationExample; - -/// An interceptor that adds a field with the authentication token to each request. -internal class SessionInterceptor : IInvoker -{ - private readonly IInvoker _next; - private readonly Guid _token; - - public Task InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken) - { - request.Fields = request.Fields.With(SessionFieldKey.Value, new ReadOnlySequence(_token.ToByteArray())); - return _next.InvokeAsync(request, cancellationToken); - } - - internal SessionInterceptor(IInvoker next, Guid token) - { - _next = next; - _token = token; - } -} diff --git a/examples/Authorization/Server/Chatbot.cs b/examples/Authorization/Server/Chatbot.cs index 04eb749de3..9c19800c59 100644 --- a/examples/Authorization/Server/Chatbot.cs +++ b/examples/Authorization/Server/Chatbot.cs @@ -12,7 +12,7 @@ internal class Chatbot : Service, IHelloService public ValueTask SayHelloAsync(IFeatureCollection features, CancellationToken cancellationToken) { - string who = features.Get()?.Name ?? "stranger"; + string who = features.Get()?.Name ?? "stranger"; Console.WriteLine($"Dispatching sayHello request {{ name = '{who}' }}"); return new($"{Greeting}, {who}!"); } diff --git a/examples/Authorization/Server/ChatbotAdmin.cs b/examples/Authorization/Server/ChatbotAdmin.cs index b5746612f6..eaa4c4fa58 100644 --- a/examples/Authorization/Server/ChatbotAdmin.cs +++ b/examples/Authorization/Server/ChatbotAdmin.cs @@ -18,6 +18,8 @@ public ValueTask ChangeGreetingAsync( IFeatureCollection features, CancellationToken cancellationToken) { + string who = features.Get()?.Name ?? "stranger"; + Console.WriteLine($"Dispatching changeGreeting request {{ name = '{who}' greeting = '{greeting}' }}"); _chatbot.Greeting = greeting; return default; } diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index 927c7151bd..2ed6d71837 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -2,23 +2,28 @@ using AuthorizationExample; using IceRpc; +using System.Security.Cryptography; + +// Use AES symmetric encryption to crypt the token. +using var aes = Aes.Create(); +aes.Padding = PaddingMode.Zeros; var chatbot = new Chatbot(); -var tokenStore = new TokenStore(); var router = new Router(); -// Loads the session token from the request and adds the session feature to the request's feature collection -router.UseLoadSession(tokenStore); +// Install a middleware to get and decrypt the authentication token from a request and to add the authentication feature +// to the request's feature collection. +router.UseAuthentication(aes); router.Route("/helloAdmin", adminRouter => { - // Requires the session feature to be present in the request's feature collection. - adminRouter.UseHasSession(); + // Install an authorization middleware to check if the caller is authorized to call the hello admin service. + adminRouter.UseAuthorization(authenticationFeature => authenticationFeature.IsAdmin); adminRouter.Map("/", new ChatbotAdmin(chatbot)); }); -router.Map("/sessionManager", tokenStore); +router.Map("/authenticator", new Authenticator(aes)); router.Map("/hello", chatbot); await using var server = new Server(router); diff --git a/examples/Authorization/Server/RouterExtensions.cs b/examples/Authorization/Server/RouterExtensions.cs index 74bffcc9d4..301afb297a 100644 --- a/examples/Authorization/Server/RouterExtensions.cs +++ b/examples/Authorization/Server/RouterExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) ZeroC, Inc. using AuthorizationExample; +using System.Security.Cryptography; namespace IceRpc; @@ -10,13 +11,13 @@ internal static class RouterExtensions /// Adds a to the router. /// The router being configured. /// The router being configured. - internal static Router UseHasSession(this Router router) => - router.Use(next => new HasSessionMiddleware(next)); + internal static Router UseAuthorization(this Router router, Func authorizeFunc) => + router.Use(next => new AuthorizationMiddleware(next, authorizeFunc)); /// Adds a to the router. /// The router being configured. /// The session token store. /// The router being configured. - internal static Router UseLoadSession(this Router router, TokenStore tokenStore) => - router.Use(next => new LoadSessionMiddleware(next, tokenStore)); + internal static Router UseAuthentication(this Router router, SymmetricAlgorithm cryptAlgorithm) => + router.Use(next => new AuthenticationMiddleware(next, cryptAlgorithm)); } diff --git a/examples/Authorization/Server/Server.csproj b/examples/Authorization/Server/Server.csproj index 475095c24a..e062c44fcb 100644 --- a/examples/Authorization/Server/Server.csproj +++ b/examples/Authorization/Server/Server.csproj @@ -4,7 +4,7 @@ - + diff --git a/examples/Authorization/Server/Session.cs b/examples/Authorization/Server/Session.cs deleted file mode 100644 index a1315f1455..0000000000 --- a/examples/Authorization/Server/Session.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using IceRpc.Features; -using IceRpc.Slice; -using System.Collections.Concurrent; - -namespace AuthorizationExample; - -/// A feature that stores a user's name. -public interface ISessionFeature -{ - string Name { get; } -} - -/// The implementation of . -internal class SessionFeature : ISessionFeature -{ - public string Name { get; } - - public SessionFeature(string name) => Name = name; -} - -/// The token store holds the session token to name dictionary and implements the -/// interface. -internal class TokenStore : Service, ISessionManagerService -{ - private readonly ConcurrentDictionary _sessions = new(); - - public ValueTask> CreateSessionAsync( - string name, - IFeatureCollection features, - CancellationToken cancellationToken) => new(CreateToken(name).ToByteArray()); - - /// Gets the name associated with the given session token. - /// The session token - /// The name. - internal string? GetName(Guid token) => _sessions.TryGetValue(token, out string? name) ? name : null; - - /// Creates a new session token and stores the name associated with it. - /// The given name. - /// A new session token. - private Guid CreateToken(string name) - { - // Guid are not cryptographically secure, but for this example it's sufficient. - var token = Guid.NewGuid(); - _sessions[token] = name; - return token; - } -} diff --git a/examples/Authorization/Server/SessionMiddleware.cs b/examples/Authorization/Server/SessionMiddleware.cs deleted file mode 100644 index 639a3bf4e0..0000000000 --- a/examples/Authorization/Server/SessionMiddleware.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using IceRpc; -using IceRpc.Features; -using System.Buffers; - -namespace AuthorizationExample; - -/// Middleware that loads the session token from the request and adds the session feature to the request's -/// feature collection. -internal class LoadSessionMiddleware : IDispatcher -{ - private readonly IDispatcher _next; - - private readonly TokenStore _tokenStore; - - internal LoadSessionMiddleware(IDispatcher next, TokenStore tokenStore) - { - _next = next; - _tokenStore = tokenStore; - } - - public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) - { - if (request.Fields.TryGetValue(SessionFieldKey.Value, out ReadOnlySequence value)) - { - var token = new Guid(value.ToArray()); - if (_tokenStore.GetName(token) is string name) - { - request.Features = request.Features.With(new SessionFeature(name)); - } - } - return _next.DispatchAsync(request, cancellationToken); - } -} - -/// Middleware that checks if the request has a session feature. If not, it throws a -/// . -internal class HasSessionMiddleware : IDispatcher -{ - private readonly IDispatcher _next; - - internal HasSessionMiddleware(IDispatcher next) => _next = next; - - public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) => - request.Features.Get() is not null ? _next.DispatchAsync(request, cancellationToken) : - throw new DispatchException(StatusCode.Unauthorized, "Not authorized."); -} diff --git a/examples/Authorization/SessionFieldKey.cs b/examples/Authorization/SessionFieldKey.cs deleted file mode 100644 index 75d541f346..0000000000 --- a/examples/Authorization/SessionFieldKey.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using IceRpc; - -namespace AuthorizationExample; - -/// The shared used by the client and server to carry the session -/// token. -public static class SessionFieldKey -{ - public const RequestFieldKey Value = (RequestFieldKey)100; -} From 66f93ec8600a73430afe60739878daa278a4c884 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Mon, 27 Mar 2023 19:18:39 +0200 Subject: [PATCH 04/26] Added missing files --- .../AuthenticationTokenFieldKey.cs | 12 +++++ .../Client/AuthenticationTokenInterceptor.cs | 25 +++++++++ examples/Authorization/Client/Client.csproj | 2 +- .../Server/AuthenticationFeature.cs | 26 ++++++++++ .../Server/AuthenticationMiddleware.cs | 32 ++++++++++++ .../Server/AuthenticationToken.cs | 52 +++++++++++++++++++ .../Authorization/Server/Authenticator.cs | 29 +++++++++++ .../Server/AuthorizationMiddleware.cs | 32 ++++++++++++ examples/Authorization/Server/Server.csproj | 2 +- 9 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 examples/Authorization/AuthenticationTokenFieldKey.cs create mode 100644 examples/Authorization/Client/AuthenticationTokenInterceptor.cs create mode 100644 examples/Authorization/Server/AuthenticationFeature.cs create mode 100644 examples/Authorization/Server/AuthenticationMiddleware.cs create mode 100644 examples/Authorization/Server/AuthenticationToken.cs create mode 100644 examples/Authorization/Server/Authenticator.cs create mode 100644 examples/Authorization/Server/AuthorizationMiddleware.cs diff --git a/examples/Authorization/AuthenticationTokenFieldKey.cs b/examples/Authorization/AuthenticationTokenFieldKey.cs new file mode 100644 index 0000000000..7eea5975f6 --- /dev/null +++ b/examples/Authorization/AuthenticationTokenFieldKey.cs @@ -0,0 +1,12 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc; + +namespace AuthorizationExample; + +/// The shared used by the client and server to carry the authentication +/// token. +public static class AuthenticationTokenFieldKey +{ + public const RequestFieldKey Value = (RequestFieldKey)100; +} diff --git a/examples/Authorization/Client/AuthenticationTokenInterceptor.cs b/examples/Authorization/Client/AuthenticationTokenInterceptor.cs new file mode 100644 index 0000000000..9f6ad94440 --- /dev/null +++ b/examples/Authorization/Client/AuthenticationTokenInterceptor.cs @@ -0,0 +1,25 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc; +using System.Buffers; + +namespace AuthorizationExample; + +/// An interceptor that adds a field with the authentication token to each request. +internal class AuthenticationTokenInterceptor : IInvoker +{ + private readonly IInvoker _next; + private readonly byte[] _token; + + public Task InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken) + { + request.Fields = request.Fields.With(AuthenticationTokenFieldKey.Value, new ReadOnlySequence(_token)); + return _next.InvokeAsync(request, cancellationToken); + } + + internal AuthenticationTokenInterceptor(IInvoker next, byte[] token) + { + _next = next; + _token = token; + } +} diff --git a/examples/Authorization/Client/Client.csproj b/examples/Authorization/Client/Client.csproj index 7f2b715dab..4e6161b8e0 100644 --- a/examples/Authorization/Client/Client.csproj +++ b/examples/Authorization/Client/Client.csproj @@ -4,6 +4,6 @@ - + diff --git a/examples/Authorization/Server/AuthenticationFeature.cs b/examples/Authorization/Server/AuthenticationFeature.cs new file mode 100644 index 0000000000..32adad5b84 --- /dev/null +++ b/examples/Authorization/Server/AuthenticationFeature.cs @@ -0,0 +1,26 @@ +// Copyright (c) ZeroC, Inc. + +namespace AuthorizationExample; + +/// A feature that stores the authenticated client's name and whether or not it has the administrative +/// privilege. +public interface IAuthenticationFeature +{ + string Name { get; } + + bool IsAdmin { get; } +} + +/// The implementation of . +internal class AuthenticationFeature : IAuthenticationFeature +{ + public string Name { get; } + + public bool IsAdmin { get; } + + public AuthenticationFeature(AuthenticationToken token) + { + Name = token.Name; + IsAdmin = token.IsAdmin; + } +} diff --git a/examples/Authorization/Server/AuthenticationMiddleware.cs b/examples/Authorization/Server/AuthenticationMiddleware.cs new file mode 100644 index 0000000000..dee9d43f3b --- /dev/null +++ b/examples/Authorization/Server/AuthenticationMiddleware.cs @@ -0,0 +1,32 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc; +using IceRpc.Features; +using System.Buffers; +using System.Security.Cryptography; + +namespace AuthorizationExample; + +/// Middleware that loads the session token from the request and adds the session feature to the request's +/// feature collection. +internal class AuthenticationMiddleware : IDispatcher +{ + private readonly SymmetricAlgorithm _cryptAlgorithm; + private readonly IDispatcher _next; + + internal AuthenticationMiddleware(IDispatcher next, SymmetricAlgorithm cryptAlgorithm) + { + _next = next; + _cryptAlgorithm = cryptAlgorithm; + } + + public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) + { + if (request.Fields.TryGetValue(AuthenticationTokenFieldKey.Value, out ReadOnlySequence buffer)) + { + request.Features = request.Features.With( + new AuthenticationFeature(AuthenticationToken.Decrypt(buffer.ToArray(), _cryptAlgorithm))); + } + return _next.DispatchAsync(request, cancellationToken); + } +} diff --git a/examples/Authorization/Server/AuthenticationToken.cs b/examples/Authorization/Server/AuthenticationToken.cs new file mode 100644 index 0000000000..65ca67f85a --- /dev/null +++ b/examples/Authorization/Server/AuthenticationToken.cs @@ -0,0 +1,52 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc.Slice; +using System.IO.Pipelines; +using System.Security.Cryptography; +using System.Text; + +namespace AuthorizationExample; + +internal readonly struct AuthenticationToken +{ + public string Name { get; } + + public bool IsAdmin { get; } + + public AuthenticationToken(string name, bool isAdmin) + { + Name = name; + IsAdmin = isAdmin; + } + + public ReadOnlyMemory Encrypt(SymmetricAlgorithm algorithm) + { + // Encode the token with the Slice2 encoding. + using var tokenStream = new MemoryStream(); + var writer = PipeWriter.Create(tokenStream, new StreamPipeWriterOptions(leaveOpen: true)); + var encoder = new SliceEncoder(writer, SliceEncoding.Slice2); + encoder.EncodeString(Name); + encoder.EncodeBool(IsAdmin); + writer.Complete(); + tokenStream.Seek(0, SeekOrigin.Begin); + + // Crypt and return the Slice2 encoded token. + using var destinationStream = new MemoryStream(); + using var cryptoStream = new CryptoStream(tokenStream, algorithm.CreateEncryptor(), CryptoStreamMode.Read); + cryptoStream.CopyTo(destinationStream); + return destinationStream.ToArray(); + } + + public static AuthenticationToken Decrypt(byte[] buffer, SymmetricAlgorithm algorithm) + { + // Decrypt the Slice2 encoded token. + using var sourceStream = new MemoryStream(buffer); + using var cryptoStream = new CryptoStream(sourceStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read); + using var destinationStream = new MemoryStream(); + cryptoStream.CopyTo(destinationStream); + + // Decode the Slice2 encoded token. + var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); + return new AuthenticationToken(decoder.DecodeString(), decoder.DecodeBool()); + } +} diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs new file mode 100644 index 0000000000..f56a5ca0e9 --- /dev/null +++ b/examples/Authorization/Server/Authenticator.cs @@ -0,0 +1,29 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc.Features; +using IceRpc.Slice; +using System.Security.Cryptography; + +namespace AuthorizationExample; + +/// The token store holds the session token to name dictionary and implements the +/// interface. +internal class Authenticator : Service, IAuthenticatorService +{ + private readonly SymmetricAlgorithm _cryptAlgorithm; + + public Authenticator(SymmetricAlgorithm cryptAlgorithm) => _cryptAlgorithm = cryptAlgorithm; + + public ValueTask> AuthenticateAsync( + string name, + string password, + IFeatureCollection features, + CancellationToken cancellationToken) + { + if (password != "password") + { + throw new AuthenticationException("Invalid password."); + } + return new(new AuthenticationToken(name, isAdmin: name == "admin").Encrypt(_cryptAlgorithm)); + } +} diff --git a/examples/Authorization/Server/AuthorizationMiddleware.cs b/examples/Authorization/Server/AuthorizationMiddleware.cs new file mode 100644 index 0000000000..d9913eceed --- /dev/null +++ b/examples/Authorization/Server/AuthorizationMiddleware.cs @@ -0,0 +1,32 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc; + +namespace AuthorizationExample; + +/// Middleware that checks if the request is authorized. If not, it throws a . +internal class AuthorizationMiddleware : IDispatcher +{ + private readonly IDispatcher _next; + private readonly Func _authorizeFunc; + + internal AuthorizationMiddleware(IDispatcher next, Func authorizeFunc) + { + _next = next; + _authorizeFunc = authorizeFunc; + } + + public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) + { + if (request.Features.Get() is IAuthenticationFeature authenticationFeature && + _authorizeFunc(authenticationFeature)) + { + return _next.DispatchAsync(request, cancellationToken); + } + else + { + throw new DispatchException(StatusCode.Unauthorized, "Not authorized."); + } + } +} diff --git a/examples/Authorization/Server/Server.csproj b/examples/Authorization/Server/Server.csproj index e062c44fcb..ebadde28ea 100644 --- a/examples/Authorization/Server/Server.csproj +++ b/examples/Authorization/Server/Server.csproj @@ -4,7 +4,7 @@ - + From 81ec962e79172b8682d85735e41cec9e2a59e39b Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 28 Mar 2023 10:04:05 +0200 Subject: [PATCH 05/26] More updates --- examples/Authorization/Authorization.slice | 8 ++- .../Client/AuthenticationInterceptor.cs | 29 +++++++++++ .../Client/AuthenticationTokenInterceptor.cs | 25 ---------- .../Client/PipelineExtensions.cs | 10 ++-- examples/Authorization/Client/Program.cs | 43 +++++++++------- .../Server/AuthenticationFeature.cs | 21 +++++--- .../Server/AuthenticationMiddleware.cs | 26 ++++++---- .../Server/AuthenticationToken.cs | 50 ++++++++++++------- .../Authorization/Server/Authenticator.cs | 18 ++++--- .../Server/AuthorizationMiddleware.cs | 20 +++++--- examples/Authorization/Server/Chatbot.cs | 3 +- examples/Authorization/Server/ChatbotAdmin.cs | 4 +- examples/Authorization/Server/Program.cs | 8 +-- .../Authorization/Server/RouterExtensions.cs | 18 ++++--- 14 files changed, 162 insertions(+), 121 deletions(-) create mode 100644 examples/Authorization/Client/AuthenticationInterceptor.cs delete mode 100644 examples/Authorization/Client/AuthenticationTokenInterceptor.cs diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index 5549ed9908..a54b10816a 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -3,17 +3,15 @@ module AuthorizationExample /// The encrypted authentication token. -typealias Token = sequence +typealias EnryptedAuthenticationToken = sequence -exception AuthenticationException { } - -/// Represents a service to create a session token. +/// Represents a service to authentication a user and return an authentication token. interface Authenticator { /// Authenticates a user. /// @param name: The user name. /// @param password: The password. /// @returns: The encrypted authentication token. - authenticate(name: string, password: string) -> Token throws AuthenticationException + authenticate(name: string, password: string) -> EnryptedAuthenticationToken } /// Represents a simple greeter. diff --git a/examples/Authorization/Client/AuthenticationInterceptor.cs b/examples/Authorization/Client/AuthenticationInterceptor.cs new file mode 100644 index 0000000000..f927c049fe --- /dev/null +++ b/examples/Authorization/Client/AuthenticationInterceptor.cs @@ -0,0 +1,29 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc; +using System.Buffers; + +namespace AuthorizationExample; + +/// An interceptor that adds the encrypted authentication token field to each request. +internal class AuthenticationInterceptor : IInvoker +{ + private readonly ReadOnlySequence _authenticationToken; + private readonly IInvoker _next; + + /// + public Task InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken) + { + request.Fields = request.Fields.With(AuthenticationTokenFieldKey.Value, _authenticationToken); + return _next.InvokeAsync(request, cancellationToken); + } + + /// Constructs an authentication interceptor. + /// The invoker to call next. + /// The encrypted authentication token. + internal AuthenticationInterceptor(IInvoker next, ReadOnlyMemory authenticationToken) + { + _next = next; + _authenticationToken = new ReadOnlySequence(authenticationToken); + } +} diff --git a/examples/Authorization/Client/AuthenticationTokenInterceptor.cs b/examples/Authorization/Client/AuthenticationTokenInterceptor.cs deleted file mode 100644 index 9f6ad94440..0000000000 --- a/examples/Authorization/Client/AuthenticationTokenInterceptor.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using IceRpc; -using System.Buffers; - -namespace AuthorizationExample; - -/// An interceptor that adds a field with the authentication token to each request. -internal class AuthenticationTokenInterceptor : IInvoker -{ - private readonly IInvoker _next; - private readonly byte[] _token; - - public Task InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken) - { - request.Fields = request.Fields.With(AuthenticationTokenFieldKey.Value, new ReadOnlySequence(_token)); - return _next.InvokeAsync(request, cancellationToken); - } - - internal AuthenticationTokenInterceptor(IInvoker next, byte[] token) - { - _next = next; - _token = token; - } -} diff --git a/examples/Authorization/Client/PipelineExtensions.cs b/examples/Authorization/Client/PipelineExtensions.cs index 44e2577ab7..368dc57331 100644 --- a/examples/Authorization/Client/PipelineExtensions.cs +++ b/examples/Authorization/Client/PipelineExtensions.cs @@ -4,14 +4,14 @@ namespace IceRpc; -/// This class provides an extension method to add an +/// This class provides an extension method to add an /// to a . internal static class PipelineExtensions { - /// Adds an to the pipeline. + /// Adds an to the pipeline. /// The pipeline being configured. - /// The authentication token. + /// The encrypted authentication token. /// The pipeline being configured. - internal static Pipeline UseAuthenticationToken(this Pipeline pipeline, byte[] token) => - pipeline.Use(next => new AuthenticationTokenInterceptor(next, token)); + internal static Pipeline UseAuthentication(this Pipeline pipeline, ReadOnlyMemory authenticationToken) => + pipeline.Use(next => new AuthenticationInterceptor(next, authenticationToken)); } diff --git a/examples/Authorization/Client/Program.cs b/examples/Authorization/Client/Program.cs index 0be7fa7e9b..136c81d12f 100644 --- a/examples/Authorization/Client/Program.cs +++ b/examples/Authorization/Client/Program.cs @@ -5,22 +5,26 @@ await using var connection = new ClientConnection(new Uri("icerpc://localhost")); -// A hello proxy that doesn't use any authentication will print a generic greeting. -var unauthenticatedHelloProxy = new HelloProxy(connection, new Uri("icerpc:/hello")); -Console.WriteLine(await unauthenticatedHelloProxy.SayHelloAsync()); - -// Authenticate the "friend" user to get its authentication token. var authenticatorProxy = new AuthenticatorProxy(connection, new Uri("icerpc:/authenticator")); -byte[] friendToken = await authenticatorProxy.AuthenticateAsync("friend", "password"); -// Setup a pipe line that inserts the "friend" token into a request field. -Pipeline friendPipeline = new Pipeline().UseAuthenticationToken(friendToken).Into(connection); +// Authenticate the "friend" user and get its authentication token. +ReadOnlyMemory friendToken = await authenticatorProxy.AuthenticateAsync("friend", "password"); + +// A hello proxy that doesn't use any authentication token. +var unauthenticatedHelloProxy = new HelloProxy(connection, new Uri("icerpc:/hello")); -// A hello proxy that uses "friend" pipeline will print a custom greeting for "friend". +// The SayHello invocation on the unauthenticated hello proxy prints a generic message. +Console.WriteLine(await unauthenticatedHelloProxy.SayHelloAsync()); + +// Create a hello proxy that uses a pipe line to insert the "friend" token into a request field. +Pipeline friendPipeline = new Pipeline().UseAuthentication(friendToken).Into(connection); var friendHelloProxy = new HelloProxy(friendPipeline, new Uri("icerpc:/hello")); + +// The SayHello invocation on the authenticated "friend" hello proxy prints a custom message for "friend". Console.WriteLine(await friendHelloProxy.SayHelloAsync()); -// A hello admin proxy that uses the "friend" pipeline is not authorized to change the greeting message. +// A hello admin proxy that uses the "friend" pipeline is not authorized to change the greeting message because "friend" +// doesn't have administrative privileges. var helloAdminProxy = new HelloAdminProxy(friendPipeline, new Uri("icerpc:/helloAdmin")); try { @@ -28,22 +32,23 @@ } catch (DispatchException exception) when (exception.StatusCode == StatusCode.Unauthorized) { - Console.WriteLine("The friend user is not authorized to change the greeting."); + Console.WriteLine("The 'friend' user is not authorized to change the greeting message."); } -// Authenticate the "admin" user to get its authentication token and setup a pipeline with the "admin" token. -byte[] adminToken = await authenticatorProxy.AuthenticateAsync("admin", "password"); -helloAdminProxy = new HelloAdminProxy( - new Pipeline().UseAuthenticationToken(adminToken).Into(connection), - new Uri("icerpc:/helloAdmin")); +// Authenticate the "admin" user and get its authentication token. +ReadOnlyMemory adminToken = await authenticatorProxy.AuthenticateAsync("admin", "password"); + +// Create a hello admin proxy that uses a pipe line to insert the "admin" token into a request field. +Pipeline adminPipeline = new Pipeline().UseAuthentication(adminToken).Into(connection); +helloAdminProxy = new HelloAdminProxy(adminPipeline, new Uri("icerpc:/helloAdmin")); -// Change the greeting message. +// Changing the greeting message should succeed this time because the "admin" user has administrative privilege. await helloAdminProxy.ChangeGreetingAsync("Bonjour"); -// Authenticated hello with updated greeting. +// The SayHello invocation should print a greeting with the updated greeting message. Console.WriteLine(await friendHelloProxy.SayHelloAsync()); -// Change back the greeting to Hello. +// Change back the greeting message to Hello. await helloAdminProxy.ChangeGreetingAsync("Hello"); await connection.ShutdownAsync(); diff --git a/examples/Authorization/Server/AuthenticationFeature.cs b/examples/Authorization/Server/AuthenticationFeature.cs index 32adad5b84..3168607124 100644 --- a/examples/Authorization/Server/AuthenticationFeature.cs +++ b/examples/Authorization/Server/AuthenticationFeature.cs @@ -2,23 +2,28 @@ namespace AuthorizationExample; -/// A feature that stores the authenticated client's name and whether or not it has the administrative -/// privilege. -public interface IAuthenticationFeature +/// A feature that provides the name and administrative privilege decoded from an authentication token +/// field. +internal interface IAuthenticationFeature { - string Name { get; } - + /// true if the authenticated client has administrative privilege, false otherwise. bool IsAdmin { get; } + + /// The name of the authenticated client. + string Name { get; } } /// The implementation of . internal class AuthenticationFeature : IAuthenticationFeature { - public string Name { get; } - + /// public bool IsAdmin { get; } - public AuthenticationFeature(AuthenticationToken token) + /// + public string Name { get; } + + /// Constructs an authentication feature from an authentication token. + internal AuthenticationFeature(AuthenticationToken token) { Name = token.Name; IsAdmin = token.IsAdmin; diff --git a/examples/Authorization/Server/AuthenticationMiddleware.cs b/examples/Authorization/Server/AuthenticationMiddleware.cs index dee9d43f3b..63692c7c6d 100644 --- a/examples/Authorization/Server/AuthenticationMiddleware.cs +++ b/examples/Authorization/Server/AuthenticationMiddleware.cs @@ -7,26 +7,30 @@ namespace AuthorizationExample; -/// Middleware that loads the session token from the request and adds the session feature to the request's -/// feature collection. +/// A middleware that decodes and decrypt an authentication token request field and adds an authentication +/// feature to the request's feature collection. internal class AuthenticationMiddleware : IDispatcher { - private readonly SymmetricAlgorithm _cryptAlgorithm; + private readonly SymmetricAlgorithm _encryptionAlgorithm; private readonly IDispatcher _next; - internal AuthenticationMiddleware(IDispatcher next, SymmetricAlgorithm cryptAlgorithm) - { - _next = next; - _cryptAlgorithm = cryptAlgorithm; - } - + /// public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { if (request.Fields.TryGetValue(AuthenticationTokenFieldKey.Value, out ReadOnlySequence buffer)) { - request.Features = request.Features.With( - new AuthenticationFeature(AuthenticationToken.Decrypt(buffer.ToArray(), _cryptAlgorithm))); + var token = AuthenticationToken.Decrypt(buffer.ToArray(), _encryptionAlgorithm); + request.Features = request.Features.With(new AuthenticationFeature(token)); } return _next.DispatchAsync(request, cancellationToken); } + + /// Constructs an authentication middleware. + /// The invoker to call next. + /// The encryption algorithm used to encrypt an authentication token. + internal AuthenticationMiddleware(IDispatcher next, SymmetricAlgorithm encryptionAlgorithm) + { + _next = next; + _encryptionAlgorithm = encryptionAlgorithm; + } } diff --git a/examples/Authorization/Server/AuthenticationToken.cs b/examples/Authorization/Server/AuthenticationToken.cs index 65ca67f85a..c67a1edfb3 100644 --- a/examples/Authorization/Server/AuthenticationToken.cs +++ b/examples/Authorization/Server/AuthenticationToken.cs @@ -3,23 +3,50 @@ using IceRpc.Slice; using System.IO.Pipelines; using System.Security.Cryptography; -using System.Text; namespace AuthorizationExample; +/// An struct that defines an authentication token and methods to encrypt and decrypt the authentication +/// token. internal readonly struct AuthenticationToken { - public string Name { get; } + /// true if the authenticated client has administrative privilege, false otherwise. + internal bool IsAdmin { get; } - public bool IsAdmin { get; } + /// The name of the authenticated client. + internal string Name { get; } - public AuthenticationToken(string name, bool isAdmin) + /// Decrypts an authentication token. + /// The byte buffer. + /// The symmetric algorithm used to decrypt an authentication token. + /// The authentication token. + internal static AuthenticationToken Decrypt(byte[] buffer, SymmetricAlgorithm algorithm) + { + // Decrypt the Slice2 encoded token. + using var sourceStream = new MemoryStream(buffer); + using var cryptoStream = new CryptoStream(sourceStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read); + using var destinationStream = new MemoryStream(); + cryptoStream.CopyTo(destinationStream); + + // Decode the Slice2 encoded token. + var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); + return new AuthenticationToken(decoder.DecodeString(), decoder.DecodeBool()); + } + + /// Constructs a new authentication token. + /// The name of the authentication token. + /// true if the authenticated client has administrative privilege, false + /// otherwise. + internal AuthenticationToken(string name, bool isAdmin) { Name = name; IsAdmin = isAdmin; } - public ReadOnlyMemory Encrypt(SymmetricAlgorithm algorithm) + /// Encodes and encrypts this authentication token. + /// The symmetric algorithm used to encrypt this authentication token. + /// The encrypted authentication token. + internal ReadOnlyMemory Encrypt(SymmetricAlgorithm algorithm) { // Encode the token with the Slice2 encoding. using var tokenStream = new MemoryStream(); @@ -36,17 +63,4 @@ public ReadOnlyMemory Encrypt(SymmetricAlgorithm algorithm) cryptoStream.CopyTo(destinationStream); return destinationStream.ToArray(); } - - public static AuthenticationToken Decrypt(byte[] buffer, SymmetricAlgorithm algorithm) - { - // Decrypt the Slice2 encoded token. - using var sourceStream = new MemoryStream(buffer); - using var cryptoStream = new CryptoStream(sourceStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read); - using var destinationStream = new MemoryStream(); - cryptoStream.CopyTo(destinationStream); - - // Decode the Slice2 encoded token. - var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); - return new AuthenticationToken(decoder.DecodeString(), decoder.DecodeBool()); - } } diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs index f56a5ca0e9..1df66c64f0 100644 --- a/examples/Authorization/Server/Authenticator.cs +++ b/examples/Authorization/Server/Authenticator.cs @@ -1,19 +1,18 @@ // Copyright (c) ZeroC, Inc. +using IceRpc; using IceRpc.Features; using IceRpc.Slice; using System.Security.Cryptography; namespace AuthorizationExample; -/// The token store holds the session token to name dictionary and implements the -/// interface. +/// An Authenticator is an IceRPC service that implements the Slice interface 'Authenticator'. internal class Authenticator : Service, IAuthenticatorService { - private readonly SymmetricAlgorithm _cryptAlgorithm; - - public Authenticator(SymmetricAlgorithm cryptAlgorithm) => _cryptAlgorithm = cryptAlgorithm; + private readonly SymmetricAlgorithm _encryptionAlgorithm; + /// public ValueTask> AuthenticateAsync( string name, string password, @@ -22,8 +21,13 @@ public ValueTask> AuthenticateAsync( { if (password != "password") { - throw new AuthenticationException("Invalid password."); + throw new DispatchException(StatusCode.Unauthorized, "Invalid password."); } - return new(new AuthenticationToken(name, isAdmin: name == "admin").Encrypt(_cryptAlgorithm)); + return new(new AuthenticationToken(name, isAdmin: name == "admin").Encrypt(_encryptionAlgorithm)); } + + /// Constructs an authenticator service. + /// The encryption algorithm used to encrypt an authentication token. + internal Authenticator(SymmetricAlgorithm encryptionAlgorithm) => _encryptionAlgorithm = encryptionAlgorithm; + } diff --git a/examples/Authorization/Server/AuthorizationMiddleware.cs b/examples/Authorization/Server/AuthorizationMiddleware.cs index d9913eceed..780b8068e6 100644 --- a/examples/Authorization/Server/AuthorizationMiddleware.cs +++ b/examples/Authorization/Server/AuthorizationMiddleware.cs @@ -4,19 +4,14 @@ namespace AuthorizationExample; -/// Middleware that checks if the request is authorized. If not, it throws a A middleware that checks if the request is authorized. If not, it throws a . internal class AuthorizationMiddleware : IDispatcher { - private readonly IDispatcher _next; private readonly Func _authorizeFunc; + private readonly IDispatcher _next; - internal AuthorizationMiddleware(IDispatcher next, Func authorizeFunc) - { - _next = next; - _authorizeFunc = authorizeFunc; - } - + /// public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { if (request.Features.Get() is IAuthenticationFeature authenticationFeature && @@ -29,4 +24,13 @@ public ValueTask DispatchAsync(IncomingRequest request, Cancel throw new DispatchException(StatusCode.Unauthorized, "Not authorized."); } } + + /// Constructs an authentication middleware. + /// The dispatcher to call next. + /// The authorization function. + internal AuthorizationMiddleware(IDispatcher next, Func authorizeFunc) + { + _next = next; + _authorizeFunc = authorizeFunc; + } } diff --git a/examples/Authorization/Server/Chatbot.cs b/examples/Authorization/Server/Chatbot.cs index 9c19800c59..2cc39b0965 100644 --- a/examples/Authorization/Server/Chatbot.cs +++ b/examples/Authorization/Server/Chatbot.cs @@ -5,11 +5,12 @@ namespace AuthorizationExample; -/// A Chatbot is an IceRPC service that implements Slice interface 'Hello'. +/// A Chatbot is an IceRPC service that implements the Slice interface 'Hello'. internal class Chatbot : Service, IHelloService { internal string Greeting { get; set; } = "Hello"; + /// public ValueTask SayHelloAsync(IFeatureCollection features, CancellationToken cancellationToken) { string who = features.Get()?.Name ?? "stranger"; diff --git a/examples/Authorization/Server/ChatbotAdmin.cs b/examples/Authorization/Server/ChatbotAdmin.cs index eaa4c4fa58..4d2d98364a 100644 --- a/examples/Authorization/Server/ChatbotAdmin.cs +++ b/examples/Authorization/Server/ChatbotAdmin.cs @@ -5,14 +5,14 @@ namespace AuthorizationExample; -/// A service that implements Slice interface HelloAdmin. It is used to change the greeting and requires -/// callers to be authenticated. +/// A ChatbotAdmin is an IceRPC service that implements the Slice interface 'HelloAdmin'. internal class ChatbotAdmin : Service, IHelloAdminService { private readonly Chatbot _chatbot; internal ChatbotAdmin(Chatbot chatbot) => _chatbot = chatbot; + /// public ValueTask ChangeGreetingAsync( string greeting, IFeatureCollection features, diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index 2ed6d71837..ea5814a66e 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -4,7 +4,7 @@ using IceRpc; using System.Security.Cryptography; -// Use AES symmetric encryption to crypt the token. +// The authentication token is encrypted and decrypted with the AES symmetric encryption algorithm. using var aes = Aes.Create(); aes.Padding = PaddingMode.Zeros; @@ -12,13 +12,13 @@ var router = new Router(); -// Install a middleware to get and decrypt the authentication token from a request and to add the authentication feature -// to the request's feature collection. +// Install a middleware to decrypt and decode the request's authentication token and add an authentication feature to +// the request's feature collection. router.UseAuthentication(aes); router.Route("/helloAdmin", adminRouter => { - // Install an authorization middleware to check if the caller is authorized to call the hello admin service. + // Install an authorization middleware that checks if the caller is authorized to call the hello admin service. adminRouter.UseAuthorization(authenticationFeature => authenticationFeature.IsAdmin); adminRouter.Map("/", new ChatbotAdmin(chatbot)); }); diff --git a/examples/Authorization/Server/RouterExtensions.cs b/examples/Authorization/Server/RouterExtensions.cs index 301afb297a..a791420ec2 100644 --- a/examples/Authorization/Server/RouterExtensions.cs +++ b/examples/Authorization/Server/RouterExtensions.cs @@ -5,19 +5,21 @@ namespace IceRpc; -/// This class provides extension methods to add session middleware to a . +/// This class provides extension methods to add an and . internal static class RouterExtensions { - /// Adds a to the router. + /// Adds an to the router. /// The router being configured. + /// The encryption algorithm used to encrypt the authentication token. /// The router being configured. - internal static Router UseAuthorization(this Router router, Func authorizeFunc) => - router.Use(next => new AuthorizationMiddleware(next, authorizeFunc)); + internal static Router UseAuthentication(this Router router, SymmetricAlgorithm encryptionAlgorithm) => + router.Use(next => new AuthenticationMiddleware(next, encryptionAlgorithm)); - /// Adds a to the router. + /// Adds an to the router. /// The router being configured. - /// The session token store. + /// The function called by the middleware to check if the request is authorized. /// The router being configured. - internal static Router UseAuthentication(this Router router, SymmetricAlgorithm cryptAlgorithm) => - router.Use(next => new AuthenticationMiddleware(next, cryptAlgorithm)); + internal static Router UseAuthorization(this Router router, Func authorizeFunc) => + router.Use(next => new AuthorizationMiddleware(next, authorizeFunc)); } From 75263d79811c2430fb9be649fb4537452859000b Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 28 Mar 2023 10:21:04 +0200 Subject: [PATCH 06/26] README.md updates --- examples/Authorization/README.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/examples/Authorization/README.md b/examples/Authorization/README.md index ddfb21f60a..294dba9d47 100644 --- a/examples/Authorization/README.md +++ b/examples/Authorization/README.md @@ -1,14 +1,16 @@ # Authorization -This example application illustrates how to create an authorization interceptor and middleware that can be used -to authorize requests. +This example application illustrates how to create an authentication interceptor, authentication middleware and +authorization middleware that can be used to authorize requests. -The server is configured with two middleware components: `LoadSession` and `HasSession`. The first middleware is -responsible for loading the session from the request field and storing it in a corresponding request feature. The second -middleware is responsible for checking if the session is present in the corresponding request feature and -returning an error if it is not. +The server is configured with two middleware: `AuthenticationMiddleware` and `AuthorizationMiddleware`. The first +middleware is responsible for decrypting an authentication token from the request field and storing it in a +corresponding request feature. The second middleware is responsible for checking if the authentication feature is +present in the corresponding request feature and to check if the request is authorized. -The client creates an authorization pipeline that is responsible for adding the session token as a request field. +The client is configured with an `AuthenticationInterceptor` interceptor. The interceptor is responsible for adding a +encrypted authentication token to a request field. The authentication token is returned by an `Authenticator` service +after authenticating the client with a login name and password. ## Running the example @@ -24,11 +26,13 @@ In a separate window, start the Client: dotnet run --project Client/Client.csproj ``` -The client first calls `SayHelloAsync` without a session token and the server responds with generic a greeting. +The client first calls `SayHelloAsync` without an authentication token and the server responds with generic a greeting. -Next, the client gets an authentication token and uses it to construct an authenticated invocation pipeline that adds -the token to each request. The client then calls `SayHelloAsync` using the authenticated pipeline and receives -a personalized message. +Next, the client gets an authentication token for the user `friend` and uses it to construct an authentication +invocation pipeline that adds the `friend` token to each request. The client then calls `SayHelloAsync` using this +`friend` authentication pipeline and receives a personalized message. -Finally, the client calls `ChangeGreetingAsync` using the authenticated pipeline to change the greeting and then calls -`SayHelloAsync` a final time. +Next, the client calls `ChangeGreetingAsync` using the `friend` authentication pipeline to change the greeting. The client doesn't have administrative privilege so the invocation fails with a `DispatchException`. + +Finally, the client authenticates the user `admin` and calls `ChangeGreetingAsync` using an `admin` authentication +pipeline to change the greeting. From 0a932d7801fcadd78af7092f56cae7ba0742158e Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 28 Mar 2023 10:31:30 +0200 Subject: [PATCH 07/26] Minor fixes --- examples/Authorization/Client/AuthenticationInterceptor.cs | 1 - examples/Authorization/Server/AuthenticationMiddleware.cs | 1 - examples/Authorization/Server/Authenticator.cs | 1 - examples/Authorization/Server/AuthorizationMiddleware.cs | 1 - examples/Authorization/Server/Chatbot.cs | 1 - examples/Authorization/Server/ChatbotAdmin.cs | 1 - 6 files changed, 6 deletions(-) diff --git a/examples/Authorization/Client/AuthenticationInterceptor.cs b/examples/Authorization/Client/AuthenticationInterceptor.cs index f927c049fe..f8a1368bd2 100644 --- a/examples/Authorization/Client/AuthenticationInterceptor.cs +++ b/examples/Authorization/Client/AuthenticationInterceptor.cs @@ -11,7 +11,6 @@ internal class AuthenticationInterceptor : IInvoker private readonly ReadOnlySequence _authenticationToken; private readonly IInvoker _next; - /// public Task InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken) { request.Fields = request.Fields.With(AuthenticationTokenFieldKey.Value, _authenticationToken); diff --git a/examples/Authorization/Server/AuthenticationMiddleware.cs b/examples/Authorization/Server/AuthenticationMiddleware.cs index 63692c7c6d..642433df3d 100644 --- a/examples/Authorization/Server/AuthenticationMiddleware.cs +++ b/examples/Authorization/Server/AuthenticationMiddleware.cs @@ -14,7 +14,6 @@ internal class AuthenticationMiddleware : IDispatcher private readonly SymmetricAlgorithm _encryptionAlgorithm; private readonly IDispatcher _next; - /// public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { if (request.Fields.TryGetValue(AuthenticationTokenFieldKey.Value, out ReadOnlySequence buffer)) diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs index 1df66c64f0..0c48ed9aee 100644 --- a/examples/Authorization/Server/Authenticator.cs +++ b/examples/Authorization/Server/Authenticator.cs @@ -12,7 +12,6 @@ internal class Authenticator : Service, IAuthenticatorService { private readonly SymmetricAlgorithm _encryptionAlgorithm; - /// public ValueTask> AuthenticateAsync( string name, string password, diff --git a/examples/Authorization/Server/AuthorizationMiddleware.cs b/examples/Authorization/Server/AuthorizationMiddleware.cs index 780b8068e6..be89383c54 100644 --- a/examples/Authorization/Server/AuthorizationMiddleware.cs +++ b/examples/Authorization/Server/AuthorizationMiddleware.cs @@ -11,7 +11,6 @@ internal class AuthorizationMiddleware : IDispatcher private readonly Func _authorizeFunc; private readonly IDispatcher _next; - /// public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { if (request.Features.Get() is IAuthenticationFeature authenticationFeature && diff --git a/examples/Authorization/Server/Chatbot.cs b/examples/Authorization/Server/Chatbot.cs index 2cc39b0965..1ef7a7818c 100644 --- a/examples/Authorization/Server/Chatbot.cs +++ b/examples/Authorization/Server/Chatbot.cs @@ -10,7 +10,6 @@ internal class Chatbot : Service, IHelloService { internal string Greeting { get; set; } = "Hello"; - /// public ValueTask SayHelloAsync(IFeatureCollection features, CancellationToken cancellationToken) { string who = features.Get()?.Name ?? "stranger"; diff --git a/examples/Authorization/Server/ChatbotAdmin.cs b/examples/Authorization/Server/ChatbotAdmin.cs index 4d2d98364a..4b6a01e084 100644 --- a/examples/Authorization/Server/ChatbotAdmin.cs +++ b/examples/Authorization/Server/ChatbotAdmin.cs @@ -12,7 +12,6 @@ internal class ChatbotAdmin : Service, IHelloAdminService internal ChatbotAdmin(Chatbot chatbot) => _chatbot = chatbot; - /// public ValueTask ChangeGreetingAsync( string greeting, IFeatureCollection features, From b42277889960c8ab8f3a46ac5db2f30e8253866b Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 28 Mar 2023 15:14:41 +0200 Subject: [PATCH 08/26] Define AuthenticationToken with Slice2 --- examples/Authorization/Authorization.slice | 9 +++++ .../Server/AuthenticationMiddleware.cs | 2 +- .../Server/AuthenticationToken.cs | 38 ++++++------------- .../Authorization/Server/Authenticator.cs | 2 +- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index 2e16f05eb2..fb7aa12833 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -2,6 +2,15 @@ module AuthorizationExample +/// An authentication token. +struct AuthenticationToken { + /// true if the authenticated client has administrative privilege, false otherwise. + isAdmin: bool + + /// The user name. + name: string +} + /// The encrypted authentication token. typealias EncryptedAuthenticationToken = sequence diff --git a/examples/Authorization/Server/AuthenticationMiddleware.cs b/examples/Authorization/Server/AuthenticationMiddleware.cs index 642433df3d..fb0638d166 100644 --- a/examples/Authorization/Server/AuthenticationMiddleware.cs +++ b/examples/Authorization/Server/AuthenticationMiddleware.cs @@ -18,7 +18,7 @@ public ValueTask DispatchAsync(IncomingRequest request, Cancel { if (request.Fields.TryGetValue(AuthenticationTokenFieldKey.Value, out ReadOnlySequence buffer)) { - var token = AuthenticationToken.Decrypt(buffer.ToArray(), _encryptionAlgorithm); + var token = buffer.DecryptAuthenticationToken(_encryptionAlgorithm); request.Features = request.Features.With(new AuthenticationFeature(token)); } return _next.DispatchAsync(request, cancellationToken); diff --git a/examples/Authorization/Server/AuthenticationToken.cs b/examples/Authorization/Server/AuthenticationToken.cs index c67a1edfb3..432de9146b 100644 --- a/examples/Authorization/Server/AuthenticationToken.cs +++ b/examples/Authorization/Server/AuthenticationToken.cs @@ -1,59 +1,45 @@ // Copyright (c) ZeroC, Inc. using IceRpc.Slice; +using System.Buffers; using System.IO.Pipelines; using System.Security.Cryptography; namespace AuthorizationExample; -/// An struct that defines an authentication token and methods to encrypt and decrypt the authentication -/// token. -internal readonly struct AuthenticationToken +/// Extension methods to encrypt and decrypt an authentication token. +public static class AuthenticationTokenExtensions { - /// true if the authenticated client has administrative privilege, false otherwise. - internal bool IsAdmin { get; } - - /// The name of the authenticated client. - internal string Name { get; } - /// Decrypts an authentication token. /// The byte buffer. /// The symmetric algorithm used to decrypt an authentication token. /// The authentication token. - internal static AuthenticationToken Decrypt(byte[] buffer, SymmetricAlgorithm algorithm) + internal static AuthenticationToken DecryptAuthenticationToken( + this ReadOnlySequence buffer, + SymmetricAlgorithm algorithm) { // Decrypt the Slice2 encoded token. - using var sourceStream = new MemoryStream(buffer); + using var sourceStream = new MemoryStream(buffer.ToArray()); using var cryptoStream = new CryptoStream(sourceStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read); using var destinationStream = new MemoryStream(); cryptoStream.CopyTo(destinationStream); // Decode the Slice2 encoded token. var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); - return new AuthenticationToken(decoder.DecodeString(), decoder.DecodeBool()); - } - - /// Constructs a new authentication token. - /// The name of the authentication token. - /// true if the authenticated client has administrative privilege, false - /// otherwise. - internal AuthenticationToken(string name, bool isAdmin) - { - Name = name; - IsAdmin = isAdmin; + return new AuthenticationToken(ref decoder); } - /// Encodes and encrypts this authentication token. + /// Encrypts an authentication token. + /// The authentication token. /// The symmetric algorithm used to encrypt this authentication token. /// The encrypted authentication token. - internal ReadOnlyMemory Encrypt(SymmetricAlgorithm algorithm) + internal static ReadOnlyMemory Encrypt(this AuthenticationToken token, SymmetricAlgorithm algorithm) { // Encode the token with the Slice2 encoding. using var tokenStream = new MemoryStream(); var writer = PipeWriter.Create(tokenStream, new StreamPipeWriterOptions(leaveOpen: true)); var encoder = new SliceEncoder(writer, SliceEncoding.Slice2); - encoder.EncodeString(Name); - encoder.EncodeBool(IsAdmin); + token.Encode(ref encoder); writer.Complete(); tokenStream.Seek(0, SeekOrigin.Begin); diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs index 0c48ed9aee..07125c4d82 100644 --- a/examples/Authorization/Server/Authenticator.cs +++ b/examples/Authorization/Server/Authenticator.cs @@ -22,7 +22,7 @@ public ValueTask> AuthenticateAsync( { throw new DispatchException(StatusCode.Unauthorized, "Invalid password."); } - return new(new AuthenticationToken(name, isAdmin: name == "admin").Encrypt(_encryptionAlgorithm)); + return new(new AuthenticationToken(isAdmin: name == "admin", name).Encrypt(_encryptionAlgorithm)); } /// Constructs an authenticator service. From d63c7c327fcc862c937d103a170d9a6be15873a2 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 28 Mar 2023 15:16:01 +0200 Subject: [PATCH 09/26] File renaming --- .../Server/AuthenticationTokenExtensions.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 examples/Authorization/Server/AuthenticationTokenExtensions.cs diff --git a/examples/Authorization/Server/AuthenticationTokenExtensions.cs b/examples/Authorization/Server/AuthenticationTokenExtensions.cs new file mode 100644 index 0000000000..432de9146b --- /dev/null +++ b/examples/Authorization/Server/AuthenticationTokenExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc.Slice; +using System.Buffers; +using System.IO.Pipelines; +using System.Security.Cryptography; + +namespace AuthorizationExample; + +/// Extension methods to encrypt and decrypt an authentication token. +public static class AuthenticationTokenExtensions +{ + /// Decrypts an authentication token. + /// The byte buffer. + /// The symmetric algorithm used to decrypt an authentication token. + /// The authentication token. + internal static AuthenticationToken DecryptAuthenticationToken( + this ReadOnlySequence buffer, + SymmetricAlgorithm algorithm) + { + // Decrypt the Slice2 encoded token. + using var sourceStream = new MemoryStream(buffer.ToArray()); + using var cryptoStream = new CryptoStream(sourceStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read); + using var destinationStream = new MemoryStream(); + cryptoStream.CopyTo(destinationStream); + + // Decode the Slice2 encoded token. + var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); + return new AuthenticationToken(ref decoder); + } + + /// Encrypts an authentication token. + /// The authentication token. + /// The symmetric algorithm used to encrypt this authentication token. + /// The encrypted authentication token. + internal static ReadOnlyMemory Encrypt(this AuthenticationToken token, SymmetricAlgorithm algorithm) + { + // Encode the token with the Slice2 encoding. + using var tokenStream = new MemoryStream(); + var writer = PipeWriter.Create(tokenStream, new StreamPipeWriterOptions(leaveOpen: true)); + var encoder = new SliceEncoder(writer, SliceEncoding.Slice2); + token.Encode(ref encoder); + writer.Complete(); + tokenStream.Seek(0, SeekOrigin.Begin); + + // Crypt and return the Slice2 encoded token. + using var destinationStream = new MemoryStream(); + using var cryptoStream = new CryptoStream(tokenStream, algorithm.CreateEncryptor(), CryptoStreamMode.Read); + cryptoStream.CopyTo(destinationStream); + return destinationStream.ToArray(); + } +} From 70e9628b198fddfd8720b9e9f1ac04247542f21d Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 28 Mar 2023 15:18:36 +0200 Subject: [PATCH 10/26] Doc fixes --- examples/Authorization/Authorization.slice | 4 +- .../Server/AuthenticationToken.cs | 52 ------------------- 2 files changed, 2 insertions(+), 54 deletions(-) delete mode 100644 examples/Authorization/Server/AuthenticationToken.cs diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index fb7aa12833..fc5992ffc2 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -23,14 +23,14 @@ interface Authenticator { authenticate(name: string, password: string) -> EncryptedAuthenticationToken } -/// Represents a simple greeter. +/// Represents a recipient of hello greetings. interface Hello { /// Creates a personalized "hello" greeting. /// @returns: The greeting. sayHello() -> string } -/// Represents a service to configure a simpler greeter. +/// Represents a service to configure the hello service greeting. interface HelloAdmin { /// Changes the greeting returned by the Hello service. /// @param greeting: The new greeting. diff --git a/examples/Authorization/Server/AuthenticationToken.cs b/examples/Authorization/Server/AuthenticationToken.cs deleted file mode 100644 index 432de9146b..0000000000 --- a/examples/Authorization/Server/AuthenticationToken.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using IceRpc.Slice; -using System.Buffers; -using System.IO.Pipelines; -using System.Security.Cryptography; - -namespace AuthorizationExample; - -/// Extension methods to encrypt and decrypt an authentication token. -public static class AuthenticationTokenExtensions -{ - /// Decrypts an authentication token. - /// The byte buffer. - /// The symmetric algorithm used to decrypt an authentication token. - /// The authentication token. - internal static AuthenticationToken DecryptAuthenticationToken( - this ReadOnlySequence buffer, - SymmetricAlgorithm algorithm) - { - // Decrypt the Slice2 encoded token. - using var sourceStream = new MemoryStream(buffer.ToArray()); - using var cryptoStream = new CryptoStream(sourceStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read); - using var destinationStream = new MemoryStream(); - cryptoStream.CopyTo(destinationStream); - - // Decode the Slice2 encoded token. - var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); - return new AuthenticationToken(ref decoder); - } - - /// Encrypts an authentication token. - /// The authentication token. - /// The symmetric algorithm used to encrypt this authentication token. - /// The encrypted authentication token. - internal static ReadOnlyMemory Encrypt(this AuthenticationToken token, SymmetricAlgorithm algorithm) - { - // Encode the token with the Slice2 encoding. - using var tokenStream = new MemoryStream(); - var writer = PipeWriter.Create(tokenStream, new StreamPipeWriterOptions(leaveOpen: true)); - var encoder = new SliceEncoder(writer, SliceEncoding.Slice2); - token.Encode(ref encoder); - writer.Complete(); - tokenStream.Seek(0, SeekOrigin.Begin); - - // Crypt and return the Slice2 encoded token. - using var destinationStream = new MemoryStream(); - using var cryptoStream = new CryptoStream(tokenStream, algorithm.CreateEncryptor(), CryptoStreamMode.Read); - cryptoStream.CopyTo(destinationStream); - return destinationStream.ToArray(); - } -} From f1ba43e678474eac75a70fdc43366ab5e39407d9 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 28 Mar 2023 15:20:12 +0200 Subject: [PATCH 11/26] Another doc fix --- examples/Authorization/Server/AuthenticationFeature.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Authorization/Server/AuthenticationFeature.cs b/examples/Authorization/Server/AuthenticationFeature.cs index 3168607124..f0564e555b 100644 --- a/examples/Authorization/Server/AuthenticationFeature.cs +++ b/examples/Authorization/Server/AuthenticationFeature.cs @@ -6,7 +6,7 @@ namespace AuthorizationExample; /// field. internal interface IAuthenticationFeature { - /// true if the authenticated client has administrative privilege, false otherwise. + /// true if the authenticated client has administrative privilege, false otherwise. bool IsAdmin { get; } /// The name of the authenticated client. From 66528d0fe4cfdf5ef07638ac5252d3905a6acf26 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Wed, 29 Mar 2023 10:47:42 +0200 Subject: [PATCH 12/26] Review fixes --- examples/Authorization/Authorization.slice | 26 ++++++------ .../Client/AuthenticationInterceptor.cs | 12 +++--- examples/Authorization/Client/Client.csproj | 2 +- .../Client/PipelineExtensions.cs | 6 +-- examples/Authorization/Client/Program.cs | 40 +++++++++---------- ...enFieldKey.cs => IdentityTokenFieldKey.cs} | 4 +- examples/Authorization/README.md | 20 +++++----- .../Server/AuthenticationMiddleware.cs | 12 +++--- .../Authorization/Server/Authenticator.cs | 20 ++++++++-- .../Server/AuthorizationMiddleware.cs | 8 ++-- examples/Authorization/Server/Chatbot.cs | 24 ++++++++--- examples/Authorization/Server/ChatbotAdmin.cs | 6 +-- ...nticationFeature.cs => IdentityFeature.cs} | 12 +++--- ...tensions.cs => IdentityTokenExtensions.cs} | 24 +++++------ examples/Authorization/Server/Program.cs | 22 +++++----- .../Authorization/Server/RouterExtensions.cs | 4 +- examples/Authorization/Server/Server.csproj | 2 +- 17 files changed, 134 insertions(+), 110 deletions(-) rename examples/Authorization/{AuthenticationTokenFieldKey.cs => IdentityTokenFieldKey.cs} (70%) rename examples/Authorization/Server/{AuthenticationFeature.cs => IdentityFeature.cs} (59%) rename examples/Authorization/Server/{AuthenticationTokenExtensions.cs => IdentityTokenExtensions.cs} (68%) diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index fc5992ffc2..e65ed73eaa 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -2,8 +2,8 @@ module AuthorizationExample -/// An authentication token. -struct AuthenticationToken { +/// An identity token. +struct IdentityToken { /// true if the authenticated client has administrative privilege, false otherwise. isAdmin: bool @@ -11,28 +11,28 @@ struct AuthenticationToken { name: string } -/// The encrypted authentication token. -typealias EncryptedAuthenticationToken = sequence +/// The encrypted identity token. +typealias EncryptedIdentityToken = sequence -/// Represents a service to authentication a user and return an authentication token. +/// Represents a service to authenticate a user and return an identity token. interface Authenticator { /// Authenticates a user. /// @param name: The user name. /// @param password: The password. - /// @returns: The encrypted authentication token. - authenticate(name: string, password: string) -> EncryptedAuthenticationToken + /// @returns: The encrypted identity token. + authenticate(name: string, password: string) -> EncryptedIdentityToken } -/// Represents a recipient of hello greetings. -interface Hello { +/// Represents a recipient of greetings. +interface Greeting { /// Creates a personalized "hello" greeting. /// @returns: The greeting. - sayHello() -> string + getGreeting() -> string } -/// Represents a service to configure the hello service greeting. -interface HelloAdmin { - /// Changes the greeting returned by the Hello service. +/// Represents a service to configure the greeting of the greeting service. +interface GreetingAdmin { + /// Changes the greeting returned by the Greeting service. /// @param greeting: The new greeting. changeGreeting(greeting: string) } diff --git a/examples/Authorization/Client/AuthenticationInterceptor.cs b/examples/Authorization/Client/AuthenticationInterceptor.cs index f8a1368bd2..e25c53ca91 100644 --- a/examples/Authorization/Client/AuthenticationInterceptor.cs +++ b/examples/Authorization/Client/AuthenticationInterceptor.cs @@ -5,24 +5,24 @@ namespace AuthorizationExample; -/// An interceptor that adds the encrypted authentication token field to each request. +/// An interceptor that adds the encrypted identity token field to each request. internal class AuthenticationInterceptor : IInvoker { - private readonly ReadOnlySequence _authenticationToken; + private readonly ReadOnlySequence _identityToken; private readonly IInvoker _next; public Task InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken) { - request.Fields = request.Fields.With(AuthenticationTokenFieldKey.Value, _authenticationToken); + request.Fields = request.Fields.With(IdentityTokenFieldKey.Value, _identityToken); return _next.InvokeAsync(request, cancellationToken); } /// Constructs an authentication interceptor. /// The invoker to call next. - /// The encrypted authentication token. - internal AuthenticationInterceptor(IInvoker next, ReadOnlyMemory authenticationToken) + /// The encrypted identity token. + internal AuthenticationInterceptor(IInvoker next, ReadOnlyMemory identityToken) { _next = next; - _authenticationToken = new ReadOnlySequence(authenticationToken); + _identityToken = new ReadOnlySequence(identityToken); } } diff --git a/examples/Authorization/Client/Client.csproj b/examples/Authorization/Client/Client.csproj index 4e6161b8e0..8815a0c268 100644 --- a/examples/Authorization/Client/Client.csproj +++ b/examples/Authorization/Client/Client.csproj @@ -4,6 +4,6 @@ - + diff --git a/examples/Authorization/Client/PipelineExtensions.cs b/examples/Authorization/Client/PipelineExtensions.cs index 368dc57331..bf9fb4acaa 100644 --- a/examples/Authorization/Client/PipelineExtensions.cs +++ b/examples/Authorization/Client/PipelineExtensions.cs @@ -10,8 +10,8 @@ internal static class PipelineExtensions { /// Adds an to the pipeline. /// The pipeline being configured. - /// The encrypted authentication token. + /// The encrypted identity token. /// The pipeline being configured. - internal static Pipeline UseAuthentication(this Pipeline pipeline, ReadOnlyMemory authenticationToken) => - pipeline.Use(next => new AuthenticationInterceptor(next, authenticationToken)); + internal static Pipeline UseAuthentication(this Pipeline pipeline, ReadOnlyMemory identityToken) => + pipeline.Use(next => new AuthenticationInterceptor(next, identityToken)); } diff --git a/examples/Authorization/Client/Program.cs b/examples/Authorization/Client/Program.cs index 136c81d12f..e21a751a4c 100644 --- a/examples/Authorization/Client/Program.cs +++ b/examples/Authorization/Client/Program.cs @@ -7,48 +7,48 @@ var authenticatorProxy = new AuthenticatorProxy(connection, new Uri("icerpc:/authenticator")); -// Authenticate the "friend" user and get its authentication token. +// Authenticate the "friend" user and get its identity token. ReadOnlyMemory friendToken = await authenticatorProxy.AuthenticateAsync("friend", "password"); -// A hello proxy that doesn't use any authentication token. -var unauthenticatedHelloProxy = new HelloProxy(connection, new Uri("icerpc:/hello")); +// A greeting proxy that doesn't use any identity token. +var unauthenticatedGreetingProxy = new GreetingProxy(connection, new Uri("icerpc:/greeting")); -// The SayHello invocation on the unauthenticated hello proxy prints a generic message. -Console.WriteLine(await unauthenticatedHelloProxy.SayHelloAsync()); +// The SayGreeting invocation on the unauthenticated greeting proxy prints a generic message. +Console.WriteLine(await unauthenticatedGreetingProxy.GetGreetingAsync()); -// Create a hello proxy that uses a pipe line to insert the "friend" token into a request field. +// Create a greeting proxy that uses a pipe line to insert the "friend" token into a request field. Pipeline friendPipeline = new Pipeline().UseAuthentication(friendToken).Into(connection); -var friendHelloProxy = new HelloProxy(friendPipeline, new Uri("icerpc:/hello")); +var friendGreetingProxy = new GreetingProxy(friendPipeline, new Uri("icerpc:/greeting")); -// The SayHello invocation on the authenticated "friend" hello proxy prints a custom message for "friend". -Console.WriteLine(await friendHelloProxy.SayHelloAsync()); +// The SayGreeting invocation on the authenticated "friend" greeting proxy prints a custom message for "friend". +Console.WriteLine(await friendGreetingProxy.GetGreetingAsync()); -// A hello admin proxy that uses the "friend" pipeline is not authorized to change the greeting message because "friend" +// A greeting admin proxy that uses the "friend" pipeline is not authorized to change the greeting message because "friend" // doesn't have administrative privileges. -var helloAdminProxy = new HelloAdminProxy(friendPipeline, new Uri("icerpc:/helloAdmin")); +var greetingAdminProxy = new GreetingAdminProxy(friendPipeline, new Uri("icerpc:/greetingAdmin")); try { - await helloAdminProxy.ChangeGreetingAsync("Bonjour"); + await greetingAdminProxy.ChangeGreetingAsync("Bonjour"); } catch (DispatchException exception) when (exception.StatusCode == StatusCode.Unauthorized) { Console.WriteLine("The 'friend' user is not authorized to change the greeting message."); } -// Authenticate the "admin" user and get its authentication token. +// Authenticate the "admin" user and get its identity token. ReadOnlyMemory adminToken = await authenticatorProxy.AuthenticateAsync("admin", "password"); -// Create a hello admin proxy that uses a pipe line to insert the "admin" token into a request field. +// Create a greeting admin proxy that uses a pipe line to insert the "admin" token into a request field. Pipeline adminPipeline = new Pipeline().UseAuthentication(adminToken).Into(connection); -helloAdminProxy = new HelloAdminProxy(adminPipeline, new Uri("icerpc:/helloAdmin")); +greetingAdminProxy = new GreetingAdminProxy(adminPipeline, new Uri("icerpc:/greetingAdmin")); // Changing the greeting message should succeed this time because the "admin" user has administrative privilege. -await helloAdminProxy.ChangeGreetingAsync("Bonjour"); +await greetingAdminProxy.ChangeGreetingAsync("Bonjour"); -// The SayHello invocation should print a greeting with the updated greeting message. -Console.WriteLine(await friendHelloProxy.SayHelloAsync()); +// The SayGreeting invocation should print a greeting with the updated greeting message. +Console.WriteLine(await friendGreetingProxy.GetGreetingAsync()); -// Change back the greeting message to Hello. -await helloAdminProxy.ChangeGreetingAsync("Hello"); +// Change back the greeting message to Greeting. +await greetingAdminProxy.ChangeGreetingAsync("Greeting"); await connection.ShutdownAsync(); diff --git a/examples/Authorization/AuthenticationTokenFieldKey.cs b/examples/Authorization/IdentityTokenFieldKey.cs similarity index 70% rename from examples/Authorization/AuthenticationTokenFieldKey.cs rename to examples/Authorization/IdentityTokenFieldKey.cs index 7eea5975f6..a21e508ac6 100644 --- a/examples/Authorization/AuthenticationTokenFieldKey.cs +++ b/examples/Authorization/IdentityTokenFieldKey.cs @@ -4,9 +4,9 @@ namespace AuthorizationExample; -/// The shared used by the client and server to carry the authentication +/// The shared used by the client and server to carry the identity /// token. -public static class AuthenticationTokenFieldKey +public static class IdentityTokenFieldKey { public const RequestFieldKey Value = (RequestFieldKey)100; } diff --git a/examples/Authorization/README.md b/examples/Authorization/README.md index de30a72c85..50fc43b15b 100644 --- a/examples/Authorization/README.md +++ b/examples/Authorization/README.md @@ -4,12 +4,12 @@ This example application illustrates how to create an authentication interceptor authorization middleware that can be used to authorize requests. The server is configured with two middleware: `AuthenticationMiddleware` and `AuthorizationMiddleware`. The first -middleware is responsible for decrypting an authentication token from the request field and storing it in a -corresponding request feature. The second middleware is responsible for checking if the authentication feature is -present in the corresponding request feature and to check if the request is authorized. +middleware is responsible for decrypting an identity token from the request field and storing it in a corresponding +request feature. The second middleware is responsible for checking if the identity feature is present in the +corresponding request feature and check if the request is authorized. -The client is configured with an `AuthenticationInterceptor` interceptor. The interceptor is responsible for adding a -encrypted authentication token to a request field. The authentication token is returned by an `Authenticator` service after +The client is configured with an `AuthenticationInterceptor` interceptor. The interceptor is responsible for adding the +encrypted identity token to a request field. The identity token is returned by an `Authenticator` service after authenticating the client with a login name and password. ## Running the example @@ -26,13 +26,13 @@ In a separate window, start the Client: dotnet run --project Client/Client.csproj ``` -The client first calls `SayHelloAsync` without an authentication token and the server responds with generic a greeting. +The client first calls `GetGreetingAsync` without an identity token and the server responds with a generic greeting. -Next, the client gets an authentication token for the user `friend` and uses it to construct an authentication -invocation pipeline that adds the `friend` token to each request. The client then calls `SayHelloAsync` using this +Next, the client gets an identity token for the user `friend` and uses it to construct an authentication invocation +pipeline that adds the `friend` identity token to each request. The client then calls `GetGreetingAsync` using the `friend` authentication pipeline and receives a personalized message. -Next, the client calls `ChangeGreetingAsync` using the `friend` authentication pipeline to change the greeting. The client doesn't have administrative privilege so the invocation fails with a `DispatchException`. +Next, the client calls `ChangeGreetingAsync` using the `friend` authentication pipeline to change the greeting. The user `friend` doesn't have administrative privilege so the invocation fails with a `DispatchException`. Finally, the client authenticates the user `admin` and calls `ChangeGreetingAsync` using an `admin` authentication -pipeline to change the greeting. +pipeline. The call succeeds because the user `admin` has administrative privilege. diff --git a/examples/Authorization/Server/AuthenticationMiddleware.cs b/examples/Authorization/Server/AuthenticationMiddleware.cs index fb0638d166..183a5719cb 100644 --- a/examples/Authorization/Server/AuthenticationMiddleware.cs +++ b/examples/Authorization/Server/AuthenticationMiddleware.cs @@ -7,8 +7,8 @@ namespace AuthorizationExample; -/// A middleware that decodes and decrypt an authentication token request field and adds an authentication -/// feature to the request's feature collection. +/// A middleware that decodes and decrypts an identity token request field and adds an identity feature to the +/// request's feature collection. internal class AuthenticationMiddleware : IDispatcher { private readonly SymmetricAlgorithm _encryptionAlgorithm; @@ -16,17 +16,17 @@ internal class AuthenticationMiddleware : IDispatcher public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { - if (request.Fields.TryGetValue(AuthenticationTokenFieldKey.Value, out ReadOnlySequence buffer)) + if (request.Fields.TryGetValue(IdentityTokenFieldKey.Value, out ReadOnlySequence buffer)) { - var token = buffer.DecryptAuthenticationToken(_encryptionAlgorithm); - request.Features = request.Features.With(new AuthenticationFeature(token)); + var token = buffer.DecryptIdentityToken(_encryptionAlgorithm); + request.Features = request.Features.With(new IdentityFeature(token)); } return _next.DispatchAsync(request, cancellationToken); } /// Constructs an authentication middleware. /// The invoker to call next. - /// The encryption algorithm used to encrypt an authentication token. + /// The encryption algorithm used to encrypt an identity token. internal AuthenticationMiddleware(IDispatcher next, SymmetricAlgorithm encryptionAlgorithm) { _next = next; diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs index 07125c4d82..183dabfdbe 100644 --- a/examples/Authorization/Server/Authenticator.cs +++ b/examples/Authorization/Server/Authenticator.cs @@ -18,15 +18,27 @@ public ValueTask> AuthenticateAsync( IFeatureCollection features, CancellationToken cancellationToken) { - if (password != "password") + // Check if the user name and password are valid. + IdentityToken identityToken; + if (name == "admin" && password == "admin-password") { - throw new DispatchException(StatusCode.Unauthorized, "Invalid password."); + identityToken = new IdentityToken(isAdmin: true, name); } - return new(new AuthenticationToken(isAdmin: name == "admin", name).Encrypt(_encryptionAlgorithm)); + else if (name == "friend" && password == "password") + { + identityToken = new IdentityToken(isAdmin: false, name); + } + else + { + throw new DispatchException(StatusCode.Unauthorized, "Unknown user or invalid password."); + } + + // Return the encrypted identity token. + return new(identityToken.Encrypt(_encryptionAlgorithm)); } /// Constructs an authenticator service. - /// The encryption algorithm used to encrypt an authentication token. + /// The encryption algorithm used to encrypt an identity token. internal Authenticator(SymmetricAlgorithm encryptionAlgorithm) => _encryptionAlgorithm = encryptionAlgorithm; } diff --git a/examples/Authorization/Server/AuthorizationMiddleware.cs b/examples/Authorization/Server/AuthorizationMiddleware.cs index be89383c54..951be389d6 100644 --- a/examples/Authorization/Server/AuthorizationMiddleware.cs +++ b/examples/Authorization/Server/AuthorizationMiddleware.cs @@ -5,15 +5,15 @@ namespace AuthorizationExample; /// A middleware that checks if the request is authorized. If not, it throws a . +/// /> with the status code. internal class AuthorizationMiddleware : IDispatcher { - private readonly Func _authorizeFunc; + private readonly Func _authorizeFunc; private readonly IDispatcher _next; public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { - if (request.Features.Get() is IAuthenticationFeature authenticationFeature && + if (request.Features.Get() is IIdentityFeature authenticationFeature && _authorizeFunc(authenticationFeature)) { return _next.DispatchAsync(request, cancellationToken); @@ -27,7 +27,7 @@ public ValueTask DispatchAsync(IncomingRequest request, Cancel /// Constructs an authentication middleware. /// The dispatcher to call next. /// The authorization function. - internal AuthorizationMiddleware(IDispatcher next, Func authorizeFunc) + internal AuthorizationMiddleware(IDispatcher next, Func authorizeFunc) { _next = next; _authorizeFunc = authorizeFunc; diff --git a/examples/Authorization/Server/Chatbot.cs b/examples/Authorization/Server/Chatbot.cs index 1ef7a7818c..c676a89d06 100644 --- a/examples/Authorization/Server/Chatbot.cs +++ b/examples/Authorization/Server/Chatbot.cs @@ -5,15 +5,27 @@ namespace AuthorizationExample; -/// A Chatbot is an IceRPC service that implements the Slice interface 'Hello'. -internal class Chatbot : Service, IHelloService +/// A Chatbot is an IceRPC service that implements the Slice interface 'Greeting'. +internal class Chatbot : Service, IGreetingService { internal string Greeting { get; set; } = "Hello"; - public ValueTask SayHelloAsync(IFeatureCollection features, CancellationToken cancellationToken) + public ValueTask GetGreetingAsync(IFeatureCollection features, CancellationToken cancellationToken) { - string who = features.Get()?.Name ?? "stranger"; - Console.WriteLine($"Dispatching sayHello request {{ name = '{who}' }}"); - return new($"{Greeting}, {who}!"); + string name; + bool isAdmin; + if (features.Get() is IIdentityFeature identityFeature) + { + name = identityFeature.Name; + isAdmin = identityFeature.IsAdmin; + } + else + { + name = "stranger"; + isAdmin = false; + } + + Console.WriteLine($"Dispatching GetGreeting request {{ name = '{name}' isAdmin = {isAdmin} }}"); + return new($"{Greeting}, {name}!"); } } diff --git a/examples/Authorization/Server/ChatbotAdmin.cs b/examples/Authorization/Server/ChatbotAdmin.cs index 4b6a01e084..40566fc450 100644 --- a/examples/Authorization/Server/ChatbotAdmin.cs +++ b/examples/Authorization/Server/ChatbotAdmin.cs @@ -5,8 +5,8 @@ namespace AuthorizationExample; -/// A ChatbotAdmin is an IceRPC service that implements the Slice interface 'HelloAdmin'. -internal class ChatbotAdmin : Service, IHelloAdminService +/// A ChatbotAdmin is an IceRPC service that implements the Slice interface 'GreetingAdmin'. +internal class ChatbotAdmin : Service, IGreetingAdminService { private readonly Chatbot _chatbot; @@ -17,7 +17,7 @@ public ValueTask ChangeGreetingAsync( IFeatureCollection features, CancellationToken cancellationToken) { - string who = features.Get()?.Name ?? "stranger"; + string who = features.Get()?.Name ?? "stranger"; Console.WriteLine($"Dispatching changeGreeting request {{ name = '{who}' greeting = '{greeting}' }}"); _chatbot.Greeting = greeting; return default; diff --git a/examples/Authorization/Server/AuthenticationFeature.cs b/examples/Authorization/Server/IdentityFeature.cs similarity index 59% rename from examples/Authorization/Server/AuthenticationFeature.cs rename to examples/Authorization/Server/IdentityFeature.cs index f0564e555b..8d8d57b023 100644 --- a/examples/Authorization/Server/AuthenticationFeature.cs +++ b/examples/Authorization/Server/IdentityFeature.cs @@ -2,9 +2,9 @@ namespace AuthorizationExample; -/// A feature that provides the name and administrative privilege decoded from an authentication token +/// A feature that provides the name and administrative privilege decoded from an identity token /// field. -internal interface IAuthenticationFeature +internal interface IIdentityFeature { /// true if the authenticated client has administrative privilege, false otherwise. bool IsAdmin { get; } @@ -13,8 +13,8 @@ internal interface IAuthenticationFeature string Name { get; } } -/// The implementation of . -internal class AuthenticationFeature : IAuthenticationFeature +/// The implementation of . +internal class IdentityFeature : IIdentityFeature { /// public bool IsAdmin { get; } @@ -22,8 +22,8 @@ internal class AuthenticationFeature : IAuthenticationFeature /// public string Name { get; } - /// Constructs an authentication feature from an authentication token. - internal AuthenticationFeature(AuthenticationToken token) + /// Constructs an identity feature from an identity token. + internal IdentityFeature(IdentityToken token) { Name = token.Name; IsAdmin = token.IsAdmin; diff --git a/examples/Authorization/Server/AuthenticationTokenExtensions.cs b/examples/Authorization/Server/IdentityTokenExtensions.cs similarity index 68% rename from examples/Authorization/Server/AuthenticationTokenExtensions.cs rename to examples/Authorization/Server/IdentityTokenExtensions.cs index 432de9146b..49b5a4b7a4 100644 --- a/examples/Authorization/Server/AuthenticationTokenExtensions.cs +++ b/examples/Authorization/Server/IdentityTokenExtensions.cs @@ -7,14 +7,14 @@ namespace AuthorizationExample; -/// Extension methods to encrypt and decrypt an authentication token. -public static class AuthenticationTokenExtensions +/// Extension methods to encrypt and decrypt an identity token. +public static class IdentityTokenExtensions { - /// Decrypts an authentication token. + /// Decrypts an identity token. /// The byte buffer. - /// The symmetric algorithm used to decrypt an authentication token. - /// The authentication token. - internal static AuthenticationToken DecryptAuthenticationToken( + /// The symmetric algorithm used to decrypt an identity token. + /// The identity token. + internal static IdentityToken DecryptIdentityToken( this ReadOnlySequence buffer, SymmetricAlgorithm algorithm) { @@ -26,14 +26,14 @@ internal static AuthenticationToken DecryptAuthenticationToken( // Decode the Slice2 encoded token. var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); - return new AuthenticationToken(ref decoder); + return new IdentityToken(ref decoder); } - /// Encrypts an authentication token. - /// The authentication token. - /// The symmetric algorithm used to encrypt this authentication token. - /// The encrypted authentication token. - internal static ReadOnlyMemory Encrypt(this AuthenticationToken token, SymmetricAlgorithm algorithm) + /// Encrypts an identity token. + /// The identity token. + /// The symmetric algorithm used to encrypt this identity token. + /// The encrypted identity token. + internal static ReadOnlyMemory Encrypt(this IdentityToken token, SymmetricAlgorithm algorithm) { // Encode the token with the Slice2 encoding. using var tokenStream = new MemoryStream(); diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index ea5814a66e..979dd41722 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -4,28 +4,28 @@ using IceRpc; using System.Security.Cryptography; -// The authentication token is encrypted and decrypted with the AES symmetric encryption algorithm. +// The identity token is encrypted and decrypted with the AES symmetric encryption algorithm. using var aes = Aes.Create(); aes.Padding = PaddingMode.Zeros; -var chatbot = new Chatbot(); - var router = new Router(); -// Install a middleware to decrypt and decode the request's authentication token and add an authentication feature to -// the request's feature collection. +// Install a middleware to decrypt and decode the request's identity token and add an identity feature to the request's +// feature collection. router.UseAuthentication(aes); -router.Route("/helloAdmin", adminRouter => +var chatbot = new Chatbot(); +router.Map("/greeting", chatbot); + +router.Map("/authenticator", new Authenticator(aes)); + +router.Route("/greetingAdmin", adminRouter => { - // Install an authorization middleware that checks if the caller is authorized to call the hello admin service. - adminRouter.UseAuthorization(authenticationFeature => authenticationFeature.IsAdmin); + // Install an authorization middleware that checks if the caller is authorized to call the greeting admin service. + adminRouter.UseAuthorization(identityFeature => identityFeature.IsAdmin); adminRouter.Map("/", new ChatbotAdmin(chatbot)); }); -router.Map("/authenticator", new Authenticator(aes)); -router.Map("/hello", chatbot); - await using var server = new Server(router); server.Listen(); diff --git a/examples/Authorization/Server/RouterExtensions.cs b/examples/Authorization/Server/RouterExtensions.cs index a791420ec2..f670122f80 100644 --- a/examples/Authorization/Server/RouterExtensions.cs +++ b/examples/Authorization/Server/RouterExtensions.cs @@ -11,7 +11,7 @@ internal static class RouterExtensions { /// Adds an to the router. /// The router being configured. - /// The encryption algorithm used to encrypt the authentication token. + /// The encryption algorithm used to encrypt the identity token. /// The router being configured. internal static Router UseAuthentication(this Router router, SymmetricAlgorithm encryptionAlgorithm) => router.Use(next => new AuthenticationMiddleware(next, encryptionAlgorithm)); @@ -20,6 +20,6 @@ internal static Router UseAuthentication(this Router router, SymmetricAlgorithm /// The router being configured. /// The function called by the middleware to check if the request is authorized. /// The router being configured. - internal static Router UseAuthorization(this Router router, Func authorizeFunc) => + internal static Router UseAuthorization(this Router router, Func authorizeFunc) => router.Use(next => new AuthorizationMiddleware(next, authorizeFunc)); } diff --git a/examples/Authorization/Server/Server.csproj b/examples/Authorization/Server/Server.csproj index ebadde28ea..689d5fe1bb 100644 --- a/examples/Authorization/Server/Server.csproj +++ b/examples/Authorization/Server/Server.csproj @@ -4,7 +4,7 @@ - + From 4fb4239756a015deafad72da9ac0108c040bc86e Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Wed, 29 Mar 2023 10:56:12 +0200 Subject: [PATCH 13/26] Doc changes --- examples/Authorization/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/Authorization/README.md b/examples/Authorization/README.md index 50fc43b15b..7238bb4a95 100644 --- a/examples/Authorization/README.md +++ b/examples/Authorization/README.md @@ -1,12 +1,13 @@ # Authorization -This example application illustrates how to create an authentication interceptor, authentication middleware and -authorization middleware that can be used to authorize requests. +This demo demonstrates how token based authorization and authentication can be implemented with an interceptor and two +middleware. The token provides identify information and is encrypted with a symmetric encryption algorithm (AES). An +application would typically use a 3rd-party token based authentication library instead. The server is configured with two middleware: `AuthenticationMiddleware` and `AuthorizationMiddleware`. The first middleware is responsible for decrypting an identity token from the request field and storing it in a corresponding request feature. The second middleware is responsible for checking if the identity feature is present in the -corresponding request feature and check if the request is authorized. +corresponding request feature and it checks if the request is authorized. The client is configured with an `AuthenticationInterceptor` interceptor. The interceptor is responsible for adding the encrypted identity token to a request field. The identity token is returned by an `Authenticator` service after From 905e7b7c7298901aad865652ab1b512642e81f80 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Wed, 29 Mar 2023 10:59:47 +0200 Subject: [PATCH 14/26] More changes --- examples/Authorization/Authorization.slice | 2 +- examples/Authorization/Client/Program.cs | 2 +- examples/Authorization/Server/IdentityFeature.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index e65ed73eaa..7b418b6227 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -4,7 +4,7 @@ module AuthorizationExample /// An identity token. struct IdentityToken { - /// true if the authenticated client has administrative privilege, false otherwise. + /// true if the authenticated user has administrative privilege, false otherwise. isAdmin: bool /// The user name. diff --git a/examples/Authorization/Client/Program.cs b/examples/Authorization/Client/Program.cs index e21a751a4c..bbef568a88 100644 --- a/examples/Authorization/Client/Program.cs +++ b/examples/Authorization/Client/Program.cs @@ -36,7 +36,7 @@ } // Authenticate the "admin" user and get its identity token. -ReadOnlyMemory adminToken = await authenticatorProxy.AuthenticateAsync("admin", "password"); +ReadOnlyMemory adminToken = await authenticatorProxy.AuthenticateAsync("admin", "admin-password"); // Create a greeting admin proxy that uses a pipe line to insert the "admin" token into a request field. Pipeline adminPipeline = new Pipeline().UseAuthentication(adminToken).Into(connection); diff --git a/examples/Authorization/Server/IdentityFeature.cs b/examples/Authorization/Server/IdentityFeature.cs index 8d8d57b023..9152b742b5 100644 --- a/examples/Authorization/Server/IdentityFeature.cs +++ b/examples/Authorization/Server/IdentityFeature.cs @@ -6,10 +6,10 @@ namespace AuthorizationExample; /// field. internal interface IIdentityFeature { - /// true if the authenticated client has administrative privilege, false otherwise. + /// true if the authenticated user has administrative privilege, false otherwise. bool IsAdmin { get; } - /// The name of the authenticated client. + /// The name of the authenticated user. string Name { get; } } From 63b14292241bb73732b24d794c5bdd3ef21a2cbe Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Wed, 29 Mar 2023 11:55:27 +0200 Subject: [PATCH 15/26] Another doc fix --- examples/Authorization/Authorization.slice | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index 7b418b6227..f38f1d8318 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -14,11 +14,11 @@ struct IdentityToken { /// The encrypted identity token. typealias EncryptedIdentityToken = sequence -/// Represents a service to authenticate a user and return an identity token. +/// Represents a service that authenticates a user and return an identity token. interface Authenticator { /// Authenticates a user. /// @param name: The user name. - /// @param password: The password. + /// @param password: The user password. /// @returns: The encrypted identity token. authenticate(name: string, password: string) -> EncryptedIdentityToken } From 051caadd4704baf27566fe50b6a403eac8bf6930 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Wed, 29 Mar 2023 17:51:46 +0200 Subject: [PATCH 16/26] Review fixes --- examples/Authorization/Server/AuthorizationMiddleware.cs | 4 ++-- examples/Authorization/Server/IdentityFeature.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/Authorization/Server/AuthorizationMiddleware.cs b/examples/Authorization/Server/AuthorizationMiddleware.cs index 951be389d6..a10e49d2df 100644 --- a/examples/Authorization/Server/AuthorizationMiddleware.cs +++ b/examples/Authorization/Server/AuthorizationMiddleware.cs @@ -13,8 +13,8 @@ internal class AuthorizationMiddleware : IDispatcher public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { - if (request.Features.Get() is IIdentityFeature authenticationFeature && - _authorizeFunc(authenticationFeature)) + if (request.Features.Get() is IIdentityFeature identityFeature && + _authorizeFunc(identityFeature)) { return _next.DispatchAsync(request, cancellationToken); } diff --git a/examples/Authorization/Server/IdentityFeature.cs b/examples/Authorization/Server/IdentityFeature.cs index 9152b742b5..b5d7036eec 100644 --- a/examples/Authorization/Server/IdentityFeature.cs +++ b/examples/Authorization/Server/IdentityFeature.cs @@ -4,7 +4,7 @@ namespace AuthorizationExample; /// A feature that provides the name and administrative privilege decoded from an identity token /// field. -internal interface IIdentityFeature +public interface IIdentityFeature { /// true if the authenticated user has administrative privilege, false otherwise. bool IsAdmin { get; } From c2a68b20dba2a6e53135952069576d4e14edb479 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Fri, 31 Mar 2023 10:17:05 +0200 Subject: [PATCH 17/26] Fix --- examples/Authorization/README.md | 2 +- .../Authorization/Server/Authenticator.cs | 24 +++++++++++++++---- examples/Authorization/Server/Program.cs | 9 ++++++- examples/Authorization/Server/Server.csproj | 1 + 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/examples/Authorization/README.md b/examples/Authorization/README.md index 7238bb4a95..54729c6299 100644 --- a/examples/Authorization/README.md +++ b/examples/Authorization/README.md @@ -1,6 +1,6 @@ # Authorization -This demo demonstrates how token based authorization and authentication can be implemented with an interceptor and two +This demo demonstrates how token based authorization and authentication can be implemented with an interceptor and two middleware. The token provides identify information and is encrypted with a symmetric encryption algorithm (AES). An application would typically use a 3rd-party token based authentication library instead. diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs index 183dabfdbe..b4b3ba01fc 100644 --- a/examples/Authorization/Server/Authenticator.cs +++ b/examples/Authorization/Server/Authenticator.cs @@ -3,6 +3,9 @@ using IceRpc; using IceRpc.Features; using IceRpc.Slice; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; using System.Security.Cryptography; namespace AuthorizationExample; @@ -10,7 +13,7 @@ namespace AuthorizationExample; /// An Authenticator is an IceRPC service that implements the Slice interface 'Authenticator'. internal class Authenticator : Service, IAuthenticatorService { - private readonly SymmetricAlgorithm _encryptionAlgorithm; + private readonly SigningCredentials _signingCredentials; public ValueTask> AuthenticateAsync( string name, @@ -18,6 +21,19 @@ public ValueTask> AuthenticateAsync( IFeatureCollection features, CancellationToken cancellationToken) { + var jwtToken = new JwtSecurityToken( + claims: new Claim[] + { + new Claim(JwtRegisteredClaimNames.Sub, "test"), + new Claim("isAdmin", (name == "admin").ToString()) + }, + audience: "Authorization example", + issuer: "icerpc://127.0.0.1", + notBefore: DateTime.UtcNow, + expires: DateTime.UtcNow + TimeSpan.FromSeconds(30), + signingCredentials: _signingCredentials); + jwtToken.RawData + // Check if the user name and password are valid. IdentityToken identityToken; if (name == "admin" && password == "admin-password") @@ -34,11 +50,11 @@ public ValueTask> AuthenticateAsync( } // Return the encrypted identity token. - return new(identityToken.Encrypt(_encryptionAlgorithm)); + return new(identityToken.Encrypt(_signingCredentials)); } /// Constructs an authenticator service. - /// The encryption algorithm used to encrypt an identity token. - internal Authenticator(SymmetricAlgorithm encryptionAlgorithm) => _encryptionAlgorithm = encryptionAlgorithm; + /// The credentials used to generate the identity token. + internal Authenticator(SigningCredentials signingCredentials) => _signingCredentials = signingCredentials; } diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index 979dd41722..bc5d2aa386 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -2,12 +2,19 @@ using AuthorizationExample; using IceRpc; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; +using System.Text; // The identity token is encrypted and decrypted with the AES symmetric encryption algorithm. using var aes = Aes.Create(); aes.Padding = PaddingMode.Zeros; +var tokenHandler = new JwtSecurityTokenHandler(); +var issuerKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("A dummy secret key for Jwt test")); +var credentials = new SigningCredentials(issuerKey, SecurityAlgorithms.HmacSha256); + var router = new Router(); // Install a middleware to decrypt and decode the request's identity token and add an identity feature to the request's @@ -17,7 +24,7 @@ var chatbot = new Chatbot(); router.Map("/greeting", chatbot); -router.Map("/authenticator", new Authenticator(aes)); +router.Map("/authenticator", new Authenticator(credentials)); router.Route("/greetingAdmin", adminRouter => { diff --git a/examples/Authorization/Server/Server.csproj b/examples/Authorization/Server/Server.csproj index abd8deea94..3a5f8a2c71 100644 --- a/examples/Authorization/Server/Server.csproj +++ b/examples/Authorization/Server/Server.csproj @@ -4,6 +4,7 @@ + From 5967f76da9e4e1a478d9908f3b17900864549b9e Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Fri, 31 Mar 2023 13:34:06 +0200 Subject: [PATCH 18/26] Added JWT support and also kept AES --- examples/Authorization/Authorization.slice | 19 +--- examples/Authorization/Client/Program.cs | 14 +-- .../Server/AesAuthenticationBearer.cs | 57 ++++++++++++ .../Server/AesIdentityToken.slice | 12 +++ .../Server/AuthenticationBearer.cs | 22 +++++ .../Server/AuthenticationMiddleware.cs | 20 ++-- .../Authorization/Server/Authenticator.cs | 31 ++----- examples/Authorization/Server/Chatbot.cs | 6 +- examples/Authorization/Server/ChatbotAdmin.cs | 2 +- .../Authorization/Server/IdentityFeature.cs | 6 +- .../Server/IdentityTokenExtensions.cs | 52 ----------- .../Server/JwtAuthenticationBearer.cs | 91 +++++++++++++++++++ examples/Authorization/Server/Program.cs | 18 +--- .../Authorization/Server/RouterExtensions.cs | 7 +- examples/Authorization/Server/Server.csproj | 1 + 15 files changed, 227 insertions(+), 131 deletions(-) create mode 100644 examples/Authorization/Server/AesAuthenticationBearer.cs create mode 100644 examples/Authorization/Server/AesIdentityToken.slice create mode 100644 examples/Authorization/Server/AuthenticationBearer.cs delete mode 100644 examples/Authorization/Server/IdentityTokenExtensions.cs create mode 100644 examples/Authorization/Server/JwtAuthenticationBearer.cs diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authorization.slice index f38f1d8318..5e6082262d 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authorization.slice @@ -2,15 +2,6 @@ module AuthorizationExample -/// An identity token. -struct IdentityToken { - /// true if the authenticated user has administrative privilege, false otherwise. - isAdmin: bool - - /// The user name. - name: string -} - /// The encrypted identity token. typealias EncryptedIdentityToken = sequence @@ -23,15 +14,15 @@ interface Authenticator { authenticate(name: string, password: string) -> EncryptedIdentityToken } -/// Represents a recipient of greetings. -interface Greeting { +/// A service to get a personalized greeting message. +interface Greeter { /// Creates a personalized "hello" greeting. /// @returns: The greeting. - getGreeting() -> string + greet() -> string } -/// Represents a service to configure the greeting of the greeting service. -interface GreetingAdmin { +/// Represents a service to configure the greeting of the greeter service. +interface GreeterAdmin { /// Changes the greeting returned by the Greeting service. /// @param greeting: The new greeting. changeGreeting(greeting: string) diff --git a/examples/Authorization/Client/Program.cs b/examples/Authorization/Client/Program.cs index bbef568a88..25bc666823 100644 --- a/examples/Authorization/Client/Program.cs +++ b/examples/Authorization/Client/Program.cs @@ -11,21 +11,21 @@ ReadOnlyMemory friendToken = await authenticatorProxy.AuthenticateAsync("friend", "password"); // A greeting proxy that doesn't use any identity token. -var unauthenticatedGreetingProxy = new GreetingProxy(connection, new Uri("icerpc:/greeting")); +var unauthenticatedGreetingProxy = new GreeterProxy(connection, new Uri("icerpc:/greeting")); // The SayGreeting invocation on the unauthenticated greeting proxy prints a generic message. -Console.WriteLine(await unauthenticatedGreetingProxy.GetGreetingAsync()); +Console.WriteLine(await unauthenticatedGreetingProxy.GreetAsync()); // Create a greeting proxy that uses a pipe line to insert the "friend" token into a request field. Pipeline friendPipeline = new Pipeline().UseAuthentication(friendToken).Into(connection); -var friendGreetingProxy = new GreetingProxy(friendPipeline, new Uri("icerpc:/greeting")); +var friendGreetingProxy = new GreeterProxy(friendPipeline, new Uri("icerpc:/greeting")); // The SayGreeting invocation on the authenticated "friend" greeting proxy prints a custom message for "friend". -Console.WriteLine(await friendGreetingProxy.GetGreetingAsync()); +Console.WriteLine(await friendGreetingProxy.GreetAsync()); // A greeting admin proxy that uses the "friend" pipeline is not authorized to change the greeting message because "friend" // doesn't have administrative privileges. -var greetingAdminProxy = new GreetingAdminProxy(friendPipeline, new Uri("icerpc:/greetingAdmin")); +var greetingAdminProxy = new GreeterAdminProxy(friendPipeline, new Uri("icerpc:/greetingAdmin")); try { await greetingAdminProxy.ChangeGreetingAsync("Bonjour"); @@ -40,13 +40,13 @@ // Create a greeting admin proxy that uses a pipe line to insert the "admin" token into a request field. Pipeline adminPipeline = new Pipeline().UseAuthentication(adminToken).Into(connection); -greetingAdminProxy = new GreetingAdminProxy(adminPipeline, new Uri("icerpc:/greetingAdmin")); +greetingAdminProxy = new GreeterAdminProxy(adminPipeline, new Uri("icerpc:/greetingAdmin")); // Changing the greeting message should succeed this time because the "admin" user has administrative privilege. await greetingAdminProxy.ChangeGreetingAsync("Bonjour"); // The SayGreeting invocation should print a greeting with the updated greeting message. -Console.WriteLine(await friendGreetingProxy.GetGreetingAsync()); +Console.WriteLine(await friendGreetingProxy.GreetAsync()); // Change back the greeting message to Greeting. await greetingAdminProxy.ChangeGreetingAsync("Greeting"); diff --git a/examples/Authorization/Server/AesAuthenticationBearer.cs b/examples/Authorization/Server/AesAuthenticationBearer.cs new file mode 100644 index 0000000000..6a59bcb8f9 --- /dev/null +++ b/examples/Authorization/Server/AesAuthenticationBearer.cs @@ -0,0 +1,57 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc.Slice; +using System.Buffers; +using System.IO.Pipelines; +using System.Security.Cryptography; + +namespace AuthorizationExample; + +public sealed class AesAuthenticationBearer : IAuthenticationBearer, IDisposable +{ + private readonly Aes _aes; + + public async Task DecodeAndValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes) + { + // Decrypt the Slice2 encoded token. + using var sourceStream = new MemoryStream(identityTokenBytes.ToArray()); + using var cryptoStream = new CryptoStream(sourceStream, _aes.CreateDecryptor(), CryptoStreamMode.Read); + using var destinationStream = new MemoryStream(); + await cryptoStream.CopyToAsync(destinationStream); + + // Decode the Slice2 encoded token and return the feature. + AesIdentityToken identityToken = DecodeIdentityToken(destinationStream.ToArray()); + return new IdentityFeature(identityToken.Name, identityToken.IsAdmin); + + AesIdentityToken DecodeIdentityToken(ReadOnlyMemory buffer) + { + var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); + return new AesIdentityToken(ref decoder); + } + } + + public void Dispose() => _aes.Dispose(); + + public ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin) + { + // Encode the token with the Slice2 encoding. + using var tokenStream = new MemoryStream(); + var writer = PipeWriter.Create(tokenStream, new StreamPipeWriterOptions(leaveOpen: true)); + var encoder = new SliceEncoder(writer, SliceEncoding.Slice2); + new AesIdentityToken(isAdmin, name).Encode(ref encoder); + writer.Complete(); + tokenStream.Seek(0, SeekOrigin.Begin); + + // Crypt and return the Slice2 encoded token. + using var destinationStream = new MemoryStream(); + using var cryptoStream = new CryptoStream(tokenStream, _aes.CreateEncryptor(), CryptoStreamMode.Read); + cryptoStream.CopyTo(destinationStream); + return destinationStream.ToArray(); + } + + internal AesAuthenticationBearer() + { + _aes = Aes.Create(); + _aes.Padding = PaddingMode.Zeros; + } +} diff --git a/examples/Authorization/Server/AesIdentityToken.slice b/examples/Authorization/Server/AesIdentityToken.slice new file mode 100644 index 0000000000..a2b551dcda --- /dev/null +++ b/examples/Authorization/Server/AesIdentityToken.slice @@ -0,0 +1,12 @@ +// Copyright (c) ZeroC, Inc. + +module AuthorizationExample + +/// An identity token. +struct AESIdentityToken { + /// true if the authenticated user has administrative privilege, false otherwise. + isAdmin: bool + + /// The user name. + name: string +} diff --git a/examples/Authorization/Server/AuthenticationBearer.cs b/examples/Authorization/Server/AuthenticationBearer.cs new file mode 100644 index 0000000000..adb0a9e6a1 --- /dev/null +++ b/examples/Authorization/Server/AuthenticationBearer.cs @@ -0,0 +1,22 @@ +// Copyright (c) ZeroC, Inc. + +using System.Buffers; + +namespace AuthorizationExample; + +/// An simple authentication bearer interface to encode, decode and validate an the identity token. The +/// identity token is attached as an IceRPC field to requests. +public interface IAuthenticationBearer +{ + /// Decodes and validates an binary identity token. + /// The binary identity token. + /// A task that provides the decoded identity token as an identity feature. + /// Thrown is the decoding or the validation failed. + Task DecodeAndValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes); + + /// Encodes the fields of an identity token. + /// The user name + /// true if the user has administrative privilege, false otherwise. + /// The binary identity token. + ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin); +} diff --git a/examples/Authorization/Server/AuthenticationMiddleware.cs b/examples/Authorization/Server/AuthenticationMiddleware.cs index 183a5719cb..cbd2d9102a 100644 --- a/examples/Authorization/Server/AuthenticationMiddleware.cs +++ b/examples/Authorization/Server/AuthenticationMiddleware.cs @@ -7,29 +7,29 @@ namespace AuthorizationExample; -/// A middleware that decodes and decrypts an identity token request field and adds an identity feature to the -/// request's feature collection. +/// A middleware that decodes an identity token request field and adds an identity feature to the request's +/// feature collection. internal class AuthenticationMiddleware : IDispatcher { - private readonly SymmetricAlgorithm _encryptionAlgorithm; + private readonly IAuthenticationBearer _authenticationBearer; private readonly IDispatcher _next; - public ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) + public async ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { if (request.Fields.TryGetValue(IdentityTokenFieldKey.Value, out ReadOnlySequence buffer)) { - var token = buffer.DecryptIdentityToken(_encryptionAlgorithm); - request.Features = request.Features.With(new IdentityFeature(token)); + IIdentityFeature identityFeature = await _authenticationBearer.DecodeAndValidateIdentityTokenAsync(buffer); + request.Features = request.Features.With(identityFeature); } - return _next.DispatchAsync(request, cancellationToken); + return await _next.DispatchAsync(request, cancellationToken); } /// Constructs an authentication middleware. /// The invoker to call next. - /// The encryption algorithm used to encrypt an identity token. - internal AuthenticationMiddleware(IDispatcher next, SymmetricAlgorithm encryptionAlgorithm) + /// The authentication bearer to decode the identity token. + internal AuthenticationMiddleware(IDispatcher next, IAuthenticationBearer authenticationBearer) { _next = next; - _encryptionAlgorithm = encryptionAlgorithm; + _authenticationBearer = authenticationBearer; } } diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs index b4b3ba01fc..7ba8c6f1f5 100644 --- a/examples/Authorization/Server/Authenticator.cs +++ b/examples/Authorization/Server/Authenticator.cs @@ -3,17 +3,13 @@ using IceRpc; using IceRpc.Features; using IceRpc.Slice; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Security.Cryptography; namespace AuthorizationExample; /// An Authenticator is an IceRPC service that implements the Slice interface 'Authenticator'. internal class Authenticator : Service, IAuthenticatorService { - private readonly SigningCredentials _signingCredentials; + private readonly IAuthenticationBearer _authenticationBearer; public ValueTask> AuthenticateAsync( string name, @@ -21,28 +17,15 @@ public ValueTask> AuthenticateAsync( IFeatureCollection features, CancellationToken cancellationToken) { - var jwtToken = new JwtSecurityToken( - claims: new Claim[] - { - new Claim(JwtRegisteredClaimNames.Sub, "test"), - new Claim("isAdmin", (name == "admin").ToString()) - }, - audience: "Authorization example", - issuer: "icerpc://127.0.0.1", - notBefore: DateTime.UtcNow, - expires: DateTime.UtcNow + TimeSpan.FromSeconds(30), - signingCredentials: _signingCredentials); - jwtToken.RawData - // Check if the user name and password are valid. - IdentityToken identityToken; + bool isAdmin; if (name == "admin" && password == "admin-password") { - identityToken = new IdentityToken(isAdmin: true, name); + isAdmin = true; } else if (name == "friend" && password == "password") { - identityToken = new IdentityToken(isAdmin: false, name); + isAdmin = false; } else { @@ -50,11 +33,11 @@ public ValueTask> AuthenticateAsync( } // Return the encrypted identity token. - return new(identityToken.Encrypt(_signingCredentials)); + return new(_authenticationBearer.EncodeIdentityToken(name, isAdmin)); } /// Constructs an authenticator service. - /// The credentials used to generate the identity token. - internal Authenticator(SigningCredentials signingCredentials) => _signingCredentials = signingCredentials; + /// The authentication bearer to encode an identity token. + internal Authenticator(IAuthenticationBearer authenticationBearer) => _authenticationBearer = authenticationBearer; } diff --git a/examples/Authorization/Server/Chatbot.cs b/examples/Authorization/Server/Chatbot.cs index c676a89d06..937745833d 100644 --- a/examples/Authorization/Server/Chatbot.cs +++ b/examples/Authorization/Server/Chatbot.cs @@ -6,11 +6,11 @@ namespace AuthorizationExample; /// A Chatbot is an IceRPC service that implements the Slice interface 'Greeting'. -internal class Chatbot : Service, IGreetingService +internal class Chatbot : Service, IGreeterService { internal string Greeting { get; set; } = "Hello"; - public ValueTask GetGreetingAsync(IFeatureCollection features, CancellationToken cancellationToken) + public ValueTask GreetAsync(IFeatureCollection features, CancellationToken cancellationToken) { string name; bool isAdmin; @@ -25,7 +25,7 @@ public ValueTask GetGreetingAsync(IFeatureCollection features, Cancellat isAdmin = false; } - Console.WriteLine($"Dispatching GetGreeting request {{ name = '{name}' isAdmin = {isAdmin} }}"); + Console.WriteLine($"Dispatching Greet request {{ name = '{name}' isAdmin = {isAdmin} }}"); return new($"{Greeting}, {name}!"); } } diff --git a/examples/Authorization/Server/ChatbotAdmin.cs b/examples/Authorization/Server/ChatbotAdmin.cs index 40566fc450..367feaee3a 100644 --- a/examples/Authorization/Server/ChatbotAdmin.cs +++ b/examples/Authorization/Server/ChatbotAdmin.cs @@ -6,7 +6,7 @@ namespace AuthorizationExample; /// A ChatbotAdmin is an IceRPC service that implements the Slice interface 'GreetingAdmin'. -internal class ChatbotAdmin : Service, IGreetingAdminService +internal class ChatbotAdmin : Service, IGreeterAdminService { private readonly Chatbot _chatbot; diff --git a/examples/Authorization/Server/IdentityFeature.cs b/examples/Authorization/Server/IdentityFeature.cs index b5d7036eec..ca9d88885d 100644 --- a/examples/Authorization/Server/IdentityFeature.cs +++ b/examples/Authorization/Server/IdentityFeature.cs @@ -23,9 +23,9 @@ internal class IdentityFeature : IIdentityFeature public string Name { get; } /// Constructs an identity feature from an identity token. - internal IdentityFeature(IdentityToken token) + internal IdentityFeature(string name, bool isAdmin) { - Name = token.Name; - IsAdmin = token.IsAdmin; + Name = name; + IsAdmin = isAdmin; } } diff --git a/examples/Authorization/Server/IdentityTokenExtensions.cs b/examples/Authorization/Server/IdentityTokenExtensions.cs deleted file mode 100644 index 49b5a4b7a4..0000000000 --- a/examples/Authorization/Server/IdentityTokenExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using IceRpc.Slice; -using System.Buffers; -using System.IO.Pipelines; -using System.Security.Cryptography; - -namespace AuthorizationExample; - -/// Extension methods to encrypt and decrypt an identity token. -public static class IdentityTokenExtensions -{ - /// Decrypts an identity token. - /// The byte buffer. - /// The symmetric algorithm used to decrypt an identity token. - /// The identity token. - internal static IdentityToken DecryptIdentityToken( - this ReadOnlySequence buffer, - SymmetricAlgorithm algorithm) - { - // Decrypt the Slice2 encoded token. - using var sourceStream = new MemoryStream(buffer.ToArray()); - using var cryptoStream = new CryptoStream(sourceStream, algorithm.CreateDecryptor(), CryptoStreamMode.Read); - using var destinationStream = new MemoryStream(); - cryptoStream.CopyTo(destinationStream); - - // Decode the Slice2 encoded token. - var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2); - return new IdentityToken(ref decoder); - } - - /// Encrypts an identity token. - /// The identity token. - /// The symmetric algorithm used to encrypt this identity token. - /// The encrypted identity token. - internal static ReadOnlyMemory Encrypt(this IdentityToken token, SymmetricAlgorithm algorithm) - { - // Encode the token with the Slice2 encoding. - using var tokenStream = new MemoryStream(); - var writer = PipeWriter.Create(tokenStream, new StreamPipeWriterOptions(leaveOpen: true)); - var encoder = new SliceEncoder(writer, SliceEncoding.Slice2); - token.Encode(ref encoder); - writer.Complete(); - tokenStream.Seek(0, SeekOrigin.Begin); - - // Crypt and return the Slice2 encoded token. - using var destinationStream = new MemoryStream(); - using var cryptoStream = new CryptoStream(tokenStream, algorithm.CreateEncryptor(), CryptoStreamMode.Read); - cryptoStream.CopyTo(destinationStream); - return destinationStream.ToArray(); - } -} diff --git a/examples/Authorization/Server/JwtAuthenticationBearer.cs b/examples/Authorization/Server/JwtAuthenticationBearer.cs new file mode 100644 index 0000000000..5d49b8b921 --- /dev/null +++ b/examples/Authorization/Server/JwtAuthenticationBearer.cs @@ -0,0 +1,91 @@ +// Copyright (c) ZeroC, Inc. + +using IceRpc; +using Microsoft.IdentityModel.Tokens; +using System.Buffers; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace AuthorizationExample; + +public sealed class JwtAuthenticationBearer : IAuthenticationBearer +{ + private readonly SigningCredentials _signingCredentials; + private readonly JwtSecurityTokenHandler _tokenHandler; + private readonly TokenValidationParameters _tokenValidationParameters; + + public async Task DecodeAndValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes) + { + string jwtTokenString = Encoding.UTF8.GetString(identityTokenBytes.ToArray()); + TokenValidationResult validationResult = await _tokenHandler.ValidateTokenAsync( + jwtTokenString, + _tokenValidationParameters); + if (validationResult.IsValid) + { + var jwtToken = (JwtSecurityToken)validationResult.SecurityToken; + + string name; + IEnumerable subjectClaim = jwtToken.Claims.Where(claim => claim.Type == JwtRegisteredClaimNames.Sub); + if (!subjectClaim.Any()) + { + throw new DispatchException(StatusCode.Unauthorized, "The JWT token is missing the subject claim."); + } + name = subjectClaim.First().Value; + + bool isAdmin = false; + IEnumerable isAdminClaim = jwtToken.Claims.Where(claim => claim.Type == "isAdmin"); + if (!isAdminClaim.Any()) + { + throw new DispatchException(StatusCode.Unauthorized, "The JWT token is missing the isAdmin claim."); + } + else if (!bool.TryParse(isAdminClaim.First().Value, out isAdmin)) + { + throw new DispatchException(StatusCode.Unauthorized, "The JWT token isAdmin claim is invalid."); + + } + + return new IdentityFeature(name, isAdmin); + } + else + { + throw new DispatchException(StatusCode.Unauthorized, "Invalid JWT token.", validationResult.Exception); + } + } + + public ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin) + { + var jwtToken = new JwtSecurityToken( + claims: new Claim[] + { + new Claim(JwtRegisteredClaimNames.Sub, "test"), + new Claim("isAdmin", (name == "admin").ToString()) + }, + audience: "Authorization example", + issuer: "icerpc://127.0.0.1", + notBefore: DateTime.UtcNow, + expires: DateTime.UtcNow + TimeSpan.FromSeconds(30), + signingCredentials: _signingCredentials); + return Encoding.UTF8.GetBytes(_tokenHandler.WriteToken(jwtToken)); + } + + internal JwtAuthenticationBearer(string secretKey) + { + _tokenHandler = new JwtSecurityTokenHandler(); + var issuerKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); + _signingCredentials = new SigningCredentials(issuerKey, SecurityAlgorithms.HmacSha256); + + _tokenValidationParameters = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + ValidateAudience = true, + ValidateIssuer = true, + ValidateLifetime = true, + RequireSignedTokens = true, + ValidAudience = "Authorization example", + ValidIssuer = "icerpc://127.0.0.1", + IssuerSigningKey = issuerKey, + ClockSkew = TimeSpan.FromSeconds(5), + }; + } +} diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index bc5d2aa386..a3ee1a9ec7 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -2,29 +2,21 @@ using AuthorizationExample; using IceRpc; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Cryptography; -using System.Text; -// The identity token is encrypted and decrypted with the AES symmetric encryption algorithm. -using var aes = Aes.Create(); -aes.Padding = PaddingMode.Zeros; - -var tokenHandler = new JwtSecurityTokenHandler(); -var issuerKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("A dummy secret key for Jwt test")); -var credentials = new SigningCredentials(issuerKey, SecurityAlgorithms.HmacSha256); +// Encodes the identity token field carried by requests as a JTW token. +//var authenticationBearer = new JwtAuthenticationBearer("A secret key for the authorization example"); +using var authenticationBearer = new AesAuthenticationBearer(); var router = new Router(); // Install a middleware to decrypt and decode the request's identity token and add an identity feature to the request's // feature collection. -router.UseAuthentication(aes); +router.UseAuthentication(authenticationBearer); var chatbot = new Chatbot(); router.Map("/greeting", chatbot); -router.Map("/authenticator", new Authenticator(credentials)); +router.Map("/authenticator", new Authenticator(authenticationBearer)); router.Route("/greetingAdmin", adminRouter => { diff --git a/examples/Authorization/Server/RouterExtensions.cs b/examples/Authorization/Server/RouterExtensions.cs index f670122f80..6b930e8246 100644 --- a/examples/Authorization/Server/RouterExtensions.cs +++ b/examples/Authorization/Server/RouterExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) ZeroC, Inc. using AuthorizationExample; -using System.Security.Cryptography; namespace IceRpc; @@ -11,10 +10,10 @@ internal static class RouterExtensions { /// Adds an to the router. /// The router being configured. - /// The encryption algorithm used to encrypt the identity token. + /// The authentication bearer to decode the identity token. /// The router being configured. - internal static Router UseAuthentication(this Router router, SymmetricAlgorithm encryptionAlgorithm) => - router.Use(next => new AuthenticationMiddleware(next, encryptionAlgorithm)); + internal static Router UseAuthentication(this Router router, IAuthenticationBearer authenticationBearer) => + router.Use(next => new AuthenticationMiddleware(next, authenticationBearer)); /// Adds an to the router. /// The router being configured. diff --git a/examples/Authorization/Server/Server.csproj b/examples/Authorization/Server/Server.csproj index 3a5f8a2c71..fc9a33025f 100644 --- a/examples/Authorization/Server/Server.csproj +++ b/examples/Authorization/Server/Server.csproj @@ -2,6 +2,7 @@ + From 365c034bed762d2e021fca1bc905e51456c4c736 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Fri, 31 Mar 2023 13:34:28 +0200 Subject: [PATCH 19/26] Missing file --- examples/Authorization/AesIdentityToken.slice | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 examples/Authorization/AesIdentityToken.slice diff --git a/examples/Authorization/AesIdentityToken.slice b/examples/Authorization/AesIdentityToken.slice new file mode 100644 index 0000000000..a2b551dcda --- /dev/null +++ b/examples/Authorization/AesIdentityToken.slice @@ -0,0 +1,12 @@ +// Copyright (c) ZeroC, Inc. + +module AuthorizationExample + +/// An identity token. +struct AESIdentityToken { + /// true if the authenticated user has administrative privilege, false otherwise. + isAdmin: bool + + /// The user name. + name: string +} From bf81d1d8fbe525490547f442da382eb6a5ed3587 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Fri, 31 Mar 2023 19:11:54 +0200 Subject: [PATCH 20/26] Review fixes --- examples/Authorization/Client/Program.cs | 6 ++--- .../Authorization/IdentityTokenFieldKey.cs | 3 +-- examples/Authorization/README.md | 19 ++++++++++---- .../Server/AesAuthenticationBearer.cs | 7 +++++- .../Server/AuthenticationBearer.cs | 22 ---------------- .../Authorization/Server/IdentityFeature.cs | 3 +-- .../Server/JwtAuthenticationBearer.cs | 25 +++++++++++-------- examples/Authorization/Server/Program.cs | 15 ++++++++--- 8 files changed, 51 insertions(+), 49 deletions(-) delete mode 100644 examples/Authorization/Server/AuthenticationBearer.cs diff --git a/examples/Authorization/Client/Program.cs b/examples/Authorization/Client/Program.cs index 25bc666823..e603ba025e 100644 --- a/examples/Authorization/Client/Program.cs +++ b/examples/Authorization/Client/Program.cs @@ -13,14 +13,14 @@ // A greeting proxy that doesn't use any identity token. var unauthenticatedGreetingProxy = new GreeterProxy(connection, new Uri("icerpc:/greeting")); -// The SayGreeting invocation on the unauthenticated greeting proxy prints a generic message. +// The Greet invocation on the unauthenticated greeting proxy prints a generic message. Console.WriteLine(await unauthenticatedGreetingProxy.GreetAsync()); // Create a greeting proxy that uses a pipe line to insert the "friend" token into a request field. Pipeline friendPipeline = new Pipeline().UseAuthentication(friendToken).Into(connection); var friendGreetingProxy = new GreeterProxy(friendPipeline, new Uri("icerpc:/greeting")); -// The SayGreeting invocation on the authenticated "friend" greeting proxy prints a custom message for "friend". +// The Greet invocation on the authenticated "friend" greeting proxy prints a custom message for "friend". Console.WriteLine(await friendGreetingProxy.GreetAsync()); // A greeting admin proxy that uses the "friend" pipeline is not authorized to change the greeting message because "friend" @@ -45,7 +45,7 @@ // Changing the greeting message should succeed this time because the "admin" user has administrative privilege. await greetingAdminProxy.ChangeGreetingAsync("Bonjour"); -// The SayGreeting invocation should print a greeting with the updated greeting message. +// The Greet invocation should print a greeting with the updated greeting message. Console.WriteLine(await friendGreetingProxy.GreetAsync()); // Change back the greeting message to Greeting. diff --git a/examples/Authorization/IdentityTokenFieldKey.cs b/examples/Authorization/IdentityTokenFieldKey.cs index a21e508ac6..9ef41d8102 100644 --- a/examples/Authorization/IdentityTokenFieldKey.cs +++ b/examples/Authorization/IdentityTokenFieldKey.cs @@ -4,8 +4,7 @@ namespace AuthorizationExample; -/// The shared used by the client and server to carry the identity -/// token. +/// The shared used to carry the identity token in a request's field. public static class IdentityTokenFieldKey { public const RequestFieldKey Value = (RequestFieldKey)100; diff --git a/examples/Authorization/README.md b/examples/Authorization/README.md index 54729c6299..e613200366 100644 --- a/examples/Authorization/README.md +++ b/examples/Authorization/README.md @@ -1,8 +1,7 @@ # Authorization This demo demonstrates how token based authorization and authentication can be implemented with an interceptor and two -middleware. The token provides identify information and is encrypted with a symmetric encryption algorithm (AES). An -application would typically use a 3rd-party token based authentication library instead. +middleware. The token provides identify information. The server is configured with two middleware: `AuthenticationMiddleware` and `AuthorizationMiddleware`. The first middleware is responsible for decrypting an identity token from the request field and storing it in a corresponding @@ -13,6 +12,10 @@ The client is configured with an `AuthenticationInterceptor` interceptor. The in encrypted identity token to a request field. The identity token is returned by an `Authenticator` service after authenticating the client with a login name and password. +The server supports two token types: +- a JWT identity token +- a custom Slice based identity token encoded with AES. + ## Running the example First start the Server: @@ -27,13 +30,19 @@ In a separate window, start the Client: dotnet run --project Client/Client.csproj ``` -The client first calls `GetGreetingAsync` without an identity token and the server responds with a generic greeting. +The client first calls `GreetAsync` without an identity token and the server responds with a generic greeting. Next, the client gets an identity token for the user `friend` and uses it to construct an authentication invocation -pipeline that adds the `friend` identity token to each request. The client then calls `GetGreetingAsync` using the -`friend` authentication pipeline and receives a personalized message. +pipeline that adds the `friend` identity token to each request. The client then calls `GreetAsync` using the `friend` +authentication pipeline and receives a personalized message. Next, the client calls `ChangeGreetingAsync` using the `friend` authentication pipeline to change the greeting. The user `friend` doesn't have administrative privilege so the invocation fails with a `DispatchException`. Finally, the client authenticates the user `admin` and calls `ChangeGreetingAsync` using an `admin` authentication pipeline. The call succeeds because the user `admin` has administrative privilege. + +To use JTW token instead of AES tokens, start the Server with the `--jwt` argument: + +```shell +dotnet run --project Server/Server.csproj --jwt +``` diff --git a/examples/Authorization/Server/AesAuthenticationBearer.cs b/examples/Authorization/Server/AesAuthenticationBearer.cs index 6a59bcb8f9..51b6f04846 100644 --- a/examples/Authorization/Server/AesAuthenticationBearer.cs +++ b/examples/Authorization/Server/AesAuthenticationBearer.cs @@ -7,7 +7,8 @@ namespace AuthorizationExample; -public sealed class AesAuthenticationBearer : IAuthenticationBearer, IDisposable +/// This is an implementation of the using an Aes token. +internal sealed class AesAuthenticationBearer : IAuthenticationBearer, IDisposable { private readonly Aes _aes; @@ -21,6 +22,10 @@ public async Task DecodeAndValidateIdentityTokenAsync(ReadOnly // Decode the Slice2 encoded token and return the feature. AesIdentityToken identityToken = DecodeIdentityToken(destinationStream.ToArray()); + + Console.WriteLine( + $"Decoded Aes identity token {{ name = '{identityToken.Name}' isAdmin = '{identityToken.IsAdmin}' }}"); + return new IdentityFeature(identityToken.Name, identityToken.IsAdmin); AesIdentityToken DecodeIdentityToken(ReadOnlyMemory buffer) diff --git a/examples/Authorization/Server/AuthenticationBearer.cs b/examples/Authorization/Server/AuthenticationBearer.cs deleted file mode 100644 index adb0a9e6a1..0000000000 --- a/examples/Authorization/Server/AuthenticationBearer.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using System.Buffers; - -namespace AuthorizationExample; - -/// An simple authentication bearer interface to encode, decode and validate an the identity token. The -/// identity token is attached as an IceRPC field to requests. -public interface IAuthenticationBearer -{ - /// Decodes and validates an binary identity token. - /// The binary identity token. - /// A task that provides the decoded identity token as an identity feature. - /// Thrown is the decoding or the validation failed. - Task DecodeAndValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes); - - /// Encodes the fields of an identity token. - /// The user name - /// true if the user has administrative privilege, false otherwise. - /// The binary identity token. - ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin); -} diff --git a/examples/Authorization/Server/IdentityFeature.cs b/examples/Authorization/Server/IdentityFeature.cs index ca9d88885d..eca78cf608 100644 --- a/examples/Authorization/Server/IdentityFeature.cs +++ b/examples/Authorization/Server/IdentityFeature.cs @@ -2,8 +2,7 @@ namespace AuthorizationExample; -/// A feature that provides the name and administrative privilege decoded from an identity token -/// field. +/// A feature that provides the name and administrative privilege of the request. public interface IIdentityFeature { /// true if the authenticated user has administrative privilege, false otherwise. diff --git a/examples/Authorization/Server/JwtAuthenticationBearer.cs b/examples/Authorization/Server/JwtAuthenticationBearer.cs index 5d49b8b921..96627d6a62 100644 --- a/examples/Authorization/Server/JwtAuthenticationBearer.cs +++ b/examples/Authorization/Server/JwtAuthenticationBearer.cs @@ -9,7 +9,8 @@ namespace AuthorizationExample; -public sealed class JwtAuthenticationBearer : IAuthenticationBearer +/// This is an implementation of the using JSON Web Token (JWT) +internal sealed class JwtAuthenticationBearer : IAuthenticationBearer { private readonly SigningCredentials _signingCredentials; private readonly JwtSecurityTokenHandler _tokenHandler; @@ -45,6 +46,8 @@ public async Task DecodeAndValidateIdentityTokenAsync(ReadOnly } + Console.WriteLine($"Decoded JWT identity token {{ name = '{name}' isAdmin = '{isAdmin}' }}"); + return new IdentityFeature(name, isAdmin); } else @@ -56,16 +59,16 @@ public async Task DecodeAndValidateIdentityTokenAsync(ReadOnly public ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin) { var jwtToken = new JwtSecurityToken( - claims: new Claim[] - { - new Claim(JwtRegisteredClaimNames.Sub, "test"), - new Claim("isAdmin", (name == "admin").ToString()) - }, - audience: "Authorization example", - issuer: "icerpc://127.0.0.1", - notBefore: DateTime.UtcNow, - expires: DateTime.UtcNow + TimeSpan.FromSeconds(30), - signingCredentials: _signingCredentials); + claims: new Claim[] + { + new Claim(JwtRegisteredClaimNames.Sub, "test"), + new Claim("isAdmin", (name == "admin").ToString()) + }, + audience: "Authorization example", + issuer: "icerpc://127.0.0.1", + notBefore: DateTime.UtcNow, + expires: DateTime.UtcNow + TimeSpan.FromSeconds(30), + signingCredentials: _signingCredentials); return Encoding.UTF8.GetBytes(_tokenHandler.WriteToken(jwtToken)); } diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index a3ee1a9ec7..239ec001dc 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -3,9 +3,18 @@ using AuthorizationExample; using IceRpc; -// Encodes the identity token field carried by requests as a JTW token. -//var authenticationBearer = new JwtAuthenticationBearer("A secret key for the authorization example"); -using var authenticationBearer = new AesAuthenticationBearer(); +IAuthenticationBearer? authenticationBearer = null; +if (args.Length == 1 && args[0] == "--jwt") +{ + authenticationBearer = new JwtAuthenticationBearer("A secret key for the authorization example"); +} +else +{ + authenticationBearer = new AesAuthenticationBearer(); +} + +// Dispose the authentication bearer if it's disposable. +using var disposable = authenticationBearer as IDisposable; var router = new Router(); From 63e99ed1400f85d47e4e45204999944d4ca27810 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Mon, 3 Apr 2023 09:46:44 +0200 Subject: [PATCH 21/26] More review fixes --- .vscode/cspell.json | 3 ++- examples/Authorization/AesIdentityToken.slice | 12 ---------- examples/Authorization/README.md | 22 +++++++++---------- .../Server/AesIdentityToken.slice | 2 +- .../Server/AuthenticationMiddleware.cs | 2 +- examples/Authorization/Server/ChatbotAdmin.cs | 4 ++-- .../Server/IAuthenticationBearer.cs | 22 +++++++++++++++++++ .../Authorization/Server/IdentityFeature.cs | 2 +- .../Server/JwtAuthenticationBearer.cs | 2 +- 9 files changed, 40 insertions(+), 31 deletions(-) delete mode 100644 examples/Authorization/AesIdentityToken.slice create mode 100644 examples/Authorization/Server/IAuthenticationBearer.cs diff --git a/.vscode/cspell.json b/.vscode/cspell.json index f018bd70ed..12fddb732b 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -18,10 +18,11 @@ "Decryptor", "docfx", "ECONNRESET", - "EPIPE", "Encryptor", + "EPIPE", "Finalizers", "globaltool", + "Hmac", "icecertutils", "icerpc", "lalrpop", diff --git a/examples/Authorization/AesIdentityToken.slice b/examples/Authorization/AesIdentityToken.slice deleted file mode 100644 index a2b551dcda..0000000000 --- a/examples/Authorization/AesIdentityToken.slice +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -module AuthorizationExample - -/// An identity token. -struct AESIdentityToken { - /// true if the authenticated user has administrative privilege, false otherwise. - isAdmin: bool - - /// The user name. - name: string -} diff --git a/examples/Authorization/README.md b/examples/Authorization/README.md index e613200366..9607b05243 100644 --- a/examples/Authorization/README.md +++ b/examples/Authorization/README.md @@ -3,18 +3,16 @@ This demo demonstrates how token based authorization and authentication can be implemented with an interceptor and two middleware. The token provides identify information. -The server is configured with two middleware: `AuthenticationMiddleware` and `AuthorizationMiddleware`. The first -middleware is responsible for decrypting an identity token from the request field and storing it in a corresponding -request feature. The second middleware is responsible for checking if the identity feature is present in the -corresponding request feature and it checks if the request is authorized. - -The client is configured with an `AuthenticationInterceptor` interceptor. The interceptor is responsible for adding the -encrypted identity token to a request field. The identity token is returned by an `Authenticator` service after -authenticating the client with a login name and password. - -The server supports two token types: -- a JWT identity token -- a custom Slice based identity token encoded with AES. +The server dispatch pipeline is configured with two middleware: +- The `AuthenticationMiddleware` is responsible for decrypting the request's identity token field and storing it using + the request's identity feature. +- The `AuthorizationMiddleware` is responsible for checking if the request's identity feature is authorized. + +The client invocation pipeline is configured with an `AuthenticationInterceptor` interceptor, which is responsible for adding a request field with the encrypted identity token. The client obtains its identity token by authenticating itself with the `Authenticator` service. + +The server supports two identity token types: +- A custom Slice based identity token encrypted with AES (the default). +- A JWT identity token. ## Running the example diff --git a/examples/Authorization/Server/AesIdentityToken.slice b/examples/Authorization/Server/AesIdentityToken.slice index a2b551dcda..3a413a7d0b 100644 --- a/examples/Authorization/Server/AesIdentityToken.slice +++ b/examples/Authorization/Server/AesIdentityToken.slice @@ -3,7 +3,7 @@ module AuthorizationExample /// An identity token. -struct AESIdentityToken { +struct AesIdentityToken { /// true if the authenticated user has administrative privilege, false otherwise. isAdmin: bool diff --git a/examples/Authorization/Server/AuthenticationMiddleware.cs b/examples/Authorization/Server/AuthenticationMiddleware.cs index cbd2d9102a..8ccb1aac12 100644 --- a/examples/Authorization/Server/AuthenticationMiddleware.cs +++ b/examples/Authorization/Server/AuthenticationMiddleware.cs @@ -25,7 +25,7 @@ public async ValueTask DispatchAsync(IncomingRequest request, } /// Constructs an authentication middleware. - /// The invoker to call next. + /// The dispatcher to call next. /// The authentication bearer to decode the identity token. internal AuthenticationMiddleware(IDispatcher next, IAuthenticationBearer authenticationBearer) { diff --git a/examples/Authorization/Server/ChatbotAdmin.cs b/examples/Authorization/Server/ChatbotAdmin.cs index 367feaee3a..942dba6538 100644 --- a/examples/Authorization/Server/ChatbotAdmin.cs +++ b/examples/Authorization/Server/ChatbotAdmin.cs @@ -10,8 +10,6 @@ internal class ChatbotAdmin : Service, IGreeterAdminService { private readonly Chatbot _chatbot; - internal ChatbotAdmin(Chatbot chatbot) => _chatbot = chatbot; - public ValueTask ChangeGreetingAsync( string greeting, IFeatureCollection features, @@ -22,4 +20,6 @@ public ValueTask ChangeGreetingAsync( _chatbot.Greeting = greeting; return default; } + + internal ChatbotAdmin(Chatbot chatbot) => _chatbot = chatbot; } diff --git a/examples/Authorization/Server/IAuthenticationBearer.cs b/examples/Authorization/Server/IAuthenticationBearer.cs new file mode 100644 index 0000000000..d07d9282e4 --- /dev/null +++ b/examples/Authorization/Server/IAuthenticationBearer.cs @@ -0,0 +1,22 @@ +// Copyright (c) ZeroC, Inc. + +using System.Buffers; + +namespace AuthorizationExample; + +/// An interface to encode, decode and validate the authentication identity token. The identity token is sent +/// in the request header using the field. +public interface IAuthenticationBearer +{ + /// Decodes and validates a binary identity token. + /// The binary identity token. + /// A task that provides the decoded identity token as an identity feature. + /// Thrown is the decoding or the validation failed. + Task DecodeAndValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes); + + /// Encodes the fields of an identity token. + /// The user name + /// true if the user has administrative privilege, false otherwise. + /// The binary identity token. + ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin); +} diff --git a/examples/Authorization/Server/IdentityFeature.cs b/examples/Authorization/Server/IdentityFeature.cs index eca78cf608..c52bb43745 100644 --- a/examples/Authorization/Server/IdentityFeature.cs +++ b/examples/Authorization/Server/IdentityFeature.cs @@ -21,7 +21,7 @@ internal class IdentityFeature : IIdentityFeature /// public string Name { get; } - /// Constructs an identity feature from an identity token. + /// Constructs an identity feature. internal IdentityFeature(string name, bool isAdmin) { Name = name; diff --git a/examples/Authorization/Server/JwtAuthenticationBearer.cs b/examples/Authorization/Server/JwtAuthenticationBearer.cs index 96627d6a62..9ca1494aa3 100644 --- a/examples/Authorization/Server/JwtAuthenticationBearer.cs +++ b/examples/Authorization/Server/JwtAuthenticationBearer.cs @@ -9,7 +9,7 @@ namespace AuthorizationExample; -/// This is an implementation of the using JSON Web Token (JWT) +/// This is an implementation of the using JSON Web Token (JWT). internal sealed class JwtAuthenticationBearer : IAuthenticationBearer { private readonly SigningCredentials _signingCredentials; From 58d545397a7ceead38d51a3465950e1ce25b28e0 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Mon, 3 Apr 2023 13:53:42 +0200 Subject: [PATCH 22/26] Renaming --- examples/Authorization/README.md | 2 +- ...r.cs => AesBearerAuthenticationHandler.cs} | 11 +++++----- .../Server/AuthenticationMiddleware.cs | 13 ++++++----- .../Authorization/Server/Authenticator.cs | 11 +++++----- .../Server/IAuthenticationBearer.cs | 22 ------------------- .../Server/IBearerAuthenticationHandler.cs | 21 ++++++++++++++++++ ...r.cs => JwtBearerAuthenticationHandler.cs} | 11 +++++----- examples/Authorization/Server/Program.cs | 18 +++++++-------- .../Authorization/Server/RouterExtensions.cs | 6 ++--- 9 files changed, 58 insertions(+), 57 deletions(-) rename examples/Authorization/Server/{AesAuthenticationBearer.cs => AesBearerAuthenticationHandler.cs} (81%) delete mode 100644 examples/Authorization/Server/IAuthenticationBearer.cs create mode 100644 examples/Authorization/Server/IBearerAuthenticationHandler.cs rename examples/Authorization/Server/{JwtAuthenticationBearer.cs => JwtBearerAuthenticationHandler.cs} (87%) diff --git a/examples/Authorization/README.md b/examples/Authorization/README.md index 9607b05243..c858a416cd 100644 --- a/examples/Authorization/README.md +++ b/examples/Authorization/README.md @@ -4,7 +4,7 @@ This demo demonstrates how token based authorization and authentication can be i middleware. The token provides identify information. The server dispatch pipeline is configured with two middleware: -- The `AuthenticationMiddleware` is responsible for decrypting the request's identity token field and storing it using +- The `AuthenticationMiddleware` is responsible for validating the request's identity token field and storing it using the request's identity feature. - The `AuthorizationMiddleware` is responsible for checking if the request's identity feature is authorized. diff --git a/examples/Authorization/Server/AesAuthenticationBearer.cs b/examples/Authorization/Server/AesBearerAuthenticationHandler.cs similarity index 81% rename from examples/Authorization/Server/AesAuthenticationBearer.cs rename to examples/Authorization/Server/AesBearerAuthenticationHandler.cs index 51b6f04846..2d83c7ab12 100644 --- a/examples/Authorization/Server/AesAuthenticationBearer.cs +++ b/examples/Authorization/Server/AesBearerAuthenticationHandler.cs @@ -7,12 +7,13 @@ namespace AuthorizationExample; -/// This is an implementation of the using an Aes token. -internal sealed class AesAuthenticationBearer : IAuthenticationBearer, IDisposable +/// This is an implementation of to create and validate a Slice +/// based identity token encrypted with Aes. +internal sealed class AesBearerAuthenticationHandler : IBearerAuthenticationHandler, IDisposable { private readonly Aes _aes; - public async Task DecodeAndValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes) + public async Task ValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes) { // Decrypt the Slice2 encoded token. using var sourceStream = new MemoryStream(identityTokenBytes.ToArray()); @@ -37,7 +38,7 @@ AesIdentityToken DecodeIdentityToken(ReadOnlyMemory buffer) public void Dispose() => _aes.Dispose(); - public ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin) + public ReadOnlyMemory CreateIdentityToken(string name, bool isAdmin) { // Encode the token with the Slice2 encoding. using var tokenStream = new MemoryStream(); @@ -54,7 +55,7 @@ public ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin) return destinationStream.ToArray(); } - internal AesAuthenticationBearer() + internal AesBearerAuthenticationHandler() { _aes = Aes.Create(); _aes.Padding = PaddingMode.Zeros; diff --git a/examples/Authorization/Server/AuthenticationMiddleware.cs b/examples/Authorization/Server/AuthenticationMiddleware.cs index 8ccb1aac12..7affffd134 100644 --- a/examples/Authorization/Server/AuthenticationMiddleware.cs +++ b/examples/Authorization/Server/AuthenticationMiddleware.cs @@ -7,18 +7,18 @@ namespace AuthorizationExample; -/// A middleware that decodes an identity token request field and adds an identity feature to the request's +/// A middleware that validates an identity token request field and adds an identity feature to the request's /// feature collection. internal class AuthenticationMiddleware : IDispatcher { - private readonly IAuthenticationBearer _authenticationBearer; + private readonly IBearerAuthenticationHandler _bearerAuthenticationHandler; private readonly IDispatcher _next; public async ValueTask DispatchAsync(IncomingRequest request, CancellationToken cancellationToken) { if (request.Fields.TryGetValue(IdentityTokenFieldKey.Value, out ReadOnlySequence buffer)) { - IIdentityFeature identityFeature = await _authenticationBearer.DecodeAndValidateIdentityTokenAsync(buffer); + IIdentityFeature identityFeature = await _bearerAuthenticationHandler.ValidateIdentityTokenAsync(buffer); request.Features = request.Features.With(identityFeature); } return await _next.DispatchAsync(request, cancellationToken); @@ -26,10 +26,11 @@ public async ValueTask DispatchAsync(IncomingRequest request, /// Constructs an authentication middleware. /// The dispatcher to call next. - /// The authentication bearer to decode the identity token. - internal AuthenticationMiddleware(IDispatcher next, IAuthenticationBearer authenticationBearer) + /// The bearer authentication handler to validate the identity + /// token. + internal AuthenticationMiddleware(IDispatcher next, IBearerAuthenticationHandler bearerAuthenticationHandler) { _next = next; - _authenticationBearer = authenticationBearer; + _bearerAuthenticationHandler = bearerAuthenticationHandler; } } diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs index 7ba8c6f1f5..984a0642ca 100644 --- a/examples/Authorization/Server/Authenticator.cs +++ b/examples/Authorization/Server/Authenticator.cs @@ -9,7 +9,7 @@ namespace AuthorizationExample; /// An Authenticator is an IceRPC service that implements the Slice interface 'Authenticator'. internal class Authenticator : Service, IAuthenticatorService { - private readonly IAuthenticationBearer _authenticationBearer; + private readonly IBearerAuthenticationHandler _bearerAuthenticationHandler; public ValueTask> AuthenticateAsync( string name, @@ -32,12 +32,11 @@ public ValueTask> AuthenticateAsync( throw new DispatchException(StatusCode.Unauthorized, "Unknown user or invalid password."); } - // Return the encrypted identity token. - return new(_authenticationBearer.EncodeIdentityToken(name, isAdmin)); + return new(_bearerAuthenticationHandler.CreateIdentityToken(name, isAdmin)); } /// Constructs an authenticator service. - /// The authentication bearer to encode an identity token. - internal Authenticator(IAuthenticationBearer authenticationBearer) => _authenticationBearer = authenticationBearer; - + /// The bearer authentication handler to create an identity token. + internal Authenticator(IBearerAuthenticationHandler bearerAuthenticationHandler) => + _bearerAuthenticationHandler = bearerAuthenticationHandler; } diff --git a/examples/Authorization/Server/IAuthenticationBearer.cs b/examples/Authorization/Server/IAuthenticationBearer.cs deleted file mode 100644 index d07d9282e4..0000000000 --- a/examples/Authorization/Server/IAuthenticationBearer.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) ZeroC, Inc. - -using System.Buffers; - -namespace AuthorizationExample; - -/// An interface to encode, decode and validate the authentication identity token. The identity token is sent -/// in the request header using the field. -public interface IAuthenticationBearer -{ - /// Decodes and validates a binary identity token. - /// The binary identity token. - /// A task that provides the decoded identity token as an identity feature. - /// Thrown is the decoding or the validation failed. - Task DecodeAndValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes); - - /// Encodes the fields of an identity token. - /// The user name - /// true if the user has administrative privilege, false otherwise. - /// The binary identity token. - ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin); -} diff --git a/examples/Authorization/Server/IBearerAuthenticationHandler.cs b/examples/Authorization/Server/IBearerAuthenticationHandler.cs new file mode 100644 index 0000000000..8b9457b1bf --- /dev/null +++ b/examples/Authorization/Server/IBearerAuthenticationHandler.cs @@ -0,0 +1,21 @@ +// Copyright (c) ZeroC, Inc. + +using System.Buffers; + +namespace AuthorizationExample; + +/// A bearer authentication handler to create and validate an identity token. +public interface IBearerAuthenticationHandler +{ + /// Creates a binary identity token. + /// The user name + /// true if the user has administrative privilege, false otherwise. + /// The binary identity token. + ReadOnlyMemory CreateIdentityToken(string name, bool isAdmin); + + /// Validates a binary identity token. + /// The binary identity token. + /// A task that returns the decoded identity token as an identity feature. + /// Thrown is the validation failed. + Task ValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes); +} diff --git a/examples/Authorization/Server/JwtAuthenticationBearer.cs b/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs similarity index 87% rename from examples/Authorization/Server/JwtAuthenticationBearer.cs rename to examples/Authorization/Server/JwtBearerAuthenticationHandler.cs index 9ca1494aa3..a3ad6b9dac 100644 --- a/examples/Authorization/Server/JwtAuthenticationBearer.cs +++ b/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs @@ -9,14 +9,15 @@ namespace AuthorizationExample; -/// This is an implementation of the using JSON Web Token (JWT). -internal sealed class JwtAuthenticationBearer : IAuthenticationBearer +/// This is an implementation of to create and validate a JWT +/// identity token. +internal sealed class JwtBearerAuthenticationHandler : IBearerAuthenticationHandler { private readonly SigningCredentials _signingCredentials; private readonly JwtSecurityTokenHandler _tokenHandler; private readonly TokenValidationParameters _tokenValidationParameters; - public async Task DecodeAndValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes) + public async Task ValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes) { string jwtTokenString = Encoding.UTF8.GetString(identityTokenBytes.ToArray()); TokenValidationResult validationResult = await _tokenHandler.ValidateTokenAsync( @@ -56,7 +57,7 @@ public async Task DecodeAndValidateIdentityTokenAsync(ReadOnly } } - public ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin) + public ReadOnlyMemory CreateIdentityToken(string name, bool isAdmin) { var jwtToken = new JwtSecurityToken( claims: new Claim[] @@ -72,7 +73,7 @@ public ReadOnlyMemory EncodeIdentityToken(string name, bool isAdmin) return Encoding.UTF8.GetBytes(_tokenHandler.WriteToken(jwtToken)); } - internal JwtAuthenticationBearer(string secretKey) + internal JwtBearerAuthenticationHandler(string secretKey) { _tokenHandler = new JwtSecurityTokenHandler(); var issuerKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index 239ec001dc..e52b91fb97 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -3,29 +3,29 @@ using AuthorizationExample; using IceRpc; -IAuthenticationBearer? authenticationBearer = null; +IBearerAuthenticationHandler? bearerAuthenticationHandler = null; if (args.Length == 1 && args[0] == "--jwt") { - authenticationBearer = new JwtAuthenticationBearer("A secret key for the authorization example"); + bearerAuthenticationHandler = new JwtBearerAuthenticationHandler("A secret key for the authorization example"); } else { - authenticationBearer = new AesAuthenticationBearer(); + bearerAuthenticationHandler = new AesBearerAuthenticationHandler(); } -// Dispose the authentication bearer if it's disposable. -using var disposable = authenticationBearer as IDisposable; +// Dispose the bearer authentication handler if it's disposable. +using var disposable = bearerAuthenticationHandler as IDisposable; var router = new Router(); -// Install a middleware to decrypt and decode the request's identity token and add an identity feature to the request's -// feature collection. -router.UseAuthentication(authenticationBearer); +// Install a middleware to validate the request's identity token and add an identity feature to the request's feature +// collection. +router.UseAuthentication(bearerAuthenticationHandler); var chatbot = new Chatbot(); router.Map("/greeting", chatbot); -router.Map("/authenticator", new Authenticator(authenticationBearer)); +router.Map("/authenticator", new Authenticator(bearerAuthenticationHandler)); router.Route("/greetingAdmin", adminRouter => { diff --git a/examples/Authorization/Server/RouterExtensions.cs b/examples/Authorization/Server/RouterExtensions.cs index 6b930e8246..951c7be363 100644 --- a/examples/Authorization/Server/RouterExtensions.cs +++ b/examples/Authorization/Server/RouterExtensions.cs @@ -10,10 +10,10 @@ internal static class RouterExtensions { /// Adds an to the router. /// The router being configured. - /// The authentication bearer to decode the identity token. + /// The bearer authentication handler to decode the identity token. /// The router being configured. - internal static Router UseAuthentication(this Router router, IAuthenticationBearer authenticationBearer) => - router.Use(next => new AuthenticationMiddleware(next, authenticationBearer)); + internal static Router UseAuthentication(this Router router, IBearerAuthenticationHandler authenticationBearerScheme) => + router.Use(next => new AuthenticationMiddleware(next, authenticationBearerScheme)); /// Adds an to the router. /// The router being configured. From ca62a3807efaa29b185240cca2fcd472b94b9e22 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 4 Apr 2023 15:20:41 +0200 Subject: [PATCH 23/26] Review fixes --- ...uthorization.slice => Authenticator.slice} | 14 ------- examples/Authorization/Client/Client.csproj | 4 +- examples/Authorization/Client/Program.cs | 42 +++++++++---------- examples/Authorization/Greeter.slice | 10 +++++ examples/Authorization/GreeterAdmin.slice | 10 +++++ examples/Authorization/README.md | 12 +++--- .../Server/AesBearerAuthenticationHandler.cs | 4 +- .../Authorization/Server/Authenticator.cs | 2 +- .../Server/AuthorizationMiddleware.cs | 4 +- examples/Authorization/Server/Chatbot.cs | 2 +- examples/Authorization/Server/ChatbotAdmin.cs | 2 +- .../Authorization/Server/IdentityFeature.cs | 2 +- examples/Authorization/Server/Program.cs | 6 +-- examples/Authorization/Server/Server.csproj | 4 +- 14 files changed, 64 insertions(+), 54 deletions(-) rename examples/Authorization/{Authorization.slice => Authenticator.slice} (53%) create mode 100644 examples/Authorization/Greeter.slice create mode 100644 examples/Authorization/GreeterAdmin.slice diff --git a/examples/Authorization/Authorization.slice b/examples/Authorization/Authenticator.slice similarity index 53% rename from examples/Authorization/Authorization.slice rename to examples/Authorization/Authenticator.slice index 5e6082262d..e5ec7c703d 100644 --- a/examples/Authorization/Authorization.slice +++ b/examples/Authorization/Authenticator.slice @@ -13,17 +13,3 @@ interface Authenticator { /// @returns: The encrypted identity token. authenticate(name: string, password: string) -> EncryptedIdentityToken } - -/// A service to get a personalized greeting message. -interface Greeter { - /// Creates a personalized "hello" greeting. - /// @returns: The greeting. - greet() -> string -} - -/// Represents a service to configure the greeting of the greeter service. -interface GreeterAdmin { - /// Changes the greeting returned by the Greeting service. - /// @param greeting: The new greeting. - changeGreeting(greeting: string) -} diff --git a/examples/Authorization/Client/Client.csproj b/examples/Authorization/Client/Client.csproj index 2dbda209f7..bbf82fccf9 100644 --- a/examples/Authorization/Client/Client.csproj +++ b/examples/Authorization/Client/Client.csproj @@ -1,7 +1,9 @@ - + + + diff --git a/examples/Authorization/Client/Program.cs b/examples/Authorization/Client/Program.cs index e603ba025e..631df10b96 100644 --- a/examples/Authorization/Client/Program.cs +++ b/examples/Authorization/Client/Program.cs @@ -7,48 +7,48 @@ var authenticatorProxy = new AuthenticatorProxy(connection, new Uri("icerpc:/authenticator")); -// Authenticate the "friend" user and get its identity token. -ReadOnlyMemory friendToken = await authenticatorProxy.AuthenticateAsync("friend", "password"); +// Authenticate the alice user and get its identity token. +ReadOnlyMemory aliceToken = await authenticatorProxy.AuthenticateAsync("alice", "password"); -// A greeting proxy that doesn't use any identity token. -var unauthenticatedGreetingProxy = new GreeterProxy(connection, new Uri("icerpc:/greeting")); +// A greeter proxy that doesn't use any identity token. +var unauthenticatedGreeterProxy = new GreeterProxy(connection, new Uri("icerpc:/greeter")); -// The Greet invocation on the unauthenticated greeting proxy prints a generic message. -Console.WriteLine(await unauthenticatedGreetingProxy.GreetAsync()); +// The Greet invocation on the unauthenticated greeter proxy prints a generic message. +Console.WriteLine(await unauthenticatedGreeterProxy.GreetAsync()); -// Create a greeting proxy that uses a pipe line to insert the "friend" token into a request field. -Pipeline friendPipeline = new Pipeline().UseAuthentication(friendToken).Into(connection); -var friendGreetingProxy = new GreeterProxy(friendPipeline, new Uri("icerpc:/greeting")); +// Create a greeter proxy that uses a pipe line to insert the "alice" token into a request field. +Pipeline alicePipeline = new Pipeline().UseAuthentication(aliceToken).Into(connection); +var aliceGreeterProxy = new GreeterProxy(alicePipeline, new Uri("icerpc:/greeter")); -// The Greet invocation on the authenticated "friend" greeting proxy prints a custom message for "friend". -Console.WriteLine(await friendGreetingProxy.GreetAsync()); +// The Greet invocation on the authenticated "alice" greeter proxy prints a custom message for "alice". +Console.WriteLine(await aliceGreeterProxy.GreetAsync()); -// A greeting admin proxy that uses the "friend" pipeline is not authorized to change the greeting message because "friend" +// A greeter admin proxy that uses the "alice" pipeline is not authorized to change the greeting message because "alice" // doesn't have administrative privileges. -var greetingAdminProxy = new GreeterAdminProxy(friendPipeline, new Uri("icerpc:/greetingAdmin")); +var greeterAdminProxy = new GreeterAdminProxy(alicePipeline, new Uri("icerpc:/greeterAdmin")); try { - await greetingAdminProxy.ChangeGreetingAsync("Bonjour"); + await greeterAdminProxy.ChangeGreetingAsync("Bonjour"); } catch (DispatchException exception) when (exception.StatusCode == StatusCode.Unauthorized) { - Console.WriteLine("The 'friend' user is not authorized to change the greeting message."); + Console.WriteLine("The 'alice' user is not authorized to change the greeting message."); } // Authenticate the "admin" user and get its identity token. ReadOnlyMemory adminToken = await authenticatorProxy.AuthenticateAsync("admin", "admin-password"); -// Create a greeting admin proxy that uses a pipe line to insert the "admin" token into a request field. +// Create a greeter admin proxy that uses a pipe line to insert the "admin" token into a request field. Pipeline adminPipeline = new Pipeline().UseAuthentication(adminToken).Into(connection); -greetingAdminProxy = new GreeterAdminProxy(adminPipeline, new Uri("icerpc:/greetingAdmin")); +greeterAdminProxy = new GreeterAdminProxy(adminPipeline, new Uri("icerpc:/greeterAdmin")); // Changing the greeting message should succeed this time because the "admin" user has administrative privilege. -await greetingAdminProxy.ChangeGreetingAsync("Bonjour"); +await greeterAdminProxy.ChangeGreetingAsync("Bonjour"); // The Greet invocation should print a greeting with the updated greeting message. -Console.WriteLine(await friendGreetingProxy.GreetAsync()); +Console.WriteLine(await aliceGreeterProxy.GreetAsync()); -// Change back the greeting message to Greeting. -await greetingAdminProxy.ChangeGreetingAsync("Greeting"); +// Change back the greeting message to Hello. +await greeterAdminProxy.ChangeGreetingAsync("Hello"); await connection.ShutdownAsync(); diff --git a/examples/Authorization/Greeter.slice b/examples/Authorization/Greeter.slice new file mode 100644 index 0000000000..6dbd8644f6 --- /dev/null +++ b/examples/Authorization/Greeter.slice @@ -0,0 +1,10 @@ +// Copyright (c) ZeroC, Inc. + +module AuthorizationExample + +/// A service to get a personalized greeting message. +interface Greeter { + /// Creates a personalized "hello" greeting. + /// @returns: The greeting. + greet() -> string +} diff --git a/examples/Authorization/GreeterAdmin.slice b/examples/Authorization/GreeterAdmin.slice new file mode 100644 index 0000000000..35e248ba4a --- /dev/null +++ b/examples/Authorization/GreeterAdmin.slice @@ -0,0 +1,10 @@ +// Copyright (c) ZeroC, Inc. + +module AuthorizationExample + +/// Represents a service to configure the greeting of the greeter service. +interface GreeterAdmin { + /// Changes the greeting returned by the Greeting service. + /// @param greeting: The new greeting. + changeGreeting(greeting: string) +} diff --git a/examples/Authorization/README.md b/examples/Authorization/README.md index c858a416cd..0c14b1ae7f 100644 --- a/examples/Authorization/README.md +++ b/examples/Authorization/README.md @@ -1,11 +1,11 @@ # Authorization This demo demonstrates how token based authorization and authentication can be implemented with an interceptor and two -middleware. The token provides identify information. +middleware. The token carries the name of the user and its administrative privilege. The server dispatch pipeline is configured with two middleware: -- The `AuthenticationMiddleware` is responsible for validating the request's identity token field and storing it using - the request's identity feature. +- The `AuthenticationMiddleware` is responsible for validating the request's identity token field and setting the + request's identity feature. - The `AuthorizationMiddleware` is responsible for checking if the request's identity feature is authorized. The client invocation pipeline is configured with an `AuthenticationInterceptor` interceptor, which is responsible for adding a request field with the encrypted identity token. The client obtains its identity token by authenticating itself with the `Authenticator` service. @@ -30,11 +30,11 @@ dotnet run --project Client/Client.csproj The client first calls `GreetAsync` without an identity token and the server responds with a generic greeting. -Next, the client gets an identity token for the user `friend` and uses it to construct an authentication invocation -pipeline that adds the `friend` identity token to each request. The client then calls `GreetAsync` using the `friend` +Next, the client gets an identity token for the user `alice` and uses it to construct an authentication invocation +pipeline that adds the `alice` identity token to each request. The client then calls `GreetAsync` using the `alice` authentication pipeline and receives a personalized message. -Next, the client calls `ChangeGreetingAsync` using the `friend` authentication pipeline to change the greeting. The user `friend` doesn't have administrative privilege so the invocation fails with a `DispatchException`. +Next, the client calls `ChangeGreetingAsync` using the `alice` authentication pipeline to change the greeting. The user `alice` doesn't have administrative privilege so the invocation fails with a `DispatchException`. Finally, the client authenticates the user `admin` and calls `ChangeGreetingAsync` using an `admin` authentication pipeline. The call succeeds because the user `admin` has administrative privilege. diff --git a/examples/Authorization/Server/AesBearerAuthenticationHandler.cs b/examples/Authorization/Server/AesBearerAuthenticationHandler.cs index 2d83c7ab12..89bafb25c7 100644 --- a/examples/Authorization/Server/AesBearerAuthenticationHandler.cs +++ b/examples/Authorization/Server/AesBearerAuthenticationHandler.cs @@ -8,7 +8,7 @@ namespace AuthorizationExample; /// This is an implementation of to create and validate a Slice -/// based identity token encrypted with Aes. +/// based identity token encrypted with AES. internal sealed class AesBearerAuthenticationHandler : IBearerAuthenticationHandler, IDisposable { private readonly Aes _aes; @@ -25,7 +25,7 @@ public async Task ValidateIdentityTokenAsync(ReadOnlySequence< AesIdentityToken identityToken = DecodeIdentityToken(destinationStream.ToArray()); Console.WriteLine( - $"Decoded Aes identity token {{ name = '{identityToken.Name}' isAdmin = '{identityToken.IsAdmin}' }}"); + $"Decoded AES identity token {{ name = '{identityToken.Name}' isAdmin = '{identityToken.IsAdmin}' }}"); return new IdentityFeature(identityToken.Name, identityToken.IsAdmin); diff --git a/examples/Authorization/Server/Authenticator.cs b/examples/Authorization/Server/Authenticator.cs index 984a0642ca..bf2b9cfbf0 100644 --- a/examples/Authorization/Server/Authenticator.cs +++ b/examples/Authorization/Server/Authenticator.cs @@ -23,7 +23,7 @@ public ValueTask> AuthenticateAsync( { isAdmin = true; } - else if (name == "friend" && password == "password") + else if (name == "alice" && password == "password") { isAdmin = false; } diff --git a/examples/Authorization/Server/AuthorizationMiddleware.cs b/examples/Authorization/Server/AuthorizationMiddleware.cs index a10e49d2df..2b80317b24 100644 --- a/examples/Authorization/Server/AuthorizationMiddleware.cs +++ b/examples/Authorization/Server/AuthorizationMiddleware.cs @@ -4,8 +4,8 @@ namespace AuthorizationExample; -/// A middleware that checks if the request is authorized. If not, it throws a with the status code. +/// A middleware that checks if the request is authorized. If not, it throws a +/// with the status code. internal class AuthorizationMiddleware : IDispatcher { private readonly Func _authorizeFunc; diff --git a/examples/Authorization/Server/Chatbot.cs b/examples/Authorization/Server/Chatbot.cs index 937745833d..1563a6bc28 100644 --- a/examples/Authorization/Server/Chatbot.cs +++ b/examples/Authorization/Server/Chatbot.cs @@ -5,7 +5,7 @@ namespace AuthorizationExample; -/// A Chatbot is an IceRPC service that implements the Slice interface 'Greeting'. +/// A Chatbot is an IceRPC service that implements the Slice interface 'Greeter'. internal class Chatbot : Service, IGreeterService { internal string Greeting { get; set; } = "Hello"; diff --git a/examples/Authorization/Server/ChatbotAdmin.cs b/examples/Authorization/Server/ChatbotAdmin.cs index 942dba6538..225ecf6e04 100644 --- a/examples/Authorization/Server/ChatbotAdmin.cs +++ b/examples/Authorization/Server/ChatbotAdmin.cs @@ -5,7 +5,7 @@ namespace AuthorizationExample; -/// A ChatbotAdmin is an IceRPC service that implements the Slice interface 'GreetingAdmin'. +/// A ChatbotAdmin is an IceRPC service that implements the Slice interface 'GreeterAdmin'. internal class ChatbotAdmin : Service, IGreeterAdminService { private readonly Chatbot _chatbot; diff --git a/examples/Authorization/Server/IdentityFeature.cs b/examples/Authorization/Server/IdentityFeature.cs index c52bb43745..4696afa77f 100644 --- a/examples/Authorization/Server/IdentityFeature.cs +++ b/examples/Authorization/Server/IdentityFeature.cs @@ -2,7 +2,7 @@ namespace AuthorizationExample; -/// A feature that provides the name and administrative privilege of the request. +/// A request feature that provides the name and administrative privilege of the request. public interface IIdentityFeature { /// true if the authenticated user has administrative privilege, false otherwise. diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index e52b91fb97..b0e676c9e1 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -23,13 +23,13 @@ router.UseAuthentication(bearerAuthenticationHandler); var chatbot = new Chatbot(); -router.Map("/greeting", chatbot); +router.Map("/greeter", chatbot); router.Map("/authenticator", new Authenticator(bearerAuthenticationHandler)); -router.Route("/greetingAdmin", adminRouter => +router.Route("/greeterAdmin", adminRouter => { - // Install an authorization middleware that checks if the caller is authorized to call the greeting admin service. + // Install an authorization middleware that checks if the caller is authorized to call the greeter admin service. adminRouter.UseAuthorization(identityFeature => identityFeature.IsAdmin); adminRouter.Map("/", new ChatbotAdmin(chatbot)); }); diff --git a/examples/Authorization/Server/Server.csproj b/examples/Authorization/Server/Server.csproj index b7742d4cda..a06afc8bac 100644 --- a/examples/Authorization/Server/Server.csproj +++ b/examples/Authorization/Server/Server.csproj @@ -1,7 +1,9 @@ - + + + From 4e8871e7d590fb463349a9bcea2d07fa803ea6ac Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Tue, 4 Apr 2023 19:08:03 +0200 Subject: [PATCH 24/26] Fixes --- .../Server/AesBearerAuthenticationHandler.cs | 19 +++++++++---------- .../Server/JwtBearerAuthenticationHandler.cs | 2 +- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/examples/Authorization/Server/AesBearerAuthenticationHandler.cs b/examples/Authorization/Server/AesBearerAuthenticationHandler.cs index 89bafb25c7..1858ad22e4 100644 --- a/examples/Authorization/Server/AesBearerAuthenticationHandler.cs +++ b/examples/Authorization/Server/AesBearerAuthenticationHandler.cs @@ -15,13 +15,13 @@ internal sealed class AesBearerAuthenticationHandler : IBearerAuthenticationHand public async Task ValidateIdentityTokenAsync(ReadOnlySequence identityTokenBytes) { - // Decrypt the Slice2 encoded token. + // Decrypt the identity token buffer. using var sourceStream = new MemoryStream(identityTokenBytes.ToArray()); using var cryptoStream = new CryptoStream(sourceStream, _aes.CreateDecryptor(), CryptoStreamMode.Read); using var destinationStream = new MemoryStream(); await cryptoStream.CopyToAsync(destinationStream); - // Decode the Slice2 encoded token and return the feature. + // Decode the identity token. AesIdentityToken identityToken = DecodeIdentityToken(destinationStream.ToArray()); Console.WriteLine( @@ -40,18 +40,17 @@ AesIdentityToken DecodeIdentityToken(ReadOnlyMemory buffer) public ReadOnlyMemory CreateIdentityToken(string name, bool isAdmin) { - // Encode the token with the Slice2 encoding. - using var tokenStream = new MemoryStream(); - var writer = PipeWriter.Create(tokenStream, new StreamPipeWriterOptions(leaveOpen: true)); + // Setup the stream to encrypt the encoded identity token. + using var destinationStream = new MemoryStream(); + using var cryptoStream = new CryptoStream(destinationStream, _aes.CreateEncryptor(), CryptoStreamMode.Write); + + // Encode the identity token. + var writer = PipeWriter.Create(cryptoStream, new StreamPipeWriterOptions(leaveOpen: true)); var encoder = new SliceEncoder(writer, SliceEncoding.Slice2); new AesIdentityToken(isAdmin, name).Encode(ref encoder); writer.Complete(); - tokenStream.Seek(0, SeekOrigin.Begin); - // Crypt and return the Slice2 encoded token. - using var destinationStream = new MemoryStream(); - using var cryptoStream = new CryptoStream(tokenStream, _aes.CreateEncryptor(), CryptoStreamMode.Read); - cryptoStream.CopyTo(destinationStream); + cryptoStream.FlushFinalBlock(); return destinationStream.ToArray(); } diff --git a/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs b/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs index a3ad6b9dac..c5098db6f1 100644 --- a/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs +++ b/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs @@ -62,7 +62,7 @@ public ReadOnlyMemory CreateIdentityToken(string name, bool isAdmin) var jwtToken = new JwtSecurityToken( claims: new Claim[] { - new Claim(JwtRegisteredClaimNames.Sub, "test"), + new Claim(JwtRegisteredClaimNames.Sub, name), new Claim("isAdmin", (name == "admin").ToString()) }, audience: "Authorization example", From bdaa0ac289859958fa3e800a4dbc4aabbbae1ff9 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Wed, 5 Apr 2023 09:17:45 +0200 Subject: [PATCH 25/26] Fixed bogus admin check --- examples/Authorization/Server/JwtBearerAuthenticationHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs b/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs index c5098db6f1..288712d17e 100644 --- a/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs +++ b/examples/Authorization/Server/JwtBearerAuthenticationHandler.cs @@ -63,7 +63,7 @@ public ReadOnlyMemory CreateIdentityToken(string name, bool isAdmin) claims: new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, name), - new Claim("isAdmin", (name == "admin").ToString()) + new Claim("isAdmin", isAdmin.ToString()) }, audience: "Authorization example", issuer: "icerpc://127.0.0.1", From 99c159f1f89dd206cfd7466b6c619ecc2b266e90 Mon Sep 17 00:00:00 2001 From: Benoit Foucher Date: Wed, 5 Apr 2023 11:05:53 +0200 Subject: [PATCH 26/26] Fixes --- examples/Authorization/Authenticator.slice | 10 ++++------ .../Client/AuthenticationInterceptor.cs | 2 +- examples/Authorization/Client/PipelineExtensions.cs | 4 ++-- examples/Authorization/Client/Program.cs | 13 ++++++------- examples/Authorization/Server/Chatbot.cs | 2 +- examples/Authorization/Server/Program.cs | 9 +++++++-- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/examples/Authorization/Authenticator.slice b/examples/Authorization/Authenticator.slice index e5ec7c703d..83e968b123 100644 --- a/examples/Authorization/Authenticator.slice +++ b/examples/Authorization/Authenticator.slice @@ -2,14 +2,12 @@ module AuthorizationExample -/// The encrypted identity token. -typealias EncryptedIdentityToken = sequence - -/// Represents a service that authenticates a user and return an identity token. +/// Represents a service that authenticates a user and return an identity token. The identity token can be either a JWT +/// token or an AES encrypted token depending on how the server was started (with or without the --jwt argument). interface Authenticator { /// Authenticates a user. /// @param name: The user name. /// @param password: The user password. - /// @returns: The encrypted identity token. - authenticate(name: string, password: string) -> EncryptedIdentityToken + /// @returns: The encoded identity token. + authenticate(name: string, password: string) -> sequence } diff --git a/examples/Authorization/Client/AuthenticationInterceptor.cs b/examples/Authorization/Client/AuthenticationInterceptor.cs index e25c53ca91..5a3f383d01 100644 --- a/examples/Authorization/Client/AuthenticationInterceptor.cs +++ b/examples/Authorization/Client/AuthenticationInterceptor.cs @@ -5,7 +5,7 @@ namespace AuthorizationExample; -/// An interceptor that adds the encrypted identity token field to each request. +/// Represents an interceptor that adds the encrypted identity token field to each request. internal class AuthenticationInterceptor : IInvoker { private readonly ReadOnlySequence _identityToken; diff --git a/examples/Authorization/Client/PipelineExtensions.cs b/examples/Authorization/Client/PipelineExtensions.cs index bf9fb4acaa..bb2de9d325 100644 --- a/examples/Authorization/Client/PipelineExtensions.cs +++ b/examples/Authorization/Client/PipelineExtensions.cs @@ -4,8 +4,8 @@ namespace IceRpc; -/// This class provides an extension method to add an -/// to a . +/// This class provides an extension method to add an to a . internal static class PipelineExtensions { /// Adds an to the pipeline. diff --git a/examples/Authorization/Client/Program.cs b/examples/Authorization/Client/Program.cs index 631df10b96..2ec0aafd83 100644 --- a/examples/Authorization/Client/Program.cs +++ b/examples/Authorization/Client/Program.cs @@ -5,17 +5,16 @@ await using var connection = new ClientConnection(new Uri("icerpc://localhost")); -var authenticatorProxy = new AuthenticatorProxy(connection, new Uri("icerpc:/authenticator")); - -// Authenticate the alice user and get its identity token. -ReadOnlyMemory aliceToken = await authenticatorProxy.AuthenticateAsync("alice", "password"); - // A greeter proxy that doesn't use any identity token. var unauthenticatedGreeterProxy = new GreeterProxy(connection, new Uri("icerpc:/greeter")); // The Greet invocation on the unauthenticated greeter proxy prints a generic message. Console.WriteLine(await unauthenticatedGreeterProxy.GreetAsync()); +// Authenticate the alice user and get its identity token. +var authenticatorProxy = new AuthenticatorProxy(connection, new Uri("icerpc:/authenticator")); +ReadOnlyMemory aliceToken = await authenticatorProxy.AuthenticateAsync("alice", "password"); + // Create a greeter proxy that uses a pipe line to insert the "alice" token into a request field. Pipeline alicePipeline = new Pipeline().UseAuthentication(aliceToken).Into(connection); var aliceGreeterProxy = new GreeterProxy(alicePipeline, new Uri("icerpc:/greeter")); @@ -24,7 +23,7 @@ Console.WriteLine(await aliceGreeterProxy.GreetAsync()); // A greeter admin proxy that uses the "alice" pipeline is not authorized to change the greeting message because "alice" -// doesn't have administrative privileges. +// doesn't have administrative privilege. var greeterAdminProxy = new GreeterAdminProxy(alicePipeline, new Uri("icerpc:/greeterAdmin")); try { @@ -38,7 +37,7 @@ // Authenticate the "admin" user and get its identity token. ReadOnlyMemory adminToken = await authenticatorProxy.AuthenticateAsync("admin", "admin-password"); -// Create a greeter admin proxy that uses a pipe line to insert the "admin" token into a request field. +// Create a greeter admin proxy that uses a pipeline to insert the "admin" token into a request field. Pipeline adminPipeline = new Pipeline().UseAuthentication(adminToken).Into(connection); greeterAdminProxy = new GreeterAdminProxy(adminPipeline, new Uri("icerpc:/greeterAdmin")); diff --git a/examples/Authorization/Server/Chatbot.cs b/examples/Authorization/Server/Chatbot.cs index 1563a6bc28..81ca2f1d4f 100644 --- a/examples/Authorization/Server/Chatbot.cs +++ b/examples/Authorization/Server/Chatbot.cs @@ -25,7 +25,7 @@ public ValueTask GreetAsync(IFeatureCollection features, CancellationTok isAdmin = false; } - Console.WriteLine($"Dispatching Greet request {{ name = '{name}' isAdmin = {isAdmin} }}"); + Console.WriteLine($"Dispatching Greet request {{ name = '{name}' isAdmin = '{isAdmin}' }}"); return new($"{Greeting}, {name}!"); } } diff --git a/examples/Authorization/Server/Program.cs b/examples/Authorization/Server/Program.cs index b0e676c9e1..91002966bf 100644 --- a/examples/Authorization/Server/Program.cs +++ b/examples/Authorization/Server/Program.cs @@ -4,13 +4,18 @@ using IceRpc; IBearerAuthenticationHandler? bearerAuthenticationHandler = null; -if (args.Length == 1 && args[0] == "--jwt") +if (args.Length == 0) +{ + bearerAuthenticationHandler = new AesBearerAuthenticationHandler(); +} +else if (args.Length == 1 && args[0] == "--jwt") { bearerAuthenticationHandler = new JwtBearerAuthenticationHandler("A secret key for the authorization example"); } else { - bearerAuthenticationHandler = new AesBearerAuthenticationHandler(); + Console.WriteLine($"Invalid server arguments."); + return; } // Dispose the bearer authentication handler if it's disposable.