Skip to content

Commit

Permalink
Add signing function and update deployment dir
Browse files Browse the repository at this point in the history
This was previously ignored from a glob too.

Fixes #31
  • Loading branch information
kzu committed Aug 23, 2023
1 parent 050765e commit 3d6b8e0
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: |
dotnet build -m:1
dotnet build ./samples/dotnet/
dotnet build src/App --output ./output
dotnet build src/App --output ./app
- name: 🧪 test
run: dotnet test --filter SponsorLink=true
Expand All @@ -48,7 +48,7 @@ jobs:
uses: Azure/functions-action@v1
with:
app-name: ${{ vars.AZURE_APPNAME }}
package: ./output
package: ./app

- name: 🚀 sleet
env:
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/app
bin
app
obj
artifacts
pack
Expand Down
73 changes: 73 additions & 0 deletions src/App/Auth0.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http.Headers;
using System.Security.Claims;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

namespace Devlooped.SponsorLink;

static class Auth0
{
static readonly IConfigurationManager<OpenIdConnectConfiguration> configuration;

static readonly string Issuer = "https://sponsorlink.us.auth0.com/";
static readonly string Audience = "https://sponsorlink.devlooped.com";

static Auth0()
{
var documentRetriever = new HttpDocumentRetriever { RequireHttps = true };

configuration = new ConfigurationManager<OpenIdConnectConfiguration>(
$"{Issuer}.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever(),
documentRetriever
);
}

public static async Task<ClaimsPrincipal?> ValidateTokenAsync(AuthenticationHeaderValue value)
{
if (value?.Scheme != "Bearer")
return null;

var config = await configuration.GetConfigurationAsync(CancellationToken.None);

var validationParameter = new TokenValidationParameters
{
RequireSignedTokens = true,
ValidAudience = Audience,
ValidateAudience = true,
ValidIssuer = Issuer,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
IssuerSigningKeys = config.SigningKeys
};

ClaimsPrincipal? result = default;
var tries = 0;

while (result == null && tries <= 1)
{
try
{
var handler = new JwtSecurityTokenHandler();
return handler.ValidateToken(value.Parameter, validationParameter, out var _);
}
catch (SecurityTokenSignatureKeyNotFoundException)
{
// This exception is thrown if the signature key of the JWT could not be found.
// This could be the case when the issuer changed its signing keys, so we trigger a
// refresh and retry validation.
configuration.RequestRefresh();
tries++;
}
catch (SecurityTokenException)
{
return null;
}
}

return result;
}
}
85 changes: 85 additions & 0 deletions src/App/Signing.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Web.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;

namespace Devlooped.SponsorLink;

public static class Signing
{
static RSA? rsa;

[FunctionName("sign")]
public static async Task<IActionResult> RunAsync(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "sign")] HttpRequestMessage req, IConfiguration configuration, ILogger logger)
{
rsa ??= InitializeKey(configuration, logger);
if (rsa == null)
return new InternalServerErrorResult();

if (req.Headers.Authorization is null ||
await Auth0.ValidateTokenAsync(req.Headers.Authorization) is not ClaimsPrincipal principal ||
principal.FindFirst(ClaimTypes.NameIdentifier)?.Value.Split('|')?[1] is not string id)
return new UnauthorizedResult();

var validation = new TokenValidationParameters
{
RequireExpirationTime = true,
ValidAudience = "SponsorLink",
ValidIssuer = "Devlooped",
ValidateIssuerSigningKey = false,
};

if (req.Content == null)
return new BadRequestResult();

var jwt = new JwtSecurityTokenHandler().ReadJwtToken(await req.Content.ReadAsStringAsync());
if (jwt.Issuer != "Devlooped" || jwt.Audiences.FirstOrDefault() != "SponsorLink")
return new BadRequestResult();

// "sub" claim must match between token claims and principal
if (jwt.Claims.FirstOrDefault(c => c.Type == "sub")?.Value != id)
return new BadRequestResult();

var signing = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);

var signed = new JwtSecurityToken(
issuer: jwt.Issuer,
audience: jwt.Audiences.First(),
claims: jwt.Claims,
// Expire at the end of the month
expires: new DateTime(DateTime.Today.Year, DateTime.Today.Month + 1, 1, 0, 0, 0, DateTimeKind.Utc),
signingCredentials: signing);

// Serialize the token and return as a string
var body = new JwtSecurityTokenHandler().WriteToken(signed);

return new ContentResult
{
Content = body,
ContentType = "text/plain",
StatusCode = (int)HttpStatusCode.OK,
};
}

static RSA? InitializeKey(IConfiguration configuration, ILogger logger)
{
if (configuration["SPONSORLINK_KEY"] is not string key)
{
logger.LogError("Missing SPONSORLINK_KEY configuration");
return null;
}

var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(key), out _);

return rsa;
}
}
12 changes: 12 additions & 0 deletions src/App/Startup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;

[assembly: FunctionsStartup(typeof(Startup))]

public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder) { }

public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
=> builder.ConfigurationBuilder.AddUserSecrets(ThisAssembly.Project.UserSecretsId);
}

0 comments on commit 3d6b8e0

Please sign in to comment.