Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
package auth
import (
"strings"
"sync"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"
)
var (
ConsumerPoolId string
cognitoOnce sync.Once
cognitoInstance *cognitoidentityprovider.CognitoIdentityProvider
)
func GetUsersList(users []string) ([]common.JSONUserProfile, error) {
var userList []common.JSONUserProfile
for _, userid := range users {
cognitoClient := getAWS()
filter := strings.Replace("username = \"userId\"", "userId", userid, -1)
request := &cognitoidentityprovider.ListUsersInput{
Filter: &filter,
UserPoolId: &ConsumerPoolId,
}
resp, err := cognitoClient.ListUsers(request)
if err != nil {
return nil, err
}
userList = append(userList, convertAWSUsers(resp.Users)...)
}
return userList, nil
}
func convertAWSUsers(users []*cognitoidentityprovider.UserType) []common.JSONUserProfile {
var userList []common.JSONUserProfile
for _, user := range users {
userList = append(userList, findUserAttributes(user))
}
return userList
}
func findUserAttributes(awsUser *cognitoidentityprovider.UserType) common.JSONUserProfile {
attributes := awsUser.Attributes
user := common.JSONUserProfile{}
user.UserName = *awsUser.Username
for _, attribute := range attributes {
switch *attribute.Name {
case "email":
user.Email = *attribute.Value
case "phone_number":
user.Phone = *attribute.Value
case "given_name":
user.FirstName = *attribute.Value
case "family_name":
user.LastName = *attribute.Value
}
}
return user
}
func getAWS() *cognitoidentityprovider.CognitoIdentityProvider {
cognitoOnce.Do(func() {
if cognitoInstance != nil {
return
}
logger.Info().Msg("Init cognito provider instance")
setPoolId()
mySession := session.Must(session.NewSession())
cognitoInstance = cognitoidentityprovider.New(mySession, aws.NewConfig().WithRegion("us-west-2"))
})
return cognitoInstance
}
func setPoolId() {
//default to dev pool
ConsumerPoolId = envtool.GetEnv("CONSUMER_COGNITO_CLIENT_ID", "us-west-2_c7Qu91m3J")
}

144
pkg/auth/user.go Normal file
View File

@@ -0,0 +1,144 @@
package auth
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/jwt"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"fiskerinc.com/modules/utils"
)
var getUserURL string = envtool.GetEnv("AUTH_GET_USER", "https://dev-auth.fiskerdps.com/auth/me")
func AppendUserMiddleware(next http.HandlerFunc, apiCalls queries.APICallsInterface) http.HandlerFunc {
wrapper := func(w http.ResponseWriter, r *http.Request) {
_, err := jwt.GetAuthorizationHeader(r)
if err != nil {
logger.Warn().Err(err).Msgf("token invalid %s %s", r.Method, r.RequestURI)
utils.RespError(w, http.StatusUnauthorized, err.Error())
return
}
// go to auth to get user information
req, _ := http.NewRequest("GET", getUserURL, nil)
req.Header.Set("Authorization", r.Header.Get("Authorization"))
resp, err := httpclient.Do(req)
if err != nil {
logger.Warn().Err(err).Msgf("Unable to fetch user %s %s", r.Method, r.RequestURI)
utils.RespError(w, http.StatusUnauthorized, err.Error())
return
}
defer resp.Body.Close()
if resp != nil && resp.StatusCode != 200 {
logger.Warn().Err(err).Msgf("Unable to fetch user %s %s", r.Method, r.RequestURI)
if err != nil {
utils.RespError(w, http.StatusUnauthorized, err.Error())
} else {
utils.RespError(w, http.StatusUnauthorized, resp.Status)
}
return
}
user, err := extractUserAttributes(resp)
if err != nil {
logger.Warn().Err(err).Msgf("Unable to parse user response %s %s", r.Method, r.RequestURI)
utils.RespError(w, http.StatusUnauthorized, err.Error())
return
}
err = logAPICall(user, r.RequestURI, r.Method, apiCalls)
if err != nil {
logger.Warn().Msgf("Call log %s %s '%v'", r.Method, r.RequestURI, err)
}
ctx := r.Context()
ctx = context.WithValue(ctx, "identity", user)
next.ServeHTTP(w, r.WithContext(ctx))
}
return wrapper
}
func AppendUserTokenMiddleware(next http.HandlerFunc, apiCalls queries.APICallsInterface) http.HandlerFunc {
wrapper := func(w http.ResponseWriter, r *http.Request) {
token, err := jwt.GetAuthorizationHeader(r)
if err != nil {
logger.Warn().Err(err).Msgf("token invalid %s %s", r.Method, r.RequestURI)
utils.RespError(w, http.StatusUnauthorized, err.Error())
return
}
valid := jwt.NewJWTValidator("")
_, err = valid.ValidateToken(token.Token)
if err != nil {
logger.Warn().Err(err).Msgf("token invalid %s %s", r.Method, r.RequestURI)
utils.RespError(w, http.StatusUnauthorized, err.Error())
return
}
payload, err := jwt.GetPayload(token.Token)
if err != nil {
logger.Warn().Err(err).Msgf("token invalid %s %s", r.Method, r.RequestURI)
utils.RespError(w, http.StatusUnauthorized, err.Error())
return
}
err = logAPICall(payload, r.RequestURI, r.Method, apiCalls)
if err != nil {
logger.Warn().Msgf("Call log %s %s '%v'", r.Method, r.RequestURI, err)
}
ctx := r.Context()
ctx = context.WithValue(ctx, "identity", payload)
next.ServeHTTP(w, r.WithContext(ctx))
}
return wrapper
}
func extractUserAttributes(resp *http.Response) (map[string]interface{}, error) {
user := make(map[string]interface{})
err := json.NewDecoder(resp.Body).Decode(&user)
if err != nil {
return nil, err
}
username, ok := user["id"]
if !ok {
return nil, errors.New("invalid user token")
}
user["username"] = username
return user, nil
}
func logAPICall(payload map[string]interface{}, uri string, method string, apiCalls queries.APICallsInterface) error {
var (
username string
ok bool
)
if username, ok = payload["username"].(string); !ok || username == "" {
return nil
}
endpoint, _, _ := strings.Cut(uri, "?")
_, err := apiCalls.Insert(common.APICall{
ClientID: username,
AccessType: common.AccessTypeJWT,
Endpoint: endpoint,
Method: method,
})
return err
}

View File

@@ -0,0 +1,140 @@
package auth
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/jwt"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"github.com/pkg/errors"
)
var cognitoUserConsentClientID = envtool.GetEnv("COGNITO_USER_CONSENT_CLIENT_ID", "REPLACE_ME")
var cognitoUserConsentClientSecret = envtool.GetEnv("COGNITO_USER_CONSENT_CLIENT_SECRET", "REPLACE_ME")
var cognitoUserConsentAuthURL = envtool.GetEnv("COGNITO_USER_CONSENT_AUTH_URL", "REPLACE_ME")
var CognitoUserConsentJWT CognitoJWT
type CognitoJWT struct {
token string
expiration *time.Time
mu sync.RWMutex
}
func (c *CognitoJWT) GetUserConsentToken() (string, error) {
c.mu.Lock()
defer c.mu.Unlock()
// check for an unexpired token. if it is there, return it
if c.token != "" && time.Now().Before(*c.expiration) {
return c.token, nil
}
// make a request to Cognito to get the token
authResponse, err := RequestUserConsentToken()
if err != nil {
return "", err
}
// cache the token and expire time
c.token = authResponse.AccessToken.JWTToken
c.expiration = &authResponse.ExpireTime
return c.token, nil
}
func RequestUserConsentToken() (AuthResponse, error) {
var resp AuthTokens
var tokens AuthResponse
tokenReq, err := getUserConsentTokenRequest()
if err != nil {
return tokens, err
}
tokenRes, err := httpclient.Do(tokenReq)
if err != nil {
return tokens, err
}
defer tokenRes.Body.Close()
err = json.NewDecoder(tokenRes.Body).Decode(&resp)
if err != nil {
logger.Error().Err(err).Send()
}
if len(resp.Error) > 0 {
return tokens, errors.New(resp.Error)
}
return getAuthResponse(&resp), nil
}
// getUserConsentTokenRequest returns http request to exchange code for a user consent token
func getUserConsentTokenRequest() (*http.Request, error) {
basicAuth := base64.StdEncoding.EncodeToString([]byte(strings.Join([]string{cognitoUserConsentClientID, ":", cognitoUserConsentClientSecret}, "")))
body := url.Values{
"client_id": {cognitoUserConsentClientID},
"grant_type": {"client_credentials"},
}
r, err := http.NewRequest(http.MethodPost, getUserConsentTokenURL(), strings.NewReader(body.Encode()))
if err != nil {
return nil, errors.WithStack(err)
}
r.Header.Add("Authorization", fmt.Sprintf("Basic %s", basicAuth))
r.Header.Add("Content-type", "application/x-www-form-urlencoded")
return r, nil
}
// GetAuthResponse converts AuthTokens response from Cognito to include decoded payload
func getAuthResponse(data *AuthTokens) AuthResponse {
var result AuthResponse
result.AccessToken.JWTToken = data.AcessToken
result.IDToken.JWTToken = data.IDToken
result.RefreshToken.Token = data.RefreshToken
// calculate the expire time of the token, leaving off one minute of wiggle room
result.ExpireTime = time.Now().Add(time.Duration(data.ExpiresIn)*time.Second - time.Minute)
return result
}
func getUserConsentTokenURL() string {
return cognitoUserConsentAuthURL + "/oauth2/token"
}
// AuthTokens json response for auth tokens
type AuthTokens struct {
AcessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
Error string `json:"error,omitempty"`
}
// JWTResponse json response
type JWTResponse struct {
JWTToken string `json:"jwtToken"`
}
// AuthResponse json response
type AuthResponse struct {
AccessToken JWTResponse `json:"accessToken,omitempty"`
IDToken JWTResponse `json:"idToken,omitempty"`
RefreshToken jwt.AuthToken `json:"refreshToken,omitempty"`
ExpireTime time.Time `json:"expire_time"`
}

View File

@@ -0,0 +1,57 @@
package auth_test
import (
"bytes"
"io/ioutil"
"net/http"
"testing"
auth "fiskerinc.com/modules/auth"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/httpclient/mock"
"fiskerinc.com/modules/testhelper"
)
const responseUserConsentJSON = `{"access_token":"eyJraWQiOiJqSXowUVRjc0tDVCtoeEd6MlMwK0NoUHlON3c4cmlQXC9sNm1xekFYUmw2bz0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIyZGQ2ZmVkOS1lNTgyLTQ1MWItYTkzYi01Yjk0MTBkZmJjNDMiLCJjb2duaXRvOmdyb3VwcyI6WyJ1cy13ZXN0LTJfQVd3akxYeW0yX0F6dXJlQUQiXSwidG9rZW5fdXNlIjoiYWNjZXNzIiwic2NvcGUiOiJodHRwczpcL1wvZmlza2VyaW5jLmNvbVwvb3RhdXBkYXRlLnJlYWQgaHR0cHM6XC9cL2Zpc2tlcmluYy5jb21cL290YXVwZGF0ZS5jcmVhdGUgb3BlbmlkIGVtYWlsIiwiYXV0aF90aW1lIjoxNjEzNjA3NTc2LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtd2VzdC0yLmFtYXpvbmF3cy5jb21cL3VzLXdlc3QtMl9BV3dqTFh5bTIiLCJleHAiOjE2MTM2MTExNzYsImlhdCI6MTYxMzYwNzU3NiwidmVyc2lvbiI6MiwianRpIjoiYzUyNjI0YjItYmJkYi00N2RiLTllNTgtOGU5ZmU3Yjg1ODMxIiwiY2xpZW50X2lkIjoiN2NrMnRmb3FhdmM3MmM0NWhoN3RnZTQya2QiLCJ1c2VybmFtZSI6ImF6dXJlYWRfand1QGZpc2tlcmluYy5jb20ifQ.FvlES5AgjhymQKnHP41D2Ude0Ten6L8REBRXTyu5dyWGrG4vTfBGoxlkGE2-MEFc0s6uhbdST_E2Mc5QNlXG47ibK14tFl6kOqDd74TCfg5sWghb_nSjC-M769eUHQSQcs4L8jcnEt0bjqMmPtt8lZwu3VS7mkSRXD6_hX43rPLGUpMaz5RqKlfHX8YUyD6UnENW9Gg3zonPRsPWVtupc494B_pSZGuFs-jVzBDgb_SdrGt5wb3GazsNcB8KeAf0m0QoEiApsCYxKGUG9eQZw_CAUrhCj9mFT-xJuyvEp0t6B8HDHrdW4mIHblKqhZok1mPwCntJmOfyOs3niNaILg","id_token":"eyJraWQiOiJlUTNuZFJLaUVcL084VUZ5RHFsYjN0S1RzWG00SzVPMlc4NXd3VWkzT2tNZz0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiMHFmcmdyVlZfOW1XRWp5MVdOZDl5QSIsInN1YiI6IjJkZDZmZWQ5LWU1ODItNDUxYi1hOTNiLTViOTQxMGRmYmM0MyIsImNvZ25pdG86Z3JvdXBzIjpbInVzLXdlc3QtMl9BV3dqTFh5bTJfQXp1cmVBRCJdLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX0FXd2pMWHltMiIsImNvZ25pdG86dXNlcm5hbWUiOiJhenVyZWFkX2p3dUBmaXNrZXJpbmMuY29tIiwiYXVkIjoiN2NrMnRmb3FhdmM3MmM0NWhoN3RnZTQya2QiLCJpZGVudGl0aWVzIjpbeyJ1c2VySWQiOiJqd3VAZmlza2VyaW5jLmNvbSIsInByb3ZpZGVyTmFtZSI6IkF6dXJlQUQiLCJwcm92aWRlclR5cGUiOiJTQU1MIiwiaXNzdWVyIjoiaHR0cHM6XC9cL3N0cy53aW5kb3dzLm5ldFwvNWFhNGI2NDAtYzlmYy00YTliLWIzYTMtZDRhN2QwMDhmYjVlXC8iLCJwcmltYXJ5IjoidHJ1ZSIsImRhdGVDcmVhdGVkIjoiMTYxMjkwMjQxMzM4MyJ9XSwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE2MTM2MDc1NzYsImV4cCI6MTYxMzYxMTE3NiwiaWF0IjoxNjEzNjA3NTc2LCJlbWFpbCI6Imp3dUBmaXNrZXJpbmMuY29tIn0.NbEWEgX48Z-zz3gREEH44OpnvhoYDcm9RlVdqKVoSJ777g0A0LDpGwz7UGcqvZLeQLPsHaMyV8-sblLvKQvpsenJfq81XddVWCAqI55VCdbnouCphIDYOEPNbWs9ORdrXxciALTt1AAehsF0dTDG0V5fce5Vku2qZZbpELdq9r4CBJQXWtFiV8lUaZPEMNJbZVdh1KjwJSpeF8CtJGKUXIIm6tAYVVKc27YWgxe2fh3zhke5MUnGYrb98-RLmDUwpUQ4eBnXu9gtA-9qIpOumXkftogWpeNZ7Rc0tAI8ZvOmG8plFyYoRMrKuC4kECeUdrsRJlCv4ijpK_L7GwEL9Q","refresh_token":"eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.LHo8ysGz7T3sJtwf8aHpWFzH8B84yDvfL4Q0YuRd0kfKSA51z4hKFPSLpo4PiFodJ-VPugJQWfSYXXpe4Tjd3bdTH-oYDJcJvRHIV3ZIID0EApt53lkxsFWV_9b33bltLYyJ7DnclQq1GnfgohDhD9F2CpnN3Xa-ntVmF9ntLe6wxZvk_zdBlbhIPwCc4FuPDIB_skNaciWCzU9LUfzvcfZJAR8KztM8ofDm3YJGZrRJltz6In78ZlN1sIlFuPSIRy56sg3yG3wMfe9Lrst0VacG_2fy6Ccg9VuLqD_xnzMmzjwMF9PGdnO5DlCblWwrHsDE6FkTuDy7ojnPJpPJlw.gzFbDwAmMKp-4eiE.AfY0IecXygmkDUkUGIIG7JmhBSzk4VD_sEAwuTeOufKD_duvNXFTQYNU_QvDc7M-9Vssbbb35dMMw3KLxW2IbC7fll8lNvHHMm1gkxlVxK1h5uRhmgt0q7tyMwLw1iKUDqOa177faHZJISN_gvfh-rlbNswswDGU061dyFh-w6Ck8SXoPnWfp9GxZJBgxzZ5uBV1D7_1bAghqWYNMsMUTSvOYyeWvVJHap-gjtGc491Vf97z6mh9PDBvIi734D90NbV5idZ11CCW7liI5L7kgRwuHZVxiu_NpkPED7dWcaBhOATur4r3P28U39JC5P5FD4JXlqyPl9FXVBkW049E1vdJrrkV3IbiqUMVXlkUeq6G87YUTdmt8qRPgiOc-G6g84RxSPQE55uojbuSSlON2CKZYmSmFVM0X7bBU42wP1wNP7Jq3LTjHcj4rOaN1ozffJxyGs54r7NP4D9u3nt2ozNkjk_DNK3UmxDPaQtZAtFO1d-T7UXv2BvzoCN2LGilzxVi04p9LcvoTDzI5GUY9OsjGUsdSZJvISylHAMMDi8nSxsBBSPD18fzV0tLhdjGM0-XljiM4bjZWNR4Nvraus33p8U4k5lmn2bx13JfHvDa3Zqf_aK57lam6Zf_6mvAK4I7A40WmiolJCxeEeDD54ljF0kAluT4sw6sxVY8It80A95TGFd0lm5e-tGrFKIoqRyPV5uwzzz3XT1HVPJda1ufdGhSUj8slsyqTUrnphh6JWbRfA9mrLdKQKuqM3xEslAwYZOhX6qOzADbo5WQMneTwn34QixMT4A6imaDBc6P4cOaLo7hNyS3e1h6SfwigEX9H42wkC3TWOiITakFq3tKkVwahMkdeds_uxloNoeicdGePjob6BfU8xq0IKxJh9UoeCsSX4KVtIrErHyYuoU-_ENZXYArSwfqorKgdjmQAa13NQjOiHzpgA5HngSCK1xy6zq9NvNA7hUe9O0gTqrFZDsYhRWSYEuOt5QpxJYalPGKIPXlsUJOURfR_J0iT52UxiOZIuXmpk-X8fgDhM_0fZm8GQ2GaIsf3nR49h7QnZRG9azTZV5q9Bs0bqPvP7wRL5xenByeIBsdwP6Bwaqd3n5BkFn-LE7eo6UPn_9o7Gx3g9VlN8pG7SIo_3a07L0yauJIO4ahL5aC07uBCLu3pJQW0ftlVpLdAA8gh3XPhfvOuH4XV7yU1fQhqGKich1hhHx2dFHyVr5mJPxc1VQvAyyAhxjvyQ2TTLfvSiYLOP1vFCVjUwb_RjF5tuh3ArH9IlXH9kIBVQbiSOWfgQ1PqD-go4jqDR_ie3aGc5Fm8Vd6lSh_2HW2GR0Ht0ASoCbl3C5roXCRkyTTaGl3nX6uwfg.thw4B0ug4OIZsZrDzCtJcQ","expires_in": 3600,"token_type": "Bearer"}`
func TestHandleUserConsentToken(t *testing.T) {
setupUserConsentMockAuthClient()
authResp, err := auth.RequestUserConsentToken()
if err != nil {
t.Errorf("should not return an error")
}
const someValueCondition = "Some value"
if len(authResp.AccessToken.JWTToken) == 0 {
t.Errorf(testhelper.TestErrorTemplate, "Access token", someValueCondition, authResp.AccessToken.JWTToken)
}
if len(authResp.IDToken.JWTToken) == 0 {
t.Errorf(testhelper.TestErrorTemplate, "ID token", someValueCondition, authResp.IDToken.JWTToken)
}
if len(authResp.RefreshToken.Token) == 0 {
t.Errorf(testhelper.TestErrorTemplate, "Refresh token", someValueCondition, authResp.RefreshToken.Token)
}
token, err := auth.CognitoUserConsentJWT.GetUserConsentToken()
if err != nil {
t.Errorf(err.Error())
}
if token != authResp.AccessToken.JWTToken {
t.Errorf(testhelper.TestErrorTemplate, "Access token", someValueCondition, authResp.AccessToken.JWTToken)
}
}
func setupUserConsentMockAuthClient() {
httpclient.Client = &mock.Client{
DoFunc: func(r *http.Request) (*http.Response, error) {
return &http.Response{
Body: ioutil.NopCloser(bytes.NewBufferString(responseUserConsentJSON)),
}, nil
},
}
}