aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-08-28 22:00:43 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-08-28 22:00:43 -0400
commit579204edb43e0d44f064cc5243bf14939f3f0895 (patch)
treea8c75531c40a311da7877679a7dd9655e8e9faf6
parentb447f0cb29e54c988dd64f28e87fd9ca81127b11 (diff)
Data extensions updates
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs229
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserAppContext.cs2
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs2
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs17
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/IOAuth2TokenResult.cs21
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/TokenStore.cs36
-rw-r--r--Plugins/OAuth2ClientApplications/src/Endpoints/ApplicationEndpoint.cs13
-rw-r--r--Plugins/OAuth2ClientApplications/src/OAuth2ClientApplications.csproj2
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;
+
/// <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);
}
}
}
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 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="FluentValidation" Version="11.6.0" />
+ <PackageReference Include="FluentValidation" Version="11.7.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj" />