33
44using System ;
55using System . Collections . Generic ;
6+ using System . Collections . Specialized ;
67using System . Linq ;
78using System . Text . Json ;
89using System . Threading . Tasks ;
@@ -19,40 +20,53 @@ namespace NuGetGallery.Services.Authentication
1920{
2021 public interface IFederatedCredentialPolicyEvaluator
2122 {
22- Task < EvaluatedFederatedCredentialPolicies > GetMatchingPolicyAsync ( IReadOnlyCollection < FederatedCredentialPolicy > policies , string bearerToken ) ;
23+ Task < EvaluatedFederatedCredentialPolicies > GetMatchingPolicyAsync (
24+ IReadOnlyCollection < FederatedCredentialPolicy > policies ,
25+ string bearerToken ,
26+ NameValueCollection requestHeaders ) ;
2327 }
2428
2529 public class FederatedCredentialPolicyEvaluator : IFederatedCredentialPolicyEvaluator
2630 {
2731 private readonly IEntraIdTokenValidator _entraIdTokenValidator ;
32+ private readonly IReadOnlyList < IFederatedCredentialValidator > _additionalValidators ;
2833 private readonly IAuditingService _auditingService ;
2934 private readonly IDateTimeProvider _dateTimeProvider ;
3035 private readonly ILogger < FederatedCredentialPolicyEvaluator > _logger ;
3136
3237 public FederatedCredentialPolicyEvaluator (
3338 IEntraIdTokenValidator entraIdTokenValidator ,
39+ IReadOnlyList < IFederatedCredentialValidator > additionalValidators ,
3440 IAuditingService auditingService ,
3541 IDateTimeProvider dateTimeProvider ,
3642 ILogger < FederatedCredentialPolicyEvaluator > logger )
3743 {
3844 _entraIdTokenValidator = entraIdTokenValidator ?? throw new ArgumentNullException ( nameof ( entraIdTokenValidator ) ) ;
45+ _additionalValidators = additionalValidators ?? throw new ArgumentNullException ( nameof ( additionalValidators ) ) ;
3946 _auditingService = auditingService ?? throw new ArgumentNullException ( nameof ( auditingService ) ) ;
4047 _dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException ( nameof ( dateTimeProvider ) ) ;
4148 _logger = logger ?? throw new ArgumentNullException ( nameof ( logger ) ) ;
4249 }
4350
44- public async Task < EvaluatedFederatedCredentialPolicies > GetMatchingPolicyAsync ( IReadOnlyCollection < FederatedCredentialPolicy > policies , string bearerToken )
51+ public async Task < EvaluatedFederatedCredentialPolicies > GetMatchingPolicyAsync (
52+ IReadOnlyCollection < FederatedCredentialPolicy > policies ,
53+ string bearerToken ,
54+ NameValueCollection requestHeaders )
4555 {
4656 // perform basic validations not specific to any federated credential policy
4757 // the error message is user-facing and should not leak sensitive information
4858 var ( userError , jwtInfo ) = await ValidateJwtByIssuer ( bearerToken ) ;
4959
60+ // Whether or not we have detected a problem already, pass the information to all additional validators.
61+ // This allows custom logic to execute but will not override any initial failed validation result.
62+ userError = await ExecuteAdditionalValidatorsAsync ( requestHeaders , userError , jwtInfo ) ;
63+
5064 var externalCredentialAudit = jwtInfo . CreateAuditRecord ( ) ;
5165 await AuditExternalCredentialAsync ( externalCredentialAudit ) ;
5266
5367 if ( userError is not null )
5468 {
55- _logger . LogInformation ( "The bearer token could not be validated . Reason: {UserError}" , userError ) ;
69+ _logger . LogInformation ( "The bearer token failed validation . Reason: {UserError}" , userError ) ;
5670 return EvaluatedFederatedCredentialPolicies . BadToken ( userError ) ;
5771 }
5872
@@ -81,6 +95,53 @@ public async Task<EvaluatedFederatedCredentialPolicies> GetMatchingPolicyAsync(I
8195 return EvaluatedFederatedCredentialPolicies . NoMatchingPolicy ( results ) ;
8296 }
8397
98+ private async Task < string ? > ExecuteAdditionalValidatorsAsync ( NameValueCollection requestHeaders , string ? userError , JwtInfo jwtInfo )
99+ {
100+ bool hasUnauthorized = false ;
101+ foreach ( var validator in _additionalValidators )
102+ {
103+ var result = await validator . ValidateAsync ( requestHeaders , jwtInfo . IssuerType , jwtInfo . Jwt ? . Claims ) ;
104+ switch ( result . Type )
105+ {
106+ case FederatedCredentialValidationType . Unauthorized :
107+ // prefer the first user error, which will be the built-in user error if the bearer token is already rejected
108+ userError ??= result . UserError ;
109+ hasUnauthorized = true ;
110+ if ( jwtInfo . IsValid )
111+ {
112+ _logger . LogWarning (
113+ "With issuer type {IssuerType}, the additional validator {Type} rejected the request, but the base validation accepted the bearer token. " +
114+ "Additional validator user message: {UserError}" ,
115+ jwtInfo . IssuerType ,
116+ validator . GetType ( ) . FullName ,
117+ result . UserError ?? "(none)" ) ;
118+ }
119+ break ;
120+ case FederatedCredentialValidationType . Valid :
121+ if ( ! jwtInfo . IsValid )
122+ {
123+ _logger . LogWarning (
124+ "With issuer type {IssuerType}, the additional validator {Type} accepted the request, but the base validation rejected the bearer token." ,
125+ jwtInfo . IssuerType ,
126+ validator . GetType ( ) . FullName ) ;
127+ }
128+ break ;
129+ case FederatedCredentialValidationType . NotApplicable :
130+ break ;
131+ default :
132+ throw new NotImplementedException ( "Unsupported validation type:" + result . Type ) ;
133+ }
134+ }
135+
136+ if ( hasUnauthorized )
137+ {
138+ userError ??= "The request could not be authenticated." ;
139+ jwtInfo . IsValid = false ;
140+ }
141+
142+ return userError ;
143+ }
144+
84145 private async Task AuditPolicyComparisonAsync ( ExternalSecurityTokenAuditRecord externalCredentialAudit , FederatedCredentialPolicy policy , bool success )
85146 {
86147 await _auditingService . SaveAuditRecordAsync ( FederatedCredentialPolicyAuditRecord . Compare (
0 commit comments