diff options
author | vnugent <public@vaughnnugent.com> | 2023-08-28 22:00:43 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-08-28 22:00:43 -0400 |
commit | 579204edb43e0d44f064cc5243bf14939f3f0895 (patch) | |
tree | a8c75531c40a311da7877679a7dd9655e8e9faf6 /Libs | |
parent | b447f0cb29e54c988dd64f28e87fd9ca81127b11 (diff) |
Data extensions updates
Diffstat (limited to 'Libs')
6 files changed, 169 insertions, 138 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; + /// <summary> /// Initializes a new <see cref="ApplicationStore"/> data store /// uisng the specified EFCore <see cref="DbContextOptions"/> object. @@ -64,8 +66,27 @@ namespace VNLib.Plugins.Essentials.Oauth.Applications SecretHashing = secretHashing; TokenStore = new TokenStore(conextOptions); } - - + + + /// <summary> + /// Generates a client application secret using the <see cref="RandomHash"/> library + /// </summary> + /// <returns>The RNG secret</returns> + public static PrivateString GenerateSecret(int secretSize = SECRET_SIZE) => (PrivateString)RandomHash.GetRandomHex(secretSize).ToLower(null)!; + + /// <inheritdoc/> + public override IDbContextHandle GetNewContext() => new UserAppContext(ConextOptions); + + /// <inheritdoc/> + public override string GetNewRecordId() => RandomHash.GetRandomHex(CLIENT_ID_SIZE).ToLower(null); + + ///<inheritdoc/> + public override void OnRecordUpdate(UserApplication newRecord, UserApplication currentRecord) + { + currentRecord.AppDescription = newRecord.AppDescription; + currentRecord.AppName = newRecord.AppName; + } + /// <summary> /// Updates the secret of an application, and if successful returns the new raw secret data /// </summary> @@ -123,6 +144,7 @@ namespace VNLib.Plugins.Essentials.Oauth.Applications public async Task<UserApplication?> 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; } - ///<inheritdoc/> - public override async Task<ERRNO> 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; - } /// <summary> - /// Generates a client application secret using the <see cref="RandomHash"/> library - /// </summary> - /// <returns>The RNG secret</returns> - public static PrivateString GenerateSecret() => (PrivateString)RandomHash.GetRandomHex(SECRET_SIZE).ToLower()!; - /// <summary> /// Creates and initializes a new <see cref="UserApplication"/> with a random clientid and /// secret that must be disposed /// </summary> /// <param name="record">The new record to create</param> + /// <param name="cancellation"></param> /// <returns>The result of the operation</returns> - public override async Task<ERRNO> CreateAsync(UserApplication record, CancellationToken cancellation = default) + public async Task<ERRNO> 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); } + - /// <summary> - /// Generates a new client ID using the <see cref="RandomHash"/> library - /// </summary> - /// <returns>The new client ID</returns> - public static string GenerateClientID() => RandomHash.GetRandomHex(CLIENT_ID_SIZE).ToLower(); - ///<inheritdoc/> - public override string RecordIdBuilder => RandomHash.GetRandomHex(CLIENT_ID_SIZE).ToLower(); - ///<inheritdoc/> - public override TransactionalDbContext NewContext() => new UserAppContext(ConextOptions); - ///<inheritdoc/> - protected override IQueryable<UserApplication> 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; - } - ///<inheritdoc/> - protected override void OnRecordUpdate(UserApplication newRecord, UserApplication currentRecord) - { - currentRecord.AppDescription = newRecord.AppDescription; - currentRecord.AppName = newRecord.AppName; - } - ///<inheritdoc/> - protected override IQueryable<UserApplication> 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 - }; - } - ///<inheritdoc/> - protected override IQueryable<UserApplication> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints) - { - return GetCollectionQueryBuilder(context, constraints[0]); - } - ///<inheritdoc/> - protected override IQueryable<UserApplication> 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 - }; - } - ///<inheritdoc/> - protected override IQueryable<UserApplication> 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); - } - ///<inheritdoc/> - protected override IQueryable<UserApplication> 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<UserApplication> QueryTable { get; } = new ApplicationQueries(); - //DO NOT ALLOW PAGINATION YET - public override Task<int> GetPageAsync(ICollection<UserApplication> collection, int page, int limit, CancellationToken cancellation = default) + sealed class ApplicationQueries : IDbQueryLookup<UserApplication> { - throw new NotSupportedException(); + ///<inheritdoc/> + public IQueryable<UserApplication> 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 + }; + } + } + + ///<inheritdoc/> + public IQueryable<UserApplication> 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 + }; + } + + ///<inheritdoc/> + public IQueryable<UserApplication> 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<UserApplication> OAuthApps { get; set; } + public DbSet<ActiveToken> 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 { /// <summary> - /// + /// Represents an OAuth2 application for a user /// </summary> [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 { + /// <summary> + /// Represents a token record in the database + /// </summary> public class ActiveToken : DbModelBase { + ///<inheritdoc/> public override string Id { get; set; } = string.Empty; + + ///<inheritdoc/> public override DateTime Created { get; set; } + + ///<inheritdoc/> public override DateTime LastModified { get; set; } + /// <summary> + /// A ID of the applicaiton this token was issued for + /// </summary> public string? ApplicationId { get; set; } + + /// <summary> + /// An optional OAuth2 refresh token, used for refreshing access tokens + /// </summary> 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 /// </summary> public interface IOAuth2TokenResult { + /// <summary> + /// An optional token that can be used to identify the user + /// </summary> string? IdentityToken { get; } + + /// <summary> + /// The access token, used for authenticating requests + /// </summary> string? AccessToken { get; } + + /// <summary> + /// An optional OAuth2 refresh token, used for refreshing access tokens + /// </summary> string? RefreshToken { get; } + + /// <summary> + /// The type of token, usually "Bearer" + /// </summary> string? TokenType { get; } + + /// <summary> + /// The number of seconds until the access token expires + /// </summary> 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); } /// <summary> @@ -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); } + /// <summary> /// Removes all token entires that were created before the specified time /// </summary> @@ -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; } + ///<inheritdoc/> public async Task RevokeTokensAsync(IReadOnlyCollection<string> 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); } + ///<inheritdoc/> 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); } } } |