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

Private feed implementation #156

Merged
merged 35 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b29393e
Private feed implementation
tomasfil Jun 17, 2024
3967c2b
case insensitive username
tomasfil Jun 17, 2024
42908d3
Cleanup
tomasfil Jun 17, 2024
50e8448
Cleanup
tomasfil Jun 17, 2024
0c596a6
Implement multi auth
tomasfil Jun 18, 2024
0871aa2
Prevent null exception.
tomasfil Jun 18, 2024
2452fa2
Requested changes
tomasfil Jun 19, 2024
66d0016
fix comment
tomasfil Jun 19, 2024
0faf0e1
Documentation
tomasfil Jun 19, 2024
19ce37b
Update config docs
tomasfil Jun 19, 2024
b01e8e8
Update apikeys docs
tomasfil Jun 19, 2024
4c7c179
feedback impl
tomasfil Jun 27, 2024
9ff7a0a
Tests
tomasfil Jun 27, 2024
2685f4c
Integration tests for basic authentication
tomasfil Jun 27, 2024
b2e467e
CodeMaid
tomasfil Jun 27, 2024
8af4c64
Remove redundant call
tomasfil Jun 27, 2024
ddfac3e
Move auth handler and tests
tomasfil Jun 27, 2024
1463c75
ApiKey to object
tomasfil Jun 27, 2024
15319b2
Move auth config to extend BaGetterApplication
tomasfil Jun 27, 2024
13b9312
Merge branch 'main' into main
tomasfil Jun 27, 2024
31b2b22
Api keys move into auth
tomasfil Jun 28, 2024
1ba8428
docs
tomasfil Jun 28, 2024
351b491
formatting
tomasfil Jun 28, 2024
818c578
VS Package manager console guide
tomasfil Jul 1, 2024
6379f24
remove extra space
tomasfil Jul 1, 2024
c9fef19
Unify line style
tomasfil Jul 1, 2024
6e4bf54
Allow auth policies config
tomasfil Sep 5, 2024
d7eb09b
Using
tomasfil Sep 5, 2024
b1d06d9
Move auth config to Bagetter project
tomasfil Sep 5, 2024
cb65543
Remove sealed class
tomasfil Sep 5, 2024
901d731
Auth options comments
tomasfil Sep 5, 2024
075db6d
Fix tests
tomasfil Sep 5, 2024
6b63d79
Merge branch 'main' into main
seriouz Sep 7, 2024
59071b9
Fix build
tomasfil Oct 21, 2024
1a3b3ea
Merge branch 'main' into main
tomasfil Oct 21, 2024
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
Expand All @@ -8,12 +9,14 @@ namespace BaGetter.Core;
public class ApiKeyAuthenticationService : IAuthenticationService
{
private readonly string _apiKey;
private readonly string[] _apiKeys;

public ApiKeyAuthenticationService(IOptionsSnapshot<BaGetterOptions> options)
{
ArgumentNullException.ThrowIfNull(options);

_apiKey = string.IsNullOrEmpty(options.Value.ApiKey) ? null : options.Value.ApiKey;
_apiKeys = options.Value.ApiKeys;
}

public Task<bool> AuthenticateAsync(string apiKey, CancellationToken cancellationToken)
Expand All @@ -22,8 +25,8 @@ public Task<bool> AuthenticateAsync(string apiKey, CancellationToken cancellatio
private bool Authenticate(string apiKey)
{
// No authentication is necessary if there is no required API key.
if (_apiKey == null) return true;
if (_apiKey == null && (_apiKeys is null || _apiKeys.Length==0)) return true;

return _apiKey == apiKey;
return _apiKey == apiKey || _apiKeys?.Contains(apiKey) == true;
}
}
12 changes: 12 additions & 0 deletions src/BaGetter.Core/Authentication/AuthenticationConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace BaGetter.Authentication;
public static class AuthenticationConstants
{
public const string NugetBasicAuthenticationScheme = "NugetBasicAuthentication";
public const string NugetUserPolicy = "NuGetUserPolicy";
}
8 changes: 8 additions & 0 deletions src/BaGetter.Core/Configuration/AuthenticationOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BaGetter.Core;

public class AuthenticationOptions
{
public string Username { get; set; }

public string Password { get; set; }
Regenhardt marked this conversation as resolved.
Show resolved Hide resolved
}
8 changes: 8 additions & 0 deletions src/BaGetter.Core/Configuration/BaGetterOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ public class BaGetterOptions
/// </summary>
public string ApiKey { get; set; }

/// <summary>
/// The API Key required to authenticate package
/// operations. If empty, package operations do not require authentication.
/// </summary>
public string[] ApiKeys { get; set; }
Regenhardt marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// The application root URL for usage in reverse proxy scenarios.
/// </summary>
Expand Down Expand Up @@ -58,4 +64,6 @@ public class BaGetterOptions
public HealthCheckOptions HealthCheck { get; set; }

public StatisticsOptions Statistics { get; set; }

public AuthenticationOptions[] Authentication { get; set; }
tomasfil marked this conversation as resolved.
Show resolved Hide resolved
}
4 changes: 4 additions & 0 deletions src/BaGetter.Web/Controllers/PackageContentController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Protocol.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NuGet.Versioning;

Expand All @@ -12,6 +14,8 @@ namespace BaGetter.Web;
/// The Package Content resource, used to download content from packages.
/// See: https://docs.microsoft.com/nuget/api/package-base-address-resource
/// </summary>

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
public class PackageContentController : Controller
{
private readonly IPackageContentService _content;
Expand Down
4 changes: 4 additions & 0 deletions src/BaGetter.Web/Controllers/PackageMetadataController.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Protocol.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NuGet.Versioning;

Expand All @@ -12,6 +14,8 @@ namespace BaGetter.Web;
/// The Package Metadata resource, used to fetch packages' information.
/// See: https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
/// </summary>

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
public class PackageMetadataController : Controller
{
private readonly IPackageMetadataService _metadata;
Expand Down
3 changes: 3 additions & 0 deletions src/BaGetter.Web/Controllers/SearchController.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Protocol.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BaGetter.Web;

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
public class SearchController : Controller
{
private readonly ISearchService _searchService;
Expand Down
4 changes: 4 additions & 0 deletions src/BaGetter.Web/Controllers/ServiceIndexController.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Protocol.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BaGetter.Web;

/// <summary>
/// The NuGet Service Index. This aids NuGet client to discover this server's services.
/// </summary>

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
tomasfil marked this conversation as resolved.
Show resolved Hide resolved
public class ServiceIndexController : Controller
{
private readonly IServiceIndexService _serviceIndex;
Expand Down
3 changes: 3 additions & 0 deletions src/BaGetter.Web/Controllers/SymbolController.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BaGetter.Authentication;
using BaGetter.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace BaGetter.Web;

[Authorize(AuthenticationSchemes = AuthenticationConstants.NugetBasicAuthenticationScheme, Policy = AuthenticationConstants.NugetUserPolicy)]
public class SymbolController : Controller
{
private readonly IAuthenticationService _authentication;
Expand Down
91 changes: 91 additions & 0 deletions src/BaGetter/Authentication/NugetBasicAuthenticationHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Azure.Core;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text;
using System.Threading.Tasks;
using System;
using BaGetter.Core;
using System.Linq;

namespace BaGetter.Web.Authentication;

public class NugetBasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IOptions<BaGetterOptions> bagetterOptions;

public NugetBasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IOptions<BaGetterOptions> bagetterOptions)
: base(options, logger, encoder)
{
this.bagetterOptions = bagetterOptions;
}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (bagetterOptions.Value.Authentication is null || bagetterOptions.Value.Authentication.Length==0 || bagetterOptions.Value.Authentication.All(a=> string.IsNullOrWhiteSpace(a.Username) && string.IsNullOrWhiteSpace(a.Password)))
{
var claims = new[]
{
new Claim(ClaimTypes.Anonymous, string.Empty),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);

var ticket = new AuthenticationTicket(principal, Scheme.Name);

return Task.FromResult(AuthenticateResult.Success(ticket));
tomasfil marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
if (!Request.Headers.TryGetValue("Authorization", out var auth))
return Task.FromResult(AuthenticateResult.NoResult());

string username = null;
string password = null;
try
{
var authHeader = AuthenticationHeaderValue.Parse(auth);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split([':'], 2);
username = credentials[0];
password = credentials[1];
Regenhardt marked this conversation as resolved.
Show resolved Hide resolved
}
catch
{
return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
}

if (!ValidateCredentials(username, password))
return Task.FromResult(AuthenticateResult.Fail("Invalid Username or Password"));

var claims = new[]
{
new Claim(ClaimTypes.Name, username),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);

var ticket = new AuthenticationTicket(principal, Scheme.Name);

return Task.FromResult(AuthenticateResult.Success(ticket));
tomasfil marked this conversation as resolved.
Show resolved Hide resolved
}
}

protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers.WWWAuthenticate = "Basic realm=\"NuGet Server\"";
await base.HandleChallengeAsync(properties);
}

private bool ValidateCredentials(string username, string password)
{
return bagetterOptions.Value.Authentication.Any(a=> a.Username.Equals(username, StringComparison.OrdinalIgnoreCase) && a.Password == password);
}
}
18 changes: 17 additions & 1 deletion src/BaGetter/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System;
using BaGetter.Authentication;
using BaGetter.Core;
using BaGetter.Core.Extensions;
using BaGetter.Web;
using BaGetter.Web.Authentication;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation;
Expand All @@ -27,6 +30,16 @@ public void ConfigureServices(IServiceCollection services)
services.ConfigureOptions<ValidateBaGetterOptions>();
services.ConfigureOptions<ConfigureBaGetterServer>();

services.AddAuthentication(AuthenticationConstants.NugetBasicAuthenticationScheme)
.AddScheme<AuthenticationSchemeOptions, NugetBasicAuthenticationHandler>(AuthenticationConstants.NugetBasicAuthenticationScheme, null);
services.AddAuthorization(options =>
{
options.AddPolicy(AuthenticationConstants.NugetUserPolicy, policy =>
{
policy.RequireAuthenticatedUser();
});
});

services.AddBaGetterOptions<IISServerOptions>(nameof(IISServerOptions));
services.AddBaGetterWebApplication(ConfigureBaGetterApplication);

Expand Down Expand Up @@ -81,9 +94,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UsePathBase(options.PathBase);

app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();

app.UseCors(ConfigureBaGetterServer.CorsPolicy);

app.UseOperationCancelledMiddleware();

app.UseEndpoints(endpoints =>
Expand Down
11 changes: 9 additions & 2 deletions src/BaGetter/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,18 @@
},

"HealthCheck": {
"Path" : "/health"
"Path": "/health"
},

"Statistics": {
"EnableStatisticsPage": true,
"ListConfiguredServices": true
}
},

//"Authentication": [
// {
// "Username": "test",
// "Password": "testPwd"
// }
//]
}