Skip to content

Commit

Permalink
IdentityServer4 IdentityProvider for Dev/Test environment Authenticat…
Browse files Browse the repository at this point in the history
…ion. (#18)

* Developer Identity provider with authentication.

* Rename healthServerName -> HealthServerKey

* Re-organize code.
Move configuration models to configuration folder.
Rename project

* comment change

Co-authored-by: Smitha Saligrama <[email protected]>
  • Loading branch information
smithago and smithago authored Apr 21, 2020
1 parent 640c9bb commit 7bcefd5
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 10 deletions.
27 changes: 17 additions & 10 deletions Microsoft.Health.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,29 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Abstractions", "src\Microsoft.Health.Abstractions\Microsoft.Health.Abstractions.csproj", "{E0CD7A0A-116D-4612-83BE-0DE036DADDF1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Api", "src\Microsoft.Health.Api\Microsoft.Health.Api.csproj", "{47D87BAA-D779-41A4-BC08-496EA36DC394}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Api", "src\Microsoft.Health.Api\Microsoft.Health.Api.csproj", "{47D87BAA-D779-41A4-BC08-496EA36DC394}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Core", "src\Microsoft.Health.Core\Microsoft.Health.Core.csproj", "{F75E1C38-B407-4040-94B0-1AE07C05D6EE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Core", "src\Microsoft.Health.Core\Microsoft.Health.Core.csproj", "{F75E1C38-B407-4040-94B0-1AE07C05D6EE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Api.UnitTests", "src\Microsoft.Health.Api.UnitTests\Microsoft.Health.Api.UnitTests.csproj", "{6E6F2E4B-DAF7-46B9-8589-045237AD65CE}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Api.UnitTests", "src\Microsoft.Health.Api.UnitTests\Microsoft.Health.Api.UnitTests.csproj", "{6E6F2E4B-DAF7-46B9-8589-045237AD65CE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.Core.UnitTests", "src\Microsoft.Health.Core.UnitTests\Microsoft.Health.Core.UnitTests.csproj", "{3670CFEA-068C-497A-8123-E149820F02DC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Core.UnitTests", "src\Microsoft.Health.Core.UnitTests\Microsoft.Health.Core.UnitTests.csproj", "{3670CFEA-068C-497A-8123-E149820F02DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.SqlServer", "src\Microsoft.Health.SqlServer\Microsoft.Health.SqlServer.csproj", "{CF5D97F8-DE8F-4965-923D-82CA40CC9E49}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.SqlServer", "src\Microsoft.Health.SqlServer\Microsoft.Health.SqlServer.csproj", "{CF5D97F8-DE8F-4965-923D-82CA40CC9E49}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.SqlServer.Api", "src\Microsoft.Health.SqlServer.Api\Microsoft.Health.SqlServer.Api.csproj", "{561B1075-FA22-4D4E-881B-366230423B2D}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.SqlServer.Api", "src\Microsoft.Health.SqlServer.Api\Microsoft.Health.SqlServer.Api.csproj", "{561B1075-FA22-4D4E-881B-366230423B2D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.SqlServer.Api.UnitTests", "src\Microsoft.Health.SqlServer.Api.UnitTests\Microsoft.Health.SqlServer.Api.UnitTests.csproj", "{7650CF08-00B4-419B-8AF9-26E4079F08F2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.SqlServer.Api.UnitTests", "src\Microsoft.Health.SqlServer.Api.UnitTests\Microsoft.Health.SqlServer.Api.UnitTests.csproj", "{7650CF08-00B4-419B-8AF9-26E4079F08F2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.SqlServer.UnitTests", "src\Microsoft.Health.SqlServer.UnitTests\Microsoft.Health.SqlServer.UnitTests.csproj", "{8FEF74DC-94A5-4E20-9BE2-095941935EC4}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.SqlServer.UnitTests", "src\Microsoft.Health.SqlServer.UnitTests\Microsoft.Health.SqlServer.UnitTests.csproj", "{8FEF74DC-94A5-4E20-9BE2-095941935EC4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.SqlServer.Tests.E2E", "test\Microsoft.Health.SqlServer.Tests.E2E\Microsoft.Health.SqlServer.Tests.E2E.csproj", "{C74D5E00-8BE4-4C99-8A59-9D58D255C140}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.SqlServer.Tests.E2E", "test\Microsoft.Health.SqlServer.Tests.E2E\Microsoft.Health.SqlServer.Tests.E2E.csproj", "{C74D5E00-8BE4-4C99-8A59-9D58D255C140}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{CCD9FF99-E177-446E-B9E5-9F570FD96A34}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Health.SqlServer.Web", "test\Microsoft.Health.SqlServer.Web\Microsoft.Health.SqlServer.Web.csproj", "{85781F6A-28D8-4850-A991-51157E5DF46E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.SqlServer.Web", "test\Microsoft.Health.SqlServer.Web\Microsoft.Health.SqlServer.Web.csproj", "{85781F6A-28D8-4850-A991-51157E5DF46E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Health.Development.IdentityProvider", "src\Microsoft.Health.Development.IdentityProvider\Microsoft.Health.Development.IdentityProvider.csproj", "{8DFE583A-60D6-4014-A95A-01AAE8D35A0E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -105,6 +107,10 @@ Global
{85781F6A-28D8-4850-A991-51157E5DF46E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85781F6A-28D8-4850-A991-51157E5DF46E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85781F6A-28D8-4850-A991-51157E5DF46E}.Release|Any CPU.Build.0 = Release|Any CPU
{8DFE583A-60D6-4014-A95A-01AAE8D35A0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8DFE583A-60D6-4014-A95A-01AAE8D35A0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8DFE583A-60D6-4014-A95A-01AAE8D35A0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8DFE583A-60D6-4014-A95A-01AAE8D35A0E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -124,6 +130,7 @@ Global
{8FEF74DC-94A5-4E20-9BE2-095941935EC4} = {8AD2A324-DAB5-4380-94A5-31F7D817C384}
{C74D5E00-8BE4-4C99-8A59-9D58D255C140} = {CCD9FF99-E177-446E-B9E5-9F570FD96A34}
{85781F6A-28D8-4850-A991-51157E5DF46E} = {CCD9FF99-E177-446E-B9E5-9F570FD96A34}
{8DFE583A-60D6-4014-A95A-01AAE8D35A0E} = {8AD2A324-DAB5-4380-94A5-31F7D817C384}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
RESX_SortFileContentOnSave = True
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;

namespace Microsoft.Health.Development.IdentityProvider.Configuration
{
public class Application
{
public string Id { get; set; }

public IList<string> Roles { get; } = new List<string>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;

namespace Microsoft.Health.Development.IdentityProvider.Configuration
{
public class Provider
{
public const string Audience = "health-api";
public const string LastModifiedClaim = "appid";
public const string ClientIdClaim = "client_id";

public bool Enabled { get; set; }

public IList<Application> ClientApplications { get; } = new List<Application>();

public IList<User> Users { get; } = new List<User>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;

namespace Microsoft.Health.Development.IdentityProvider.Configuration
{
public class User
{
public string Id { get; set; }

public IList<string> Roles { get; } = new List<string>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Ensure.That" Version="8.1.2" />
<PackageReference Include="IdentityServer4" Version="3.1.2" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using EnsureThat;
using IdentityServer4.Models;
using IdentityServer4.Test;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Config = Microsoft.Health.Development.IdentityProvider.Configuration;

namespace Microsoft.Health.Development.IdentityProvider
{
public static class RegistrationExtensions
{
private const string WrongAudienceClient = "wrongAudienceClient";

/// <summary>
/// Adds an in-process identity provider if enabled in configuration.
/// </summary>
/// <param name="services">The services collection.</param>
/// <param name="configuration">The configuration root. The "DevelopmentIdentityProvider" section will be used to populate configuration values.</param>
/// <returns>The same services collection.</returns>
public static IServiceCollection AddDevelopmentIdentityProvider(this IServiceCollection services, IConfiguration configuration)
{
EnsureArg.IsNotNull(services, nameof(services));
EnsureArg.IsNotNull(configuration, nameof(configuration));

var developmentIdentityProviderConfiguration = new Config.Provider();
configuration.GetSection("DevelopmentIdentityProvider").Bind(developmentIdentityProviderConfiguration);
services.AddSingleton(Options.Create(developmentIdentityProviderConfiguration));

if (developmentIdentityProviderConfiguration.Enabled)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(new[]
{
new ApiResource(Config.Provider.Audience) { },
new ApiResource(WrongAudienceClient) { },
})
.AddTestUsers(developmentIdentityProviderConfiguration.Users?.Select(user =>
new TestUser
{
Username = user.Id,
Password = user.Id,
IsActive = true,
SubjectId = user.Id,
}).ToList())
.AddInMemoryClients(
developmentIdentityProviderConfiguration.ClientApplications.Select(
applicationConfiguration =>
new Client
{
ClientId = applicationConfiguration.Id,

// client credentials and ROPC for testing
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,

// secret for authentication
ClientSecrets = { new Secret(applicationConfiguration.Id.Sha256()) },

// scopes that client has access to
AllowedScopes = { Config.Provider.Audience, WrongAudienceClient },
}));
}

return services;
}

/// <summary>
/// Adds the in-process identity provider to the pipeline if enabled in configuration.
/// </summary>
/// <param name="app">The application builder</param>
/// <returns>The application builder.</returns>
public static IApplicationBuilder UseDevelopmentIdentityProviderIfConfigured(this IApplicationBuilder app)
{
EnsureArg.IsNotNull(app, nameof(app));
if (app.ApplicationServices.GetService<IOptions<Config.Provider>>()?.Value?.Enabled == true)
{
app.UseIdentityServer();
}

return app;
}

/// <summary>
/// If <paramref name="existingConfiguration"/> contains a value for TestAuthEnvironment:FilePath and the file exists, this method adds an <see cref="IConfigurationBuilder"/> that
/// reads a testauthenvironment.json file and reshapes it to fit into the expected schema of the server
/// configuration. Also sets the security audience and sets the development identity server as enabled.
/// This is an optional configuration source and is only intended to be used for local development.
/// </summary>
/// <param name="configurationBuilder">The configuration builder.</param>
/// <param name="existingConfiguration">Configuration root</param>
/// <param name="healthServerKey">Server key used in the appsettings.json.</param>
/// <returns>The same configuration builder.</returns>
public static IConfigurationBuilder AddDevelopmentAuthEnvironmentIfConfigured(this IConfigurationBuilder configurationBuilder, IConfigurationRoot existingConfiguration, string healthServerKey)
{
EnsureArg.IsNotNull(existingConfiguration, nameof(existingConfiguration));
EnsureArg.IsNotNullOrWhiteSpace(healthServerKey, nameof(healthServerKey));

string testEnvironmentFilePath = existingConfiguration["TestAuthEnvironment:FilePath"];

if (string.IsNullOrWhiteSpace(testEnvironmentFilePath))
{
return configurationBuilder;
}

testEnvironmentFilePath = Path.GetFullPath(testEnvironmentFilePath);
if (!File.Exists(testEnvironmentFilePath))
{
return configurationBuilder;
}

return configurationBuilder.Add(new DevelopmentAuthEnvironmentConfigurationSource(testEnvironmentFilePath, existingConfiguration, healthServerKey));
}

private class DevelopmentAuthEnvironmentConfigurationSource : IConfigurationSource
{
private readonly string _filePath;
private readonly IConfigurationRoot _existingConfiguration;
private readonly string _healthServerKey;

public DevelopmentAuthEnvironmentConfigurationSource(string filePath, IConfigurationRoot existingConfiguration, string healthServerKey)
{
EnsureArg.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
_filePath = filePath;
_existingConfiguration = existingConfiguration;
_healthServerKey = healthServerKey;
}

public IConfigurationProvider Build(IConfigurationBuilder builder)
{
var jsonConfigurationSource = new JsonConfigurationSource
{
Path = _filePath,
Optional = true,
};

jsonConfigurationSource.ResolveFileProvider();
return new Provider(jsonConfigurationSource, _existingConfiguration, _healthServerKey);
}

private class Provider : JsonConfigurationProvider
{
private readonly string _authorityKey;
private readonly string _audienceKey;
private readonly string _securityEnabledKey;
private const string DevelopmentIdpEnabledKey = "DevelopmentIdentityProvider:Enabled";

private readonly IConfigurationRoot _existingConfiguration;
private static readonly Dictionary<string, string> Mappings = new Dictionary<string, string>
{
{ "^users:", "DevelopmentIdentityProvider:Users:" },
{ "^clientApplications:", "DevelopmentIdentityProvider:ClientApplications:" },
};

public Provider(JsonConfigurationSource source, IConfigurationRoot existingConfiguration, string healthServerKey)
: base(source)
{
_existingConfiguration = existingConfiguration;
_authorityKey = $"{healthServerKey}:Security:Authentication:Authority";
_audienceKey = $"{healthServerKey}:Security:Authentication:Audience";
_securityEnabledKey = $"{healthServerKey}:Security:Enabled";
}

public override void Load()
{
base.Load();

// remap the entries
Data = Data.ToDictionary(
p => Mappings.Aggregate(p.Key, (acc, mapping) => Regex.Replace(acc, mapping.Key, mapping.Value, RegexOptions.IgnoreCase)),
p => p.Value,
StringComparer.OrdinalIgnoreCase);

// add properties related to the development identity provider.
if (bool.TryParse(_securityEnabledKey, out bool securityEnabledValue)
&& securityEnabledValue == true)
{
Data[DevelopmentIdpEnabledKey] = bool.TrueString;
}

if (string.IsNullOrWhiteSpace(_existingConfiguration[_audienceKey]))
{
Data[_audienceKey] = Config.Provider.Audience;
}

if (string.IsNullOrWhiteSpace(_existingConfiguration[_authorityKey]))
{
Data[_authorityKey] = GetAuthority();
}
}

private string GetAuthority()
{
return _existingConfiguration["ASPNETCORE_URLS"]
?.Split(';', StringSplitOptions.RemoveEmptyEntries)
.Where(u =>
{
if (Uri.TryCreate(u, UriKind.Absolute, out Uri uri))
{
if (uri.Scheme == "https")
{
return true;
}
}

return false;
}).FirstOrDefault()?.TrimEnd('/');
}
}
}
}
}

0 comments on commit 7bcefd5

Please sign in to comment.