Skip to content
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
5 changes: 0 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
# This example is for local development without Azure
DISABLE_REDIS=true
SECRET_Auth0Issuer=
SECRET_Auth0Audience=
SECRET_Auth0AdminClientId=
SECRET_Auth0AdminClientId=
SECRET_Auth0AdminClientSecret=
SECRET_FRCApiKey="Basic KEY"
SECRET_FRCCurrentSeason=2025
SECRET_MailchimpAPIKey=
Expand Down
5 changes: 2 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ GATool API is a .NET 10 API service that provides game announcer data for FIRST
```

### Secrets Management
- Secrets are stored in **AWS Secrets Manager** with plain names (no prefix): `Auth0Issuer`, `FRCApiKey`, `TBAApiKey`, etc.
- Secrets are stored in **AWS Secrets Manager** with plain names (no prefix): `FRCApiKey`, `TBAApiKey`, etc.
- `Program.cs` preloads all secrets at startup via `AwsSecretProvider.PreloadSecretsAsync()`
- `GetSecret()` and `GetSecretAsync()` calls throughout the codebase use these plain names
- `NEW_RELIC_LICENSE_KEY` is injected as an ECS container secret (from Secrets Manager → env var), not loaded by the app
Expand Down Expand Up @@ -119,7 +119,7 @@ aws ecs describe-services --cluster gatool --services gatool-api --profile gatoo
## Common Patterns

### Authentication/Authorization
- JWT-based auth using Auth0
- JWT-based auth using the in-house `GatoolJwt` bearer scheme (see `Program.cs`)
- Role-based access control via policies:
- "user": Basic access
- "admin": Administrative functions
Expand Down Expand Up @@ -151,7 +151,6 @@ public async Task<IActionResult> GetExample()
- The Blue Alliance API
- Statbotics.io
- FTC Scout
- Auth0 (authentication)
- AWS Secrets Manager (secrets)
- AWS S3 (storage for user data, high scores, team updates)
- Redis (caching, runs as ECS sidecar)
Expand Down
191 changes: 0 additions & 191 deletions Jobs/BackfillUsersFromAuth0Job.cs

This file was deleted.

6 changes: 1 addition & 5 deletions LOCAL_DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,6 @@ The application retrieves these secrets from AWS Secrets Manager (prefix: `gatoo

| Secret Name | Description | Required For |
|-------------------------------|-----------------------------|-------------------------|
| `gatool/Auth0Issuer` | Auth0 domain URL | Authentication |
| `gatool/Auth0Audience` | Auth0 API audience | Authentication |
| `gatool/FRCApiKey` | FIRST API key | FRC data endpoints |
| `gatool/TBAApiKey` | The Blue Alliance API key | TBA/offseason endpoints |
| `gatool/FTCApiKey` | FTC API key | FTC data endpoints |
Expand All @@ -109,8 +107,6 @@ The application retrieves these secrets from AWS Secrets Manager (prefix: `gatoo
| `gatool/MailChimpAPIKey` | MailChimp API key | User sync |
| `gatool/MailchimpAPIURL` | MailChimp API URL | User sync |
| `gatool/MailchimpListID` | MailChimp list ID | User sync |
| `gatool/Auth0AdminClientId` | Auth0 Management client ID | User sync |
| `gatool/Auth0AdminClientSecret` | Auth0 Management secret | User sync |
| `gatool/NewRelicLicenseKey` | New Relic license key | Monitoring |

To create all secrets at once, use the provided script:
Expand All @@ -121,7 +117,7 @@ AWS_REGION=us-east-2 ./scripts/create-secrets.sh

## Testing Without Authentication

Some endpoints require authentication. To test without setting up Auth0:
Some endpoints require authentication. To test without a signed-in user:

### Option 1: Test Public Endpoints

Expand Down
51 changes: 4 additions & 47 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.IdentityModel.Tokens;
using NewRelic.LogEnrichers.Serilog;
using NSwag;
using NSwag.Generation.Processors.Security;
Expand Down Expand Up @@ -47,7 +46,6 @@
var smClient = new AmazonSecretsManagerClient();
var secretNames = new[]
{
"Auth0Issuer", "Auth0Audience", // kept during migration window for legacy token validation
"FRCApiKey", "TBAApiKey", "FTCApiKey", "CasterstoolApiKey", "TOAApiKey",
"FRCCurrentSeason", "FTCCurrentSeason",
"MailChimpAPIKey", "MailchimpAPIURL", "MailchimpListID",
Expand All @@ -73,8 +71,7 @@

builder.Services.AddAuthentication(options =>
{
// Default scheme is our self-issued ES256 JWT. We also keep the Auth0 scheme
// around during the migration window (see AuthenticationSchemes on policies).
// Self-issued ES256 JWT is the only accepted scheme.
options.DefaultAuthenticateScheme = "GatoolJwt";
options.DefaultChallengeScheme = "GatoolJwt";
})
Expand All @@ -84,39 +81,6 @@
// Configured asynchronously below once the DI container is built.
options.RequireHttpsMetadata = false;
options.SaveToken = true;
// During the Auth0 migration window we accept tokens from two issuers.
// Without forwarding, every request would be tried against BOTH schemes,
// producing noisy "kid not found" log entries from the scheme that doesn't
// own the token. Forward to the Auth0 scheme when we see an Auth0-issued
// token so each token is validated exactly once by the right handler.
options.ForwardDefaultSelector = ctx =>
{
var auth = ctx.Request.Headers.Authorization.ToString();
if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
return null;
var token = auth["Bearer ".Length..].Trim();
var dot1 = token.IndexOf('.');
var dot2 = dot1 < 0 ? -1 : token.IndexOf('.', dot1 + 1);
if (dot2 < 0) return null;
try
{
var payload = token.Substring(dot1 + 1, dot2 - dot1 - 1);
var json = System.Text.Encoding.UTF8.GetString(Base64UrlEncoder.DecodeBytes(payload));
using var doc = System.Text.Json.JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("iss", out var iss))
{
var issuer = iss.GetString();
var auth0Issuer = ctx.RequestServices.GetRequiredService<ISecretProvider>().GetSecret("Auth0Issuer");
if (!string.IsNullOrEmpty(issuer) && !string.IsNullOrEmpty(auth0Issuer) &&
issuer.TrimEnd('/').Equals(auth0Issuer.TrimEnd('/'), StringComparison.OrdinalIgnoreCase))
{
return "Auth0";
}
}
}
catch { /* malformed token — let the default scheme reject it */ }
return null;
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = async ctx =>
Expand All @@ -130,23 +94,17 @@
}
}
};
})
.AddJwtBearer("Auth0", options =>
{
// Legacy Auth0 tokens — accepted during migration window.
options.Authority = secretProvider.GetSecret("Auth0Issuer");
options.Audience = secretProvider.GetSecret("Auth0Audience");
});

builder.Services.AddAuthorizationBuilder()
.AddPolicy("user", policy =>
{
policy.AuthenticationSchemes = ["GatoolJwt", "Auth0"];
policy.AuthenticationSchemes = ["GatoolJwt"];
policy.Requirements.Add(new HasRoleRequirement("user"));
})
.AddPolicy("admin", policy =>
{
policy.AuthenticationSchemes = ["GatoolJwt", "Auth0"];
policy.AuthenticationSchemes = ["GatoolJwt"];
policy.Requirements.Add(new HasRoleRequirement("admin"));
});
builder.Services.AddSingleton<IAuthorizationHandler, HasRoleHandler>();
Expand All @@ -157,7 +115,7 @@
builder.Services.AddAWSService<IAmazonDynamoDB>();
builder.Services.AddAWSService<IAmazonSimpleEmailServiceV2>();

// Custom auth services (email OTP + WebAuthn passkeys, replaces Auth0)
// Custom auth services (email OTP + WebAuthn passkeys)
builder.Services.AddSingleton<AuthSigningKeyProvider>();
builder.Services.AddSingleton<OtpPepperProvider>();
builder.Services.AddSingleton<AuthRepository>();
Expand Down Expand Up @@ -332,7 +290,6 @@
// Register job services
builder.Services.AddScoped<JobRunnerService>();
builder.Services.AddScoped<UpdateGlobalHighScoresJob>();
builder.Services.AddScoped<BackfillUsersFromAuth0Job>();

var app = builder.Build();

Expand Down
3 changes: 1 addition & 2 deletions Services/JobRunnerService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ public class JobRunnerService(IServiceProvider serviceProvider, ILogger<JobRunne
{
private readonly Dictionary<string, Type> _availableJobs = new()
{
{ "UpdateGlobalHighScores", typeof(UpdateGlobalHighScoresJob) },
{ "BackfillUsersFromAuth0", typeof(BackfillUsersFromAuth0Job) }
{ "UpdateGlobalHighScores", typeof(UpdateGlobalHighScoresJob) }
};

public async Task RunJobAsync(string jobName, CancellationToken cancellationToken = default)
Expand Down
Loading
Loading