Skip to content

Commit

Permalink
Merge pull request #23 from ITfoxtec/development
Browse files Browse the repository at this point in the history
Development
  • Loading branch information
Revsgaard authored Nov 28, 2023
2 parents 8b8b99c + df2f31a commit fb23549
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 97 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# ITfoxtec.Identity.BlazorWebAssembly.OpenidConnect
A JavaScript free OpenID Connect PKCE library for Blazor WebAssembly.

* **Support .NET 8.0**
* **Support .NET 7.0**
* **Support .NET 6.0**
* **Support .NET 5.0**
* **Support .NET Standard 2.0**

The library support login and logout with OpenID Connect (OIDC) using Proof Key for Code Exchange (PKCE) instead of a client secret.
The received ID token is validated by the component in the client using the OpenID Provider (OP) discovery document.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>BlazorWebAssemblyOidcSample.Client</AssemblyName>
<RootNamespace>BlazorWebAssemblyOidcSample.Client</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.1" PrivateAssets="all" />
<PackageReference Include="System.Net.Http.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>BlazorWebAssemblyOidcSample.Server</AssemblyName>
<RootNamespace>BlazorWebAssemblyOidcSample.Server</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>BlazorWebAssemblyOidcSample.Shared</AssemblyName>
<RootNamespace>BlazorWebAssemblyOidcSample.Shared</RootNamespace>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Blazored.SessionStorage;
using ITfoxtec.Identity.Discovery;
using ITfoxtec.Identity.Helpers;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Logging;
Expand All @@ -22,18 +23,11 @@ public static IServiceCollection AddOpenidConnectPkce(this IServiceCollection se
services.AddSingleton(openIDClientPkceSettings);

services.AddScoped<OpenidConnectPkce>();
#if NETSTANDARD
services.AddSingleton(sp => new OidcDiscoveryHandler(sp.GetService<HttpClient>()));
#else
services.AddSingleton(sp => new OidcDiscoveryHandler(sp.GetService<IHttpClientFactory>()));
#endif
services.AddScoped(sp => new OidcHelper(sp.GetService<IHttpClientFactory>(), sp.GetService<OidcDiscoveryHandler>()));

services.AddScoped<AuthenticationStateProvider, OidcAuthenticationStateProvider>();
#if NETSTANDARD
services.AddTransient<AccessTokenMessageHandler>();
#else
services.AddScoped<AccessTokenMessageHandler>();
#endif

services.AddOptions();
services.AddAuthorizationCore();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFrameworks>net6.0;net5.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>net8.0;net7.0;net6.0;net5.0</TargetFrameworks>
<AssemblyName>ITfoxtec.Identity.BlazorWebAssembly.OpenidConnect</AssemblyName>
<RootNamespace>ITfoxtec.Identity.BlazorWebAssembly.OpenidConnect</RootNamespace>

<Authors>Anders Revsgaard</Authors>
<Company>ITfoxtec</Company>
<Description>A JavaScript free OpenID Connect PKCE library for Blazor WebAssembly.

Support .NET 8.0
Support .NET 7.0
Support .NET 6.0
Support .NET 5.0
Support .NET Standard 2.0

The received ID token is validated by the component in the client using the OpenID Provider (OP) discovery document.
The component automatically handle token / session update with use of the refresh token if the offline_access scope is specified.</Description>
Expand All @@ -22,17 +23,55 @@ The component automatically handle token / session update with use of the refres
<PackageTags>Blazor WebAssembly OpenID Connect (OIDC) Proof Key for Code Exchange (PKCE) id token access token refresh token</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Copyright>© 2020 ITfoxtec</Copyright>
<AssemblyVersion>1.6.3.0</AssemblyVersion>
<FileVersion>1.6.3.0</FileVersion>
<Version>1.6.3</Version>
<AssemblyVersion>1.6.5.2</AssemblyVersion>
<FileVersion>1.6.5.2</FileVersion>
<Version>1.6.5.2</Version>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Blazored.SessionStorage" Version="2.2.0" />
<PackageReference Include="ITfoxtec.Identity" Version="2.5.2" />
<PackageReference Include="ITfoxtec.Identity" Version="2.5.41.5" />
</ItemGroup>


<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.0" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net8.0'">
<DefineConstants>NET80;NET</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0|AnyCPU'">
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0|AnyCPU'">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573</NoWarn>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net7.0'">
<PackageReference Include="Blazored.SessionStorage" Version="2.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="7.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="7.0.12" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net7.0'">
<DefineConstants>NET70;NET</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0|AnyCPU'">
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0|AnyCPU'">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573</NoWarn>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net6.0'">
<PackageReference Include="Blazored.SessionStorage" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="6.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="6.0.1" />
</ItemGroup>
Expand All @@ -51,6 +90,7 @@ The component automatically handle token / session update with use of the refres
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net5.0'">
<PackageReference Include="Blazored.SessionStorage" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Components" Version="5.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="5.0.9" />
</ItemGroup>
Expand All @@ -67,24 +107,5 @@ The component automatically handle token / session update with use of the refres
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573</NoWarn>
</PropertyGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.1'">
<PackageReference Include="Microsoft.AspNetCore.Components" Version="3.1.14" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="3.1.14" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.18" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard2.1'">
<DefineConstants>NETSTANDARD2;NETSTANDARD</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|netstandard2.1|AnyCPU'">
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard2.1|AnyCPU'">
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;1573</NoWarn>
</PropertyGroup>

</Project>
59 changes: 7 additions & 52 deletions src/ITfoxtec.Identity.BlazorWA.Oidc/OpenidConnectPkce.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Blazored.SessionStorage;
using ITfoxtec.Identity.Discovery;
using ITfoxtec.Identity.Helpers;
using ITfoxtec.Identity.Messages;
using ITfoxtec.Identity.Tokens;
using ITfoxtec.Identity.Util;
Expand All @@ -15,9 +16,6 @@
using System.Security;
using System.Security.Claims;
using System.Threading.Tasks;
#if !NET50 && !NET60
using ITfoxtec.Identity.Models;
#endif

namespace ITfoxtec.Identity.BlazorWebAssembly.OpenidConnect
{
Expand All @@ -27,14 +25,16 @@ public class OpenidConnectPkce
protected readonly OpenidConnectPkceSettings globalOpenidClientPkceSettings;
protected readonly NavigationManager navigationManager;
protected readonly ISessionStorageService sessionStorage;
protected readonly OidcHelper oidcHelper;
protected readonly AuthenticationStateProvider authenticationStateProvider;

public OpenidConnectPkce(IServiceProvider serviceProvider, OpenidConnectPkceSettings globalOpenidClientPkceSettings, NavigationManager navigationManager, ISessionStorageService sessionStorage, AuthenticationStateProvider authenticationStateProvider)
public OpenidConnectPkce(IServiceProvider serviceProvider, OpenidConnectPkceSettings globalOpenidClientPkceSettings, NavigationManager navigationManager, ISessionStorageService sessionStorage, OidcHelper oidcHelper, AuthenticationStateProvider authenticationStateProvider)
{
this.serviceProvider = serviceProvider;
this.globalOpenidClientPkceSettings = globalOpenidClientPkceSettings;
this.navigationManager = navigationManager;
this.sessionStorage = sessionStorage;
this.oidcHelper = oidcHelper;
this.authenticationStateProvider = authenticationStateProvider;
}

Expand Down Expand Up @@ -164,26 +164,7 @@ public async Task LoginCallBackAsync(string responseUrl)
if (tokenResponse.AccessToken.IsNullOrEmpty()) throw new ArgumentNullException(nameof(tokenResponse.AccessToken), tokenResponse.GetTypeName());
if (tokenResponse.ExpiresIn <= 0) throw new ArgumentNullException(nameof(tokenResponse.ExpiresIn), tokenResponse.GetTypeName());

// .NET 5.0 error, System.Security.Cryptography.RSA.Create() - System.PlatformNotSupportedException: System.Security.Cryptography.Algorithms is not supported on this platform.
// https://github.com/dotnet/aspnetcore/issues/26123
// https://github.com/dotnet/runtime/issues/40074
// .NET 7
// https://github.com/dotnet/designs/blob/main/accepted/2021/blazor-wasm-crypto.md#net-7-plan
#if !NET50 && !NET60
var oidcDiscoveryKeySet = await GetOidcDiscoveryKeysAsync(openidClientPkceState.OidcDiscoveryUri);

(var idTokenPrincipal, _) = JwtHandler.ValidateToken(tokenResponse.IdToken, oidcDiscovery.Issuer, oidcDiscoveryKeySet.Keys.ToMSJsonWebKeys(), openidClientPkceState.ClientId,
nameClaimType: globalOpenidClientPkceSettings.NameClaimType, roleClaimType: globalOpenidClientPkceSettings.RoleClaimType);
#else
var idTokenPrincipal = JwtHandler.ReadTokenClaims(tokenResponse.IdToken);
#endif

var nonce = idTokenPrincipal.Claims.Where(c => c.Type == JwtClaimTypes.Nonce).Select(c => c.Value).FirstOrDefault();
if (!openidClientPkceState.Nonce.Equals(nonce, StringComparison.Ordinal))
{
throw new SecurityException("Nonce do not match.");
}

var idTokenPrincipal = await oidcHelper.ValidateOidcWithUserInfoEndpoint(tokenResponse.IdToken, tokenResponse.AccessToken, openidClientPkceState.Nonce);
return (idTokenPrincipal, tokenResponse);

case HttpStatusCode.BadRequest:
Expand Down Expand Up @@ -237,19 +218,7 @@ public async Task<OidcUserSession> HandleRefreshTokenAsync(OidcUserSession userS
if (tokenResponse.AccessToken.IsNullOrEmpty()) throw new ArgumentNullException(nameof(tokenResponse.AccessToken), tokenResponse.GetTypeName());
if (tokenResponse.ExpiresIn <= 0) throw new ArgumentNullException(nameof(tokenResponse.ExpiresIn), tokenResponse.GetTypeName());

// .NET 5.0 error, System.Security.Cryptography.RSA.Create() - System.PlatformNotSupportedException: System.Security.Cryptography.Algorithms is not supported on this platform.
// https://github.com/dotnet/aspnetcore/issues/26123
// https://github.com/dotnet/runtime/issues/40074
// .NET 7
// https://github.com/dotnet/designs/blob/main/accepted/2021/blazor-wasm-crypto.md#net-7-plan
#if !NET50 && !NET60
var oidcDiscoveryKeySet = await GetOidcDiscoveryKeysAsync(oidcDiscoveryUri);

(var idTokenPrincipal, _) = JwtHandler.ValidateToken(tokenResponse.IdToken, oidcDiscovery.Issuer, oidcDiscoveryKeySet.Keys, clientId,
nameClaimType: globalOpenidClientPkceSettings.NameClaimType, roleClaimType: globalOpenidClientPkceSettings.RoleClaimType);
#else
var idTokenPrincipal = JwtHandler.ReadTokenClaims(tokenResponse.IdToken);
#endif
var idTokenPrincipal = await oidcHelper.ValidateOidcWithUserInfoEndpoint(tokenResponse.IdToken, tokenResponse.AccessToken);

if (!subject.IsNullOrEmpty() && subject != idTokenPrincipal.Claims.Where(c => c.Type == globalOpenidClientPkceSettings.NameClaimType).Single().Value)
{
Expand Down Expand Up @@ -360,6 +329,7 @@ private async Task<OidcDiscovery> GetOidcDiscoveryAsync(string oidcDiscoveryUri)
try
{
var oidcDiscoveryHandler = serviceProvider.GetService<OidcDiscoveryHandler>();
oidcDiscoveryHandler.SetDefaultOidcDiscoveryUri(oidcDiscoveryUri);
return await oidcDiscoveryHandler.GetOidcDiscoveryAsync(oidcDiscoveryUri);
}
catch (Exception ex)
Expand All @@ -368,21 +338,6 @@ private async Task<OidcDiscovery> GetOidcDiscoveryAsync(string oidcDiscoveryUri)
}
}

#if !NET50 && !NET60
private async Task<JsonWebKeySet> GetOidcDiscoveryKeysAsync(string oidcDiscoveryUri)
{
try
{
var oidcDiscoveryHandler = serviceProvider.GetService<OidcDiscoveryHandler>();
return await oidcDiscoveryHandler.GetOidcDiscoveryKeysAsync(oidcDiscoveryUri);
}
catch (Exception ex)
{
throw new Exception($"Failed to fetch OIDC Discovery Keys from discovery '{oidcDiscoveryUri}'.", ex);
}
}
#endif

private async Task<string> SaveStateAsync(OpenidConnectPkceSettings openidConnectPkceSettings, string callBackUri, string redirectUri, string codeVerifier = null, string nonce = null)
{
var state = RandomGenerator.GenerateNonce(32);
Expand Down

0 comments on commit fb23549

Please sign in to comment.