Skip to content

Instantly share code, notes, and snippets.

@cgrotz
Created October 7, 2024 19:52
Show Gist options
  • Save cgrotz/0c39dababb0a62e133ad0d254a0bae40 to your computer and use it in GitHub Desktop.
Save cgrotz/0c39dababb0a62e133ad0d254a0bae40 to your computer and use it in GitHub Desktop.
Go example for OAuth 2.0 Device Authorization Grant
// 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