Created
October 7, 2024 19:52
-
-
Save cgrotz/0c39dababb0a62e133ad0d254a0bae40 to your computer and use it in GitHub Desktop.
Go example for OAuth 2.0 Device Authorization Grant
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2024 Google LLC | |
// | |
// Licensed under the Apache License, Version 2.0 (the "License"); | |
// you may not use this file except in compliance with the License. | |
// You may obtain a copy of the License at | |
// | |
// http://www.apache.org/licenses/LICENSE-2.0 | |
// | |
// Unless required by applicable law or agreed to in writing, software | |
// distributed under the License is distributed on an "AS IS" BASIS, | |
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
// See the License for the specific language governing permissions and | |
// limitations under the License. | |
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
"io" | |
"log" | |
"net/http" | |
"strings" | |
"time" | |
) | |
type DeviceCodeResponse struct { | |
DeviceCode string `json:"device_code"` | |
UserCode string `json:"user_code"` | |
ErrorCode string `json:"error_code"` | |
VerificationURL string `json:"verification_url"` | |
ExpiresIn int `json:"expires_in"` | |
Interval int `json:"interval"` | |
} | |
type ErrorResponse struct { | |
Error string `json:"error"` | |
ErrorDescription string `json:"error_description"` | |
} | |
type TokenResponse struct { | |
AccessToken string `json:"access_token"` | |
RefreshToken string `json:"refresh_token"` | |
IDToken string `json:"id_token"` | |
TokenType string `json:"token_type"` | |
ExpiresIn int32 `json:"expires_in"` | |
} | |
var ( | |
ClientId = "" | |
ClientSecret = "" | |
deviceCodeEndpoint = "https://oauth2.googleapis.com/device/code" | |
tokenEndpoint = "https://oauth2.googleapis.com/token" | |
) | |
func main() { | |
deviceCode, err := fetchDeviceCode(deviceCodeEndpoint, ClientId) | |
if err != nil { | |
log.Panicf("unable to fetch device code: %v\n", err) | |
} | |
log.Printf("UserCode %s\n", deviceCode.UserCode) | |
log.Printf("VerificationURL %s\n", deviceCode.VerificationURL) | |
if err != nil { | |
log.Panicf("unable to send login challenge: %v\n", err) | |
} | |
fmt.Print("Not yet authorized, waiting") | |
intervalCorrection := 0 | |
for { | |
time.Sleep(time.Duration(deviceCode.Interval) * time.Second) | |
token, err := fetchToken(tokenEndpoint, ClientId, ClientSecret, deviceCode.DeviceCode) | |
if err != nil { | |
if err.Error() == "authorization_pending" { | |
fmt.Print(".") | |
continue | |
} else if err.Error() == "slow_down" { | |
log.Printf("Slow down, increasing interval by one second\n") | |
intervalCorrection += 1 | |
} else if err.Error() == "expired_token" { | |
log.Panicln("Authentication expired, please restart the process") | |
} else if err.Error() == "access_denied" { | |
log.Panicln("Access denied, contact the application owner") | |
} else { | |
log.Panicf("unable to authenticate: %v\n", err) | |
} | |
} | |
fmt.Println() | |
tokenString, _ := json.MarshalIndent(token, "", " ") | |
fmt.Printf("Received token: %s\n", tokenString) | |
return | |
} | |
} | |
func fetchDeviceCode(endpoint string, clientId string) (*DeviceCodeResponse, error) { | |
payload := strings.NewReader(fmt.Sprintf("client_id=%s&scope=email profile", clientId)) | |
req, err := http.NewRequest("POST", endpoint, payload) | |
if err != nil { | |
return nil, err | |
} | |
req.Header.Add("content-type", "application/x-www-form-urlencoded") | |
res, err := http.DefaultClient.Do(req) | |
if err != nil { | |
return nil, err | |
} | |
defer res.Body.Close() | |
body, err := io.ReadAll(res.Body) | |
if err != nil { | |
return nil, err | |
} | |
var deviceCode DeviceCodeResponse | |
if err := json.Unmarshal(body, &deviceCode); err != nil { | |
return nil, err | |
} | |
return &deviceCode, nil | |
} | |
func fetchToken(endpoint string, clientId string, clientSecret string, deviceCode string) (*TokenResponse, error) { | |
grantType := "urn:ietf:params:oauth:grant-type:device_code" | |
payload := strings.NewReader(fmt.Sprintf("grant_type=%s&device_code=%s&client_id=%s&client_secret=%s", grantType, deviceCode, clientId, clientSecret)) | |
req, err := http.NewRequest("POST", endpoint, payload) | |
if err != nil { | |
return nil, err | |
} | |
req.Header.Add("content-type", "application/x-www-form-urlencoded") | |
res, err := http.DefaultClient.Do(req) | |
if err != nil { | |
return nil, err | |
} | |
defer res.Body.Close() | |
body, err := io.ReadAll(res.Body) | |
if err != nil { | |
return nil, err | |
} | |
if res.StatusCode == 200 { | |
var token TokenResponse | |
if err := json.Unmarshal(body, &token); err != nil { | |
return nil, err | |
} | |
return &token, nil | |
} else { | |
var errorResponse ErrorResponse | |
if err := json.Unmarshal(body, &errorResponse); err != nil { | |
return nil, err | |
} | |
if errorResponse.Error == "authorization_pending" { | |
return nil, fmt.Errorf("authorization_pending") | |
} else if errorResponse.Error == "slow_down" { | |
return nil, fmt.Errorf("slow_down") | |
} else if errorResponse.Error == "expired_token" { | |
return nil, fmt.Errorf("expired_token") | |
} else if errorResponse.Error == "access_denied" { | |
return nil, fmt.Errorf("access_denied") | |
} else { | |
return nil, fmt.Errorf("unknown error %s", errorResponse.Error) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment