CORS Proxy for Athena providers (#398)

Handle cors_relay_required = true for Providers in UI.
Added a safe CORS Proxy, which support for Whitelisted endpoints only.
This commit is contained in:
Jason Kulatunga 2024-01-29 19:45:21 -08:00 committed by GitHub
parent 8ee98845cc
commit c48af66f00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 69 additions and 15 deletions

View File

@ -2,6 +2,7 @@ package handler
import ( import (
"fmt" "fmt"
sourceDefinitions "github.com/fastenhealth/fasten-sources/definitions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"log" "log"
"net/http" "net/http"
@ -10,15 +11,50 @@ import (
"strings" "strings"
) )
//TODO, there are security implications to this, we need to make sure we lock this down. // SECURITY: there are security implications to this, this may require some additional authentication to limit misuse
// this is a whitelisted CORS proxy, it is only used to proxy requests to Token Exchange urls for specified endpoint
func CORSProxy(c *gin.Context) { func CORSProxy(c *gin.Context) {
//appConfig := c.MustGet("CONFIG").(config.Interface)
endpointId := strings.Trim(c.Param("endpointId"), "/")
//get the endpoint definition
endpointDefinition, err := sourceDefinitions.GetSourceDefinition(sourceDefinitions.GetSourceConfigOptions{
EndpointId: endpointId,
})
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": fmt.Sprintf("endpoint not found: %s", endpointId),
})
return
}
//SECURITY: if the endpoint definition does not have CORSRelayRequired set to true, then return a 404
if endpointDefinition.CORSRelayRequired != true {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "endpoint does not require CORS Relay.",
})
return
}
//SECURITY: the proxy URL must start with the same URL as the endpoint.TokenUri
corsUrl := fmt.Sprintf("https://%s", strings.TrimPrefix(c.Param("proxyPath"), "/")) corsUrl := fmt.Sprintf("https://%s", strings.TrimPrefix(c.Param("proxyPath"), "/"))
//we'll lowercase to normalize the comparison
if !strings.HasPrefix(strings.ToLower(corsUrl), strings.ToLower(endpointDefinition.TokenEndpoint)) {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "invalid proxy URL, must match TokenEndpoint",
})
return
}
remote, err := url.Parse(corsUrl) remote, err := url.Parse(corsUrl)
remote.RawQuery = c.Request.URL.Query().Encode() remote.RawQuery = c.Request.URL.Query().Encode()
if err != nil { if err != nil {
panic(err) c.JSON(http.StatusInternalServerError, gin.H{
"error": "invalid proxy URL, could not parse",
})
return
} }
proxy := httputil.ReverseProxy{} proxy := httputil.ReverseProxy{}

View File

@ -53,10 +53,12 @@ func (ae *AppEngine) Setup() (*gin.RouterGroup, *gin.Engine) {
api.POST("/auth/signup", handler.AuthSignup) api.POST("/auth/signup", handler.AuthSignup)
api.POST("/auth/signin", handler.AuthSignin) api.POST("/auth/signin", handler.AuthSignin)
//
//r.Any("/database/*proxyPath", handler.CouchDBProxy) //whitelisted CORS PROXY
//r.GET("/cors/*proxyPath", handler.CORSProxy) api.GET("/cors/:endpointId/*proxyPath", handler.CORSProxy)
//r.OPTIONS("/cors/*proxyPath", handler.CORSProxy) api.POST("/cors/:endpointId/*proxyPath", handler.CORSProxy)
api.OPTIONS("/cors/:endpointId/*proxyPath", handler.CORSProxy)
api.GET("/glossary/code", handler.GlossarySearchByCode) api.GET("/glossary/code", handler.GlossarySearchByCode)
api.POST("/support/request", handler.SupportRequest) api.POST("/support/request", handler.SupportRequest)

View File

@ -15,6 +15,7 @@ import {OpenExternalLink} from '../../lib/utils/external_link';
import {Router, UrlSerializer} from '@angular/router'; import {Router, UrlSerializer} from '@angular/router';
import {Location} from '@angular/common'; import {Location} from '@angular/common';
import {PatientAccessBrand, PatientAccessEndpoint, PatientAccessPortal} from '../models/patient-access-brands'; import {PatientAccessBrand, PatientAccessEndpoint, PatientAccessPortal} from '../models/patient-access-brands';
import {GetEndpointAbsolutePath} from '../../lib/utils/endpoint_absolute_path';
export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120) export const sourceConnectDesktopTimeout = 24*5000 //wait 2 minutes (5 * 24 = 120)
@ -242,11 +243,10 @@ export class LighthouseService {
} }
//this is for providers that support CORS & PKCE (public client auth) //this is for providers that support CORS & PKCE (public client auth)
let codeVerifier = undefined let codeVerifier = undefined
if(!sourceMetadata.confidential){
client.token_endpoint_auth_method = 'none'
codeVerifier = expectedSourceStateInfo.code_verifier
} else { let tokenEndpointUrl = sourceMetadata.token_endpoint
if(sourceMetadata.confidential) {
console.log("This is a confidential client, using lighthouse token endpoint.") console.log("This is a confidential client, using lighthouse token endpoint.")
//if this is a confidential client, we need to "override" token endpoint, and use the Fasten Lighthouse to complete the swap //if this is a confidential client, we need to "override" token endpoint, and use the Fasten Lighthouse to complete the swap
sourceMetadata.token_endpoint = this.pathJoin([environment.lighthouse_api_endpoint_base, `token/${expectedSourceStateInfo.endpoint_id}`]) sourceMetadata.token_endpoint = this.pathJoin([environment.lighthouse_api_endpoint_base, `token/${expectedSourceStateInfo.endpoint_id}`])
@ -259,12 +259,28 @@ export class LighthouseService {
} else { } else {
codeVerifier = "placeholder" codeVerifier = "placeholder"
} }
} else {
//is not confidential
client.token_endpoint_auth_method = 'none'
codeVerifier = expectedSourceStateInfo.code_verifier
//check if source requires a CORS relay
if(sourceMetadata.cors_relay_required){
let corsProxyBaseUrl = `${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/cors/${sourceMetadata.id}/`
//this endpoint requires a CORS relay
//get the path to the Fasten server, and append `cors/` and then append the request url
let tokenEndpointUrlParts = new URL(tokenEndpointUrl)
tokenEndpointUrl = corsProxyBaseUrl + `${tokenEndpointUrlParts.hostname}${tokenEndpointUrlParts.pathname}${tokenEndpointUrlParts.search}`
console.warn("Using local CORS proxy for token endpoint", tokenEndpointUrl)
}
} }
const as = { const as = {
issuer: sourceMetadata.issuer, issuer: sourceMetadata.issuer,
authorization_endpoint: sourceMetadata.authorization_endpoint, authorization_endpoint: sourceMetadata.authorization_endpoint,
token_endpoint: sourceMetadata.token_endpoint, token_endpoint: tokenEndpointUrl,
introspection_endpoint: sourceMetadata.introspection_endpoint, introspection_endpoint: sourceMetadata.introspection_endpoint,
} }

2
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/dave/jennifer v1.6.1 github.com/dave/jennifer v1.6.1
github.com/dominikbraun/graph v0.15.0 github.com/dominikbraun/graph v0.15.0
github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3 github.com/dop251/goja v0.0.0-20230605162241-28ee0ee714f3
github.com/fastenhealth/fasten-sources v0.5.6 github.com/fastenhealth/fasten-sources v0.5.8
github.com/fastenhealth/gofhir-models v0.0.6 github.com/fastenhealth/gofhir-models v0.0.6
github.com/gin-gonic/gin v1.9.0 github.com/gin-gonic/gin v1.9.0
github.com/go-gormigrate/gormigrate/v2 v2.1.1 github.com/go-gormigrate/gormigrate/v2 v2.1.1

4
go.sum
View File

@ -101,8 +101,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fastenhealth/fasten-sources v0.5.6 h1:F4Qmw9ABLSkqkWncoSnChBRDVWLrzkJv+z4z/Ue/fdc= github.com/fastenhealth/fasten-sources v0.5.8 h1:zcohdfd7QBWxPcD4TTniHBiD+x4tqi6YIXAjLu72AY0=
github.com/fastenhealth/fasten-sources v0.5.6/go.mod h1:hUQATAu5KrxKbACJoVt4iEKIGnRtmiOmHz+5TLfyiCM= github.com/fastenhealth/fasten-sources v0.5.8/go.mod h1:hUQATAu5KrxKbACJoVt4iEKIGnRtmiOmHz+5TLfyiCM=
github.com/fastenhealth/gofhir-models v0.0.6 h1:yJYYaV1eJtHiGEfA1rXLsyOm/9hIi6s2cGoZzGfW1tM= github.com/fastenhealth/gofhir-models v0.0.6 h1:yJYYaV1eJtHiGEfA1rXLsyOm/9hIi6s2cGoZzGfW1tM=
github.com/fastenhealth/gofhir-models v0.0.6/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis= github.com/fastenhealth/gofhir-models v0.0.6/go.mod h1:xB8ikGxu3bUq2b1JYV+CZpHqBaLXpOizFR0eFBCunis=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=