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

Unify /me as the endpoint for sponsor manifests #217

Merged
merged 1 commit into from
Jun 8, 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
13 changes: 13 additions & 0 deletions src/Web/SponsorLink.Legacy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;

partial class SponsorLink
{
/// <summary>
/// Backwards compatibility for pre-beta endpoint.
/// </summary>
[Function("sync")]
public IActionResult LegacySyncAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "sync")] HttpRequest req)
=> new RedirectResult("me", true, true);
}
22 changes: 13 additions & 9 deletions src/Web/SponsorLink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;

namespace Devlooped.Sponsors;

/// <summary>
/// Returns a JWT or JSON manifest of the authenticated user's claims.
/// </summary>
class SponsorLink(IConfiguration configuration, IHttpClientFactory httpFactory, SponsorsManager sponsors, RSA rsa, IOptions<SponsorLinkOptions> options, IWebHostEnvironment host, ILogger<SponsorLink> logger)
partial class SponsorLink(IConfiguration configuration, IHttpClientFactory httpFactory, SponsorsManager sponsors, RSA rsa, IOptions<SponsorLinkOptions> options, IWebHostEnvironment host, ILogger<SponsorLink> logger)
{
SponsorLinkOptions options = options.Value;

[Function("me")]
/// <summary>
/// Helper to visualize the user's claims and the request/response headers as available to the backend.
/// </summary>
[Function("user")]
public async Task<IActionResult> UserAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{
if (!configuration.TryGetClientId(logger, out var clientId))
Expand Down Expand Up @@ -98,8 +102,8 @@ public async Task<IActionResult> GetStatus([HttpTrigger(AuthorizationLevel.Anony
/// <summary>
/// Depending on the Accept header, returns a JWT or JSON manifest of the authenticated user's claims.
/// </summary>
[Function("sync")]
public async Task<IActionResult> SyncAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "sync")] HttpRequest req)
[Function("me-get")]
public async Task<IActionResult> SyncAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "me")] HttpRequest req)
{
if (!configuration.TryGetClientId(logger, out var clientId))
return new StatusCodeResult(500);
Expand All @@ -110,7 +114,7 @@ public async Task<IActionResult> SyncAsync([HttpTrigger(AuthorizationLevel.Anony
// or the token-based principal population won't work.
// Never redirect requests for JWT, as they are likely from a CLI or other non-browser client.
if (!req.Headers.Accept.Contains("application/jwt") && !string.IsNullOrEmpty(clientId))
return new RedirectResult($"https://github.com/login/oauth/authorize?client_id={clientId}&scope=read:user%20read:org%20user:email&redirect_uri=https://{req.Headers["Host"]}/.auth/login/github/callback&state=redir=/sync");
return new RedirectResult($"https://github.com/login/oauth/authorize?client_id={clientId}&scope=read:user%20read:org%20user:email&redirect_uri=https://{req.Headers["Host"]}/.auth/login/github/callback&state=redir=/me");

logger.LogError("Ensure GitHub identity provider is configured for the functions app.");

Expand Down Expand Up @@ -171,13 +175,13 @@ public async Task<IActionResult> SyncAsync([HttpTrigger(AuthorizationLevel.Anony
};
}

[Function("delete")]
public IActionResult Delete([HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "sync")] HttpRequest req)
[Function("me-delete")]
public IActionResult Delete([HttpTrigger(AuthorizationLevel.Anonymous, "delete", Route = "me")] HttpRequest req)
{
if (!configuration.TryGetClientId(logger, out var clientId))
if (!configuration.TryGetClientId(logger, out _))
return new StatusCodeResult(500);

if (ClaimsPrincipal.Current is not { Identity.IsAuthenticated: true } principal)
if (ClaimsPrincipal.Current is not { Identity.IsAuthenticated: true })
return new UnauthorizedResult();

logger.LogInformation("We don't persist anything, so there's nothing to delete :)");
Expand Down
36 changes: 1 addition & 35 deletions src/Web/Version.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ public async Task<IActionResult> GetStatus([HttpTrigger(AuthorizationLevel.Anony
var json = jwt.Payload.SerializeToJson();
var doc = JsonDocument.Parse(json);

var pubKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
if (pubKey != manifest.PublicKey)
if (!rsa.ThumbprintEquals(manifest.SecurityKey))
{
logger.LogError($"Configured private key 'SponsorLink:{nameof(SponsorLinkOptions.PrivateKey)}' does not match the manifest public key.");
return new StatusCodeResult(500);
Expand Down Expand Up @@ -59,37 +58,4 @@ public IActionResult GetVersion([HttpTrigger(AuthorizationLevel.Anonymous, "get"
StatusCode = 200,
};
}

[Function("pub")]
public IActionResult GetPublicKey([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
{
if (options.PublicKey is not { Length: > 0 } key)
{
logger.LogError($"Missing required configuration 'SponsorLink:{nameof(SponsorLinkOptions.PublicKey)}'");
return new StatusCodeResult(500);
}

if (req.GetTypedHeaders().Accept?.Any(x =>
x.MediaType == "application/json" ||
x.MediaType == "application/jwk+json") == true)
{
var rsa = RSA.Create();
rsa.ImportRSAPublicKey(Convert.FromBase64String(key), out _);
return new ContentResult
{
Content = JsonSerializer.Serialize(
JsonWebKeyConverter.ConvertFromRSASecurityKey(new RsaSecurityKey(rsa.ExportParameters(false))),
JsonOptions.JsonWebKey),
ContentType = "application/jwk+json",
StatusCode = 200,
};
}

return new ContentResult
{
Content = key,
ContentType = "text/plain",
StatusCode = 200,
};
}
}
Loading