Skip to content

Instantly share code, notes, and snippets.

@corvofeng
Created June 26, 2022 08:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save corvofeng/64c245edf113e870e8a841651243d6bb to your computer and use it in GitHub Desktop.
Save corvofeng/64c245edf113e870e8a841651243d6bb to your computer and use it in GitHub Desktop.
github device token
package main
// Refer to:
// https://github.com/golang/oauth2/pull/356/files/7cf8880eb1e002ce6fb90ffadae90b41452462e3
// https://docs.github.com/cn/developers/apps/building-oauth-apps/authorizing-oauth-apps#device-flow
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/google/go-github/github"
"golang.org/x/net/context/ctxhttp"
"golang.org/x/oauth2"
)
const (
errAuthorizationPending = "authorization_pending"
errSlowDown = "slow_down"
errAccessDenied = "access_denied"
errExpiredToken = "expired_token"
)
type DeviceAuth struct {
DeviceCode string `json:"device_code"`
UserCode string `json:"user_code"`
VerificationURI string `json:"verification_uri,verification_url"`
VerificationURIComplete string `json:"verification_uri_complete,omitempty"`
ExpiresIn int `json:"expires_in,omitempty"`
Interval int `json:"interval,omitempty"`
raw map[string]interface{}
}
func retrieveGithubDeviceAuth(ctx context.Context, c *oauth2.Config, v url.Values) (*DeviceAuth, error) {
req, err := http.NewRequest("POST", "https://github.com/login/device/code", strings.NewReader(v.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
r, err := ctxhttp.Do(ctx, nil, req)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("oauth2: cannot auth device: %v", err)
}
if code := r.StatusCode; code < 200 || code > 299 {
return nil, &oauth2.RetrieveError{
Response: r,
Body: body,
}
}
var da = &DeviceAuth{}
err = json.Unmarshal(body, &da)
fmt.Printf("%+v %s\n", da, err)
if err != nil {
return nil, err
}
_ = json.Unmarshal(body, &da.raw)
// Azure AD supplies verification_url instead of verification_uri
if da.VerificationURI == "" {
da.VerificationURI, _ = da.raw["verification_url"].(string)
}
return da, nil
}
func parseError(err error) string {
e, ok := err.(*oauth2.RetrieveError)
if ok {
eResp := make(map[string]string)
_ = json.Unmarshal(e.Body, &eResp)
return eResp["error"]
}
return ""
}
type tokenJSON struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
RefreshToken string `json:"refresh_token"`
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
Error string `json:"error"`
}
type expirationTime int32
func (e *tokenJSON) expiry() (t time.Time) {
if v := e.ExpiresIn; v != 0 {
return time.Now().Add(time.Duration(v) * time.Second)
}
return
}
func poolToSuccess(ctx context.Context, da *DeviceAuth, c *oauth2.Config) (*oauth2.Token, error) {
v := url.Values{
"client_id": {c.ClientID},
"grant_type": {"urn:ietf:params:oauth:grant-type:device_code"},
"device_code": {da.DeviceCode},
}
if len(c.Scopes) > 0 {
v.Set("scope", strings.Join(c.Scopes, " "))
}
interval := da.Interval
if interval == 0 {
interval = 5
}
for {
time.Sleep(time.Duration(interval) * time.Second)
fmt.Println("Check for token")
req, err := http.NewRequest("POST", c.Endpoint.TokenURL, strings.NewReader(v.Encode()))
if err != nil {
fmt.Println(err)
return nil, err
}
req.Header.Set("Accept", "application/json")
r, err := ctxhttp.Do(ctx, nil, req)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return nil, fmt.Errorf("oauth2: cannot auth device: %v", err)
}
var tj tokenJSON
var tok *oauth2.Token
err = json.Unmarshal(body, &tj)
if err == nil && tj.Error == "" {
tok = &oauth2.Token{
AccessToken: tj.AccessToken,
TokenType: tj.TokenType,
RefreshToken: tj.RefreshToken,
Expiry: tj.expiry(),
}
return tok, nil
}
switch tj.Error {
case errAccessDenied, errExpiredToken:
return tok, errors.New("oauth2: " + tj.Error)
case errSlowDown:
interval += 5
fallthrough
case errAuthorizationPending:
}
}
}
func main() {
ctx := context.Background()
conf := &oauth2.Config{
ClientID: "eace333330cb808173cb",
ClientSecret: "keep it empty",
Scopes: []string{"user", "email"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://github.com/login/oauth/authorize",
TokenURL: "https://github.com/login/oauth/access_token",
},
}
v := url.Values{
"client_id": {conf.ClientID},
}
da, err := retrieveGithubDeviceAuth(ctx, conf, v)
if err != nil {
log.Fatal(err)
}
tok, err := poolToSuccess(ctx, da, conf)
fmt.Println(tok, err)
// client := conf.Client(ctx, tok)
oauthClient := conf.Client(oauth2.NoContext, tok)
client := github.NewClient(oauthClient)
user, _, err := client.Users.Get(ctx, "")
fmt.Println(user)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment