StartupHelpers.cs 19.4 KB
Newer Older
1
2
using System;
using System.Globalization;
janskoruba's avatar
janskoruba committed
3
using IdentityServer4.EntityFramework.Storage;
4
using Microsoft.AspNetCore.Authentication;
5
6
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
7
using Microsoft.AspNetCore.HttpOverrides;
8
using Microsoft.AspNetCore.Identity;
9
using Microsoft.AspNetCore.Identity.UI.Services;
10
11
12
13
14
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
15
using Microsoft.Extensions.DependencyInjection.Extensions;
16
using Microsoft.Extensions.Logging;
17
using Microsoft.Extensions.Options;
Michał Drzał's avatar
Michał Drzał committed
18
using SendGrid;
19
using Serilog;
20
using Skoruba.IdentityServer4.STS.Identity.Configuration;
21
using Skoruba.IdentityServer4.STS.Identity.Configuration.ApplicationParts;
22
using Skoruba.IdentityServer4.STS.Identity.Configuration.Constants;
Dmitrii Tarasov's avatar
Dmitrii Tarasov committed
23
using Skoruba.IdentityServer4.STS.Identity.Configuration.Interfaces;
24
using Skoruba.IdentityServer4.STS.Identity.Helpers.Localization;
25
using Skoruba.IdentityServer4.STS.Identity.Services;
26
using ILogger = Microsoft.Extensions.Logging.ILogger;
27
using System.Linq;
janskoruba's avatar
janskoruba committed
28
using Microsoft.Extensions.Hosting;
janskoruba's avatar
janskoruba committed
29
30
31
32
33
using Skoruba.IdentityServer4.Admin.EntityFramework.Interfaces;
using Skoruba.IdentityServer4.Admin.EntityFramework.MySql.Extensions;
using Skoruba.IdentityServer4.Admin.EntityFramework.PostgreSQL.Extensions;
using Skoruba.IdentityServer4.Admin.EntityFramework.Shared.Configuration;
using Skoruba.IdentityServer4.Admin.EntityFramework.SqlServer.Extensions;
34
35
36
37
38

namespace Skoruba.IdentityServer4.STS.Identity.Helpers
{
    public static class StartupHelpers
    {
janskoruba's avatar
janskoruba committed
39
40
41
42
        /// <summary>
        /// Register services for MVC and localization including available languages
        /// </summary>
        /// <param name="services"></param>
43
        public static void AddMvcWithLocalization<TUser, TKey>(this IServiceCollection services, IConfiguration configuration)
44
45
            where TUser : IdentityUser<TKey>
            where TKey : IEquatable<TKey>
46
47
48
        {
            services.AddLocalization(opts => { opts.ResourcesPath = ConfigurationConsts.ResourcesPath; });

49
            services.TryAddTransient(typeof(IGenericControllerLocalizer<>), typeof(GenericControllerLocalizer<>));
janskoruba's avatar
janskoruba committed
50

janskoruba's avatar
janskoruba committed
51
            services.AddControllersWithViews(o =>
52
53
54
                {
                    o.Conventions.Add(new GenericControllerRouteConvention());
                })
55
56
57
                .AddViewLocalization(
                    LanguageViewLocationExpanderFormat.Suffix,
                    opts => { opts.ResourcesPath = ConfigurationConsts.ResourcesPath; })
58
59
60
61
62
                .AddDataAnnotationsLocalization()
                .ConfigureApplicationPartManager(m =>
                {
                    m.FeatureProviders.Add(new GenericTypeControllerFeatureProvider<TUser, TKey>());
                });
63

64
            var cultureConfiguration = configuration.GetSection(nameof(CultureConfiguration)).Get<CultureConfiguration>();
65
66
67
            services.Configure<RequestLocalizationOptions>(
                opts =>
                {
68
69
70
71
                    // If cultures are specified in the configuration, use them (making sure they are among the available cultures),
                    // otherwise use all the available cultures
                    var supportedCultureCodes = (cultureConfiguration?.Cultures?.Count > 0 ?
                        cultureConfiguration.Cultures.Intersect(CultureConfiguration.AvailableCultures) :
janskoruba's avatar
janskoruba committed
72
                        CultureConfiguration.AvailableCultures).ToArray();
73

janskoruba's avatar
janskoruba committed
74
75
                    if (!supportedCultureCodes.Any()) supportedCultureCodes = CultureConfiguration.AvailableCultures;
                    var supportedCultures = supportedCultureCodes.Select(c => new CultureInfo(c)).ToList();
76

77
                    // If the default culture is specified use it, otherwise use CultureConfiguration.DefaultRequestCulture ("en")
janskoruba's avatar
janskoruba committed
78
                    var defaultCultureCode = string.IsNullOrEmpty(cultureConfiguration?.DefaultCulture) ?
79
                        CultureConfiguration.DefaultRequestCulture : cultureConfiguration?.DefaultCulture;
janskoruba's avatar
janskoruba committed
80

81
                    // If the default culture is not among the supported cultures, use the first supported culture as default
janskoruba's avatar
janskoruba committed
82
                    if (!supportedCultureCodes.Contains(defaultCultureCode)) defaultCultureCode = supportedCultureCodes.FirstOrDefault();
83
84

                    opts.DefaultRequestCulture = new RequestCulture(defaultCultureCode);
85
86
87
88
89
                    opts.SupportedCultures = supportedCultures;
                    opts.SupportedUICultures = supportedCultures;
                });
        }

janskoruba's avatar
janskoruba committed
90
91
92
93
        /// <summary>
        /// Using of Forwarded Headers and Referrer Policy
        /// </summary>
        /// <param name="app"></param>
94
95
96
97
98
99
        public static void UseSecurityHeaders(this IApplicationBuilder app)
        {
            app.UseForwardedHeaders(new ForwardedHeadersOptions()
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });
janskoruba's avatar
janskoruba committed
100

Michał Drzał's avatar
Michał Drzał committed
101
            app.UseHsts(options => options.MaxAge(days: 365));
janskoruba's avatar
janskoruba committed
102
            app.UseReferrerPolicy(options => options.NoReferrer());
103
104
        }

janskoruba's avatar
janskoruba committed
105
106
107
108
109
        /// <summary>
        /// Add email senders - configuration of sendgrid, smtp senders
        /// </summary>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
110
        public static void AddEmailSenders(this IServiceCollection services, IConfiguration configuration)
Michał Drzał's avatar
Michał Drzał committed
111
112
        {
            var smtpConfiguration = configuration.GetSection(nameof(SmtpConfiguration)).Get<SmtpConfiguration>();
janskoruba's avatar
janskoruba committed
113
            var sendGridConfiguration = configuration.GetSection(nameof(SendgridConfiguration)).Get<SendgridConfiguration>();
Michał Drzał's avatar
Michał Drzał committed
114

janskoruba's avatar
janskoruba committed
115
            if (sendGridConfiguration != null && !string.IsNullOrWhiteSpace(sendGridConfiguration.ApiKey))
Michał Drzał's avatar
Michał Drzał committed
116
            {
janskoruba's avatar
janskoruba committed
117
118
                services.AddSingleton<ISendGridClient>(_ => new SendGridClient(sendGridConfiguration.ApiKey));
                services.AddSingleton(sendGridConfiguration);
Michał Drzał's avatar
Michał Drzał committed
119
120
                services.AddTransient<IEmailSender, SendgridEmailSender>();
            }
121
            else if (smtpConfiguration != null && !string.IsNullOrWhiteSpace(smtpConfiguration.Host))
Michał Drzał's avatar
Michał Drzał committed
122
123
124
            {
                services.AddSingleton(smtpConfiguration);
                services.AddTransient<IEmailSender, SmtpEmailSender>();
125
126
            }
            else
Michał Drzał's avatar
Michał Drzał committed
127
128
129
130
131
            {
                services.AddSingleton<IEmailSender, EmailSender>();
            }
        }

janskoruba's avatar
janskoruba committed
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
        /// <summary>
        /// Register DbContexts for IdentityServer ConfigurationStore and PersistedGrants and Identity
        /// Configure the connection strings in AppSettings.json
        /// </summary>
        /// <typeparam name="TConfigurationDbContext"></typeparam>
        /// <typeparam name="TPersistedGrantDbContext"></typeparam>
        /// <typeparam name="TIdentityDbContext"></typeparam>
        /// <param name="services"></param>
        /// <param name="environment"></param>
        /// <param name="configuration"></param>
        public static void RegisterDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext>(this IServiceCollection services, IWebHostEnvironment environment, IConfiguration configuration)
            where TIdentityDbContext : DbContext
            where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
        {
            if (environment.IsStaging())
            {
                services.RegisterDbContextsInMemory<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext>();
            }
            else
            {
                services.RegisterDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext>(configuration);
            }
        }


        /// <summary>
        /// Register DbContexts for IdentityServer ConfigurationStore and PersistedGrants and Identity
        /// Configure the connection strings in AppSettings.json
        /// </summary>
        /// <typeparam name="TConfigurationDbContext"></typeparam>
        /// <typeparam name="TPersistedGrantDbContext"></typeparam>
        /// <typeparam name="TIdentityDbContext"></typeparam>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        public static void RegisterDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext>(this IServiceCollection services, IConfiguration configuration)
            where TIdentityDbContext : DbContext
            where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
        {
            var databaseProvider = configuration.GetSection(nameof(DatabaseProviderConfiguration)).Get<DatabaseProviderConfiguration>();
janskoruba's avatar
janskoruba committed
173
            
janskoruba's avatar
janskoruba committed
174
175
176
177
            var identityConnectionString = configuration.GetConnectionString(ConfigurationConsts.IdentityDbConnectionStringKey);
            var configurationConnectionString = configuration.GetConnectionString(ConfigurationConsts.ConfigurationDbConnectionStringKey);
            var persistedGrantsConnectionString = configuration.GetConnectionString(ConfigurationConsts.PersistedGrantDbConnectionStringKey);

janskoruba's avatar
janskoruba committed
178
            switch (databaseProvider.ProviderType)
janskoruba's avatar
janskoruba committed
179
180
181
182
183
184
185
186
187
188
189
            {
                case DatabaseProviderType.SqlServer:
                    services.RegisterSqlServerDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext>(identityConnectionString, configurationConnectionString, persistedGrantsConnectionString);
                    break;
                case DatabaseProviderType.PostgreSQL:
                    services.RegisterNpgSqlDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext>(identityConnectionString, configurationConnectionString, persistedGrantsConnectionString);
                    break;
                case DatabaseProviderType.MySql:
                    services.RegisterMySqlDbContexts<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext>(identityConnectionString, configurationConnectionString, persistedGrantsConnectionString);
                    break;
                default:
190
                    throw new ArgumentOutOfRangeException(nameof(databaseProvider.ProviderType), $@"The value needs to be one of {string.Join(", ", Enum.GetNames(typeof(DatabaseProviderType)))}.");
janskoruba's avatar
janskoruba committed
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
            }
        }

        /// <summary>
        /// Register InMemory DbContexts for IdentityServer ConfigurationStore and PersistedGrants and Identity
        /// Configure the connection strings in AppSettings.json
        /// </summary>
        /// <typeparam name="TConfigurationDbContext"></typeparam>
        /// <typeparam name="TPersistedGrantDbContext"></typeparam>
        /// <typeparam name="TIdentityDbContext"></typeparam>
        /// <param name="services"></param>
        public static void RegisterDbContextsInMemory<TIdentityDbContext, TConfigurationDbContext, TPersistedGrantDbContext>(
            this IServiceCollection services)
            where TIdentityDbContext : DbContext
            where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
        {
            var identityDatabaseName = Guid.NewGuid().ToString();
            services.AddDbContext<TIdentityDbContext>(optionsBuilder => optionsBuilder.UseInMemoryDatabase(identityDatabaseName));

            var configurationDatabaseName = Guid.NewGuid().ToString();
            var operationalDatabaseName = Guid.NewGuid().ToString();

            services.AddConfigurationDbContext<TConfigurationDbContext>(options =>
            {
                options.ConfigureDbContext = b => b.UseInMemoryDatabase(configurationDatabaseName);
            });

            services.AddOperationalDbContext<TPersistedGrantDbContext>(options =>
            {
                options.ConfigureDbContext = b => b.UseInMemoryDatabase(operationalDatabaseName);
            });
        }

janskoruba's avatar
janskoruba committed
225
226
227
228
229
230
231
232
233
234
235
        /// <summary>
        /// Add services for authentication, including Identity model, IdentityServer4 and external providers
        /// </summary>
        /// <typeparam name="TIdentityDbContext">DbContext for Identity</typeparam>
        /// <typeparam name="TUserIdentity">User Identity class</typeparam>
        /// <typeparam name="TUserIdentityRole">User Identity Role class</typeparam>
        /// <typeparam name="TConfigurationDbContext"></typeparam>
        /// <typeparam name="TPersistedGrantDbContext"></typeparam>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <param name="logger"></param>
janskoruba's avatar
janskoruba committed
236
237
238
        public static void AddAuthenticationServices<TConfigurationDbContext, TPersistedGrantDbContext, TIdentityDbContext, TUserIdentity, TUserIdentityRole>(this IServiceCollection services, IConfiguration configuration, ILogger logger) where TIdentityDbContext : DbContext
            where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
239
240
            where TUserIdentity : class
            where TUserIdentityRole : class
241
        {
242
            var loginConfiguration = GetLoginConfiguration(configuration);
243
            var registrationConfiguration = GetRegistrationConfiguration(configuration);
Michał Drzał's avatar
Michał Drzał committed
244
245

            services
246
                .AddSingleton(registrationConfiguration)
Michał Drzał's avatar
Michał Drzał committed
247
248
249
                .AddSingleton(loginConfiguration)
                .AddScoped<UserResolver<TUserIdentity>>()
                .AddIdentity<TUserIdentity, TUserIdentityRole>(options =>
250
251
252
                {
                    options.User.RequireUniqueEmail = true;
                })
janskoruba's avatar
janskoruba committed
253
                .AddEntityFrameworkStores<TIdentityDbContext>()
254
                .AddDefaultTokenProviders();
255

256
257
258
259
260
261
            services.Configure<IISOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows";
                iis.AutomaticAuthentication = false;
            });

262
263
264
265
            var authenticationBuilder = services.AddAuthentication();

            AddExternalProviders(authenticationBuilder, configuration);

janskoruba's avatar
janskoruba committed
266
            AddIdentityServer<TConfigurationDbContext, TPersistedGrantDbContext, TUserIdentity>(services, configuration, logger);
267
268
        }

269
270
271
272
273
274
275
276
        /// <summary>
        /// Get configuration for login
        /// </summary>
        /// <param name="configuration"></param>
        /// <returns></returns>
        private static LoginConfiguration GetLoginConfiguration(IConfiguration configuration)
        {
            var loginConfiguration = configuration.GetSection(nameof(LoginConfiguration)).Get<LoginConfiguration>();
277

278
279
280
281
282
283
284
285
286
            // Cannot load configuration - use default configuration values
            if (loginConfiguration == null)
            {
                return new LoginConfiguration();
            }

            return loginConfiguration;
        }

287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
        /// <summary>
        /// Get configuration for registration
        /// </summary>
        /// <param name="configuration"></param>
        /// <returns></returns>
        private static RegisterConfiguration GetRegistrationConfiguration(IConfiguration configuration)
        {
            var registerConfiguration = configuration.GetSection(nameof(RegisterConfiguration)).Get<RegisterConfiguration>();

            // Cannot load configuration - use default configuration values
            if (registerConfiguration == null)
            {
                return new RegisterConfiguration();
            }

            return registerConfiguration;
        }

janskoruba's avatar
janskoruba committed
305
306
307
308
        /// <summary>
        /// Add configuration for IdentityServer4
        /// </summary>
        /// <typeparam name="TUserIdentity"></typeparam>
309
310
        /// <typeparam name="TConfigurationDbContext"></typeparam>
        /// <typeparam name="TPersistedGrantDbContext"></typeparam>
janskoruba's avatar
janskoruba committed
311
312
313
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <param name="logger"></param>
314
315
        private static void AddIdentityServer<TConfigurationDbContext, TPersistedGrantDbContext, TUserIdentity>(
            IServiceCollection services,
janskoruba's avatar
janskoruba committed
316
317
318
            IConfiguration configuration, ILogger logger)
            where TPersistedGrantDbContext : DbContext, IAdminPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IAdminConfigurationDbContext
janskoruba's avatar
janskoruba committed
319
            where TUserIdentity : class
320
        {
321
            var builder = services.AddIdentityServer(options =>
322
323
324
325
326
327
                {
                    options.Events.RaiseErrorEvents = true;
                    options.Events.RaiseInformationEvents = true;
                    options.Events.RaiseFailureEvents = true;
                    options.Events.RaiseSuccessEvents = true;
                })
janskoruba's avatar
janskoruba committed
328
329
330
                .AddConfigurationStore<TConfigurationDbContext>()
                .AddOperationalStore<TPersistedGrantDbContext>()
                .AddAspNetIdentity<TUserIdentity>();
331

332
            builder.AddCustomSigningCredential(configuration);
333
            builder.AddCustomValidationKey(configuration, logger);
334
335
        }

janskoruba's avatar
janskoruba committed
336
337
338
339
340
        /// <summary>
        /// Add external providers
        /// </summary>
        /// <param name="authenticationBuilder"></param>
        /// <param name="configuration"></param>
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
        private static void AddExternalProviders(AuthenticationBuilder authenticationBuilder,
            IConfiguration configuration)
        {
            var externalProviderConfiguration = configuration.GetSection(nameof(ExternalProvidersConfiguration)).Get<ExternalProvidersConfiguration>();

            if (externalProviderConfiguration.UseGitHubProvider)
            {
                authenticationBuilder.AddGitHub(options =>
                {
                    options.ClientId = externalProviderConfiguration.GitHubClientId;
                    options.ClientSecret = externalProviderConfiguration.GitHubClientSecret;
                    options.Scope.Add("user:email");
                });
            }
        }

janskoruba's avatar
janskoruba committed
357
358
359
360
        /// <summary>
        /// Register middleware for localization
        /// </summary>
        /// <param name="app"></param>
361
362
363
364
365
        public static void UseMvcLocalizationServices(this IApplicationBuilder app)
        {
            var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
            app.UseRequestLocalization(options.Value);
        }
366

janskoruba's avatar
janskoruba committed
367
368
369
370
371
372
        /// <summary>
        /// Add configuration for logging
        /// </summary>
        /// <param name="app"></param>
        /// <param name="loggerFactory"></param>
        /// <param name="configuration"></param>
janskoruba's avatar
janskoruba committed
373
374
375
376
377
378
        public static void AddLogging(this IApplicationBuilder app, ILoggerFactory loggerFactory, IConfiguration configuration)
        {
            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .CreateLogger();
        }
379
380
381
382
383

        /// <summary>
        /// Add authorization policies
        /// </summary>
        /// <param name="services"></param>
janskoruba's avatar
janskoruba committed
384
        /// <param name="rootConfiguration"></param>
385
386
        public static void AddAuthorizationPolicies(this IServiceCollection services,
                IRootConfiguration rootConfiguration)
387
388
389
390
        {
            services.AddAuthorization(options =>
            {
                options.AddPolicy(AuthorizationConsts.AdministrationPolicy,
391
                    policy => policy.RequireRole(rootConfiguration.AdminConfiguration.AdministrationRole));
392
393
            });
        }
janskoruba's avatar
janskoruba committed
394
    }
395
}