Skip to content

Commit 7d8689d

Browse files
authored
[OIDC] Emit audit records during token exchange and policy admin (#10305)
1 parent 13f29a0 commit 7d8689d

4 files changed

Lines changed: 239 additions & 1 deletion

File tree

src/NuGetGallery.Services/Authentication/Federated/FederatedCredentialPolicyEvaluator.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.IdentityModel.JsonWebTokens;
1212
using Microsoft.IdentityModel.Tokens;
1313
using NuGet.Services.Entities;
14+
using NuGetGallery.Auditing;
1415

1516
#nullable enable
1617

@@ -24,15 +25,18 @@ public interface IFederatedCredentialPolicyEvaluator
2425
public class FederatedCredentialPolicyEvaluator : IFederatedCredentialPolicyEvaluator
2526
{
2627
private readonly IEntraIdTokenValidator _entraIdTokenValidator;
28+
private readonly IAuditingService _auditingService;
2729
private readonly IDateTimeProvider _dateTimeProvider;
2830
private readonly ILogger<FederatedCredentialPolicyEvaluator> _logger;
2931

3032
public FederatedCredentialPolicyEvaluator(
3133
IEntraIdTokenValidator entraIdTokenValidator,
34+
IAuditingService auditingService,
3235
IDateTimeProvider dateTimeProvider,
3336
ILogger<FederatedCredentialPolicyEvaluator> logger)
3437
{
3538
_entraIdTokenValidator = entraIdTokenValidator ?? throw new ArgumentNullException(nameof(entraIdTokenValidator));
39+
_auditingService = auditingService ?? throw new ArgumentNullException(nameof(auditingService));
3640
_dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException(nameof(dateTimeProvider));
3741
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3842
}
@@ -43,6 +47,9 @@ public async Task<EvaluatedFederatedCredentialPolicies> GetMatchingPolicyAsync(I
4347
// the error message is user-facing and should not leak sensitive information
4448
var (userError, jwtInfo) = await ValidateJwtByIssuer(bearerToken);
4549

50+
var externalCredentialAudit = jwtInfo.CreateAuditRecord();
51+
await AuditExternalCredentialAsync(externalCredentialAudit);
52+
4653
if (userError is not null)
4754
{
4855
_logger.LogInformation("The bearer token could not be validated. Reason: {UserError}", userError);
@@ -64,6 +71,7 @@ public async Task<EvaluatedFederatedCredentialPolicies> GetMatchingPolicyAsync(I
6471
results.Add(result);
6572

6673
var success = result.Type == FederatedCredentialPolicyResultType.Success;
74+
await AuditPolicyComparisonAsync(externalCredentialAudit, policy, success);
6775
if (success)
6876
{
6977
return EvaluatedFederatedCredentialPolicies.NewMatchedPolicy(results, result.Policy, result.FederatedCredential);
@@ -73,6 +81,19 @@ public async Task<EvaluatedFederatedCredentialPolicies> GetMatchingPolicyAsync(I
7381
return EvaluatedFederatedCredentialPolicies.NoMatchingPolicy(results);
7482
}
7583

84+
private async Task AuditPolicyComparisonAsync(ExternalSecurityTokenAuditRecord externalCredentialAudit, FederatedCredentialPolicy policy, bool success)
85+
{
86+
await _auditingService.SaveAuditRecordAsync(FederatedCredentialPolicyAuditRecord.Compare(
87+
policy,
88+
externalCredentialAudit,
89+
success));
90+
}
91+
92+
private async Task AuditExternalCredentialAsync(ExternalSecurityTokenAuditRecord externalCredentialAudit)
93+
{
94+
await _auditingService.SaveAuditRecordAsync(externalCredentialAudit);
95+
}
96+
7697
private FederatedCredentialPolicyResult EvaluatePolicy(FederatedCredentialPolicy policy, JwtInfo info)
7798
{
7899
// Evaluate the policy and return an unauthorized result if the policy does not match.
@@ -123,6 +144,23 @@ private class JwtInfo
123144
public string? Identifier { get; set; }
124145

125146
public FederatedCredentialIssuerType IssuerType { get; set; } = FederatedCredentialIssuerType.Unsupported;
147+
148+
public ExternalSecurityTokenAuditRecord CreateAuditRecord()
149+
{
150+
var payload = Jwt?.EncodedPayload;
151+
string? claims = null;
152+
if (!string.IsNullOrWhiteSpace(payload))
153+
{
154+
claims = Base64UrlEncoder.Decode(payload);
155+
}
156+
157+
return new ExternalSecurityTokenAuditRecord(
158+
IsValid ? AuditedExternalSecurityTokenAction.Validated : AuditedExternalSecurityTokenAction.Rejected,
159+
IssuerType,
160+
Jwt?.Issuer,
161+
claims,
162+
Identifier);
163+
}
126164
}
127165

128166
/// <summary>

src/NuGetGallery.Services/Authentication/Federated/FederatedCredentialService.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text.Json;
77
using System.Threading.Tasks;
88
using NuGet.Services.Entities;
9+
using NuGetGallery.Auditing;
910
using NuGetGallery.Authentication;
1011
using NuGetGallery.Infrastructure.Authentication;
1112

@@ -50,6 +51,7 @@ public class FederatedCredentialService : IFederatedCredentialService
5051
private readonly IEntraIdTokenValidator _entraIdTokenValidator;
5152
private readonly ICredentialBuilder _credentialBuilder;
5253
private readonly IAuthenticationService _authenticationService;
54+
private readonly IAuditingService _auditingService;
5355
private readonly IDateTimeProvider _dateTimeProvider;
5456
private readonly IFeatureFlagService _featureFlagService;
5557
private readonly IFederatedCredentialConfiguration _configuration;
@@ -61,6 +63,7 @@ public FederatedCredentialService(
6163
IEntraIdTokenValidator entraIdTokenValidator,
6264
ICredentialBuilder credentialBuilder,
6365
IAuthenticationService authenticationService,
66+
IAuditingService auditingService,
6467
IDateTimeProvider dateTimeProvider,
6568
IFeatureFlagService featureFlagService,
6669
IFederatedCredentialConfiguration configuration)
@@ -71,6 +74,7 @@ public FederatedCredentialService(
7174
_entraIdTokenValidator = entraIdTokenValidator ?? throw new ArgumentNullException(nameof(EntraIdTokenValidator));
7275
_credentialBuilder = credentialBuilder ?? throw new ArgumentNullException(nameof(credentialBuilder));
7376
_authenticationService = authenticationService ?? throw new ArgumentNullException(nameof(authenticationService));
77+
_auditingService = auditingService ?? throw new ArgumentNullException(nameof(auditingService));
7478
_dateTimeProvider = dateTimeProvider ?? throw new ArgumentNullException(nameof(dateTimeProvider));
7579
_featureFlagService = featureFlagService ?? throw new ArgumentNullException(nameof(featureFlagService));
7680
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
@@ -112,6 +116,8 @@ public async Task<AddFederatedCredentialPolicyResult> AddEntraIdServicePrincipal
112116

113117
await _repository.AddPolicyAsync(policy, saveChanges: true);
114118

119+
await _auditingService.SaveAuditRecordAsync(FederatedCredentialPolicyAuditRecord.Create(policy));
120+
115121
return AddFederatedCredentialPolicyResult.Created(policy);
116122
}
117123

@@ -123,7 +129,13 @@ public async Task DeletePolicyAsync(FederatedCredentialPolicy policy)
123129
await _authenticationService.RemoveCredential(policy.CreatedBy, credential, commitChanges: false);
124130
}
125131

132+
// Initialize the audit record before deletion so details are still available.
133+
// Entity Framework unlinks navigation properties.
134+
var auditRecord = FederatedCredentialPolicyAuditRecord.Delete(policy, credentials);
135+
126136
await _repository.DeletePolicyAsync(policy, saveChanges: true);
137+
138+
await _auditingService.SaveAuditRecordAsync(auditRecord);
127139
}
128140

129141
public async Task<GenerateApiKeyResult> GenerateApiKeyAsync(string username, string bearerToken)
@@ -204,9 +216,18 @@ private static GenerateApiKeyResult NoMatchingPolicy(string username)
204216
}
205217
catch (DataException ex) when (ex.IsSqlUniqueConstraintViolation())
206218
{
219+
await _auditingService.SaveAuditRecordAsync(FederatedCredentialPolicyAuditRecord.RejectReplay(
220+
evaluation.MatchedPolicy,
221+
evaluation.FederatedCredential));
222+
207223
return GenerateApiKeyResult.Unauthorized("This bearer token has already been used. A new bearer token must be used for each request.");
208224
}
209225

226+
await _auditingService.SaveAuditRecordAsync(FederatedCredentialPolicyAuditRecord.ExchangeForApiKey(
227+
evaluation.MatchedPolicy,
228+
evaluation.FederatedCredential,
229+
apiKeyCredential));
230+
210231
return null;
211232
}
212233

0 commit comments

Comments
 (0)