mirror of
https://github.com/AlexMacocian/MTSC.git
synced 2026-07-15 14:59:33 +00:00
Setup OAuth2 support (#20)
* Setup OAuth2 support * Setup tests project * Setup OAuth2 unit tests in CI pipeline * Setup authorizeattribute tests * Test * Setup tests for AuthorizeAttribute * Setup OAuth2 support Unit Tests coverage * Add OAuth2 project to release
This commit is contained in:
@@ -20,6 +20,7 @@ jobs:
|
||||
Solution_Path: MTSC.sln
|
||||
Test_Project_Path: MTSC.UnitTests\MTSC.UnitTests.csproj
|
||||
Source_Project_Path: MTSC\MTSC.csproj
|
||||
Oauth_Project_Path: MTSC.OAuth2\MTSC.OAuth2.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
@@ -44,9 +45,12 @@ jobs:
|
||||
- name: Build MTSC project
|
||||
run: dotnet build MTSC -c $env:Configuration
|
||||
|
||||
- name: Package
|
||||
- name: Package MTSC
|
||||
run: dotnet pack -c Release -o . $env:Source_Project_Path
|
||||
|
||||
- name: Package MTSC.OAuth2
|
||||
run: dotnet pack -c Release -o . $env:Oauth_Project_Path
|
||||
|
||||
- name: Publish
|
||||
run: dotnet nuget push *.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||
|
||||
@@ -22,6 +22,8 @@ jobs:
|
||||
Solution_Path: MTSC.sln
|
||||
Test_Project_Path: MTSC.UnitTests\MTSC.UnitTests.csproj
|
||||
Source_Project_Path: MTSC\MTSC.csproj
|
||||
OAuth2Source_Project_Path: MTSC.OAuth2\MTSC.OAuth2.csproj
|
||||
OAuth2Test_Project_Path: MTSC.OAuth2.Tests\MTSC.OAuth2.Tests.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
@@ -33,7 +35,7 @@ jobs:
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: '6.x.x'
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v1.0.1
|
||||
@@ -41,6 +43,9 @@ jobs:
|
||||
- name: Execute Unit Tests
|
||||
run: dotnet test $env:Test_Project_Path -v n --filter "TestCategory!=ServerTests"
|
||||
|
||||
- name: Execute OAuth2 Tests
|
||||
run: dotnet test $env:OAuth2Test_Project_Path -v n
|
||||
|
||||
- name: Restore Project
|
||||
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace MTSC.OAuth2.Tests;
|
||||
|
||||
public static class CertificateUtilities
|
||||
{
|
||||
public static X509Certificate2 CreateNewSelfSignedCertificate()
|
||||
{
|
||||
using var parent = RSA.Create(4096);
|
||||
using var rsa = RSA.Create(2048);
|
||||
var parentReq = new CertificateRequest(
|
||||
"CN=Experimental Issuing Authority",
|
||||
parent,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
|
||||
parentReq.CertificateExtensions.Add(
|
||||
new X509BasicConstraintsExtension(true, false, 0, true));
|
||||
|
||||
parentReq.CertificateExtensions.Add(
|
||||
new X509SubjectKeyIdentifierExtension(parentReq.PublicKey, false));
|
||||
|
||||
return parentReq.CreateSelfSigned(
|
||||
DateTimeOffset.UtcNow.AddDays(-45),
|
||||
DateTimeOffset.UtcNow.AddDays(365));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
|
||||
<PackageReference Include="Moq" Version="4.18.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.10" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.10" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.20.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MTSC.OAuth2\MTSC.OAuth2.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Tests.Models
|
||||
{
|
||||
public sealed class HttpClientMock : HttpClient
|
||||
{
|
||||
public Func<HttpRequestMessage, Task<HttpResponseMessage>> SendingAsync { get;set; }
|
||||
public Func<HttpRequestMessage, HttpResponseMessage> Sending { get; set; }
|
||||
|
||||
public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.SendingAsync is not null)
|
||||
{
|
||||
return await this.SendingAsync(request);
|
||||
}
|
||||
|
||||
return new HttpResponseMessage();
|
||||
}
|
||||
|
||||
public override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.Sending is not null)
|
||||
{
|
||||
return this.Sending(request);
|
||||
}
|
||||
|
||||
return new HttpResponseMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Http;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Tests.Models
|
||||
{
|
||||
public sealed class ScopedHttpClientMock<T> : IHttpClient<T>
|
||||
{
|
||||
private readonly HttpClient httpClient = new();
|
||||
|
||||
public Uri BaseAddress { get; set; }
|
||||
public HttpRequestHeaders DefaultRequestHeaders { get; }
|
||||
public long MaxResponseContentBufferSize { get; set; }
|
||||
public TimeSpan Timeout { get; set; }
|
||||
|
||||
public event EventHandler<HttpClientEventMessage> EventEmitted;
|
||||
|
||||
public void CancelPendingRequests()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> DeleteAsync(string requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> DeleteAsync(string requestUri, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> DeleteAsync(Uri requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> DeleteAsync(Uri requestUri, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(string requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(string requestUri, HttpCompletionOption completionOption)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(string requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(string requestUri, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(Uri requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(Uri requestUri, HttpCompletionOption completionOption)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(Uri requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetAsync(Uri requestUri, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<byte[]> GetByteArrayAsync(string requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<byte[]> GetByteArrayAsync(Uri requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<Stream> GetStreamAsync(string requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<Stream> GetStreamAsync(Uri requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetStringAsync(string requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> GetStringAsync(Uri requestUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PostAsync(string requestUri, HttpContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PostAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PutAsync(string requestUri, HttpContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PutAsync(Uri requestUri, HttpContent content)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> PutAsync(Uri requestUri, HttpContent content, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
using MTSC.Common.Http;
|
||||
using MTSC.OAuth2.Attributes;
|
||||
using MTSC.OAuth2.Authorization;
|
||||
using MTSC.OAuth2.Models;
|
||||
using MTSC.ServerSide;
|
||||
using Slim;
|
||||
using Slim.Exceptions;
|
||||
using System;
|
||||
using System.Extensions;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Tests.UnitTests
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class AuthorizeAttributeTests
|
||||
{
|
||||
private const string AccessTokenObject = "AccessTokenObject";
|
||||
private const string AuthorizationCodeValue = "someValue";
|
||||
private const string AuthorizationCodeKey = "code";
|
||||
private const string StateValue = "someState";
|
||||
private const string StateKey = "state";
|
||||
private const string MaxAgeAttribute = "Max-Age";
|
||||
private const string RedirectUri = "RedirectUri";
|
||||
private const string OAuthUri = "OAuthUri";
|
||||
private const string AccessTokenKey = "JsonWebTokenString";
|
||||
private const string AccessTokenValue = "SomeValueHere";
|
||||
|
||||
private readonly Mock<ILogger<AuthorizeAttribute>> loggerMock = new();
|
||||
private readonly Mock<IAuthorizationProvider> authorizationProviderMock = new();
|
||||
private readonly AuthorizeAttribute authorizeAttribute;
|
||||
|
||||
public AuthorizeAttributeTests()
|
||||
{
|
||||
this.authorizeAttribute = new AuthorizeAttribute(this.authorizationProviderMock.Object, this.loggerMock.Object);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ParameterlessConstructor_DoesNotInject()
|
||||
{
|
||||
var container = new ServiceManager();
|
||||
container.RegisterSingleton<AuthorizeAttribute>();
|
||||
|
||||
var action = () => container.GetService<AuthorizeAttribute>();
|
||||
|
||||
action.Should().Throw<DependencyInjectionException>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructors_PreferresConstructorWithILogger()
|
||||
{
|
||||
var calledExpectedConstructor = false;
|
||||
var container = new ServiceManager();
|
||||
container.RegisterSingleton<AuthorizeAttribute>();
|
||||
container.RegisterSingleton(sp => this.authorizationProviderMock.Object);
|
||||
container.RegisterSingleton(sp =>
|
||||
{
|
||||
calledExpectedConstructor = true;
|
||||
return this.loggerMock.Object;
|
||||
});
|
||||
container.RegisterSingleton(sp => new Server());
|
||||
|
||||
_ = container.GetService<AuthorizeAttribute>();
|
||||
|
||||
calledExpectedConstructor.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor1_AuthorizationProviderIsNull_ThrowsArgumentNullException()
|
||||
{
|
||||
var action = () => new AuthorizeAttribute(null, this.loggerMock.Object);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor1_ILoggerIsNull_ThrowsArgumentNullException()
|
||||
{
|
||||
var action = () => new AuthorizeAttribute(this.authorizationProviderMock.Object, null as ILogger<AuthorizeAttribute>);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor2_AuthorizationProviderIsNull_ThrowsArgumentNullException()
|
||||
{
|
||||
var action = () => new AuthorizeAttribute(null, new Server());
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor2_ServerIsNull_ThrowsArgumentNullException()
|
||||
{
|
||||
var action = () => new AuthorizeAttribute(this.authorizationProviderMock.Object, null as Server);
|
||||
|
||||
action.Should().Throw<ArgumentNullException>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieExists_VerifiesAccessToken()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
httpRequest.Cookies.Add(new Cookie(AccessTokenKey, AccessTokenValue));
|
||||
this.authorizationProviderMock.Setup(u => u.VerifyAccessToken(AccessTokenValue))
|
||||
.ReturnsAsync(new TokenValidationResponse { IsValid = true });
|
||||
|
||||
await this.authorizeAttribute.HandleRequestAsync(routeContext);
|
||||
|
||||
this.authorizationProviderMock.Verify();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieExists_AcceptsRequest()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
httpRequest.Cookies.Add(new Cookie(AccessTokenKey, AccessTokenValue));
|
||||
this.authorizationProviderMock.Setup(u => u.VerifyAccessToken(AccessTokenValue))
|
||||
.ReturnsAsync(new TokenValidationResponse { IsValid = true });
|
||||
|
||||
var response = await this.authorizeAttribute.HandleRequestAsync(routeContext);
|
||||
|
||||
response.Should().Be(RouteEnablerAsyncResponse.Accept);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieExists_SetsAccessTokenInResources()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
httpRequest.Cookies.Add(new Cookie(AccessTokenKey, AccessTokenValue));
|
||||
this.authorizationProviderMock.Setup(u => u.VerifyAccessToken(AccessTokenValue))
|
||||
.ReturnsAsync(new TokenValidationResponse { IsValid = true });
|
||||
|
||||
_ = await this.authorizeAttribute.HandleRequestAsync(routeContext);
|
||||
|
||||
routeContext.Resources[AccessTokenKey].Should().Be(AccessTokenValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieExists_ReturnsError()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
httpRequest.Cookies.Add(new Cookie(AccessTokenKey, AccessTokenValue));
|
||||
this.authorizationProviderMock.Setup(u => u.VerifyAccessToken(AccessTokenValue))
|
||||
.ReturnsAsync(new TokenValidationResponse { IsValid = false });
|
||||
|
||||
var response = await this.authorizeAttribute.HandleRequestAsync(routeContext);
|
||||
|
||||
response.Should().BeOfType<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieExists_Redirects()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
httpRequest.Cookies.Add(new Cookie(AccessTokenKey, AccessTokenValue));
|
||||
this.authorizationProviderMock.Setup(u => u.VerifyAccessToken(AccessTokenValue))
|
||||
.ReturnsAsync(new TokenValidationResponse { IsValid = false });
|
||||
this.authorizationProviderMock.Setup(u => u.GetRedirectUri())
|
||||
.ReturnsAsync(RedirectUri);
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.Headers.ContainsHeader(HttpMessage.ResponseHeaders.Location).Should().BeTrue();
|
||||
response.Response.Headers[HttpMessage.ResponseHeaders.Location].Should().Be(RedirectUri);
|
||||
response.Response.StatusCode.Should().Be(HttpMessage.StatusCodes.TemporaryRedirect);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieExists_ExpiresCookie()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
httpRequest.Cookies.Add(new Cookie(AccessTokenKey, AccessTokenValue));
|
||||
this.authorizationProviderMock.Setup(u => u.VerifyAccessToken(AccessTokenValue))
|
||||
.ReturnsAsync(new TokenValidationResponse { IsValid = false });
|
||||
this.authorizationProviderMock.Setup(u => u.GetRedirectUri())
|
||||
.ReturnsAsync(RedirectUri);
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.Cookies.First(c => c.Key == AccessTokenKey).Attributes.First(a => a.Key == MaxAgeAttribute).Value.Should().Be("-1");
|
||||
response.Response.Cookies.First(c => c.Key == AccessTokenKey).Value.Should().Be(AccessTokenValue);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_StateKeyMissing_RedirectsToOAuth()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.Headers.ContainsHeader(HttpMessage.ResponseHeaders.Location).Should().BeTrue();
|
||||
response.Response.Headers[HttpMessage.ResponseHeaders.Location].Should().Be(OAuthUri);
|
||||
response.Response.StatusCode.Should().Be(HttpMessage.StatusCodes.TemporaryRedirect);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_StateKeyMissing_SetsStateCookie()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.Cookies.First(c => c.Key == StateKey).Value.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_RequestQueryMissing_RedirectsToOAuth()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.Headers.ContainsHeader(HttpMessage.ResponseHeaders.Location).Should().BeTrue();
|
||||
response.Response.Headers[HttpMessage.ResponseHeaders.Location].Should().Be(OAuthUri);
|
||||
response.Response.StatusCode.Should().Be(HttpMessage.StatusCodes.TemporaryRedirect);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_RequestQueryMissing_SetsStateCookie()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.Cookies.First(c => c.Key == StateKey).Value.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_NoStateInQuery_ReturnsUnauthorized()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
httpRequest.RequestQuery = "someKey=someValue";
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.StatusCode.Should().Be(HttpMessage.StatusCodes.Unauthorized);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_StatesDoNotMatch_ReturnsUnauthorized()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
httpRequest.RequestQuery = $"{StateKey}=someValue";
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.StatusCode.Should().Be(HttpMessage.StatusCodes.Unauthorized);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_NoAuthCode_ReturnsUnauthorized()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
httpRequest.RequestQuery = $"{StateKey}={StateValue}";
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.StatusCode.Should().Be(HttpMessage.StatusCodes.Unauthorized);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_WithAuthCode_RetrievesAccessToken()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
this.authorizationProviderMock.Setup(u => u.RetrieveAccessToken(AuthorizationCodeValue))
|
||||
.ReturnsAsync(Optional.None<JsonWebToken>());
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
httpRequest.RequestQuery = $"{StateKey}={StateValue}&{AuthorizationCodeKey}={AuthorizationCodeValue}";
|
||||
|
||||
_ = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
this.authorizationProviderMock.Verify();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_WrongAuthCode_ReturnsUnauthorized()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
this.authorizationProviderMock.Setup(u => u.RetrieveAccessToken(AuthorizationCodeValue))
|
||||
.ReturnsAsync(Optional.None<JsonWebToken>());
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
httpRequest.RequestQuery = $"{StateKey}={StateValue}&{AuthorizationCodeKey}={AuthorizationCodeValue}";
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.StatusCode.Should().Be(HttpMessage.StatusCodes.Unauthorized);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_WithAuthCode_RedirectsToRedirectUri()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
var jsonWebToken = new JsonWebToken(new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken()));
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
this.authorizationProviderMock.Setup(u => u.GetRedirectUri())
|
||||
.ReturnsAsync(RedirectUri);
|
||||
this.authorizationProviderMock.Setup(u => u.RetrieveAccessToken(AuthorizationCodeValue))
|
||||
.ReturnsAsync(jsonWebToken);
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
httpRequest.RequestQuery = $"{StateKey}={StateValue}&{AuthorizationCodeKey}={AuthorizationCodeValue}";
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.StatusCode.Should().Be(HttpMessage.StatusCodes.TemporaryRedirect);
|
||||
response.Response.Headers.ContainsHeader(HttpMessage.ResponseHeaders.Location).Should().BeTrue();
|
||||
response.Response.Headers[HttpMessage.ResponseHeaders.Location].Should().Be(RedirectUri);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_WithAuthCode_SetsAccessTokenInCookie()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
var jsonWebToken = new JsonWebToken(new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken()));
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
this.authorizationProviderMock.Setup(u => u.GetRedirectUri())
|
||||
.ReturnsAsync(RedirectUri);
|
||||
this.authorizationProviderMock.Setup(u => u.RetrieveAccessToken(AuthorizationCodeValue))
|
||||
.ReturnsAsync(jsonWebToken);
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
httpRequest.RequestQuery = $"{StateKey}={StateValue}&{AuthorizationCodeKey}={AuthorizationCodeValue}";
|
||||
|
||||
var response = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
response.Response.Cookies.First(c => c.Key == AccessTokenKey).Value.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task HandleRequestAsync_AccessTokenCookieMissing_WithAuthCode_SetsRouteContextResources()
|
||||
{
|
||||
var httpRequest = new HttpRequest();
|
||||
var routeContext = new RouteContext(
|
||||
server: null,
|
||||
httpRequest,
|
||||
clientData: null,
|
||||
scopedServiceProvider: null,
|
||||
urlValues: null);
|
||||
var jsonWebToken = new JsonWebToken(new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken()));
|
||||
this.authorizationProviderMock.Setup(u => u.GetOAuthUri(It.IsAny<string>()))
|
||||
.ReturnsAsync(OAuthUri);
|
||||
this.authorizationProviderMock.Setup(u => u.GetRedirectUri())
|
||||
.ReturnsAsync(RedirectUri);
|
||||
this.authorizationProviderMock.Setup(u => u.RetrieveAccessToken(AuthorizationCodeValue))
|
||||
.ReturnsAsync(jsonWebToken);
|
||||
httpRequest.Cookies.Add(new Cookie(StateKey, StateValue));
|
||||
httpRequest.RequestQuery = $"{StateKey}={StateValue}&{AuthorizationCodeKey}={AuthorizationCodeValue}";
|
||||
|
||||
_ = (await this.authorizeAttribute.HandleRequestAsync(routeContext)).Cast<RouteEnablerAsyncResponse.RouteEnablerAsyncResponseError>();
|
||||
|
||||
routeContext.Resources.TryGetValue(AccessTokenKey, out var accessTokenString).Should().BeTrue();
|
||||
accessTokenString.Should().NotBeNull();
|
||||
routeContext.Resources.TryGetValue(AccessTokenObject, out var accessTokenObject).Should().BeTrue();
|
||||
accessTokenObject.Should().BeOfType<JsonWebToken>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MTSC.OAuth2.Authorization;
|
||||
using MTSC.OAuth2.Models;
|
||||
using MTSC.OAuth2.Tests.Models;
|
||||
using Slim;
|
||||
using System.Http;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Tests.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class ClientCertificateAuthorizationProviderTests
|
||||
{
|
||||
private const string AuthorizationCode = "AuthorizationCode";
|
||||
|
||||
private readonly ClientCertificateAuthorizationProvider provider;
|
||||
private readonly AuthorizationOptions authorizationOptions;
|
||||
private readonly ScopedHttpClientMock<ClientCertificateAuthorizationProvider> httpClientMock = new();
|
||||
private readonly HttpClientMock httpClientMock2 = new();
|
||||
|
||||
public ClientCertificateAuthorizationProviderTests()
|
||||
{
|
||||
this.authorizationOptions = new AuthorizationOptions
|
||||
{
|
||||
ClientCertificate = CertificateUtilities.CreateNewSelfSignedCertificate(),
|
||||
OpenIdConfigurationUri = string.Empty,
|
||||
OAuthUri = string.Empty,
|
||||
AuthenticationMode = AuthorizationOptions.Authentication.ClientCertificate,
|
||||
AuthTokenEndpoint = string.Empty,
|
||||
ClientId = string.Empty,
|
||||
ClientSecret = string.Empty,
|
||||
RedirectUri = string.Empty,
|
||||
Scopes = string.Empty
|
||||
};
|
||||
|
||||
this.provider = new ClientCertificateAuthorizationProvider(this.httpClientMock2, this.authorizationOptions);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_WithIHttpClient_AndOptions_Builds()
|
||||
{
|
||||
var container = new ServiceManager();
|
||||
container.RegisterSingleton<ClientCertificateAuthorizationProvider>();
|
||||
container.RegisterSingleton<IHttpClient<ClientCertificateAuthorizationProvider>>(sp => this.httpClientMock);
|
||||
container.RegisterSingleton(sp => this.authorizationOptions);
|
||||
|
||||
var provider = container.GetService<ClientCertificateAuthorizationProvider>();
|
||||
|
||||
provider.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_WithHttpClient_AndOptions_Builds()
|
||||
{
|
||||
var container = new ServiceManager();
|
||||
container.RegisterSingleton<ClientCertificateAuthorizationProvider>();
|
||||
container.RegisterSingleton<HttpClient>(sp => this.httpClientMock2);
|
||||
container.RegisterSingleton(sp => this.authorizationOptions);
|
||||
|
||||
var provider = container.GetService<ClientCertificateAuthorizationProvider>();
|
||||
|
||||
provider.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_WithOptions_Builds()
|
||||
{
|
||||
var container = new ServiceManager();
|
||||
container.RegisterSingleton<ClientCertificateAuthorizationProvider>();
|
||||
container.RegisterSingleton(sp => this.authorizationOptions);
|
||||
|
||||
var provider = container.GetService<ClientCertificateAuthorizationProvider>();
|
||||
|
||||
provider.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetrieveAccessToken_SetsClientCertificate()
|
||||
{
|
||||
this.httpClientMock2.SendingAsync = async (request) =>
|
||||
{
|
||||
request.Content.Should().BeOfType<FormUrlEncodedContent>();
|
||||
var contentString = await request.Content.As<FormUrlEncodedContent>().ReadAsStringAsync();
|
||||
var content = contentString.Split('&');
|
||||
var clientAssertion = content.Where(s => s.StartsWith("client_assertion=")).FirstOrDefault();
|
||||
clientAssertion.Should().NotBeNull();
|
||||
|
||||
return new HttpResponseMessage()
|
||||
{
|
||||
StatusCode = System.Net.HttpStatusCode.BadRequest
|
||||
};
|
||||
};
|
||||
|
||||
await this.provider.RetrieveAccessToken(AuthorizationCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using MTSC.OAuth2.Authorization;
|
||||
using MTSC.OAuth2.Models;
|
||||
using MTSC.OAuth2.Tests.Models;
|
||||
using Slim;
|
||||
using System.Http;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Tests.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class ClientSecretAuthorizationProviderTests
|
||||
{
|
||||
private const string AuthorizationCode = "AuthorizationCode";
|
||||
private const string ClientSecret = "clientSecret";
|
||||
|
||||
private readonly ClientSecretAuthorizationProvider provider;
|
||||
private readonly AuthorizationOptions authorizationOptions;
|
||||
private readonly ScopedHttpClientMock<ClientSecretAuthorizationProvider> httpClientMock = new();
|
||||
private readonly HttpClientMock httpClientMock2 = new();
|
||||
|
||||
public ClientSecretAuthorizationProviderTests()
|
||||
{
|
||||
this.authorizationOptions = new AuthorizationOptions
|
||||
{
|
||||
ClientCertificate = CertificateUtilities.CreateNewSelfSignedCertificate(),
|
||||
OpenIdConfigurationUri = string.Empty,
|
||||
OAuthUri = string.Empty,
|
||||
AuthenticationMode = AuthorizationOptions.Authentication.ClientSecret,
|
||||
AuthTokenEndpoint = string.Empty,
|
||||
ClientId = string.Empty,
|
||||
ClientSecret = ClientSecret,
|
||||
RedirectUri = string.Empty,
|
||||
Scopes = string.Empty
|
||||
};
|
||||
|
||||
this.provider = new ClientSecretAuthorizationProvider(this.authorizationOptions, this.httpClientMock2);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_WithIHttpClient_AndOptions_Builds()
|
||||
{
|
||||
var container = new ServiceManager();
|
||||
container.RegisterSingleton<ClientSecretAuthorizationProvider>();
|
||||
container.RegisterSingleton<IHttpClient<ClientSecretAuthorizationProvider>>(sp => this.httpClientMock);
|
||||
container.RegisterSingleton(sp => this.authorizationOptions);
|
||||
|
||||
var provider = container.GetService<ClientSecretAuthorizationProvider>();
|
||||
|
||||
provider.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_WithHttpClient_AndOptions_Builds()
|
||||
{
|
||||
var container = new ServiceManager();
|
||||
container.RegisterSingleton<ClientSecretAuthorizationProvider>();
|
||||
container.RegisterSingleton<HttpClient>(sp => this.httpClientMock2);
|
||||
container.RegisterSingleton(sp => this.authorizationOptions);
|
||||
|
||||
var provider = container.GetService<ClientSecretAuthorizationProvider>();
|
||||
|
||||
provider.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Constructor_WithOptions_Builds()
|
||||
{
|
||||
var container = new ServiceManager();
|
||||
container.RegisterSingleton<ClientSecretAuthorizationProvider>();
|
||||
container.RegisterSingleton(sp => this.authorizationOptions);
|
||||
|
||||
var provider = container.GetService<ClientSecretAuthorizationProvider>();
|
||||
|
||||
provider.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetrieveAccessToken_SetsClientSecret()
|
||||
{
|
||||
this.httpClientMock2.SendingAsync = async (request) =>
|
||||
{
|
||||
request.Content.Should().BeOfType<FormUrlEncodedContent>();
|
||||
var contentString = await request.Content.As<FormUrlEncodedContent>().ReadAsStringAsync();
|
||||
var content = contentString.Split('&');
|
||||
var clientSecret = content.Where(s => s.StartsWith("client_secret=")).FirstOrDefault();
|
||||
clientSecret.Should().NotBeNull();
|
||||
clientSecret?.Replace("client_secret=", "").Should().Be(ClientSecret);
|
||||
|
||||
return new HttpResponseMessage()
|
||||
{
|
||||
StatusCode = System.Net.HttpStatusCode.BadRequest
|
||||
};
|
||||
};
|
||||
|
||||
await this.provider.RetrieveAccessToken(AuthorizationCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MTSC.Common.Http;
|
||||
using MTSC.Common.Http.Attributes;
|
||||
using MTSC.OAuth2.Models;
|
||||
using System;
|
||||
using System.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using Slim.Attributes;
|
||||
using MTSC.ServerSide;
|
||||
using MTSC.OAuth2.Authorization;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
|
||||
namespace MTSC.OAuth2.Attributes
|
||||
{
|
||||
public sealed class AuthorizeAttribute : RouteFilterAttribute
|
||||
{
|
||||
private const string StateKey = "state";
|
||||
private const string AuthorizationCodeKey = "code";
|
||||
private const string MaxAgeAttribute = "Max-Age";
|
||||
private const string AccessTokenObject = "AccessTokenObject";
|
||||
|
||||
public const string AccessTokenKey = "JsonWebTokenString";
|
||||
public const string AccessTokenObjectKey = "JsonWebToken";
|
||||
|
||||
private static readonly RandomNumberGenerator RandomNumberGenerator = RandomNumberGenerator.Create();
|
||||
|
||||
private readonly ILogger<AuthorizeAttribute> logger;
|
||||
private readonly IAuthorizationProvider authorizationProvider;
|
||||
|
||||
[DoNotInject]
|
||||
///<summary>
|
||||
/// Public parameterless constructor to be used for decorating endpoints. Do not use to instantiate new <see cref="AuthorizeAttribute"/>.
|
||||
/// Use instead any of the other constructors.
|
||||
///</summary>
|
||||
public AuthorizeAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
[PreferredConstructor(Priority = 0)]
|
||||
public AuthorizeAttribute(
|
||||
IAuthorizationProvider authorizationProvider,
|
||||
ILogger<AuthorizeAttribute> logger)
|
||||
{
|
||||
this.authorizationProvider = authorizationProvider.ThrowIfNull(nameof(authorizationProvider));
|
||||
this.logger = logger.ThrowIfNull(nameof(logger));
|
||||
}
|
||||
|
||||
[PreferredConstructor(Priority = 1)]
|
||||
public AuthorizeAttribute(
|
||||
IAuthorizationProvider authorizationProvider,
|
||||
Server server)
|
||||
{
|
||||
this.authorizationProvider = authorizationProvider.ThrowIfNull(nameof(authorizationProvider));
|
||||
this.logger = new ServerDebugLogger<AuthorizeAttribute>(server.ThrowIfNull(nameof(server)));
|
||||
}
|
||||
|
||||
public override async Task<RouteEnablerAsyncResponse> HandleRequestAsync(RouteContext routeContext)
|
||||
{
|
||||
this.logger.LogInformation("Authorizing request");
|
||||
if (TryGetCookieValue(routeContext.HttpRequest, AccessTokenKey, out var accessToken))
|
||||
{
|
||||
this.logger.LogInformation("Found authorization token. Validating");
|
||||
var validationResponse = await this.authorizationProvider.VerifyAccessToken(accessToken);
|
||||
if (validationResponse.IsValid is false)
|
||||
{
|
||||
this.logger.LogInformation("Validation failed");
|
||||
return await this.ExpireAccessTokenAndRedirect(accessToken);
|
||||
}
|
||||
|
||||
this.logger.LogInformation("Validation succeeded");
|
||||
routeContext.Resources[AccessTokenKey] = accessToken;
|
||||
routeContext.Resources[AccessTokenObjectKey] = validationResponse.JsonWebToken;
|
||||
return RouteEnablerAsyncResponse.Accept;
|
||||
}
|
||||
|
||||
if (TryGetCookieValue(routeContext.HttpRequest, StateKey, out var state) &&
|
||||
TryParseRequestQuery(routeContext.HttpRequest.RequestQuery, out var queryDictionary))
|
||||
{
|
||||
this.logger.LogInformation("Received redirect from OAuth server. Checking state value");
|
||||
if (queryDictionary.TryGetValue(StateKey, out var redirectState) is false)
|
||||
{
|
||||
this.logger.LogInformation("No state value found in query. Rejecting request");
|
||||
return RouteEnablerAsyncResponse.Error(Unauthorized401);
|
||||
}
|
||||
|
||||
if (state != redirectState)
|
||||
{
|
||||
this.logger.LogInformation("State values do not match. Rejecting request");
|
||||
return RouteEnablerAsyncResponse.Error(Unauthorized401);
|
||||
}
|
||||
|
||||
if (queryDictionary.TryGetValue(AuthorizationCodeKey, out var authCode) is false)
|
||||
{
|
||||
this.logger.LogInformation("No authorization code found in the query. Rejecting request");
|
||||
return RouteEnablerAsyncResponse.Error(Unauthorized401);
|
||||
}
|
||||
|
||||
this.logger.LogInformation("Requesting access token");
|
||||
var maybeAccessToken = await this.authorizationProvider.RetrieveAccessToken(authCode);
|
||||
var retrievedAccessToken = maybeAccessToken.ExtractValue();
|
||||
if (retrievedAccessToken is null)
|
||||
{
|
||||
this.logger.LogInformation("Failed to retrieve access token. Rejecting request");
|
||||
return RouteEnablerAsyncResponse.Error(Unauthorized401);
|
||||
}
|
||||
|
||||
routeContext.Resources[AccessTokenKey] = retrievedAccessToken.EncodedToken;
|
||||
routeContext.Resources[AccessTokenObject] = retrievedAccessToken;
|
||||
return await this.RedirectToUri(retrievedAccessToken);
|
||||
}
|
||||
|
||||
return await this.RedirectWithStateToOAuth();
|
||||
}
|
||||
public override void HandleResponse(RouteContext routeContext)
|
||||
{
|
||||
SetAccessTokenCookieInContext(routeContext);
|
||||
}
|
||||
|
||||
private async Task<RouteEnablerAsyncResponse> RedirectWithStateToOAuth()
|
||||
{
|
||||
var state = GetRandomState();
|
||||
this.logger.LogDebug($"Redirecting client to OAuth server with state {state}");
|
||||
return RouteEnablerAsyncResponse.Error(await this.OAuthRedirect307(state));
|
||||
}
|
||||
private async Task<RouteEnablerAsyncResponse> RedirectToUri(JsonWebToken accessTokenObject)
|
||||
{
|
||||
this.logger.LogDebug($"Redirecting to redirect uri");
|
||||
return RouteEnablerAsyncResponse.Error(await this.RedirectToUri307(accessTokenObject));
|
||||
}
|
||||
private async Task<RouteEnablerAsyncResponse> ExpireAccessTokenAndRedirect(string accessToken)
|
||||
{
|
||||
this.logger.LogDebug($"Expiring access token and redirecting");
|
||||
return RouteEnablerAsyncResponse.Error(await this.ExpireAccessAndRedirect307(accessToken));
|
||||
}
|
||||
private async Task<HttpResponse> OAuthRedirect307(string state)
|
||||
{
|
||||
var response = new HttpResponse
|
||||
{
|
||||
StatusCode = HttpMessage.StatusCodes.TemporaryRedirect
|
||||
};
|
||||
|
||||
var oAuthUri = await this.authorizationProvider.GetOAuthUri(state);
|
||||
response.Headers.AddHeader(HttpMessage.ResponseHeaders.Location, oAuthUri);
|
||||
response.Cookies.Add(new Cookie(StateKey, state));
|
||||
return response;
|
||||
}
|
||||
private async Task<HttpResponse> RedirectToUri307(JsonWebToken accessTokenObject)
|
||||
{
|
||||
var response = new HttpResponse
|
||||
{
|
||||
StatusCode = HttpMessage.StatusCodes.TemporaryRedirect
|
||||
};
|
||||
|
||||
response.Headers.AddHeader(HttpMessage.ResponseHeaders.Location, await this.authorizationProvider.GetRedirectUri());
|
||||
SetAccessTokenCookieInHttpResponse(response, accessTokenObject);
|
||||
return response;
|
||||
}
|
||||
private async Task<HttpResponse> ExpireAccessAndRedirect307(string accessToken)
|
||||
{
|
||||
var response = new HttpResponse
|
||||
{
|
||||
StatusCode = HttpMessage.StatusCodes.TemporaryRedirect
|
||||
};
|
||||
|
||||
response.Headers.AddHeader(HttpMessage.ResponseHeaders.Location, await this.authorizationProvider.GetRedirectUri());
|
||||
ResetAccessTokenCookieInHttpResponse(response, accessToken);
|
||||
return response;
|
||||
}
|
||||
|
||||
private static void SetAccessTokenCookieInContext(RouteContext routeContext)
|
||||
{
|
||||
if (routeContext.Resources.TryGetValue(AccessTokenObject, out var accessToken) is false ||
|
||||
accessToken is not JsonWebToken accessTokenObject)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetAccessTokenCookieInHttpResponse(routeContext.HttpResponse, accessTokenObject);
|
||||
}
|
||||
private static void SetAccessTokenCookieInHttpResponse(HttpResponse response, JsonWebToken jsonWebToken)
|
||||
{
|
||||
var cookie = new Cookie(AccessTokenKey, jsonWebToken.EncodedToken);
|
||||
cookie.Attributes.Add(MaxAgeAttribute, (jsonWebToken.ValidTo - DateTime.Now).TotalSeconds.ToString());
|
||||
response?.Cookies?.Add(cookie);
|
||||
}
|
||||
private static void ResetAccessTokenCookieInHttpResponse(HttpResponse response, string accessToken)
|
||||
{
|
||||
var cookie = new Cookie(AccessTokenKey, accessToken);
|
||||
cookie.Attributes.Add(MaxAgeAttribute, "-1");
|
||||
response?.Cookies?.Add(cookie);
|
||||
}
|
||||
private static bool TryParseRequestQuery(string query, out Dictionary<string, string> queryValues)
|
||||
{
|
||||
queryValues = null;
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
queryValues = query.Split('&')
|
||||
.ToDictionary(c => c.Split('=')[0].ToLowerInvariant(), c => Uri.UnescapeDataString(c.Split('=')[1]));
|
||||
return true;
|
||||
}
|
||||
catch(Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
private static bool TryGetCookieValue(HttpRequest httpRequest, string cookieKey, out string cookieValue)
|
||||
{
|
||||
var cookie = httpRequest.Cookies.FirstOrDefault(c => c.Key == cookieKey);
|
||||
cookieValue = cookie?.Value;
|
||||
if (string.IsNullOrWhiteSpace(cookieValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
private static string GetRandomState()
|
||||
{
|
||||
var bytes = new byte[64];
|
||||
RandomNumberGenerator.GetNonZeroBytes(bytes);
|
||||
return Convert.ToBase64String(bytes)
|
||||
.Replace("=", "")
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
}
|
||||
private static HttpResponse Unauthorized401 => new()
|
||||
{
|
||||
StatusCode = HttpMessage.StatusCodes.Unauthorized,
|
||||
BodyString = "Authorization failed"
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MTSC.OAuth2.Models;
|
||||
using Slim.Attributes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Extensions;
|
||||
using System.Http;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace MTSC.OAuth2.Authorization
|
||||
{
|
||||
internal sealed class ClientCertificateAuthorizationProvider : FormAuthorizationProvider<ClientCertificateAuthorizationProvider>
|
||||
{
|
||||
private readonly AuthorizationOptions authorizationOptions;
|
||||
|
||||
[PreferredConstructor(Priority = 0)]
|
||||
public ClientCertificateAuthorizationProvider(
|
||||
IHttpClient<ClientCertificateAuthorizationProvider> httpClient,
|
||||
AuthorizationOptions authorizationOptions)
|
||||
: base(httpClient, authorizationOptions)
|
||||
{
|
||||
this.authorizationOptions = authorizationOptions.ThrowIfNull(nameof(authorizationOptions));
|
||||
this.authorizationOptions.ClientCertificate.ThrowIfNull(nameof(this.authorizationOptions.ClientCertificate));
|
||||
}
|
||||
|
||||
[PreferredConstructor(Priority = 1)]
|
||||
public ClientCertificateAuthorizationProvider(
|
||||
HttpClient httpClient,
|
||||
AuthorizationOptions authorizationOptions)
|
||||
: base(httpClient, authorizationOptions)
|
||||
{
|
||||
this.authorizationOptions = authorizationOptions.ThrowIfNull(nameof(authorizationOptions));
|
||||
this.authorizationOptions.ClientCertificate.ThrowIfNull(nameof(this.authorizationOptions.ClientCertificate));
|
||||
}
|
||||
|
||||
[PreferredConstructor(Priority = 2)]
|
||||
public ClientCertificateAuthorizationProvider(
|
||||
AuthorizationOptions authorizationOptions)
|
||||
: base(new HttpClient(), authorizationOptions)
|
||||
{
|
||||
this.authorizationOptions = authorizationOptions.ThrowIfNull(nameof(authorizationOptions));
|
||||
this.authorizationOptions.ClientCertificate.ThrowIfNull(nameof(this.authorizationOptions.ClientCertificate));
|
||||
}
|
||||
|
||||
protected override IEnumerable<KeyValuePair<string, string>> GetAdditionalFormFields()
|
||||
{
|
||||
var claims = this.GetClaims();
|
||||
var securityTokenDescriptor = new SecurityTokenDescriptor
|
||||
{
|
||||
Claims = claims,
|
||||
SigningCredentials = new X509SigningCredentials(this.authorizationOptions.ClientCertificate)
|
||||
};
|
||||
|
||||
var handler = new JsonWebTokenHandler();
|
||||
var signedClientAssertion = handler.CreateToken(securityTokenDescriptor);
|
||||
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{ "client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" },
|
||||
{ "client_assertion", signedClientAssertion }
|
||||
};
|
||||
}
|
||||
|
||||
private Dictionary<string, object> GetClaims()
|
||||
{
|
||||
return new Dictionary<string, object>()
|
||||
{
|
||||
{ "aud", $"{this.authorizationOptions.OAuthUri}/{this.authorizationOptions.AuthTokenEndpoint}" },
|
||||
{ "exp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 30 },
|
||||
{ "iss", this.authorizationOptions.ClientId },
|
||||
{ "jti", Guid.NewGuid().ToString() },
|
||||
{ "nbf", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
|
||||
{ "sub", this.authorizationOptions.ClientId }
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using MTSC.OAuth2.Models;
|
||||
using Slim.Attributes;
|
||||
using System.Collections.Generic;
|
||||
using System.Http;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace MTSC.OAuth2.Authorization
|
||||
{
|
||||
internal sealed class ClientSecretAuthorizationProvider : FormAuthorizationProvider<ClientSecretAuthorizationProvider>
|
||||
{
|
||||
[PreferredConstructor(Priority = 0)]
|
||||
public ClientSecretAuthorizationProvider(
|
||||
AuthorizationOptions options,
|
||||
IHttpClient<ClientSecretAuthorizationProvider> httpClient)
|
||||
: base(httpClient, options)
|
||||
{
|
||||
}
|
||||
|
||||
[PreferredConstructor(Priority = 1)]
|
||||
public ClientSecretAuthorizationProvider(
|
||||
AuthorizationOptions options,
|
||||
HttpClient httpClient)
|
||||
: base(httpClient, options)
|
||||
{
|
||||
}
|
||||
|
||||
[PreferredConstructor(Priority = 2)]
|
||||
public ClientSecretAuthorizationProvider(
|
||||
AuthorizationOptions options)
|
||||
: base(new HttpClient(), options)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<KeyValuePair<string, string>> GetAdditionalFormFields()
|
||||
{
|
||||
return new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("client_secret", this.Options.ClientSecret)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MTSC.OAuth2.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Extensions;
|
||||
using System.Http;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Authorization
|
||||
{
|
||||
internal abstract class FormAuthorizationProvider<T> : IAuthorizationProvider
|
||||
{
|
||||
private const string RedirectUri = "{SERVERPLACEHOLDER}/authorize?response_type=code&client_id={CLIENTID}&redirect_uri={REDIRECTURI}&scope={SCOPE}&state={STATE}";
|
||||
private const string PostAuthCodeUri = "{SERVERPLACEHOLDER}/{TOKEN}";
|
||||
private const string ClientIdPlaceholder = "{CLIENTID}";
|
||||
private const string RedirectUriPlaceholder = "{REDIRECTURI}";
|
||||
private const string ScopePlaceholder = "{SCOPE}";
|
||||
private const string StatePlaceholder = "{STATE}";
|
||||
private const string ServerPlaceholder = "{SERVERPLACEHOLDER}";
|
||||
private const string TokenPlaceholder = "{TOKEN}";
|
||||
|
||||
private readonly AccessTokenValidator<T> accessTokenValidator;
|
||||
|
||||
protected AuthorizationHttpClientWrapper<T> AuthorizationClientWrapper { get; set; }
|
||||
protected AuthorizationOptions Options { get; set; }
|
||||
|
||||
public FormAuthorizationProvider(
|
||||
IHttpClient<T> httpClient,
|
||||
AuthorizationOptions options)
|
||||
{
|
||||
this.AuthorizationClientWrapper = new AuthorizationHttpClientWrapper<T>(httpClient);
|
||||
this.Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
this.accessTokenValidator = new AccessTokenValidator<T>(this.AuthorizationClientWrapper, this.Options.ClientId, this.Options.OpenIdConfigurationUri);
|
||||
}
|
||||
|
||||
public FormAuthorizationProvider(
|
||||
HttpClient httpClient,
|
||||
AuthorizationOptions options)
|
||||
{
|
||||
this.AuthorizationClientWrapper = new AuthorizationHttpClientWrapper<T>(httpClient);
|
||||
this.Options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
this.accessTokenValidator = new AccessTokenValidator<T>(this.AuthorizationClientWrapper, this.Options.ClientId, this.Options.OpenIdConfigurationUri);
|
||||
}
|
||||
|
||||
public async Task<TokenValidationResponse> VerifyAccessToken(string accessToken)
|
||||
{
|
||||
var validationResult = await this.accessTokenValidator.ValidateAccessToken(accessToken);
|
||||
return validationResult.Switch<TokenValidationResponse>(
|
||||
onSuccess: result => result,
|
||||
onFailure: exception =>
|
||||
{
|
||||
return new TokenValidationResponse { IsValid = false };
|
||||
});
|
||||
}
|
||||
|
||||
public Task<string> GetOAuthUri(string state)
|
||||
{
|
||||
return Task.FromResult(RedirectUri
|
||||
.Replace(ServerPlaceholder, this.Options.OAuthUri)
|
||||
.Replace(RedirectUriPlaceholder, this.Options.RedirectUri)
|
||||
.Replace(ClientIdPlaceholder, this.Options.ClientId)
|
||||
.Replace(ScopePlaceholder, this.Options.Scopes)
|
||||
.Replace(StatePlaceholder, state));
|
||||
}
|
||||
|
||||
public async Task<Optional<JsonWebToken>> RetrieveAccessToken(
|
||||
string authorizationCode)
|
||||
{
|
||||
using var formContent = new FormUrlEncodedContent(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "grant_type", "authorization_code" },
|
||||
{ "code", authorizationCode },
|
||||
{ "redirect_uri", this.Options.RedirectUri },
|
||||
{ "client_id", this.Options.ClientId },
|
||||
}.AddRange(GetAdditionalFormFields()));
|
||||
|
||||
var response = await this.AuthorizationClientWrapper.PostAsync(
|
||||
PostAuthCodeUri
|
||||
.Replace(ServerPlaceholder, this.Options.OAuthUri)
|
||||
.Replace(TokenPlaceholder, this.Options.AuthTokenEndpoint), formContent);
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
return Optional.None<JsonWebToken>();
|
||||
}
|
||||
|
||||
var accessToken = JsonConvert.DeserializeObject<AccessTokenResponse>(await response.Content.ReadAsStringAsync());
|
||||
var jsonWebTokenHandler = new JsonWebTokenHandler();
|
||||
var valid = await jsonWebTokenHandler.ValidateTokenAsync(accessToken.AccessToken, new TokenValidationParameters());
|
||||
var token = jsonWebTokenHandler.ReadJsonWebToken(accessToken.AccessToken);
|
||||
return token;
|
||||
}
|
||||
|
||||
public Task<string> GetRedirectUri()
|
||||
{
|
||||
return Task.FromResult(this.Options.RedirectUri);
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<KeyValuePair<string, string>> GetAdditionalFormFields() { return Array.Empty<KeyValuePair<string, string>>(); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using MTSC.OAuth2.Models;
|
||||
using System.Extensions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Authorization
|
||||
{
|
||||
public interface IAuthorizationProvider
|
||||
{
|
||||
Task<Optional<JsonWebToken>> RetrieveAccessToken(string authorizationCode);
|
||||
Task<TokenValidationResponse> VerifyAccessToken(string accessToken);
|
||||
Task<string> GetOAuthUri(string state);
|
||||
Task<string> GetRedirectUri();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using MTSC.OAuth2.Authorization;
|
||||
using MTSC.OAuth2.Models;
|
||||
using MTSC.ServerSide;
|
||||
using System;
|
||||
using System.Extensions;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using static MTSC.OAuth2.Models.AuthorizationOptions;
|
||||
|
||||
namespace MTSC.OAuth2.Builders
|
||||
{
|
||||
public sealed class MicrosoftGraphAuthorizationBuilder
|
||||
{
|
||||
private const string AuthTokenEndpoint = "token";
|
||||
private const string TenantPlaceholder = "[TENANT]";
|
||||
private const string OAuthUri = "https://login.microsoftonline.com/[TENANT]/oauth2/v2.0";
|
||||
private const string OpenIdConfigurationUri = "https://login.microsoftonline.com/[TENANT]/.well-known/openid-configuration";
|
||||
|
||||
private readonly Server chainedServer;
|
||||
private string Tenant { get; set; }
|
||||
private string ClientId { get; set; }
|
||||
private string Scopes { get; set; }
|
||||
private Authentication AuthenticationMode { get; set; }
|
||||
private string ClientSecret { get; set; }
|
||||
private string ClientCertificateThumbprint { get; set; }
|
||||
private X509Certificate2 ClientCertificate { get; set; }
|
||||
private string RedirectUri { get; set; }
|
||||
|
||||
internal MicrosoftGraphAuthorizationBuilder(Server chainedServer)
|
||||
{
|
||||
this.chainedServer = chainedServer.ThrowIfNull(nameof(chainedServer));
|
||||
}
|
||||
|
||||
public MicrosoftGraphAuthorizationBuilder WithTenant(string tenant)
|
||||
{
|
||||
this.Tenant = tenant.ThrowIfNull(nameof(tenant));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MicrosoftGraphAuthorizationBuilder WithClientId(string clientId)
|
||||
{
|
||||
this.ClientId = clientId.ThrowIfNull(nameof(clientId));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MicrosoftGraphAuthorizationBuilder WithScopes(string scopes)
|
||||
{
|
||||
this.Scopes = scopes.ThrowIfNull(nameof(scopes));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MicrosoftGraphAuthorizationBuilder WithRedirectUri(string redirectUri)
|
||||
{
|
||||
this.RedirectUri = redirectUri.ThrowIfNull(nameof(redirectUri));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MicrosoftGraphAuthorizationBuilder WithClientSecret(string clientSecret)
|
||||
{
|
||||
this.ClientSecret = clientSecret.ThrowIfNull(nameof(clientSecret));
|
||||
this.AuthenticationMode = Authentication.ClientSecret;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MicrosoftGraphAuthorizationBuilder WithClientCertificateThumbprint(string clientCertificateThumbprint)
|
||||
{
|
||||
this.ClientCertificateThumbprint = clientCertificateThumbprint.ThrowIfNull(nameof(clientCertificateThumbprint));
|
||||
this.AuthenticationMode = Authentication.ClientCertificate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MicrosoftGraphAuthorizationBuilder WithClientCertificate(X509Certificate2 x509Certificate2)
|
||||
{
|
||||
this.ClientCertificate = x509Certificate2.ThrowIfNull(nameof(x509Certificate2));
|
||||
if (this.ClientCertificate.HasPrivateKey is false)
|
||||
{
|
||||
throw new InvalidOperationException("Client certificate must have a private key");
|
||||
}
|
||||
|
||||
this.AuthenticationMode = Authentication.ClientCertificate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Server Build()
|
||||
{
|
||||
this.Tenant.ThrowIfNull(nameof(this.Tenant));
|
||||
this.ClientId.ThrowIfNull(nameof(this.Tenant));
|
||||
this.Scopes.ThrowIfNull(nameof(this.Tenant));
|
||||
this.RedirectUri.ThrowIfNull(nameof(this.RedirectUri));
|
||||
var oauthUri = OAuthUri.Replace(TenantPlaceholder, this.Tenant);
|
||||
var openIdConfigurationUri = OpenIdConfigurationUri.Replace(TenantPlaceholder, this.Tenant);
|
||||
var options = new AuthorizationOptions
|
||||
{
|
||||
ClientId = this.ClientId,
|
||||
Scopes = this.Scopes,
|
||||
OpenIdConfigurationUri = openIdConfigurationUri,
|
||||
OAuthUri = oauthUri,
|
||||
RedirectUri = this.RedirectUri,
|
||||
AuthTokenEndpoint = AuthTokenEndpoint
|
||||
};
|
||||
|
||||
if (this.AuthenticationMode == Authentication.ClientSecret &&
|
||||
this.ClientSecret.ThrowIfNull(nameof(this.ClientSecret)) is not null)
|
||||
{
|
||||
options.ClientSecret = this.ClientSecret;
|
||||
options.AuthenticationMode = Authentication.ClientSecret;
|
||||
this.chainedServer.ServiceManager.RegisterScoped<IAuthorizationProvider, ClientSecretAuthorizationProvider>();
|
||||
}
|
||||
else if (this.ClientCertificate is not null)
|
||||
{
|
||||
options.ClientCertificate = this.ClientCertificate;
|
||||
options.AuthenticationMode = Authentication.ClientCertificate;
|
||||
this.chainedServer.ServiceManager.RegisterScoped<IAuthorizationProvider, ClientCertificateAuthorizationProvider>();
|
||||
}
|
||||
else if (this.ClientCertificateThumbprint.ThrowIfNull(nameof(this.ClientCertificateThumbprint)) is not null)
|
||||
{
|
||||
options.ClientCertificate = GetClientCertificate(this.ClientCertificateThumbprint);
|
||||
options.AuthenticationMode = Authentication.ClientCertificate;
|
||||
this.chainedServer.ServiceManager.RegisterScoped<IAuthorizationProvider, ClientCertificateAuthorizationProvider>();
|
||||
}
|
||||
|
||||
this.chainedServer.ServiceManager.RegisterSingleton(sp => options);
|
||||
return this.chainedServer;
|
||||
}
|
||||
|
||||
private static X509Certificate2 GetClientCertificate(string thumbprint)
|
||||
{
|
||||
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
|
||||
store.Open(OpenFlags.ReadOnly);
|
||||
var results = store.Certificates.Find(X509FindType.FindByThumbprint, thumbprint, true);
|
||||
return results.Count > 0 ?
|
||||
results[0] :
|
||||
null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace MTSC.OAuth2.Exceptions
|
||||
{
|
||||
public sealed class TokenValidationException : Exception
|
||||
{
|
||||
private const string ErrorMessage = "Failed to validate access token. See inner exception for details";
|
||||
|
||||
public TokenValidationException(Exception innerException) : base(ErrorMessage, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using MTSC.OAuth2.Builders;
|
||||
using MTSC.ServerSide;
|
||||
|
||||
namespace MTSC.OAuth2.Extensions
|
||||
{
|
||||
public static class ServerExtensions
|
||||
{
|
||||
public static MicrosoftGraphAuthorizationBuilder WithMicrosoftGraphAuthorization(this Server server)
|
||||
{
|
||||
return new MicrosoftGraphAuthorizationBuilder(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("MTSC.OAuth2.Tests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<Version>1.0.0</Version>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Authors>Alexandru-Victor Macocian</Authors>
|
||||
<Product>MTSC.OAuth2</Product>
|
||||
<PackageProjectUrl>https://github.com/AlexMacocian/MTSC</PackageProjectUrl>
|
||||
<PackageIconUrl>https://github.com/AlexMacocian/MTSC/blob/master/docs/MTSC_Logo.png</PackageIconUrl>
|
||||
<RepositoryUrl>https://github.com/AlexMacocian/MTSC</RepositoryUrl>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\LICENSE" Link="LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="6.17.0" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.17.0" />
|
||||
<PackageReference Include="MTSC" Version="5.4.3" />
|
||||
<PackageReference Include="SystemExtensions.NetStandard" Version="1.4.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
public sealed class AccessTokenResponse
|
||||
{
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
[JsonProperty("expires_in")]
|
||||
public int ExpiresIn { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using MTSC.OAuth2.Exceptions;
|
||||
using System;
|
||||
using System.Extensions;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
internal sealed class AccessTokenValidator<T>
|
||||
{
|
||||
private readonly OpenIdConfigurationCache openIdConfigurationCache;
|
||||
private readonly string clientId;
|
||||
private readonly AuthorizationHttpClientWrapper<T> authorizationHttpClientWrapper;
|
||||
|
||||
public AccessTokenValidator(
|
||||
AuthorizationHttpClientWrapper<T> authorizationHttpClientWrapper,
|
||||
string clientId,
|
||||
string openIdConfigurationUri)
|
||||
{
|
||||
this.authorizationHttpClientWrapper = authorizationHttpClientWrapper;
|
||||
this.clientId = clientId;
|
||||
this.openIdConfigurationCache = OpenIdConfigurationCache.GetForConfigurationUri(openIdConfigurationUri);
|
||||
}
|
||||
|
||||
public async Task<Result<TokenValidationResponse, TokenValidationException>> ValidateAccessToken(string accessToken)
|
||||
{
|
||||
var openIdConfiguration = await this.openIdConfigurationCache.GetOpenIdConfiguration(this.authorizationHttpClientWrapper);
|
||||
|
||||
try
|
||||
{
|
||||
(var validationResult, var jsonWebToken) = await this.ValidateTokenInternal(accessToken, openIdConfiguration);
|
||||
if (validationResult.Exception is Exception validationException)
|
||||
{
|
||||
return new TokenValidationException(validationException);
|
||||
}
|
||||
|
||||
return new TokenValidationResponse { JsonWebToken = jsonWebToken, IsValid = validationResult.IsValid };
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return new TokenValidationException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(TokenValidationResult, JsonWebToken)> ValidateTokenInternal(string accessToken, OpenIdConfiguration openIdConfiguration)
|
||||
{
|
||||
var jsonWebTokenHandler = new JsonWebTokenHandler();
|
||||
var token = new JsonWebToken(accessToken);
|
||||
TokenValidationParameters validationParameters;
|
||||
if (token.Audiences.Any(audience => audience == this.clientId))
|
||||
{
|
||||
validationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateAudience = true,
|
||||
ValidAlgorithms = openIdConfiguration.SupportedAlgorithms,
|
||||
ValidAudience = clientId,
|
||||
ValidIssuer = openIdConfiguration.Issuer,
|
||||
IssuerSigningKeys = openIdConfiguration.SigningKeys,
|
||||
ValidateTokenReplay = true,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
validationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateLifetime = true,
|
||||
ValidateAudience = false,
|
||||
ValidateActor = false,
|
||||
ValidateIssuer = false,
|
||||
ValidateIssuerSigningKey = false,
|
||||
SignatureValidator = (token, parameters) =>
|
||||
{
|
||||
var jwt = new JsonWebToken(accessToken);
|
||||
return jwt;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var validation = await jsonWebTokenHandler.ValidateTokenAsync(accessToken, validationParameters);
|
||||
return (validation, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Http;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
internal sealed class AuthorizationHttpClientWrapper<T>
|
||||
{
|
||||
private readonly IHttpClient<T> scopedHttpClient;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
public AuthorizationHttpClientWrapper(IHttpClient<T> scopedHttpClient)
|
||||
{
|
||||
this.scopedHttpClient = scopedHttpClient;
|
||||
}
|
||||
|
||||
public AuthorizationHttpClientWrapper(HttpClient httpClient)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> PostAsync(string url, HttpContent httpContent)
|
||||
{
|
||||
if (this.scopedHttpClient is not null)
|
||||
{
|
||||
return await this.scopedHttpClient.PostAsync(url, httpContent);
|
||||
}
|
||||
|
||||
if (this.httpClient is not null)
|
||||
{
|
||||
return await this.httpClient.PostAsync(url, httpContent);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No usable http client found");
|
||||
}
|
||||
|
||||
public async Task<HttpResponseMessage> GetAsync(string url)
|
||||
{
|
||||
if (this.scopedHttpClient is not null)
|
||||
{
|
||||
return await this.scopedHttpClient.GetAsync(url);
|
||||
}
|
||||
|
||||
if (this.httpClient is not null)
|
||||
{
|
||||
return await this.httpClient.GetAsync(url);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No usable http client found");
|
||||
}
|
||||
|
||||
public void SetAuthorizationHeader(string accessToken)
|
||||
{
|
||||
if (this.scopedHttpClient is not null)
|
||||
{
|
||||
this.scopedHttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.httpClient is not null)
|
||||
{
|
||||
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("No usable http client found");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
public sealed class AuthorizationOptions
|
||||
{
|
||||
public enum Authentication
|
||||
{
|
||||
ClientSecret,
|
||||
ClientCertificate
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uri of the openid configuration for your OAuth provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Azure: "https://login.microsoftonline.com/common/.well-known/openid-configuration".
|
||||
/// Tenant Azure: "https://login.microsoftonline.com/[TENANT]/.well-known/openid-configuration".
|
||||
/// Google: "https://accounts.google.com/.well-known/openid-configuration".
|
||||
/// Facebook: "https://www.facebook.com/.well-known/openid-configuration/"
|
||||
/// </remarks>
|
||||
public string OpenIdConfigurationUri { get; set; }
|
||||
/// <summary>
|
||||
/// OAuth uri for the OAuth provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Azure: "https://login.microsoftonline.com/common/oauth2/v2.0".
|
||||
/// Azure Tenant: "https://login.microsoftonline.com/{tenant}/oauth2/v2.0".
|
||||
/// Google: "https://accounts.google.com/o/oauth2/v2/auth".
|
||||
/// Facebook: "https://graph.facebook.com/oauth"
|
||||
/// </remarks>
|
||||
public string OAuthUri { get; set; }
|
||||
/// <summary>
|
||||
/// The endpoint for the access token. Most providers use /token but some providers use /access_token or other endpoints.
|
||||
/// If your provider uses a different endpoint than /token, override this property with the correct endpoint.
|
||||
/// </summary>
|
||||
public string AuthTokenEndpoint { get; set; } = "token";
|
||||
/// <summary>
|
||||
/// ClientID of your Application, as it is registered on your OAuth provider.
|
||||
/// </summary>
|
||||
public string ClientId { get; set; }
|
||||
/// <summary>
|
||||
/// Certificate that is whitelisted as your Application on your OAuth provider.
|
||||
/// </summary>
|
||||
public X509Certificate2 ClientCertificate { get; set; }
|
||||
/// <summary>
|
||||
/// Value provided by your OAuth provider to authenticate your service as your Application.
|
||||
/// </summary>
|
||||
public string ClientSecret { get; set; }
|
||||
/// <summary>
|
||||
/// URI to redirect the request once authorization succeeds. This URI needs to be whitelisted with your
|
||||
/// OAuth provider.
|
||||
/// </summary>
|
||||
public string RedirectUri { get; set; }
|
||||
/// <summary>
|
||||
/// Scopes of your authorization request. Example "User.Read" to read the user information.
|
||||
/// </summary>
|
||||
public string Scopes { get; set; }
|
||||
/// <summary>
|
||||
/// Specify what to use for authentication.
|
||||
/// When using <see cref="Authentication.ClientSecret"/>, the authorization flow will use the <see cref="ClientSecret"/> to authenticate with the OAuth provider.
|
||||
/// When using <see cref="Authentication.ClientCertificate"/>, the authorization flow will use the <see cref="ClientCertificateThumbprint"/> to authenticate with the OAuth provider.
|
||||
/// </summary>
|
||||
public Authentication AuthenticationMode { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
internal sealed class OpenIdConfiguration
|
||||
{
|
||||
[JsonProperty("issuer")]
|
||||
public string Issuer { get; set; }
|
||||
[JsonProperty("id_token_signing_alg_values_supported")]
|
||||
public List<string> SupportedAlgorithms { get; set; }
|
||||
[JsonProperty("claims_supported")]
|
||||
public List<string> SupportedClaims { get; set; }
|
||||
[JsonProperty("jwks_uri")]
|
||||
public string SigningKeysUri { get; set; }
|
||||
public List<JsonWebKey> SigningKeys { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
internal sealed class OpenIdConfigurationCache
|
||||
{
|
||||
private static ConcurrentDictionary<string, OpenIdConfigurationCache> CacheDictionary = new();
|
||||
|
||||
private readonly string openIdConfigurationUri;
|
||||
private readonly TimeSpan configurationExpirationTime = TimeSpan.FromMinutes(30);
|
||||
|
||||
private DateTime lastConfigRetrieveTime = DateTime.MinValue;
|
||||
private OpenIdConfiguration openIdConfiguration;
|
||||
|
||||
private OpenIdConfigurationCache(string openIdConfigurationUri)
|
||||
{
|
||||
this.openIdConfigurationUri = openIdConfigurationUri;
|
||||
}
|
||||
|
||||
internal async Task<OpenIdConfiguration> GetOpenIdConfiguration<T>(AuthorizationHttpClientWrapper<T> httpClientWrapper)
|
||||
{
|
||||
if (this.openIdConfiguration is null ||
|
||||
DateTime.Now - this.lastConfigRetrieveTime > configurationExpirationTime)
|
||||
{
|
||||
await this.RetrieveOpenIdConfiguration(httpClientWrapper);
|
||||
}
|
||||
|
||||
return this.openIdConfiguration;
|
||||
}
|
||||
|
||||
private async Task RetrieveOpenIdConfiguration<T>(AuthorizationHttpClientWrapper<T> httpClientWrapper)
|
||||
{
|
||||
var response = await httpClientWrapper.GetAsync(openIdConfigurationUri);
|
||||
if (response.IsSuccessStatusCode is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to retrieve openid configuration from {this.openIdConfigurationUri}. Response status code {response.StatusCode}");
|
||||
}
|
||||
|
||||
var responseString = await response.Content.ReadAsStringAsync();
|
||||
this.openIdConfiguration = JsonConvert.DeserializeObject<OpenIdConfiguration>(responseString);
|
||||
|
||||
var keysResponse = await httpClientWrapper.GetAsync(this.openIdConfiguration.SigningKeysUri);
|
||||
if (keysResponse.IsSuccessStatusCode is false)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to retrieve openid signing keys from {this.openIdConfiguration.SigningKeysUri}. Response status code {response.StatusCode}");
|
||||
}
|
||||
|
||||
var keysResponseString = await keysResponse.Content.ReadAsStringAsync();
|
||||
this.openIdConfiguration.SigningKeys = new List<JsonWebKey>(JsonConvert.DeserializeObject<SigningKeysResponse>(keysResponseString).SigningKeys.Select(signingKey => new JsonWebKey(JsonConvert.SerializeObject(signingKey))));
|
||||
this.lastConfigRetrieveTime = DateTime.Now;
|
||||
}
|
||||
|
||||
internal static OpenIdConfigurationCache GetForConfigurationUri(string configurationUri)
|
||||
{
|
||||
if (CacheDictionary.TryGetValue(configurationUri, out var openIdConfigurationCache))
|
||||
{
|
||||
return openIdConfigurationCache;
|
||||
}
|
||||
|
||||
var newOpenIdConfigurationCache = new OpenIdConfigurationCache(configurationUri);
|
||||
CacheDictionary.AddOrUpdate(configurationUri, newOpenIdConfigurationCache, (uri, oldValue) => newOpenIdConfigurationCache);
|
||||
|
||||
return newOpenIdConfigurationCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MTSC.ServerSide;
|
||||
using System;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
internal sealed class ServerDebugLogger<T> : ILogger<T>
|
||||
{
|
||||
private readonly Server server;
|
||||
|
||||
public ServerDebugLogger(Server server)
|
||||
{
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
var message = formatter(state, exception);
|
||||
this.server.LogDebug(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
internal sealed class SigningKeysResponse
|
||||
{
|
||||
[JsonProperty("keys")]
|
||||
public List<JObject> SigningKeys { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
|
||||
namespace MTSC.OAuth2.Models
|
||||
{
|
||||
public sealed class TokenValidationResponse
|
||||
{
|
||||
public bool IsValid { get; set; }
|
||||
public JsonWebToken JsonWebToken { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# OAuth2 Web Server App Implementation
|
||||
|
||||
## Setup
|
||||
* Decorate your `HttpRouteBase` with `[Authorize]`
|
||||
* Setup server wtih an `IAuthorizationProvider`. Alternatively use `server.WithMicrosoftGraphAuthorization()` extension to use the in-built Microsoft Graph authorization provider.
|
||||
* [Optionally] implement `IHttpClient<T>` resolver to have a scoped `HttpClient` for the OAuth flow. If no `IHttpClient<T>` is present, the flow will use a `new HttpClient()` instance.
|
||||
* [Optionally] implement `ILogger<T>` resolver to have a scoped `ILogger` for the OAuth flow. If no `ILogger<T>` is present, the flow will use `Server.LogDebug()`.
|
||||
|
||||
## Functionality
|
||||
* On the first request arriving at an endpoint decorated with `[Authorize]`, the route filter with check for the presence of `AccessToken` cookie. If it is not present, the filter will redirect the client to the OAuth server to peform the authorization.
|
||||
* The OAuth server redirects the client, on a successful authorization, to the `RedirectUri`. The `RedirectUri` endpoint must also be decorated with `[Authorize]`.
|
||||
* The redirect endpoint picks up the authorization code from the OAuth server and performs a request to the OAuth server to retrieve the `AccessToken`. The filter will use the `ClientSecret` as identifier of your Web Server App to the OAuth server.
|
||||
* Once the OAuth server returns the `AccessToken`, the filter will set a `RouteContext.Resources` resource with the key `AccessToken` and value of the code provided by the OAuth server.
|
||||
* On response from the Web Server App, the filter will also set a response cookie with the `AccessToken`.
|
||||
@@ -18,6 +18,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pipelines", "Pipelines", "{
|
||||
.github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MTSC.OAuth2", "MTSC.OAuth2\MTSC.OAuth2.csproj", "{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MTSC.OAuth2.Tests", "MTSC.OAuth2.Tests\MTSC.OAuth2.Tests.csproj", "{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -58,6 +62,22 @@ Global
|
||||
{55CC0666-5599-4C3C-ADB5-E48F06361C52}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{55CC0666-5599-4C3C-ADB5-E48F06361C52}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{55CC0666-5599-4C3C-ADB5-E48F06361C52}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E1B2524A-D117-408E-A8B1-FDDE4B46AE24}.Release|x64.Build.0 = Release|Any CPU
|
||||
{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{8BE61757-1CC0-4571-8F6E-3BE0888BD17D}.Release|x64.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user