From 792c254a3c0e21edb3bdcf1cfd16e5a910a71082 Mon Sep 17 00:00:00 2001 From: paragon Date: Sun, 24 May 2026 13:38:44 -0400 Subject: [PATCH] more samples --- .../Constants/Protectors.cs | 13 ++ .../ApplicationApiSampleController.cs | 2 +- .../CommentsApiSampleController.cs | 60 +++++++++ .../EmailApiRelaySampleController.cs | 42 +++++++ .../Controllers/EmailApiSampleController.cs | 9 +- .../Controllers/EntityApiSampleController.cs | 100 +++++++++++++++ .../Controllers/FilesApiSampleController.cs | 70 +++++++++++ .../Controllers/KeysApiSampleController.cs | 2 +- .../Controllers/TokensApiSampleController.cs | 2 +- .../UsersApiRelaySampleController.cs | 101 +++++++++++++++ .../Controllers/UsersApiSampleController.cs | 119 ++++++++++++++++++ .../Models/ProtectorDefinitions.cs | 26 ++++ .../Program.cs | 2 + .../TriunniaLabs.Arch.Sample.Server.csproj | 4 + 14 files changed, 546 insertions(+), 6 deletions(-) create mode 100644 src/TriunniaLabs.Arch.Sample.Server/Constants/Protectors.cs create mode 100644 src/TriunniaLabs.Arch.Sample.Server/Controllers/CommentsApiSampleController.cs create mode 100644 src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiRelaySampleController.cs create mode 100644 src/TriunniaLabs.Arch.Sample.Server/Controllers/EntityApiSampleController.cs create mode 100644 src/TriunniaLabs.Arch.Sample.Server/Controllers/FilesApiSampleController.cs create mode 100644 src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiRelaySampleController.cs create mode 100644 src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiSampleController.cs create mode 100644 src/TriunniaLabs.Arch.Sample.Server/Models/ProtectorDefinitions.cs diff --git a/src/TriunniaLabs.Arch.Sample.Server/Constants/Protectors.cs b/src/TriunniaLabs.Arch.Sample.Server/Constants/Protectors.cs new file mode 100644 index 0000000..a76a1db --- /dev/null +++ b/src/TriunniaLabs.Arch.Sample.Server/Constants/Protectors.cs @@ -0,0 +1,13 @@ +namespace TriunniaLabs.Arch.Sample.Server +{ + /// + /// Defines service key constants for protectors. + /// + public static class Protectors + { + /// + /// Gets the service key for protected byte array types. + /// + public const string ProtectedByteArrayServiceKey = nameof(ProtectedByteArrayServiceKey); + } +} diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/ApplicationApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/ApplicationApiSampleController.cs index 746550b..debb643 100644 --- a/src/TriunniaLabs.Arch.Sample.Server/Controllers/ApplicationApiSampleController.cs +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/ApplicationApiSampleController.cs @@ -41,7 +41,7 @@ namespace TriunniaLabs.Arch.Sample.Server } /// - /// A sample method that shows how to work with the Keys Api. + /// A sample method that shows how to work with the Application Api. /// internal async Task SampleAsync() { diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/CommentsApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/CommentsApiSampleController.cs new file mode 100644 index 0000000..c158fae --- /dev/null +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/CommentsApiSampleController.cs @@ -0,0 +1,60 @@ +using TriunniaLabs.Arch.Api.Comments.Client; +using TriunniaLabs.Arch.Microservice; +using TriunniaLabs.Memory.Protected; + +namespace TriunniaLabs.Arch.Sample.Server +{ + /// + /// A controller that shows how to work with the Comments Api. + /// + public class CommentsApiSampleController : ArchControllerBase + { + // Services + private readonly ICommentsApiClient _commentsApiClient; + + /// + /// Constructor. + /// + /// Reference to the microservice context. + /// Reference to the Comments Api client. + public CommentsApiSampleController(IMicroserviceContext microserviceContext, ICommentsApiClient commentsApiClient) : base(microserviceContext) + { + _commentsApiClient = commentsApiClient ?? throw new ArgumentNullException(nameof(commentsApiClient)); + } + + /// + /// A sample method that shows how to work with the Comments Api. + /// + internal async Task SampleAsync() + { + // Comments are posted within a context. The context can be anything, like an article, forum category, another comment (in the case of a reply), etc. + var contextId = Guid.NewGuid().ToString().AsProtected(); // Id of the context to which the comment applies. + var contextType = 1; // Developer-defined type for the context + var commentType = 1; // Developer-defined type for the comment + var commentStatus = 1; // Developer-defined type for the comment's status + var commentText = "This is my comment".AsProtected(); // The encrypted text of the comment + var authorUserId = Guid.NewGuid().ToString().AsProtected(); // Id of the user who wrote the comment + var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the entity + var commentId = Guid.NewGuid(); + + // Create a new comment in the system + await _commentsApiClient.CreateCommentAsync(contextId, contextType, commentType, commentStatus, commentText, authorUserId, encryptionKey); + + // Get the specified comment + var comment = await _commentsApiClient.GetCommentAsync(contextId, contextType, commentId, encryptionKey); + + // Get all comments within a context + var comments = await _commentsApiClient.GetCommentsAsync(contextId, contextType, encryptionKey); + + // Update an existing comment + comment.CommentText = "This is my updated comment".AsProtected(); + await _commentsApiClient.UpdateCommentAsync(comment, encryptionKey); + + // Delete the specified comment from the context + await _commentsApiClient.DeleteCommentAsync(contextId, contextType, comment.CommentId); + + // Delete the entire comment thread + await _commentsApiClient.DeleteCommentsAsync(contextId, contextType); + } + } +} diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiRelaySampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiRelaySampleController.cs new file mode 100644 index 0000000..06cc95b --- /dev/null +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiRelaySampleController.cs @@ -0,0 +1,42 @@ +using TriunniaLabs.Arch.Microservice; +using TriunniaLabs.Arch.Relay.Email.Blazor; + +namespace TriunniaLabs.Arch.Sample.Server +{ + /// + /// A controller that shows how to work with the Email Api Relay Client. + /// This controller demonstrates using the Relay client from a Blazor page. + /// Relay clients contact their corresponding server endpoints that are brought in via TriunniaLabs.Arch.Relay.Email.Server. + /// Those server endpoints then contact the normal Email Api Client. + /// + public class EmailApiRelaySampleController : ArchControllerBase + { + // Services + private readonly IEmailApiRelayClient _emailApiRelayClient; + + /// + /// Constructor. + /// + /// Reference to the microservice context. + /// Reference to the Email Api Relay client. + public EmailApiRelaySampleController(IMicroserviceContext microserviceContext, IEmailApiRelayClient emailApiRelayClient) : base(microserviceContext) + { + _emailApiRelayClient = emailApiRelayClient ?? throw new ArgumentNullException(nameof(emailApiRelayClient)); + } + + /// + /// A sample method that shows how to work with the Email Api. + /// + internal async Task SampleAsync() + { + var verifyToken = Guid.NewGuid(); // A verify token from the verification email + + // Verifies the user's email address, given the verify token + await _emailApiRelayClient.VerifyEmailAddressAsync(verifyToken); + + // Reports abuse of the email address, given the verify token. + // This would be called by the actual owner of the email address, who did not register the email in our system. + await _emailApiRelayClient.ReportAbuseAsync(verifyToken); + } + } +} diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiSampleController.cs index b3fdec3..c4cdca2 100644 --- a/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiSampleController.cs +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EmailApiSampleController.cs @@ -5,7 +5,7 @@ using TriunniaLabs.Memory.Protected; namespace TriunniaLabs.Arch.Sample.Server { /// - /// A controller that shows how to work the Email Api. + /// A controller that shows how to work with the Email Api. /// public class EmailApiSampleController : ArchControllerBase { @@ -16,7 +16,7 @@ namespace TriunniaLabs.Arch.Sample.Server /// Constructor. /// /// Reference to the microservice context. - /// Reference to the Tokens Api client. + /// Reference to the Email Api client. public EmailApiSampleController(IMicroserviceContext microserviceContext, IEmailApiClient emailApiClient) : base(microserviceContext) { _emailApiClient = emailApiClient ?? throw new ArgumentNullException(nameof(emailApiClient)); @@ -45,9 +45,12 @@ namespace TriunniaLabs.Arch.Sample.Server var emailResponse = await _emailApiClient.GetEmailAddressAsync(emailAddress.Hash, encryptionKey); // Get by hash emailResponse = await _emailApiClient.GetEmailAddressAsync(emailId, encryptionKey); // Get by Id - // Email verification procedure. This will send an email to the user with the verify token. Record its verification or abuse with the other methods. + // Email verification procedure. This will send an email to the user with the verify token. await _emailApiClient.InitEmailVerifyAsync(emailId, encryptionKey, ipAddress); var result = await _emailApiClient.CommitEmailVerifyAsync(verifyToken, ipAddress); + + // Reports abuse of the email address, given the verify token. + // This would be called by the actual owner of the email address, who did not register the email in our system. result = await _emailApiClient.ReportAbuseAsync(verifyToken); // When using Postmark, receive their webhooks and forward to the Email Api with this call. diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/EntityApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EntityApiSampleController.cs new file mode 100644 index 0000000..9d76b92 --- /dev/null +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/EntityApiSampleController.cs @@ -0,0 +1,100 @@ +using System.Text; +using System.Text.Json; +using TriunniaLabs.Arch.Api.Entity.Client; +using TriunniaLabs.Arch.Microservice; +using TriunniaLabs.Memory.Protected; + +namespace TriunniaLabs.Arch.Sample.Server +{ + /// + /// A controller that shows how to work with the Entity Api. + /// + public class EntityApiSampleController : ArchControllerBase + { + // Services + private readonly IEntityApiClient _entityApiClient; + private readonly IProtector _byteArrayProtector; + + /// + /// Constructor. + /// + /// Reference to the microservice context. + /// Reference to the Entity Api client. + /// A custom protector for protecting byte[] data types. + public EntityApiSampleController(IMicroserviceContext microserviceContext, IEntityApiClient entityApiClient, + [FromKeyedServices(Protectors.ProtectedByteArrayServiceKey)] IProtector byteArrayProtector) + : base(microserviceContext) + { + _entityApiClient = entityApiClient ?? throw new ArgumentNullException(nameof(entityApiClient)); + _byteArrayProtector = byteArrayProtector ?? throw new ArgumentNullException(nameof(byteArrayProtector)); + } + + /// + /// A sample method that shows how to work with the Entity Api. + /// + internal async Task SampleAsync() + { + var entity = new SampleEntity { Id = Guid.NewGuid(), Name = "MyEntity", Value = 1234 }; + var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the entity + var entityId = entity.Id.AsProtected(); + var entityType = entity.GetType().FullName!.AsProtected(); + + // Serialize the entity to bytes if you want to do your own serialization (and protection) + var serialized = JsonSerializer.Serialize(entity); + var bytes = Encoding.UTF8.GetBytes(serialized); + + + // Create a new entity in the system with the specified id + await _entityApiClient.CreateEntityAsync(entityId, entity, _byteArrayProtector, encryptionKey); + await _entityApiClient.CreateEntityAsync(entityId, entityType, bytes, encryptionKey); + await _entityApiClient.CreateEntityAsync(entityId, bytes, encryptionKey); + + // Update an existing entity in the system + entity.Value = 5678; + await _entityApiClient.UpdateEntityAsync(entityId, entity, _byteArrayProtector, encryptionKey); + await _entityApiClient.UpdateEntityAsync(entityId, entityType, bytes, encryptionKey); + await _entityApiClient.UpdateEntityAsync(entityId, bytes, encryptionKey); + + + // Get the entity and metadata, then deserialize + var entityResponse = await _entityApiClient.GetEntityAsync(entityId, entityType, encryptionKey); + entityResponse = await _entityApiClient.GetEntityAsync(entityId, encryptionKey); + entity = JsonSerializer.Deserialize(entityResponse!.Data.GetDecryptedValue()); + + // Get the deserialized entity immediately + entity = await _entityApiClient.GetEntityDeserializedAsync(entityId, encryptionKey, _byteArrayProtector); + + + // The Entity Api records and versions all changes to an entity. Get the history of all changes and their metadata with this call. + var history = await _entityApiClient.GetEntityHistoryAsync(entityId, entityType, encryptionKey); + history = await _entityApiClient.GetEntityHistoryAsync(entityId, encryptionKey); + + // Or just get the history of the entities directly without any metadata + var deserializedHistory = await _entityApiClient.GetEntityHistoryDeserializedAsync(entityId, encryptionKey, _byteArrayProtector); + + // Gets the archived version of the specified entity and its metadata + entityResponse = await _entityApiClient.GetEntityArchivedAsync(entityId, entityType, encryptionKey); + entityResponse = await _entityApiClient.GetEntityArchivedAsync(entityId, encryptionKey); + + // Or just get the archived version without any metadata + entity = await _entityApiClient.GetEntityArchivedDeserializedAsync(entityId, encryptionKey, _byteArrayProtector); + + + // Mark the entity has deleted without actually deleting it + await _entityApiClient.SoftDeleteEntityAsync(entityId, entityType); + await _entityApiClient.SoftDeleteEntityAsync(entityId); + + + // Purge the entity from the system. This is a hard delete. It also deletes all archives and histories. + await _entityApiClient.PurgeEntityAsync(entityId, entityType); + await _entityApiClient.PurgeEntityAsync(entityId); + } + + private class SampleEntity + { + public Guid Id { get; set; } + public string Name { get; set; } + public int Value { get; set; } + } + } +} diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/FilesApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/FilesApiSampleController.cs new file mode 100644 index 0000000..b4e363b --- /dev/null +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/FilesApiSampleController.cs @@ -0,0 +1,70 @@ +using TriunniaLabs.Arch.Api.Files.Client; +using TriunniaLabs.Arch.Microservice; +using TriunniaLabs.Memory.Protected; + +namespace TriunniaLabs.Arch.Sample.Server +{ + /// + /// A controller that shows how to work with the Files Api. + /// + public class FilesApiSampleController : ArchControllerBase + { + // Services + private readonly IFilesApiClient _filesApiClient; + + /// + /// Constructor. + /// + /// Reference to the microservice context. + /// Reference to the Files Api client. + public FilesApiSampleController(IMicroserviceContext microserviceContext, IFilesApiClient filesApiClient) : base(microserviceContext) + { + _filesApiClient = filesApiClient ?? throw new ArgumentNullException(nameof(filesApiClient)); + } + + /// + /// A sample method that shows how to work with the Files Api. + /// + internal async Task SampleAsync() + { + var applicationId = MicroserviceContext.Settings.Application.ApplicationId; // Id of this application + var folder = "MySampleFolder".AsProtected(); // Files are stored in folders + var filename = "MySampleFile".AsProtected(); // The name of the file + var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the file + var fileContents = new byte[] { 1, 2, 3, 4 }; + using var fileStream = new MemoryStream(fileContents); + + // Upload a file; overwrite if it exists Protect the stream to upload it. + var protectedStream = new Protected(fileStream, encryptionKey.GetDecryptedValue()); + await _filesApiClient.UploadFileAsync(applicationId, folder, filename, protectedStream, encryptionKey); + + // Download a file via streaming + protectedStream = await _filesApiClient.DownloadFileAsync(applicationId, folder, filename, encryptionKey); + using (var stream = protectedStream.GetDecryptedValueAsStream()) // Decrypt the stream for writing, if you want + { + // Save the stream to a file + } + + // Download a file and get its bytes directly. Does not stream. Use for small files only. + var protectedFileBytes = await _filesApiClient.DownloadSmallFileAsync(applicationId, folder, filename, encryptionKey); + + // Check if the file exists + var exists = await _filesApiClient.GetFileExistsAsync(applicationId, folder, filename); + + // Gets the list of all files stored in the folder + var filenames = await _filesApiClient.GetFilesAsync(applicationId, folder); + + // Get the size of the file, in bytes + var size = await _filesApiClient.GetFileSizeAsync(applicationId, folder, filename); + + // Get the list of all folders in the application + var folders = await _filesApiClient.GetFoldersAsync(applicationId); + + // Delete the specified file from the server + await _filesApiClient.DeleteFileAsync(applicationId, folder, filename); + + // Delete the specified folder and all files within from the server, for the application + await _filesApiClient.DeleteFolderAsync(applicationId); + } + } +} diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/KeysApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/KeysApiSampleController.cs index 122be20..51cf180 100644 --- a/src/TriunniaLabs.Arch.Sample.Server/Controllers/KeysApiSampleController.cs +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/KeysApiSampleController.cs @@ -5,7 +5,7 @@ using TriunniaLabs.Memory.Protected; namespace TriunniaLabs.Arch.Sample.Server { /// - /// A controller that shows how to work the Keys Api. + /// A controller that shows how to work with the Keys Api. /// public class KeysApiSampleController : ArchControllerBase { diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/TokensApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/TokensApiSampleController.cs index 434a8ca..9d8255b 100644 --- a/src/TriunniaLabs.Arch.Sample.Server/Controllers/TokensApiSampleController.cs +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/TokensApiSampleController.cs @@ -5,7 +5,7 @@ using TriunniaLabs.Memory.Protected; namespace TriunniaLabs.Arch.Sample.Server { /// - /// A controller that shows how to work the Tokens Api. + /// A controller that shows how to work with the Tokens Api. /// public class TokensApiSampleController : ArchControllerBase { diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiRelaySampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiRelaySampleController.cs new file mode 100644 index 0000000..9d15aa6 --- /dev/null +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiRelaySampleController.cs @@ -0,0 +1,101 @@ +using TriunniaLabs.Arch.Microservice; +using TriunniaLabs.Arch.Microservice.Client; +using TriunniaLabs.Arch.Relay.Users.Blazor; +using TriunniaLabs.Memory.Protected; +using TriunniaLabs.Blazor.ZeroKnowledge; + +namespace TriunniaLabs.Arch.Sample.Server +{ + /// + /// A controller that shows how to work with the Users Api Relay Client. + /// This controller demonstrates using the Relay client from a Blazor page. + /// Relay clients contact their corresponding server endpoints that are brought in via TriunniaLabs.Arch.Relay.Users.Server. + /// Those server endpoints then contact the normal Users Api Client. + /// + public class UsersApiRelaySampleController : ArchControllerBase + { + // Services + private readonly IUsersApiRelayClient _usersApiRelayClient; + private readonly IZeroKnowledgeService _zeroKnowledgeService; + + /// + /// Constructor. + /// + /// Reference to the microservice context. + /// Reference to the Users Api Relay client. + /// Client-side logic for the zero-knowledge protocol in Blazor. + public UsersApiRelaySampleController(IMicroserviceContext microserviceContext, IUsersApiRelayClient usersApiRelayClient, IZeroKnowledgeService zeroKnowledgeService) : base(microserviceContext) + { + _usersApiRelayClient = usersApiRelayClient ?? throw new ArgumentNullException(nameof(usersApiRelayClient)); + _zeroKnowledgeService = zeroKnowledgeService ?? throw new ArgumentNullException(nameof(zeroKnowledgeService)); + } + + /// + /// A sample method that shows how to work with the Users Api. + /// + internal async Task SampleAsync() + { + var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the email address + var emailAddress = "sample@email.address".AsProtected(encryptionKey.GetDecryptedValue()); // Per-row encrypted email address + var checkEmailAddress = "sample@email.address".AsRelayProtected(); // Relay encrypted email address (lower security) + var ipAddress = IpAddress.AsProtected(); // User's ip address + var sessionToken = Guid.NewGuid().ToString().AsProtected(); // Authenticated session token + var resetToken = Guid.NewGuid().AsProtected(); // Password reset token + var deleteToken = Guid.NewGuid().AsProtected(); // Delete account token + + + // Construct origin graph -- the mechanism that verifies the user's password without knowing it + var originGraph = await _zeroKnowledgeService.GenerateOriginGraphAsync("password"); + + // Generate the key file, then save keyFileContents to a file. The file itself is password-protected via metadataPassword + var cycle = await _zeroKnowledgeService.GenerateHamiltonianCycleAsync(originGraph, "password"); + var keyFileContents = await _zeroKnowledgeService.SerializeZeroKnowledgeMetadataAsync(originGraph, cycle, "metadataPassword"); + + // Read origin graph from key file + var (origin, cycle2) = await _zeroKnowledgeService.DeserializeZeroKnowledgeMetadataAsync(keyFileContents, "metadataPassword", "password"); + + // Set http headers on the client for authentication + _usersApiRelayClient.SetAuthorizationHeader("MySampleAuthScheme", sessionToken); // Authentication scheme must match appsettings.json + + + // Register a new user in the system + sessionToken = await _usersApiRelayClient.RegisterAsync(emailAddress, originGraph); + + // Sign in to the system. It requires passing up to 12 challenges. + // The server knows the user's origin graph. It is asking the user to validate it by issuing challenges that the user answers. + // The Relay client does this procedure for you and returns the session token if the user authenticates successfully. + sessionToken = await _usersApiRelayClient.LoginAsync(emailAddress, originGraph, cycle); + + + // End the authenticated session; log the user out of the system + await _usersApiRelayClient.LogoutAsync(); + + + // Check if the email address already exists in the system + var exists = await _usersApiRelayClient.CheckEmailAsync(checkEmailAddress); + + // Get the user's email address and metadata + var emailResponse = await _usersApiRelayClient.GetEmailAsync(); + + // Update the user's email address + await _usersApiRelayClient.UpdateEmailAsync("updated.sample@email.address".AsProtected()); + + // Send a verification email for the user + await _usersApiRelayClient.SendVerificationEmailAsync(); + + + // Validate a user's session token. Used to verify that a user is in fact signed in + var validated = _usersApiRelayClient.ValidateAsync(sessionToken); + + + // Password reset procedure + await _usersApiRelayClient.InitiatePasswordResetAsync(emailAddress); // Sends a verification email with resetToken + await _usersApiRelayClient.ValidatePasswordResetTokenAsync(emailAddress, resetToken); + await _usersApiRelayClient.CommitPasswordResetAsync(emailAddress, resetToken, originGraph); // Generate a new origin graph based on a new password for the user + + // Delete account procedure + await _usersApiRelayClient.InitiateDeleteAccountAsync(); // Sends a confirmation email with deleteToken + await _usersApiRelayClient.CommitDeleteAccountAsync(deleteToken); + } + } +} diff --git a/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiSampleController.cs b/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiSampleController.cs new file mode 100644 index 0000000..3159610 --- /dev/null +++ b/src/TriunniaLabs.Arch.Sample.Server/Controllers/UsersApiSampleController.cs @@ -0,0 +1,119 @@ +using TriunniaLabs.Arch.Api.Users.Client; +using TriunniaLabs.Arch.Microservice; +using TriunniaLabs.Arch.Microservice.Client; +using TriunniaLabs.Memory.Protected; +using TriunniaLabs.ZeroKnowledge.Client; + +namespace TriunniaLabs.Arch.Sample.Server +{ + /// + /// A controller that shows how to work with the Users Api. + /// + public class UsersApiSampleController : ArchControllerBase + { + // Services + private readonly IUsersApiClient _usersApiClient; + private readonly IZeroKnowledgeService _zeroKnowledgeService; + + /// + /// Constructor. + /// + /// Reference to the microservice context. + /// Reference to the Users Api client. + /// Client-side logic for the zero-knowledge protocol. + public UsersApiSampleController(IMicroserviceContext microserviceContext, IUsersApiClient usersApiClient, IZeroKnowledgeService zeroKnowledgeService) : base(microserviceContext) + { + _usersApiClient = usersApiClient ?? throw new ArgumentNullException(nameof(usersApiClient)); + _zeroKnowledgeService = zeroKnowledgeService ?? throw new ArgumentNullException(nameof(zeroKnowledgeService)); + } + + /// + /// A sample method that shows how to work with the Users Api. + /// + internal async Task SampleAsync() + { + var encryptionKey = "SampleEncryptionKey".AsProtected(); // Per-row encryption key for the email address + var emailAddress = "sample@email.address".AsProtected(encryptionKey.GetDecryptedValue()); // Per-row encrypted email address + var checkEmailAddress = "sample@email.address".AsRelayProtected(); // Relay encrypted email address (lower security) + var ipAddress = IpAddress.AsProtected(); // User's ip address + var sessionToken = Guid.NewGuid().ToString().AsProtected(); // Authenticated session token + var resetToken = Guid.NewGuid().AsProtected(); // Password reset token + var deleteToken = Guid.NewGuid().AsProtected(); // Delete account token + + + // Construct origin graph -- the mechanism that verifies the user's password without knowing it + var originGraph = _zeroKnowledgeService.GenerateOriginGraph("password"); + var serializedOrigin = _zeroKnowledgeService.SerializeOriginGraph(originGraph); + var protectedOrigin = new Protected(serializedOrigin); + + // Generate the key file, then save keyFileContents to a file. The file itself is password-protected via metadataPassword + var cycle = _zeroKnowledgeService.GenerateHamiltonianCycle(originGraph, "password"); + var keyFileContents = _zeroKnowledgeService.SerializeZeroKnowledgeMetadata(originGraph, cycle, "metadataPassword"); + + // Read origin graph from key file + _zeroKnowledgeService.DeserializeZeroKnowledgeMetadata(keyFileContents, "metadataPassword", "password", out originGraph!, out cycle!); + + // Set http headers on the client for authentication + _usersApiClient.SetAuthorizationHeader(sessionToken); + _usersApiClient.SetIpAddressOriginHeader(ipAddress); + + + // Register a new user in the system + sessionToken = await _usersApiClient.RegisterAsync(emailAddress, protectedOrigin); + + // Sign in to the system. It requires passing up to 12 challenges. + // The server knows the user's origin graph. It is asking the user to validate it by issuing challenges that the user answers. + // Challenges have their own session token. Not to be confused with the user's session token. + await _usersApiClient.InitiateLoginAsync(emailAddress); + for (int i = 0; i < 12; i++) + { + // Create the challenge + var challengeResponse = await _usersApiClient.CreateChallengeAsync(emailAddress); + var challenge = new ZeroKnowledge.Client.ChallengeResponse { Seed = challengeResponse.Seed, SessionToken = challengeResponse.SessionToken }; + + // Answer the challenge + var answer = _zeroKnowledgeService.AnswerChallenge(challenge, originGraph, cycle); + sessionToken = await _usersApiClient.EvaluateAnswerAsync(emailAddress, challenge.SessionToken, answer.Isomorphism.ToArray(), answer.Cycle); + if (sessionToken is not null && !sessionToken.IsEmpty) + { + // Session token returned -- User is authenticated + } + } + // Authentication fails if the loop is finished after 12 tries with no session token + + // End the authenticated session; log the user out of the system + await _usersApiClient.LogoutAsync(); + + + // Check if the email address already exists in the system + var exists = await _usersApiClient.CheckEmailAsync(checkEmailAddress); + + // Get the user's email address and metadata + var emailResponse = await _usersApiClient.GetEmailAsync(); + + // Update the user's email address + await _usersApiClient.UpdateEmailAsync("updated.sample@email.address".AsProtected()); + + // Send a verification email for the user + await _usersApiClient.SendVerificationEmailAsync(); + + + // Get user metadata + var user = await _usersApiClient.GetUserAsync(); + + + // Validate a user's session token. Used to verify that a user is in fact signed in + var userId = _usersApiClient.ValidateSessionAsync(sessionToken, ipAddress); + + + // Password reset procedure + await _usersApiClient.InitiatePasswordResetAsync(emailAddress); // Sends a verification email with resetToken + await _usersApiClient.ValidatePasswordResetTokenAsync(emailAddress, resetToken); + await _usersApiClient.CommitPasswordResetAsync(emailAddress, resetToken, protectedOrigin); // Generate a new origin graph based on a new password for the user + + // Delete account procedure + await _usersApiClient.InitiateDeleteAccountAsync(); // Sends a confirmation email with deleteToken + await _usersApiClient.CommitDeleteAccountAsync(deleteToken); + } + } +} diff --git a/src/TriunniaLabs.Arch.Sample.Server/Models/ProtectorDefinitions.cs b/src/TriunniaLabs.Arch.Sample.Server/Models/ProtectorDefinitions.cs new file mode 100644 index 0000000..1eb1425 --- /dev/null +++ b/src/TriunniaLabs.Arch.Sample.Server/Models/ProtectorDefinitions.cs @@ -0,0 +1,26 @@ +using TriunniaLabs.Memory.Protected; + +namespace TriunniaLabs.Arch.Sample.Server +{ + /// + /// Defines the protectors used by the application. + /// + public class ProtectorDefinitions + { + /// + /// Defines the protector for byte arrays. + /// + /// + /// In Program.cs, the call to AddTriunniaLabsMicroservice() will internally call AddTriunniaLabsMemoryProtection(), + /// which will scan types in the assemblies for any protector registrations such as this one and automatically create protectors + /// for them. The protector matching this definition can be obtained by injecting + /// [FromKeyedServices(Protectors.ProtectedByteArrayServiceKey)] IProtector. + /// + [HashAlgorithm(HmacSha512HashAlgorithm.ServiceKey)] + [EncryptionAlgorithm(AesEncryptionAlgorithm.ServiceKey, Order = 1)] + [EncryptionAlgorithm(GZipCompressionAlgorithm.ServiceKey, Order = 2)] + [StringConverter(Base65536StringConverter.ServiceKey)] + [Protector(Protectors.ProtectedByteArrayServiceKey)] + public Protected ByteArrayProtector { get; set; } = Protected.Empty; + } +} diff --git a/src/TriunniaLabs.Arch.Sample.Server/Program.cs b/src/TriunniaLabs.Arch.Sample.Server/Program.cs index dabd878..c0cc976 100644 --- a/src/TriunniaLabs.Arch.Sample.Server/Program.cs +++ b/src/TriunniaLabs.Arch.Sample.Server/Program.cs @@ -12,6 +12,7 @@ using TriunniaLabs.Arch.Relay.Email.Server; using TriunniaLabs.Arch.Relay.Users.Server; using TriunniaLabs.Arch.Sample.Server; using TriunniaLabs.Memory.Protected; +using TriunniaLabs.ZeroKnowledge.Client; // Start the ASP.NET application builder var builder = WebApplication.CreateBuilder(args); @@ -27,6 +28,7 @@ builder.Services.AddTriunniaLabsUsersApi(builder.Configuration, builder.Services builder.Services.AddTriunniaLabsCommentsApi(builder.Configuration, builder.Services.AddStandardMessagePackFormatters); builder.Services.AddTriunniaLabsEntityApi(builder.Configuration, builder.Services.AddStandardMessagePackFormatters); builder.Services.AddTriunniaLabsFilesApi(builder.Configuration, builder.Services.AddStandardMessagePackFormatters); +builder.Services.AddZeroKnowledge(); // Configuration builder.Services.AddRouting(options => options.LowercaseUrls = true); diff --git a/src/TriunniaLabs.Arch.Sample.Server/TriunniaLabs.Arch.Sample.Server.csproj b/src/TriunniaLabs.Arch.Sample.Server/TriunniaLabs.Arch.Sample.Server.csproj index d4cc6d2..b8ac45e 100644 --- a/src/TriunniaLabs.Arch.Sample.Server/TriunniaLabs.Arch.Sample.Server.csproj +++ b/src/TriunniaLabs.Arch.Sample.Server/TriunniaLabs.Arch.Sample.Server.csproj @@ -15,8 +15,12 @@ + + + +