From fc7ae6ecc155ad9b71b4b510ccb5bd426a1ee6b6 Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Fri, 18 Oct 2024 18:41:21 +0200 Subject: [PATCH] First commit Migrate from https://github.com/engineering87/pdnd-client-assertion-generator --- .gitignore | 398 ++++++++++++++++++ LICENSE | 21 + README.md | 88 ++++ .../Controllers/ClientAssertionController.cs | 34 ++ .../PDNDClientAssertionGenerator.Api.csproj | 29 ++ .../PDNDClientAssertionGenerator.Api.http | 6 + .../Program.cs | 44 ++ .../Properties/launchSettings.json | 41 ++ .../appsettings.Development.json | 8 + .../appsettings.json | 22 + .../ClientAssertionGeneratorTests.cs | 40 ++ .../DateTimeUtilsTests.cs | 52 +++ .../OAuth2ServiceTests.cs | 68 +++ .../PDNDClientAssertionGenerator.Tests.csproj | 34 ++ .../SecurityUtilsTests.cs | 65 +++ .../TokenUtilsTests.cs | 62 +++ .../Configuration/ClientAssertionConfig.cs | 67 +++ .../Interfaces/IClientAssertionGenerator.cs | 12 + .../Interfaces/IOAuth2Service.cs | 12 + .../PDNDClientAssertionServiceExtensions.cs | 56 +++ .../Models/PDNDClientAssertion.cs | 64 +++ .../Models/PDNDTokenResponse.cs | 18 + .../PDNDClientAssertionGenerator.csproj | 41 ++ .../PDNDClientAssertionGenerator.sln | 42 ++ .../ClientAssertionGeneratorService.cs | 34 ++ .../Services/OAuth2Service.cs | 127 ++++++ .../Utils/DateTimeUtils.cs | 23 + .../Utils/SecurityUtils.cs | 99 +++++ .../Utils/TokenUtils.cs | 29 ++ 29 files changed, 1636 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/PDNDClientAssertionGenerator.Api/Controllers/ClientAssertionController.cs create mode 100644 src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.csproj create mode 100644 src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.http create mode 100644 src/PDNDClientAssertionGenerator.Api/Program.cs create mode 100644 src/PDNDClientAssertionGenerator.Api/Properties/launchSettings.json create mode 100644 src/PDNDClientAssertionGenerator.Api/appsettings.Development.json create mode 100644 src/PDNDClientAssertionGenerator.Api/appsettings.json create mode 100644 src/PDNDClientAssertionGenerator.Tests/ClientAssertionGeneratorTests.cs create mode 100644 src/PDNDClientAssertionGenerator.Tests/DateTimeUtilsTests.cs create mode 100644 src/PDNDClientAssertionGenerator.Tests/OAuth2ServiceTests.cs create mode 100644 src/PDNDClientAssertionGenerator.Tests/PDNDClientAssertionGenerator.Tests.csproj create mode 100644 src/PDNDClientAssertionGenerator.Tests/SecurityUtilsTests.cs create mode 100644 src/PDNDClientAssertionGenerator.Tests/TokenUtilsTests.cs create mode 100644 src/PDNDClientAssertionGenerator/Configuration/ClientAssertionConfig.cs create mode 100644 src/PDNDClientAssertionGenerator/Interfaces/IClientAssertionGenerator.cs create mode 100644 src/PDNDClientAssertionGenerator/Interfaces/IOAuth2Service.cs create mode 100644 src/PDNDClientAssertionGenerator/Middleware/PDNDClientAssertionServiceExtensions.cs create mode 100644 src/PDNDClientAssertionGenerator/Models/PDNDClientAssertion.cs create mode 100644 src/PDNDClientAssertionGenerator/Models/PDNDTokenResponse.cs create mode 100644 src/PDNDClientAssertionGenerator/PDNDClientAssertionGenerator.csproj create mode 100644 src/PDNDClientAssertionGenerator/PDNDClientAssertionGenerator.sln create mode 100644 src/PDNDClientAssertionGenerator/Services/ClientAssertionGeneratorService.cs create mode 100644 src/PDNDClientAssertionGenerator/Services/OAuth2Service.cs create mode 100644 src/PDNDClientAssertionGenerator/Utils/DateTimeUtils.cs create mode 100644 src/PDNDClientAssertionGenerator/Utils/SecurityUtils.cs create mode 100644 src/PDNDClientAssertionGenerator/Utils/TokenUtils.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a30d25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..622ff2e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Francesco Del Re + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6cfde20 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# PDND Client Assertion Generator + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Nuget](https://img.shields.io/nuget/v/PDNDClientAssertionGenerator?style=plastic)](https://www.nuget.org/packages/PDNDClientAssertionGenerator) +![NuGet Downloads](https://img.shields.io/nuget/dt/PDNDClientAssertionGenerator) +[![Build](https://github.com/engineering87/pdnd-client-assertion-generator/actions/workflows/dotnet.yml/badge.svg)](https://github.com/engineering87/pdnd-client-assertion-generator/actions/workflows/dotnet.yml) +[![issues - pdnd-client-assertion-generator](https://img.shields.io/github/issues/engineering87/pdnd-client-assertion-generator)](https://github.com/engineering87/pdnd-client-assertion-generator/issues) +[![Language - C#](https://img.shields.io/static/v1?label=Language&message=C%23&color=blueviolet)](https://dotnet.microsoft.com/it-it/languages/csharp) +[![stars - pdnd-client-assertion-generator](https://img.shields.io/github/stars/engineering87/pdnd-client-assertion-generator?style=social)](https://github.com/engineering87/pdnd-client-assertion-generator) + +.NET implementation of **OAuth2** authentication for **PDND** service with client assertion generation. + +## Contents +- [PDND](#pdnd) +- [Voucher](#voucher) +- [Requesting a Voucher](#requesting-a-voucher) +- [How to Use the Client Assertion Generator](#how-to-use-the-client-assertion-generator) +- [Licensee](#licensee) +- [Contact](#contact) + +## PDND +The **[Piattaforma Digitale Nazionale Dati (PDND)](https://developers.italia.it/it/pdnd/)** is an Italian digital infrastructure designed to facilitate **data interoperability** and exchange between public administrations and private entities. The platform aims to simplify the sharing of public data by providing a secure, standardized, and centralized system for data integration, access, and management. PDND promotes digital transformation within the public sector by ensuring data is accessible, reliable, and reusable, enabling more efficient public services, enhancing transparency, and supporting **data-driven decision-making** for both government and citizens. + +## Voucher +Vouchers are simple JWT tokens. The implemented authentication flow is OAuth 2.0, which refers to [**RFC6750**](https://datatracker.ietf.org/doc/html/rfc6750) for the use of Bearer tokens and to [**RFC7521**](https://datatracker.ietf.org/doc/html/rfc7521) for client authorization via client assertion. + +## Requesting a Voucher +To obtain a valid voucher, you must first upload at least one public key to an interop API client. The first step is to create a valid client assertion and sign it with your private key (which must match the public key registered with the client on PDND Interoperabilità). The client assertion consists of a header and a payload. + +## Voucher Flow for Interoperability APIs +The user requests a voucher. Once obtained, they include it as an authorization header in subsequent calls to the PDND Interoperability APIs. + +## How to Use the Client Assertion Generator +To properly set up and use the Client Assertion Generator in your ASP.NET Core application, follow these steps: + +1. Configure Client Assertion Settings, an example below: + ```xml + "ClientAssertionConfig": { + "ServerUrl": "", + "KeyId": "ZmYxZGE2YjQtMzY2Yy00NWI5LThjNGItMDJmYmQyZGIyMmZh", + "Algorithm": "RS256", + "Type": "at+jwt", + "ClientId": "9b361d49-33f4-4f1e-a88b-4e12661f2309", + "Issuer": "interop.pagopa.it", + "Subject": "9b361d49-33f4-4f1e-a88b-4e12661f2309", + "Audience": "https://erogatore.example/ente-example/v1", + "PurposeId": "1b361d49-33f4-4f1e-a88b-4e12661f2300", + "KeyPath": "/path/", + "Duration": "600" + }, + ``` + +2. Register Services: + ```csharp + builder.Services.AddPDNDClientAssertionServices(); + ``` + +Then you can use `ClientAssertionGeneratorService`, which provides the following methods: +- `GetClientAssertionAsync` +- `GetTokenAsync(clientAssertion)` + +## Testing the PDNDClientAssertionGenerator +This project includes a test application, **PDNDClientAssertionGenerator.Api**, designed to help you test the software with your own configuration. This application acts as a sandbox where you can validate the behavior of the PDNDClientAssertionGenerator components. + +### How to Use the Test Application: + +1. Configuration: Update the configuration settings in the `appsettings.json` file or through environment variables to match your specific use case and environment. + +2. Running the Test Application: + - Navigate to the PDNDClientAssertionGenerator.Api folder. + - Use the following command to run the application: + `dotnet run --project src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.csproj` + +3. Testing Scenarios: Once the application is running, you can use various `GetClientAssertion` and `GetToken` to test the functionality of the software in different configurations. + +## How to Contribute +Thank you for considering to help out with the source code! +If you'd like to contribute, please fork, fix, commit and send a pull request for the maintainers to review and merge into the main code base. + + * [Setting up Git](https://docs.github.com/en/get-started/getting-started-with-git/set-up-git) + * [Fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) + * [Open an issue](https://github.com/engineering87/pdnd-client-assertion-generator/issues) if you encounter a bug or have a suggestion for improvements/features + +## Licensee +Repository source code is available under MIT License, see license in the source. + +## Contact +Please contact at francesco.delre.87[at]gmail.com for any details. diff --git a/src/PDNDClientAssertionGenerator.Api/Controllers/ClientAssertionController.cs b/src/PDNDClientAssertionGenerator.Api/Controllers/ClientAssertionController.cs new file mode 100644 index 0000000..c2539c9 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Api/Controllers/ClientAssertionController.cs @@ -0,0 +1,34 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Microsoft.AspNetCore.Mvc; +using PDNDClientAssertionGenerator.Interfaces; +using PDNDClientAssertionGenerator.Models; + +namespace PDNDClientAssertionGenerator.Api.Controllers +{ + [ApiController] + [Route("[controller]")] + public class ClientAssertionController : ControllerBase + { + private readonly IClientAssertionGenerator _clientAssertionGenerator; + private readonly ILogger _logger; + + public ClientAssertionController(ILogger logger, IClientAssertionGenerator clientAssertionGenerator) + { + _logger = logger; + _clientAssertionGenerator = clientAssertionGenerator; + } + + [HttpGet("GetClientAssertion", Name = "GetClientAssertion")] + public async Task GetClientAssertionAsync() + { + return await _clientAssertionGenerator.GetClientAssertionAsync(); + } + + [HttpGet("GetToken", Name = "GetToken")] + public async Task GetToken([FromQuery] string clientAssertion) + { + return await _clientAssertionGenerator.GetTokenAsync(clientAssertion); + } + } +} diff --git a/src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.csproj b/src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.csproj new file mode 100644 index 0000000..275c1f1 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.http b/src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.http new file mode 100644 index 0000000..dbe86da --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Api/PDNDClientAssertionGenerator.Api.http @@ -0,0 +1,6 @@ +@PDNDClientAssertionGenerator.Api_HostAddress = http://localhost:5227 + +GET {{PDNDClientAssertionGenerator.Api_HostAddress}}/clientassertion/ +Accept: application/json + +### diff --git a/src/PDNDClientAssertionGenerator.Api/Program.cs b/src/PDNDClientAssertionGenerator.Api/Program.cs new file mode 100644 index 0000000..1d66591 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Api/Program.cs @@ -0,0 +1,44 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using PDNDClientAssertionGenerator.Middleware; + +internal class Program +{ + private static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + + builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + var configuration = builder.Configuration; + + // Add configuration PDNDClientAssertionGenerator + //builder.Services.Configure(configuration.GetSection("ClientAssertionConfig")); + //builder.Services.AddSingleton(); + //builder.Services.AddScoped(); + //builder.Services.AddScoped(); + builder.Services.AddPDNDClientAssertionServices(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } +} \ No newline at end of file diff --git a/src/PDNDClientAssertionGenerator.Api/Properties/launchSettings.json b/src/PDNDClientAssertionGenerator.Api/Properties/launchSettings.json new file mode 100644 index 0000000..159d7f9 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:49042", + "sslPort": 44309 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5227", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7188;http://localhost:5227", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/PDNDClientAssertionGenerator.Api/appsettings.Development.json b/src/PDNDClientAssertionGenerator.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/PDNDClientAssertionGenerator.Api/appsettings.json b/src/PDNDClientAssertionGenerator.Api/appsettings.json new file mode 100644 index 0000000..49016a8 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Api/appsettings.json @@ -0,0 +1,22 @@ +{ + "ClientAssertionConfig": { + "ServerUrl": "https://test-server-url.com", + "KeyId": "ZmYxZGE2YjQtMzY2Yy00NWI5LThjNGItMDJmYmQyZGIyMmZh", + "Algorithm": "RS256", + "Type": "at+jwt", + "ClientId": "9b361d49-33f4-4f1e-a88b-4e12661f2309", + "Issuer": "interop.pagopa.it", + "Subject": "9b361d49-33f4-4f1e-a88b-4e12661f2309", + "Audience": "https://erogatore.example/ente-example/v1", + "PurposeId": "1b361d49-33f4-4f1e-a88b-4e12661f2300", + "KeyPath": "/path/", + "Duration": "600" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/PDNDClientAssertionGenerator.Tests/ClientAssertionGeneratorTests.cs b/src/PDNDClientAssertionGenerator.Tests/ClientAssertionGeneratorTests.cs new file mode 100644 index 0000000..9884659 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Tests/ClientAssertionGeneratorTests.cs @@ -0,0 +1,40 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Moq; +using PDNDClientAssertionGenerator.Interfaces; +using PDNDClientAssertionGenerator.Services; + +namespace PDNDClientAssertionGenerator.Tests +{ + public class ClientAssertionGeneratorServiceTests + { + [Fact] + public async Task GetClientAssertionAsync_ShouldCall_GenerateClientAssertionAsync() + { + // Arrange + var oauth2ServiceMock = new Mock(); + var clientAssertionGeneratorService = new ClientAssertionGeneratorService(oauth2ServiceMock.Object); + + // Act + await clientAssertionGeneratorService.GetClientAssertionAsync(); + + // Assert + oauth2ServiceMock.Verify(o => o.GenerateClientAssertionAsync(), Times.Once); + } + + [Fact] + public async Task GetToken_ShouldCall_RequestAccessTokenAsync_WithClientAssertion() + { + // Arrange + var oauth2ServiceMock = new Mock(); + var clientAssertionGeneratorService = new ClientAssertionGeneratorService(oauth2ServiceMock.Object); + var clientAssertion = "testClientAssertion"; + + // Act + await clientAssertionGeneratorService.GetTokenAsync(clientAssertion); + + // Assert + oauth2ServiceMock.Verify(o => o.RequestAccessTokenAsync(clientAssertion), Times.Once); + } + } +} \ No newline at end of file diff --git a/src/PDNDClientAssertionGenerator.Tests/DateTimeUtilsTests.cs b/src/PDNDClientAssertionGenerator.Tests/DateTimeUtilsTests.cs new file mode 100644 index 0000000..49ed31f --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Tests/DateTimeUtilsTests.cs @@ -0,0 +1,52 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using PDNDClientAssertionGenerator.Utils; + +namespace PDNDClientAssertionGenerator.Tests +{ + public class DateTimeUtilsTests + { + [Fact] + public void ToUnixTimestamp_ShouldReturnCorrectUnixTimestamp_ForGivenDateTime() + { + // Arrange + DateTime dateTime = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + int expectedUnixTimestamp = 1704067200; + + // Act + int actualUnixTimestamp = dateTime.ToUnixTimestamp(); + + // Assert + Assert.Equal(expectedUnixTimestamp, actualUnixTimestamp); + } + + [Fact] + public void ToUnixTimestamp_ShouldHandleDateTimeInDifferentTimeZones_Correctly() + { + // Arrange + DateTime dateTimeLocal = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Local); + DateTime dateTimeUtc = dateTimeLocal.ToUniversalTime(); + int expectedUnixTimestamp = (int)new DateTimeOffset(dateTimeUtc).ToUnixTimeSeconds(); + + // Act + int actualUnixTimestamp = dateTimeLocal.ToUnixTimestamp(); + + // Assert + Assert.Equal(expectedUnixTimestamp, actualUnixTimestamp); + } + + [Fact] + public void ToUnixTimestamp_ShouldReturnCorrectUnixTimestamp_ForEpochStart() + { + // Arrange + DateTime epochStart = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + int expectedUnixTimestamp = 0; + + // Act + int actualUnixTimestamp = epochStart.ToUnixTimestamp(); + + // Assert + Assert.Equal(expectedUnixTimestamp, actualUnixTimestamp); + } + } +} diff --git a/src/PDNDClientAssertionGenerator.Tests/OAuth2ServiceTests.cs b/src/PDNDClientAssertionGenerator.Tests/OAuth2ServiceTests.cs new file mode 100644 index 0000000..cd8aa2e --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Tests/OAuth2ServiceTests.cs @@ -0,0 +1,68 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; +using PDNDClientAssertionGenerator.Configuration; +using PDNDClientAssertionGenerator.Services; +using System.Net; + +namespace PDNDClientAssertionGenerator.Tests +{ + public class OAuth2ServiceTests + { + private readonly OAuth2Service _oauth2Service; + private readonly Mock _handlerMock; + private readonly Mock> _mockOptions; + + public OAuth2ServiceTests() + { + // Set up the HttpMessageHandler mock + _handlerMock = new Mock(); + + // Set up the ClientAssertionConfig object + var clientAssertionConfig = new ClientAssertionConfig + { + ServerUrl = "https://test-server-url.com", + ClientId = "test-client-id", + KeyId = "test-key-id", + Algorithm = "RS256", + Type = "JWT", + Issuer = "test-issuer", + Subject = "test-subject", + Audience = "test-audience", + PurposeId = "test-purpose-id", + KeyPath = "path/to/key", + Duration = 60 // token duration in minutes + }; + + // Create the mock IOptions instance + _mockOptions = new Mock>(); + _mockOptions.Setup(o => o.Value).Returns(clientAssertionConfig); + + // Initialize OAuth2Service with the mocked IOptions + _oauth2Service = new OAuth2Service(_mockOptions.Object); + } + + [Fact] + public async Task RequestAccessTokenAsync_ThrowsExceptionOnInvalidJsonResponse() + { + // Arrange: Invalid JSON Mock + _handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("invalid_json_response"), + }); + + // Act & Assert: Verify that an exception is thrown on an invalid response + var exception = await Assert.ThrowsAsync(() => _oauth2Service.RequestAccessTokenAsync("valid_client_assertion")); + } + } +} diff --git a/src/PDNDClientAssertionGenerator.Tests/PDNDClientAssertionGenerator.Tests.csproj b/src/PDNDClientAssertionGenerator.Tests/PDNDClientAssertionGenerator.Tests.csproj new file mode 100644 index 0000000..0897187 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Tests/PDNDClientAssertionGenerator.Tests.csproj @@ -0,0 +1,34 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/src/PDNDClientAssertionGenerator.Tests/SecurityUtilsTests.cs b/src/PDNDClientAssertionGenerator.Tests/SecurityUtilsTests.cs new file mode 100644 index 0000000..81b3a23 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Tests/SecurityUtilsTests.cs @@ -0,0 +1,65 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using PDNDClientAssertionGenerator.Utils; + +namespace PDNDClientAssertionGenerator.Tests +{ + public class SecurityUtilsTests + { + [Fact] + public void ExtractBase64Key_ShouldRemoveHeaderFooterAndWhitespace_FromValidPemContent() + { + // Arrange + string pemContent = @" + -----BEGIN RSA PRIVATE KEY----- + MIIBOgIBAAJBAK5QpQfskLq1djoi0yRz4ksblMdxI0m5lBw9fAWvntA59NgIUlHw + fNhgUJmOygwoQ6dzQbPUZp0ZEOtR10Q+/gECAwEAAQJAR1XlnMvJ0IbG4P1Rb0P/ + gRJyOgkMxybMfzoVr8f+f4IkH2XfsnBhdCdHkHhbtRfct+dM+7Rp3JFd+n+6IOCC + RQIhAPPAz9jYPX+3oBlfV92MdhLB3UjsoXTvGaMDHrG7PjCFAiEAuSMOFJNGFmRJ + kYmujA6SeyDEzJHpxEnbx9FA41gKkBcCIQDHaXsDBL2/WPBOFcOfTLNfBQXoTpEu + AfWRAd5Nvg0I9QIhAL3n2dfYFXAGpPCTg2MgttVfSe+oAapTknnCK6CRz58nAiEA + 4lYtY4jOOBVZWz1vUpcsWgGVpRfyRbGmJfrJ6UAKfBM= + -----END RSA PRIVATE KEY----- + "; + + string expectedBase64Key = "MIIBOgIBAAJBAK5QpQfskLq1djoi0yRz4ksblMdxI0m5lBw9fAWvntA59NgIUlHwfNhgUJmOygwoQ6dzQbPUZp0ZEOtR10Q+/gECAwEAAQJAR1XlnMvJ0IbG4P1Rb0P/gRJyOgkMxybMfzoVr8f+f4IkH2XfsnBhdCdHkHhbtRfct+dM+7Rp3JFd+n+6IOCCRQIhAPPAz9jYPX+3oBlfV92MdhLB3UjsoXTvGaMDHrG7PjCFAiEAuSMOFJNGFmRJkYmujA6SeyDEzJHpxEnbx9FA41gKkBcCIQDHaXsDBL2/WPBOFcOfTLNfBQXoTpEuAfWRAd5Nvg0I9QIhAL3n2dfYFXAGpPCTg2MgttVfSe+oAapTknnCK6CRz58nAiEA4lYtY4jOOBVZWz1vUpcsWgGVpRfyRbGmJfrJ6UAKfBM="; + + // Act + string actualBase64Key = SecurityUtils.ExtractBase64Key(pemContent); + + // Assert + Assert.Equal(expectedBase64Key, actualBase64Key); + } + + [Fact] + public void ExtractBase64Key_ShouldReturnEmptyString_WhenPemContentIsEmpty() + { + // Arrange + string pemContent = ""; + string expectedBase64Key = ""; + + // Act + string actualBase64Key = SecurityUtils.ExtractBase64Key(pemContent); + + // Assert + Assert.Equal(expectedBase64Key, actualBase64Key); + } + + [Fact] + public void ExtractBase64Key_ShouldReturnEmptyString_WhenPemContentIsOnlyHeaderFooter() + { + // Arrange + string pemContent = @" + -----BEGIN RSA PRIVATE KEY----- + -----END RSA PRIVATE KEY----- + "; + string expectedBase64Key = ""; + + // Act + string actualBase64Key = SecurityUtils.ExtractBase64Key(pemContent); + + // Assert + Assert.Equal(expectedBase64Key, actualBase64Key); + } + } +} diff --git a/src/PDNDClientAssertionGenerator.Tests/TokenUtilsTests.cs b/src/PDNDClientAssertionGenerator.Tests/TokenUtilsTests.cs new file mode 100644 index 0000000..7444186 --- /dev/null +++ b/src/PDNDClientAssertionGenerator.Tests/TokenUtilsTests.cs @@ -0,0 +1,62 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using PDNDClientAssertionGenerator.Utils; + +namespace PDNDClientAssertionGenerator.Tests +{ + public class TokenUtilsTests + { + [Fact] + public void ExtractAccessToken_ShouldReturnAccessToken_WhenValidJsonResponseIsProvided() + { + // Arrange + string jsonResponse = "{\"access_token\": \"abc123\", \"expires_in\": 3600}"; + string expectedAccessToken = "abc123"; + + // Act + string actualAccessToken = TokenUtils.ExtractAccessToken(jsonResponse); + + // Assert + Assert.Equal(expectedAccessToken, actualAccessToken); + } + + [Fact] + public void ExtractAccessToken_ShouldReturnEmptyString_WhenAccessTokenIsMissing() + { + // Arrange + string jsonResponse = "{\"expires_in\": 3600}"; + + // Act + string actualAccessToken = TokenUtils.ExtractAccessToken(jsonResponse); + + // Assert + Assert.Equal(string.Empty, actualAccessToken); + } + + [Fact] + public void ExtractAccessToken_ShouldReturnEmptyString_WhenJsonIsInvalid() + { + // Arrange + string invalidJsonResponse = "{\"access_token\": \"abc123\", \"expires_in\": }"; // Invalid JSON + + // Act + string actualAccessToken = TokenUtils.ExtractAccessToken(invalidJsonResponse); + + // Assert + Assert.Equal(string.Empty, actualAccessToken); + } + + [Fact] + public void ExtractAccessToken_ShouldReturnEmptyString_WhenAccessTokenIsNull() + { + // Arrange + string jsonResponse = "{\"access_token\": null}"; + + // Act + string actualAccessToken = TokenUtils.ExtractAccessToken(jsonResponse); + + // Assert + Assert.Equal(string.Empty, actualAccessToken); + } + } +} diff --git a/src/PDNDClientAssertionGenerator/Configuration/ClientAssertionConfig.cs b/src/PDNDClientAssertionGenerator/Configuration/ClientAssertionConfig.cs new file mode 100644 index 0000000..d8cfaf5 --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Configuration/ClientAssertionConfig.cs @@ -0,0 +1,67 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +namespace PDNDClientAssertionGenerator.Configuration +{ + public class ClientAssertionConfig + { + /// + /// Gets or sets the authentication server URL + /// + public string ServerUrl { get; set; } + + /// + /// Gets or sets the public key ID (kid) + /// + public string KeyId { get; set; } + + /// + /// Gets or sets the signing algorithm (alg) + /// + /// Actually only RS256 is available + /// "RS256" + public string Algorithm { get; set; } + + /// + /// Gets or sets the type of object + /// + /// "JWT" + public string Type { get; set; } + + /// + /// Gets or sets the Client identifier + /// + public string ClientId { get; set; } + + /// + /// Gets or sets the issuer (iss) + /// + /// Should be set as the client ID + public string Issuer { get; set; } + + /// + /// Gets or sets the subject (sub) + /// + /// Should be set as the client ID + public string Subject { get; set; } + + /// + /// Gets or sets the audience (aud) + /// + public string Audience { get; set; } + + /// + /// Gets or sets the purpose for which access to resources will be requested (purposeId) + /// + public string PurposeId { get; set; } + + /// + /// Gets or sets the path to the private key to sign the client assertion + /// + public string KeyPath { get; set; } + + /// + /// Gets or sets the duration in minutes of the token (this will be used to calculate the token expiration) + /// + public int Duration { get; set; } + } +} diff --git a/src/PDNDClientAssertionGenerator/Interfaces/IClientAssertionGenerator.cs b/src/PDNDClientAssertionGenerator/Interfaces/IClientAssertionGenerator.cs new file mode 100644 index 0000000..b03d926 --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Interfaces/IClientAssertionGenerator.cs @@ -0,0 +1,12 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using PDNDClientAssertionGenerator.Models; + +namespace PDNDClientAssertionGenerator.Interfaces +{ + public interface IClientAssertionGenerator + { + Task GetClientAssertionAsync(); + Task GetTokenAsync(string clientAssertion); + } +} diff --git a/src/PDNDClientAssertionGenerator/Interfaces/IOAuth2Service.cs b/src/PDNDClientAssertionGenerator/Interfaces/IOAuth2Service.cs new file mode 100644 index 0000000..56e7d6a --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Interfaces/IOAuth2Service.cs @@ -0,0 +1,12 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using PDNDClientAssertionGenerator.Models; + +namespace PDNDClientAssertionGenerator.Interfaces +{ + public interface IOAuth2Service + { + Task GenerateClientAssertionAsync(); + Task RequestAccessTokenAsync(string clientAssertion); + } +} \ No newline at end of file diff --git a/src/PDNDClientAssertionGenerator/Middleware/PDNDClientAssertionServiceExtensions.cs b/src/PDNDClientAssertionGenerator/Middleware/PDNDClientAssertionServiceExtensions.cs new file mode 100644 index 0000000..307d94c --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Middleware/PDNDClientAssertionServiceExtensions.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using PDNDClientAssertionGenerator.Configuration; +using PDNDClientAssertionGenerator.Interfaces; +using PDNDClientAssertionGenerator.Services; + +namespace PDNDClientAssertionGenerator.Middleware +{ + public static class PDNDClientAssertionServiceExtensions + { + /// + /// Configures the services required for the PDND Client Assertion process. + /// This method sets up the configuration for `ClientAssertionConfig` and registers necessary services. + /// + /// The IServiceCollection to which the services are added. + /// The updated IServiceCollection instance. + public static IServiceCollection AddPDNDClientAssertionServices(this IServiceCollection services) + { + // Use ConfigurationManager to load the configuration file (appsettings.json) + var configuration = new ConfigurationManager() + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) // Load configuration + .Build(); + + // Ensure that the configuration contains required sections and values + var configSection = configuration.GetSection("ClientAssertionConfig"); + if (!configSection.Exists()) + { + throw new InvalidOperationException("Missing 'ClientAssertionConfig' section in appsettings.json."); + } + + // Register ClientAssertionConfig as a singleton using the IOptions pattern + services.Configure(config => + { + // Copy values from the configuration file into the ClientAssertionConfig model + config.ClientId = configuration["ClientAssertionConfig:ClientId"]; + config.ServerUrl = configuration["ClientAssertionConfig:ServerUrl"]; + config.KeyId = configuration["ClientAssertionConfig:KeyId"]; + config.Algorithm = configuration["ClientAssertionConfig:Algorithm"]; + config.Type = configuration["ClientAssertionConfig:Type"]; + config.Issuer = configuration["ClientAssertionConfig:Issuer"]; + config.Subject = configuration["ClientAssertionConfig:Subject"]; + config.Audience = configuration["ClientAssertionConfig:Audience"]; + config.PurposeId = configuration["ClientAssertionConfig:PurposeId"]; + config.KeyPath = configuration["ClientAssertionConfig:KeyPath"]; + config.Duration = int.Parse(configuration["ClientAssertionConfig:Duration"]); + }); + + // Register OAuth2Service and ClientAssertionGeneratorService as scoped services + services.AddScoped(); + services.AddScoped(); + + // Return the updated service collection + return services; + } + } +} diff --git a/src/PDNDClientAssertionGenerator/Models/PDNDClientAssertion.cs b/src/PDNDClientAssertionGenerator/Models/PDNDClientAssertion.cs new file mode 100644 index 0000000..5aabb59 --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Models/PDNDClientAssertion.cs @@ -0,0 +1,64 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +namespace PDNDClientAssertionGenerator.Models +{ + public class PDNDClientAssertion + { + /// + /// Gets or sets the public key ID (kid) + /// + public required string KeyId { get; init; } + + /// + /// Gets or sets the signing algorithm (alg) + /// + /// "RS256" + public required string Algorithm { get; init; } + + /// + /// Gets or sets the type of object + /// + /// "JWT" + public required string Type { get; init; } + + /// + /// Gets or sets the issuer (iss) + /// + public required string Issuer { get; init; } + + /// + /// Gets or sets the subject (sub) + /// + public required string Subject { get; init; } + + /// + /// Gets or sets the audience (aud) + /// + public required string Audience { get; init; } + + /// + /// Gets or sets the purpose (purposeId) + /// + public required string PurposeId { get; init; } + + /// + /// Gets or sets the JWT identifier + /// + public required Guid TokenId { get; init; } + + /// + /// Gets or sets the token creation date + /// + public required DateTime IssuedAt { get; init; } + + /// + /// Gets or sets the token expiration date + /// + public required DateTime Expiration { get; init; } + + /// + /// Gets or sets the client assertion + /// + public required string ClientAssertion { get; init; } + } +} \ No newline at end of file diff --git a/src/PDNDClientAssertionGenerator/Models/PDNDTokenResponse.cs b/src/PDNDClientAssertionGenerator/Models/PDNDTokenResponse.cs new file mode 100644 index 0000000..bf68057 --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Models/PDNDTokenResponse.cs @@ -0,0 +1,18 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using System.Text.Json.Serialization; + +namespace PDNDClientAssertionGenerator.Models +{ + public class PDNDTokenResponse + { + [JsonPropertyName("token_type")] + public required string TokenType { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } + + [JsonPropertyName("access_token")] + public required string AccessToken { get; set; } + } +} diff --git a/src/PDNDClientAssertionGenerator/PDNDClientAssertionGenerator.csproj b/src/PDNDClientAssertionGenerator/PDNDClientAssertionGenerator.csproj new file mode 100644 index 0000000..4190c62 --- /dev/null +++ b/src/PDNDClientAssertionGenerator/PDNDClientAssertionGenerator.csproj @@ -0,0 +1,41 @@ + + + + net8.0 + enable + enable + .NET Client Assertion Generator for PDND Service API + https://github.com/italia/pdnd-client-assertion-generator + https://github.com/engineering87/pdnd-client-assertion-generator + LICENSE + True + .NET Client Assertion Generator for PDND Service API + README.md + 1.0.1 + + + + + + True + \ + + + True + \ + + + + + + + + + + + + + + + + diff --git a/src/PDNDClientAssertionGenerator/PDNDClientAssertionGenerator.sln b/src/PDNDClientAssertionGenerator/PDNDClientAssertionGenerator.sln new file mode 100644 index 0000000..1b7a839 --- /dev/null +++ b/src/PDNDClientAssertionGenerator/PDNDClientAssertionGenerator.sln @@ -0,0 +1,42 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35201.131 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PDNDClientAssertionGenerator", "PDNDClientAssertionGenerator.csproj", "{5C5D5A27-89FC-4115-908B-8F14355C0143}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{EC21B885-CE5E-427F-9D06-CAED570F2184}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PDNDClientAssertionGenerator.Tests", "..\PDNDClientAssertionGenerator.Tests\PDNDClientAssertionGenerator.Tests.csproj", "{7A2109DE-C26B-4B1C-BD3E-2E2E451DC067}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PDNDClientAssertionGenerator.Api", "..\PDNDClientAssertionGenerator.Api\PDNDClientAssertionGenerator.Api.csproj", "{26CA7E5F-DE21-4155-882A-CF248007B48A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5C5D5A27-89FC-4115-908B-8F14355C0143}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C5D5A27-89FC-4115-908B-8F14355C0143}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5C5D5A27-89FC-4115-908B-8F14355C0143}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5C5D5A27-89FC-4115-908B-8F14355C0143}.Release|Any CPU.Build.0 = Release|Any CPU + {7A2109DE-C26B-4B1C-BD3E-2E2E451DC067}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A2109DE-C26B-4B1C-BD3E-2E2E451DC067}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A2109DE-C26B-4B1C-BD3E-2E2E451DC067}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A2109DE-C26B-4B1C-BD3E-2E2E451DC067}.Release|Any CPU.Build.0 = Release|Any CPU + {26CA7E5F-DE21-4155-882A-CF248007B48A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26CA7E5F-DE21-4155-882A-CF248007B48A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26CA7E5F-DE21-4155-882A-CF248007B48A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26CA7E5F-DE21-4155-882A-CF248007B48A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {26CA7E5F-DE21-4155-882A-CF248007B48A} = {EC21B885-CE5E-427F-9D06-CAED570F2184} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7CF6FBBF-1EE4-44D6-BE3A-8765E5901463} + EndGlobalSection +EndGlobal diff --git a/src/PDNDClientAssertionGenerator/Services/ClientAssertionGeneratorService.cs b/src/PDNDClientAssertionGenerator/Services/ClientAssertionGeneratorService.cs new file mode 100644 index 0000000..2782e83 --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Services/ClientAssertionGeneratorService.cs @@ -0,0 +1,34 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using PDNDClientAssertionGenerator.Interfaces; +using PDNDClientAssertionGenerator.Models; + +namespace PDNDClientAssertionGenerator.Services +{ + // This service handles the generation of client assertions and the retrieval of access tokens. + public class ClientAssertionGeneratorService : IClientAssertionGenerator + { + // Dependency on the OAuth2 service for generating client assertions and requesting tokens. + private readonly IOAuth2Service _oauth2Service; + + // Constructor that injects the IOAuth2Service dependency. + // Throws an ArgumentNullException if the provided OAuth2 service is null. + public ClientAssertionGeneratorService(IOAuth2Service oauth2Service) + { + _oauth2Service = oauth2Service ?? throw new ArgumentNullException(nameof(oauth2Service)); + } + + // Asynchronously generates a client assertion (JWT) by delegating to the OAuth2 service. + public async Task GetClientAssertionAsync() + { + return await _oauth2Service.GenerateClientAssertionAsync(); + } + + // Asynchronously requests an OAuth2 access token using the provided client assertion. + // Delegates the actual token request to the OAuth2 service. + public async Task GetTokenAsync(string clientAssertion) + { + return await _oauth2Service.RequestAccessTokenAsync(clientAssertion); + } + } +} \ No newline at end of file diff --git a/src/PDNDClientAssertionGenerator/Services/OAuth2Service.cs b/src/PDNDClientAssertionGenerator/Services/OAuth2Service.cs new file mode 100644 index 0000000..03137b3 --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Services/OAuth2Service.cs @@ -0,0 +1,127 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using PDNDClientAssertionGenerator.Configuration; +using PDNDClientAssertionGenerator.Interfaces; +using PDNDClientAssertionGenerator.Models; +using PDNDClientAssertionGenerator.Utils; +using System.IdentityModel.Tokens.Jwt; +using System.Net.Http.Headers; +using System.Security.Claims; +using System.Text.Json; + +namespace PDNDClientAssertionGenerator.Services +{ + public class OAuth2Service : IOAuth2Service + { + private readonly ClientAssertionConfig _config; + + // Constructor for OAuth2Service, takes a configuration object. + public OAuth2Service(IOptions config) + { + _config = config.Value ?? throw new ArgumentNullException(nameof(config)); + } + + // Asynchronously generates a client assertion JWT token. + public async Task GenerateClientAssertionAsync() + { + // Generate a unique token ID (JWT ID) + Guid tokenId = Guid.NewGuid(); + + // Define the current UTC time and the token expiration time. + DateTime issuedAt = DateTime.UtcNow; + DateTime expiresAt = issuedAt.AddMinutes(_config.Duration); + + // Define JWT header as a dictionary of key-value pairs. + Dictionary headers = new() + { + { "kid", _config.KeyId }, // Key ID used to identify the signing key + { "alg", _config.Algorithm }, // Algorithm used for signing (e.g., RS256) + { "typ", _config.Type } // Type of the token, usually "JWT" + }; + + // Define the payload as a list of claims, which represent the content of the JWT. + var payloadClaims = new List + { + new Claim(JwtRegisteredClaimNames.Iss, _config.Issuer), // Issuer of the token + new Claim(JwtRegisteredClaimNames.Sub, _config.Subject), // Subject of the token + new Claim(JwtRegisteredClaimNames.Aud, _config.Audience), // Audience for which the token is intended + new Claim("purposeId", _config.PurposeId), // Custom claim for the purpose of the token + new Claim(JwtRegisteredClaimNames.Jti, tokenId.ToString("D").ToLower()), // JWT ID + new Claim(JwtRegisteredClaimNames.Iat, issuedAt.ToUnixTimestamp().ToString(), ClaimValueTypes.Integer64), // Issued At time (as Unix timestamp) + new Claim(JwtRegisteredClaimNames.Exp, expiresAt.ToUnixTimestamp().ToString(), ClaimValueTypes.Integer64) // Expiration time (as Unix timestamp) + }; + + // Create signing credentials using RSA for signing the token. + using var rsa = SecurityUtils.GetRsaFromKeyPath(_config.KeyPath); + var rsaSecurityKey = new RsaSecurityKey(rsa); + var signingCredentials = new SigningCredentials(rsaSecurityKey, SecurityAlgorithms.RsaSha256) + { + CryptoProviderFactory = new CryptoProviderFactory { CacheSignatureProviders = false } + }; + + // Create the JWT token with the specified header and payload claims. + var token = new JwtSecurityToken( + new JwtHeader(signingCredentials, headers), + new JwtPayload(payloadClaims) + ); + + // Use JwtSecurityTokenHandler to convert the token into a string. + var tokenHandler = new JwtSecurityTokenHandler(); + string clientAssertion = string.Empty; + + try + { + clientAssertion = tokenHandler.WriteToken(token); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to generate JWT token.", ex); + } + + return await Task.FromResult(clientAssertion); // Return the generated token as a string. + } + + // Asynchronously requests an access token by sending the client assertion to the OAuth2 server. + public async Task RequestAccessTokenAsync(string clientAssertion) + { + using var httpClient = new HttpClient(); + + // Create the payload for the POST request in URL-encoded format. + var payload = new Dictionary + { + { "client_id", _config.ClientId }, // Client ID as per OAuth2 spec + { "client_assertion", clientAssertion }, // Client assertion (JWT) generated in the previous step + { "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" }, // Assertion type + { "grant_type", "client_credentials" } // Grant type for client credentials + }; + + // Set the Accept header to request JSON responses from the server. + httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // Create the content for the POST request (FormUrlEncodedContent). + var content = new FormUrlEncodedContent(payload); + + // Send the POST request to the OAuth2 server and await the response. + HttpResponseMessage response = await httpClient.PostAsync(_config.ServerUrl, content); + + // Ensure the response indicates success (throws an exception if not). + response.EnsureSuccessStatusCode(); + + // Read and parse the response body as a JSON string. + string jsonResponse = await response.Content.ReadAsStringAsync(); + + try + { + // Deserialize the JSON response into the PDNDTokenResponse object. + return JsonSerializer.Deserialize(jsonResponse); + } + catch (JsonException ex) + { + // Handle JSON deserialization errors. + throw new InvalidOperationException("Failed to deserialize the token response.", ex); + } + } + } +} diff --git a/src/PDNDClientAssertionGenerator/Utils/DateTimeUtils.cs b/src/PDNDClientAssertionGenerator/Utils/DateTimeUtils.cs new file mode 100644 index 0000000..f4b8d3a --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Utils/DateTimeUtils.cs @@ -0,0 +1,23 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +namespace PDNDClientAssertionGenerator.Utils +{ + public static class DateTimeUtils + { + /// + /// Convert a DateTime to a Unix timestamp + /// + /// + /// + public static int ToUnixTimestamp(this DateTime dateTime) + { + // Ensure the DateTime is in UTC to avoid timezone issues + DateTimeOffset dateTimeOffset = new DateTimeOffset(dateTime.ToUniversalTime()); + + // Convert to Unix time in seconds + int unixTimestamp = (int)dateTimeOffset.ToUnixTimeSeconds(); + + return unixTimestamp; + } + } +} diff --git a/src/PDNDClientAssertionGenerator/Utils/SecurityUtils.cs b/src/PDNDClientAssertionGenerator/Utils/SecurityUtils.cs new file mode 100644 index 0000000..2c8010d --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Utils/SecurityUtils.cs @@ -0,0 +1,99 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using System.Security.Cryptography; + +namespace PDNDClientAssertionGenerator.Utils +{ + public static class SecurityUtils + { + /// + /// Retrieves the RSAParameters from a PEM file located at the specified key path. + /// + /// The file path to the PEM file containing the RSA private key. + /// An RSAParameters object containing the RSA key parameters. + /// Thrown when the specified key file is not found. + /// Thrown when the key format is invalid. + public static RSAParameters GetSecurityParameters(string keyPath) + { + // Check if the key path is valid + if (string.IsNullOrWhiteSpace(keyPath)) + { + throw new ArgumentException("Key path cannot be null or empty.", nameof(keyPath)); + } + + // Read the PEM content from the specified file + string pemContent; + try + { + pemContent = File.ReadAllText(keyPath).Trim(); + } + catch (Exception ex) + { + throw new FileNotFoundException("Unable to read the key file.", ex); + } + + // Extract the base64 key content + string base64Key = ExtractBase64Key(pemContent); + byte[] privateKeyBytes; + + try + { + privateKeyBytes = Convert.FromBase64String(base64Key); + } + catch (FormatException ex) + { + throw new FormatException("The key format is invalid.", ex); + } + + using (var rsa = RSA.Create()) + { + rsa.ImportRSAPrivateKey(privateKeyBytes, out _); + return rsa.ExportParameters(true); + } + } + + /// + /// Extracts the base64 encoded key from the PEM formatted string. + /// + /// The PEM formatted string. + /// The base64 encoded key. + public static string ExtractBase64Key(string pemContent) + { + // Remove the header, footer, and any newlines or whitespaces + return pemContent + .Replace("-----BEGIN RSA PRIVATE KEY-----", string.Empty) + .Replace("-----END RSA PRIVATE KEY-----", string.Empty) + .Replace("\n", string.Empty) + .Replace("\r", string.Empty) + .Replace(" ", string.Empty) + .Trim(); + } + + /// + /// Retrieves and imports RSA security parameters from the specified key path. + /// + /// The file path to the RSA private key. + /// An instance of RSA with the imported key parameters. + /// Thrown if there is an error retrieving or importing the RSA parameters. + public static RSA GetRsaFromKeyPath(string keyPath) + { + try + { + // Retrieve RSA security parameters from the specified key path. + RSAParameters rsaParams = GetSecurityParameters(keyPath); + + // Create a new instance of RSA and import the retrieved key parameters. + var rsa = RSA.Create(); + rsa.ImportParameters(rsaParams); + + // Return the configured RSA instance. + return rsa; + } + catch (Exception ex) + { + // If there is an error during the retrieval or import of the key, throw an InvalidOperationException. + throw new InvalidOperationException("Failed to retrieve or import RSA security parameters.", ex); + } + } + } +} diff --git a/src/PDNDClientAssertionGenerator/Utils/TokenUtils.cs b/src/PDNDClientAssertionGenerator/Utils/TokenUtils.cs new file mode 100644 index 0000000..80d052b --- /dev/null +++ b/src/PDNDClientAssertionGenerator/Utils/TokenUtils.cs @@ -0,0 +1,29 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using System.Text.Json; + +namespace PDNDClientAssertionGenerator.Utils +{ + public static class TokenUtils + { + public static string ExtractAccessToken(string response) + { + try + { + using (JsonDocument document = JsonDocument.Parse(response)) + { + if (document.RootElement.TryGetProperty("access_token", out JsonElement accessTokenElement)) + { + return accessTokenElement.GetString() ?? string.Empty; + } + } + + return string.Empty; + } + catch (JsonException) + { + return string.Empty; + } + } + } +}