Skip to content

Commit 03211b9

Browse files
committed
API clients now pulled from database. Full OIDC flow now implemented
1 parent 5828283 commit 03211b9

8 files changed

Lines changed: 79 additions & 89 deletions

File tree

src/Database/api/Procedures/Authorisation.GetAccessClients.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ select
1515
SecretKey,
1616
IsAdministratorApp
1717
from AccessClient
18-
where (@ClientId = null or ClientId = @ClientId);
18+
where (@ClientId is null or ClientId = @ClientId);

src/OpenPerpetuum.Api/Authorisation/ApplicationContext.cs

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/OpenPerpetuum.Api/Authorisation/AuthorisationProvider.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
11
using AspNet.Security.OpenIdConnect.Primitives;
22
using AspNet.Security.OpenIdConnect.Server;
3-
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.Extensions.Caching.Memory;
4+
using OpenPerpetuum.Api.Configuration;
45
using OpenPerpetuum.Core.Authorisation.Models;
5-
using OpenPerpetuum.Core.Authorisation.Queries;
6-
using OpenPerpetuum.Core.Foundation.Processing;
76
using System;
7+
using System.Collections.ObjectModel;
88
using System.Linq;
9-
using System.Threading;
109
using System.Threading.Tasks;
1110

1211
namespace OpenPerpetuum.Api.Authorisation
1312
{
1413
// Note: This class is *always* a singleton. Don't inject scoped dependencies
1514
public sealed class AuthorisationProvider : OpenIdConnectServerProvider
1615
{
17-
private readonly ApplicationContext dbContext;
16+
private readonly IMemoryCache dbContext;
1817

19-
public AuthorisationProvider(ApplicationContext dbContext)
18+
public AuthorisationProvider(IMemoryCache dbContext)
2019
{
2120
this.dbContext = dbContext;
2221
}
2322

2423
// Implement OnValidateAuthorizationRequest to support interactive flows (code/implicit/hybrid).
25-
public override async Task ValidateAuthorizationRequest(ValidateAuthorizationRequestContext context)
24+
public override Task ValidateAuthorizationRequest(ValidateAuthorizationRequestContext context)
2625
{
2726
/* This method must run in a 'time-constant' fashion.
2827
* Even when authorisation has failed at a certain point
@@ -32,17 +31,18 @@ public override async Task ValidateAuthorizationRequest(ValidateAuthorizationReq
3231
* authorisation process, the more of the process you get correct, the longer it will take to return.
3332
* Ideally, the time to return should always stay the same.
3433
*/
34+
3535
bool isError = false;
3636

3737
if (!context.Request.IsAuthorizationCodeFlow())
3838
{
3939
context.Reject(
4040
error: OpenIdConnectConstants.Errors.UnsupportedResponseType,
4141
description: "Only authorisation code flow is supported by this server");
42-
return; // Given that this is a protocol error I'm not too fussed about the early return
42+
return Task.FromResult(0); // Given that this is a protocol error I'm not too fussed about the early return
4343
}
4444

45-
AccessClientModel accessClient = await GetApplicationAsync(context.ClientId, context.HttpContext.RequestAborted) ?? AccessClientModel.DefaultValue;
45+
AccessClientModel accessClient = GetApplication(context.ClientId) ?? AccessClientModel.DefaultValue;
4646

4747
if (Equals(accessClient.ClientId, Guid.Empty))
4848
{
@@ -68,7 +68,7 @@ public override async Task ValidateAuthorizationRequest(ValidateAuthorizationReq
6868
if (!isError)
6969
context.Validate();
7070

71-
return;
71+
return Task.FromResult(0);
7272
}
7373

7474
// Implement OnValidateTokenRequest to support flows using the token endpoint
@@ -85,16 +85,19 @@ public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
8585
return Task.FromResult(0);
8686
}
8787

88-
private async Task<AccessClientModel> GetApplicationAsync(string identifier, CancellationToken cancellationToken)
88+
private AccessClientModel GetApplication(string identifier)
8989
{
9090
if (string.IsNullOrWhiteSpace(identifier))
9191
return null;
9292

9393
if (!Guid.TryParse(identifier, out Guid clientId))
9494
clientId = Guid.Empty;
9595

96+
if (!dbContext.TryGetValue(CacheKeys.AccessClients, out ReadOnlyCollection<AccessClientModel> applications) || applications == null || applications.Count == 0)
97+
return AccessClientModel.DefaultValue;
98+
9699
// Retrieve the application details corresponding to the requested client_id.
97-
return await dbContext.Applications.Where(application => application.ClientId == clientId).SingleOrDefaultAsync(cancellationToken);
100+
return applications.SingleOrDefault(ap => ap.ClientId == clientId) ?? AccessClientModel.DefaultValue;
98101
}
99102
}
100103
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace OpenPerpetuum.Api.Configuration
2+
{
3+
public static class CacheKeys
4+
{
5+
public const string AccessClients = "AccessClients";
6+
}
7+
}

src/OpenPerpetuum.Api/Controllers/AuthorisationController.cs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,40 @@
1-
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Security.Claims;
5-
using System.Threading;
6-
using System.Threading.Tasks;
7-
using AspNet.Security.OpenIdConnect.Extensions;
1+
using AspNet.Security.OpenIdConnect.Extensions;
82
using AspNet.Security.OpenIdConnect.Primitives;
93
using AspNet.Security.OpenIdConnect.Server;
104
using Microsoft.AspNetCore.Authentication;
11-
using Microsoft.AspNetCore.Authentication.Cookies;
125
using Microsoft.AspNetCore.Authorization;
136
using Microsoft.AspNetCore.Mvc;
147
using Microsoft.EntityFrameworkCore;
8+
using Microsoft.Extensions.Caching.Memory;
159
using Microsoft.Extensions.DependencyInjection;
16-
using OpenPerpetuum.Api.Authorisation;
10+
using OpenPerpetuum.Api.Configuration;
1711
using OpenPerpetuum.Api.DependencyInstallers;
1812
using OpenPerpetuum.Api.Models.Authorisation;
1913
using OpenPerpetuum.Core.Authorisation.Models;
2014
using OpenPerpetuum.Core.Authorisation.Queries;
2115
using OpenPerpetuum.Core.Foundation.Processing;
2216
using OpenPerpetuum.Core.Foundation.Security;
17+
using System;
18+
using System.Collections.Generic;
19+
using System.Collections.ObjectModel;
20+
using System.Linq;
21+
using System.Security.Claims;
22+
using System.Threading;
23+
using System.Threading.Tasks;
2324

2425
namespace OpenPerpetuum.Api.Controllers
2526
{
2627
public class AuthorisationController : ApiControllerBase
2728
{
28-
private readonly ApplicationContext dbContext;
29+
private readonly IMemoryCache cache;
2930

30-
public AuthorisationController(ICoreContext coreContext, ApplicationContext context) : base(coreContext)
31+
public AuthorisationController(ICoreContext coreContext, IMemoryCache cache) : base(coreContext)
3132
{
32-
dbContext = context;
33+
this.cache = cache;
3334
}
3435

3536
[Authorize, HttpGet("~/connect/authorise")]
36-
public async Task<IActionResult> Authorise(CancellationToken cancellationToken)
37+
public IActionResult Authorise(CancellationToken cancellationToken)
3738
{
3839
// Extract the auth request from the context
3940
var response = HttpContext.GetOpenIdConnectResponse();
@@ -54,7 +55,7 @@ public async Task<IActionResult> Authorise(CancellationToken cancellationToken)
5455
ErrorDescription = "Internal error"
5556
});
5657

57-
AccessClientModel accessClient = await GetApplicationAsync(request.ClientId, cancellationToken) ?? AccessClientModel.DefaultValue;
58+
AccessClientModel accessClient = GetApplication(request.ClientId) ?? AccessClientModel.DefaultValue;
5859

5960
if (accessClient.ClientId == Guid.Empty)
6061
return View("Error", new ErrorViewModel
@@ -68,7 +69,7 @@ public async Task<IActionResult> Authorise(CancellationToken cancellationToken)
6869

6970
[Authorize, FormValueRequired("submit.Accept")]
7071
[HttpPost("~/connect/authorise"), ValidateAntiForgeryToken]
71-
public async Task<IActionResult> Accept(CancellationToken cancellationToken)
72+
public IActionResult Accept(CancellationToken cancellationToken)
7273
{
7374
var response = HttpContext.GetOpenIdConnectResponse();
7475
if (response != null)
@@ -104,7 +105,7 @@ public async Task<IActionResult> Accept(CancellationToken cancellationToken)
104105
OpenIdConnectConstants.Destinations.AccessToken,
105106
OpenIdConnectConstants.Destinations.IdentityToken));
106107

107-
var application = await GetApplicationAsync(request.ClientId, cancellationToken);
108+
var application = GetApplication(request.ClientId);
108109
if (application == null)
109110
{
110111
return View("Error", new OpenIdConnectResponse
@@ -229,16 +230,19 @@ await HttpContext.SignInAsync(
229230
return Redirect(viewModel.ReturnUrl);
230231
}
231232

232-
protected virtual async Task<AccessClientModel> GetApplicationAsync(string identifier, CancellationToken cancellationToken)
233+
private AccessClientModel GetApplication(string identifier)
233234
{
234235
if (string.IsNullOrWhiteSpace(identifier))
235236
return null;
236237

237238
if (!Guid.TryParse(identifier, out Guid clientId))
238239
clientId = Guid.Empty;
239240

241+
if (!cache.TryGetValue(CacheKeys.AccessClients, out ReadOnlyCollection<AccessClientModel> applications) || applications == null || applications.Count == 0)
242+
return AccessClientModel.DefaultValue;
243+
240244
// Retrieve the application details corresponding to the requested client_id.
241-
return await dbContext.Applications.Where(application => application.ClientId == clientId).SingleOrDefaultAsync(cancellationToken);
245+
return applications.SingleOrDefault(ap => ap.ClientId == clientId) ?? AccessClientModel.DefaultValue;
242246
}
243247
}
244248
}

src/OpenPerpetuum.Api/Startup.cs

Lines changed: 29 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
1212
using Microsoft.EntityFrameworkCore;
1313
using Microsoft.Extensions.Caching.Distributed;
14+
using Microsoft.Extensions.Caching.Memory;
1415
using Microsoft.Extensions.Configuration;
1516
using Microsoft.Extensions.DependencyInjection;
1617
using Microsoft.Extensions.Logging;
@@ -20,15 +21,19 @@
2021
using OpenPerpetuum.Api.Configuration;
2122
using OpenPerpetuum.Api.DependencyInstallers;
2223
using OpenPerpetuum.Core.Authorisation.Models;
24+
using OpenPerpetuum.Core.Authorisation.Queries;
2325
using OpenPerpetuum.Core.DataServices;
2426
using OpenPerpetuum.Core.Extensions;
27+
using OpenPerpetuum.Core.Foundation.Processing;
2528
using SimpleInjector;
2629
using SimpleInjector.Integration.AspNetCore.Mvc;
2730
using SimpleInjector.Lifestyles;
2831
using System;
2932
using System.Collections.Generic;
33+
using System.Collections.ObjectModel;
3034
using System.Linq;
3135
using System.Net;
36+
using System.Threading;
3237
using System.Threading.Tasks;
3338

3439
namespace OpenPerpetuum.Api
@@ -55,15 +60,11 @@ public void ConfigureServices(IServiceCollection services)
5560
services.AddOptions();
5661
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
5762

58-
services
59-
.AddEntityFrameworkInMemoryDatabase()
60-
.AddDbContext<ApplicationContext>(options => options.UseInMemoryDatabase(nameof(ApplicationContext)));
61-
6263
var openIdConnectConfig = Configuration.GetSection("OpenIdConnect").Get<OpenIdConnectConfiguration>();
6364

6465
services.Configure<OpenIdConnectConfiguration>(options => Configuration.GetSection("OpenIdConnect").Bind(options));
6566
services.Configure<DataProviderConfiguration>(options => Configuration.GetSection("DataProviders").Bind(options));
66-
67+
6768
services.AddAuthentication(sharedOptions =>
6869
{
6970
sharedOptions.DefaultScheme = "ServerCookie";
@@ -136,14 +137,15 @@ public void ConfigureServices(IServiceCollection services)
136137
#endif
137138
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
138139

140+
services.AddMemoryCache();
139141
services.AddDistributedMemoryCache();
140142

141143
services.EnableSimpleInjectorCrossWiring(container);
142144
services.UseSimpleInjectorAspNetRequestScoping(container);
143145
}
144146

145147
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
146-
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
148+
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime applicationLifetime)
147149
{
148150
// Event log is only support on Windows systems
149151
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
@@ -171,23 +173,6 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF
171173
}
172174
});
173175

174-
using (AsyncScopedLifestyle.BeginScope(container))
175-
{
176-
var dbContext = container.GetRequiredService<ApplicationContext>();
177-
dbContext.AddRange(new[]
178-
{
179-
AccessClientModel.DefaultValue,
180-
new AccessClientModel
181-
{
182-
AdministratorContactAddress = "admin@email",
183-
AdministratorName = "Development",
184-
ClientId = Guid.Parse("8d24b83a-f04b-483d-8eb7-efd98ac91a9d"),
185-
FriendlyName = "Development Postman Test",
186-
RedirectUri = "https://www.getpostman.com/oauth2/callback"
187-
}
188-
});
189-
dbContext.SaveChanges();
190-
}
191176
bool isDevMode = false, isHsts = false, isHttps = false;
192177

193178
if (env.IsDevelopment())
@@ -209,6 +194,10 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF
209194
app.UseAuthentication();
210195
app.UseStaticFiles();
211196
app.UseMvc();
197+
CancellationTokenSource tokenSource = new CancellationTokenSource();
198+
Task.Run(() => RunPeriodically(() => PopulateApplications(container.GetRequiredService<IMemoryCache>(), container.GetRequiredService<IQueryProcessor>()), TimeSpan.FromMinutes(3), tokenSource.Token));
199+
200+
applicationLifetime.ApplicationStopping.Register(() => tokenSource.Cancel());
212201

213202
UriParser.Register(new GenericUriParser(GenericUriParserOptions.GenericAuthority), "pack", -1); // Don't fail with Azure packed claims
214203

@@ -239,16 +228,21 @@ private void InitialiseContainer(IApplicationBuilder app, ILoggerFactory loggerF
239228
container.RegisterPerpetuumApiTypes();
240229
}
241230

242-
// This should stop the API from returning 302 redirects (attempts to present you a login page) for 401 Unauthorised
243-
private static Func<RedirectContext<CookieAuthenticationOptions>, Task> ReplaceRedirector(HttpStatusCode statusCode, Func<RedirectContext<CookieAuthenticationOptions>, Task> existingRedirector) =>
244-
context =>
245-
{
246-
if (context.Request.Path.StartsWithSegments("/api"))
247-
{
248-
context.Response.StatusCode = (int)statusCode;
249-
return Task.CompletedTask;
250-
}
251-
return existingRedirector(context);
252-
};
253-
}
231+
private async Task RunPeriodically(Action action, TimeSpan interval, CancellationToken token)
232+
{
233+
while(true)
234+
{
235+
action();
236+
await Task.Delay(interval, token);
237+
}
238+
}
239+
240+
private void PopulateApplications(IMemoryCache dbContext, IQueryProcessor queryProcessor)
241+
{
242+
dbContext.Remove(CacheKeys.AccessClients);
243+
244+
ReadOnlyCollection<AccessClientModel> applications = queryProcessor.Process(new API_GetPermittedClientQuery { ClientId = null }) ?? new List<AccessClientModel>().AsReadOnly();
245+
dbContext.Set(CacheKeys.AccessClients, applications);
246+
}
247+
}
254248
}

src/OpenPerpetuum.Core.Authorisation/DatabaseResults/AccessClientsResult.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using OpenPerpetuum.Core.DataServices.Database;
22
using System;
3+
using System.Collections.Generic;
34
using System.Collections.ObjectModel;
45

56
namespace OpenPerpetuum.Core.Authorisation.DatabaseResults
@@ -53,7 +54,7 @@ public string Secret
5354

5455
internal class AccessClientsResult : DatabaseResult
5556
{
56-
public ReadOnlyCollection<AccessClientData> Clients => ((ResultSet<AccessClientData>)Results[0])?.Data;
57+
public ReadOnlyCollection<AccessClientData> Clients => ((ResultSet<AccessClientData>)Results[0])?.Data ?? new List<AccessClientData>().AsReadOnly();
5758

5859
public AccessClientsResult()
5960
{

0 commit comments

Comments
 (0)