You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/06/17 20:30:04 UTC
(airavata-custos) 04/15: Add CORS middleware reading origins from config
This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch auth-endpoints
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit ed26a034e5251d29263f72608cdee10777d869df
Author: lahiruj <la...@gmail.com>
AuthorDate: Tue Jun 16 19:16:40 2026 -0400
Add CORS middleware reading origins from config
Co-Authored-By: Claude Opus 4.7 <no...@anthropic.com>
---
config/custos.yaml | 4 +++
config/custos.yaml.example | 3 ++
internal/config/config.go | 8 +++++
internal/server/middleware/cors.go | 74 ++++++++++++++++++++++++++++++++++++++
4 files changed, 89 insertions(+)
diff --git a/config/custos.yaml b/config/custos.yaml
index d8f910886..7e0b7ab84 100644
--- a/config/custos.yaml
+++ b/config/custos.yaml
@@ -6,6 +6,10 @@ core:
auth:
issuer: "${OIDC_ISSUER_URL}"
audience: "${OIDC_AUDIENCE}"
+ cors:
+ allowed_origins:
+ - "http://localhost:3000"
+ - "https://portal.dev.nexus.cybershuttle.org"
log_level: "info"
connectors:
diff --git a/config/custos.yaml.example b/config/custos.yaml.example
index 782532948..01174db69 100644
--- a/config/custos.yaml.example
+++ b/config/custos.yaml.example
@@ -13,6 +13,9 @@ core:
issuer: "${OIDC_ISSUER_URL}" # OIDC IdP issuer URL
audience: "${OIDC_AUDIENCE}" # JWT audience claim the IdP issues for this server
# jwks_url: "" # optional override; leave empty to discover via issuer
+ cors:
+ allowed_origins: # browser callers; empty list disables CORS
+ - "http://localhost:3000"
log_level: "info"
connectors:
diff --git a/internal/config/config.go b/internal/config/config.go
index 24de001aa..aa9efaf12 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -35,9 +35,17 @@ type CoreConfig struct {
Database DatabaseConfig `yaml:"database"`
API APIConfig `yaml:"api"`
Auth AuthConfig `yaml:"auth"`
+ CORS CORSConfig `yaml:"cors"`
LogLevel string `yaml:"log_level"`
}
+// CORSConfig drives the origin allowlist for browser callers. An empty
+// AllowedOrigins makes the middleware a no-op pass-through, which is the
+// right behaviour for server-to-server deployments.
+type CORSConfig struct {
+ AllowedOrigins []string `yaml:"allowed_origins"`
+}
+
type DatabaseConfig struct {
URL string `yaml:"url"`
}
diff --git a/internal/server/middleware/cors.go b/internal/server/middleware/cors.go
new file mode 100644
index 000000000..d7d9f5343
--- /dev/null
+++ b/internal/server/middleware/cors.go
@@ -0,0 +1,74 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package middleware
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/apache/airavata-custos/internal/config"
+)
+
+// CORS enforces an explicit origin allowlist. Preflight (OPTIONS) requests
+// short-circuit here so they never reach the auth middleware that follows.
+type CORS struct {
+ allowed map[string]struct{}
+ methods string
+ headers string
+}
+
+// NewCORS reads the allowlist from cfg. An empty allowlist makes the
+// middleware a no-op pass-through, which is the right behaviour when CORS is
+// not configured (server-to-server deployment, integration tests).
+func NewCORS(cfg config.CORSConfig) *CORS {
+ c := &CORS{
+ allowed: make(map[string]struct{}, len(cfg.AllowedOrigins)),
+ methods: "GET, POST, PUT, PATCH, DELETE, OPTIONS",
+ headers: "Authorization, Content-Type, X-Trace-Id",
+ }
+ for _, o := range cfg.AllowedOrigins {
+ o = strings.TrimSpace(o)
+ if o != "" {
+ c.allowed[o] = struct{}{}
+ }
+ }
+ return c
+}
+
+// Wrap returns a handler that adds CORS response headers for allowlisted
+// origins and answers preflight requests directly. Non-allowlisted origins
+// pass through without CORS headers; the browser will reject the response.
+func (c *CORS) Wrap(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ origin := r.Header.Get("Origin")
+ if origin != "" {
+ if _, ok := c.allowed[origin]; ok {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+ w.Header().Set("Access-Control-Allow-Credentials", "true")
+ w.Header().Set("Access-Control-Allow-Methods", c.methods)
+ w.Header().Set("Access-Control-Allow-Headers", c.headers)
+ w.Header().Add("Vary", "Origin")
+ }
+ }
+ if r.Method == http.MethodOptions {
+ w.WriteHeader(http.StatusNoContent)
+ return
+ }
+ next.ServeHTTP(w, r)
+ })
+}