diff --git a/OutOfSchool/OutOfSchool.DataAccess/Extensions/DbSetExtensions.cs b/OutOfSchool/OutOfSchool.DataAccess/Extensions/DbSetExtensions.cs
new file mode 100644
index 0000000000..dcad3527f5
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.DataAccess/Extensions/DbSetExtensions.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Linq;
+using System.Linq.Expressions;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
+
+#nullable enable
+
+namespace OutOfSchool.Services.Extensions
+{
+ public static class DbSetExtensions
+ {
+ ///
+ /// Add entry if it doesn't exist based on a predicate.
+ /// Not thread safe! There's a window between Any and Add where another thread can add an entry.
+ /// Performance issues is need to add multiple entities.
+ ///
+ /// Extension target.
+ /// Entry to add.
+ /// Optional predicate.
+ /// Entity type.
+ /// Return an entity that was just added or null.
+ public static EntityEntry? AddIfNotExists(this DbSet dbSet, T entity, Expression>? predicate = null)
+ where T : class, new()
+ {
+ var exists = predicate != null ? dbSet.Any(predicate) : dbSet.Any();
+ return !exists ? dbSet.Add(entity) : null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/Config.cs b/OutOfSchool/OutOfSchool.IdentityServer/Config.cs
deleted file mode 100644
index 5316ad6222..0000000000
--- a/OutOfSchool/OutOfSchool.IdentityServer/Config.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using System.Collections.Generic;
-using IdentityServer4;
-using IdentityServer4.Models;
-
-namespace OutOfSchool.IdentityServer
-{
- public static class Config
- {
- public static IEnumerable IdentityResources =>
- new[]
- {
- new IdentityResources.OpenId(),
- new IdentityResources.Profile(),
- new IdentityResource
- {
- Name = "role",
- UserClaims = new List { "role" },
- },
- };
-
- public static IEnumerable ApiScopes =>
- new[]
- {
- new ApiScope("outofschoolapi.read"),
- new ApiScope("outofschoolapi.write"),
- };
-
- public static IEnumerable ApiResources(string apiSecret) => new[]
- {
- new ApiResource("outofschoolapi")
- {
- Scopes = new List { "outofschoolapi.read", "outofschoolapi.write" },
- ApiSecrets = new List { new Secret(apiSecret.Sha256()) },
- UserClaims = new List { "role" },
- },
- };
-
- public static IEnumerable Clients(string clientSecret) =>
- new[]
- {
- // m2m client credentials flow client
- new Client
- {
- ClientId = "m2m.client",
- ClientName = "Client Credentials Client",
-
- AllowedGrantTypes = GrantTypes.ClientCredentials,
- ClientSecrets = { new Secret(clientSecret.Sha256()) },
-
- AllowedScopes = { "outofschoolapi.read", "outofschoolapi.write" },
- },
-
- new Client
- {
- ClientId = "angular",
-
- AllowedGrantTypes = GrantTypes.Code,
- RequirePkce = true,
- RequireClientSecret = false,
- AllowOfflineAccess = true,
-
- RedirectUris = { "http://localhost:4200", "http://oos.dmytrominochkin.cloud" },
- PostLogoutRedirectUris = { "http://localhost:4200", "http://oos.dmytrominochkin.cloud" },
- AllowedCorsOrigins = { "http://localhost:4200", "http://oos.dmytrominochkin.cloud" },
-
- AllowedScopes =
- {
- IdentityServerConstants.StandardScopes.OpenId,
- "outofschoolapi.read", "outofschoolapi.write",
- },
-
- AllowAccessTokensViaBrowser = true,
- RequireConsent = false,
- },
- };
- }
-}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/Config/AdditionalIdentityClients.cs b/OutOfSchool/OutOfSchool.IdentityServer/Config/AdditionalIdentityClients.cs
new file mode 100644
index 0000000000..fb7f456b0f
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.IdentityServer/Config/AdditionalIdentityClients.cs
@@ -0,0 +1,13 @@
+namespace OutOfSchool.IdentityServer.Config
+{
+ public class AdditionalIdentityClients
+ {
+ public string ClientId { get; set; }
+
+ public string[] RedirectUris { get; set; }
+
+ public string[] PostLogoutRedirectUris { get; set; }
+
+ public string[] AllowedCorsOrigins { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/Config/IdentityAccessOptions.cs b/OutOfSchool/OutOfSchool.IdentityServer/Config/IdentityAccessOptions.cs
new file mode 100644
index 0000000000..8bce2a290c
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.IdentityServer/Config/IdentityAccessOptions.cs
@@ -0,0 +1,9 @@
+namespace OutOfSchool.IdentityServer.Config
+{
+ public class IdentityAccessOptions
+ {
+ public readonly string Name = "IdentityAccessConfig";
+
+ public AdditionalIdentityClients[] AdditionalIdentityClients { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/Config/StaticConfig.cs b/OutOfSchool/OutOfSchool.IdentityServer/Config/StaticConfig.cs
new file mode 100644
index 0000000000..9c038308c3
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.IdentityServer/Config/StaticConfig.cs
@@ -0,0 +1,79 @@
+using System.Collections.Generic;
+using System.Linq;
+using IdentityServer4;
+using IdentityServer4.Models;
+
+namespace OutOfSchool.IdentityServer.Config
+{
+ public static class StaticConfig
+ {
+ public static IEnumerable IdentityResources =>
+ new[]
+ {
+ new IdentityResources.OpenId(),
+ new IdentityResources.Profile(),
+ new IdentityResource
+ {
+ Name = "role",
+ UserClaims = new List {"role"},
+ },
+ };
+
+ public static IEnumerable ApiScopes =>
+ new[]
+ {
+ new ApiScope("outofschoolapi.read"),
+ new ApiScope("outofschoolapi.write"),
+ };
+
+ public static IEnumerable ApiResources(string apiSecret) => new[]
+ {
+ new ApiResource("outofschoolapi")
+ {
+ Scopes = new List {"outofschoolapi.read", "outofschoolapi.write"},
+ ApiSecrets = new List { new Secret(apiSecret.Sha256()) },
+ UserClaims = new List {"role"},
+ },
+ };
+
+ public static IEnumerable Clients(string clientSecret, IEnumerable additionalClients)
+ {
+ // m2m client credentials flow client
+ var clients = new List
+ {
+ new Client
+ {
+ ClientId = "m2m.client",
+ ClientName = "Client Credentials Client",
+ AllowedGrantTypes = GrantTypes.ClientCredentials,
+ ClientSecrets = {new Secret(clientSecret.Sha256()) },
+ AllowedScopes = {"outofschoolapi.read", "outofschoolapi.write"},
+ },
+ };
+
+ clients.AddRange(additionalClients.Select(c => new Client
+ {
+ ClientId = c.ClientId,
+ AllowedGrantTypes = GrantTypes.Code,
+ RequirePkce = true,
+ RequireClientSecret = false,
+ AllowOfflineAccess = true,
+
+ RedirectUris = c.RedirectUris,
+ PostLogoutRedirectUris = c.PostLogoutRedirectUris,
+ AllowedCorsOrigins = c.AllowedCorsOrigins,
+
+ AllowedScopes =
+ {
+ IdentityServerConstants.StandardScopes.OpenId,
+ "outofschoolapi.read", "outofschoolapi.write",
+ },
+
+ AllowAccessTokensViaBrowser = true,
+ RequireConsent = false,
+ }));
+
+ return clients;
+ }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/Program.cs b/OutOfSchool/OutOfSchool.IdentityServer/Program.cs
index 98b2c9ce15..b642268a44 100644
--- a/OutOfSchool/OutOfSchool.IdentityServer/Program.cs
+++ b/OutOfSchool/OutOfSchool.IdentityServer/Program.cs
@@ -9,10 +9,11 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using OutOfSchool.IdentityServer;
+using OutOfSchool.IdentityServer.Config;
using OutOfSchool.Services;
+using OutOfSchool.Services.Extensions;
-namespace IdentityServer
+namespace OutOfSchool.IdentityServer
{
public class Program
{
@@ -34,8 +35,12 @@ public static void Main(string[] args)
.Database.Migrate();
var identityContext = scope.ServiceProvider.GetRequiredService();
var configService = scope.ServiceProvider.GetRequiredService();
+
+ // TODO: Move to identity options
var apiSecret = configService["outofschoolapi:ApiSecret"];
var clientSecret = configService["m2m.client:ClientSecret"];
+ var identityOptions = new IdentityAccessOptions();
+ configService.GetSection(identityOptions.Name).Bind(identityOptions);
context.Database.Migrate();
identityContext.Database.Migrate();
@@ -46,45 +51,34 @@ public static void Main(string[] args)
RolesInit(manager);
}
- if (!context.Clients.Any())
+ foreach (var client in StaticConfig.Clients(clientSecret, identityOptions.AdditionalIdentityClients))
{
- foreach (var client in Config.Clients(clientSecret))
- {
- context.Clients.Add(client.ToEntity());
- }
-
- context.SaveChanges();
+ context.Clients.AddIfNotExists(client.ToEntity(), c => c.ClientId == client.ClientId);
}
- if (!context.IdentityResources.Any())
- {
- foreach (var resource in Config.IdentityResources)
- {
- context.IdentityResources.Add(resource.ToEntity());
- }
+ context.SaveChanges();
- context.SaveChanges();
- }
- if (!context.ApiResources.Any())
+ foreach (var resource in StaticConfig.IdentityResources)
{
- foreach (var resource in Config.ApiResources(apiSecret))
- {
- context.ApiResources.Add(resource.ToEntity());
- }
-
- context.SaveChanges();
+ context.IdentityResources.AddIfNotExists(resource.ToEntity(), ir => resource.Name == ir.Name);
}
- if (!context.ApiScopes.Any())
+ context.SaveChanges();
+
+ foreach (var resource in StaticConfig.ApiResources(apiSecret))
{
- foreach (var resource in Config.ApiScopes)
- {
- context.ApiScopes.Add(resource.ToEntity());
- }
+ context.ApiResources.AddIfNotExists(resource.ToEntity(), ar => resource.Name == ar.Name);
+ }
+
+ context.SaveChanges();
- context.SaveChanges();
+ foreach (var resource in StaticConfig.ApiScopes)
+ {
+ context.ApiScopes.AddIfNotExists(resource.ToEntity(), apiScope => apiScope.Name == resource.Name);
}
+
+ context.SaveChanges();
}
host.Run();
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/Startup.cs b/OutOfSchool/OutOfSchool.IdentityServer/Startup.cs
index 386ab609e4..a14ee4dee1 100644
--- a/OutOfSchool/OutOfSchool.IdentityServer/Startup.cs
+++ b/OutOfSchool/OutOfSchool.IdentityServer/Startup.cs
@@ -65,6 +65,7 @@ public void ConfigureServices(IServiceCollection services)
{
if (env.IsEnvironment("Release"))
{
+ // TODO: Change this to something decent and configurable
options.IssuerUri = "http://hostname:5443";
}
})
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/appsettings.Azure.json b/OutOfSchool/OutOfSchool.IdentityServer/appsettings.Azure.json
index 4179e4c0f5..ee422f1116 100644
--- a/OutOfSchool/OutOfSchool.IdentityServer/appsettings.Azure.json
+++ b/OutOfSchool/OutOfSchool.IdentityServer/appsettings.Azure.json
@@ -5,5 +5,36 @@
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Warning"
}
+ },
+ "IdentityAccessConfig": {
+ "AdditionalIdentityClients": [
+ {
+ "ClientId": "angular",
+ "RedirectUris": [
+ "http://localhost:4200",
+ "http://oos.dmytrominochkin.cloud"
+ ],
+ "PostLogoutRedirectUris": [
+ "http://localhost:4200",
+ "http://oos.dmytrominochkin.cloud"
+ ],
+ "AllowedCorsOrigins": [
+ "http://localhost:4200",
+ "http://oos.dmytrominochkin.cloud"
+ ]
+ },
+ {
+ "ClientId": "Swagger",
+ "RedirectUris": [
+ "https://api.oos.dmytrominochkin.cloud/swagger/oauth2-redirect.html"
+ ],
+ "PostLogoutRedirectUris": [
+ "https://api.oos.dmytrominochkin.cloud/swagger/oauth2-redirect.html"
+ ],
+ "AllowedCorsOrigins": [
+ "https://api.oos.dmytrominochkin.cloud"
+ ]
+ }
+ ]
}
}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.IdentityServer/appsettings.json b/OutOfSchool/OutOfSchool.IdentityServer/appsettings.json
index 7c1edd3be1..3e00cc578f 100644
--- a/OutOfSchool/OutOfSchool.IdentityServer/appsettings.json
+++ b/OutOfSchool/OutOfSchool.IdentityServer/appsettings.json
@@ -17,5 +17,36 @@
"Username": "",
"Password": ""
}
+ },
+ "IdentityAccessConfig": {
+ "AdditionalIdentityClients": [
+ {
+ "ClientId": "angular",
+ "RedirectUris": [
+ "http://localhost:4200",
+ "http://oos.dmytrominochkin.cloud"
+ ],
+ "PostLogoutRedirectUris": [
+ "http://localhost:4200",
+ "http://oos.dmytrominochkin.cloud"
+ ],
+ "AllowedCorsOrigins": [
+ "http://localhost:4200",
+ "http://oos.dmytrominochkin.cloud"
+ ]
+ },
+ {
+ "ClientId": "Swagger",
+ "RedirectUris": [
+ "http://localhost:5000/swagger/oauth2-redirect.html"
+ ],
+ "PostLogoutRedirectUris": [
+ "http://localhost:5000/swagger/oauth2-redirect.html"
+ ],
+ "AllowedCorsOrigins": [
+ "http://localhost:5000"
+ ]
+ }
+ ]
}
}
diff --git a/OutOfSchool/OutOfSchool.WebApi/Properties/launchSettings.json b/OutOfSchool/OutOfSchool.WebApi/Properties/launchSettings.json
index c6684c196e..ce4f7b7a14 100644
--- a/OutOfSchool/OutOfSchool.WebApi/Properties/launchSettings.json
+++ b/OutOfSchool/OutOfSchool.WebApi/Properties/launchSettings.json
@@ -20,7 +20,7 @@
"OutOfSchool": {
"commandName": "Project",
"launchBrowser": true,
- "launchUrl": "Organization/TestOk",
+ "launchUrl": "swagger",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
diff --git a/OutOfSchool/OutOfSchool.WebApi/Startup.cs b/OutOfSchool/OutOfSchool.WebApi/Startup.cs
index 88feb153ab..8740dca06a 100644
--- a/OutOfSchool/OutOfSchool.WebApi/Startup.cs
+++ b/OutOfSchool/OutOfSchool.WebApi/Startup.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
@@ -11,11 +12,14 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Models;
using OutOfSchool.Services;
using OutOfSchool.Services.Models;
using OutOfSchool.Services.Repository;
using OutOfSchool.WebApi.Extensions;
using OutOfSchool.WebApi.Services;
+using OutOfSchool.WebApi.Util;
using Serilog;
namespace OutOfSchool.WebApi
@@ -61,7 +65,12 @@ public static void Configure(IApplicationBuilder app, IWebHostEnvironment env)
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
- app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Out Of School API"); });
+ app.UseSwaggerUI(c =>
+ {
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "Out Of School API");
+ c.OAuthClientId("Swagger");
+ c.OAuthUsePkce();
+ });
app.UseHttpsRedirection();
@@ -161,7 +170,25 @@ public void ConfigureServices(IServiceCollection services)
throw new InvalidOperationException("Unable to determine tag for endpoint.");
});
-
+ c.OperationFilter();
+ var baseUrl = Configuration["SwaggerIdentityAccess:BaseUrl"];
+ c.AddSecurityDefinition("Identity server", new OpenApiSecurityScheme
+ {
+ Description = "Identity server",
+ Type = SecuritySchemeType.OAuth2,
+ Flows = new OpenApiOAuthFlows
+ {
+ AuthorizationCode = new OpenApiOAuthFlow
+ {
+ AuthorizationUrl = new Uri($"{baseUrl}/connect/authorize", UriKind.Absolute),
+ TokenUrl = new Uri($"{baseUrl}/connect/token", UriKind.Absolute),
+ Scopes = new Dictionary
+ {
+ { "openid outofschoolapi.read offline_access", "Scopes" },
+ },
+ },
+ },
+ });
c.DocInclusionPredicate((name, api) => true);
});
diff --git a/OutOfSchool/OutOfSchool.WebApi/Util/AuthorizeCheckOperationFilter.cs b/OutOfSchool/OutOfSchool.WebApi/Util/AuthorizeCheckOperationFilter.cs
new file mode 100644
index 0000000000..68dd8581a7
--- /dev/null
+++ b/OutOfSchool/OutOfSchool.WebApi/Util/AuthorizeCheckOperationFilter.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.OpenApi.Any;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace OutOfSchool.WebApi.Util
+{
+ public class AuthorizeCheckOperationFilter : IOperationFilter
+ {
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ var hasAuthorize =
+ context.MethodInfo.DeclaringType!.GetCustomAttributes(true).OfType().Any()
+ || context.MethodInfo.GetCustomAttributes(true).OfType().Any();
+
+ if (hasAuthorize)
+ {
+ operation.Security = new List
+ {
+ new OpenApiSecurityRequirement
+ {
+ [
+ new OpenApiSecurityScheme {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = "Identity server",
+ },
+ }
+
+ ] = new List(),
+ },
+ };
+
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.WebApi/appsettings.Azure.json b/OutOfSchool/OutOfSchool.WebApi/appsettings.Azure.json
index 4179e4c0f5..16d9b06005 100644
--- a/OutOfSchool/OutOfSchool.WebApi/appsettings.Azure.json
+++ b/OutOfSchool/OutOfSchool.WebApi/appsettings.Azure.json
@@ -5,5 +5,8 @@
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Warning"
}
+ },
+ "SwaggerIdentityAccess": {
+ "BaseUrl": "http://auth.oos.dmytrominochkin.cloud"
}
}
\ No newline at end of file
diff --git a/OutOfSchool/OutOfSchool.WebApi/appsettings.json b/OutOfSchool/OutOfSchool.WebApi/appsettings.json
index 4a80e668f3..6a6f5302e8 100644
--- a/OutOfSchool/OutOfSchool.WebApi/appsettings.json
+++ b/OutOfSchool/OutOfSchool.WebApi/appsettings.json
@@ -9,6 +9,9 @@
"Identity": {
"Authority": "http://localhost:5443"
},
+ "SwaggerIdentityAccess": {
+ "BaseUrl": "http://localhost:5443"
+ },
"AllowedHosts": "*"
}
\ No newline at end of file