Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the Client and configure Identity Server API sample #3

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AspireForIdentityServer.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

_ = builder.AddProject<Projects.Client>("clientapp")
.WithExternalHttpEndpoints()
.WithReference(redis, connectionName: "Redis")
.WithEnvironment("IdentityProvider__Authority", identityServer.GetEndpoint("https"))
.WithEnvironment("IdentityProvider__ClientId", "mvc.par")
.WithEnvironment("IdentityProvider__ClientSecret", "secret");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ namespace IdentityServer.ApiControllers;
[ApiVersion(version: 1)]
[Authorize(LocalApi.PolicyName)]
[ApiController, Route(template: "api/v{v:apiVersion}/samples")]
public class SampleApiController(IDistributedCache distributedCache, ILogger<SampleApiController> logger, CancellationToken cancellationToken)
public class SampleApiController(IDistributedCache distributedCache, ILogger<SampleApiController> logger)
: ApiControllerBase
{
private readonly CancellationToken _cancellationToken = cancellationToken;
private readonly CancellationToken _cancellationToken = CancellationToken.None;
private readonly IDistributedCache _distributedCache = distributedCache;
private readonly ILogger<SampleApiController> _logger = logger;

Expand Down
4 changes: 4 additions & 0 deletions AspireForIdentityServer.ParClient/Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@
<PackageReference Include="Duende.BFF" Version="2.2.0" />
<PackageReference Include="HealthChecks.Uptime" Version="2.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.7" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.7" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.3" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="StackExchange.Redis" Version="2.7.27" />
<PackageReference Include="System.Formats.Asn1" Version="8.0.1" />
<PackageReference Include="System.Text.Json" Version="8.0.4" />
</ItemGroup>

<ItemGroup>
Expand Down
40 changes: 40 additions & 0 deletions AspireForIdentityServer.ParClient/Common/UserClaimAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Json;

namespace Client.Common;

public class UserClaimAction(string claimName) : ClaimAction(claimName, ClaimValueTypes.String)
{
private readonly string _claimName = claimName;

public override void Run(JsonElement userData, ClaimsIdentity identity, string issuer)
{
if (userData.TryGetProperty(_claimName, out var tokens))
{
var values = new List<string>();

if (tokens.ValueKind == JsonValueKind.String)
{
values.Add(tokens.ToString());
}
else
{
foreach (var token in tokens.EnumerateArray())
{
values.Add(token.ToString());
}
}

foreach (var v in values)
{
Claim claim = new(_claimName, v, ValueType, issuer);
if (!identity.HasClaim(c => c.Type == identity.RoleClaimType && c.Value == claim.Value))
{
identity.AddClaim(claim);
}
}
}
}
}
2 changes: 2 additions & 0 deletions AspireForIdentityServer.ParClient/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
public class ConfigurationSections
{
public const string IdentityProvider = "IdentityProvider";
public const string PollyResilience = "PollyResilience";
public const string ConnectionStrings = "ConnectionStrings";
}
52 changes: 50 additions & 2 deletions AspireForIdentityServer.ParClient/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,64 @@
using Client.Services;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Client.Controllers;

public class HomeController() : Controller
public class HomeController(
ILogger<HomeController> logger,
RedisUserSessionStore redisUserSessionStore,
IdentityServerSamplesApiService IdentityServerSamplesApiService
) : Controller
{
private readonly ILogger<HomeController> _logger = logger;

private readonly IdentityServerSamplesApiService _identityServerSamplesApiService = IdentityServerSamplesApiService;
private readonly RedisUserSessionStore _redisUserSessionStore = redisUserSessionStore;

[AllowAnonymous]
public IActionResult Index() => View();

public IActionResult Secure() => View();
public async Task<IActionResult> Secure()
{
#region Example of using the RedisUserSessionStore

var hasBeenSecured = _redisUserSessionStore.GetString(User.FindFirst("sub").Value, "secure_page");

if (string.IsNullOrWhiteSpace(hasBeenSecured))
{
_redisUserSessionStore.SetString(User.FindFirst("sub").Value, "secure_page", "visited");
}

var sessionKeys = _redisUserSessionStore.GetUserStorageKeys(User.FindFirst("sub").Value);
var sessionItems = new List<KeyValuePair<string, object>>();

foreach (var key in sessionKeys)
{
var value = _redisUserSessionStore.GetString(User.FindFirst("sub").Value, key);
sessionItems.Add(new KeyValuePair<string, object>(key, value));
}

ViewBag.SessionKeyValues = sessionItems;

#endregion

#region Example of using the IdentityServerSamplesApiService

// Call the IdentityServerSamplesApiService to get sample data
// The endpoint has caching enabled, so the first call will generate new data and cache it.
var sampleData = await _identityServerSamplesApiService.GetSampleData();
ViewBag.SampleData = sampleData;

#endregion


return View();
}

[AllowAnonymous]
public IActionResult Logout()
Expand Down
10 changes: 10 additions & 0 deletions AspireForIdentityServer.ParClient/Dtos/SampleDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace Client.Dtos;

public class SampleDto
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime GeneratedDate { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Client.Common;
using IdentityModel;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth.Claims;
using System.Collections.Generic;

namespace Client.Extensions;

public static class ClaimActionsExtensions
{
/// <summary>
/// Applies custom claim actions to the claim action collection.
/// </summary>
/// <param name="claimActions">The claim action collection to apply the custom claim actions to.</param>
public static void ApplyCustomClaimsActions(this ClaimActionCollection claimActions)
{
claimActions.Clear();
claimActions.RemoveUnwantedClaimActions();
claimActions.AddCustomClaimActions();
}

private static void AddCustomClaimActions(this ClaimActionCollection claimActions)
{
List<string> addClaimActions = [
JwtClaimTypes.Name,
JwtClaimTypes.Email,
JwtClaimTypes.EmailVerified,
JwtClaimTypes.GivenName,
JwtClaimTypes.MiddleName,
JwtClaimTypes.FamilyName,
JwtClaimTypes.Role
];

addClaimActions.ForEach(claimAction => claimActions.Add(action: new UserClaimAction(claimAction)));
}

private static void RemoveUnwantedClaimActions(this ClaimActionCollection claimActions)
{
List<string> removeClaimActions = [
JwtClaimTypes.IdentityProvider,
JwtClaimTypes.Nonce,
JwtClaimTypes.AccessTokenHash
];

removeClaimActions.ForEach(claimAction => claimActions.DeleteClaim(claimAction));
}
}
53 changes: 53 additions & 0 deletions AspireForIdentityServer.ParClient/Extensions/HostingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Client.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Client.Extensions;

public static class HostingExtensions
{
public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
{
// Add custom services
builder.AddAndConfigurePushedAuthorizationSupport();
builder.AddAndConfigureRemoteApi();
builder.AddAndConfigureSessionWithRedis();
builder.AddAndConfigureAuthorization();

// Add DI Services
builder.Services.AddScoped<IdentityServerSamplesApiService>();
builder.Services.AddTransient<IdentityServerApiServiceBase>();
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

// Add MVC
builder.Services.AddControllersWithViews();

return builder.Build();
}

public static WebApplication ConfigurePipeline(this WebApplication app)
{
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseSession();
app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute()
.RequireAuthorization();

app.MapBffManagementBackchannelEndpoint();

return app;
}
}
27 changes: 27 additions & 0 deletions AspireForIdentityServer.ParClient/Extensions/OptionsExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Client.Options;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using System;

namespace Client.Extensions;

public static class OptionsExtensions
{
/// <summary>
/// Retrieves a custom configuration section from an application's configuration and binds it to a strongly typed object.
/// </summary>
/// <typeparam name="T">The type of the custom options class to bind to, which must implement the <see cref="ICustomOptions"/> interface and have a parameterless constructor.</typeparam>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> instance used to build the web application.</param>
/// <param name="configurationSectionName">The name of the configuration section in the application's configuration files (e.g., appsettings.json) that contains the settings to be bound to the custom options class.</param>
/// <returns>An instance of the specified custom options class <typeparamref name="T"/>, with its properties populated from the specified configuration section.</returns>
/// <remarks>
/// This method creates a new instance of the custom options class and uses the <see cref="IConfiguration"/> available in <paramref name="builder"/> to bind the configuration values from the specified section to the properties of the class.
/// </remarks>
public static T GetCustomOptionsConfiguration<T>(this WebApplicationBuilder builder, string configurationSectionName) where T : class, ICustomOptions, new()
{
var optionsClass = Activator.CreateInstance<T>();
builder.Configuration.GetSection(configurationSectionName).Bind(optionsClass);

return optionsClass;
}
}
Loading