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 )