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

Apply latest ASP.NET Core/C# standards #483

Closed
wants to merge 2 commits into from
Closed
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
41 changes: 20 additions & 21 deletions IdentityServer/v6/docs/content/ui/login/dynamicproviders.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ weight: 65

## Dynamic Identity Providers

Normally authentication handlers for external providers are added into your IdentityServer using *AddAuthentication()* and *AddOpenIdConnect()*. This is fine for a handful of schemes, but the authentication handler architecture in ASP.NET Core was not designed for dozens or more statically registered in the DI system. At some point you will incur a performance penalty for having too many. Also, as you need to add or change this configuration you will need to re-compile and re-run your startup code for those changes to take effect.
Normally authentication handlers for external providers are added into your IdentityServer using _AddAuthentication()_ and _AddOpenIdConnect()_. This is fine for a handful of schemes, but the authentication handler architecture in ASP.NET Core was not designed for dozens or more statically registered in the DI system. At some point you will incur a performance penalty for having too many. Also, as you need to add or change this configuration you will need to re-compile and re-run your startup code for those changes to take effect.

Duende IdentityServer provides support for dynamic configuration of OpenID Connect providers loaded from a store. This is designed to address the performance concern as well as allowing changes to the configuration to a running server.

Support for Dynamic Identity Providers is included in [IdentityServer](https://duendesoftware.com/products/identityserver) Enterprise Edition.
Support for Dynamic Identity Providers is included in [IdentityServer](https://duendesoftware.com/products/identityserver) Enterprise Edition.

### Listing and displaying the dynamic providers on the login page

Expand All @@ -32,12 +32,11 @@ The [identity provider store]({{<ref "/reference/stores/idp_store">}}) can be us

These results can then be used to populate the list of options presented to the user on the login page.

This API is deliberately separate than the *IAuthenticationSchemeProvider* provided by ASP.NET Core, which returns the list of statically configured providers (from *Startup.cs*).
This API is deliberately separate than the _IAuthenticationSchemeProvider_ provided by ASP.NET Core, which returns the list of statically configured providers (from _Startup.cs_).
This allows the developer to have more control over the customization on the login page (e.g. there might be hundreds or thousands on dynamic providers, and therefore you would not want them displayed on the login page, but you might have a few social providers statically configured that you would want to display).

Here is an example of how the [IdentityServer Quickstart UI](https://github.com/DuendeSoftware/IdentityServer.Quickstart.UI/blob/main/Quickstart/Account/AccountController.cs#L265-L282) uses both interfaces to then present a merged and unified list to the end user:


```cs
var schemes = await _schemeProvider.GetAllSchemesAsync();

Expand Down Expand Up @@ -66,18 +65,17 @@ To use the dynamic providers feature an [identity provider store]({{<ref "/refer
If you're using the [Entity Framework Integration]({{<ref "/data/ef">}}) then this is implemented for you.

{{% notice note %}}
Like other configuration data in IdentityServer, by default the dynamic provider configuration is loaded from the store on every request unless caching is enabled.
Like other configuration data in IdentityServer, by default the dynamic provider configuration is loaded from the store on every request unless caching is enabled.
If you use a custom store, there is an [extension method to enable caching]({{<ref "/data/configuration#caching-configuration-data">}}).
If you use the EF stores, there is general helper [to enable caching for all configuration data]({{<ref "/data/ef#enabling-caching-for-configuration-store">}}).
{{% /notice %}}

The configuration data for the OIDC provider is used to assign the configuration on the ASP.NET Core [OpenID Connect Options](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions) class, much like you would if you were to statically configure the options when using *AddOpenIdConnect()*.
The configuration data for the OIDC provider is used to assign the configuration on the ASP.NET Core [OpenID Connect Options](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions) class, much like you would if you were to statically configure the options when using _AddOpenIdConnect()_.
The [identity provider model documentation]({{<ref "/reference/models/idp">}}) provides details for the model properties and how they are mapped to the options.


#### Customizing OpenIdConnectOptions

If it is needed to further customize the *OpenIdConnectOptions*, you can register in the DI system an instance of *IConfigureNamedOptions\<OpenIdConnectOptions>*. For example:
If it is needed to further customize the _OpenIdConnectOptions_, you can register in the DI system an instance of _IConfigureNamedOptions\<OpenIdConnectOptions>_. For example:

```cs
public class CustomConfig : IConfigureNamedOptions<OpenIdConnectOptions>
Expand Down Expand Up @@ -107,14 +105,15 @@ And to register this in the DI system:

#### Accessing OidcProvider data in IConfigureNamedOptions

If your customization of the *OpenIdConnectOptions* requires per-provider data that you are storing on the *OidcProvider*, then we provide an abstraction for the *IConfigureNamedOptions\<OpenIdConnectOptions>*.
This abstraction requires your code to derive from *ConfigureAuthenticationOptions\<OpenIdConnectOptions, OidcProvider>* (rather than *IConfigureNamedOptions\<OpenIdConnectOptions>*).
If your customization of the _OpenIdConnectOptions_ requires per-provider data that you are storing on the _OidcProvider_, then we provide an abstraction for the _IConfigureNamedOptions\<OpenIdConnectOptions>_.
This abstraction requires your code to derive from _ConfigureAuthenticationOptions\<OpenIdConnectOptions, OidcProvider>_ (rather than _IConfigureNamedOptions\<OpenIdConnectOptions>_).
For example:

```cs
class CustomOidcConfigureOptions : ConfigureAuthenticationOptions<OpenIdConnectOptions, OidcProvider>
{
public CustomOidcConfigureOptions(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor)
public CustomOidcConfigureOptions(IHttpContextAccessor httpContextAccessor,
ILogger<CustomOidcConfigureOptions> logger) : base(httpContextAccessor, logger)
{
}

Expand All @@ -131,7 +130,7 @@ class CustomOidcConfigureOptions : ConfigureAuthenticationOptions<OpenIdConnectO
The above class would need to be configured in DI (as before):

```cs
public void Configure(IServiceCollection services)
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureOptions<CustomOidcConfigureOptions>();
}
Expand All @@ -140,21 +139,21 @@ public void Configure(IServiceCollection services)
### Callback Paths

As part of the architecture of the dynamic providers feature, the various callback paths are required and are automatically set to follow a convention.
The convention of these paths follows the form of *~/federation/{scheme}/{suffix}*.
The convention of these paths follows the form of _~/federation/{scheme}/{suffix}_.

These are three paths that are set on the *OpenIdConnectOptions*:
These are three paths that are set on the _OpenIdConnectOptions_:

* [CallbackPath](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.callbackpath). This is the OIDC redirect URI protocol value. The suffix "/signin" is used for this path.
* [SignedOutCallbackPath](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutcallbackpath). This is the OIDC post logout redirect URI protocol value. The suffix "/signout-callback" is used for this path.
* [RemoteSignOutPath](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.remotesignoutpath). This is the OIDC front channel logout URI protocol value. The suffix "/signout" is used for this path.
- [CallbackPath](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.remoteauthenticationoptions.callbackpath). This is the OIDC redirect URI protocol value. The suffix "/signin" is used for this path.
- [SignedOutCallbackPath](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.signedoutcallbackpath). This is the OIDC post logout redirect URI protocol value. The suffix "/signout-callback" is used for this path.
- [RemoteSignOutPath](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.remotesignoutpath). This is the OIDC front channel logout URI protocol value. The suffix "/signout" is used for this path.

This means for your IdentityServer running at "https://sample.duendesoftware.com" and an OIDC identity provider whose scheme is "idp1", your client configuration with the external OIDC identity provider would be:

* The redirect URI would be "https://sample.duendesoftware.com/federation/idp1/signin"
* The post logout redirect URI would be "https://sample.duendesoftware.com/federation/idp1/signout-callback"
* The front channel logout URI would be "https://sample.duendesoftware.com/federation/idp1/signout"
- The redirect URI would be "https://sample.duendesoftware.com/federation/idp1/signin"
- The post logout redirect URI would be "https://sample.duendesoftware.com/federation/idp1/signout-callback"
- The front channel logout URI would be "https://sample.duendesoftware.com/federation/idp1/signout"

### DynamicProviderOptions

The *DynamicProviderOptions* is a new options class in the IdentityServer options object model.
The _DynamicProviderOptions_ is a new options class in the IdentityServer options object model.
It provides [shared settings]({{< ref "/reference/options#dynamic-providers">}}) for the dynamic identity providers feature.
80 changes: 37 additions & 43 deletions IdentityServer/v7/docs/content/apis/aspnetcore/authorization.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ date: 2020-09-10T08:22:12+02:00
weight: 30
---

The access token will include additional claims that can be used for authorization, e.g. the *scope* claim will reflect the scope the client requested (and was granted) during the token request.
The access token will include additional claims that can be used for authorization, e.g. the _scope_ claim will reflect the scope the client requested (and was granted) during the token request.

In ASP.NET core, the contents of the JWT payload get transformed into claims and packaged up in a *ClaimsPrincipal*. So you can always write custom validation or authorization logic in C#:
In ASP.NET core, the contents of the JWT payload get transformed into claims and packaged up in a _ClaimsPrincipal_. So you can always write custom validation or authorization logic in C#:

```cs
public IActionResult Get()
Expand All @@ -22,23 +22,17 @@ For better encapsulation and re-use, consider using the ASP.NET Core [authorizat
With this approach, you would first turn the claim requirement(s) into a named policy:

```cs
public void ConfigureServices(IServiceCollection services)
builder.Services.AddAuthorization(options =>
{
services.AddAuthorization(options =>
{
options.AddPolicy("read_access", policy =>
policy.RequireClaim("scope", "read");
});
}
options.AddPolicy("read_access", policy =>
policy.RequireClaim("scope", "read");
});
```

..and then enforce it, e.g. using the routing table:

```cs
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers().RequireAuthorization("read_access");
});
app.MapControllers().RequireAuthorization("read_access");
```

...or imperatively inside the controller:
Expand Down Expand Up @@ -78,60 +72,60 @@ public class DataController : ControllerBase
```

#### Scope claim format
Historically, Duende IdentityServer emitted the *scope* claims as an array in the JWT. This works very well with the .NET deserialization logic, which turns every array item into a separate claim of type *scope*.

The newer *JWT Profile for OAuth* [spec]({{< ref "/overview/specs" >}}) mandates that the scope claim is a single space delimited string. You can switch the format by setting the *EmitScopesAsSpaceDelimitedStringInJwt* on the [options]({{< ref "/reference/options" >}}). But this means that the code consuming access tokens might need to be adjusted. The following code can do a conversion to the *multiple claims* format that .NET prefers:
Historically, Duende IdentityServer emitted the _scope_ claims as an array in the JWT. This works very well with the .NET deserialization logic, which turns every array item into a separate claim of type _scope_.

The newer _JWT Profile for OAuth_ [spec]({{< ref "/overview/specs" >}}) mandates that the scope claim is a single space delimited string. You can switch the format by setting the _EmitScopesAsSpaceDelimitedStringInJwt_ on the [options]({{< ref "/reference/options" >}}). But this means that the code consuming access tokens might need to be adjusted. The following code can do a conversion to the _multiple claims_ format that .NET prefers:

```cs
namespace IdentityModel.AspNetCore.AccessTokenValidation
namespace IdentityModel.AspNetCore.AccessTokenValidation;

/// <summary>
/// Logic for normalizing scope claims to separate claim types
/// </summary>
public static class ScopeConverter
{
/// <summary>
/// Logic for normalizing scope claims to separate claim types
/// </summary>
public static class ScopeConverter
/// <param name="principal"></param>
/// <returns></returns>
public static ClaimsPrincipal NormalizeScopeClaims(this ClaimsPrincipal principal)
{
/// <summary>
/// Logic for normalizing scope claims to separate claim types
/// </summary>
/// <param name="principal"></param>
/// <returns></returns>
public static ClaimsPrincipal NormalizeScopeClaims(this ClaimsPrincipal principal)
var identities = new List<ClaimsIdentity>();

foreach (var id in principal.Identities)
{
var identities = new List<ClaimsIdentity>();
var identity = new ClaimsIdentity(id.AuthenticationType, id.NameClaimType, id.RoleClaimType);

foreach (var id in principal.Identities)
foreach (var claim in id.Claims)
{
var identity = new ClaimsIdentity(id.AuthenticationType, id.NameClaimType, id.RoleClaimType);

foreach (var claim in id.Claims)
if (claim.Type == "scope")
{
if (claim.Type == "scope")
if (claim.Value.Contains(' '))
{
if (claim.Value.Contains(' '))
{
var scopes = claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var scopes = claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);

foreach (var scope in scopes)
{
identity.AddClaim(new Claim("scope", scope, claim.ValueType, claim.Issuer));
}
}
else
foreach (var scope in scopes)
{
identity.AddClaim(claim);
identity.AddClaim(new Claim("scope", scope, claim.ValueType, claim.Issuer));
}
}
else
{
identity.AddClaim(claim);
}
}

identities.Add(identity);
else
{
identity.AddClaim(claim);
}
}
return new ClaimsPrincipal(identities);

identities.Add(identity);
}

return new ClaimsPrincipal(identities);
}
}
```
Expand Down
5 changes: 3 additions & 2 deletions IdentityServer/v7/docs/content/ui/login/dynamicproviders.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ For example:
```cs
class CustomOidcConfigureOptions : ConfigureAuthenticationOptions<OpenIdConnectOptions, OidcProvider>
{
public CustomOidcConfigureOptions(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor)
public CustomOidcConfigureOptions(IHttpContextAccessor httpContextAccessor,
ILogger<CustomOidcConfigureOptions> logger) : base(httpContextAccessor, logger)
{
}

Expand All @@ -131,7 +132,7 @@ class CustomOidcConfigureOptions : ConfigureAuthenticationOptions<OpenIdConnectO
The above class would need to be configured in DI (as before):

```cs
public void Configure(IServiceCollection services)
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureOptions<CustomOidcConfigureOptions>();
}
Expand Down
Loading