Skip to content

Commit c49bce5

Browse files
authored
Merge pull request #296
* Update samples * Update tokenexchange sample * Add missing reference * Refactor OpenAPI response transformer and document writer * make sample work again * cleanup
1 parent e688fad commit c49bce5

302 files changed

Lines changed: 123180 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

BFF/v4/DPoP/.vscode/launch.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"version": "0.2.0",
3+
"compounds": [
4+
{
5+
"name": "Run All",
6+
"configurations": ["BFF", "API"],
7+
"presentation": {
8+
"hidden": false,
9+
"group": "",
10+
"order": 1
11+
}
12+
}
13+
],
14+
"configurations": [
15+
{
16+
"name": "API",
17+
"type": "coreclr",
18+
"request": "launch",
19+
"preLaunchTask": "build-api",
20+
"program": "${workspaceFolder}/DPoP.Api/bin/Debug/net8.0/DPoP.Api.dll",
21+
"args": [],
22+
"cwd": "${workspaceFolder}/DPoP.Api",
23+
"env": {
24+
"ASPNETCORE_ENVIRONMENT": "Development"
25+
},
26+
"console": "externalTerminal",
27+
},
28+
{
29+
"name": "BFF",
30+
"type": "coreclr",
31+
"request": "launch",
32+
"preLaunchTask": "build-bff",
33+
"program": "${workspaceFolder}/DPoP.Bff/bin/Debug/net8.0/DPoP.Bff.dll",
34+
"args": [],
35+
"cwd": "${workspaceFolder}/DPoP.Bff",
36+
"env": {
37+
"ASPNETCORE_ENVIRONMENT": "Development"
38+
},
39+
"serverReadyAction": {
40+
"action": "openExternally",
41+
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
42+
},
43+
"console": "externalTerminal",
44+
}
45+
]
46+
}

BFF/v4/DPoP/.vscode/tasks.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "build",
6+
"type": "process",
7+
"command": "dotnet",
8+
"args": [
9+
"build",
10+
"${workspaceFolder}/DPoP.sln",
11+
"/property:GenerateFullPaths=true",
12+
"/consoleloggerparameters:NoSummary"
13+
],
14+
"problemMatcher": "$msCompile"
15+
},
16+
{
17+
"label": "build-api",
18+
"type": "process",
19+
"command": "dotnet",
20+
"args": [
21+
"build",
22+
"${workspaceFolder}\\DPoP.Api\\DPoP.Api.csproj",
23+
"/property:GenerateFullPaths=true",
24+
"/consoleloggerparameters:NoSummary"
25+
],
26+
"problemMatcher": "$msCompile"
27+
},
28+
{
29+
"label": "build-bff",
30+
"type": "process",
31+
"command": "dotnet",
32+
"args": [
33+
"build",
34+
"${workspaceFolder}\\DPoP.Bff\\DPoP.Bff.csproj",
35+
36+
"/property:GenerateFullPaths=true",
37+
"/consoleloggerparameters:NoSummary"
38+
],
39+
"problemMatcher": "$msCompile"
40+
}
41+
]
42+
43+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>true</ImplicitUsings>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="IdentityModel" version="7.0.0" />
10+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
11+
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
12+
</ItemGroup>
13+
</Project>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Authentication.JwtBearer;
5+
using Microsoft.Extensions.Options;
6+
7+
namespace DPoP.Api;
8+
9+
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
10+
{
11+
private readonly string _configScheme;
12+
13+
public ConfigureJwtBearerOptions(string configScheme)
14+
{
15+
_configScheme = configScheme;
16+
}
17+
18+
public void PostConfigure(string name, JwtBearerOptions options)
19+
{
20+
if (_configScheme == name)
21+
{
22+
if (options.EventsType != null && !typeof(DPoPJwtBearerEvents).IsAssignableFrom(options.EventsType))
23+
{
24+
throw new Exception("EventsType on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
25+
}
26+
if (options.Events != null && !typeof(DPoPJwtBearerEvents).IsAssignableFrom(options.Events.GetType()))
27+
{
28+
throw new Exception("Events on JwtBearerOptions must derive from DPoPJwtBearerEvents to work with the DPoP support.");
29+
}
30+
31+
if (options.Events == null && options.EventsType == null)
32+
{
33+
options.EventsType = typeof(DPoPJwtBearerEvents);
34+
}
35+
}
36+
}
37+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Text.Json;
5+
using IdentityModel;
6+
using Microsoft.AspNetCore.Authentication;
7+
using Microsoft.IdentityModel.Tokens;
8+
9+
namespace DPoP.Api;
10+
11+
/// <summary>
12+
/// Extensions methods for DPoP
13+
/// </summary>
14+
static class DPoPExtensions
15+
{
16+
const string DPoPPrefix = OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP + " ";
17+
18+
public static bool IsDPoPAuthorizationScheme(this HttpRequest request)
19+
{
20+
var authz = request.Headers.Authorization.FirstOrDefault();
21+
return authz?.StartsWith(DPoPPrefix, System.StringComparison.Ordinal) == true;
22+
}
23+
24+
public static bool TryGetDPoPAccessToken(this HttpRequest request, out string token)
25+
{
26+
token = null;
27+
28+
var authz = request.Headers.Authorization.FirstOrDefault();
29+
if (authz?.StartsWith(DPoPPrefix, System.StringComparison.Ordinal) == true)
30+
{
31+
token = authz[DPoPPrefix.Length..].Trim();
32+
return true;
33+
}
34+
return false;
35+
}
36+
37+
public static string GetAuthorizationScheme(this HttpRequest request)
38+
{
39+
return request.Headers.Authorization.FirstOrDefault()?.Split(' ', System.StringSplitOptions.RemoveEmptyEntries)[0];
40+
}
41+
42+
public static string GetDPoPProofToken(this HttpRequest request)
43+
{
44+
return request.Headers[OidcConstants.HttpHeaders.DPoP].FirstOrDefault();
45+
}
46+
47+
public static string GetDPoPNonce(this AuthenticationProperties props)
48+
{
49+
if (props.Items.ContainsKey("DPoP-Nonce"))
50+
{
51+
return props.Items["DPoP-Nonce"] as string;
52+
}
53+
return null;
54+
}
55+
public static void SetDPoPNonce(this AuthenticationProperties props, string nonce)
56+
{
57+
props.Items["DPoP-Nonce"] = nonce;
58+
}
59+
60+
/// <summary>
61+
/// Create the value of a thumbprint-based cnf claim
62+
/// </summary>
63+
public static string CreateThumbprintCnf(this JsonWebKey jwk)
64+
{
65+
var jkt = jwk.CreateThumbprint();
66+
var values = new Dictionary<string, string>
67+
{
68+
{ JwtClaimTypes.ConfirmationMethods.JwkThumbprint, jkt }
69+
};
70+
return JsonSerializer.Serialize(values);
71+
}
72+
73+
/// <summary>
74+
/// Create the value of a thumbprint
75+
/// </summary>
76+
public static string CreateThumbprint(this JsonWebKey jwk)
77+
{
78+
var jkt = Base64Url.Encode(jwk.ComputeJwkThumbprint());
79+
return jkt;
80+
}
81+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Text;
5+
using IdentityModel;
6+
using Microsoft.AspNetCore.Authentication.JwtBearer;
7+
using Microsoft.Extensions.Options;
8+
using Microsoft.Net.Http.Headers;
9+
using static IdentityModel.OidcConstants;
10+
11+
namespace DPoP.Api;
12+
13+
public class DPoPJwtBearerEvents : JwtBearerEvents
14+
{
15+
private readonly IOptionsMonitor<DPoPOptions> _optionsMonitor;
16+
private readonly DPoPProofValidator _validator;
17+
18+
public DPoPJwtBearerEvents(IOptionsMonitor<DPoPOptions> optionsMonitor, DPoPProofValidator validator)
19+
{
20+
_optionsMonitor = optionsMonitor;
21+
_validator = validator;
22+
}
23+
24+
public override Task MessageReceived(MessageReceivedContext context)
25+
{
26+
var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);
27+
28+
if (context.HttpContext.Request.TryGetDPoPAccessToken(out var token))
29+
{
30+
context.Token = token;
31+
}
32+
else if (dpopOptions.Mode == DPoPMode.DPoPOnly)
33+
{
34+
// this rejects the attempt for this handler,
35+
// since we don't want to attempt Bearer given the Mode
36+
context.NoResult();
37+
}
38+
39+
return Task.CompletedTask;
40+
}
41+
42+
public override async Task TokenValidated(TokenValidatedContext context)
43+
{
44+
var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);
45+
46+
if (context.HttpContext.Request.TryGetDPoPAccessToken(out var at))
47+
{
48+
var proofToken = context.HttpContext.Request.GetDPoPProofToken();
49+
var result = await _validator.ValidateAsync(new DPoPProofValidatonContext
50+
{
51+
Scheme = context.Scheme.Name,
52+
ProofToken = proofToken,
53+
AccessToken = at,
54+
Method = context.HttpContext.Request.Method,
55+
Url = context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host + context.HttpContext.Request.PathBase + context.HttpContext.Request.Path
56+
});
57+
58+
if (result.IsError)
59+
{
60+
// fails the result
61+
context.Fail(result.ErrorDescription ?? result.Error);
62+
63+
// we need to stash these values away so they are available later when the Challenge method is called later
64+
context.HttpContext.Items["DPoP-Error"] = result.Error;
65+
if (!string.IsNullOrWhiteSpace(result.ErrorDescription))
66+
{
67+
context.HttpContext.Items["DPoP-ErrorDescription"] = result.ErrorDescription;
68+
}
69+
if (!string.IsNullOrWhiteSpace(result.ServerIssuedNonce))
70+
{
71+
context.HttpContext.Items["DPoP-Nonce"] = result.ServerIssuedNonce;
72+
}
73+
}
74+
}
75+
else if (dpopOptions.Mode == DPoPMode.DPoPAndBearer)
76+
{
77+
// if the scheme used was not DPoP, then it was Bearer
78+
// and if a access token was presented with a cnf, then the
79+
// client should have sent it as DPoP, so we fail the request
80+
if (context.Principal.HasClaim(x => x.Type == JwtClaimTypes.Confirmation))
81+
{
82+
context.HttpContext.Items["Bearer-ErrorDescription"] = "Must use DPoP when using an access token with a 'cnf' claim";
83+
context.Fail("Must use DPoP when using an access token with a 'cnf' claim");
84+
}
85+
}
86+
}
87+
88+
public override Task Challenge(JwtBearerChallengeContext context)
89+
{
90+
var dpopOptions = _optionsMonitor.Get(context.Scheme.Name);
91+
92+
if (dpopOptions.Mode == DPoPMode.DPoPOnly)
93+
{
94+
// if we are using DPoP only, then we don't need/want the default
95+
// JwtBearerHandler to add its WWW-Authenticate response header
96+
// so we have to set the status code ourselves
97+
context.Response.StatusCode = 401;
98+
context.HandleResponse();
99+
}
100+
else if (context.HttpContext.Items.ContainsKey("Bearer-ErrorDescription"))
101+
{
102+
var description = context.HttpContext.Items["Bearer-ErrorDescription"] as string;
103+
context.ErrorDescription = description;
104+
}
105+
106+
if (context.HttpContext.Request.IsDPoPAuthorizationScheme())
107+
{
108+
// if we are challening due to dpop, then don't allow bearer www-auth to emit an error
109+
context.Error = null;
110+
}
111+
112+
// now we always want to add our WWW-Authenticate for DPoP
113+
// For example:
114+
// WWW-Authenticate: DPoP error="invalid_dpop_proof", error_description="Invalid 'iat' value."
115+
var sb = new StringBuilder();
116+
sb.Append(OidcConstants.AuthenticationSchemes.AuthorizationHeaderDPoP);
117+
118+
if (context.HttpContext.Items.ContainsKey("DPoP-Error"))
119+
{
120+
var error = context.HttpContext.Items["DPoP-Error"] as string;
121+
sb.Append(" error=\"");
122+
sb.Append(error);
123+
sb.Append('\"');
124+
125+
if (context.HttpContext.Items.ContainsKey("DPoP-ErrorDescription"))
126+
{
127+
var description = context.HttpContext.Items["DPoP-ErrorDescription"] as string;
128+
129+
sb.Append(", error_description=\"");
130+
sb.Append(description);
131+
sb.Append('\"');
132+
}
133+
}
134+
135+
context.Response.Headers.Append(HeaderNames.WWWAuthenticate, sb.ToString());
136+
137+
138+
if (context.HttpContext.Items.ContainsKey("DPoP-Nonce"))
139+
{
140+
var nonce = context.HttpContext.Items["DPoP-Nonce"] as string;
141+
context.Response.Headers[HttpHeaders.DPoPNonce] = nonce;
142+
}
143+
else
144+
{
145+
var nonce = context.Properties.GetDPoPNonce();
146+
if (nonce != null)
147+
{
148+
context.Response.Headers[HttpHeaders.DPoPNonce] = nonce;
149+
}
150+
}
151+
152+
return Task.CompletedTask;
153+
}
154+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
namespace DPoP.Api;
5+
6+
public enum DPoPMode
7+
{
8+
/// <summary>
9+
/// Only DPoP tokens will be accepted
10+
/// </summary>
11+
DPoPOnly,
12+
/// <summary>
13+
/// Both DPoP and Bearer tokens will be accepted
14+
/// </summary>
15+
DPoPAndBearer
16+
}

0 commit comments

Comments
 (0)