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) + }) +}
