From a43010af71f02dc499f3c47eebaea918711404c6 Mon Sep 17 00:00:00 2001 From: Tobias Sibera Date: Tue, 29 Oct 2019 20:31:45 +0100 Subject: [PATCH] Initial commit --- .gitignore | 330 ++++++++++++++++++ LICENSE | 21 ++ .../Authenticators/BasicAuthenticatorTests.cs | 80 +++++ .../Authenticators/JwtAuthenticatorTests.cs | 80 +++++ .../Authenticators/OidcAuthenticatorTests.cs | 161 +++++++++ .../OData.Client.Manager.Tests.csproj | 22 ++ .../ODataManagerTests.cs | 66 ++++ .../ODataMangerConfigurationTests.cs | 64 ++++ OData.Client.Manager.Tests/Product.cs | 21 ++ .../UriExtensionsTests.cs | 53 +++ .../MimeTypeVersioningManagerTests.cs | 59 ++++ .../QueryParamVersioningManagerTests.cs | 48 +++ OData.Client.Manager.sln | 37 ++ .../Authenticators/AuthenticatorBase.cs | 54 +++ .../Authenticators/BasicAuthenticator.cs | 19 + .../Authenticators/IAuthenticator.cs | 31 ++ .../Authenticators/JwtAuthenticator.cs | 21 ++ .../Authenticators/OidcAuthenticator.cs | 166 +++++++++ .../Authenticators/OidcSettings.cs | 75 ++++ .../Extensions/UriExtensions.cs | 33 ++ OData.Client.Manager/IODataManager.cs | 12 + .../OData.Client.Manager.csproj | 52 +++ OData.Client.Manager/ODataManager.cs | 67 ++++ .../ODataManagerConfiguration.cs | 40 +++ .../Properties/AssemblyInfo.cs | 3 + .../Versioning/IVersioningManager.cs | 20 ++ .../Versioning/MimeTypeVersioningManager.cs | 47 +++ .../Versioning/QueryParamVersioningManager.cs | 38 ++ README.md | 96 +++++ TestAuthorizationServer/Program.cs | 20 ++ TestAuthorizationServer/Startup.cs | 69 ++++ .../TestAuthorizationServer.csproj | 14 + .../appsettings.Development.json | 9 + TestAuthorizationServer/appsettings.json | 10 + TestAuthorizationServer/tempkey.rsa | 1 + code-analysis.ruleset | 86 +++++ icon.png | Bin 0 -> 1638 bytes stylecop.json | 25 ++ 38 files changed, 2050 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 OData.Client.Manager.Tests/Authenticators/BasicAuthenticatorTests.cs create mode 100644 OData.Client.Manager.Tests/Authenticators/JwtAuthenticatorTests.cs create mode 100644 OData.Client.Manager.Tests/Authenticators/OidcAuthenticatorTests.cs create mode 100644 OData.Client.Manager.Tests/OData.Client.Manager.Tests.csproj create mode 100644 OData.Client.Manager.Tests/ODataManagerTests.cs create mode 100644 OData.Client.Manager.Tests/ODataMangerConfigurationTests.cs create mode 100644 OData.Client.Manager.Tests/Product.cs create mode 100644 OData.Client.Manager.Tests/UriExtensionsTests.cs create mode 100644 OData.Client.Manager.Tests/Versioning/MimeTypeVersioningManagerTests.cs create mode 100644 OData.Client.Manager.Tests/Versioning/QueryParamVersioningManagerTests.cs create mode 100644 OData.Client.Manager.sln create mode 100644 OData.Client.Manager/Authenticators/AuthenticatorBase.cs create mode 100644 OData.Client.Manager/Authenticators/BasicAuthenticator.cs create mode 100644 OData.Client.Manager/Authenticators/IAuthenticator.cs create mode 100644 OData.Client.Manager/Authenticators/JwtAuthenticator.cs create mode 100644 OData.Client.Manager/Authenticators/OidcAuthenticator.cs create mode 100644 OData.Client.Manager/Authenticators/OidcSettings.cs create mode 100644 OData.Client.Manager/Extensions/UriExtensions.cs create mode 100644 OData.Client.Manager/IODataManager.cs create mode 100644 OData.Client.Manager/OData.Client.Manager.csproj create mode 100644 OData.Client.Manager/ODataManager.cs create mode 100644 OData.Client.Manager/ODataManagerConfiguration.cs create mode 100644 OData.Client.Manager/Properties/AssemblyInfo.cs create mode 100644 OData.Client.Manager/Versioning/IVersioningManager.cs create mode 100644 OData.Client.Manager/Versioning/MimeTypeVersioningManager.cs create mode 100644 OData.Client.Manager/Versioning/QueryParamVersioningManager.cs create mode 100644 README.md create mode 100644 TestAuthorizationServer/Program.cs create mode 100644 TestAuthorizationServer/Startup.cs create mode 100644 TestAuthorizationServer/TestAuthorizationServer.csproj create mode 100644 TestAuthorizationServer/appsettings.Development.json create mode 100644 TestAuthorizationServer/appsettings.json create mode 100644 TestAuthorizationServer/tempkey.rsa create mode 100644 code-analysis.ruleset create mode 100644 icon.png create mode 100644 stylecop.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e759b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,330 @@ +## 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/master/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# 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 + +# 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/ +**/Properties/launchSettings.json + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.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 + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# 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 +# 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 + +# 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 + +# 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 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/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# 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/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf0ffba --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Tobias Sibera + +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/OData.Client.Manager.Tests/Authenticators/BasicAuthenticatorTests.cs b/OData.Client.Manager.Tests/Authenticators/BasicAuthenticatorTests.cs new file mode 100644 index 0000000..aa4467c --- /dev/null +++ b/OData.Client.Manager.Tests/Authenticators/BasicAuthenticatorTests.cs @@ -0,0 +1,80 @@ +using OData.Client.Manager.Authenticators; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace OData.Client.Manager.Tests.Authenticators +{ + public class BasicAuthenticatorTests + { + private const string uriString = "http://domain.com"; + private readonly HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(uriString)); + private readonly HttpClient httpClient = new HttpClient { BaseAddress = new Uri(uriString) }; + + [Theory] + [InlineData("foo", "bar", "Basic Zm9vOmJhcg==", "Request header 'Authorization' already set")] + [InlineData("foo", "bar", "Basic Zm9vOmJhcg==", null)] + public async Task AuthenticateWithRequestMessage_Sucess(string user, string pw, string expectedAuthHeader, string errorMessage) + { + string error = null; + var replaceAuthHeader = errorMessage == null; + var authenticator = new BasicAuthenticator(user, pw) + { + ReplaceAuthorizationHeader = replaceAuthHeader + }; + authenticator.OnTrace += (msg) => error = msg; + + Assert.True(await authenticator.AuthenticateAsync(requestMessage)); + Assert.Equal(replaceAuthHeader, await authenticator.AuthenticateAsync(requestMessage)); + + Assert.Equal(errorMessage, error); + Assert.NotNull(requestMessage.Headers.Authorization); + Assert.Equal(authenticator.Header, requestMessage.Headers.Authorization); + Assert.Equal(expectedAuthHeader, requestMessage.Headers.Authorization.ToString()); + } + + [Theory] + [InlineData("foo", "bar", "Basic Zm9vOmJhcg==", "Request header 'Authorization' already set")] + [InlineData("foo", "bar", "Basic Zm9vOmJhcg==", null)] + public async Task AuthenticateWithHttpClient_Sucess(string user, string pw, string expectedAuthHeader, string errorMessage) + { + string error = null; + var replaceAuthHeader = errorMessage == null; + var authenticator = new BasicAuthenticator(user, pw) + { + ReplaceAuthorizationHeader = replaceAuthHeader + }; + authenticator.OnTrace += (msg) => error = msg; + + Assert.True(await authenticator.AuthenticateAsync(httpClient)); + Assert.Equal(replaceAuthHeader, await authenticator.AuthenticateAsync(httpClient)); + + Assert.Equal(errorMessage, error); + Assert.NotNull(httpClient.DefaultRequestHeaders.Authorization); + Assert.Equal(authenticator.Header, httpClient.DefaultRequestHeaders.Authorization); + Assert.Equal(expectedAuthHeader, httpClient.DefaultRequestHeaders.Authorization.ToString()); + } + + [Theory] + [InlineData(null, "foo", "userName")] + public void InstantiateWithInvalidValues_Exception(string user, string pw, string paramName) + { + var ex = Assert.Throws(() => new BasicAuthenticator(user, pw)); + Assert.Equal(paramName, ex.ParamName); + } + + [Fact] + public async Task AuthenticateOnNullObject_Exception() + { + var versioningManager = new BasicAuthenticator("foo", "bar"); + ArgumentNullException ex; + + ex = await Assert.ThrowsAsync(() => versioningManager.AuthenticateAsync(requestMessage: null)); + Assert.Equal("requestMessage", ex.ParamName); + + ex = await Assert.ThrowsAsync(() => versioningManager.AuthenticateAsync(httpClient: null)); + Assert.Equal("httpClient", ex.ParamName); + } + } +} diff --git a/OData.Client.Manager.Tests/Authenticators/JwtAuthenticatorTests.cs b/OData.Client.Manager.Tests/Authenticators/JwtAuthenticatorTests.cs new file mode 100644 index 0000000..a52d709 --- /dev/null +++ b/OData.Client.Manager.Tests/Authenticators/JwtAuthenticatorTests.cs @@ -0,0 +1,80 @@ +using OData.Client.Manager.Authenticators; +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace OData.Client.Manager.Tests.Authenticators +{ + public class JwtAuthenticatorTests + { + private const string uriString = "http://domain.com"; + private readonly HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(uriString)); + private readonly HttpClient httpClient = new HttpClient { BaseAddress = new Uri(uriString) }; + + [Theory] + [InlineData("foobar", "Bearer foobar", "Request header 'Authorization' already set")] + [InlineData("foobar", "Bearer foobar", null)] + public async Task AuthenticateWithRequestMessage_Sucess(string token, string expectedAuthHeader, string errorMessage) + { + string error = null; + var replaceAuthHeader = errorMessage == null; + var authenticator = new JwtAuthenticator(token) + { + ReplaceAuthorizationHeader = replaceAuthHeader + }; + authenticator.OnTrace += (msg) => error = msg; + + Assert.True(await authenticator.AuthenticateAsync(requestMessage)); + Assert.Equal(replaceAuthHeader, await authenticator.AuthenticateAsync(requestMessage)); + + Assert.Equal(errorMessage, error); + Assert.NotNull(requestMessage.Headers.Authorization); + Assert.Equal(authenticator.Header, requestMessage.Headers.Authorization); + Assert.Equal(expectedAuthHeader, requestMessage.Headers.Authorization.ToString()); + } + + [Theory] + [InlineData("foobar", "Bearer foobar", "Request header 'Authorization' already set")] + [InlineData("foobar", "Bearer foobar", null)] + public async Task AuthenticateWithHttpClient_Sucess(string token, string expectedAuthHeader, string errorMessage) + { + string error = null; + var replaceAuthHeader = errorMessage == null; + var authenticator = new JwtAuthenticator(token) + { + ReplaceAuthorizationHeader = replaceAuthHeader + }; + authenticator.OnTrace += (msg) => error = msg; + + Assert.True(await authenticator.AuthenticateAsync(httpClient)); + Assert.Equal(replaceAuthHeader, await authenticator.AuthenticateAsync(httpClient)); + + Assert.Equal(errorMessage, error); + Assert.NotNull(httpClient.DefaultRequestHeaders.Authorization); + Assert.Equal(authenticator.Header, httpClient.DefaultRequestHeaders.Authorization); + Assert.Equal(expectedAuthHeader, httpClient.DefaultRequestHeaders.Authorization.ToString()); + } + + [Theory] + [InlineData(null, "token")] + public void InstantiateWithInvalidValues_Exception(string token, string paramName) + { + var ex = Assert.Throws(() => new JwtAuthenticator(token)); + Assert.Equal(paramName, ex.ParamName); + } + + [Fact] + public async Task AuthenticateOnNullObject_Exception() + { + var versioningManager = new JwtAuthenticator("foobar"); + ArgumentNullException ex; + + ex = await Assert.ThrowsAsync(() => versioningManager.AuthenticateAsync(requestMessage: null)); + Assert.Equal("requestMessage", ex.ParamName); + + ex = await Assert.ThrowsAsync(() => versioningManager.AuthenticateAsync(httpClient: null)); + Assert.Equal("httpClient", ex.ParamName); + } + } +} diff --git a/OData.Client.Manager.Tests/Authenticators/OidcAuthenticatorTests.cs b/OData.Client.Manager.Tests/Authenticators/OidcAuthenticatorTests.cs new file mode 100644 index 0000000..eae14ff --- /dev/null +++ b/OData.Client.Manager.Tests/Authenticators/OidcAuthenticatorTests.cs @@ -0,0 +1,161 @@ +using IdentityModel; +using OData.Client.Manager.Authenticators; +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Xunit; + +namespace OData.Client.Manager.Tests.Authenticators +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Major Code Smell", "S3881:\"IDisposable\" should be implemented correctly", Justification = "Test case")] + public class OidcAuthenticatorTests : IDisposable + { + private readonly int pid; + private readonly Process api; + + private const string uriString = "http://domain.com"; + private readonly HttpRequestMessage requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(uriString)); + private readonly HttpClient httpClient = new HttpClient { BaseAddress = new Uri(uriString) }; + private readonly OidcSettings settings = new OidcSettings(null) + { + DiscoveryPolicy = null, + GrantType = null, + ClientId = null, + ClientSecret = null, + Username = null, + Password = null, + Scope = null, + RedirectUri = null, + Code = null, + CodeVerifier = null, + HttpClient = null + }; + + public OidcAuthenticatorTests() + { + api = new Process { StartInfo = new ProcessStartInfo(Path.GetFullPath($"../../../../TestAuthorizationServer/bin/{(Debugger.IsAttached ? "Debug" : "Release")}/netcoreapp3.0/TestAuthorizationServer.exe")) }; + api.Start(); + pid = api.Id; + } + + public void Dispose() + { + Process.GetProcessById(pid).Kill(); + api?.Dispose(); + requestMessage?.Dispose(); + httpClient?.Dispose(); + } + + [Theory] + [InlineData("Request header 'Authorization' already set")] + [InlineData(null)] + public async Task AuthenticateWithRequestMessage_Sucess(string errorMessage) + { + settings.AuthUri = new Uri("http://localhost:5000"); + settings.ClientId = "odata-manager"; + settings.ClientSecret = "secret"; + settings.Scope = "api1"; + settings.Username = "bob"; + settings.Password = "bob"; + settings.GrantType = OidcConstants.GrantTypes.Password; + + string error = null; + var replaceAuthHeader = errorMessage == null; + var authenticator = new OidcAuthenticator(settings) + { + ReplaceAuthorizationHeader = replaceAuthHeader + }; + + authenticator.OnTrace += (msg) => error = msg; + + Assert.True(await authenticator.AuthenticateAsync(requestMessage)); + Assert.Equal(replaceAuthHeader, await authenticator.AuthenticateAsync(requestMessage)); + + Assert.Equal(errorMessage, error); + Assert.NotNull(requestMessage.Headers.Authorization); + Assert.Equal(authenticator.Header, requestMessage.Headers.Authorization); + Assert.StartsWith("Bearer ey", requestMessage.Headers.Authorization.ToString()); + } + + [Theory] + [InlineData("Request header 'Authorization' already set")] + [InlineData(null)] + public async Task AuthenticateWithHttpClient_Sucess(string errorMessage) + { + settings.AuthUri = new Uri("http://localhost:5000"); + settings.ClientId = "odata-manager"; + settings.ClientSecret = "secret"; + settings.Scope = "api1"; + settings.Username = "bob"; + settings.Password = "bob"; + settings.GrantType = OidcConstants.GrantTypes.Password; + + string error = null; + var replaceAuthHeader = errorMessage == null; + var authenticator = new OidcAuthenticator(settings) + { + ReplaceAuthorizationHeader = replaceAuthHeader + }; + authenticator.OnTrace += (msg) => error = msg; + + Assert.True(await authenticator.AuthenticateAsync(httpClient)); + Assert.Equal(replaceAuthHeader, await authenticator.AuthenticateAsync(httpClient)); + + Assert.Equal(errorMessage, error); + Assert.NotNull(httpClient.DefaultRequestHeaders.Authorization); + Assert.Equal(authenticator.Header, httpClient.DefaultRequestHeaders.Authorization); + Assert.StartsWith("Bearer ey", httpClient.DefaultRequestHeaders.Authorization.ToString()); + } + + [Fact] + public void InstantiateWithInvalidValues_Exception() + { + var ex = Assert.Throws(() => new OidcAuthenticator(null)); + Assert.Equal("oidcSettings", ex.ParamName); + } + + [Fact] + public async Task AuthenticateOnNullObject_Exception() + { + var versioningManager = new OidcAuthenticator(settings); + ArgumentNullException ex; + + ex = await Assert.ThrowsAsync(() => versioningManager.AuthenticateAsync(requestMessage: null)); + Assert.Equal("requestMessage", ex.ParamName); + + ex = await Assert.ThrowsAsync(() => versioningManager.AuthenticateAsync(httpClient: null)); + Assert.Equal("httpClient", ex.ParamName); + } + + [Fact] + public async Task GetTokenWithRefreshToken_Sucess() + { + settings.AuthUri = new Uri("http://localhost:5000"); + settings.ClientId = "odata-manager-2"; + settings.ClientSecret = "secret"; + settings.Scope = "api1 offline_access"; + settings.Username = "alice"; + settings.Password = "alice"; + settings.GrantType = OidcConstants.GrantTypes.Password; + + string error = null; + var authenticator = new OidcAuthenticator(settings); + authenticator.OnTrace += (msg) => error = msg; + + Assert.True(await authenticator.AuthenticateAsync(httpClient)); + + Assert.NotNull(httpClient.DefaultRequestHeaders.Authorization); + Assert.StartsWith("Bearer ey", httpClient.DefaultRequestHeaders.Authorization.ToString()); + + var token1 = await authenticator.GetTokenAsync(default); + Assert.NotNull(token1?.RefreshToken); + + var token2 = await authenticator.GetTokenAsync(default); + Assert.NotNull(token2?.RefreshToken); + + Assert.NotEqual(token1.RefreshToken, token2.RefreshToken); + } + } +} diff --git a/OData.Client.Manager.Tests/OData.Client.Manager.Tests.csproj b/OData.Client.Manager.Tests/OData.Client.Manager.Tests.csproj new file mode 100644 index 0000000..2d781d0 --- /dev/null +++ b/OData.Client.Manager.Tests/OData.Client.Manager.Tests.csproj @@ -0,0 +1,22 @@ + + + + {337f47d0-c9e8-4312-92c6-6aa89f07bd92} + netcoreapp2.2 + latest + false + + + + + + + + + + + + + + + diff --git a/OData.Client.Manager.Tests/ODataManagerTests.cs b/OData.Client.Manager.Tests/ODataManagerTests.cs new file mode 100644 index 0000000..e58c26b --- /dev/null +++ b/OData.Client.Manager.Tests/ODataManagerTests.cs @@ -0,0 +1,66 @@ +using Extensions.Dictionary; +using OData.Client.Manager.Authenticators; +using OData.Client.Manager.Versioning; +using Simple.OData.Client; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace OData.Client.Manager.Tests +{ + public class ODataManagerTests + { + private const string BaseAdress = "https://services.odata.org/V4/OData/OData.svc/"; + private static readonly Uri BaseUri = new Uri(BaseAdress); + + [Fact] + public void CtorWithNullValues_Exception() + { + var ex1 = Assert.Throws(() => new ODataManager(configuration: null)); + Assert.Equal("configuration", ex1.ParamName); + + var ex2 = Assert.Throws(() => new ODataManager(apiEndpoint: null)); + Assert.Equal("Unable to create client session with no URI specified.", ex2.Message); + } + + [Fact] + public void ClientIsNeverNull_True() + { + var manager = new ODataManager(BaseUri); + Assert.NotNull(manager.Client); + } + + [Fact] + public async Task UseODataClientAndOnTraceAndConverter_Success() + { + var trace = default(string); + ODataManagerConfiguration config = new ODataManagerConfiguration(BaseUri) + { + Authenticator = new BasicAuthenticator("user", "pw"), + VersioningManager = new QueryParamVersioningManager("1.0") + }; + config.TypeCache.Converter.RegisterTypeConverter((IDictionary dict) => dict.ToInstance()); + config.OnTrace = (format, args) => trace = string.Format(format, args); + var manager = new ODataManager(config); + + trace = null; + var dx = ODataDynamic.Expression; + IEnumerable entities = await manager.Client + .For(dx.Products) + .FindEntriesAsync(); + + Assert.NotEmpty(trace); + Assert.Equal(11, entities.ToList().Count); + + trace = null; + var entities2 = await manager.Client + .For("Products") + .FindEntriesAsync(); + + Assert.NotEmpty(trace); + Assert.Equal(11, entities2.ToList().Count); + } + } +} diff --git a/OData.Client.Manager.Tests/ODataMangerConfigurationTests.cs b/OData.Client.Manager.Tests/ODataMangerConfigurationTests.cs new file mode 100644 index 0000000..349dc84 --- /dev/null +++ b/OData.Client.Manager.Tests/ODataMangerConfigurationTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Net.Http; +using Xunit; + +namespace OData.Client.Manager.Tests +{ + public class ODataMangerConfigurationTests + { + [Fact] + public void Initialize_Configuration_DefaultCtor() + { + var config = new ODataManagerConfiguration(); + + Assert.True(config.UseAbsoluteReferenceUris); + Assert.Null(config.BaseUri); + Assert.Null(config.HttpClient); + Assert.Null(config.Authenticator); + Assert.Null(config.VersioningManager); + } + + [Fact] + public void Initialize_Configuration_HttpClientCtor() + { + var config = new ODataManagerConfiguration(new HttpClient { BaseAddress = new Uri("https://localhost:5000") }); + + Assert.True(config.UseAbsoluteReferenceUris); + Assert.NotNull(config.BaseUri); + Assert.NotNull(config.HttpClient); + Assert.Null(config.Authenticator); + Assert.Null(config.VersioningManager); + + Assert.Equal("https://localhost:5000/", config.BaseUri.ToString()); + Assert.Equal(config.BaseUri.ToString(), config.HttpClient.BaseAddress.ToString()); + } + + [Fact] + public void Initialize_Configuration_UriClientCtor() + { + var config = new ODataManagerConfiguration(new Uri("https://localhost:5000")); + + Assert.True(config.UseAbsoluteReferenceUris); + Assert.NotNull(config.BaseUri); + Assert.Null(config.HttpClient); + Assert.Null(config.Authenticator); + Assert.Null(config.VersioningManager); + + Assert.Equal("https://localhost:5000/", config.BaseUri.ToString()); + } + + [Fact] + public void CtorWithOnlyOneApiEndpoint_Exception() + { + var ex = Assert.Throws(() => + { + _ = new ODataManagerConfiguration(new HttpClient { BaseAddress = new Uri("http://localhost") }) + { + BaseUri = new Uri("http://localhost") + }; + }); + + Assert.Equal("Unable to set BaseUri when BaseAddress is specified on HttpClient.", ex.Message); + } + } +} diff --git a/OData.Client.Manager.Tests/Product.cs b/OData.Client.Manager.Tests/Product.cs new file mode 100644 index 0000000..0654236 --- /dev/null +++ b/OData.Client.Manager.Tests/Product.cs @@ -0,0 +1,21 @@ +using System; + +namespace OData.Client.Manager.Tests +{ + public class Product + { + public int ID { get; set; } + + public string Name { get; set; } + + public string Description { get; set; } + + public DateTime ReleaseDate { get; set; } + + public DateTime? DiscontinuedDate { get; set; } + + public int Rating { get; set; } + + public double Price { get; set; } + } +} diff --git a/OData.Client.Manager.Tests/UriExtensionsTests.cs b/OData.Client.Manager.Tests/UriExtensionsTests.cs new file mode 100644 index 0000000..48076b3 --- /dev/null +++ b/OData.Client.Manager.Tests/UriExtensionsTests.cs @@ -0,0 +1,53 @@ +using OData.Client.Manager.Extensions; +using System; +using Xunit; + +namespace OData.Client.Manager.Tests +{ + public class UriExtensionsTests + { + [Theory] + [InlineData("http://domain.com", "foo", "bar", "http://domain.com/?foo=bar")] + [InlineData("http://domain.com", "foo", "better bar", "http://domain.com/?foo=better bar")] + [InlineData("http://domain.com?test=value", "foo", "bar", "http://domain.com/?test=value&foo=bar")] + [InlineData("http://domain.com", "foo", null, "http://domain.com/")] + [InlineData("http://domain.com", "foo", "", "http://domain.com/")] + [InlineData("http://domain.com", "foo", " ", "http://domain.com/")] + [InlineData("http://domain.com?test=value", "foo", null, "http://domain.com/?test=value")] + [InlineData("http://domain.com?test=value", "foo", "", "http://domain.com/?test=value")] + [InlineData("http://domain.com?test=value", "foo", " ", "http://domain.com/?test=value")] + public void AddParameter_ValidValues_Success(string adress, string name, string value, string expectedAdress) + { + // Arrange + var request = new Uri(adress); + + // Act + var newRequest = request.AddParameter(name, value); + + // Assert + Assert.Equal(expectedAdress, newRequest.ToString()); + } + + [Fact] + public void AddParameter_NullEmptyWhitespaceArguments_ArgumentNullException() + { + // Arrange + Uri uri = null; + ArgumentNullException ex; + + // Act / Assert + ex = Assert.Throws(() => uri.AddParameter("foo", "bar")); + Assert.Equal(nameof(uri), ex.ParamName); + + uri = new Uri("http://domain.com"); + ex = Assert.Throws(() => uri.AddParameter(null, "bar")); + Assert.Equal("name", ex.ParamName); + + ex = Assert.Throws(() => uri.AddParameter(string.Empty, "bar")); + Assert.Equal("name", ex.ParamName); + + ex = Assert.Throws(() => uri.AddParameter(" ", "bar")); + Assert.Equal("name", ex.ParamName); + } + } +} diff --git a/OData.Client.Manager.Tests/Versioning/MimeTypeVersioningManagerTests.cs b/OData.Client.Manager.Tests/Versioning/MimeTypeVersioningManagerTests.cs new file mode 100644 index 0000000..b97af40 --- /dev/null +++ b/OData.Client.Manager.Tests/Versioning/MimeTypeVersioningManagerTests.cs @@ -0,0 +1,59 @@ +using OData.Client.Manager.Versioning; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using Xunit; + +namespace OData.Client.Manager.Tests.Versioning +{ + public class MimeTypeVersioningManagerTests + { + [Theory] + [InlineData("accept", "v", "1.1", "v=1.1")] + [InlineData("accept", "v=1.2", "", "v=1.2")] + [InlineData("accept", "v=1.2", " ", "v=1.2")] + [InlineData("accept", "v=1.2", null, "v=1.2")] + [InlineData("custom", "v", "1.3", "v=1.3")] + [InlineData("custom", "v=1.4", "", "v=1.4")] + [InlineData("custom", "v=1.4", " ", "v=1.4")] + [InlineData("custom", "v=1.4", null, "v=1.4")] + public void InstantiateAndApplyWithValidValues_Success(string header, string mime, string version, string expectedValue) + { + string error = null; + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri("http://domain.com")); + requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var versioningManager = new MimeTypeVersioningManager(version, mime, header); + versioningManager.OnTrace += (msg) => error = msg; + + versioningManager.ApplyVersion(requestMessage); + versioningManager.ApplyVersion(requestMessage); + + Assert.NotEmpty(error); + Assert.True(requestMessage.Headers.TryGetValues(header, out IEnumerable values)); + Assert.Single(values, expectedValue); + } + + [Theory] + [InlineData("", "foo", "header")] + [InlineData(" ", "foo", "header")] + [InlineData(null, "foo", "header")] + [InlineData("foo", "", "mimeType")] + [InlineData("foo", " ", "mimeType")] + [InlineData("foo", null, "mimeType")] + public void InstantiateWithInvalidValues_Exception(string header, string mime, string paramName) + { + var ex = Assert.Throws(() => new MimeTypeVersioningManager(null, mime, header)); + Assert.Equal(paramName, ex.ParamName); + } + + [Fact] + public void ApplyVersionOnNullObject_Exception() + { + var versioningManager = new MimeTypeVersioningManager("1.0"); + + var ex = Assert.Throws(() => versioningManager.ApplyVersion(null)); + Assert.Equal("requestMessage", ex.ParamName); + } + } +} diff --git a/OData.Client.Manager.Tests/Versioning/QueryParamVersioningManagerTests.cs b/OData.Client.Manager.Tests/Versioning/QueryParamVersioningManagerTests.cs new file mode 100644 index 0000000..1450a98 --- /dev/null +++ b/OData.Client.Manager.Tests/Versioning/QueryParamVersioningManagerTests.cs @@ -0,0 +1,48 @@ +using OData.Client.Manager.Versioning; +using System; +using System.Net.Http; +using Xunit; + +namespace OData.Client.Manager.Tests.Versioning +{ + public class QueryParamVersioningManagerTests + { + [Theory] + [InlineData("http://domain.com", "v", "1.1", "http://domain.com/?v=1.1", "The already existing query parameter v gets overridden")] + [InlineData("http://domain.com", "v", "", "http://domain.com/", null)] + [InlineData("http://domain.com", "v", " ", "http://domain.com/", null)] + [InlineData("http://domain.com", "v", null, "http://domain.com/", null)] + public void InstantiateAndApplyWithValidValues_Success(string uriString, string paramName, string version, string expectedUri, string errorMessage) + { + string error = null; + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, new Uri(uriString)); + var versioningManager = new QueryParamVersioningManager(version, paramName); + versioningManager.OnTrace += (msg) => error = msg; + + versioningManager.ApplyVersion(requestMessage); + versioningManager.ApplyVersion(requestMessage); + + Assert.Equal(errorMessage, error); + Assert.Equal(expectedUri, requestMessage.RequestUri.ToString()); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void InstantiateWithInvalidValues_Exception(string paramName) + { + var ex = Assert.Throws(() => new QueryParamVersioningManager(null, paramName)); + Assert.Equal("parameterName", ex.ParamName); + } + + [Fact] + public void ApplyVersionOnNullObject_Exception() + { + var versioningManager = new QueryParamVersioningManager("1.0"); + + var ex = Assert.Throws(() => versioningManager.ApplyVersion(null)); + Assert.Equal("requestMessage", ex.ParamName); + } + } +} diff --git a/OData.Client.Manager.sln b/OData.Client.Manager.sln new file mode 100644 index 0000000..fc3bb32 --- /dev/null +++ b/OData.Client.Manager.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29102.190 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OData.Client.Manager", "OData.Client.Manager\OData.Client.Manager.csproj", "{083B92DC-DD57-4D4A-AAF4-C6740685C603}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OData.Client.Manager.Tests", "OData.Client.Manager.Tests\OData.Client.Manager.Tests.csproj", "{083B92DC-DD57-4D4A-AAF4-A6740685C602}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestAuthorizationServer", "TestAuthorizationServer\TestAuthorizationServer.csproj", "{8EE1380B-F3B8-4DA3-B33C-D8B904353227}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {083B92DC-DD57-4D4A-AAF4-C6740685C603}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {083B92DC-DD57-4D4A-AAF4-C6740685C603}.Debug|Any CPU.Build.0 = Debug|Any CPU + {083B92DC-DD57-4D4A-AAF4-C6740685C603}.Release|Any CPU.ActiveCfg = Release|Any CPU + {083B92DC-DD57-4D4A-AAF4-C6740685C603}.Release|Any CPU.Build.0 = Release|Any CPU + {083B92DC-DD57-4D4A-AAF4-A6740685C602}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {083B92DC-DD57-4D4A-AAF4-A6740685C602}.Debug|Any CPU.Build.0 = Debug|Any CPU + {083B92DC-DD57-4D4A-AAF4-A6740685C602}.Release|Any CPU.ActiveCfg = Release|Any CPU + {083B92DC-DD57-4D4A-AAF4-A6740685C602}.Release|Any CPU.Build.0 = Release|Any CPU + {8EE1380B-F3B8-4DA3-B33C-D8B904353227}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EE1380B-F3B8-4DA3-B33C-D8B904353227}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EE1380B-F3B8-4DA3-B33C-D8B904353227}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EE1380B-F3B8-4DA3-B33C-D8B904353227}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1E70B770-3B2F-4723-A5E1-4E411EB9F7E2} + EndGlobalSection +EndGlobal diff --git a/OData.Client.Manager/Authenticators/AuthenticatorBase.cs b/OData.Client.Manager/Authenticators/AuthenticatorBase.cs new file mode 100644 index 0000000..8ac4f96 --- /dev/null +++ b/OData.Client.Manager/Authenticators/AuthenticatorBase.cs @@ -0,0 +1,54 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace OData.Client.Manager.Authenticators +{ + public abstract class AuthenticatorBase : IAuthenticator + { + public AuthenticationHeaderValue? Header { get; protected set; } + + public bool ReplaceAuthorizationHeader { get; set; } + + /// + public Action? OnTrace { get; set; } + + /// + public virtual Task AuthenticateAsync(HttpRequestMessage requestMessage, CancellationToken ct = default) + { + if (requestMessage == null) + { + throw new ArgumentNullException(nameof(requestMessage)); + } + + if (!ReplaceAuthorizationHeader && requestMessage.Headers.Authorization != null) + { + OnTrace?.Invoke($"Request header '{nameof(requestMessage.Headers.Authorization)}' already set"); + return Task.FromResult(false); + } + + requestMessage.Headers.Authorization = Header; + return Task.FromResult(true); + } + + /// + public virtual Task AuthenticateAsync(HttpClient httpClient, CancellationToken ct = default) + { + if (httpClient == null) + { + throw new ArgumentNullException(nameof(httpClient)); + } + + if (!ReplaceAuthorizationHeader && httpClient.DefaultRequestHeaders.Authorization != null) + { + OnTrace?.Invoke($"Request header '{nameof(httpClient.DefaultRequestHeaders.Authorization)}' already set"); + return Task.FromResult(false); + } + + httpClient.DefaultRequestHeaders.Authorization = Header; + return Task.FromResult(true); + } + } +} diff --git a/OData.Client.Manager/Authenticators/BasicAuthenticator.cs b/OData.Client.Manager/Authenticators/BasicAuthenticator.cs new file mode 100644 index 0000000..06d04a7 --- /dev/null +++ b/OData.Client.Manager/Authenticators/BasicAuthenticator.cs @@ -0,0 +1,19 @@ +using System; +using System.Net.Http; + +namespace OData.Client.Manager.Authenticators +{ + public class BasicAuthenticator : AuthenticatorBase + { + public BasicAuthenticator(string username, string password) + { + Header = new BasicAuthenticationHeaderValue(username, password); + } + + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + private BasicAuthenticator() + { + throw new NotSupportedException(); + } + } +} diff --git a/OData.Client.Manager/Authenticators/IAuthenticator.cs b/OData.Client.Manager/Authenticators/IAuthenticator.cs new file mode 100644 index 0000000..760bfb8 --- /dev/null +++ b/OData.Client.Manager/Authenticators/IAuthenticator.cs @@ -0,0 +1,31 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace OData.Client.Manager.Authenticators +{ + public interface IAuthenticator + { + /// + /// Gets or sets the action delegate that will be executed to write trace messages. + /// + Action? OnTrace { get; set; } + + /// + /// Ensures the authentication of the given http request message. + /// + /// The http request message which has to be authenticated. + /// Cancellation token. + /// Indicates whether the authentication was successful or not. + Task AuthenticateAsync(HttpRequestMessage requestMessage, CancellationToken ct = default); + + /// + /// Ensures the authentication of http request messages of the http client. + /// + /// The http client. + /// Cancellation token. + /// Indicates whether the authentication was successful or not. + Task AuthenticateAsync(HttpClient httpClient, CancellationToken ct = default); + } +} diff --git a/OData.Client.Manager/Authenticators/JwtAuthenticator.cs b/OData.Client.Manager/Authenticators/JwtAuthenticator.cs new file mode 100644 index 0000000..28d3a0a --- /dev/null +++ b/OData.Client.Manager/Authenticators/JwtAuthenticator.cs @@ -0,0 +1,21 @@ +using System; +using System.Net.Http.Headers; + +namespace OData.Client.Manager.Authenticators +{ + public class JwtAuthenticator : AuthenticatorBase + { + private const string Scheme = "Bearer"; + + public JwtAuthenticator(string token) + { + Header = new AuthenticationHeaderValue(Scheme, token ?? throw new ArgumentNullException(nameof(token))); + } + + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + private JwtAuthenticator() + { + throw new NotSupportedException(); + } + } +} diff --git a/OData.Client.Manager/Authenticators/OidcAuthenticator.cs b/OData.Client.Manager/Authenticators/OidcAuthenticator.cs new file mode 100644 index 0000000..bcfb96c --- /dev/null +++ b/OData.Client.Manager/Authenticators/OidcAuthenticator.cs @@ -0,0 +1,166 @@ +using IdentityModel; +using IdentityModel.Client; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace OData.Client.Manager.Authenticators +{ + public class OidcAuthenticator : AuthenticatorBase + { + private readonly OidcSettings oidcSettings; + private readonly HttpClient httpClient; + private readonly DiscoveryCache discoveryCache; + private TokenResponse? token; + private DateTime tokenExpiry = DateTime.Now; + + public OidcAuthenticator(OidcSettings oidcSettings) + { + this.oidcSettings = oidcSettings ?? throw new ArgumentNullException(nameof(oidcSettings)); + + httpClient = this.oidcSettings.HttpClient ?? new HttpClient(); + var discoveryPolicy = this.oidcSettings.DiscoveryPolicy ?? new DiscoveryPolicy(); + discoveryCache = new DiscoveryCache(this.oidcSettings.AuthUri?.ToString(), () => httpClient, discoveryPolicy); + } + + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + private OidcAuthenticator() + { + throw new NotSupportedException(); + } + + /// + public override Task AuthenticateAsync(HttpRequestMessage requestMessage, CancellationToken ct = default) + { + if (requestMessage == null) + { + throw new ArgumentNullException(nameof(requestMessage)); + } + + if (!ReplaceAuthorizationHeader && requestMessage.Headers.Authorization != null) + { + OnTrace?.Invoke($"Request header '{nameof(requestMessage.Headers.Authorization)}' already set"); + return Task.FromResult(false); + } + + return GetAndSetTokenAsync(requestMessage, ct); + } + + /// + public override Task AuthenticateAsync(HttpClient httpClient, CancellationToken ct = default) + { + if (httpClient == null) + { + throw new ArgumentNullException(nameof(httpClient)); + } + + if (!ReplaceAuthorizationHeader && httpClient.DefaultRequestHeaders.Authorization != null) + { + OnTrace?.Invoke($"Request header '{nameof(httpClient.DefaultRequestHeaders.Authorization)}' already set"); + return Task.FromResult(false); + } + + return GetAndSetTokenAsync(httpClient, ct); + } + + private async Task GetAndSetTokenAsync(HttpRequestMessage requestMessage, CancellationToken ct = default) + { + var localToken = await GetTokenAsync(ct).ConfigureAwait(false); + if (localToken != null) + { + requestMessage.SetBearerToken(localToken.AccessToken); + Header = requestMessage.Headers.Authorization; + return true; + } + + OnTrace?.Invoke($"{nameof(localToken)} response could not be set."); + return false; + } + + private async Task GetAndSetTokenAsync(HttpClient httpClient, CancellationToken ct = default) + { + var localToken = await GetTokenAsync(ct).ConfigureAwait(false); + if (localToken != null) + { + httpClient.SetBearerToken(localToken.AccessToken); + Header = httpClient.DefaultRequestHeaders.Authorization; + return true; + } + + OnTrace?.Invoke($"{nameof(localToken)} response could not be set."); + return false; + } + + public async Task GetTokenAsync(CancellationToken ct = default) + { + if (token == null || DateTime.Now >= tokenExpiry) + { + var discovery = await discoveryCache.GetAsync().ConfigureAwait(false); + if (discovery.IsError) + { + OnTrace?.Invoke($"{nameof(discovery)} response has errors: {discovery.Error}"); + return token; + } + + if (token == null) + { + token = oidcSettings.GrantType switch + { + OidcConstants.GrantTypes.Password => await httpClient.RequestPasswordTokenAsync( + new PasswordTokenRequest + { + Address = discovery.TokenEndpoint, + ClientId = oidcSettings.ClientId, + ClientSecret = oidcSettings.ClientSecret, + Scope = oidcSettings.Scope, + UserName = oidcSettings.Username, + Password = oidcSettings.Password + }, ct).ConfigureAwait(false), + OidcConstants.GrantTypes.ClientCredentials => await httpClient.RequestClientCredentialsTokenAsync( + new ClientCredentialsTokenRequest + { + Address = discovery.TokenEndpoint, + ClientId = oidcSettings.ClientId, + ClientSecret = oidcSettings.ClientSecret, + Scope = oidcSettings.Scope + }, ct).ConfigureAwait(false), + OidcConstants.GrantTypes.AuthorizationCode => await httpClient.RequestAuthorizationCodeTokenAsync( + new AuthorizationCodeTokenRequest + { + Address = discovery.TokenEndpoint, + ClientId = oidcSettings.ClientId, + ClientSecret = oidcSettings.ClientSecret, + Code = oidcSettings.Code, + RedirectUri = oidcSettings.RedirectUri?.ToString(), + CodeVerifier = oidcSettings.CodeVerifier + }, ct).ConfigureAwait(false), + _ => throw new NotSupportedException($"Grant type '{oidcSettings.GrantType}' is not supported") + }; + } + else + { + using var request = new RefreshTokenRequest + { + Address = discovery.TokenEndpoint, + ClientId = oidcSettings.ClientId, + ClientSecret = oidcSettings.ClientSecret, + Scope = oidcSettings.Scope, + RefreshToken = token.RefreshToken + }; + token = await httpClient.RequestRefreshTokenAsync(request, ct).ConfigureAwait(false); + } + + if (token.IsError) + { + OnTrace?.Invoke($"{nameof(token)} response has errors: {token.Error}"); + return token; + } + + tokenExpiry = DateTime.Now.AddSeconds(token.ExpiresIn - 10); + } + + return token; + } + } +} \ No newline at end of file diff --git a/OData.Client.Manager/Authenticators/OidcSettings.cs b/OData.Client.Manager/Authenticators/OidcSettings.cs new file mode 100644 index 0000000..c4388b6 --- /dev/null +++ b/OData.Client.Manager/Authenticators/OidcSettings.cs @@ -0,0 +1,75 @@ +using IdentityModel; +using IdentityModel.Client; +using System; +using System.Net.Http; + +namespace OData.Client.Manager.Authenticators +{ + public class OidcSettings + { + public OidcSettings(Uri? authUri = null) + { + AuthUri = authUri; + } + + /// + /// Gets or sets the authentication endpoint uri. + /// + public Uri? AuthUri { get; set; } + + /// + /// Gets or sets the discovery policy. + /// + public DiscoveryPolicy? DiscoveryPolicy { get; set; } + + /// + /// Gets or sets the grant type (default: Password). + /// + public string GrantType { get; set; } = OidcConstants.GrantTypes.Password; + + /// + /// Gets or sets the client identifier. + /// + public string? ClientId { get; set; } + + /// + /// Gets or sets the client secret. + /// + public string? ClientSecret { get; set; } + + /// + /// Gets or sets the username. + /// + public string? Username { get; set; } + + /// + /// Gets or sets the password (when using grant type: password). + /// + public string? Password { get; set; } + + /// + /// Gets or sets the scope. + /// + public string? Scope { get; set; } + + /// + /// Gets or sets the redirect uri (when using grant type: authorization_code). + /// + public Uri? RedirectUri { get; set; } + + /// + /// Gets or sets the authorization code (when using grant type: authorization_code). + /// + public string? Code { get; set; } + + /// + /// Gets or sets the optional PKCE parameter (when using grant type: authorization_code). + /// + public string? CodeVerifier { get; set; } + + /// + /// Gets or sets an optional http client. + /// + public HttpClient? HttpClient { get; set; } + } +} diff --git a/OData.Client.Manager/Extensions/UriExtensions.cs b/OData.Client.Manager/Extensions/UriExtensions.cs new file mode 100644 index 0000000..efc1e06 --- /dev/null +++ b/OData.Client.Manager/Extensions/UriExtensions.cs @@ -0,0 +1,33 @@ +using System; + +namespace OData.Client.Manager.Extensions +{ + internal static class UriExtensions + { + public static Uri AddParameter(this Uri uri, string? name, string? value) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(value)) + { + return uri; + } + + var builder = new UriBuilder(uri); + var queryString = name + '=' + value; + if (builder.Query == null || builder.Query.Length <= 1) + { + builder.Query = queryString; + } + else if (!builder.Query.Contains(name)) + { + builder.Query = builder.Query.Substring(1) + '&' + queryString; + } + + return builder.Uri; + } + } +} diff --git a/OData.Client.Manager/IODataManager.cs b/OData.Client.Manager/IODataManager.cs new file mode 100644 index 0000000..708e391 --- /dev/null +++ b/OData.Client.Manager/IODataManager.cs @@ -0,0 +1,12 @@ +using Simple.OData.Client; + +namespace OData.Client.Manager +{ + public interface IODataManager + { + /// + /// Gets the OData client implementation. + /// + IODataClient Client { get; } + } +} diff --git a/OData.Client.Manager/OData.Client.Manager.csproj b/OData.Client.Manager/OData.Client.Manager.csproj new file mode 100644 index 0000000..af429d5 --- /dev/null +++ b/OData.Client.Manager/OData.Client.Manager.csproj @@ -0,0 +1,52 @@ + + + + {da19bdb3-a27a-4de6-91fd-4d22e801a18c} + true + latest + enable + netstandard2.0 + 2.0.0 + 2.0.0.0 + + + + OData.Client.Manager + Tobias Sibera + Sibera Industries + OData client manager library which uses the IODataClient implementation of Simple.OData.Client to communicate with OData APIs and handles OIDC authentication as well as request versioning requirements on top. + icon.png + git + https://github.com/SiberaIndustries/OData.Client.Manager + https://github.com/SiberaIndustries/OData.Client.Manager/releases + https://github.com/SiberaIndustries/OData.Client.Manager + MIT + OData;netstandard;client;api;rest;versioning;authentication + + true + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + + + + IOperation + true + ..\code-analysis.ruleset + + + + + + + + + + + + + + + + + + diff --git a/OData.Client.Manager/ODataManager.cs b/OData.Client.Manager/ODataManager.cs new file mode 100644 index 0000000..3c28b68 --- /dev/null +++ b/OData.Client.Manager/ODataManager.cs @@ -0,0 +1,67 @@ +using Simple.OData.Client; +using System; + +namespace OData.Client.Manager +{ + public class ODataManager : IODataManager + { + public ODataManager(ODataManagerConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + var settings = configuration as ODataClientSettings; + + var authenticator = configuration.Authenticator; + if (authenticator != null) + { + var authenticatorName = authenticator.GetType().Name; + authenticator.OnTrace += (msg) => settings.OnTrace?.Invoke("{0}: {1}", new[] { authenticatorName, msg }); + } + + var versioningManager = configuration.VersioningManager; + if (versioningManager != null) + { + var versioningManagerName = versioningManager.GetType().Name; + versioningManager.OnTrace += (msg) => settings.OnTrace?.Invoke("{0}: {1}", new[] { versioningManagerName, msg }); + } + + var beforeRequestTemp = settings.BeforeRequestAsync; + settings.BeforeRequest = async (requestMessage) => + { + if (authenticator != null && !await authenticator.AuthenticateAsync(requestMessage).ConfigureAwait(false)) + { + settings.OnTrace?.Invoke("{0}: Authentication not successful", new[] { nameof(ODataManager) }); + } + + if (versioningManager != null && !versioningManager.ApplyVersion(requestMessage)) + { + settings.OnTrace?.Invoke("{0}: Applying version not successful", new[] { nameof(ODataManager) }); + } + + if (beforeRequestTemp != null) + { + await beforeRequestTemp.Invoke(requestMessage).ConfigureAwait(false); + } + }; + + Client = new ODataClient(settings); + } + + public ODataManager(Uri apiEndpoint) + : this(new ODataManagerConfiguration(apiEndpoint)) + { + } + + [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + private ODataManager() + { + throw new NotSupportedException(); + } + + /// + public IODataClient Client { get; private set; } + } +} diff --git a/OData.Client.Manager/ODataManagerConfiguration.cs b/OData.Client.Manager/ODataManagerConfiguration.cs new file mode 100644 index 0000000..9dff5a9 --- /dev/null +++ b/OData.Client.Manager/ODataManagerConfiguration.cs @@ -0,0 +1,40 @@ +using OData.Client.Manager.Authenticators; +using OData.Client.Manager.Versioning; +using Simple.OData.Client; +using System; +using System.Net; +using System.Net.Http; + +namespace OData.Client.Manager +{ + public class ODataManagerConfiguration : ODataClientSettings + { + public ODataManagerConfiguration(HttpClient httpClient, Uri? baseUri = null, ICredentials? credentials = null) + : base(httpClient, baseUri, credentials) + { + UseAbsoluteReferenceUris = true; + } + + public ODataManagerConfiguration(Uri baseUri, ICredentials? credentials = null) + : base(baseUri, credentials) + { + UseAbsoluteReferenceUris = true; + } + + public ODataManagerConfiguration() + : base() + { + UseAbsoluteReferenceUris = true; + } + + /// + /// Gets or sets the api version manager for the base uri. + /// + public IVersioningManager? VersioningManager { get; set; } + + /// + /// Gets or sets the authenticator. + /// + public IAuthenticator? Authenticator { get; set; } + } +} diff --git a/OData.Client.Manager/Properties/AssemblyInfo.cs b/OData.Client.Manager/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c3838bd --- /dev/null +++ b/OData.Client.Manager/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("OData.Client.Manager.Tests")] diff --git a/OData.Client.Manager/Versioning/IVersioningManager.cs b/OData.Client.Manager/Versioning/IVersioningManager.cs new file mode 100644 index 0000000..38faee6 --- /dev/null +++ b/OData.Client.Manager/Versioning/IVersioningManager.cs @@ -0,0 +1,20 @@ +using System; +using System.Net.Http; + +namespace OData.Client.Manager.Versioning +{ + public interface IVersioningManager + { + /// + /// Gets or sets the action delegate that will be executed to write trace messages. + /// + Action? OnTrace { get; set; } + + /// + /// Applies the api version to the given http request message. + /// + /// The http request message. + /// Indicates whether the version was successfully applied or not. + bool ApplyVersion(HttpRequestMessage requestMessage); + } +} diff --git a/OData.Client.Manager/Versioning/MimeTypeVersioningManager.cs b/OData.Client.Manager/Versioning/MimeTypeVersioningManager.cs new file mode 100644 index 0000000..3401e71 --- /dev/null +++ b/OData.Client.Manager/Versioning/MimeTypeVersioningManager.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; + +namespace OData.Client.Manager.Versioning +{ + public class MimeTypeVersioningManager : IVersioningManager + { + private readonly string header; + private readonly string value; + + public MimeTypeVersioningManager(string version, string mimeType = "v", string header = "accept") + { + this.header = string.IsNullOrWhiteSpace(header) + ? throw new ArgumentNullException(nameof(header)) + : header.Trim(); + + var trimmedMimeType = string.IsNullOrWhiteSpace(mimeType) + ? throw new ArgumentNullException(nameof(mimeType)) + : mimeType.Trim(); + + value = string.IsNullOrWhiteSpace(version) + ? trimmedMimeType + : trimmedMimeType + '=' + version.Trim(); + } + + public Action? OnTrace { get; set; } + + public bool ApplyVersion(HttpRequestMessage requestMessage) + { + if (requestMessage == null) + { + throw new ArgumentNullException(nameof(requestMessage)); + } + + if (requestMessage.Headers.TryGetValues(header, out IEnumerable values) && values.Contains(value)) + { + OnTrace?.Invoke($"The already existing mime type and value {value} gets not applied again"); + return true; + } + + requestMessage.Headers.TryAddWithoutValidation(header, value); + return true; + } + } +} diff --git a/OData.Client.Manager/Versioning/QueryParamVersioningManager.cs b/OData.Client.Manager/Versioning/QueryParamVersioningManager.cs new file mode 100644 index 0000000..548f8e2 --- /dev/null +++ b/OData.Client.Manager/Versioning/QueryParamVersioningManager.cs @@ -0,0 +1,38 @@ +using OData.Client.Manager.Extensions; +using System; +using System.Net.Http; + +namespace OData.Client.Manager.Versioning +{ + public class QueryParamVersioningManager : IVersioningManager + { + private readonly string parameterName; + private readonly string version; + + public QueryParamVersioningManager(string version, string parameterName = "api-version") + { + this.parameterName = string.IsNullOrWhiteSpace(parameterName) + ? throw new ArgumentNullException(nameof(parameterName)) + : parameterName; + this.version = version; + } + + public Action? OnTrace { get; set; } + + public bool ApplyVersion(HttpRequestMessage requestMessage) + { + if (requestMessage == null) + { + throw new ArgumentNullException(nameof(requestMessage)); + } + + if (requestMessage.RequestUri.Query.Contains(parameterName)) + { + OnTrace?.Invoke($"The already existing query parameter {parameterName} gets overridden"); + } + + requestMessage.RequestUri = requestMessage.RequestUri.AddParameter(parameterName, version); + return true; + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf325ca --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# ODataClientManager + +[![NuGet](https://img.shields.io/nuget/v/OData.Client.Manager.svg)](https://www.nuget.org/packages/OData.Client.Manager) +[![Build status](https://ci.appveyor.com/api/projects/status/6bx528e35dt43783/branch/master?svg=true)](https://ci.appveyor.com/project/SiberaIndustries/odata-client-manager/branch/master) +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=SiberaIndustries_OData.Client.Manager&metric=alert_status)](https://sonarcloud.io/dashboard?id=SiberaIndustries_OData.Client.Manager) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=SiberaIndustries_OData.Client.Manager&metric=coverage)](https://sonarcloud.io/dashboard?id=SiberaIndustries_OData.Client.Manager) +[![CodeFactor](https://www.codefactor.io/repository/github/siberaindustries/odata.client.manager/badge)](https://www.codefactor.io/repository/github/siberaindustries/odata.client.manager) + +## Purpose + +This repository provides a C# based OData client manager library. +The Manager uses the `IODataClient` implementation of `Simple.OData.Client` to communicate with OData APIs and is able handle authorization and versioning requirements on top. + +## Getting started + +The easiest way to start using the `ODataManager` is to install it’s Nuget package: + +```sh +Install-Package OData.Client.Manager +``` + +In the source file where you will be using the `ODataManager` import the namespace: + +```cs +using OData.Client.Manager; +``` + +### Quickstart + +The following code snipped shows an example of how to use the `IODataManger` implementation. + +```cs +// Create the manager +var odataEndpoint = new Uri("http://localhost:12345/api"); +var manager = new ODataManager(odataEndpoint); + +// Use the client of the manager (example of the typed fluent API syntax) +IEnumerable entities = await manager.Client + .For() + .FindEntriesAsync(); + +// Use the client of the manager (example of the dynamic fluent API syntax) +var dx = ODataDynamic.Expression; +IEnumerable entities = await manager.Client + .For(dx.Products) + .FindEntriesAsync(); +``` + +For more information about how to use the Odata client, please read the [documentation of Simple.OData.Client](https://github.com/simple-odata-client/Simple.OData.Client/wiki). + +### Make use of autenticated and versioned requests + +* To make use of authentication, just use one of the existing authenticators in the `OData.Client.Manager.Authenticators` namespace or create your own by implementing the `IAuthenticator` interface. +* To make use of authentication, just use one of the existing managers in the `OData.Client.Manager.Versioning` namespace or create your own by implementing the `IVersioningManager` interface. + +```cs +// Setup the configuration +var config = new ODataManagerConfiguration(new Uri("http://localhost:12345/api")) +{ + // Authenticated requests + Authenticator = new OidcAuthenticator(new OidcSettings + { + AuthUri = new Uri("http://localhost:5000"), + ClientId = "ClientAppX", + ClientSecret = "Secret", + Username = "User", + Password = "Password", + Scope = "api1", + + GrantType = "Password", // Default + DiscoveryPolicy = new DiscoveryPolicy { RequireHttps = false }, + }), + + // Versioned requests + VersioningManager = new QueryParamVersioningManager("1.2", "api-version") +}; + +// Use the configuration in the ctor of the manager +var manager = new ODataManager(config); +``` + +## FAQ + +1. Why I get the error `Https required`? + * OIDC endponts must provide an encrypted connection (https) by default (except URIs of localhost). To disable this requirement, make use of the `OidcSettings` and set `RequireHttps` of the `DiscoveryPolicy` property to `false`: `settings.DiscoveryPolicy = new DiscoveryPolicy { RequireHttps = requireHttps };`. + +## Links + +* OData: or +* Simple.OData.Client: +* IdentityModel: +* OIDC: + +## Open Source License Acknowledgements and Third-Party Copyrights + +* Icon made by [Freepik](https://www.flaticon.com/authors/freepik) diff --git a/TestAuthorizationServer/Program.cs b/TestAuthorizationServer/Program.cs new file mode 100644 index 0000000..d4dabfd --- /dev/null +++ b/TestAuthorizationServer/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace TestAuthorizationServer +{ + public static class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/TestAuthorizationServer/Startup.cs b/TestAuthorizationServer/Startup.cs new file mode 100644 index 0000000..0c2aa1b --- /dev/null +++ b/TestAuthorizationServer/Startup.cs @@ -0,0 +1,69 @@ +using IdentityModel; +using IdentityServer4.Models; +using IdentityServer4.Test; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Security.Claims; + +namespace TestAuthorizationServer +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services + .AddIdentityServer() + .AddDeveloperSigningCredential() + .AddInMemoryClients(GetClients()) + .AddInMemoryIdentityResources(GetIdentityResources()) + .AddInMemoryApiResources(GetApiResources()) + .AddTestUsers(GetTestUsers()); + } + + public void Configure(IApplicationBuilder app) + { + app.UseIdentityServer(); + } + + private static IEnumerable GetApiResources() => new List + { + new ApiResource("api1", "Api 1"), + new ApiResource("api2", "Api 1"), + }; + + public static IEnumerable GetIdentityResources() => new List + { + new IdentityResources.OpenId(), + new IdentityResources.Profile(), + }; + + public static IEnumerable GetClients() => new List + { + new Client + { + ClientId = "odata-manager", + ClientSecrets = { new Secret("secret".Sha512()) }, + RedirectUris = { "http://localhost:5000" }, + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + AllowedScopes = { "api1" } + }, + new Client + { + ClientId = "odata-manager-2", + ClientSecrets = { new Secret("secret".Sha512()) }, + RedirectUris = { "http://localhost:5000" }, + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + AllowedScopes = { "api1" }, + AllowOfflineAccess = true, + AccessTokenLifetime = 1 + } + }; + + public static List GetTestUsers() => new List + { + new TestUser { Username = "bob", Password = "bob", SubjectId = "bob", Claims = { new Claim(JwtClaimTypes.Email, "bob@mail.com") } }, + new TestUser { Username = "alice", Password = "alice", SubjectId = "alice", Claims = { new Claim(JwtClaimTypes.Email, "alice@mail.com") } }, + }; + } +} diff --git a/TestAuthorizationServer/TestAuthorizationServer.csproj b/TestAuthorizationServer/TestAuthorizationServer.csproj new file mode 100644 index 0000000..0589b78 --- /dev/null +++ b/TestAuthorizationServer/TestAuthorizationServer.csproj @@ -0,0 +1,14 @@ + + + + latest + enable + netcoreapp3.0 + + + + + + + + diff --git a/TestAuthorizationServer/appsettings.Development.json b/TestAuthorizationServer/appsettings.Development.json new file mode 100644 index 0000000..e203e94 --- /dev/null +++ b/TestAuthorizationServer/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/TestAuthorizationServer/appsettings.json b/TestAuthorizationServer/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/TestAuthorizationServer/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/TestAuthorizationServer/tempkey.rsa b/TestAuthorizationServer/tempkey.rsa new file mode 100644 index 0000000..755aeea --- /dev/null +++ b/TestAuthorizationServer/tempkey.rsa @@ -0,0 +1 @@ +{"KeyId":"IqF-mmxYEJZVtZ81E8l8LQ","Parameters":{"D":"cNifHmEXQnF9cVCrZ/RCOilI99f+5X1QIRNqoaWwRYq3dY6z8lDKiAPxHUX3g42P73gAbG4G2vd+suYPiaRkNRalbSmB0tPTN9fJ3rA0YN9w1556qJPsUXsWSFtIpaYWuyFrLL+NJBsFyX6FoWoySNh72itWIMmSbrW0+FAyZiN0e0gFmdoXRGlKSVoa2lzXb1M+oFbJ5QjITPO9lNofcJpzPgLNXyocXiyZTUW+MrAWBGcWZUtVph876UAzOGVLtaF2c+E+wz/mvEgkYdr+AUlz5u/37VI7K3EKynHPHBS1As6gqn3vwsPPVdH6bqAEj7fyDVVi80A1itG4e6CQ9Q==","DP":"1Uf/0hhHS7O/2vFDjycedbzzF5/5UXqUtSeNXKIdAs0fQZOFyi9A/apN1VJUK9asE/in97qZEU808ZV5onCMWlysi0es7T//CNk5LzMmUOimmMykl4VBiK4VT+GovDkS+zLo0qrBEdCXMiI1RAVrrj+SBRWKOEKjkJnkVW0On+s=","DQ":"Se6TDFYTyCMeyCoACdrt5nwbiwxX/K3sP84aPSpwvuSSz1a2zoH/DEdLwbMlAVd3H9/4cm/CGEcYdoZnNlG6zwBsKw5YPA/FAUnXuwoMQT90OwF70VWaDJUjITk2gdwMUeDpTKluIQSONfvlGh0XiUDcFAj+Sb9OngZcSSj5Uec=","Exponent":"AQAB","InverseQ":"rUi4eOVV8TUh4ymada88v8ZnwdXHKargRpEjzpw76IgQSqc7Hx242YlCcEHKPrlS+heZ3AYQaIGEFOeC6XKkuoXF2S7urLLK0CPN0zAI30aAtk/6CPnlUCoL9wccJW/RdKV24TjStXDuNn+tnh8qR8GfWe852bmyFPkRKYH4Hnk=","Modulus":"1D4nYuWos57fykQjxNdHJHGp44ulf4Fw232Erx8GaHqj9+etqg/y5jGwAIWwRM21DigoLPN/cBuT183EAPCbeDjpihNfm01IE8U1aFUv19OFLwHgH2ANNIhieQN/QtqorWLWyvu7Dg5AhgmltQRegKJAmRoPtVqljitbzHaY/iHy4g5lcYQxG2l8cAJ1Ybs+3Md18hfOAiplxJ9IHaSLpb9auq8NgF6AG2w5WFSRTIMvaijgNQuaa4iY9nW1x2bGoblhcym7LP+o5008L8uLPjASqNLHsxVATggr4nEC35RZfETCA0TA6GmcXeN82TV4lr9JRNlq7unFzmfWveP+SQ==","P":"3HPuIVel/sK6hVUBGInx5XRxHjFmJsi/P7vKvNE7VdWjbiB/5l+6Bm7YMUIzkW5yTBQGo6bWmD2Oq2c3ajM3Hr3t63MnyRwAX/cX5E3JBF5UvNMOQ+km3Gx48LKXPuJEtj7x/fiw5RSTSL/9kgEOf1lU4j7gtlrR0+RKRZKGxNM=","Q":"9ndSEErM0C6NfO4zl2TGNrFO6l5y/3BaZTY2FX7ErQ3JVRe9tS+W98me17VkmfqCwjLeQ+ZGgHBPxJrUFguoCGBM0rqmApQTBqOl5ilSnZa5IInPTus/dW9fpscNGeHKjoDG8egaE3ASDjivnXpWXdeE0sMtpt86cnnxOg457vM="}} \ No newline at end of file diff --git a/code-analysis.ruleset b/code-analysis.ruleset new file mode 100644 index 0000000..e1f05fe --- /dev/null +++ b/code-analysis.ruleset @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..17dae66977426fefe67f96cff0a35d9cdf925a73 GIT binary patch literal 1638 zcmV-s2ATPZP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00002 zVoOIv0063uBQgL0010qNS#tmY4#WTe4#WYKD-Ig~000McNliru;|dfN2oC&1-EjZ_ z1vE)SK~!koy_tJ#6h$1zKX>%7D$io14QNWSfFKVO(bx(p7-$r$rXr#Zh!HhvNC+e< zS|5RuisGZ8X@Dr9#-Irl6{A(b1eG)qYpteM8=((NurDkKeO`O#AKlyT%+BuZ?p^uK zAKjbz&hPu3-I?ESX6nP~CNJLC&+7O=)P-b#`ky}itbXrZg z6R{DGVIA8jrQ76qjS|1igbPv0Clr$6O~AJnldA@w%O1zQ2^h3+_nKvor!0E>>xJI0 z$3od^E&Euvc(4S^mY`)zpJVUiNM<~z?cR;wvL)5BCCw4}a$31S9VaGUb_?K~*pYJ|az0U&__%E^{pP9=-ALWiO9ry|cM z@}miNn)xIF0TwYuLp{!a0w&0D)@b{v0XC^dwm}&SW(t_=s8FB^w9%ClribUqr8#N} zD>4g6k~9yMe)h3~8KkkCVnwtJ)2~g+5|>$Ys#!vjOBX|BKLG1p_Uhm&nSL^7JPBxE z5-2~7UD5MQBn|{Pq)i~VmP#ppzH!wj%rA0Po~O}AN;ZBzQq%{j;waS&m|DJ;1+0uo zz*68&sV+pZo}jO1zp2kWQGm}fzC|y)0C-=jeMTDM7Ulf8eHbu`QoW&{BlKGm4)UEL zi5F}8hI#>W$n@QP98>uE!U%~-Xkjss#+!3DupftjUndr9dnL4oY? z+Y0EVRruYcz?@UmT}`s0_69{=8BuJzu&Q$u2)|;LtIrCr3W)P;Aq&4RngZtP=jJ+^ z$>tII=%R>59Z|y^Q?GXB(IV8OQO#Ik>?@;kn=D1&j2LIb0uFMFQ>OWyE)1hyYG3P| zHbKu^_vqaRO!NF2x+IEj_VNHrSVgI67d1eVQ~En-rI#1T?;-RNi*nRmOk*2X(d zU@5da7|TH*)6u9KA`wuTEtCSpgUbo>*}xuY16v5dOM2iA(x@P>B@Z9DG|@?}(--xk z!9gDn>-EXPF0eyeln>EOm>%xK&r)^>H*a4OJ?IJM<5><1m(?4E_DRBB$%i~mA~1*N z4SStm1%yk>m0n<{>85OV literal 0 HcmV?d00001 diff --git a/stylecop.json b/stylecop.json new file mode 100644 index 0000000..3f70105 --- /dev/null +++ b/stylecop.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", + "settings": { + "documentationRules": { + "companyName": "Microsoft", + "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} in the project root for license information.", + "documentInterfaces": true, + "documentExposedElements": true, + "documentInternalElements": false, + "documentPrivateElements": false, + "documentPrivateFields": false, + "variables": { + "licenseName": "MIT", + "licenseFile": "LICENSE" + }, + "xmlHeader": false + }, + "layoutRules": { + "newlineAtEndOfFile": "omit" + }, + "orderingRules": { + "blankLinesBetweenUsingGroups": "omit" + } + } +}