github.com/schmorrison/Zoho@v1.1.4/oauth2.go (about)

     1  package zoho
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  	"time"
    13  )
    14  
    15  func (z *Zoho) SetRefreshToken(refreshToken string) {
    16  	z.oauth.token.RefreshToken = refreshToken
    17  }
    18  
    19  // GetRefreshToken is used to obtain the oAuth2 refresh token
    20  func (z *Zoho) GetRefreshToken() string {
    21  	return z.oauth.token.RefreshToken
    22  }
    23  
    24  func (z *Zoho) SetClientID(clientID string) {
    25  	z.oauth.clientID = clientID
    26  }
    27  
    28  func (z *Zoho) SetClientSecret(clientSecret string) {
    29  	z.oauth.clientSecret = clientSecret
    30  }
    31  
    32  func (z *Zoho) RefreshTokenURL() string {
    33  	q := url.Values{}
    34  	q.Set("client_id", z.oauth.clientID)
    35  	q.Set("client_secret", z.oauth.clientSecret)
    36  	q.Set("refresh_token", z.oauth.token.RefreshToken)
    37  	q.Set("grant_type", "refresh_token")
    38  
    39  	return fmt.Sprintf("%s%s?%s", z.oauth.baseURL, oauthGenerateTokenRequestSlug, q.Encode())
    40  }
    41  
    42  // RefreshTokenRequest is used to refresh the oAuth2 access token
    43  func (z *Zoho) RefreshTokenRequest() (err error) {
    44  	tokenURL := z.RefreshTokenURL()
    45  	resp, err := z.client.Post(tokenURL, "application/x-www-form-urlencoded", nil)
    46  	if err != nil {
    47  		return fmt.Errorf("Failed while requesting refresh token: %s", err)
    48  	}
    49  
    50  	defer func() {
    51  		if err := resp.Body.Close(); err != nil {
    52  			fmt.Printf("Failed to close request body: %s\n", err)
    53  		}
    54  	}()
    55  
    56  	body, err := ioutil.ReadAll(resp.Body)
    57  	if err != nil {
    58  		return fmt.Errorf(
    59  			"Failed to read request body on request to %s%s: %s",
    60  			z.oauth.baseURL,
    61  			oauthGenerateTokenRequestSlug,
    62  			err,
    63  		)
    64  	}
    65  
    66  	if resp.StatusCode != 200 {
    67  		return fmt.Errorf(
    68  			"Got non-200 status code from request to refresh token: %s\n%s",
    69  			resp.Status,
    70  			string(body),
    71  		)
    72  	}
    73  
    74  	tokenResponse := AccessTokenResponse{}
    75  	err = json.Unmarshal(body, &tokenResponse)
    76  	if err != nil {
    77  		return fmt.Errorf(
    78  			"Failed to unmarshal access token response from request to refresh token: %s",
    79  			err,
    80  		)
    81  	}
    82  	//If the tokenResponse is not valid it should not update local tokens
    83  	if tokenResponse.Error == "invalid_code" {
    84  		return ErrTokenInvalidCode
    85  	}
    86  
    87  	//If the tokenResponse is not obtained from proper client secret it should not update local tokens
    88  	if tokenResponse.Error == "invalid_client_secret" {
    89  		return ErrClientSecretInvalidCode
    90  	}
    91  
    92  	z.oauth.token.AccessToken = tokenResponse.AccessToken
    93  	z.oauth.token.APIDomain = tokenResponse.APIDomain
    94  	z.oauth.token.ExpiresIn = tokenResponse.ExpiresIn
    95  	z.oauth.token.TokenType = tokenResponse.TokenType
    96  
    97  	err = z.SaveTokens(z.oauth.token)
    98  	if err != nil {
    99  		return fmt.Errorf("Failed to save access tokens: %s", err)
   100  	}
   101  
   102  	return nil
   103  }
   104  
   105  func (z *Zoho) GenerateTokenURL(code, clientID, clientSecret string) string {
   106  	q := url.Values{}
   107  	q.Set("client_id", clientID)
   108  	q.Set("client_secret", clientSecret)
   109  	q.Set("code", code)
   110  	q.Set("redirect_uri", z.oauth.redirectURI)
   111  	q.Set("grant_type", "authorization_code")
   112  
   113  	return fmt.Sprintf("%s%s?%s", z.oauth.baseURL, oauthGenerateTokenRequestSlug, q.Encode())
   114  }
   115  
   116  // GenerateTokenRequest will get the Access token and Refresh token and hold them in the Zoho struct. This function can be used rather than
   117  // AuthorizationCodeRequest is you do not want to click on a link and redirect to a consent screen. Instead you can go to, https://accounts.zoho.com/developerconsole
   118  // and click the kebab icon beside your clientID, and click 'Self-Client'; then you can define you scopes and an expiry, then provide the generated authorization code
   119  // to this function which will generate your access token and refresh tokens.
   120  func (z *Zoho) GenerateTokenRequest(clientID, clientSecret, code, redirectURI string) (err error) {
   121  
   122  	z.oauth.clientID = clientID
   123  	z.oauth.clientSecret = clientSecret
   124  	z.oauth.redirectURI = redirectURI
   125  
   126  	err = z.CheckForSavedTokens()
   127  	if err == ErrTokenExpired {
   128  		return z.RefreshTokenRequest()
   129  	}
   130  
   131  	// q := url.Values{}
   132  	// q.Set("client_id", clientID)
   133  	// q.Set("client_secret", clientSecret)
   134  	// q.Set("code", code)
   135  	// q.Set("redirect_uri", redirectURI)
   136  	// q.Set("grant_type", "authorization_code")
   137  
   138  	// tokenURL := fmt.Sprintf("%s%s?%s", z.oauth.baseURL, oauthGenerateTokenRequestSlug, q.Encode())
   139  	tokenURL := z.GenerateTokenURL(code, clientID, clientSecret)
   140  	resp, err := z.client.Post(tokenURL, "application/x-www-form-urlencoded", nil)
   141  	if err != nil {
   142  		return fmt.Errorf("Failed while requesting generate token: %s", err)
   143  	}
   144  
   145  	defer func() {
   146  		if err := resp.Body.Close(); err != nil {
   147  			fmt.Printf("Failed to close request body: %s\n", err)
   148  		}
   149  	}()
   150  
   151  	body, err := ioutil.ReadAll(resp.Body)
   152  	if err != nil {
   153  		return fmt.Errorf(
   154  			"Failed to read request body on request to %s%s: %s",
   155  			z.oauth.baseURL,
   156  			oauthGenerateTokenRequestSlug,
   157  			err,
   158  		)
   159  	}
   160  
   161  	if resp.StatusCode != 200 {
   162  		return fmt.Errorf(
   163  			"Got non-200 status code from request to generate token: %s\n%s",
   164  			resp.Status,
   165  			string(body),
   166  		)
   167  	}
   168  
   169  	tokenResponse := AccessTokenResponse{}
   170  	err = json.Unmarshal(body, &tokenResponse)
   171  	if err != nil {
   172  		return fmt.Errorf(
   173  			"Failed to unmarshal access token response from request to generate token: %s",
   174  			err,
   175  		)
   176  	}
   177  
   178  	//If the tokenResponse is not valid it should not update local tokens
   179  	if tokenResponse.Error == "invalid_code" {
   180  		return ErrTokenInvalidCode
   181  	}
   182  
   183  	//If the tokenResponse is not obtained from proper client secret it should not update local tokens
   184  	if tokenResponse.Error == "invalid_client_secret" {
   185  		return ErrClientSecretInvalidCode
   186  	}
   187  
   188  	z.oauth.clientID = clientID
   189  	z.oauth.clientSecret = clientSecret
   190  	z.oauth.redirectURI = redirectURI
   191  	z.oauth.token = tokenResponse
   192  
   193  	err = z.SaveTokens(z.oauth.token)
   194  	if err != nil {
   195  		return fmt.Errorf("Failed to save access tokens: %s", err)
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  func (z *Zoho) AuthorizationCodeURL(scopes, clientID, redirectURI string, consent bool) string {
   202  	q := url.Values{}
   203  	q.Set("scope", scopes)
   204  	q.Set("client_id", clientID)
   205  	q.Set("redirect_uri", redirectURI)
   206  	q.Set("response_type", "code")
   207  	q.Set("access_type", "offline")
   208  
   209  	if consent {
   210  		q.Set("prompt", "consent")
   211  	}
   212  
   213  	return fmt.Sprintf("%s%s?%s", z.oauth.baseURL, oauthAuthorizationRequestSlug, q.Encode())
   214  }
   215  
   216  // AuthorizationCodeRequest will request an authorization code from Zoho. This authorization code is then used to generate access and refresh tokens.
   217  // This function will print a link that needs to be pasted into a browser to continue the oAuth2 flow. Then it will redirect to the redirectURL, it
   218  // must be the same as the redirect URL that was provided to Zoho when generating your client ID and client secret. If the redirect URL was a localhost
   219  // domain, the function will start a server that will get the code from the URL when the browser redirects.
   220  // If the domain is not a localhost, you will be prompted to paste the code from the URL back into the terminal window,
   221  // eg. https://domain.com/redirect-url?code=xxxxxxxxxx
   222  func (z *Zoho) AuthorizationCodeRequest(
   223  	clientID, clientSecret string,
   224  	scopes []ScopeString,
   225  	redirectURI string,
   226  ) (err error) {
   227  	// check for existing tokens
   228  	err = z.CheckForSavedTokens()
   229  	if err == nil {
   230  		z.oauth.clientID = clientID
   231  		z.oauth.clientSecret = clientSecret
   232  		z.oauth.redirectURI = redirectURI
   233  		z.oauth.scopes = scopes
   234  		return nil
   235  	}
   236  
   237  	// user may be able to issue a refresh if they have a refresh token, but maybe they are trying to get a new token.
   238  	// a breaking change could be to provide a bool: consent - where the user forces the consent screen otherwise we will try to refresh
   239  	requiresConsentPrompt := false
   240  	if err == ErrTokenExpired {
   241  		// currently we will simply check if the token is expired and if it is we will "prompt=consent"
   242  		requiresConsentPrompt = true
   243  	}
   244  
   245  	scopeStr := ""
   246  	for i, a := range scopes {
   247  		scopeStr += string(a)
   248  		if i < len(scopes)-1 {
   249  			scopeStr += ","
   250  		}
   251  	}
   252  
   253  	z.oauth.scopes = scopes
   254  
   255  	// q := url.Values{}
   256  	// q.Set("scope", scopeStr)
   257  	// q.Set("client_id", clientID)
   258  	// q.Set("redirect_uri", redirectURI)
   259  	// q.Set("response_type", "code")
   260  	// q.Set("access_type", "offline")
   261  
   262  	// authURL := fmt.Sprintf("%s%s?%s", z.oauth.baseURL, oauthAuthorizationRequestSlug, q.Encode())
   263  	authURL := z.AuthorizationCodeURL(scopeStr, clientID, redirectURI, requiresConsentPrompt)
   264  
   265  	srvChan := make(chan int)
   266  	codeChan := make(chan string)
   267  	var srv *http.Server
   268  
   269  	localRedirect := strings.Contains(redirectURI, "localhost")
   270  	if localRedirect {
   271  		// start a localhost server that will handle the redirect url
   272  		u, err := url.Parse(redirectURI)
   273  		if err != nil {
   274  			return fmt.Errorf("Failed to parse redirect URI: %s", err)
   275  		}
   276  		_, port, err := net.SplitHostPort(u.Host)
   277  		if err != nil {
   278  			return fmt.Errorf("Failed to split redirect URI into host and port segments: %s", err)
   279  		}
   280  		srv = &http.Server{Addr: ":" + port}
   281  
   282  		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
   283  			w.Write([]byte("Code retrieved, you can close this window to continue"))
   284  
   285  			codeChan <- r.URL.Query().Get("code")
   286  		})
   287  
   288  		go func() {
   289  			srvChan <- 1
   290  			err := srv.ListenAndServe()
   291  			if err != nil && err != http.ErrServerClosed {
   292  				fmt.Printf("Error while serving locally: %s\n", err)
   293  			}
   294  		}()
   295  
   296  		<-srvChan
   297  	}
   298  
   299  	fmt.Printf("Go to the following authentication URL to begin oAuth2 flow:\n %s\n\n", authURL)
   300  
   301  	code := ""
   302  
   303  	if localRedirect {
   304  		// wait for code to be returned by the server
   305  		code = <-codeChan
   306  		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   307  		defer func() {
   308  			cancel()
   309  		}()
   310  		if err := srv.Shutdown(ctx); err != nil {
   311  			fmt.Printf("Error while shutting down local server: %s\n", err)
   312  		}
   313  	} else {
   314  		fmt.Printf("Paste code and press enter:\n")
   315  		_, err := fmt.Scan(&code)
   316  		if err != nil {
   317  			return fmt.Errorf("Failed to read code from input: %s", err)
   318  		}
   319  	}
   320  
   321  	if code == "" {
   322  		return fmt.Errorf("No code was recieved from oAuth2 flow")
   323  	}
   324  
   325  	err = z.GenerateTokenRequest(clientID, clientSecret, code, redirectURI)
   326  	if err != nil {
   327  		return fmt.Errorf("Failed to retrieve oAuth2 token: %s", err)
   328  	}
   329  
   330  	return nil
   331  }
   332  
   333  // AccessTokenResponse is the data returned when generating AccessTokens, or Refreshing the token
   334  type AccessTokenResponse struct {
   335  	AccessToken  string `json:"access_token,omitempty"`
   336  	RefreshToken string `json:"refresh_token,omitempty"`
   337  	ExpiresIn    int    `json:"expires_in,omitempty"`
   338  	APIDomain    string `json:"api_domain,omitempty"`
   339  	TokenType    string `json:"token_type,omitempty"`
   340  	Error        string `json:"error,omitempty"`
   341  }
   342  
   343  const (
   344  	oauthAuthorizationRequestSlug = "auth"
   345  	oauthGenerateTokenRequestSlug = "token"
   346  	oauthRevokeTokenRequestSlug   = "revoke"
   347  )
   348  
   349  // ScopeString is a type for defining scopes for oAuth2 flow
   350  type ScopeString string
   351  
   352  // BuildScope is used to generate a scope string for oAuth2 flow
   353  func BuildScope(service Service, scope Scope, method Method, operation Operation) ScopeString {
   354  	built := fmt.Sprintf("%s.%s", service, scope)
   355  	if method != "" {
   356  		built += fmt.Sprintf(".%s", method)
   357  	}
   358  	if operation != "" {
   359  		built += fmt.Sprintf(".%s", operation)
   360  	}
   361  	return ScopeString(built)
   362  }
   363  
   364  // Service is a type for building scopes
   365  type Service string
   366  
   367  const (
   368  	// Crm is the Service portion of the scope string
   369  	Crm Service = "ZohoCRM"
   370  	// Expense is the Service portion of the scope string
   371  	Expense Service = "ZohoExpense"
   372  	// Bookings is the Service portion of the scope string
   373  	Bookings Service = "zohobookings"
   374  )
   375  
   376  // Scope is a type for building scopes
   377  type Scope string
   378  
   379  const (
   380  	// UsersScope is a possible Scope portion of the scope string
   381  	UsersScope Scope = "users"
   382  	// OrgScope is a possible Scope portion of the scope string
   383  	OrgScope Scope = "org"
   384  	// SettingsScope is a possible Scope portion of the scope string
   385  	SettingsScope Scope = "settings"
   386  	// ModulesScope is a possible Scope portion of the scope string
   387  	ModulesScope Scope = "modules"
   388  
   389  	// Additional Scopes related to expense APIs
   390  
   391  	// FullAccessScope is a possible Method portion of the scope string
   392  	FullAccessScope Scope = "fullaccess"
   393  	// ExpenseReportScope is a possible Method portion of the scope string
   394  	ExpenseReportScope Scope = "expensereport"
   395  	// ApprovalScope is a possible Method portion of the scope string
   396  	ApprovalScope Scope = "approval"
   397  	// ReimbursementScope is a possible Method portion of the scope string
   398  	ReimbursementScope Scope = "reimbursement"
   399  	// AdvanceScope is a possible Method portion of the scope string
   400  	AdvanceScope Scope = "advance"
   401  	// DataScope is a possible Method portion of the scope string
   402  	DataScope Scope = "data"
   403  )
   404  
   405  // Method is a type for building scopes
   406  type Method string
   407  
   408  // SettingsMethod is a type for building scopes
   409  type SettingsMethod = Method
   410  
   411  // ModulesMethod is a type for building scopes
   412  type ModulesMethod = Method
   413  
   414  const (
   415  	// AllMethod is a possible Method portion of the scope string
   416  	AllMethod Method = "ALL"
   417  
   418  	// Territories is a possible Method portion of the scope string
   419  	Territories SettingsMethod = "territories"
   420  	// CustomViews is a possible Method portion of the scope string
   421  	CustomViews SettingsMethod = "custom_views"
   422  	// RelatedLists is a possible Method portion of the scope string
   423  	RelatedLists SettingsMethod = "related_lists"
   424  	// Modules is a possible Method portion of the scope string
   425  	Modules SettingsMethod = "modules"
   426  	// TabGroups is a possible Method portion of the scope string
   427  	TabGroups SettingsMethod = "tab_groups"
   428  	// Fields is a possible Method portion of the scope string
   429  	Fields SettingsMethod = "fields"
   430  	// Layouts is a possible Method portion of the scope string
   431  	Layouts SettingsMethod = "layouts"
   432  	// Macros is a possible Method portion of the scope string
   433  	Macros SettingsMethod = "macros"
   434  	// CustomLinks is a possible Method portion of the scope string
   435  	CustomLinks SettingsMethod = "custom_links"
   436  	// CustomButtons is a possible Method portion of the scope string
   437  	CustomButtons SettingsMethod = "custom_buttons"
   438  	// Roles is a possible Method portion of the scope string
   439  	Roles SettingsMethod = "roles"
   440  	// Profiles is a possible Method portion of the scope string
   441  	Profiles SettingsMethod = "profiles"
   442  
   443  	// Approvals is a possible Method portion of the scope string
   444  	Approvals ModulesMethod = "approvals"
   445  	// Leads is a possible Method portion of the scope string
   446  	Leads ModulesMethod = "leads"
   447  	// Accounts is a possible Method portion of the scope string
   448  	Accounts ModulesMethod = "accounts"
   449  	// Contacts is a possible Method portion of the scope string
   450  	Contacts ModulesMethod = "contacts"
   451  	// Deals is a possible Method portion of the scope string
   452  	Deals ModulesMethod = "deals"
   453  	// Campaigns is a possible Method portion of the scope string
   454  	Campaigns ModulesMethod = "campaigns"
   455  	// Tasks is a possible Method portion of the scope string
   456  	Tasks ModulesMethod = "tasks"
   457  	// Cases is a possible Method portion of the scope string
   458  	Cases ModulesMethod = "cases"
   459  	// Events is a possible Method portion of the scope string
   460  	Events ModulesMethod = "events"
   461  	// Calls is a possible Method portion of the scope string
   462  	Calls ModulesMethod = "calls"
   463  	// Solutions is a possible Method portion of the scope string
   464  	Solutions ModulesMethod = "solutions"
   465  	// Products is a possible Method portion of the scope string
   466  	Products ModulesMethod = "products"
   467  	// Vendors is a possible Method portion of the scope string
   468  	Vendors ModulesMethod = "vendors"
   469  	// PriceBooks is a possible Method portion of the scope string
   470  	PriceBooks ModulesMethod = "pricebooks"
   471  	// Quotes is a possible Method portion of the scope string
   472  	Quotes ModulesMethod = "quotes"
   473  	// SalesOrders is a possible Method portion of the scope string
   474  	SalesOrders ModulesMethod = "salesorders"
   475  	// PurchaseOrders is a possible Method portion of the scope string
   476  	PurchaseOrders ModulesMethod = "purchaseorders"
   477  	// Invoices is a possible Method portion of the scope string
   478  	Invoices ModulesMethod = "invoices"
   479  	// Custom is a possible Method portion of the scope string
   480  	Custom ModulesMethod = "custom"
   481  	// Dashboards is a possible Method portion of the scope string
   482  	Dashboards ModulesMethod = "dashboards"
   483  	// Notes is a possible Method portion of the scope string
   484  	Notes ModulesMethod = "notes"
   485  	// Activities is a possible Method portion of the scope string
   486  	Activities ModulesMethod = "activities"
   487  	// Search is a possible Method portion of the scope string
   488  	Search ModulesMethod = "search"
   489  )
   490  
   491  // Operation is a type for building scopes
   492  type Operation string
   493  
   494  const (
   495  	// NoOp is a possible Operation portion of the scope string
   496  	NoOp Operation = ""
   497  	// All is a possible Operation portion of the scope string
   498  	All Operation = "ALL"
   499  	// Read is a possible Operation portion of the scope string
   500  	Read Operation = "READ"
   501  	// Create is a possible Operation portion of the scope string
   502  	Create Operation = "CREATE"
   503  	// Update is a possible Operation portion of the scope string
   504  	Update Operation = "UPDATE"
   505  	// Delete is a possible Operation portion of the scope string
   506  	Delete Operation = "DELETE"
   507  )