Skip to content

Commit d392cdf

Browse files
tvidobrac
andcommitted
Add OAuth JWT auth for dashboard API
Dashboard and API auth were tied to Supabase-specific HMAC JWT configuration (`SUPABASE_JWT_SECRETS`) and Supabase-specific headers. That made it hard to support other auth providers and forced provider-specific settings to spread across several environment variables. This change introduces one generic auth-provider JWT verification path that supports both OIDC-compliant issuers (asymmetric keys, JWKS auto-discovered) and non-OIDC HMAC-signed JWTs. It keeps the old Supabase headers as compatibility aliases, while adding provider-neutral bearer token and team header schemes. - Add shared auth-provider JWT verification with OIDC and bearer (HMAC) verifier strategies under `packages/auth/pkg/auth/`. - Replace API/dashboard service-level `SUPABASE_JWT_SECRETS` with one structured `AUTH_PROVIDER_CONFIG` JSON value, defaulted (in Terraform) to the existing Supabase JWT secret wrapped in a `bearer` entry. - Support multiple OIDC issuers and multiple bearer-token sources at once, useful during migrations and multi-tenant deployments. - For OIDC entries, fetch the discovery document at startup, cross-check `issuer`, and resolve `jwks_uri` from it. JWKS HTTPS is required. Lookups/caching/refresh use `github.com/MicahParks/keyfunc/v3`/`jwkset`. Fail-fast on discovery errors. - Per-entry audience matching with `MatchAny` (default) or `MatchAll`. - Per-entry `claimMappings.username.claim` (default `sub`) resolves directly to an internal UUID; no email fallback. - Wire auth-provider bearer token and `X-Team-Id` team header in both API and dashboard API, while keeping Supabase headers as compatibility aliases. Example with one OIDC issuer and one bearer source (e.g. during migration or key rotation): ```json { "jwt": [ { "issuer": { "url": "https://issuer.example.com", "discoveryURL": "https://issuer.example.com/.well-known/openid-configuration", "audiences": ["dashboard-api"], "audienceMatchPolicy": "MatchAny" }, "claimMappings": { "username": { "claim": "sub" } }, "jwksCacheDuration": "5m" } ], "bearer": [ { "hmac": { "secrets": ["legacy-secret"] }, "claimMappings": { "username": { "claim": "sub" } } } ] } ``` Co-authored-by: Jakub Dobry <dobrac@users.noreply.github.com>
1 parent ed90bd3 commit d392cdf

50 files changed

Lines changed: 2273 additions & 698 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

iac/modules/job-api/jobs/api.hcl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ job "api" {
151151
AUTH_DB_READ_REPLICA_CONNECTION_STRING = "${postgres_read_replica_connection_string}"
152152
AUTH_DB_MAX_OPEN_CONNECTIONS = "${auth_db_max_open_connections}"
153153
AUTH_DB_MIN_IDLE_CONNECTIONS = "${auth_db_min_idle_connections}"
154-
SUPABASE_JWT_SECRETS = "${supabase_jwt_secrets}"
155154

156155
LOKI_URL = "${loki_url}"
157156
CLICKHOUSE_CONNECTION_STRING = "${clickhouse_connection_string}"

iac/modules/job-api/main.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
locals {
22
default_job_env_vars = {
3-
GIN_MODE : "release"
3+
GIN_MODE = "release"
4+
AUTH_PROVIDER_CONFIG = jsonencode(var.auth_provider_config)
45
}
56

67
job_env_vars = merge(local.default_job_env_vars, var.job_env_vars)
@@ -27,7 +28,6 @@ resource "nomad_job" "api" {
2728
api_docker_image = var.api_docker_image
2829
postgres_connection_string = var.postgres_connection_string
2930
postgres_read_replica_connection_string = var.postgres_read_replica_connection_string
30-
supabase_jwt_secrets = var.supabase_jwt_secrets
3131
posthog_api_key = var.posthog_api_key
3232
analytics_collector_host = var.analytics_collector_host
3333
analytics_collector_api_token = var.analytics_collector_api_token

iac/modules/job-api/variables.tf

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,35 @@ variable "postgres_read_replica_connection_string" {
6767
sensitive = true
6868
}
6969

70-
variable "supabase_jwt_secrets" {
71-
type = string
70+
variable "auth_provider_config" {
71+
type = object({
72+
jwt = optional(list(object({
73+
issuer = object({
74+
url = string
75+
discoveryURL = optional(string)
76+
audiences = list(string)
77+
audienceMatchPolicy = optional(string)
78+
})
79+
claimMappings = optional(object({
80+
username = object({
81+
claim = string
82+
})
83+
}))
84+
jwksCacheDuration = optional(string)
85+
})))
86+
bearer = optional(list(object({
87+
hmac = object({
88+
secrets = list(string)
89+
})
90+
claimMappings = optional(object({
91+
username = object({
92+
claim = string
93+
})
94+
}))
95+
})))
96+
})
7297
sensitive = true
98+
default = null
7399
}
74100

75101
variable "posthog_api_key" {

iac/modules/job-dashboard-api/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ locals {
88
AUTH_DB_READ_REPLICA_CONNECTION_STRING = var.auth_db_read_replica_connection_string
99
SUPABASE_DB_CONNECTION_STRING = var.supabase_db_connection_string
1010
CLICKHOUSE_CONNECTION_STRING = var.clickhouse_connection_string
11-
SUPABASE_JWT_SECRETS = var.supabase_jwt_secrets
11+
AUTH_PROVIDER_CONFIG = jsonencode(var.auth_provider_config)
1212
REDIS_URL = var.redis_url
1313
REDIS_CLUSTER_URL = var.redis_cluster_url
1414
REDIS_TLS_CA_BASE64 = var.redis_tls_ca_base64

iac/modules/job-dashboard-api/variables.tf

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,35 @@ variable "clickhouse_connection_string" {
5050
sensitive = true
5151
}
5252

53-
variable "supabase_jwt_secrets" {
54-
type = string
53+
variable "auth_provider_config" {
54+
type = object({
55+
jwt = optional(list(object({
56+
issuer = object({
57+
url = string
58+
discoveryURL = optional(string)
59+
audiences = list(string)
60+
audienceMatchPolicy = optional(string)
61+
})
62+
claimMappings = optional(object({
63+
username = object({
64+
claim = string
65+
})
66+
}))
67+
jwksCacheDuration = optional(string)
68+
})))
69+
bearer = optional(list(object({
70+
hmac = object({
71+
secrets = list(string)
72+
})
73+
claimMappings = optional(object({
74+
username = object({
75+
claim = string
76+
})
77+
}))
78+
})))
79+
})
5580
sensitive = true
81+
default = null
5682
}
5783

5884
variable "enable_auth_user_sync_background_worker" {
@@ -64,6 +90,7 @@ variable "enable_billing_http_team_provision_sink" {
6490
type = bool
6591
default = false
6692
}
93+
6794
variable "otel_collector_grpc_port" {
6895
type = number
6996
default = 4317

iac/provider-aws/main.tf

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,20 @@ module "nomad" {
182182
clickhouse_node_pool = local.clickhouse_pool_name
183183
clickhouse_jobs_prefix = local.clickhouse_jobs_prefix
184184

185-
api_cluster_size = var.api_cluster_size
186-
api_internal_grpc_port = var.api_internal_grpc_port
187-
api_repository_name = module.init.api_repository_name
188-
db_migrator_repository_name = module.init.db_migrator_repository_name
189-
postgres_connection_string = module.init.postgres_connection_string
190-
supabase_jwt_secrets = module.init.supabase_jwt_secrets
185+
api_cluster_size = var.api_cluster_size
186+
api_internal_grpc_port = var.api_internal_grpc_port
187+
api_repository_name = module.init.api_repository_name
188+
db_migrator_repository_name = module.init.db_migrator_repository_name
189+
postgres_connection_string = module.init.postgres_connection_string
190+
auth_provider_config = {
191+
bearer = [
192+
{
193+
hmac = {
194+
secrets = split(",", trimspace(module.init.supabase_jwt_secrets))
195+
}
196+
}
197+
]
198+
}
191199
admin_token = module.init.admin_token
192200
sandbox_access_token_hash_seed = module.init.sandbox_access_token_hash_seed
193201

iac/provider-aws/nomad/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ module "api" {
126126
environment = var.environment
127127
api_docker_image = data.aws_ecr_image.api.image_uri
128128
postgres_connection_string = var.postgres_connection_string
129-
supabase_jwt_secrets = var.supabase_jwt_secrets
129+
auth_provider_config = var.auth_provider_config
130130
nomad_acl_token = var.nomad_acl_token
131131
admin_token = var.admin_token
132132
redis_url = var.redis_url

iac/provider-aws/nomad/variables.tf

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,35 @@ variable "postgres_connection_string" {
188188
sensitive = true
189189
}
190190

191-
variable "supabase_jwt_secrets" {
192-
type = string
191+
variable "auth_provider_config" {
192+
type = object({
193+
jwt = optional(list(object({
194+
issuer = object({
195+
url = string
196+
discoveryURL = optional(string)
197+
audiences = list(string)
198+
audienceMatchPolicy = optional(string)
199+
})
200+
claimMappings = optional(object({
201+
username = object({
202+
claim = string
203+
})
204+
}))
205+
jwksCacheDuration = optional(string)
206+
})))
207+
bearer = optional(list(object({
208+
hmac = object({
209+
secrets = list(string)
210+
})
211+
claimMappings = optional(object({
212+
username = object({
213+
claim = string
214+
})
215+
}))
216+
})))
217+
})
193218
sensitive = true
219+
default = null
194220
}
195221

196222
variable "admin_token" {

iac/provider-gcp/nomad/main.tf

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ locals {
66
enable_billing_http_team_provision_sink = var.enable_billing_http_team_provision_sink
77
dashboard_api_billing_server_url = local.enable_billing_http_team_provision_sink ? trimspace(data.google_secret_manager_secret_version.billing_server_url[0].secret_data) : ""
88
dashboard_api_billing_server_api_token = local.enable_billing_http_team_provision_sink ? trimspace(data.google_secret_manager_secret_version.billing_server_api_token[0].secret_data) : ""
9+
default_auth_provider_config = {
10+
bearer = [
11+
{
12+
hmac = {
13+
secrets = split(",", trimspace(data.google_secret_manager_secret_version.supabase_jwt_secrets.secret_data))
14+
}
15+
}
16+
]
17+
}
18+
auth_provider_config = var.auth_provider_config != null ? var.auth_provider_config : local.default_auth_provider_config
919
}
1020

1121
# API
@@ -117,7 +127,7 @@ module "api" {
117127
api_docker_image = data.google_artifact_registry_docker_image.api_image.self_link
118128
postgres_connection_string = data.google_secret_manager_secret_version.postgres_connection_string.secret_data
119129
postgres_read_replica_connection_string = trimspace(data.google_secret_manager_secret_version.postgres_read_replica_connection_string.secret_data)
120-
supabase_jwt_secrets = trimspace(data.google_secret_manager_secret_version.supabase_jwt_secrets.secret_data)
130+
auth_provider_config = local.auth_provider_config
121131
posthog_api_key = trimspace(data.google_secret_manager_secret_version.posthog_api_key.secret_data)
122132
environment = var.environment
123133
analytics_collector_host = trimspace(data.google_secret_manager_secret_version.analytics_collector_host.secret_data)
@@ -166,7 +176,7 @@ module "dashboard_api" {
166176
auth_db_read_replica_connection_string = trimspace(data.google_secret_manager_secret_version.postgres_read_replica_connection_string.secret_data)
167177
supabase_db_connection_string = trimspace(data.google_secret_manager_secret_version.supabase_db_connection_string.secret_data)
168178
clickhouse_connection_string = local.clickhouse_connection_string
169-
supabase_jwt_secrets = trimspace(data.google_secret_manager_secret_version.supabase_jwt_secrets.secret_data)
179+
auth_provider_config = local.auth_provider_config
170180
redis_url = local.redis_url
171181
redis_cluster_url = local.redis_cluster_url
172182
redis_tls_ca_base64 = trimspace(data.google_secret_manager_secret_version.redis_tls_ca_base64.secret_data)

iac/provider-gcp/nomad/variables.tf

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,37 @@ variable "supabase_db_connection_string_secret_version" {
473473
type = any
474474
}
475475

476+
variable "auth_provider_config" {
477+
type = object({
478+
jwt = optional(list(object({
479+
issuer = object({
480+
url = string
481+
discoveryURL = optional(string)
482+
audiences = list(string)
483+
audienceMatchPolicy = optional(string)
484+
})
485+
claimMappings = optional(object({
486+
username = object({
487+
claim = string
488+
})
489+
}))
490+
jwksCacheDuration = optional(string)
491+
})))
492+
bearer = optional(list(object({
493+
hmac = object({
494+
secrets = list(string)
495+
})
496+
claimMappings = optional(object({
497+
username = object({
498+
claim = string
499+
})
500+
}))
501+
})))
502+
})
503+
sensitive = true
504+
default = null
505+
}
506+
476507
variable "enable_auth_user_sync_background_worker" {
477508
type = bool
478509
default = false
@@ -482,6 +513,7 @@ variable "enable_billing_http_team_provision_sink" {
482513
type = bool
483514
default = false
484515
}
516+
485517
variable "volume_token_issuer" {
486518
type = string
487519
}

0 commit comments

Comments
 (0)