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

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

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

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

61
            var cultureConfiguration = configuration.GetSection(nameof(CultureConfiguration)).Get<CultureConfiguration>();
62
63
64
            services.Configure<RequestLocalizationOptions>(
                opts =>
                {
65
66
67
68
                    // 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
69
70
71
72
                        CultureConfiguration.AvailableCultures).ToArray();

                    if (!supportedCultureCodes.Any()) supportedCultureCodes = CultureConfiguration.AvailableCultures;
                    var supportedCultures = supportedCultureCodes.Select(c => new CultureInfo(c)).ToList();
73

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

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

                    opts.DefaultRequestCulture = new RequestCulture(defaultCultureCode);
82
83
84
85
86
                    opts.SupportedCultures = supportedCultures;
                    opts.SupportedUICultures = supportedCultures;
                });
        }

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

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

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

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

janskoruba's avatar
janskoruba committed
129
130
131
132
133
134
135
136
137
138
139
140
        /// <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="hostingEnvironment"></param>
        /// <param name="configuration"></param>
        /// <param name="logger"></param>
janskoruba's avatar
janskoruba committed
141
        public static void AddAuthenticationServices<TConfigurationDbContext, TPersistedGrantDbContext, TIdentityDbContext, TUserIdentity, TUserIdentityRole>(this IServiceCollection services, IWebHostEnvironment hostingEnvironment, IConfiguration configuration, ILogger logger)
janskoruba's avatar
janskoruba committed
142
143
144
            where TPersistedGrantDbContext : DbContext, IPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IConfigurationDbContext
            where TIdentityDbContext : DbContext
145
146
            where TUserIdentity : class
            where TUserIdentityRole : class
147
        {
148
            var loginConfiguration = GetLoginConfiguration(configuration);
149
            var registrationConfiguration = GetRegistrationConfiguration(configuration);
Michał Drzał's avatar
Michał Drzał committed
150
151

            services
152
                .AddSingleton(registrationConfiguration)
Michał Drzał's avatar
Michał Drzał committed
153
154
155
                .AddSingleton(loginConfiguration)
                .AddScoped<UserResolver<TUserIdentity>>()
                .AddIdentity<TUserIdentity, TUserIdentityRole>(options =>
156
157
158
                {
                    options.User.RequireUniqueEmail = true;
                })
janskoruba's avatar
janskoruba committed
159
                .AddEntityFrameworkStores<TIdentityDbContext>()
160
                .AddDefaultTokenProviders();
161

162
163
164
165
166
167
            services.Configure<IISOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows";
                iis.AutomaticAuthentication = false;
            });

168
169
170
171
            var authenticationBuilder = services.AddAuthentication();

            AddExternalProviders(authenticationBuilder, configuration);

172
            AddIdentityServer<TConfigurationDbContext, TPersistedGrantDbContext, TUserIdentity>(services, configuration, logger, hostingEnvironment);
173
174
        }

175
176
177
178
179
180
181
182
        /// <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>();
183

184
185
186
187
188
189
190
191
192
            // Cannot load configuration - use default configuration values
            if (loginConfiguration == null)
            {
                return new LoginConfiguration();
            }

            return loginConfiguration;
        }

193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
        /// <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;
        }

211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
        /// <summary>
        /// Configuration root configuration
        /// </summary>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <returns></returns>
        public static IServiceCollection ConfigureRootConfiguration(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddOptions();

            services.Configure<AdminConfiguration>(configuration.GetSection(ConfigurationConsts.AdminConfigurationKey));
            services.Configure<RegisterConfiguration>(configuration.GetSection(ConfigurationConsts.RegisterConfiguration));

            services.TryAddSingleton<IRootConfiguration, RootConfiguration>();

            return services;
        }

janskoruba's avatar
janskoruba committed
229
230
231
232
        /// <summary>
        /// Add configuration for IdentityServer4
        /// </summary>
        /// <typeparam name="TUserIdentity"></typeparam>
233
234
        /// <typeparam name="TConfigurationDbContext"></typeparam>
        /// <typeparam name="TPersistedGrantDbContext"></typeparam>
janskoruba's avatar
janskoruba committed
235
236
237
        /// <param name="services"></param>
        /// <param name="configuration"></param>
        /// <param name="logger"></param>
238
239
240
        /// <param name="hostingEnvironment"></param>
        private static void AddIdentityServer<TConfigurationDbContext, TPersistedGrantDbContext, TUserIdentity>(
            IServiceCollection services,
janskoruba's avatar
janskoruba committed
241
            IConfiguration configuration, ILogger logger, IWebHostEnvironment hostingEnvironment)
janskoruba's avatar
janskoruba committed
242
243
244
            where TUserIdentity : class
            where TPersistedGrantDbContext : DbContext, IPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IConfigurationDbContext
245
        {
246
            var builder = services.AddIdentityServer(options =>
247
248
249
250
251
252
                {
                    options.Events.RaiseErrorEvents = true;
                    options.Events.RaiseInformationEvents = true;
                    options.Events.RaiseFailureEvents = true;
                    options.Events.RaiseSuccessEvents = true;
                })
253
                .AddAspNetIdentity<TUserIdentity>()
254
                .AddIdentityServerStoresWithDbContexts<TConfigurationDbContext, TPersistedGrantDbContext>(configuration, hostingEnvironment);
255

256
257
            builder.AddCustomSigningCredential(configuration, logger);
            builder.AddCustomValidationKey(configuration, logger);
258
259
        }

janskoruba's avatar
janskoruba committed
260
261
262
263
264
        /// <summary>
        /// Add external providers
        /// </summary>
        /// <param name="authenticationBuilder"></param>
        /// <param name="configuration"></param>
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
        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");
                });
            }
        }

281
282
283
284
285
286
        /// <summary>
        /// Add DbContext for Identity
        /// </summary>
        /// <typeparam name="TContext"></typeparam>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
287
288
        /// <param name="hostingEnvironment"></param>
        public static void AddIdentityDbContext<TContext>(this IServiceCollection services,
janskoruba's avatar
janskoruba committed
289
            IConfiguration configuration, IWebHostEnvironment hostingEnvironment)
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
            where TContext : DbContext
        {
            if (hostingEnvironment.IsStaging())
            {
                RegisterIdentityDbContextStaging<TContext>(services);
            }
            else
            {
                RegisterIdentityDbContext<TContext>(services, configuration);
            }
        }

        private static void RegisterIdentityDbContextStaging<TContext>(IServiceCollection services) where TContext : DbContext
        {
            var identityDatabaseName = Guid.NewGuid().ToString();

            services.AddDbContext<TContext>(optionsBuilder => optionsBuilder.UseInMemoryDatabase(identityDatabaseName));
        }

        private static void RegisterIdentityDbContext<TContext>(IServiceCollection services, IConfiguration configuration)
310
311
312
313
314
315
            where TContext : DbContext
        {
            var connectionString = configuration.GetConnectionString(ConfigurationConsts.IdentityDbConnectionStringKey);
            services.AddDbContext<TContext>(options => options.UseSqlServer(connectionString));
        }

janskoruba's avatar
janskoruba committed
316
317
318
319
320
321
        /// <summary>
        /// Add shared DbContext for Identity and IdentityServer4 stores
        /// </summary>
        /// <typeparam name="TContext"></typeparam>
        /// <param name="services"></param>
        /// <param name="configuration"></param>
322
        public static void AddDbContexts<TContext>(this IServiceCollection services, IConfiguration configuration)
janskoruba's avatar
janskoruba committed
323
            where TContext : DbContext
324
325
326
327
328
        {
            var connectionString = configuration.GetConnectionString(ConfigurationConsts.AdminConnectionStringKey);
            services.AddDbContext<TContext>(options => options.UseSqlServer(connectionString));
        }

janskoruba's avatar
janskoruba committed
329
330
331
332
        /// <summary>
        /// Register DbContexts and configure stores for IdentityServer4
        /// </summary>
        /// <typeparam name="TConfigurationDbContext"></typeparam>
333
        /// <typeparam name="TPersistedGrantDbContext"></typeparam>
janskoruba's avatar
janskoruba committed
334
335
        /// <param name="builder"></param>
        /// <param name="configuration"></param>
336
337
338
        /// <param name="hostingEnvironment"></param>
        public static IIdentityServerBuilder AddIdentityServerStoresWithDbContexts<TConfigurationDbContext,
            TPersistedGrantDbContext>(this IIdentityServerBuilder builder, IConfiguration configuration,
janskoruba's avatar
janskoruba committed
339
            IWebHostEnvironment hostingEnvironment)
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
            where TPersistedGrantDbContext : DbContext, IPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IConfigurationDbContext
        {
            if (hostingEnvironment.IsStaging())
            {
                return RegisterIdentityServerStoresWithDbContextsStaging<TConfigurationDbContext, TPersistedGrantDbContext>(builder, configuration);
            }
            else
            {
                return RegisterIdentityServerStoresWithDbContexts<TConfigurationDbContext, TPersistedGrantDbContext>(builder, configuration);
            }
        }

        private static IIdentityServerBuilder
            RegisterIdentityServerStoresWithDbContextsStaging<TConfigurationDbContext, TPersistedGrantDbContext>(
                IIdentityServerBuilder builder, IConfiguration configuration)
            where TPersistedGrantDbContext : DbContext, IPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IConfigurationDbContext
        {
            var configurationDatabaseName = Guid.NewGuid().ToString();
            var operationalDatabaseName = Guid.NewGuid().ToString();

            builder.AddConfigurationStore<TConfigurationDbContext>(options =>
            {
                options.ConfigureDbContext = b => b.UseInMemoryDatabase(configurationDatabaseName);
            });

            builder.AddOperationalStore<TPersistedGrantDbContext>(options =>
            {
                options.ConfigureDbContext = b => b.UseInMemoryDatabase(operationalDatabaseName);
            });

            return builder;
        }

        private static IIdentityServerBuilder
            RegisterIdentityServerStoresWithDbContexts<TConfigurationDbContext, TPersistedGrantDbContext>(
                IIdentityServerBuilder builder, IConfiguration configuration)
janskoruba's avatar
janskoruba committed
378
379
380
            where TPersistedGrantDbContext : DbContext, IPersistedGrantDbContext
            where TConfigurationDbContext : DbContext, IConfigurationDbContext
        {
381
382
            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

janskoruba's avatar
janskoruba committed
383
384
385
386
            // Config DB from existing connection
            builder.AddConfigurationStore<TConfigurationDbContext>(options =>
            {
                options.ConfigureDbContext = b =>
387
388
                    b.UseSqlServer(
                        configuration.GetConnectionString(ConfigurationConsts.ConfigurationDbConnectionStringKey),
389
                        sql => sql.MigrationsAssembly(migrationsAssembly));
janskoruba's avatar
janskoruba committed
390
391
392
393
394
395
396
397
398
399
            });

            // Operational DB from existing connection
            builder.AddOperationalStore<TPersistedGrantDbContext>(options =>
            {
                options.EnableTokenCleanup = true;
#if DEBUG
                options.TokenCleanupInterval = 15;
#endif
                options.ConfigureDbContext = b =>
400
401
                    b.UseSqlServer(
                        configuration.GetConnectionString(ConfigurationConsts.PersistedGrantDbConnectionStringKey),
402
                        sql => sql.MigrationsAssembly(migrationsAssembly));
janskoruba's avatar
janskoruba committed
403
404
405
406
407
408
409
410
411
            });

            return builder;
        }

        /// <summary>
        /// Register middleware for localization
        /// </summary>
        /// <param name="app"></param>
412
413
414
415
416
        public static void UseMvcLocalizationServices(this IApplicationBuilder app)
        {
            var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
            app.UseRequestLocalization(options.Value);
        }
417

janskoruba's avatar
janskoruba committed
418
419
420
421
422
423
        /// <summary>
        /// Add configuration for logging
        /// </summary>
        /// <param name="app"></param>
        /// <param name="loggerFactory"></param>
        /// <param name="configuration"></param>
janskoruba's avatar
janskoruba committed
424
425
426
427
428
429
        public static void AddLogging(this IApplicationBuilder app, ILoggerFactory loggerFactory, IConfiguration configuration)
        {
            Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .CreateLogger();
        }
430
431
432
433
434

        /// <summary>
        /// Add authorization policies
        /// </summary>
        /// <param name="services"></param>
435
436
        public static void AddAuthorizationPolicies(this IServiceCollection services,
                IRootConfiguration rootConfiguration)
437
438
439
440
        {
            services.AddAuthorization(options =>
            {
                options.AddPolicy(AuthorizationConsts.AdministrationPolicy,
441
                    policy => policy.RequireRole(rootConfiguration.AdminConfiguration.AdministrationRole));
442
443
            });
        }
janskoruba's avatar
janskoruba committed
444
    }
445
}