Skip to content
Open
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
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ generate-grpc: ## generate grpc files
generate-contracts: ## generate contracts
go tool abigen --abi internal/contracts/erc1271/IERC1271.json --pkg erc1271 --type Erc1271 --out internal/contracts/erc1271/erc1271.go
go tool abigen --abi internal/contracts/sacd/sacd.json --pkg sacd --type Sacd --out internal/contracts/sacd/sacd.go
go tool abigen --abi internal/contracts/template/template.json --pkg template --type Template --out internal/contracts/template/template.go
go tool abigen --abi internal/contracts/template/template.json --pkg template --type Template --out internal/contracts/template/template.go
go tool abigen --abi internal/contracts/devlicenseaccount/devlicenseaccount.json --pkg devlicenseaccount --type DevLicenseAccount --out internal/contracts/devlicenseaccount/devlicenseaccount.go
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,26 @@ curl -X POST https://token-exchange-api.dimo.zone/v1/tokens/exchange \
---

📖 **For service developers**: See [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md) for architecture details, testing, and contribution guidelines.

## Signer check

Developer JWTs carry `signer_address`, the license signer (API key) that minted them. On every
exchange, token-exchange-api checks that this signer is still enabled on the license in
`ethereum_address`, whatever the token's audience. Addresses that aren't licenses are skipped.
Answers are cached for 60 s; Identity and chain calls time out after 2 s each, so a cold check
finishes inside the 5 s deadline that `SignerCheck` callers set.
`SignerCheck` (gRPC) serves the same answers to vehicle-triggers-api and tesla-oracle.

| Setting | Values | Effect |
| ------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SIGNER_CHECK_MODE` | `enforce` (default), `log`, `off` | `enforce` answers 403 `signer no longer authorized for this license` or 503 `could not verify signer`; `log` only logs and counts; `off` skips the check. |

- **Metric:** `signer_check_total{service,result}`, with `result` one of `allowed`, `denied`, `error`, `skipped`.
Prometheus keeps the ServiceMonitor's own `service` label (the Kubernetes Service), so the app's
`service` label arrives as `exported_service`; filter on that, e.g.
`sum by (result) (rate(signer_check_total{exported_service="vehicle-triggers-api"}[5m]))`.
- **Alert:** `SignerCheckErrors` fires when `error` exceeds 1% of non-skipped checks (`allowed` + `denied` + `error`) for 5 minutes.
- **Rollback order:**
- turn the console's `NEXT_PUBLIC_TEAM_DATA_ACCESS_ENABLED` off;
- set `SIGNER_CHECK_MODE=off` (or roll back) on vehicle-triggers-api and tesla-oracle, then token-exchange-api;
- roll back dex last.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: {{ include "token-exchange-api.fullname" . }}-signer-check
namespace: {{ .Release.Namespace }}
labels:
{{- include "token-exchange-api.labels" . | nindent 4 }}
spec:
groups:
- name: signer-check
rules:
- alert: SignerCheckErrors
expr: |
sum by (service) (rate(signer_check_total{namespace="{{ .Release.Namespace }}",result="error"}[5m]))
/
sum by (service) (rate(signer_check_total{namespace="{{ .Release.Namespace }}",result!="skipped"}[5m]))
> 0.01
for: 5m
labels:
severity: critical
annotations:
summary: 'Signer checks are failing in {{ "{{" }} $labels.service {{ "}}" }}'
description: >-
More than 1% of non-skipped developer-JWT signer checks (allowed + denied + error)
in {{ "{{" }} $labels.service {{ "}}" }} errored over 5 minutes. In enforce mode those requests get 503. Check
token-exchange-api's RPC and Identity reachability; to stop refusing, set
SIGNER_CHECK_MODE=log on the affected service.
1 change: 1 addition & 0 deletions charts/token-exchange-api/values-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ cronJob:
schedule: 0 0 * * 0
env:
ENVIRONMENT: prod
SIGNER_CHECK_MODE: log
ENABLE_PPROF: false
JWT_KEY_SET_URL: https://auth.dimo.zone/keys
DEX_GRPC_ADDRESS: dex-roles-rights-prod:5557
Expand Down
1 change: 1 addition & 0 deletions charts/token-exchange-api/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ cronJob:
schedule: 0 0 * * 0
env:
ENVIRONMENT: dev
SIGNER_CHECK_MODE: log
LOG_LEVEL: info
JWT_KEY_SET_URL: https://auth.dev.dimo.zone/keys
PORT: 8080
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ require (
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/mitchellh/pointerstructure v1.2.0 // indirect
Expand Down
26 changes: 20 additions & 6 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
"github.com/DIMO-Network/token-exchange-api/internal/middleware"
"github.com/DIMO-Network/token-exchange-api/internal/services"
"github.com/DIMO-Network/token-exchange-api/internal/services/access"
"github.com/DIMO-Network/token-exchange-api/internal/services/licensesigner"
"github.com/DIMO-Network/token-exchange-api/internal/services/sacdproxy"
templatesvs "github.com/DIMO-Network/token-exchange-api/internal/services/template"
txgrpc "github.com/DIMO-Network/token-exchange-api/pkg/grpc"
"github.com/DIMO-Network/token-exchange-api/pkg/signercheck"
"github.com/ethereum/go-ethereum/ethclient"
jwtware "github.com/gofiber/contrib/jwt"
"github.com/gofiber/fiber/v2"
Expand Down Expand Up @@ -80,23 +82,35 @@ func CreateServers(logger zerolog.Logger, settings *config.Settings) (*fiber.App
return nil, nil, fmt.Errorf("failed to create access service: %w", err)
}

app, err := createHTTPServer(logger, settings, dexSvc, accessService)
idSvc := services.NewIdentityController(&logger, settings)
signerChecker := licensesigner.NewChecker(ethClient, idSvc)
signerMode, err := signercheck.ParseMode(settings.SignerCheckMode)
if err != nil {
return nil, nil, err
}
signerMiddleware := signercheck.Middleware(signercheck.Config{
Service: "token-exchange-api",
Mode: signerMode,
Checker: signerChecker,
Token: signercheck.MapClaimsToken("user"),
Logger: logger,
})

app, err := createHTTPServer(logger, settings, dexSvc, accessService, idSvc, signerMiddleware)
if err != nil {
return nil, nil, fmt.Errorf("failed to create http server: %w", err)
}

grpcServer := createGRPCServer(rpc.NewTokenExchangeServer(accessService))
grpcServer := createGRPCServer(rpc.NewTokenExchangeServer(accessService, signerChecker))

return app, grpcServer, nil
}

func createHTTPServer(logger zerolog.Logger, settings *config.Settings, dexSvc *services.DexClient, accessService *access.Service) (*fiber.App, error) {
func createHTTPServer(logger zerolog.Logger, settings *config.Settings, dexSvc *services.DexClient, accessService *access.Service, idSvc *services.IdentityController, signerMiddleware fiber.Handler) (*fiber.App, error) {
httpCtrl, err := httpcontroller.NewTokenExchangeController(settings, dexSvc, accessService)
if err != nil {
logger.Fatal().Err(err).Msg("Failed to initialize token exchange controller")
}
idSvc := services.NewIdentityController(&logger, settings)

devLicenseMiddleware := middleware.NewDevLicenseValidator(idSvc, logger)

app := fiber.New(fiber.Config{
Expand All @@ -121,7 +135,7 @@ func createHTTPServer(logger zerolog.Logger, settings *config.Settings, dexSvc *
JWKSetURLs: []string{settings.JWKKeySetURL},
})

handlers := []fiber.Handler{jwtAuth, devLicenseMiddleware}
handlers := []fiber.Handler{jwtAuth, devLicenseMiddleware, signerMiddleware}

// All api routes should be under v1
v1Route := app.Group("/v1")
Expand Down
2 changes: 2 additions & 0 deletions internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ type Settings struct {
IPFSBaseURL string `yaml:"IPFS_BASE_URL"`
IPFSTimeout string `yaml:"IPFS_TIMEOUT"`
DIMORegistryChainID uint64 `yaml:"DIMO_REGISTRY_CHAIN_ID"`
// SignerCheckMode is enforce (default), log or off.
SignerCheckMode string `yaml:"SIGNER_CHECK_MODE"`
}
212 changes: 212 additions & 0 deletions internal/contracts/devlicenseaccount/devlicenseaccount.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions internal/contracts/devlicenseaccount/devlicenseaccount.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"inputs": [{ "internalType": "address", "name": "signer", "type": "address" }],
"name": "isSigner",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function"
}
]
Loading
Loading