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 <[email protected]>
AuthorDate: Tue Jun 16 19:16:40 2026 -0400

    Add CORS middleware reading origins from config
    
    Co-Authored-By: Claude Opus 4.7 <[email protected]>
---
 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)
+       })
+}

Reply via email to