diff --git a/Makefile b/Makefile index 6e4d567..91e8d8a 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/middleware.go b/middleware.go index f666049..7e70100 100644 --- a/middleware.go +++ b/middleware.go @@ -1,9 +1,9 @@ package authcontrol import ( - "cmp" "context" "errors" + "log/slog" "net/http" "strings" "time" @@ -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 != "" { @@ -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 @@ -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 + } } } diff --git a/proto/authcontrol.gen.go b/proto/authcontrol.gen.go index fedf599..7e50cd6 100644 --- a/proto/authcontrol.gen.go +++ b/proto/authcontrol.gen.go @@ -1,4 +1,4 @@ -// authcontrol v0.9.1 809804f85757ee407e93c191d6d5bfb75b82cb56 +// authcontrol v0.9.1 6d8f688a98165b12e0ddfa4ecbaeb8bd7d7f92ac // -- // Code generated by webrpc-gen@v0.22.1 with golang generator. DO NOT EDIT. // @@ -29,7 +29,7 @@ func WebRPCSchemaVersion() string { // Schema hash generated from your RIDL schema func WebRPCSchemaHash() string { - return "809804f85757ee407e93c191d6d5bfb75b82cb56" + return "6d8f688a98165b12e0ddfa4ecbaeb8bd7d7f92ac" } type WebrpcGenVersions struct { @@ -301,13 +301,14 @@ var ( // Schema errors var ( - ErrUnauthorized = WebRPCError{Code: 1000, Name: "Unauthorized", Message: "Unauthorized access", HTTPStatus: 401} - ErrPermissionDenied = WebRPCError{Code: 1001, Name: "PermissionDenied", Message: "Permission denied", HTTPStatus: 403} - ErrSessionExpired = WebRPCError{Code: 1002, Name: "SessionExpired", Message: "Session expired", HTTPStatus: 403} - ErrMethodNotFound = WebRPCError{Code: 1003, Name: "MethodNotFound", Message: "Method not found", HTTPStatus: 404} - ErrRequestConflict = WebRPCError{Code: 1004, Name: "RequestConflict", Message: "Conflict with target resource", HTTPStatus: 409} - ErrAborted = WebRPCError{Code: 1005, Name: "Aborted", Message: "Request aborted", HTTPStatus: 400} - ErrGeoblocked = WebRPCError{Code: 1006, Name: "Geoblocked", Message: "Geoblocked region", HTTPStatus: 451} - ErrRateLimited = WebRPCError{Code: 1007, Name: "RateLimited", Message: "Rate-limited. Please slow down.", HTTPStatus: 429} - ErrProjectNotFound = WebRPCError{Code: 1008, Name: "ProjectNotFound", Message: "Project not found", HTTPStatus: 401} + ErrUnauthorized = WebRPCError{Code: 1000, Name: "Unauthorized", Message: "Unauthorized access", HTTPStatus: 401} + ErrPermissionDenied = WebRPCError{Code: 1001, Name: "PermissionDenied", Message: "Permission denied", HTTPStatus: 403} + ErrSessionExpired = WebRPCError{Code: 1002, Name: "SessionExpired", Message: "Session expired", HTTPStatus: 403} + ErrMethodNotFound = WebRPCError{Code: 1003, Name: "MethodNotFound", Message: "Method not found", HTTPStatus: 404} + ErrRequestConflict = WebRPCError{Code: 1004, Name: "RequestConflict", Message: "Conflict with target resource", HTTPStatus: 409} + ErrAborted = WebRPCError{Code: 1005, Name: "Aborted", Message: "Request aborted", HTTPStatus: 400} + ErrGeoblocked = WebRPCError{Code: 1006, Name: "Geoblocked", Message: "Geoblocked region", HTTPStatus: 451} + ErrRateLimited = WebRPCError{Code: 1007, Name: "RateLimited", Message: "Rate-limited. Please slow down.", HTTPStatus: 429} + ErrProjectNotFound = WebRPCError{Code: 1008, Name: "ProjectNotFound", Message: "Project not found", HTTPStatus: 401} + ErrSecretKeyCorsDisallowed = WebRPCError{Code: 1009, Name: "SecretKeyCorsDisallowed", Message: "CORS disallowed. Admin API Secret Key can't be used from a web app.", HTTPStatus: 403} ) diff --git a/proto/authcontrol.gen.ts b/proto/authcontrol.gen.ts index 8e14b4e..62250e0 100644 --- a/proto/authcontrol.gen.ts +++ b/proto/authcontrol.gen.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -// authcontrol v0.9.1 809804f85757ee407e93c191d6d5bfb75b82cb56 +// authcontrol v0.9.1 6d8f688a98165b12e0ddfa4ecbaeb8bd7d7f92ac // -- // Code generated by webrpc-gen@v0.22.1 with typescript generator. DO NOT EDIT. // @@ -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; @@ -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', @@ -435,6 +448,7 @@ export enum errors { Geoblocked = 'Geoblocked', RateLimited = 'RateLimited', ProjectNotFound = 'ProjectNotFound', + SecretKeyCorsDisallowed = 'SecretKeyCorsDisallowed', } export enum WebrpcErrorCodes { @@ -458,6 +472,7 @@ export enum WebrpcErrorCodes { Geoblocked = 1006, RateLimited = 1007, ProjectNotFound = 1008, + SecretKeyCorsDisallowed = 1009, } export const webrpcErrorByCode: { [code: number]: any } = { @@ -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 diff --git a/proto/errors.ridl b/proto/errors.ridl index 0a81f86..f7ba4d4 100644 --- a/proto/errors.ridl +++ b/proto/errors.ridl @@ -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 +