/* * 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 { /// /// Represents a database backed /// that allows for communicating token information to /// plugins /// public sealed class TokenStore : ITokenManager { private readonly DbContextOptions Options; /// /// Initializes a new that will make quries against /// the supplied /// /// The DB connection context public TokenStore(DbContextOptions options) => Options = options; /// /// 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 /// /// The token (or session id) /// The applicaiton the token belongs to /// The tokens refresh token /// The maxium number of allowed tokens for a given application /// A token to cancel the operation /// /// if the opreation succeeds (aka 0x01), /// if the operation fails, or the number /// of active tokens if the maximum has been reached. /// public async Task 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; } /// /// Revokes/removes a single token from the store by its ID /// /// The token to remove /// /// A task that revolves when the token is removed from the table if it exists 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); } /// /// Removes all token entires that were created before the specified time /// /// The time before which all tokens are invaid /// A token the cancel the operation /// A task that resolves to a collection of tokens that were removed public async Task> 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; } /// public async Task RevokeTokensAsync(IReadOnlyCollection 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); } /// 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); } } }