Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved authorization demo #2830

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ebd5fbf
Added doc to examples Slice interfaces
bentoi Mar 27, 2023
793b45a
One more fix
bentoi Mar 27, 2023
fc57cd5
Better authorization demo
bentoi Mar 27, 2023
66f93ec
Added missing files
bentoi Mar 27, 2023
81ec962
More updates
bentoi Mar 28, 2023
75263d7
README.md updates
bentoi Mar 28, 2023
ca6ff6b
Merge remote-tracking branch 'origin/main' into authorizationdemo
bentoi Mar 28, 2023
0a932d7
Minor fixes
bentoi Mar 28, 2023
5f988aa
Merge remote-tracking branch 'origin/main' into authorizationdemo
bentoi Mar 28, 2023
b422778
Define AuthenticationToken with Slice2
bentoi Mar 28, 2023
d63c7c3
File renaming
bentoi Mar 28, 2023
70e9628
Doc fixes
bentoi Mar 28, 2023
f1ba43e
Another doc fix
bentoi Mar 28, 2023
66528d0
Review fixes
bentoi Mar 29, 2023
4fb4239
Doc changes
bentoi Mar 29, 2023
905e7b7
More changes
bentoi Mar 29, 2023
63b1429
Another doc fix
bentoi Mar 29, 2023
051caad
Review fixes
bentoi Mar 29, 2023
354c602
Merge remote-tracking branch 'origin/main' into authorizationdemo
bentoi Mar 29, 2023
c2a68b2
Fix
bentoi Mar 31, 2023
5967f76
Added JWT support and also kept AES
bentoi Mar 31, 2023
365c034
Missing file
bentoi Mar 31, 2023
bf81d1d
Review fixes
bentoi Mar 31, 2023
f6ea2ab
Merge remote-tracking branch 'origin/main' into authorizationdemo
bentoi Mar 31, 2023
63e99ed
More review fixes
bentoi Apr 3, 2023
8d50b54
Merge remote-tracking branch 'origin/main' into authorizationdemo
bentoi Apr 3, 2023
58d5453
Renaming
bentoi Apr 3, 2023
ca62a38
Review fixes
bentoi Apr 4, 2023
4e8871e
Fixes
bentoi Apr 4, 2023
bdaa0ac
Fixed bogus admin check
bentoi Apr 5, 2023
bc54375
Merge remote-tracking branch 'origin/main' into authorizationdemo
bentoi Apr 5, 2023
99c159f
Fixes
bentoi Apr 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
"contentfiles",
"Datagram",
"decoratee",
"Decryptor",
"docfx",
"ECONNRESET",
"EPIPE",
"Encryptor",
"Finalizers",
"globaltool",
"icecertutils",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

namespace AuthorizationExample;

/// <summary>The shared <see cref="RequestFieldKey" /> used by the client and server to carry the session
/// <summary>The shared <see cref="RequestFieldKey" /> used by the client and server to carry the authentication
/// token.</summary>
public static class SessionFieldKey
public static class AuthenticationTokenFieldKey
{
public const RequestFieldKey Value = (RequestFieldKey)100;
}
26 changes: 18 additions & 8 deletions examples/Authorization/Authorization.slice
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@

module AuthorizationExample

/// The authorization token
typealias Token = sequence<uint8>
/// An authentication token.
struct AuthenticationToken {
/// true if the authenticated client has administrative privilege, false otherwise.
isAdmin: bool

/// Represents a service to create a session token.
interface SessionManager {
/// Creates a new session token.
/// The user name.
name: string
}

/// The encrypted authentication token.
typealias EncryptedAuthenticationToken = sequence<uint8>

/// Represents a service to authentication a user and return an authentication 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) -> EncryptedAuthenticationToken
}

/// Represents a recipient of hello greetings.
Expand All @@ -20,7 +30,7 @@ interface Hello {
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.
Expand Down
28 changes: 28 additions & 0 deletions examples/Authorization/Client/AuthenticationInterceptor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) ZeroC, Inc.

using IceRpc;
using System.Buffers;

namespace AuthorizationExample;

/// <summary>An interceptor that adds the encrypted authentication token field to each request.</summary>
internal class AuthenticationInterceptor : IInvoker
{
private readonly ReadOnlySequence<byte> _authenticationToken;
private readonly IInvoker _next;

public Task<IncomingResponse> InvokeAsync(OutgoingRequest request, CancellationToken cancellationToken)
{
request.Fields = request.Fields.With(AuthenticationTokenFieldKey.Value, _authenticationToken);
return _next.InvokeAsync(request, cancellationToken);
}

/// <summary>Constructs an authentication interceptor.</summary>
/// <param name="next">The invoker to call next.</param>
/// <param name="authenticationToken">The encrypted authentication token.</param>
internal AuthenticationInterceptor(IInvoker next, ReadOnlyMemory<byte> authenticationToken)
{
_next = next;
_authenticationToken = new ReadOnlySequence<byte>(authenticationToken);
}
}
2 changes: 1 addition & 1 deletion examples/Authorization/Client/Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
<SliceCompile Include="../Authorization.slice" />
<PackageReference Include="IceRpc.Builder.MSBuild" Version="$(IceRpcBuilderVersion)" PrivateAssets="All" />
<PackageReference Include="IceRpc" Version="$(IceRpcVersion)" />
<Compile Include="../SessionFieldKey.cs" Link="SessionFieldKey.cs" />
<Compile Include="../AuthenticationTokenFieldKey.cs" Link="AuthenticationTokenFieldKey.cs" />
</ItemGroup>
</Project>
10 changes: 5 additions & 5 deletions examples/Authorization/Client/PipelineExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

namespace IceRpc;

/// <summary>This class provides an extension method to add a <see cref="SessionInterceptor" />
/// <summary>This class provides an extension method to add an <see cref="AuthenticationInterceptor" />
/// to a <see cref="Pipeline" />.</summary>
internal static class PipelineExtensions
{
/// <summary>Adds a <see cref="SessionInterceptor" /> to the pipeline.</summary>
/// <summary>Adds an <see cref="AuthenticationInterceptor" /> to the pipeline.</summary>
/// <param name="pipeline">The pipeline being configured.</param>
/// <param name="token">The session token.</param>
/// <param name="authenticationToken">The encrypted authentication token.</param>
/// <returns>The pipeline being configured.</returns>
internal static Pipeline UseSession(this Pipeline pipeline, Guid token) =>
pipeline.Use(next => new SessionInterceptor(next, token));
internal static Pipeline UseAuthentication(this Pipeline pipeline, ReadOnlyMemory<byte> authenticationToken) =>
pipeline.Use(next => new AuthenticationInterceptor(next, authenticationToken));
}
63 changes: 39 additions & 24 deletions examples/Authorization/Client/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,50 @@

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`.
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"));
var authenticatorProxy = new AuthenticatorProxy(connection, new Uri("icerpc:/authenticator"));

// Add an interceptor to the invocation pipeline that inserts the token into a request field.
Pipeline authenticatedPipeline = new Pipeline().UseSession(token).Into(connection);
// Authenticate the "friend" user and get its authentication token.
ReadOnlyMemory<byte> friendToken = await authenticatorProxy.AuthenticateAsync("friend", "password");

// 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"));
// A hello proxy that doesn't use any authentication token.
var unauthenticatedHelloProxy = new HelloProxy(connection, new Uri("icerpc:/hello"));

// Authenticated hello.
Console.WriteLine(await helloProxy.SayHelloAsync());
// The SayHello invocation on the unauthenticated hello proxy prints a generic message.
Console.WriteLine(await unauthenticatedHelloProxy.SayHelloAsync());

// Change the greeting using the authentication token.
// 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 because "friend"
// doesn't have administrative privileges.
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 message.");
}

// Authenticate the "admin" user and get its authentication token.
ReadOnlyMemory<byte> 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"));

// Changing the greeting message should succeed this time because the "admin" user has administrative privilege.
await helloAdminProxy.ChangeGreetingAsync("Bonjour");

// Authenticated hello with updated greeting.
Console.WriteLine(await helloProxy.SayHelloAsync());
// The SayHello invocation should print a greeting with the updated greeting message.
Console.WriteLine(await friendHelloProxy.SayHelloAsync());

// Change back the greeting message to Hello.
await helloAdminProxy.ChangeGreetingAsync("Hello");

await connection.ShutdownAsync();
25 changes: 0 additions & 25 deletions examples/Authorization/Client/SessionInterceptor.cs

This file was deleted.

30 changes: 17 additions & 13 deletions examples/Authorization/README.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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.
31 changes: 31 additions & 0 deletions examples/Authorization/Server/AuthenticationFeature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) ZeroC, Inc.

namespace AuthorizationExample;

/// <summary>A feature that provides the name and administrative privilege decoded from an authentication token
/// field.</summary>
internal interface IAuthenticationFeature
{
/// <summary><c>true</c> if the authenticated client has administrative privilege, <c>false</c> otherwise.</summary>
bool IsAdmin { get; }

/// <summary>The name of the authenticated client.</summary>
string Name { get; }
}

/// <summary>The implementation of <see cref="IAuthenticationFeature" />.</summary>
internal class AuthenticationFeature : IAuthenticationFeature
{
/// <inheritdoc/>
public bool IsAdmin { get; }

/// <inheritdoc/>
public string Name { get; }

/// <summary>Constructs an authentication feature from an authentication token.</summary>
internal AuthenticationFeature(AuthenticationToken token)
{
Name = token.Name;
IsAdmin = token.IsAdmin;
}
}
35 changes: 35 additions & 0 deletions examples/Authorization/Server/AuthenticationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (c) ZeroC, Inc.

using IceRpc;
using IceRpc.Features;
using System.Buffers;
using System.Security.Cryptography;

namespace AuthorizationExample;

/// <summary>A middleware that decodes and decrypt an authentication token request field and adds an authentication
/// feature to the request's feature collection.</summary>
internal class AuthenticationMiddleware : IDispatcher
{
private readonly SymmetricAlgorithm _encryptionAlgorithm;
private readonly IDispatcher _next;

public ValueTask<OutgoingResponse> DispatchAsync(IncomingRequest request, CancellationToken cancellationToken)
{
if (request.Fields.TryGetValue(AuthenticationTokenFieldKey.Value, out ReadOnlySequence<byte> buffer))
{
var token = buffer.DecryptAuthenticationToken(_encryptionAlgorithm);
request.Features = request.Features.With<IAuthenticationFeature>(new AuthenticationFeature(token));
}
return _next.DispatchAsync(request, cancellationToken);
}

/// <summary>Constructs an authentication middleware.</summary>
/// <param name="next">The invoker to call next.</param>
/// <param name="encryptionAlgorithm">The encryption algorithm used to encrypt an authentication token.</param>
internal AuthenticationMiddleware(IDispatcher next, SymmetricAlgorithm encryptionAlgorithm)
{
_next = next;
_encryptionAlgorithm = encryptionAlgorithm;
}
}
52 changes: 52 additions & 0 deletions examples/Authorization/Server/AuthenticationTokenExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (c) ZeroC, Inc.

using IceRpc.Slice;
using System.Buffers;
using System.IO.Pipelines;
using System.Security.Cryptography;

namespace AuthorizationExample;

/// <summary>Extension methods to encrypt and decrypt an authentication token.</summary>
public static class AuthenticationTokenExtensions
{
/// <summary>Decrypts an authentication token.</summary>
/// <param name="buffer">The byte buffer.</param>
/// <param name="algorithm">The symmetric algorithm used to decrypt an authentication token.</param>
/// <returns>The authentication token.</returns>
internal static AuthenticationToken DecryptAuthenticationToken(
this ReadOnlySequence<byte> 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);
}

/// <summary>Encrypts an authentication token.</summary>
/// <param name="token">The authentication token.</param>
/// <param name="algorithm">The symmetric algorithm used to encrypt this authentication token.</param>
/// <returns>The encrypted authentication token.</returns>
internal static ReadOnlyMemory<byte> 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();
}
}
Loading