aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Essentials.Oauth/src
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-01-12 17:47:41 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-01-12 17:47:41 -0500
commite5bffa4bfa5a5a4a29dc5d1ccb250f37d938664e (patch)
tree0157df9ed996611908de95195cca9a4b98a06cc4 /Libs/VNLib.Plugins.Essentials.Oauth/src
parent47795f89123059e4d278ba299658ef663eb5d833 (diff)
Large project reorder and consolidation
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Oauth/src')
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs284
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserAppContext.cs43
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs157
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs40
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/IOAuth2TokenResult.cs38
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ITokenManager.cs51
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/TokenStore.cs195
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/VNLib.Plugins.Essentials.Oauth.csproj47
8 files changed, 855 insertions, 0 deletions
diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs
new file mode 100644
index 0000000..2fe44ef
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/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
+{
+ /// <summary>
+ /// A DbStore for <see cref="UserApplication"/>s for OAuth2 client applications
+ /// </summary>
+ public sealed partial class ApplicationStore : DbStore<UserApplication>
+ {
+ 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;
+
+ /// <summary>
+ /// Initializes a new <see cref="ApplicationStore"/> data store
+ /// uisng the specified EFCore <see cref="DbContextOptions"/> object.
+ /// </summary>
+ /// <param name="conextOptions">EFCore context options for connecting to a remote data-store</param>
+ /// <param name="secretHashing">A <see cref="PasswordHashing"/> structure for hashing client secrets</param>
+ public ApplicationStore(DbContextOptions conextOptions, PasswordHashing secretHashing)
+ {
+ this.ConextOptions = conextOptions;
+ this.SecretHashing = secretHashing;
+ this.TokenStore = new TokenStore(conextOptions);
+ }
+
+
+ /// <summary>
+ /// Updates the secret of an application, and if successful returns the new raw secret data
+ /// </summary>
+ /// <param name="userId">The user-id of that owns the application</param>
+ /// <param name="appId">The id of the application to update</param>
+ /// <returns>A task that resolves to the raw secret that was used to generate the hash, or null if the operation failed</returns>
+ public async Task<PrivateString?> 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;
+ }
+
+ /// <summary>
+ /// Attempts to retreive an application by the specified client id and compares the raw secret against the
+ /// stored secret hash.
+ /// </summary>
+ /// <param name="clientId">The clientid of the application to search</param>
+ /// <param name="secret">The secret to compare against</param>
+ /// <returns>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</returns>
+ public async Task<UserApplication?> 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;
+ }
+ ///<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>
+ /// <returns>The result of the operation</returns>
+ public override async Task<ERRNO> 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);
+ }
+
+ /// <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;
+ }
+
+ //DO NOT ALLOW PAGINATION YET
+ public override Task<int> GetPageAsync(ICollection<UserApplication> collection, int page, int limit)
+ {
+ throw new NotSupportedException();
+ }
+ }
+} \ 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
new file mode 100644
index 0000000..53a4d43
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserAppContext.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Oauth
+* File: UserAppContext.cs
+*
+* UserAppContext.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 Microsoft.EntityFrameworkCore;
+
+
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Essentials.Oauth.Tokens;
+
+namespace VNLib.Plugins.Essentials.Oauth.Applications
+{
+ public sealed class UserAppContext : TransactionalDbContext
+ {
+ public DbSet<UserApplication> OAuthApps { get; set; }
+ public DbSet<ActiveToken> OAuthTokens { get; set; }
+#nullable disable
+ public UserAppContext(DbContextOptions options) : base(options)
+ {
+
+ }
+ }
+} \ No newline at end of file
diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs
new file mode 100644
index 0000000..d42ac26
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/UserApplication.cs
@@ -0,0 +1,157 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Oauth
+* File: UserApplication.cs
+*
+* UserApplication.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.Text.Json;
+using System.ComponentModel;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Hashing.IdentityUtility;
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Extensions.Data.Abstractions;
+
+using IndexAttribute = Microsoft.EntityFrameworkCore.IndexAttribute;
+
+
+namespace VNLib.Plugins.Essentials.Oauth.Applications
+{
+ /// <summary>
+ ///
+ /// </summary>
+ [Index(nameof(ClientId), IsUnique = true)]
+ public class UserApplication : DbModelBase, IUserEntity, IJsonOnDeserialized
+ {
+ ///<inheritdoc/>
+ [Key, Required]
+ public override string? Id { get; set; }
+ ///<inheritdoc/>
+ public override DateTime Created { get; set; }
+ ///<inheritdoc/>
+ public override DateTime LastModified { get; set; }
+
+ ///<inheritdoc/>
+ [Required]
+ [JsonIgnore]
+ public string? UserId { get; set; }
+
+ /// <summary>
+ /// The OAuth2 application's associated client-id
+ /// </summary>
+ [Required]
+ [JsonPropertyName("client_id")]
+ public string? ClientId { get; set; }
+
+ /// <summary>
+ /// The hash of the application's secret (no json-serializable)
+ /// </summary>
+ [JsonIgnore]
+ [MaxLength(1000)]
+ public string? SecretHash { get; set; }
+
+ /// <summary>
+ /// The user-defined name of the application
+ /// </summary>
+ [DisplayName("Application Name")]
+ [JsonPropertyName("name")]
+ public string? AppName { get; set; }
+
+ /// <summary>
+ /// The user-defined description for the application
+ /// </summary>
+ [JsonPropertyName("description")]
+ [DisplayName("Application Description")]
+ public string? AppDescription { get; set; }
+
+ /// <summary>
+ /// The permissions for the application
+ /// </summary>
+ [JsonPropertyName("permissions")]
+ [Column("permissions")]
+ public string? Permissions { get; set; }
+
+
+ [NotMapped]
+ [JsonIgnore]
+ public PrivateString? RawSecret { get; set; }
+
+ void IJsonOnDeserialized.OnDeserialized()
+ {
+ Id = Id?.Trim();
+ ClientId = ClientId?.Trim();
+ UserId = UserId?.Trim();
+ AppName = AppName?.Trim();
+ AppDescription = AppDescription?.Trim();
+ Permissions = Permissions?.Trim();
+ }
+
+
+ /// <summary>
+ /// Creates a new <see cref="UserApplication"/> instance
+ /// from the supplied <see cref="JsonElement"/> assuming
+ /// JWT format
+ /// </summary>
+ /// <param name="appEl">The application JWT payalod element</param>
+ /// <returns>The recovered application</returns>
+ public static UserApplication FromJwtDoc(in JsonElement appEl)
+ {
+ return new()
+ {
+ UserId = appEl.GetPropString("sub"),
+ ClientId = appEl.GetPropString("azp"),
+ Id = appEl.GetPropString("appid"),
+ Permissions = appEl.GetPropString("scope"),
+ };
+ }
+ /// <summary>
+ /// Stores the
+ /// </summary>
+ /// <param name="app">The application to serialze to JWT format</param>
+ /// <param name="dict">Jwt dictionary payload</param>
+ public static void ToJwtDict(UserApplication app, IDictionary<string, string?> dict)
+ {
+ dict["appid"] = app.Id;
+ dict["azp"] = app.ClientId;
+ dict["sub"] = app.UserId;
+ dict["scope"] = app.Permissions;
+ }
+
+ /// <summary>
+ /// Stores the
+ /// </summary>
+ /// <param name="app"></param>
+ /// <param name="payload">JW payload parameter</param>
+ public static void ToJwtPayload(UserApplication app, in JwtPayload payload)
+ {
+ payload["appid"] = app.Id;
+ payload["azp"] = app.ClientId;
+ payload["sub"] = app.UserId;
+ payload["scope"] = app.Permissions;
+ }
+ }
+} \ No newline at end of file
diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs
new file mode 100644
index 0000000..3f4ad8b
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ActiveToken.cs
@@ -0,0 +1,40 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Oauth
+* File: ActiveToken.cs
+*
+* ActiveToken.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 VNLib.Plugins.Extensions.Data;
+
+namespace VNLib.Plugins.Essentials.Oauth.Tokens
+{
+ public class ActiveToken : DbModelBase
+ {
+ public override string Id { get; set; } = string.Empty;
+ public override DateTime Created { get; set; }
+ public override DateTime LastModified { get; set; }
+
+ public string? ApplicationId { get; set; }
+ 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
new file mode 100644
index 0000000..8b237df
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/IOAuth2TokenResult.cs
@@ -0,0 +1,38 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Oauth
+* File: IOAuth2TokenResult.cs
+*
+* IOAuth2TokenResult.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/.
+*/
+
+namespace VNLib.Plugins.Essentials.Oauth.Tokens
+{
+ /// <summary>
+ /// The result of an OAuth2Token creation
+ /// </summary>
+ public interface IOAuth2TokenResult
+ {
+ string? IdentityToken { get; }
+ string? AccessToken { get; }
+ string? RefreshToken { get; }
+ string? TokenType { get; }
+ int ExpiresSeconds { get; }
+ }
+} \ No newline at end of file
diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ITokenManager.cs b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ITokenManager.cs
new file mode 100644
index 0000000..3adcc5d
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/ITokenManager.cs
@@ -0,0 +1,51 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Oauth
+* File: ITokenManager.cs
+*
+* ITokenManager.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.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace VNLib.Plugins.Essentials.Oauth.Tokens
+{
+ /// <summary>
+ /// Provides token creation and revocation
+ /// </summary>
+ public interface ITokenManager
+ {
+ /// <summary>
+ /// Revokes a colleciton of toke
+ /// </summary>
+ /// <param name="tokens">A collection of tokens to revoke</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that completes when the tokens have been revoked</returns>
+ Task RevokeTokensAsync(IReadOnlyCollection<string> tokens, CancellationToken cancellation = default);
+ /// <summary>
+ /// Attempts to revoke tokens that belong to a specified application
+ /// </summary>
+ /// <param name="appId">The application to revoke tokens for</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>A task that completes when the work is complete</returns>
+ Task RevokeTokensForAppAsync(string appId, CancellationToken cancellation = default);
+ }
+} \ 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
new file mode 100644
index 0000000..d139357
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/Tokens/TokenStore.cs
@@ -0,0 +1,195 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Oauth
+* File: TokenStore.cs
+*
+* TokenStore.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.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Utils;
+using VNLib.Plugins.Essentials.Oauth.Applications;
+
+namespace VNLib.Plugins.Essentials.Oauth.Tokens
+{
+ /// <summary>
+ /// Represents a database backed <see cref="ITokenManager"/>
+ /// that allows for communicating token information to
+ /// plugins
+ /// </summary>
+ public sealed class TokenStore : ITokenManager
+ {
+ private readonly DbContextOptions Options;
+
+ /// <summary>
+ /// Initializes a new <see cref="TokenStore"/> that will make quries against
+ /// the supplied <see cref="DbContextOptions"/>
+ /// </summary>
+ /// <param name="options">The DB connection context</param>
+ public TokenStore(DbContextOptions options) => Options = options;
+
+ /// <summary>
+ /// Inserts a new token into the table for a specified application id. Also determines if
+ /// the user has reached the maximum number of allowed tokens
+ /// </summary>
+ /// <param name="token">The token (or session id)</param>
+ /// <param name="appId">The applicaiton the token belongs to</param>
+ /// <param name="refreshToken">The tokens refresh token</param>
+ /// <param name="maxTokens">The maxium number of allowed tokens for a given application</param>
+ /// <param name="cancellation">A token to cancel the operation</param>
+ /// <returns>
+ /// <see cref="ERRNO.SUCCESS"/> if the opreation succeeds (aka 0x01),
+ /// <see cref="ERRNO.E_FAIL"/> if the operation fails, or the number
+ /// of active tokens if the maximum has been reached.
+ /// </returns>
+ public async Task<ERRNO> InsertTokenAsync(string token, string appId, string? refreshToken, int maxTokens, CancellationToken cancellation)
+ {
+ await using UserAppContext ctx = new (Options);
+ await ctx.OpenTransactionAsync(cancellation);
+
+ //Check active token count
+ int count = await (from t in ctx.OAuthTokens
+ where t.ApplicationId == appId
+ select t)
+ .CountAsync(cancellation);
+ //Check count
+ if(count >= maxTokens)
+ {
+ return count;
+ }
+
+ //Try to add the new token
+ ActiveToken newToken = new()
+ {
+ ApplicationId = appId,
+ Id = token,
+ RefreshToken = refreshToken,
+ Created = DateTime.UtcNow,
+ LastModified = DateTime.UtcNow
+ };
+ //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;
+ }
+
+ /// <summary>
+ /// Revokes/removes a single token from the store by its ID
+ /// </summary>
+ /// <param name="token">The token to remove</param>
+ /// <param name="cancellation"></param>
+ /// <returns>A task that revolves when the token is removed from the table if it exists</returns>
+ public async Task RevokeTokenAsync(string token, CancellationToken cancellation)
+ {
+ await using UserAppContext ctx = new (Options);
+ await ctx.OpenTransactionAsync(cancellation);
+ //Get the token from the db if it exists
+ ActiveToken? at = await (from t in ctx.OAuthTokens
+ where t.Id == token
+ select t)
+ .FirstOrDefaultAsync(cancellation);
+ if(at == null)
+ {
+ return;
+ }
+ //delete token
+ ctx.OAuthTokens.Remove(at);
+ //Save changes
+ await ctx.SaveChangesAsync(cancellation);
+ await ctx.CommitTransactionAsync(cancellation);
+ }
+ /// <summary>
+ /// Removes all token entires that were created before the specified time
+ /// </summary>
+ /// <param name="validAfter">The time before which all tokens are invaid</param>
+ /// <param name="cancellation">A token the cancel the operation</param>
+ /// <returns>A task that resolves to a collection of tokens that were removed</returns>
+ public async Task<IReadOnlyCollection<ActiveToken>> CleanupExpiredTokensAsync(DateTime validAfter, CancellationToken cancellation)
+ {
+ await using UserAppContext ctx = new (Options);
+ await ctx.OpenTransactionAsync(cancellation);
+ //Get the token from the db if it exists
+ ActiveToken[] at = await (from t in ctx.OAuthTokens
+ where t.Created < validAfter
+ select t)
+ .ToArrayAsync(cancellation);
+
+ //delete token
+ ctx.OAuthTokens.RemoveRange(at);
+ //Save changes
+ int count = await ctx.SaveChangesAsync(cancellation);
+ await ctx.CommitTransactionAsync(cancellation);
+ return at;
+ }
+ ///<inheritdoc/>
+ public async Task RevokeTokensAsync(IReadOnlyCollection<string> tokens, CancellationToken cancellation = default)
+ {
+ await using UserAppContext ctx = new (Options);
+ await ctx.OpenTransactionAsync(cancellation);
+ //Get all tokenes that are contained in the collection
+ ActiveToken[] at = await (from t in ctx.OAuthTokens
+ where tokens.Contains(t.Id)
+ select t)
+ .ToArrayAsync(cancellation);
+
+ //delete token
+ ctx.OAuthTokens.RemoveRange(at);
+ //Save changes
+ await ctx.SaveChangesAsync(cancellation);
+ await ctx.CommitTransactionAsync(cancellation);
+ }
+ ///<inheritdoc/>
+ async Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation)
+ {
+ await using UserAppContext ctx = new (Options);
+ await ctx.OpenTransactionAsync(cancellation);
+ //Get the token from the db if it exists
+ ActiveToken[] at = await (from t in ctx.OAuthTokens
+ where t.ApplicationId == appId
+ select t)
+ .ToArrayAsync(cancellation);
+ //Set created time to 0 to invalidate the token
+ foreach(ActiveToken t in at)
+ {
+ //Expire token so next cleanup round will wipe tokens
+ t.Created = DateTime.MinValue;
+ }
+ //Save changes
+ await ctx.SaveChangesAsync(cancellation);
+ await ctx.CommitTransactionAsync(cancellation);
+ }
+ }
+}
diff --git a/Libs/VNLib.Plugins.Essentials.Oauth/src/VNLib.Plugins.Essentials.Oauth.csproj b/Libs/VNLib.Plugins.Essentials.Oauth/src/VNLib.Plugins.Essentials.Oauth.csproj
new file mode 100644
index 0000000..252bbb9
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Oauth/src/VNLib.Plugins.Essentials.Oauth.csproj
@@ -0,0 +1,47 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <Version>1.0.2.1</Version>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <PropertyGroup>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ <Description>A library containing common data-structures for client application driven OAuth2 implementations</Description>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.11" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\..\core\lib\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\..\..\..\core\lib\Utils\src\VNLib.Utils.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Data\src\VNLib.Plugins.Extensions.Data.csproj" />
+ </ItemGroup>
+
+</Project>