Skip to content

Commit 66fb09a

Browse files
committed
Merge branch 'feature/update'
2 parents 714fc6e + ce652b0 commit 66fb09a

14 files changed

Lines changed: 419 additions & 15 deletions

OAuthClientAssertionsPerInstance.sln

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.6.33829.357
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.7.11903.348 stable
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_solutionsFiles", "_solutionsFiles", "{987D7C1A-2964-4FB5-8863-50B02F5ABA72}"
77
ProjectSection(SolutionItems) = preProject
@@ -13,12 +13,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerateCertificate", "src\
1313
EndProject
1414
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DPoPApiDefault", "src\DPoPApiDefault\DPoPApiDefault.csproj", "{EA050747-1F4A-54F3-8B8C-891C2D156023}"
1515
EndProject
16-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsolePerInstanceAssertion", "src\ConsolePerInstanceAssertion\ConsolePerInstanceAssertion.csproj", "{D97819C8-1FFD-210D-BBC9-DCA3952E0CA0}"
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsolePerInstanceAssertionRS256", "src\ConsolePerInstanceAssertion\ConsolePerInstanceAssertionRS256.csproj", "{D97819C8-1FFD-210D-BBC9-DCA3952E0CA0}"
1717
EndProject
1818
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Idp", "src\Idp\Idp.csproj", "{70288A7A-3DC2-7D4C-163B-7A3016EC29A4}"
1919
EndProject
2020
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthFlow", "src\AuthFlow\AuthFlow.csproj", "{A8F2D081-7C4F-A151-C6B5-D86834A285DE}"
2121
EndProject
22+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsolePerInstanceAssertionES256", "src\ConsolePerInstanceAssertionES256\ConsolePerInstanceAssertionES256.csproj", "{4CA3B2C4-4E9F-D92B-43F2-954D941F5264}"
23+
EndProject
2224
Global
2325
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2426
Debug|Any CPU = Debug|Any CPU
@@ -45,6 +47,10 @@ Global
4547
{A8F2D081-7C4F-A151-C6B5-D86834A285DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
4648
{A8F2D081-7C4F-A151-C6B5-D86834A285DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
4749
{A8F2D081-7C4F-A151-C6B5-D86834A285DE}.Release|Any CPU.Build.0 = Release|Any CPU
50+
{4CA3B2C4-4E9F-D92B-43F2-954D941F5264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51+
{4CA3B2C4-4E9F-D92B-43F2-954D941F5264}.Debug|Any CPU.Build.0 = Debug|Any CPU
52+
{4CA3B2C4-4E9F-D92B-43F2-954D941F5264}.Release|Any CPU.ActiveCfg = Release|Any CPU
53+
{4CA3B2C4-4E9F-D92B-43F2-954D941F5264}.Release|Any CPU.Build.0 = Release|Any CPU
4854
EndGlobalSection
4955
GlobalSection(SolutionProperties) = preSolution
5056
HideSolutionNode = FALSE

src/ConsolePerInstanceAssertion/ClientAssertionService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
using System.Threading;
1111
using System.Threading.Tasks;
1212

13-
namespace ConsolePerInstanceAssertion;
13+
namespace ConsolePerInstanceAssertionRS256;
1414

1515
public class ClientAssertionService : IClientAssertionService
1616
{

src/ConsolePerInstanceAssertion/ConsolePerInstanceAssertion.csproj renamed to src/ConsolePerInstanceAssertion/ConsolePerInstanceAssertionRS256.csproj

File renamed without changes.

src/ConsolePerInstanceAssertion/DPoPClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
using System.Threading;
1010
using System.Threading.Tasks;
1111

12-
namespace ConsolePerInstanceAssertion;
12+
namespace ConsolePerInstanceAssertionRS256;
1313

1414
public class DPoPClient : BackgroundService
1515
{

src/ConsolePerInstanceAssertion/KeySessionService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
using AuthFlow;
1010
using System.Text.Json;
1111

12-
namespace ConsolePerInstanceAssertion;
12+
namespace ConsolePerInstanceAssertionRS256;
1313

1414
/// <summary>
1515
/// Creates a new key for the application client assertion.

src/ConsolePerInstanceAssertion/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
using System.Security.Cryptography;
1010
using System.Text.Json;
1111

12-
namespace ConsolePerInstanceAssertion;
12+
namespace ConsolePerInstanceAssertionRS256;
1313

1414
public class Program
1515
{
1616
public static void Main(string[] args)
1717
{
18-
Console.Title = "DPoP client with client assertions";
18+
Console.Title = "RS256 DPoP client with client assertions";
1919

2020
Log.Logger = new LoggerConfiguration()
2121
.MinimumLevel.Debug()
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
////using Duende.AccessTokenManagement;
2+
using Duende.AccessTokenManagement;
3+
using Duende.IdentityModel;
4+
using Duende.IdentityModel.Client;
5+
using Microsoft.Extensions.Options;
6+
using Microsoft.IdentityModel.JsonWebTokens;
7+
using Microsoft.IdentityModel.Tokens;
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
13+
namespace ConsolePerInstanceAssertionES256;
14+
15+
public class ClientAssertionService : IClientAssertionService
16+
{
17+
private readonly IOptionsSnapshot<ClientCredentialsClient> _options;
18+
private readonly KeySessionService _keySessionService;
19+
20+
public ClientAssertionService(IOptionsSnapshot<ClientCredentialsClient> options,
21+
KeySessionService keySessionService)
22+
{
23+
_options = options;
24+
_keySessionService = keySessionService;
25+
}
26+
27+
public async Task<ClientAssertion?> GetClientAssertionAsync(ClientCredentialsClientName? clientName = null, TokenRequestParameters? parameters = null, CancellationToken ct = default)
28+
{
29+
if ((clientName == "mobile-dpop-client") || (clientName == "onboarding-user-client"))
30+
{
31+
var key = await _keySessionService.CreateGetSessionAsync();
32+
var options = _options.Get(clientName);
33+
34+
var descriptor = new SecurityTokenDescriptor
35+
{
36+
Issuer = options.ClientId,
37+
Audience = options.TokenEndpoint!.ToString(),
38+
Expires = DateTime.UtcNow.AddMinutes(1),
39+
SigningCredentials = key.SigningCredentials,
40+
41+
Claims = new Dictionary<string, object>
42+
{
43+
{ JwtClaimTypes.JwtId, Guid.NewGuid().ToString() },
44+
{ JwtClaimTypes.Subject, options.ClientId.ToString()! },
45+
{ JwtClaimTypes.IssuedAt, DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
46+
{ "device_auth_session", key.AuthSession! }
47+
}
48+
};
49+
50+
var handler = new JsonWebTokenHandler();
51+
var jwt = handler.CreateToken(descriptor);
52+
53+
return await Task.FromResult<ClientAssertion?>(new ClientAssertion
54+
{
55+
Type = OidcConstants.ClientAssertionTypes.JwtBearer,
56+
Value = jwt
57+
});
58+
}
59+
60+
return await Task.FromResult<ClientAssertion?>(null);
61+
}
62+
}
63+
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Worker">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Duende.AccessTokenManagement" Version="4.2.0"></PackageReference>
10+
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
11+
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.19.1" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\AuthFlow\AuthFlow.csproj" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Brock Allen & Dominick Baier. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3+
4+
using Microsoft.Extensions.Hosting;
5+
using Microsoft.Extensions.Logging;
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Net.Http;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
namespace ConsolePerInstanceAssertionES256;
13+
14+
public class DPoPClient : BackgroundService
15+
{
16+
private readonly ILogger<DPoPClient> _logger;
17+
private readonly IHttpClientFactory _clientFactory;
18+
private readonly KeySessionService _keySessionService;
19+
20+
public DPoPClient(ILogger<DPoPClient> logger, IHttpClientFactory factory, KeySessionService keySessionService)
21+
{
22+
_logger = logger;
23+
_clientFactory = factory;
24+
_keySessionService = keySessionService;
25+
}
26+
27+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
28+
{
29+
await Task.Delay(2000, stoppingToken);
30+
31+
while (!stoppingToken.IsCancellationRequested)
32+
{
33+
Console.WriteLine("\n\n");
34+
_logger.LogInformation("DPoPClient running at: {time}", DateTimeOffset.UtcNow);
35+
36+
var session = await _keySessionService.CreateGetSessionAsync();
37+
38+
// Onboarding User API
39+
var onboardingClient = _clientFactory.CreateClient("onboarding-user-client");
40+
var formData = new List<KeyValuePair<string, string>>
41+
{
42+
new KeyValuePair<string, string>("email", "your.email@email.ch")
43+
};
44+
var content = new FormUrlEncodedContent(formData);
45+
await onboardingClient.PostAsync("api/AuthorizationChallengeRequest/StartEmailVerification", content, stoppingToken);
46+
47+
// Call mobile API
48+
var client = _clientFactory.CreateClient("mobile-dpop-client");
49+
var response = await client.GetAsync("api/values", stoppingToken);
50+
51+
if (response.IsSuccessStatusCode)
52+
{
53+
var responseContent = await response.Content.ReadAsStringAsync(stoppingToken);
54+
_logger.LogInformation("API response: {response}", responseContent);
55+
}
56+
else
57+
{
58+
_logger.LogError("API returned: {statusCode}", response.StatusCode);
59+
}
60+
61+
await Task.Delay(5000, stoppingToken);
62+
}
63+
}
64+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
2+
using Microsoft.IdentityModel.Protocols;
3+
using Microsoft.IdentityModel.Tokens;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Net.Http;
7+
using System.Security.Cryptography;
8+
using System.Threading.Tasks;
9+
using AuthFlow;
10+
using System.Text.Json;
11+
12+
namespace ConsolePerInstanceAssertionES256;
13+
14+
/// <summary>
15+
/// Creates a new key for the application client assertion.
16+
/// The public key is exchanged with the IDP and connected with a session.
17+
/// The application should validate more identity data like an email, sms and connect this to the session.
18+
/// </summary>
19+
public class KeySessionService
20+
{
21+
/// <summary>
22+
/// One signing key per application instance
23+
/// </summary>
24+
private static (string? AuthSession, SigningCredentials? SigningCredentials) _inMemoryCache = (null, null);
25+
26+
private readonly AuthFlowConfiguration _authFlowConfiguration = new AuthFlowConfiguration
27+
{
28+
ClientId = "cid-fp-device",
29+
TokenMetadataAddress = "https://localhost:5101/.well-known/openid-configuration",
30+
TokenAuthority = "https://localhost:5101"
31+
};
32+
33+
public async Task<(string? AuthSession, SigningCredentials? SigningCredentials)> CreateGetSessionAsync()
34+
{
35+
if (_inMemoryCache.AuthSession != null)
36+
{
37+
return _inMemoryCache;
38+
}
39+
40+
var ecdsa = ECDsa.Create();
41+
ecdsa.KeySize = 256;
42+
var ecdsaCertificateKey = new ECDsaSecurityKey(ecdsa);
43+
var publicKeyPem = ecdsa.ExportSubjectPublicKeyInfoPem();
44+
45+
var httpClient = new HttpClient();
46+
47+
var nonce = RandomNumberGenerator.GetHexString(73);
48+
var state = RandomNumberGenerator.GetHexString(67);
49+
//client_id = cid_235saw4r4
50+
//& grant_type = fp_register
51+
//& public_key =< public_key >
52+
//&state =< state >
53+
//&nonce =< nonce >
54+
var formData = new List<KeyValuePair<string, string>>
55+
{
56+
new KeyValuePair<string, string>("client_id", _authFlowConfiguration.ClientId),
57+
new KeyValuePair<string, string>("grant_type", OAuthConsts.GRANT_TYPE),
58+
new KeyValuePair<string, string>("public_key", publicKeyPem),
59+
new KeyValuePair<string, string>("alg", "ES256"),
60+
new KeyValuePair<string, string>("state", state),
61+
new KeyValuePair<string, string>("nonce", nonce)
62+
};
63+
64+
// Encodes the key-value pairs for the ContentType 'application/x-www-form-urlencoded'
65+
HttpContent content = new FormUrlEncodedContent(formData);
66+
var response = await httpClient.PostAsync("https://localhost:5101/api/DeviceRegistration", content);
67+
68+
if (response.IsSuccessStatusCode)
69+
{
70+
var signingCredentials = new SigningCredentials(ecdsaCertificateKey, "ES256");
71+
var responseResult = await response.Content.ReadAsStringAsync();
72+
var deviceRegistrationResponse = JsonSerializer.Deserialize<DeviceRegistrationResponse>(responseResult);
73+
74+
if (deviceRegistrationResponse == null)
75+
{
76+
throw new Exception("no response");
77+
}
78+
// TODO
79+
// Validate state
80+
// Validate JWT signing credential
81+
// Validate nbf, exp, iat
82+
// Validate nonce
83+
// Validate aud (clientId)
84+
// Validate iss
85+
// Validate "typ": "fp+jwt"
86+
87+
var (Valid, Reason, Error) = ValidateTokenResponsePayload
88+
.IsValid(deviceRegistrationResponse, _authFlowConfiguration, state);
89+
90+
if (!Valid)
91+
{
92+
Console.WriteLine($"UnauthorizedValidationParametersFailed {Reason} {Error}");
93+
throw new ArgumentNullException("auth_session", "UnauthorizedValidationParametersFailed");
94+
}
95+
96+
// get well known endpoints and validate access token sent in the assertion
97+
var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>(
98+
_authFlowConfiguration.TokenMetadataAddress,
99+
new OpenIdConnectConfigurationRetriever());
100+
101+
var wellKnownEndpoints = await configurationManager.GetConfigurationAsync();
102+
103+
var deviceTokenValidationResult = await ValidateTokenResponsePayload.ValidateTokenAndSignature(
104+
deviceRegistrationResponse.FpToken, _authFlowConfiguration, wellKnownEndpoints.SigningKeys);
105+
106+
if (!deviceTokenValidationResult.Valid)
107+
{
108+
Console.WriteLine($"UnauthorizedValidationTokenAndSignatureFailed {Reason} {Error}");
109+
throw new ArgumentNullException("auth_session", "UnauthorizedValidationTokenAndSignatureFailed");
110+
}
111+
112+
var nonceInResponse = ValidateTokenResponsePayload.GetNonce(deviceTokenValidationResult.ClaimsIdentity!);
113+
if (nonceInResponse != nonce)
114+
{
115+
Console.WriteLine("Nonce validation failed");
116+
throw new ArgumentNullException("auth_session", "Nonce validation failed");
117+
}
118+
119+
var authSession = ValidateTokenResponsePayload.GetAuthSession(deviceTokenValidationResult.ClaimsIdentity!);
120+
_inMemoryCache = (authSession, signingCredentials);
121+
122+
// TODO persist key in TPM and re-use
123+
124+
return _inMemoryCache;
125+
}
126+
127+
throw new ArgumentNullException("auth_session", "something went wrong");
128+
}
129+
}

0 commit comments

Comments
 (0)