Created
June 26, 2022 08:12
-
-
Save corvofeng/64c245edf113e870e8a841651243d6bb to your computer and use it in GitHub Desktop.
github device token
This file contains 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
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