Skip to content
Snippets Groups Projects
main.go 18.93 KiB
package main

import (
	"encoding/json"
	"encoding/base64"
	"flag"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"reflect"
	"os"
	"strconv"
	"strings"
	log "github.com/sirupsen/logrus"
)

var (
	httpPort    		= flag.Int("http_port", LookupEnvOrInt("HTTP_PORT", 80), "HTTP port to serve on (defaults to 80)")
	logLevel    		= flag.String("loglevel", LookupEnvOrString("LOGLEVEL", "debug"), "log level (debug, info, warning, error) (defaults to debug)")
	cozyDomain 			= flag.String("cozy_domain", LookupEnvOrString("COZY_DOMAIN", "cozy.wf.alpha.grandlyon.com"), "Cozy domain (defaults to cozy.wf.alpha.grandlyon.com)")
	cozyRedirectURI 	= flag.String("cozy_redirect_uri", LookupEnvOrString("COZY_REDIRECT_URI", "/accounts/enedisgrandlyon/redirect"), "Cozy redirect URI (defaults to /accounts/enedisgrandlyon/redirect)")
	cozyGrdfRedirectURI 	= flag.String("cozy_grdf_redirect_uri", LookupEnvOrString("COZY_GRDF_REDIRECT_URI", "/accounts/grdfgrandlyon/redirect"), "Cozy redirect URI (defaults to /accounts/grdfgrandlyon/redirect)")
)

type EnedisTokenResponse struct {
	AccessToken          string `json:"access_token"`
	TokenType            string `json:"token_type"`
	ExpiresIn            int    `json:"expires_in"`
	RefreshToken         string `json:"refresh_token"`
	Scope                string `json:"scope"`
	RefreshTokenIssuedAt string `json:"refresh_token_issued_at"`
	IssueAt              string `json:"issued_at"`
	UsagePointId         string `json:"usage_points_id"`
}

type GrdfConsentement []struct {
	Pce             string `json:"pce"`
	IdAccreditation string `json:"id_accreditation"`
}

type GrdfConsentementToken struct {
	AtHash          string `json:"at_hash"`
	Sub             string `json:"sub"`
	AuditTrackingId string `json:"auditTrackingId"`
	Iss             string `json:"iss"`
	TokenName       string `json:"tokenName"`
	Aud             string `json:"aud"`
	CHash           string `json:"c_hash"`
	Acr             string `json:"acr"`
	Azp             string `json:"azp"`
	AuthYime        int    `json:"auth_time"`
	Realm           string `json:"realm"`
	Consentements   string `json:"consentements"`
	Exp             int    `json:"exp"`
	TokenType       string `json:"tokenType"`
	Iat             int    `json:"iat"`
}

type GrdfTokenResponse struct {
	AccessToken         string `json:"access_token"`
	RefreshToken        string `json:"refresh_token"`
	IdToken				string `json:"id_token"`
	TokenType           string `json:"token_type"`
	ExpiresIn           int    `json:"expires_in"`
	Scope               string `json:"scope"`
	Pce					string `json:"pce"`
}

func LookupEnvOrString(key string, defaultVal string) string {
	if val, ok := os.LookupEnv(key); ok {
		return val
	}
	return defaultVal
}

func LookupEnvOrInt(key string, defaultVal int) int {
	if val, ok := os.LookupEnv(key); ok {
		v, err := strconv.Atoi(val)
		if err != nil {
			log.Fatalf("LookupEnvOrInt[%s]: %v", key, err)
		}
		return v
	}
	return defaultVal
}

func findItem(arrayType interface{}, item interface{}) bool {
	arr := reflect.ValueOf(arrayType)
	if arr.Kind() != reflect.Array {
		panic("Invalid data-type")
	}
	for i := 0; i < arr.Len(); i++ {
		if arr.Index(i).Interface() == item {
			return true
		}
	}

	return false
}

func main() {
	// Parse the flags
	flag.Parse()

	// Init logging
	log.SetOutput(os.Stdout)
	log.SetFormatter(&log.TextFormatter{
		PadLevelText:     true,
		ForceQuote:       true,
		DisableTimestamp: false,
		FullTimestamp:    true,
		TimestampFormat:  "2006-01-02 15:04:05",
	})

	// Configure log level
	switch strings.ToLower(*logLevel) {
	case "error":
		log.SetLevel(log.ErrorLevel)
	case "warning":
		log.SetLevel(log.WarnLevel)
	case "info":
		log.SetLevel(log.InfoLevel)
	case "debug":
		log.SetLevel(log.DebugLevel)
	default:
		log.SetLevel(log.DebugLevel)
		log.Fatalf("Unknown logging level %s. Choose between debug, info, warning or error.", *logLevel)
	}

	mux := http.NewServeMux()
	log.Infof("Starting Server on port %d\n", *httpPort)

	mux.HandleFunc("/healthcheck", func(w http.ResponseWriter, r *http.Request) {
		io.WriteString(w, "OK\n")
	})

	// ENEDIS AUTH ENDPOINT
	mux.HandleFunc("/auth", func(w http.ResponseWriter, r *http.Request) {
		log.Debug("New auth request")
		query := r.URL.Query()
		log.Debug("Query received - ", query)

		clientId := query.Get("client_id")
		state := query.Get("state")
		responseType := "code"
		// here we use the redirect_uri param to transmit our stack url
		// We keep only the instance name to not reach the 100 max char of redirectUrl
		cozyOrigin := query.Get("redirect_uri")
		splitIndexStart := strings.Index(cozyOrigin, ":")
		if splitIndexStart == -1 {
			log.Error("redirect_uri bad format " + cozyOrigin)
			http.Error(w, http.StatusText(500), 500)
		}
		splitIndexEnd := strings.Index(cozyOrigin, ".")
		if splitIndexEnd == -1 {
			log.Error("redirect_uri bad format " + cozyOrigin)
			http.Error(w, http.StatusText(500), 500)
		}
		instanceName := cozyOrigin[splitIndexStart+3:splitIndexEnd]

		// DEV API
		// authURL := "https://gw.hml.api.enedis.fr/dataconnect/v1/oauth2/authorize"
		// PROD API
		authURL := "https://mon-compte-particulier.enedis.fr/dataconnect/v1/oauth2/authorize"

		redirectUrl := authURL + "?client_id=" + clientId + "&duration=P6M&response_type=" + responseType + "&state=" + state + "-" + instanceName

		log.Debug("Redirect to - ", redirectUrl)
		http.Redirect(w, r, redirectUrl, 302)
	})

	// GRDF ADICT AUTHORIZE ENDPOINT
	mux.HandleFunc("/grdf_authorize", func(w http.ResponseWriter, r *http.Request) {
		log.Debug("New grdf auth request")
		query := r.URL.Query()
		log.Debug("Query received - ", query)

		clientId := query.Get("client_id")
		state := query.Get("state")
		cozyOrigin := query.Get("redirect_uri")
		splitIndexStart := strings.Index(cozyOrigin, ":")
		if splitIndexStart == -1 {
			log.Error("redirect_uri bad format " + cozyOrigin)
			http.Error(w, http.StatusText(500), 500)
		}
		splitIndexEnd := strings.Index(cozyOrigin, ".")
		if splitIndexEnd == -1 {
			log.Error("redirect_uri bad format " + cozyOrigin)
			http.Error(w, http.StatusText(500), 500)
		}
		instanceName := cozyOrigin[splitIndexStart+3:splitIndexEnd]

		redirectProxy := "https://oauth-proxy.wf.alpha.grandlyon.com/redirect-grdf"
		authURL := "https://sofit-sso-oidc.grdf.fr/openam/oauth2/realms/externeGrdf/authorize"

		redirectUrl := authURL + "?client_id=" + clientId + "&scope=openid&response_type=code&redirect_uri="+ redirectProxy + "&login_hint=Sebastien|Riera|sebastien.riera@laposte.net|Ecolyo&state=" + state + "-" + instanceName

		// TODO Add Login Hint in request
		log.Debug("Redirect to - ", redirectUrl)
		http.Redirect(w, r, redirectUrl, 302)
	})

	//ENEDIS REDIRECT ENDPOINT
	mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
		log.Debug("New redirect request")
		query := r.URL.Query()
		log.Debug(query)

		code := query.Get("code")
		req_state := query.Get("state")
		statusCodes := [4]string{"400", "403", "500", "503"}

		if (findItem(statusCodes, code)) {
			intCode, err := strconv.Atoi(code)
			if err != nil {
				log.Print("status code string to int error: ", err)
			}
			log.Print("status code error : ", code)
			http.Error(w, http.StatusText(intCode), intCode)
		} else {
			splitIndex := strings.Index(req_state, "-")
			if splitIndex == -1 {
				log.Warning("No host found")
			}
			state := req_state[0:splitIndex]
			host := req_state[splitIndex+1:]

			usagePointId := query.Get("usage_point_id")

			cozyURL := "https://" + host + "." + *cozyDomain + *cozyRedirectURI

			redir := cozyURL + "?code=" + code + "&state=" + state + "&usage_point_id=" + usagePointId
			log.Debug("Redirect to - ", redir)
			http.Redirect(w, r, redir, 302)
		}
	})

	//GRDF REDIRECT ENDPOINT
	mux.HandleFunc("/redirect-grdf", func(w http.ResponseWriter, r *http.Request) {
		log.Debug("New redirection on grdf-redirect")
		query := r.URL.Query()
		log.Debug(query)

		code := query.Get("code")
		req_state := query.Get("state")
		//TODO Get pce_id
		splitIndex := strings.Index(req_state, "-")
		if splitIndex == -1 {
			log.Warning("No host found")
		}
		state := req_state[0:splitIndex]
		host := req_state[splitIndex+1:]

		cozyURL := "https://" + host + "." + *cozyDomain + *cozyGrdfRedirectURI

		redir := cozyURL + "?code=" + code + "&state=" + state
		log.Debug("Redirect to  -", redir)
		http.Redirect(w, r, redir, 302)
	})

	//ENEDIS TOKEN ENDPOINT
	mux.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
		log.Debug("New token request")
		query := r.URL.Query()
		log.Debug(query)

		clientId := ""
		clientSecret := ""
		code := ""
		grantType := ""
		refreshToken := ""

		// For request token params are into query parameters
		if len(query) == 0 {
			log.Warn("No params found in url query - Trying to catch them from body")
			contents, err := ioutil.ReadAll(r.Body)
			if err != nil {
				log.Error(err)
			}
			pageContent := string(contents)
			//Check for client_id
			clientIdStartIndex := strings.Index(pageContent, "client_id=")
			if clientIdStartIndex == -1 {
				log.Error("No client_id found")
				http.Error(w, http.StatusText(500), 500)
			}
			clientIdStartIndex += 10
			clientId = pageContent[clientIdStartIndex : clientIdStartIndex+36]
			//Check for client_secret
			clientSecretStartIndex := strings.Index(pageContent, "client_secret=")
			if clientSecretStartIndex == -1 {
				log.Error("No client_secret found")
				http.Error(w, http.StatusText(500), 500)
			}
			clientSecretStartIndex += 14
			clientSecret = pageContent[clientSecretStartIndex : clientSecretStartIndex+36]
			//Check for code
			codeStartIndex := strings.Index(pageContent, "code=")
			if codeStartIndex == -1 {
				log.Info("No code found (optional param)")
			} else {
				codeStartIndex += 5
				code = pageContent[codeStartIndex : codeStartIndex+30]
			}
			//Check for grant_type
			grandTypeStartIndex := strings.Index(pageContent, "grant_type=")
			if grandTypeStartIndex == -1 {
				log.Error("No grant_type found")
				http.Error(w, http.StatusText(500), 500)
			}
			grandTypeStartIndex += 11
			tempGrandTypeString := pageContent[grandTypeStartIndex:]
			grandTypeEndIndex := strings.Index(tempGrandTypeString, "&")
			if grandTypeEndIndex == -1 {
				log.Error("No closing tag for grant_type found")
				http.Error(w, http.StatusText(500), 500)
			}
			grantType = tempGrandTypeString[0:grandTypeEndIndex]
			//Check for refresh_token
			refershTokenStartIndex := strings.Index(pageContent, "refresh_token=")
			if refershTokenStartIndex == -1 {
				log.Error("No refresh_token found")
				http.Error(w, http.StatusText(500), 500)
			}
			refershTokenStartIndex += 14
			refreshToken = pageContent[refershTokenStartIndex : refershTokenStartIndex+46]
		} else {
			// Retrieve params from query
			clientId = query.Get("client_id")
			clientSecret = query.Get("client_secret")
			code = query.Get("code")
			grantType = query.Get("grant_type")
			refreshToken = query.Get("refresh_token")
		}
		// Print out the result
		log.WithFields(log.Fields{
			"client_id":     clientId,
			"client_secret": clientSecret,
			"code":          code,
			"grant_type":    grantType,
			"refresh_token": refreshToken,
		}).Debug("result")

		// DEV API
		// tokenUrl := "https://gw.hml.api.enedis.fr/v1/oauth2/token"
		// PROD API
		tokenUrl := "https://gw.prd.api.enedis.fr/v1/oauth2/token"

		data := url.Values{}
		data.Set("client_id", clientId)
		data.Set("client_secret", clientSecret)
		data.Set("code", code)
		data.Set("grant_type", grantType)
		if refreshToken != "" {
			data.Set("refresh_token", refreshToken)
			data.Set("grant_type", "refresh_token")
		}

		log.Debug("Send request to token endpoint: ", tokenUrl)
		response, err := http.PostForm(tokenUrl, data)
		if err != nil {
			log.Error(err)
		} else {
			log.Debug("Endpoint response with status ", response.Status)
			defer response.Body.Close()
			if response.StatusCode >= 200 && response.StatusCode <= 299 {
				// Set Content-Type in response header
				w.Header().Add("Content-Type", "application/json")

				// Decode response Body using the defined type "TokenResponse"
				data := EnedisTokenResponse{}
				decodeError := json.NewDecoder(response.Body).Decode(&data)
				if decodeError != nil {
					http.Error(w, decodeError.Error(), 500)
					return
				}

				// Response with json data
				jsonError := json.NewEncoder(w).Encode(data)
				if jsonError != nil {
					http.Error(w, jsonError.Error(), 500)
					return
				}
			} else {
				http.Error(w, http.StatusText(response.StatusCode), response.StatusCode)
			}
		}
	})

	//GRDF TOKEN ENDPOINT
	mux.HandleFunc("/grdf_token", func(w http.ResponseWriter, r *http.Request) {
		log.Debug("New GRDF token request")
		query := r.URL.Query()
		log.Debug(query)

		clientId := ""
		clientSecret := ""
		code := ""
		grantType := ""
		scope := ""
		redirectUri := "https://oauth-proxy.wf.alpha.grandlyon.com/redirect-grdf"
		pce := ""
		IdToken := ""

		// For request token params are into query parameters
		if len(query) == 0 {
			log.Warn("No params found in url query \nStack probably asks for a refresh token \nTrying to catch them from body")
			contents, err := ioutil.ReadAll(r.Body)
			if err != nil {
				log.Error(err)
			}
			pageContent := string(contents)
			//Check for client_id
			clientIdKey := "client_id="
			clientIdLength := 22
			clientIdStartIndex := strings.Index(pageContent, clientIdKey)
			if clientIdStartIndex == -1 {
				log.Error("No client_id found")
				http.Error(w, http.StatusText(500), 500)
			}
			clientIdStartIndex += len(clientIdKey)
			clientId = pageContent[clientIdStartIndex : clientIdStartIndex+clientIdLength]
			//Check for client_secret
			clientSecretKey := "client_secret="
			clientSecretLength := 14
			clientSecretStartIndex := strings.Index(pageContent, clientSecretKey)
			if clientSecretStartIndex == -1 {
				log.Error("No client_secret found")
				http.Error(w, http.StatusText(500), 500)
			}
			clientSecretStartIndex += len(clientSecretKey)
			clientSecret = pageContent[clientSecretStartIndex : clientSecretStartIndex+clientSecretStartIndex]
			//Check for code
			codeKey := "code="
			codeLength := 27
			codeStartIndex := strings.Index(pageContent, codeKey)
			if codeStartIndex == -1 {
				log.Info("No code found (optional param)")
			} else {
				codeStartIndex += len(codeKey)
				code = pageContent[codeStartIndex : codeStartIndex+codeLength]
			}
			//Check for grant_type
			grandTypeKey := "grant_type="
			grandTypeLength := 27
			grandTypeStartIndex := strings.Index(pageContent, grandTypeKey)
			if grandTypeStartIndex == -1 {
				log.Error("No grant_type found")
				http.Error(w, http.StatusText(500), 500)
			}
			grandTypeStartIndex += len(grandTypeKey)
			tempGrandTypeString := pageContent[grandTypeStartIndex:]
			grandTypeEndIndex := strings.Index(tempGrandTypeString, "&")
			if grandTypeEndIndex == -1 {
				log.Error("No closing tag for grant_type found")
				http.Error(w, http.StatusText(500), 500)
			}
			grantType = tempGrandTypeString[0:grandTypeEndIndex]

		} else {
			// Retrieve params from query
			clientId = query.Get("client_id")
			clientSecret = query.Get("client_secret")
			code = query.Get("code")
			grantType = query.Get("grant_type")
		}


		// Print out the result
		log.WithFields(log.Fields{
			"client_id":     clientId,
			"client_secret": clientSecret,
			"code":          code,
			"grant_type":    grantType,
			"redirect_uri":	 redirectUri,
			"scope": scope,
		}).Debug("result")

		tokenUrl := "https://sofit-sso-oidc.grdf.fr/openam/oauth2/realms/externeGrdf/access_token"

		if grantType != "refresh_token"{
			// Call GRDF access_token endpoint with code & grant_type = "authorization_code"
			data := url.Values{}
			data.Set("client_id", clientId)
			data.Set("client_secret", clientSecret)
			data.Set("grant_type", "authorization_code")
			data.Set("redirect_uri", redirectUri)
			data.Set("code", code)
			log.Debug("data sent is : ", data)
			log.Debug("Send request to access_token endpoint with authorization_code: ", tokenUrl)
			response, err := http.PostForm(tokenUrl, data)
			if err != nil {
				log.Error(err)
			} else {
				log.Debug("Endpoint response with status ", response.Status)
				defer response.Body.Close()
				if response.StatusCode >= 200 && response.StatusCode <= 299 {
					// Decode response Body using the defined type "GrdfTokenResponse"
					data := GrdfTokenResponse{}
					decodeError := json.NewDecoder(response.Body).Decode(&data)
					if decodeError != nil {
						http.Error(w, decodeError.Error(), 500)
						return
					}
					// Check if IdToken exist
					// Decode the token and retrieve the pce from it
					if len(data.IdToken) > 0 {
						IdToken = data.IdToken
						s := strings.Split(IdToken, ".")
						if len(s[1]) > 0 {
							payload, _ := base64.StdEncoding.DecodeString(s[1])
							log.Debug(string(payload))
							// Decode the payload from the token
							var token GrdfConsentementToken
							err := json.Unmarshal(payload, &token)
							if err != nil {
								log.Error(err.Error())
								http.Error(w, err.Error(), 500)
								return
							}
							log.Debug("Consentements found : ", token.Consentements)
							// Decode the consentement information
							if len(token.Consentements) > 0 {
								var consentements GrdfConsentement
								err2 := json.Unmarshal([]byte(token.Consentements), &consentements)
								if err2 != nil {
									log.Error(err2.Error())
									http.Error(w, err2.Error(), 500)
									return
								}
								if len(consentements[0].Pce) > 0 {
									pce = consentements[0].Pce
								}
							}
						}
					}
					if len(pce) <= 0 {
						log.Error("No PCE found")
						http.Error(w, http.StatusText(500), 500)
						return
					}
				} else {
					http.Error(w, http.StatusText(response.StatusCode), response.StatusCode)
				}
			}
		}

		// Call GRDF access_token endpoint with scope & grant_type = "client_credentials"
		data2 := url.Values{}
		data2.Set("client_id", clientId)
		data2.Set("client_secret", clientSecret)
		data2.Set("grant_type", "client_credentials")
		data2.Set("redirect_uri", redirectUri)
		data2.Set("scope", "/adict/v1")

		log.Debug("data sent is : ", data2)
		log.Debug("Send request to access_token endpoint with client_credentials: ", tokenUrl)
		response2, err2 := http.PostForm(tokenUrl, data2)
		if err2 != nil {
			log.Error(err2)
		} else {
			log.Debug("Endpoint response with status ", response2.Status)
			defer response2.Body.Close()
			if response2.StatusCode >= 200 && response2.StatusCode <= 299 {
				// Set Content-Type in response header
				w.Header().Add("Content-Type", "application/json")

				// Decode response Body using the defined type "GrdfTokenResponse"
				data := GrdfTokenResponse{}
				decodeError := json.NewDecoder(response2.Body).Decode(&data)
				if decodeError != nil {
					http.Error(w, decodeError.Error(), 500)
					return
				}
				if grantType != "refresh_token"{
					data.RefreshToken = "-"
					data.Pce = pce
					data.IdToken = IdToken
				}
				
				jsonError := json.NewEncoder(w).Encode(data)
				if jsonError != nil {
					http.Error(w, jsonError.Error(), 500)
					return
				}
			} else {
				http.Error(w, http.StatusText(response2.StatusCode), response2.StatusCode)
			}
		}
	})

	log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*httpPort), mux))
}