From 785b68f51c8ed4534b69fc87a2af15d4f6506a41 Mon Sep 17 00:00:00 2001 From: amantahiliani Date: Tue, 1 Apr 2025 23:11:30 -0400 Subject: [PATCH] Initial Commit --- app/main.go | 370 ++++++++++++++++++++++++++++++++++++++++++ conf/application.conf | 0 go.mod | 3 + 3 files changed, 373 insertions(+) create mode 100644 app/main.go create mode 100644 conf/application.conf create mode 100644 go.mod diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..7384bf7 --- /dev/null +++ b/app/main.go @@ -0,0 +1,370 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" +) + +type Application struct { + config *ApplicationConfig +} + +type ApplicationConfig struct { + smart *SMARTAppConfig + eHRs []EHRClientsConfig // List of EHR clients configuration +} + +type SMARTAppConfig struct { + oAuth1 bool // Enable OAuth1 for certain EHRs + oAuth2 bool // Enable OAuth2 for others + redirectPath string // Where to redirect after auth + clientID string // OAuth client ID (if applicable) + clientSecret string // OAuth client secret + scopes []string +} + +type EHRClientsConfig struct { + name string // Name of the EHR, e.g., "EHR_A" + url string // FHIR endpoint URL + authType string // auth-1 or auth-2 for OAuth type + clientID string // OAuth client ID (for Azure AD) + clientSecret string // OAuth secret (for Azure AD) + patientAPI bool // Whether to enable patient API endpoints +} + +type LaunchContext struct { + LaunchID string `json:"launch"` + Patient string `json:"patient"` +} + +func main() { + log.Printf("Starting EHR integration application...") + + appConfig := &ApplicationConfig{ + smart: &SMARTAppConfig{ + redirectPath: "http://localhost:8080/auth-redirect", + clientID: "my-smart-client-id", + clientSecret: "my-smart-client-secret", + scopes: []string{"launch", "patient/*.read"}, + oAuth1: true, + oAuth2: false, + }, + eHRs: []EHRClientsConfig{ // Sample EHR clients configuration + { + name: "EHR_A", + url: "https://launch.smarthealthit.org/v/r4/fhir", + authType: "auth-2", + clientID: "abcdefghijklmnopqrst", + clientSecret: "ehr_a_client_secret", + patientAPI: true, + }, + { + name: "EHR_B", + url: "https://ehr-b.healthcare-provider.com/fhir/v2", + authType: "auth-2", + clientID: "ehr_b_client_id", // OAuth 2.0 client ID + clientSecret: "ehr_b_client_secret", + patientAPI: true, + }, + }, + } + + app := &Application{ + config: appConfig, + } + + log.Printf("Listening on port %d...", 8080) + + // Start HTTP server and handle routes + http.HandleFunc("/", app.handleRoot) + http.HandleFunc("/launch", app.handleLaunch) + http.HandleFunc("/auth-redirect", app.handleAuthRedirect) + + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +// handleRoot is a method of Application that handles the root endpoint. +func (app *Application) handleRoot(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("Welcome to the EHR integration application!")) +} + +func getWellKnownUrl(iss string) (string, error) { + wellKnownUrl := fmt.Sprintf("%s/.well-known/smart-configuration", iss) + resp, err := http.Get(wellKnownUrl) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch well-known URL") + } + + var bodyBytes []byte + bodyBytes, err = io.ReadAll(resp.Body) + if err != nil { + return "", err + } + return string(bodyBytes), nil + +} + +// handleLaunch processes the SMART on FHIR launch request. +func (app *Application) handleLaunch(w http.ResponseWriter, r *http.Request) { + launchID := r.URL.Query().Get("launch") + iss := r.URL.Query().Get("iss") + // Log the query parameters and body data for debugging + log.Printf("Query parameters: %v", r.URL.Query()) + if launchID == "" || iss == "" { + http.Error(w, "Missing launch or iss parameter", http.StatusBadRequest) + return + } + + // Validate the FHIR server base URL (iss) + _, err := url.ParseRequestURI(iss) + if err != nil { + http.Error(w, "Invalid FHIR server base URL (iss)", http.StatusBadRequest) + return + } + + //Make sure the FHIR server base URL (iss) is registered in the application's configuration + var ehrConfig *EHRClientsConfig + for _, ehr := range app.config.eHRs { + if ehr.url == iss { + ehrConfig = &ehr + break + } + } + + if ehrConfig == nil { + http.Error(w, "Unsupported FHIR server base URL (iss)", http.StatusBadRequest) + return + } + + var clientID string + var clientSecret string + clientID = ehrConfig.clientID + clientSecret = ehrConfig.clientSecret + + if clientID == "" || clientSecret == "" { + http.Error(w, "Missing client ID or client secret", http.StatusBadRequest) + return + } + + wellKnownBody, err := getWellKnownUrl(iss) + if err != nil { + http.Error(w, "Failed to fetch SMART configuration from FHIR server", http.StatusInternalServerError) + return + } + // Get the authorization endpoint from the wellKnownBody and use it to construct the authorization URL + var discoveryDoc map[string]interface{} + if err := json.NewDecoder(strings.NewReader(wellKnownBody)).Decode(&discoveryDoc); err != nil { + http.Error(w, "Invalid SMART configuration response", http.StatusInternalServerError) + return + } + + authEndpoint, ok := discoveryDoc["authorization_endpoint"].(string) + if !ok || authEndpoint == "" { + http.Error(w, "Authorization endpoint not found in SMART configuration", http.StatusInternalServerError) + return + } + + // Generate a state value that can be used to identify the EHR client and can be used to prevent CSRF attacks. + state := iss + "state_hash" + launchID + + // Construct the authorization URL + authURL := fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&launch=%s&scope=openid+profile+launch+patient/*.read&state=%s&aud=%s", + authEndpoint, clientID, url.QueryEscape(app.config.smart.redirectPath), launchID, state, iss) + + http.Redirect(w, r, authURL, http.StatusFound) +} + +// handleAuthRedirect processes the authorization redirect and retrieves patient details. +func (app *Application) handleAuthRedirect(w http.ResponseWriter, r *http.Request) { + // Log the query parameters and body data for debugging + log.Printf("Query parameters: %v", r.URL.Query()) + + bodyData := make(map[string]interface{}) + if err := json.NewDecoder(r.Body).Decode(&bodyData); err != nil { + log.Printf("Failed to decode body data: %v", err) + } else { + log.Printf("Body data: %v", bodyData) + } + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + + if code == "" || state == "" { + http.Error(w, "Missing code or state parameter", http.StatusBadRequest) + return + } + // Extract the EHR client ID from the state parameter + stateParts := strings.Split(state, "state_hash") + if len(stateParts) != 2 { + http.Error(w, "Invalid state parameter", http.StatusBadRequest) + return + } + ehr_iss := stateParts[0] + // Fetch the EHR client configuration based on the EHR client ID + var ehrConfig *EHRClientsConfig + for _, ehr := range app.config.eHRs { + if ehr.url == ehr_iss { + ehrConfig = &ehr + break + } + } + if ehrConfig == nil { + http.Error(w, "Unsupported EHR client", http.StatusBadRequest) + return + } + wellKnownBody, err := getWellKnownUrl(ehr_iss) + if err != nil { + http.Error(w, "Failed to fetch SMART configuration from FHIR server", http.StatusInternalServerError) + return + } + // Construct the token request URL from the well-knownBody + var discoveryDoc map[string]interface{} + if err := json.NewDecoder(strings.NewReader(wellKnownBody)).Decode(&discoveryDoc); err != nil { + http.Error(w, "Invalid SMART configuration response", http.StatusInternalServerError) + return + } + tokenEndpoint, ok := discoveryDoc["token_endpoint"].(string) + if !ok || tokenEndpoint == "" { + http.Error(w, "Token endpoint not found in SMART configuration", http.StatusInternalServerError) + return + } + + // Construct the token request URL from the well-knownBody + // Prepare form data for token request + formData := url.Values{ + "grant_type": {"authorization_code"}, + "code": {code}, + "redirect_uri": {app.config.smart.redirectPath}, + } + + // Create request with Basic authentication + req, err := http.NewRequest("POST", tokenEndpoint, strings.NewReader(formData.Encode())) + if err != nil { + http.Error(w, "Failed to exchange authorization code for token", http.StatusInternalServerError) + return + } + + // Add required headers + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.SetBasicAuth(ehrConfig.clientID, ehrConfig.clientSecret) + + // Make the token request + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Printf("Failed to make token request: %v", err) + http.Error(w, "Failed to exchange authorization code for token: ", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + log.Printf("Failed to exchange authorization code for token: %v", resp.Status) + + // Decode and log the response body + var responseBody map[string]interface{} + bodyBytes, _ := io.ReadAll(resp.Body) + if err := json.Unmarshal(bodyBytes, &responseBody); err != nil { + log.Printf("Failed to decode token response body: %v", err) + } else { + log.Printf("Token response: %v", responseBody) + } + // Reset the response body for later use + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + http.Error(w, "Failed to exchange authorization code for token", http.StatusInternalServerError) + return + } + // Parse the token response + var tokenResponse map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil { + http.Error(w, "Invalid token response", http.StatusInternalServerError) + return + } + //log the token response + log.Printf("Token response: %v", tokenResponse) + // Extract the access token and patient ID from the token response + accessToken, ok := tokenResponse["access_token"].(string) + if !ok || accessToken == "" { + http.Error(w, "Access token not found in token response", http.StatusInternalServerError) + return + } + patientID, ok := tokenResponse["patient"].(string) + if !ok || patientID == "" { + http.Error(w, "Patient ID not found in token response", http.StatusInternalServerError) + return + } + // Fetch patient details from the EHR using the access token + patientURL := fmt.Sprintf("%s/Patient/%s", ehrConfig.url, patientID) + + req, err = http.NewRequest("GET", patientURL, nil) + if err != nil { + http.Error(w, "Failed to create patient details request", http.StatusInternalServerError) + return + } + req.Header.Set("Authorization", "Bearer "+accessToken) + resp, err = http.DefaultClient.Do(req) + if err != nil { + http.Error(w, "Failed to fetch patient details", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + http.Error(w, "Failed to fetch patient details", http.StatusInternalServerError) + return + } + // Parse the patient details response + var patientDetails map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&patientDetails); err != nil { + http.Error(w, "Invalid patient details response", http.StatusInternalServerError) + return + } + // Log the patient details for debugging purposes + log.Printf("Patient details: %v", patientDetails) + // Render the patient details in the response + w.Header().Set("Content-Type", "text/html") + // Create HTML table header + html := "" + html += "

Patient Details

" + + // Recursively build table rows from JSON + var buildTableRows func(data map[string]interface{}, indent string) string + buildTableRows = func(data map[string]interface{}, indent string) string { + var rows string + for key, value := range data { + rows += "" + rows += fmt.Sprintf("", indent, key) + + switch v := value.(type) { + case map[string]interface{}: + rows += "" + case []interface{}: + rows += "" + default: + rows += fmt.Sprintf("", v) + } + rows += "" + } + return rows + } + + html += buildTableRows(patientDetails, "") + html += "
%s%s" + buildTableRows(v, indent+"  ") + "" + for _, item := range v { + if m, ok := item.(map[string]interface{}); ok { + rows += buildTableRows(m, indent+"  ") + } else { + rows += fmt.Sprintf("", item) + } + } + rows += "
%v
%v
" + + w.Write([]byte(html)) +} diff --git a/conf/application.conf b/conf/application.conf new file mode 100644 index 0000000..e69de29 diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..72ae54a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/AmanTahiliani/FHIR-Sandbox + +go 1.24.0