From ef98ef0329d6ee8cec7f040f6c472dc1ea68e8dd Mon Sep 17 00:00:00 2001 From: vman Date: Fri, 18 Nov 2022 17:43:57 -0500 Subject: Add project files. --- .../Applications/Applications.cs | 260 +++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs (limited to 'Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs') diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs b/Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs new file mode 100644 index 0000000..850ccba --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Oauth/Applications/Applications.cs @@ -0,0 +1,260 @@ +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 -- cgit