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 22 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
12 changes: 12 additions & 0 deletions examples/Authorization/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
}
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.Builder.MSBuild" Version="$(IceRpcBuilderVersion)" 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 SayGreeting 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 SayGreeting 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 SayGreeting 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.

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 identity
/// token.</summary>
public static class SessionFieldKey
public static class IdentityTokenFieldKey
{
public const RequestFieldKey Value = (RequestFieldKey)100;
}
31 changes: 18 additions & 13 deletions examples/Authorization/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
# 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 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 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 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 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 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

Expand All @@ -24,11 +27,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 `GetGreetingAsync` without an identity token and the server responds with a generic 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 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.

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 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.
57 changes: 57 additions & 0 deletions examples/Authorization/Server/AesAuthenticationBearer.cs
Original file line number Diff line number Diff line change
@@ -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<IIdentityFeature> DecodeAndValidateIdentityTokenAsync(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());
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> 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;
}
}
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
}
22 changes: 22 additions & 0 deletions examples/Authorization/Server/AuthenticationBearer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) ZeroC, Inc.

using System.Buffers;

namespace AuthorizationExample;

/// <summary>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.</summary>
public interface IAuthenticationBearer
{
/// <summary>Decodes and validates an binary identity token.</summary>
/// <param name="identityTokenBytes">The binary identity token.</param>
/// <returns>A task that provides the decoded identity token as an identity feature.</returns>
/// <exception cref="DispatchException">Thrown is the decoding or the validation failed.</exception>
Task<IIdentityFeature> DecodeAndValidateIdentityTokenAsync(ReadOnlySequence<byte> identityTokenBytes);

/// <summary>Encodes the fields of an identity token.</summary>
/// <param name="name">The user name</param>
/// <param name="isAdmin"><c>true</c> if the user has administrative privilege, <c>false</c> otherwise.</param>
/// <returns>The binary identity token.</returns>
ReadOnlyMemory<byte> EncodeIdentityToken(string name, bool isAdmin);
}
Loading