Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OIDC 2] Add federated credential entities (no DB change yet) #10252

Merged
merged 2 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/NuGet.Services.Entities/FederatedCredential.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace NuGet.Services.Entities
{
/// <summary>
/// The record of a federated credential that was accepted by a federated credential policy.
/// </summary>
public class FederatedCredential : IEntity
{
/// <summary>
/// The unique key for this federated credential. Generated by the database.
/// </summary>
public int Key { get; set; }

/// <summary>
/// A type enum of the <see cref="FederatedCredentialPolicy.Type"/> that accepted this federated credential.
/// </summary>
[Required]
[Column("TypeKey")]
public FederatedCredentialType Type { get; set; }

/// <summary>
/// The key of the federated credential policy that accepted this federated credential. This does not have a
/// foreign key constraint because the policy may be deleted, but the credential record should remain to ensure
/// the <see cref="Identity"/> unique constraint is enforced (to prevent replay).
/// </summary>
public int FederatedCredentialPolicyKey { get; set; }

/// <summary>
/// A unique identifier for the federated credential used to create this credential record. For OIDC tokens,
/// this is the "jti" or "uti" claim. This must be unique to ensure tokens are not replayed.
/// </summary>
[StringLength(maximumLength: 64)]
public string Identity { get; set; }

/// <summary>
/// When this record was first created. The timestamp is in UTC.
/// </summary>
public DateTime Created { get; set; }

/// <summary>
/// When the federated credential expires. For OIDC tokens, this will be the "exp" claim. The timestamp is in UTC.
/// </summary>
public DateTime Expires { get; set; }
}
}
86 changes: 86 additions & 0 deletions src/NuGet.Services.Entities/FederatedCredentialPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace NuGet.Services.Entities
{
/// <summary>
/// This entity defines which federated credentials can operate on behalf of a user. The policy is used to evaluate
/// an incoming federated credential and define what actions can be taken on behalf of the user.
/// </summary>
public class FederatedCredentialPolicy : IEntity
{
/// <summary>
/// The unique key for this federated credential policy. Generated by the database.
/// </summary>
public int Key { get; set; }

/// <summary>
/// When this entity was first created. The timestamp is in UTC.
/// </summary>
public DateTime Created { get; set; }

/// <summary>
/// When this policy was last evaluated against an external credential and it matched. The timestamp is in UTC.
/// </summary>
public DateTime? LastMatched { get; set; }

/// <summary>
/// A type enum to determine how the <see cref="Criteria"/> field should be interpreted.
///
/// Think of this field as a category of federated credential, where a given type value is specific to a
/// particular identity provider (e.g. Entra ID) and specific type of credential provided by that identity
/// provider (e.g. OpenID Connect token for a service principal in the app-only flow).
///
/// The type should be one of the values defined in <see cref="FederatedCredentialType"/>.
///
/// This column name ends in "Key" to allow a future where this is normalized into its own table, much like
/// <see cref="Package.PackageStatusKey"/> and <see cref="Package.SemVerLevelKey"/>.
/// </summary>
[Required]
[Column("TypeKey")]
public FederatedCredentialType Type { get; set; }

/// <summary>
/// A JSON object used to evaluate whether a token matches a user-provided pattern. Some criteria may be
/// implied by the <see cref="System.Type"/> and may not be explicitly states in this field. Only criteria that varies
/// from user to user or between individual cases should be expressed in this JSON object.
///
/// The maximum length is defined by the application and has an unbounded length in the database.
/// </summary>
[Required]
public string Criteria { get; set; }

/// <summary>
/// The key of the user that created this policy. If this policy was created by a site admin, this key will
/// point to the user record that the site admin was acting on behalf of, not the site admin themselves.
/// </summary>
public int CreatedByUserKey { get; set; }

/// <summary>
/// The key of the owner user or owner organization that this policy will create API keys scoped for. This is
/// the user or organization that the user referred to by <see cref="UserKey"/> is acting on behalf of. This
/// value will be used in resulting short-lived API keys in the <see cref="Scope.OwnerKey"/> value.
/// </summary>
public int PackageOwnerUserKey { get; set; }

/// <summary>
/// The navigation property for the user that created the policy, defined by <see cref="CreatedByUserKey"/>.
/// </summary>
public virtual User CreatedBy { get; set; }

/// <summary>
/// The navigation property for the package owner user, defined by <see cref="PackageOwnerUserKey"/>.
/// </summary>
public virtual User PackageOwner { get; set; }

/// <summary>
/// The navigation property for the credentials that were allowed and created by this policy.
/// </summary>
public virtual ICollection<Credential> Credentials { get; set; }
}
}
23 changes: 23 additions & 0 deletions src/NuGet.Services.Entities/FederatedCredentialType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

namespace NuGet.Services.Entities
{
/// <summary>
/// The types of federated credentials criteria that can be used to validate external credentials. This
/// enum is used for the <see cref="FederatedCredentialPolicy.Type"/> property.
/// </summary>
public enum FederatedCredentialType
{
/// <summary>
/// This credential type applies to Microsoft Entra ID OpenID Connect (OIDC) tokens, issued for a specific
/// service principal. The service principal is identified by a tenant (directory ID) and an object ID (object
/// ID). The application (client) ID is not used because the object ID uniquely identifies the service principal
/// within the tenant. An object ID is required to show the service principal is provisioned within the tenant.
///
/// Additional validation is done on the token claims which are the same for all Entra ID tokens, such as
/// subject and expiration claims.
/// </summary>
EntraIdServicePrincipal = 1,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading.Tasks;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Validators;

#nullable enable

namespace NuGetGallery.Services.Authentication
{
/// <summary>
/// This interface is used to ensure a given token is issued by Entra ID.
/// </summary>
public interface IEntraIdTokenValidator
{
/// <summary>
/// Perform minimal validation of the token to ensure it was issued by Entra ID. Validations:
/// - Expected issuer (Entra ID)
/// - Expected audience
/// - Valid signature
/// - Not expired
/// </summary>
/// <param name="token">The parsed JWT</param>
/// <returns>The token validation result, check the <see cref="TokenValidationResult.IsValid"/> for success or failure.</returns>
Task<TokenValidationResult> ValidateAsync(JsonWebToken token);
}

public class EntraIdTokenValidator : IEntraIdTokenValidator
{
private static string EntraIdAuthority { get; } = "https://login.microsoftonline.com/common/v2.0";
public static string MetadataAddress { get; } = $"{EntraIdAuthority}/.well-known/openid-configuration";

private readonly ConfigurationManager<OpenIdConnectConfiguration> _oidcConfigManager;
private readonly JsonWebTokenHandler _jsonWebTokenHandler;
private readonly IFederatedCredentialConfiguration _configuration;

public EntraIdTokenValidator(
ConfigurationManager<OpenIdConnectConfiguration> oidcConfigManager,
JsonWebTokenHandler jsonWebTokenHandler,
IFederatedCredentialConfiguration configuration)
{
_oidcConfigManager = oidcConfigManager ?? throw new ArgumentNullException(nameof(oidcConfigManager));
_jsonWebTokenHandler = jsonWebTokenHandler ?? throw new ArgumentNullException(nameof(jsonWebTokenHandler));
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}

public async Task<TokenValidationResult> ValidateAsync(JsonWebToken token)
{
if (string.IsNullOrWhiteSpace(_configuration.EntraIdAudience))
{
throw new InvalidOperationException("Unable to validate Entra ID token. Entra ID audience is not configured.");
}

var tokenValidationParameters = new TokenValidationParameters
{
IssuerValidator = AadIssuerValidator.GetAadIssuerValidator(EntraIdAuthority).Validate,
ValidAudience = _configuration.EntraIdAudience,
ConfigurationManager = _oidcConfigManager,
};

tokenValidationParameters.EnableAadSigningKeyIssuerValidation();

var result = await _jsonWebTokenHandler.ValidateTokenAsync(token, tokenValidationParameters);

return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

namespace NuGetGallery.Services.Authentication
{
public interface IFederatedCredentialConfiguration
{
/// <summary>
/// The expected audience for the incoming token. This is the "aud" claim and should be specific to the gallery
/// service itself (not shared between multiple services). This is used only for Entra ID token validation.
/// </summary>
string? EntraIdAudience { get; }
}

public class FederatedCredentialConfiguration : IFederatedCredentialConfiguration
{
public string? EntraIdAudience { get; set; }
}
}
15 changes: 13 additions & 2 deletions src/NuGetGallery.Services/Configuration/ConfigurationService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
Expand All @@ -14,6 +14,7 @@
using NuGet.Services.Configuration;
using NuGet.Services.KeyVault;
using NuGetGallery.Configuration.SecretReader;
using NuGetGallery.Services.Authentication;

namespace NuGetGallery.Configuration
{
Expand All @@ -23,6 +24,7 @@ public class ConfigurationService : IGalleryConfigurationService, IConfiguration
protected const string FeaturePrefix = "Feature.";
protected const string ServiceBusPrefix = "AzureServiceBus.";
protected const string PackageDeletePrefix = "PackageDelete.";
protected const string FederatedCredentialPrefix = "FederatedCredential.";

private readonly Lazy<string> _httpSiteRootThunk;
private readonly Lazy<string> _httpsSiteRootThunk;
Expand All @@ -31,6 +33,7 @@ public class ConfigurationService : IGalleryConfigurationService, IConfiguration
private readonly Lazy<FeatureConfiguration> _lazyFeatureConfiguration;
private readonly Lazy<IServiceBusConfiguration> _lazyServiceBusConfiguration;
private readonly Lazy<IPackageDeleteConfiguration> _lazyPackageDeleteConfiguration;
private readonly Lazy<FederatedCredentialConfiguration> _lazyFederatedCredentialConfiguration;

private static readonly HashSet<string> NotInjectedSettingNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {
SettingPrefix + "SqlServer",
Expand Down Expand Up @@ -66,6 +69,7 @@ public ConfigurationService()
_lazyFeatureConfiguration = new Lazy<FeatureConfiguration>(() => ResolveFeatures().Result);
_lazyServiceBusConfiguration = new Lazy<IServiceBusConfiguration>(() => ResolveServiceBus().Result);
_lazyPackageDeleteConfiguration = new Lazy<IPackageDeleteConfiguration>(() => ResolvePackageDelete().Result);
_lazyFederatedCredentialConfiguration = new Lazy<FederatedCredentialConfiguration>(() => ResolveFederatedCredential().Result);
}

public static IEnumerable<PropertyDescriptor> GetConfigProperties<T>(T instance)
Expand All @@ -81,6 +85,8 @@ public static IEnumerable<PropertyDescriptor> GetConfigProperties<T>(T instance)

public IPackageDeleteConfiguration PackageDelete => _lazyPackageDeleteConfiguration.Value;

public FederatedCredentialConfiguration FederatedCredential => _lazyFederatedCredentialConfiguration.Value;

/// <summary>
/// Gets the site root using the specified protocol
/// </summary>
Expand Down Expand Up @@ -206,6 +212,11 @@ private async Task<IPackageDeleteConfiguration> ResolvePackageDelete()
return await ResolveConfigObject(new PackageDeleteConfiguration(), PackageDeletePrefix);
}

private async Task<FederatedCredentialConfiguration> ResolveFederatedCredential()
{
return await ResolveConfigObject(new FederatedCredentialConfiguration(), FederatedCredentialPrefix);
}

protected virtual string GetAppSetting(string settingName)
{
return WebConfigurationManager.AppSettings[settingName];
Expand Down Expand Up @@ -273,4 +284,4 @@ private void CheckValidSiteRoot(string siteRoot)
}
}
}
}
}
Loading