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 29 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
15 changes: 15 additions & 0 deletions examples/Authorization/Authenticator.slice
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) ZeroC, Inc.

module AuthorizationExample

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

/// 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 user password.
/// @returns: The encrypted identity token.
authenticate(name: string, password: string) -> EncryptedIdentityToken
}
28 changes: 0 additions & 28 deletions examples/Authorization/Authorization.slice

This file was deleted.

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);
}
}
6 changes: 4 additions & 2 deletions examples/Authorization/Client/Client.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<SliceC Include="../Authorization.slice" />
<SliceC Include="../Greeter.slice" />
<SliceC Include="../GreeterAdmin.slice" />
<SliceC Include="../Authenticator.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 alice user and get its identity token.
ReadOnlyMemory<byte> aliceToken = await authenticatorProxy.AuthenticateAsync("alice", "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 greeter proxy that doesn't use any identity token.
var unauthenticatedGreeterProxy = new GreeterProxy(connection, new Uri("icerpc:/greeter"));

// 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 greeter proxy prints a generic message.
Console.WriteLine(await unauthenticatedGreeterProxy.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 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"));

// 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 "alice" greeter proxy prints a custom message for "alice".
Console.WriteLine(await aliceGreeterProxy.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 greeter admin proxy that uses the "alice" pipeline is not authorized to change the greeting message because "alice"
// doesn't have administrative privileges.
var greeterAdminProxy = new GreeterAdminProxy(alicePipeline, new Uri("icerpc:/greeterAdmin"));
try
{
await greeterAdminProxy.ChangeGreetingAsync("Bonjour");
}
catch (DispatchException exception) when (exception.StatusCode == StatusCode.Unauthorized)
{
Console.WriteLine("The 'alice' 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 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);
greeterAdminProxy = new GreeterAdminProxy(adminPipeline, new Uri("icerpc:/greeterAdmin"));

// 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 greeterAdminProxy.ChangeGreetingAsync("Bonjour");

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

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

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

This file was deleted.

10 changes: 10 additions & 0 deletions examples/Authorization/Greeter.slice
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions examples/Authorization/GreeterAdmin.slice
Original file line number Diff line number Diff line change
@@ -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)
}
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 carries the name of the user and its administrative privilege.

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 setting 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 `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 `alice` authentication pipeline to change the greeting. The user `alice` 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
```
62 changes: 62 additions & 0 deletions examples/Authorization/Server/AesBearerAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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 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 identity token.
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)
{
// 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();

cryptoStream.FlushFinalBlock();
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
}
Loading