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

Restrict CORS for Builder Secret Keys #31

Merged
merged 1 commit into from
Dec 11, 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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ test-coverage-inspect: test-coverage
generate:
go generate -x ./...

.PHONY: proto
proto:
go generate -x ./proto/...

lint:
golangci-lint run ./... --fix -c .golangci.yml

45 changes: 36 additions & 9 deletions middleware.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package authcontrol

import (
"cmp"
"context"
"errors"
"log/slog"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -143,10 +143,8 @@ func Session(cfg Options) func(next http.Handler) http.Handler {
return
}

var (
accessKey string
sessionType proto.SessionType
)
sessionType := proto.SessionType_Public
var accessKey string

for _, f := range cfg.AccessKeyFuncs {
if accessKey = f(r); accessKey != "" {
Expand All @@ -163,15 +161,18 @@ func Session(cfg Options) func(next http.Handler) http.Handler {
serviceClaim, _ := claims["service"].(string)
accountClaim, _ := claims["account"].(string)
adminClaim, _ := claims["admin"].(bool)
// We support both claims for now, we'll deprecate one if we can.
// - `project` is used by the builder to generate Project JWT tokens.
// - `project_id` is used by API for WaaS related authentication.
projectClaim, _ := cmp.Or(claims["project"], claims["project_id"]).(float64)

// - `project` claim is used in Builder Admin API Secret Keys (JWT used by third-party customers).
projectClaim, _ := claims["project"].(float64)

// - `project_id` claim is used by API->WaaS related authentication.
projectIDClaim, _ := claims["project_id"].(float64)

switch {
case serviceClaim != "":
ctx = WithService(ctx, serviceClaim)
sessionType = proto.SessionType_InternalService

case accountClaim != "":
ctx = WithAccount(ctx, accountClaim)
sessionType = proto.SessionType_Wallet
Expand Down Expand Up @@ -200,6 +201,32 @@ func Session(cfg Options) func(next http.Handler) http.Handler {
if projectClaim > 0 {
ctx = WithProjectID(ctx, uint64(projectClaim))
sessionType = max(sessionType, proto.SessionType_Project)
} else if projectIDClaim > 0 {
ctx = WithProjectID(ctx, uint64(projectIDClaim))
sessionType = max(sessionType, proto.SessionType_Project)
}

// Restrict CORS for Builder Admin API Secret Keys.
// These keys are designed for backend service use by third-party customers, not for web apps.
if accountClaim != "" && projectClaim > 0 {
// Secret Keys are distinguished from Wallet JWTs or Builder session JWTs
// by the presence of both `project` and `account` claims. (As of Dec '24)
// Related discussion: https://github.com/0xsequence/issue-tracker/issues/3802.

origin := r.Header.Get("Origin")
if origin != "" {
err := proto.ErrSecretKeyCorsDisallowed.WithCausef("project_id: %v", projectClaim)

slog.ErrorContext(ctx, "CORS disallowed for Secret Key",
slog.Any("error", err),
slog.String("origin", origin),
slog.Uint64("project_id", uint64(projectClaim)),
)

// TODO: Uncomment once we're confident it won't disrupt major customers.
// cfg.ErrHandler(r, w, err)
// return
}
Comment on lines +216 to +229
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, this will only log error with origin and project_id fields, if we encounter a Secret Key misused from a web app.

Eventually, we will error out instead, once we're confident it won't disrupt major customers.

}
}

Expand Down
23 changes: 12 additions & 11 deletions proto/authcontrol.gen.go

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

20 changes: 18 additions & 2 deletions proto/authcontrol.gen.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable */
// authcontrol v0.9.1 809804f85757ee407e93c191d6d5bfb75b82cb56
// authcontrol v0.9.1 6d8f688a98165b12e0ddfa4ecbaeb8bd7d7f92ac
// --
// Code generated by [email protected] with typescript generator. DO NOT EDIT.
//
Expand All @@ -16,7 +16,7 @@ export const WebRPCVersion = "v1"
export const WebRPCSchemaVersion = "v0.9.1"

// Schema hash generated from your RIDL schema
export const WebRPCSchemaHash = "809804f85757ee407e93c191d6d5bfb75b82cb56"
export const WebRPCSchemaHash = "6d8f688a98165b12e0ddfa4ecbaeb8bd7d7f92ac"

type WebrpcGenVersions = {
webrpcGenVersion: string;
Expand Down Expand Up @@ -413,6 +413,19 @@ export class ProjectNotFoundError extends WebrpcError {
}
}

export class SecretKeyCorsDisallowedError extends WebrpcError {
constructor(
name: string = 'SecretKeyCorsDisallowed',
code: number = 1009,
message: string = `CORS disallowed. Admin API Secret Key can't be used from a web app.`,
status: number = 0,
cause?: string
) {
super(name, code, message, status, cause)
Object.setPrototypeOf(this, SecretKeyCorsDisallowedError.prototype)
}
}


export enum errors {
WebrpcEndpoint = 'WebrpcEndpoint',
Expand All @@ -435,6 +448,7 @@ export enum errors {
Geoblocked = 'Geoblocked',
RateLimited = 'RateLimited',
ProjectNotFound = 'ProjectNotFound',
SecretKeyCorsDisallowed = 'SecretKeyCorsDisallowed',
}

export enum WebrpcErrorCodes {
Expand All @@ -458,6 +472,7 @@ export enum WebrpcErrorCodes {
Geoblocked = 1006,
RateLimited = 1007,
ProjectNotFound = 1008,
SecretKeyCorsDisallowed = 1009,
}

export const webrpcErrorByCode: { [code: number]: any } = {
Expand All @@ -481,6 +496,7 @@ export const webrpcErrorByCode: { [code: number]: any } = {
[1006]: GeoblockedError,
[1007]: RateLimitedError,
[1008]: ProjectNotFoundError,
[1009]: SecretKeyCorsDisallowedError,
}

export type Fetch = (input: RequestInfo, init?: RequestInit) => Promise<Response>
Expand Down
20 changes: 11 additions & 9 deletions proto/errors.ridl
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ webrpc = v1
name = authcontrol
version = v0.9.1

error 1000 Unauthorized "Unauthorized access" HTTP 401
error 1001 PermissionDenied "Permission denied" HTTP 403
error 1002 SessionExpired "Session expired" HTTP 403
error 1003 MethodNotFound "Method not found" HTTP 404
error 1004 RequestConflict "Conflict with target resource" HTTP 409
error 1005 Aborted "Request aborted" HTTP 400
error 1006 Geoblocked "Geoblocked region" HTTP 451
error 1007 RateLimited "Rate-limited. Please slow down." HTTP 429
error 1008 ProjectNotFound "Project not found" HTTP 401
error 1000 Unauthorized "Unauthorized access" HTTP 401
error 1001 PermissionDenied "Permission denied" HTTP 403
error 1002 SessionExpired "Session expired" HTTP 403
error 1003 MethodNotFound "Method not found" HTTP 404
error 1004 RequestConflict "Conflict with target resource" HTTP 409
error 1005 Aborted "Request aborted" HTTP 400
error 1006 Geoblocked "Geoblocked region" HTTP 451
error 1007 RateLimited "Rate-limited. Please slow down." HTTP 429
error 1008 ProjectNotFound "Project not found" HTTP 401
error 1009 SecretKeyCorsDisallowed "CORS disallowed. Admin API Secret Key can't be used from a web app." HTTP 403

Loading