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 27 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
3 changes: 3 additions & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
"contentfiles",
"Datagram",
"decoratee",
"Decryptor",
"docfx",
"ECONNRESET",
"Encryptor",
"EPIPE",
"Finalizers",
"globaltool",
"Hmac",
"icecertutils",
"icerpc",
"lalrpop",
Expand Down
27 changes: 14 additions & 13 deletions examples/Authorization/Authorization.slice
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,28 @@

module AuthorizationExample

/// The authorization token
typealias Token = sequence<uint8>
/// The encrypted identity token.
typealias EncryptedIdentityToken = sequence<uint8>

/// Represents a service to create a session token.
interface SessionManager {
/// Creates a new session token.
/// Represents a service that authenticates a user and return an identity token.
interface Authenticator {
/// Authenticates a user.
/// @param name: The user name.
/// @returns: The token.
createSession(name: string) -> Token
/// @param password: The user password.
/// @returns: The encrypted identity token.
authenticate(name: string, password: string) -> EncryptedIdentityToken
}

/// Represents a recipient of hello greetings.
interface Hello {
/// A service to get a personalized greeting message.
interface Greeter {
/// Creates a personalized "hello" greeting.
/// @returns: The greeting.
sayHello() -> string
greet() -> string
}

/// Represents a service to configure a simpler greeter.
interface HelloAdmin {
/// Changes the greeting returned by the Hello service.
/// 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)
}
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 identity token field to each request.</summary>
internal class AuthenticationInterceptor : IInvoker
{
private readonly ReadOnlySequence<byte> _identityToken;
private readonly IInvoker _next;

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

/// <summary>Constructs an authentication interceptor.</summary>
/// <param name="next">The invoker to call next.</param>
/// <param name="identityToken">The encrypted identity token.</param>
internal AuthenticationInterceptor(IInvoker next, ReadOnlyMemory<byte> identityToken)
{
_next = next;
_identityToken = new ReadOnlySequence<byte>(identityToken);
}
}
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 @@
<SliceC Include="../Authorization.slice" />
<PackageReference Include="IceRpc.Slice.Tools" Version="$(IceRpcVersion)" PrivateAssets="All" />
<PackageReference Include="IceRpc" Version="$(IceRpcVersion)" />
<Compile Include="../SessionFieldKey.cs" Link="SessionFieldKey.cs" />
<Compile Include="../IdentityTokenFieldKey.cs" Link="IdentityTokenFieldKey.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="identityToken">The encrypted identity 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> identityToken) =>
pipeline.Use(next => new AuthenticationInterceptor(next, identityToken));
}
57 changes: 36 additions & 21 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"));
var authenticatorProxy = new AuthenticatorProxy(connection, new Uri("icerpc:/authenticator"));

// Unauthenticated hello; prints generic greeting.
Console.WriteLine(await unauthenticatedHelloProxy.SayHelloAsync());
// Authenticate the "friend" user and get its identity token.
ReadOnlyMemory<byte> friendToken = await authenticatorProxy.AuthenticateAsync("friend", "password");

// A `SessionManager` proxy that doesn't use any authentication. Used to create new session tokens.
var sessionManagerProxy = new SessionManagerProxy(connection, new Uri("icerpc:/sessionManager"));
// A greeting proxy that doesn't use any identity token.
var unauthenticatedGreetingProxy = new GreeterProxy(connection, new Uri("icerpc:/greeting"));

// Get an authentication token. The token is used to authenticate future requests.
var token = new Guid(await sessionManagerProxy.CreateSessionAsync("friend"));
// The Greet invocation on the unauthenticated greeting proxy prints a generic message.
Console.WriteLine(await unauthenticatedGreetingProxy.GreetAsync());

// Add an interceptor to the invocation pipeline that inserts the token into a request field.
Pipeline authenticatedPipeline = new Pipeline().UseSession(token).Into(connection);
// 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"));

// 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"));
// The Greet invocation on the authenticated "friend" greeting proxy prints a custom message for "friend".
Console.WriteLine(await friendGreetingProxy.GreetAsync());

// 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 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 GreeterAdminProxy(friendPipeline, new Uri("icerpc:/greetingAdmin"));
try
{
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.");
}

// Authenticated hello.
Console.WriteLine(await helloProxy.SayHelloAsync());
// Authenticate the "admin" user and get its identity token.
ReadOnlyMemory<byte> adminToken = await authenticatorProxy.AuthenticateAsync("admin", "admin-password");

// Change the greeting using the authentication token.
await helloAdminProxy.ChangeGreetingAsync("Bonjour");
// 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 GreeterAdminProxy(adminPipeline, new Uri("icerpc:/greetingAdmin"));

// Authenticated hello with updated greeting.
Console.WriteLine(await helloProxy.SayHelloAsync());
// Changing the greeting message should succeed this time because the "admin" user has administrative privilege.
await greetingAdminProxy.ChangeGreetingAsync("Bonjour");

// The Greet invocation should print a greeting with the updated greeting message.
Console.WriteLine(await friendGreetingProxy.GreetAsync());

// Change back the greeting message to Greeting.
await greetingAdminProxy.ChangeGreetingAsync("Greeting");

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

This file was deleted.

11 changes: 11 additions & 0 deletions examples/Authorization/IdentityTokenFieldKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) ZeroC, Inc.

using IceRpc;

namespace AuthorizationExample;

/// <summary>The shared <see cref="RequestFieldKey" /> used to carry the identity token in a request's field.</summary>
public static class IdentityTokenFieldKey
{
public const RequestFieldKey Value = (RequestFieldKey)100;
}
38 changes: 25 additions & 13 deletions examples/Authorization/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# Authorization

This example application illustrates how to create an authorization interceptor and 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.

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 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 `AuthorizationMiddleware` is responsible for checking if the request's identity feature is authorized.

The client creates an authorization pipeline that is responsible for adding the session token as a request field.
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

Expand All @@ -24,11 +28,19 @@ 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 `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`
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 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.
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.

Finally, the client calls `ChangeGreetingAsync` using the authenticated pipeline to change the greeting and then calls
`SayHelloAsync` a final time.
To use JTW token instead of AES tokens, start the Server with the `--jwt` argument:

```shell
dotnet run --project Server/Server.csproj --jwt
```
63 changes: 63 additions & 0 deletions examples/Authorization/Server/AesBearerAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) ZeroC, Inc.

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

namespace AuthorizationExample;

/// <summary>This is an implementation of <see cref="IBearerAuthenticationHandler" /> to create and validate a Slice
/// based identity token encrypted with Aes.</summary>
internal sealed class AesBearerAuthenticationHandler : IBearerAuthenticationHandler, IDisposable
{
private readonly Aes _aes;

public async Task<IIdentityFeature> ValidateIdentityTokenAsync(ReadOnlySequence<byte> 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());

Console.WriteLine(
$"Decoded Aes identity token {{ name = '{identityToken.Name}' isAdmin = '{identityToken.IsAdmin}' }}");

return new IdentityFeature(identityToken.Name, identityToken.IsAdmin);

AesIdentityToken DecodeIdentityToken(ReadOnlyMemory<byte> buffer)
{
var decoder = new SliceDecoder(destinationStream.ToArray(), SliceEncoding.Slice2);
return new AesIdentityToken(ref decoder);
}
}

public void Dispose() => _aes.Dispose();

public ReadOnlyMemory<byte> 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));
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 AesBearerAuthenticationHandler()
{
_aes = Aes.Create();
_aes.Padding = PaddingMode.Zeros;
}
}
12 changes: 12 additions & 0 deletions examples/Authorization/Server/AesIdentityToken.slice
Original file line number Diff line number Diff line change
@@ -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
}
36 changes: 36 additions & 0 deletions examples/Authorization/Server/AuthenticationMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) ZeroC, Inc.

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

namespace AuthorizationExample;

/// <summary>A middleware that validates an identity token request field and adds an identity feature to the request's
/// feature collection.</summary>
internal class AuthenticationMiddleware : IDispatcher
{
private readonly IBearerAuthenticationHandler _bearerAuthenticationHandler;
private readonly IDispatcher _next;

public async ValueTask<OutgoingResponse> DispatchAsync(IncomingRequest request, CancellationToken cancellationToken)
{
if (request.Fields.TryGetValue(IdentityTokenFieldKey.Value, out ReadOnlySequence<byte> buffer))
{
IIdentityFeature identityFeature = await _bearerAuthenticationHandler.ValidateIdentityTokenAsync(buffer);
request.Features = request.Features.With(identityFeature);
}
return await _next.DispatchAsync(request, cancellationToken);
}

/// <summary>Constructs an authentication middleware.</summary>
/// <param name="next">The dispatcher to call next.</param>
/// <param name="bearerAuthenticationHandler">The bearer authentication handler to validate the identity
/// token.</param>
internal AuthenticationMiddleware(IDispatcher next, IBearerAuthenticationHandler bearerAuthenticationHandler)
{
_next = next;
_bearerAuthenticationHandler = bearerAuthenticationHandler;
}
}
Loading