From e578b05e736a8be07bbbe81bb53c0102c307e2d1 Mon Sep 17 00:00:00 2001 From: vman Date: Wed, 30 Nov 2022 14:58:49 -0500 Subject: Project cleanup + analyzer updates --- .../Applications/ApplicationStore.cs | 284 +++++++++++++++++++++ .../Applications/Applications.cs | 284 --------------------- .../Applications/UserAppContext.cs | 2 +- .../Tokens/ActiveToken.cs | 2 +- .../VNLib.Plugins.Essentials.Oauth.csproj | 39 ++- 5 files changed, 305 insertions(+), 306 deletions(-) create mode 100644 Libs/VNLib.Plugins.Essentials.Oauth/Applications/ApplicationStore.cs delete mode 100644 Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs (limited to 'Libs/VNLib.Plugins.Essentials.Oauth') diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/Applications/ApplicationStore.cs b/Libs/VNLib.Plugins.Essentials.Oauth/Applications/ApplicationStore.cs new file mode 100644 index 0000000..2fe44ef --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Oauth/Applications/ApplicationStore.cs @@ -0,0 +1,284 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Oauth +* File: Applications.cs +* +* Applications.cs is part of VNLib.Plugins.Essentials.Oauth which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Oauth is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Plugins.Essentials.Oauth is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Essentials.Oauth. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Data; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Hashing; +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Plugins.Extensions.Data; +using VNLib.Plugins.Essentials.Accounts; +using VNLib.Plugins.Essentials.Oauth.Tokens; + +namespace VNLib.Plugins.Essentials.Oauth.Applications +{ + /// + /// A DbStore for s for OAuth2 client applications + /// + public sealed partial class ApplicationStore : DbStore + { + public const int SECRET_SIZE = 32; + public const int CLIENT_ID_SIZE = 16; + + private readonly PasswordHashing SecretHashing; + private readonly DbContextOptions ConextOptions; + private readonly ITokenManager TokenStore; + + /// + /// Initializes a new data store + /// uisng the specified EFCore object. + /// + /// EFCore context options for connecting to a remote data-store + /// A structure for hashing client secrets + public ApplicationStore(DbContextOptions conextOptions, PasswordHashing secretHashing) + { + this.ConextOptions = conextOptions; + this.SecretHashing = secretHashing; + this.TokenStore = new TokenStore(conextOptions); + } + + + /// + /// Updates the secret of an application, and if successful returns the new raw secret data + /// + /// The user-id of that owns the application + /// The id of the application to update + /// A task that resolves to the raw secret that was used to generate the hash, or null if the operation failed + public async Task UpdateSecretAsync(string userId, string appId) + { + /* + * Delete open apps first, incase there are any issues, worse case + * the user's will have to re-authenticate. + * + * If we delete tokens after update, the user wont see the new + * secret and may lose access to the updated app, not a big deal + * but avoidable. + */ + await TokenStore.RevokeTokensForAppAsync(appId, CancellationToken.None); + //Generate the new secret + PrivateString secret = GenerateSecret(); + //Hash the secret + using PrivateString secretHash = SecretHashing.Hash(secret); + //Open new db context + await using UserAppContext Database = new(ConextOptions); + //Open transaction + await Database.OpenTransactionAsync(); + //Get the app to update the secret on + UserApplication? app = await (from ap in Database.OAuthApps + where ap.UserId == userId && ap.Id == appId + select ap) + .SingleOrDefaultAsync(); + if (app == null) + { + return null; + } + //Store the new secret hash + app.SecretHash = (string)secretHash; + //Save changes + if (await Database.SaveChangesAsync() <= 0) + { + return null; + } + //Commit transaction + await Database.CommitTransactionAsync(); + //return the raw secret + return secret; + } + + /// + /// Attempts to retreive an application by the specified client id and compares the raw secret against the + /// stored secret hash. + /// + /// The clientid of the application to search + /// The secret to compare against + /// True if the application was found and the secret matches the stored secret, false if the appliation was not found or the secret does not match + public async Task VerifyAppAsync(string clientId, PrivateString secret) + { + UserApplication? app; + //Open new db context + await using (UserAppContext Database = new(ConextOptions)) + { + //Open transaction + await Database.OpenTransactionAsync(); + //Get the application with its secret + app = await (from userApp in Database.OAuthApps + where userApp.ClientId == clientId + select userApp) + .FirstOrDefaultAsync(); + //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)) + { + app.SecretHash = null; + //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) + { + record.RawSecret = GenerateSecret(); + //Hash the secret + using PrivateString secretHash = SecretHashing.Hash(record.RawSecret); + record.ClientId = GenerateClientID(); + record.SecretHash = (string)secretHash; + //Wait for the rescord to be created before wiping the secret + return await base.CreateAsync(record); + } + + /// + /// 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; + } + + //DO NOT ALLOW PAGINATION YET + public override Task GetPageAsync(ICollection collection, int page, int limit) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs b/Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs deleted file mode 100644 index 34db018..0000000 --- a/Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs +++ /dev/null @@ -1,284 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Oauth -* File: Applications.cs -* -* Applications.cs is part of VNLib.Plugins.Essentials.Oauth which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Oauth is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published -* by the Free Software Foundation, either version 2 of the License, -* or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Oauth is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -* General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with VNLib.Plugins.Essentials.Oauth. If not, see http://www.gnu.org/licenses/. -*/ - -using System; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Collections.Generic; - -using Microsoft.EntityFrameworkCore; - -using VNLib.Hashing; -using VNLib.Utils; -using VNLib.Utils.Memory; -using VNLib.Plugins.Extensions.Data; -using VNLib.Plugins.Essentials.Accounts; -using VNLib.Plugins.Essentials.Oauth.Tokens; - -namespace VNLib.Plugins.Essentials.Oauth.Applications -{ - /// - /// A DbStore for s for OAuth2 client applications - /// - public sealed partial class Applications : DbStore - { - public const int SECRET_SIZE = 32; - public const int CLIENT_ID_SIZE = 16; - - private readonly PasswordHashing SecretHashing; - private readonly DbContextOptions ConextOptions; - private readonly ITokenManager TokenStore; - - /// - /// Initializes a new data store - /// uisng the specified EFCore object. - /// - /// EFCore context options for connecting to a remote data-store - /// A structure for hashing client secrets - public Applications(DbContextOptions conextOptions, PasswordHashing secretHashing) - { - this.ConextOptions = conextOptions; - this.SecretHashing = secretHashing; - this.TokenStore = new TokenStore(conextOptions); - } - - - /// - /// Updates the secret of an application, and if successful returns the new raw secret data - /// - /// The user-id of that owns the application - /// The id of the application to update - /// A task that resolves to the raw secret that was used to generate the hash, or null if the operation failed - public async Task UpdateSecretAsync(string userId, string appId) - { - /* - * Delete open apps first, incase there are any issues, worse case - * the user's will have to re-authenticate. - * - * If we delete tokens after update, the user wont see the new - * secret and may lose access to the updated app, not a big deal - * but avoidable. - */ - await TokenStore.RevokeTokensForAppAsync(appId, CancellationToken.None); - //Generate the new secret - PrivateString secret = GenerateSecret(); - //Hash the secret - using PrivateString secretHash = SecretHashing.Hash(secret); - //Open new db context - await using UserAppContext Database = new(ConextOptions); - //Open transaction - await Database.OpenTransactionAsync(); - //Get the app to update the secret on - UserApplication? app = await (from ap in Database.OAuthApps - where ap.UserId == userId && ap.Id == appId - select ap) - .SingleOrDefaultAsync(); - if (app == null) - { - return null; - } - //Store the new secret hash - app.SecretHash = (string)secretHash; - //Save changes - if (await Database.SaveChangesAsync() <= 0) - { - return null; - } - //Commit transaction - await Database.CommitTransactionAsync(); - //return the raw secret - return secret; - } - - /// - /// Attempts to retreive an application by the specified client id and compares the raw secret against the - /// stored secret hash. - /// - /// The clientid of the application to search - /// The secret to compare against - /// True if the application was found and the secret matches the stored secret, false if the appliation was not found or the secret does not match - public async Task VerifyAppAsync(string clientId, PrivateString secret) - { - UserApplication? app; - //Open new db context - await using (UserAppContext Database = new(ConextOptions)) - { - //Open transaction - await Database.OpenTransactionAsync(); - //Get the application with its secret - app = await (from userApp in Database.OAuthApps - where userApp.ClientId == clientId - select userApp) - .FirstOrDefaultAsync(); - //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)) - { - app.SecretHash = null; - //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) - { - record.RawSecret = GenerateSecret(); - //Hash the secret - using PrivateString secretHash = SecretHashing.Hash(record.RawSecret); - record.ClientId = GenerateClientID(); - record.SecretHash = (string)secretHash; - //Wait for the rescord to be created before wiping the secret - return await base.CreateAsync(record); - } - - /// - /// 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 userId) - { - 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 - }; - } - /// - protected override IQueryable GetCollectionQueryBuilder(TransactionalDbContext context, params string[] args) - { - return GetCollectionQueryBuilder(context, args[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; - } - - //DO NOT ALLOW PAGINATION YET - public override Task GetPageAsync(ICollection collection, int page, int limit) - { - throw new NotSupportedException(); - } - } -} \ No newline at end of file diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/Applications/UserAppContext.cs b/Libs/VNLib.Plugins.Essentials.Oauth/Applications/UserAppContext.cs index 05a901e..53a4d43 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/Applications/UserAppContext.cs +++ b/Libs/VNLib.Plugins.Essentials.Oauth/Applications/UserAppContext.cs @@ -30,7 +30,7 @@ using VNLib.Plugins.Essentials.Oauth.Tokens; namespace VNLib.Plugins.Essentials.Oauth.Applications { - internal class UserAppContext : TransactionalDbContext + public sealed class UserAppContext : TransactionalDbContext { public DbSet OAuthApps { get; set; } public DbSet OAuthTokens { get; set; } diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/Tokens/ActiveToken.cs b/Libs/VNLib.Plugins.Essentials.Oauth/Tokens/ActiveToken.cs index 8a3f3f3..3f4ad8b 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/Tokens/ActiveToken.cs +++ b/Libs/VNLib.Plugins.Essentials.Oauth/Tokens/ActiveToken.cs @@ -30,7 +30,7 @@ namespace VNLib.Plugins.Essentials.Oauth.Tokens { public class ActiveToken : DbModelBase { - public override string Id { get; set; } + public override string Id { get; set; } = string.Empty; public override DateTime Created { get; set; } public override DateTime LastModified { get; set; } diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/VNLib.Plugins.Essentials.Oauth.csproj b/Libs/VNLib.Plugins.Essentials.Oauth/VNLib.Plugins.Essentials.Oauth.csproj index 49c3c84..aeb97c0 100644 --- a/Libs/VNLib.Plugins.Essentials.Oauth/VNLib.Plugins.Essentials.Oauth.csproj +++ b/Libs/VNLib.Plugins.Essentials.Oauth/VNLib.Plugins.Essentials.Oauth.csproj @@ -2,7 +2,25 @@ net6.0 - AnyCPU;x64 + Vaughn Nugent + Copyright © 2022 Vaughn Nugent + https://www.vaughnnugent.com/resources + 1.0.2.1 + enable + + + + true + True + latest-all + + + + False + + + + False @@ -19,27 +37,8 @@ - - - true - Vaughn Nugent - Copyright © 2022 Vaughn Nugent - www.vaughnnugent.com/resources - 1.0.2.1 - enable - True - - - - - - - - - - -- cgit