1+ using OAuch . Compliance . Tests . Features ;
2+ using OAuch . Protocols . Http ;
3+ using OAuch . Protocols . JWK ;
4+ using OAuch . Protocols . JWT ;
5+ using OAuch . Protocols . OAuth2 ;
6+ using OAuch . Protocols . OAuth2 . BuildingBlocks ;
7+ using OAuch . Protocols . OAuth2 . Pipeline ;
8+ using OAuch . Shared ;
9+ using OAuch . Shared . Enumerations ;
10+ using System ;
11+ using System . Security . Claims ;
12+ using System . Threading . Tasks ;
13+
14+ namespace OAuch . Compliance . Tests . DPoP {
15+ public class HasRequiredClaimsTest : Test {
16+ public override string Title => "Are all required DPoP claims present" ;
17+ public override string Description => "This test checks if the authorization server only accepts DPoP proofs that contain all required claims ('jti', 'htm', 'htu', 'iat', and 'ath' for API requests)." ;
18+ public override TestResultFormatter ResultFormatter => TestResultFormatter . YesGoodNoBad ;
19+ public override Type ResultType => typeof ( HasRequiredClaimsTestResult ) ;
20+ }
21+
22+ public class HasRequiredClaimsTestResult : TestResult {
23+ public HasRequiredClaimsTestResult ( string testId ) : base ( testId ) { }
24+ public override Type ImplementationType => typeof ( HasRequiredClaimsTestImplementation ) ;
25+ }
26+
27+ public class HasRequiredClaimsTestImplementation : TestImplementation {
28+ public HasRequiredClaimsTestImplementation ( TestRunContext context , HasRequiredClaimsTestResult result , HasSupportedFlowsTestResult flows , IsDPoPSupportedTestResult dpop )
29+ : base ( context , result , flows , dpop ) { }
30+
31+ public override async Task Run ( ) {
32+ if ( HasFailed < IsDPoPSupportedTestResult > ( ) ) {
33+ Result . Outcome = TestOutcomes . Skipped ;
34+ return ;
35+ }
36+
37+ var flows = GetDependency < HasSupportedFlowsTestResult > ( true ) ;
38+ if ( flows == null ) {
39+ Result . Outcome = TestOutcomes . Skipped ;
40+ return ;
41+ }
42+
43+ var prov = flows . CreateProviderWithStage < AddDPoPHeader , HttpRequest , HttpRequest > ( Context ) ;
44+ if ( prov == null ) {
45+ Result . Outcome = TestOutcomes . Skipped ;
46+ return ;
47+ }
48+ var processor = new RemoveDPoPClaimProcessor ( ) ;
49+ prov . Pipeline . Replace < AddDPoPHeader , HttpRequest , HttpRequest > ( processor ) ;
50+
51+ var requiredClaims = new [ ] { "jti" , "htm" , "htu" , "iat" } ;
52+ foreach ( var claim in requiredClaims ) {
53+ processor . ClaimToRemove = claim ;
54+ var token = await prov . GetToken ( ) ;
55+ if ( token . AccessToken != null || token . IdentityToken != null ) {
56+ Result . Outcome = TestOutcomes . SpecificationNotImplemented ;
57+ LogInfo ( $ "The server accepted a DPoP proof without the required '{ claim } ' claim.") ;
58+ return ;
59+ }
60+ }
61+ Result . Outcome = TestOutcomes . SpecificationFullyImplemented ;
62+
63+ // Now test 'ath' claim (API request)
64+ var validProvider = flows . CreateProvider ( Context , mustHaveDPoPTokens : true ) ;
65+ if ( validProvider == null ) {
66+ return ;
67+ }
68+ var validToken = await validProvider . GetToken ( ) ;
69+ if ( string . IsNullOrWhiteSpace ( validToken . AccessToken ) ) {
70+ return ;
71+ }
72+
73+ // Use a custom ApiRequest that removes 'ath' from the DPoP proof
74+ var apiRequest = new RemoveAthApiRequest ( Context ) ;
75+ var response = await apiRequest . Send ( validToken ) ;
76+ if ( response . StatusCode . IsOk ( ) ) {
77+ Result . Outcome = TestOutcomes . SpecificationNotImplemented ;
78+ LogInfo ( "The server accepted an API request with a DPoP proof missing the 'ath' claim." ) ;
79+ return ;
80+ }
81+
82+ LogInfo ( "The server correctly rejected DPoP proofs missing required claims." ) ;
83+ }
84+
85+ // Processor to remove a specific claim from the DPoP proof
86+ public class RemoveDPoPClaimProcessor : Processor < HttpRequest , HttpRequest > {
87+ public string ? ClaimToRemove { get ; set ; }
88+ public override Task < HttpRequest ? > Process ( HttpRequest value , IProvider provider , TokenResult tokenResult ) {
89+ var dpop = OAuthHelper . CreateDPoPToken ( provider . SiteSettings , value , null , tokenResult . AuthorizationDPoPNonce ) ;
90+ if ( dpop != null ) {
91+ var builder = JwtTokenBuilder . CreateFromToken ( dpop ) ;
92+ if ( builder != null && ClaimToRemove != null ) {
93+ builder . Claims . Remove ( ClaimToRemove ) ;
94+ JsonWebKey key = JsonWebKey . Create ( provider . SiteSettings . DPoPSigningKey ) ! ;
95+ value . Headers [ HttpRequestHeaders . DPoP ] = builder . Build ( key . Algorithm ! , key . TokenKey ) ;
96+ return Task . FromResult < HttpRequest ? > ( value ) ;
97+ }
98+ }
99+ this . Succeeded = false ;
100+ return Task . FromResult < HttpRequest ? > ( null ) ;
101+ }
102+ }
103+
104+ // Custom ApiRequest that removes the 'ath' claim from the DPoP proof
105+ public class RemoveAthApiRequest : ApiRequest {
106+ public RemoveAthApiRequest ( TestRunContext context ) : base ( context ) { }
107+ protected override HttpRequest GetRequest ( string uri , TokenResult token ) {
108+ var req = base . GetRequest ( uri , token ) ;
109+ if ( req . Headers . TryGetValue ( HttpRequestHeaders . DPoP , out var dpop ) ) {
110+ var builder = JwtTokenBuilder . CreateFromToken ( dpop , Context . Log ) ;
111+ if ( builder != null ) {
112+ builder . Claims . Remove ( "ath" ) ;
113+ JsonWebKey key = JsonWebKey . Create ( Context . SiteSettings . DPoPSigningKey ) ! ;
114+ req . Headers [ HttpRequestHeaders . DPoP ] = builder . Build ( key . Algorithm ! , key . TokenKey ) ;
115+ }
116+ }
117+ return req ;
118+ }
119+ }
120+ }
121+ }
0 commit comments