From 579204edb43e0d44f064cc5243bf14939f3f0895 Mon Sep 17 00:00:00 2001 From: vnugent Date: Mon, 28 Aug 2023 22:00:43 -0400 Subject: Data extensions updates --- .../src/Applications/ApplicationStore.cs | 229 +++++++++++---------- .../src/Applications/UserAppContext.cs | 2 + .../src/Applications/UserApplication.cs | 2 +- .../src/Tokens/ActiveToken.cs | 17 +- .../src/Tokens/IOAuth2TokenResult.cs | 21 +- .../src/Tokens/TokenStore.cs | 36 ++-- .../src/Endpoints/ApplicationEndpoint.cs | 13 +- .../src/OAuth2ClientApplications.csproj | 2 +- 8 files changed, 181 insertions(+), 141 deletions(-) diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs index da70a17..17db978 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs +++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs @@ -27,7 +27,6 @@ using System.Data; using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Collections.Generic; using Microsoft.EntityFrameworkCore; @@ -37,6 +36,8 @@ using VNLib.Utils.Memory; using VNLib.Plugins.Extensions.Data; using VNLib.Plugins.Essentials.Accounts; using VNLib.Plugins.Essentials.Oauth.Tokens; +using VNLib.Plugins.Extensions.Data.Abstractions; +using VNLib.Plugins.Extensions.Data.Extensions; namespace VNLib.Plugins.Essentials.Oauth.Applications { @@ -52,6 +53,7 @@ namespace VNLib.Plugins.Essentials.Oauth.Applications private readonly DbContextOptions ConextOptions; private readonly ITokenManager TokenStore; + /// /// Initializes a new data store /// uisng the specified EFCore object. @@ -64,8 +66,27 @@ namespace VNLib.Plugins.Essentials.Oauth.Applications SecretHashing = secretHashing; TokenStore = new TokenStore(conextOptions); } - - + + + /// + /// Generates a client application secret using the library + /// + /// The RNG secret + public static PrivateString GenerateSecret(int secretSize = SECRET_SIZE) => (PrivateString)RandomHash.GetRandomHex(secretSize).ToLower(null)!; + + /// + public override IDbContextHandle GetNewContext() => new UserAppContext(ConextOptions); + + /// + public override string GetNewRecordId() => RandomHash.GetRandomHex(CLIENT_ID_SIZE).ToLower(null); + + /// + public override void OnRecordUpdate(UserApplication newRecord, UserApplication currentRecord) + { + currentRecord.AppDescription = newRecord.AppDescription; + currentRecord.AppName = newRecord.AppName; + } + /// /// Updates the secret of an application, and if successful returns the new raw secret data /// @@ -123,6 +144,7 @@ namespace VNLib.Plugins.Essentials.Oauth.Applications public async Task VerifyAppAsync(string clientId, PrivateString secret) { UserApplication? app; + //Open new db context await using (UserAppContext Database = new(ConextOptions)) { @@ -136,14 +158,17 @@ namespace VNLib.Plugins.Essentials.Oauth.Applications //commit the transaction await Database.CommitTransactionAsync(); } + //make sure app exists if (string.IsNullOrWhiteSpace(app?.UserId) || !app.ClientId!.Equals(clientId, StringComparison.Ordinal)) { //Not found or not valid return null; } + //Convert the secret hash to a private string so it will be cleaned up using PrivateString secretHash = (PrivateString)app.SecretHash!; + //Verify the secret against the hash if (SecretHashing.Verify(secretHash, secret)) { @@ -151,134 +176,114 @@ namespace VNLib.Plugins.Essentials.Oauth.Applications //App was successfully verified return app; } + //Not found or not valid return null; } - /// - public override async Task DeleteAsync(params string[] specifiers) - { - //get app id to remove tokens from - string appId = specifiers[0]; - //Delete app from store - ERRNO result = await base.DeleteAsync(specifiers); - if(result) - { - //Delete active tokens - await TokenStore.RevokeTokensForAppAsync(appId, CancellationToken.None); - } - return result; - } - /// - /// Generates a client application secret using the library - /// - /// The RNG secret - public static PrivateString GenerateSecret() => (PrivateString)RandomHash.GetRandomHex(SECRET_SIZE).ToLower()!; /// /// Creates and initializes a new with a random clientid and /// secret that must be disposed /// /// The new record to create + /// /// The result of the operation - public override async Task CreateAsync(UserApplication record, CancellationToken cancellation = default) + public async Task CreateAppAsync(UserApplication record, CancellationToken cancellation = default) { record.RawSecret = GenerateSecret(); //Hash the secret using PrivateString secretHash = SecretHashing.Hash(record.RawSecret); - record.ClientId = GenerateClientID(); + record.ClientId = GetNewRecordId(); record.SecretHash = (string)secretHash; - //Wait for the rescord to be created before wiping the secret - return await base.CreateAsync(record, cancellation); + //Wait for the record to be created before wiping the secret + return await this.CreateAsync(record, cancellation); } + - /// - /// Generates a new client ID using the library - /// - /// The new client ID - public static string GenerateClientID() => RandomHash.GetRandomHex(CLIENT_ID_SIZE).ToLower(); - /// - public override string RecordIdBuilder => RandomHash.GetRandomHex(CLIENT_ID_SIZE).ToLower(); - /// - public override TransactionalDbContext NewContext() => new UserAppContext(ConextOptions); - /// - protected override IQueryable AddOrUpdateQueryBuilder(TransactionalDbContext context, UserApplication record) - { - UserAppContext ctx = (context as UserAppContext)!; - //get a single record by the id for the specific user - return from userApp in ctx.OAuthApps - where userApp.UserId == record.UserId - && userApp.Id == record.Id - select userApp; - } - /// - protected override void OnRecordUpdate(UserApplication newRecord, UserApplication currentRecord) - { - currentRecord.AppDescription = newRecord.AppDescription; - currentRecord.AppName = newRecord.AppName; - } - /// - protected override IQueryable GetCollectionQueryBuilder(TransactionalDbContext context, string specifier) - { - UserAppContext ctx = (context as UserAppContext)!; - //Get the user's applications based on their userid - return from userApp in ctx.OAuthApps - where userApp.UserId == specifier - orderby userApp.Created ascending - select new UserApplication - { - AppDescription = userApp.AppDescription, - Id = userApp.Id, - AppName = userApp.AppName, - ClientId = userApp.ClientId, - Created = userApp.Created, - Permissions = userApp.Permissions - }; - } - /// - protected override IQueryable GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints) - { - return GetCollectionQueryBuilder(context, constraints[0]); - } - /// - protected override IQueryable GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints) - { - string appId = constraints[0]; - string userId = constraints[1]; - UserAppContext ctx = (context as UserAppContext)!; - //Query to get a new single application with limit results output - return from userApp in ctx.OAuthApps - where userApp.UserId == userId - && userApp.Id == appId - select new UserApplication - { - AppDescription = userApp.AppDescription, - Id = userApp.Id, - AppName = userApp.AppName, - ClientId = userApp.ClientId, - Created = userApp.Created, - Permissions = userApp.Permissions, - Version = userApp.Version - }; - } - /// - protected override IQueryable GetSingleQueryBuilder(TransactionalDbContext context, UserApplication record) - { - //Use the query for single record with the other record's userid and record id - return GetSingleQueryBuilder(context, record.Id, record.UserId); - } - /// - protected override IQueryable UpdateQueryBuilder(TransactionalDbContext context, UserApplication record) - { - UserAppContext ctx = (context as UserAppContext)!; - return from userApp in ctx.OAuthApps - where userApp.UserId == record.UserId && userApp.Id == record.Id - select userApp; - } + public override IDbQueryLookup QueryTable { get; } = new ApplicationQueries(); - //DO NOT ALLOW PAGINATION YET - public override Task GetPageAsync(ICollection collection, int page, int limit, CancellationToken cancellation = default) + sealed class ApplicationQueries : IDbQueryLookup { - throw new NotSupportedException(); + /// + public IQueryable GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints) + { + //When only a single contraint is specified, we are getting all applications for a user + if (constraints.Length == 1) + { + string userId = constraints[0]; + + UserAppContext ctx = (context as UserAppContext)!; + //Get the user's applications based on their userid + return from userApp in ctx.OAuthApps + where userApp.UserId == userId + orderby userApp.Created ascending + select new UserApplication + { + AppDescription = userApp.AppDescription, + Id = userApp.Id, + AppName = userApp.AppName, + ClientId = userApp.ClientId, + Created = userApp.Created, + Permissions = userApp.Permissions + }; + } + //When two constraints are specified, we are getting a single application + else + { + string appId = constraints[0]; + string userId = constraints[1]; + + UserAppContext ctx = (context as UserAppContext)!; + //Query to get a new single application with limit results output + return from userApp in ctx.OAuthApps + where userApp.UserId == userId + && userApp.Id == appId + select new UserApplication + { + AppDescription = userApp.AppDescription, + Id = userApp.Id, + AppName = userApp.AppName, + ClientId = userApp.ClientId, + Created = userApp.Created, + Permissions = userApp.Permissions, + Version = userApp.Version + }; + } + } + + /// + public IQueryable GetSingleQueryBuilder(IDbContextHandle context, params string[] constraints) + { + string appId = constraints[0]; + string userId = constraints[1]; + UserAppContext ctx = (context as UserAppContext)!; + //Query to get a new single application with limit results output + return from userApp in ctx.OAuthApps + where userApp.UserId == userId + && userApp.Id == appId + select new UserApplication + { + AppDescription = userApp.AppDescription, + Id = userApp.Id, + AppName = userApp.AppName, + ClientId = userApp.ClientId, + Created = userApp.Created, + Permissions = userApp.Permissions, + Version = userApp.Version + }; + } + + /// + public IQueryable AddOrUpdateQueryBuilder(IDbContextHandle context, UserApplication record) + { + UserAppContext ctx = (context as UserAppContext)!; + //get a single record by the id for the specific user + return from userApp in ctx.OAuthApps + where userApp.UserId == record.UserId + && userApp.Id == record.Id + select userApp; + } + } } } \ No newline at end of file diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserAppContext.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserAppContext.cs index 26dfbe6..e4d98e6 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserAppContext.cs +++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserAppContext.cs @@ -32,7 +32,9 @@ namespace VNLib.Plugins.Essentials.Oauth.Applications public class UserAppContext : TransactionalDbContext { public DbSet OAuthApps { get; set; } + public DbSet OAuthTokens { get; set; } + #nullable disable public UserAppContext(DbContextOptions options) : base(options) { diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs index 9c2f543..37f4e77 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs +++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs @@ -42,7 +42,7 @@ using IndexAttribute = Microsoft.EntityFrameworkCore.IndexAttribute; namespace VNLib.Plugins.Essentials.Oauth.Applications { /// - /// + /// Represents an OAuth2 application for a user /// [Index(nameof(ClientId), IsUnique = true)] public class UserApplication : DbModelBase, IUserEntity, IJsonOnDeserialized diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs index 8e8fb5e..0cb7f6f 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs +++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Oauth @@ -28,13 +28,28 @@ using VNLib.Plugins.Extensions.Data; namespace VNLib.Plugins.Essentials.Oauth.Tokens { + /// + /// Represents a token record in the database + /// public class ActiveToken : DbModelBase { + /// public override string Id { get; set; } = string.Empty; + + /// public override DateTime Created { get; set; } + + /// public override DateTime LastModified { get; set; } + /// + /// A ID of the applicaiton this token was issued for + /// public string? ApplicationId { get; set; } + + /// + /// An optional OAuth2 refresh token, used for refreshing access tokens + /// public string? RefreshToken { get; set; } } } diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/IOAuth2TokenResult.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/IOAuth2TokenResult.cs index 0a4cc31..bd9ffce 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/IOAuth2TokenResult.cs +++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/IOAuth2TokenResult.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Oauth @@ -29,10 +29,29 @@ namespace VNLib.Plugins.Essentials.Oauth.Tokens /// public interface IOAuth2TokenResult { + /// + /// An optional token that can be used to identify the user + /// string? IdentityToken { get; } + + /// + /// The access token, used for authenticating requests + /// string? AccessToken { get; } + + /// + /// An optional OAuth2 refresh token, used for refreshing access tokens + /// string? RefreshToken { get; } + + /// + /// The type of token, usually "Bearer" + /// string? TokenType { get; } + + /// + /// The number of seconds until the access token expires + /// int ExpiresSeconds { get; } } } \ No newline at end of file diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/TokenStore.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/TokenStore.cs index f160a79..7b07f46 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/TokenStore.cs +++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/TokenStore.cs @@ -92,20 +92,11 @@ namespace VNLib.Plugins.Essentials.Oauth.Tokens Created = now, LastModified = now, }; + //Add token to store - ctx.OAuthTokens.Add(newToken); - //commit changes - ERRNO result = await ctx.SaveChangesAsync(cancellation); - if (result) - { - //End transaction - await ctx.CommitTransactionAsync(cancellation); - } - else - { - await ctx.RollbackTransctionAsync(cancellation); - } - return result; + ctx.Add(newToken); + + return await ctx.SaveAndCloseAsync(true, cancellation); } /// @@ -128,11 +119,11 @@ namespace VNLib.Plugins.Essentials.Oauth.Tokens return; } //delete token - ctx.OAuthTokens.Remove(at); + ctx.Remove(at); //Save changes - await ctx.SaveChangesAsync(cancellation); - await ctx.CommitTransactionAsync(cancellation); + await ctx.SaveAndCloseAsync(true, cancellation); } + /// /// Removes all token entires that were created before the specified time /// @@ -150,12 +141,12 @@ namespace VNLib.Plugins.Essentials.Oauth.Tokens .ToArrayAsync(cancellation); //delete token - ctx.OAuthTokens.RemoveRange(at); + ctx.RemoveRange(at); //Save changes - int count = await ctx.SaveChangesAsync(cancellation); - await ctx.CommitTransactionAsync(cancellation); + await ctx.SaveAndCloseAsync(true, cancellation); return at; } + /// public async Task RevokeTokensAsync(IReadOnlyCollection tokens, CancellationToken cancellation = default) { @@ -170,9 +161,9 @@ namespace VNLib.Plugins.Essentials.Oauth.Tokens //delete token ctx.OAuthTokens.RemoveRange(at); //Save changes - await ctx.SaveChangesAsync(cancellation); - await ctx.CommitTransactionAsync(cancellation); + await ctx.SaveAndCloseAsync(true, cancellation); } + /// async Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation) { @@ -190,8 +181,7 @@ namespace VNLib.Plugins.Essentials.Oauth.Tokens t.Created = DateTime.MinValue; } //Save changes - await ctx.SaveChangesAsync(cancellation); - await ctx.CommitTransactionAsync(cancellation); + await ctx.SaveAndCloseAsync(true, cancellation); } } } diff --git a/Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs b/Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs index 0f253fa..4f9d057 100644 --- a/Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs +++ b/Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs @@ -42,9 +42,9 @@ using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Extensions.Validation; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Sql; +using VNLib.Plugins.Extensions.Data.Extensions; using static VNLib.Plugins.Essentials.Statics; - namespace OAuth2ClientApplications.Endpoints { [ConfigurationName("applications")] @@ -295,10 +295,12 @@ namespace OAuth2ClientApplications.Endpoints //Parse permission string an re-build it to clean it up newApp.Permissions = ParsePermissions(newApp.Permissions); + //Set user-id newApp.UserId = entity.Session.UserID; + //Create the new application - if (!await Applications.CreateAsync(newApp)) + if (!await Applications.CreateAppAsync(newApp)) { webm.Result = "The was an issue creating your application"; entity.CloseResponse(webm); @@ -344,18 +346,25 @@ namespace OAuth2ClientApplications.Endpoints { [JsonPropertyName("name")] public string? ApplicationName { get; set; } + [JsonPropertyName("description")] public string? Description { get; set; } + [JsonPropertyName("client_id")] public string? ClientID { get; set; } + [JsonPropertyName("raw_secret")] public string? RawSecret { get; set; } + [JsonPropertyName("Id")] public string? ApplicationID { get; set; } + [JsonPropertyName("permissions")] public string? Permissions { get; set; } + [JsonPropertyName("Created")] public string? CreatedTime { get; set; } + [JsonPropertyName("LastModified")] public string? LastUpdatedTime { get; set; } } diff --git a/Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj b/Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj index c748419..91ef465 100644 --- a/Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj +++ b/Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj @@ -42,7 +42,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + -- cgit