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"` }