aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs
diff options
context:
space:
mode:
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs')
-rw-r--r--Libs/VNLib.Plugins.Essentials.Oauth/src/Applications/ApplicationStore.cs284
1 files changed, 284 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