From 618f38c01e41ed0d16c8d595a729b7f16f3346a8 Mon Sep 17 00:00:00 2001 From: YAEGASHI Takeshi Date: Mon, 16 Sep 2019 06:52:33 +0900 Subject: [PATCH] Implement Azure AD v2 device auth and purge go-autorest https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth2-device-code --- adal/client.go | 71 ------------------------ auth/deviceauth.go | 121 +++++++++++++++++++++++++++++++++++++++++ cmd/msgraph-me/main.go | 37 +++++++------ go.mod | 5 +- go.sum | 20 ------- 5 files changed, 142 insertions(+), 112 deletions(-) delete mode 100644 adal/client.go create mode 100644 auth/deviceauth.go diff --git a/adal/client.go b/adal/client.go deleted file mode 100644 index e42f4419..00000000 --- a/adal/client.go +++ /dev/null @@ -1,71 +0,0 @@ -package adal - -import ( - "context" - "fmt" - "net/http" - - "github.com/Azure/go-autorest/autorest/adal" - "golang.org/x/oauth2" -) - -// Client is Microsoft Graph Client -type Client struct { - Authority string - TenantID string - ClientID string - Resource string - Store string - oauthConfig *adal.OAuthConfig - spToken *adal.ServicePrincipalToken -} - -// LoadToken loads tokens from the persistent store. If it's unavailable, run device code authentication. -func (cli *Client) LoadToken() error { - callback := func(token adal.Token) error { return cli.SaveToken(token) } - var err error - cli.oauthConfig, err = adal.NewOAuthConfig(cli.Authority, cli.TenantID) - if err != nil { - return err - } - token, err := adal.LoadToken(cli.Store) - if err == nil { - spToken, err := adal.NewServicePrincipalTokenFromManualToken(*cli.oauthConfig, cli.ClientID, cli.Resource, *token, callback) - if err == nil { - spToken.SetAutoRefresh(true) - if spToken.EnsureFresh() == nil { - cli.spToken = spToken - return nil - } - } - } - oauthClient := &http.Client{} - deviceCode, err := adal.InitiateDeviceAuth(oauthClient, *cli.oauthConfig, cli.ClientID, cli.Resource) - if err != nil { - return err - } - fmt.Println(*deviceCode.Message) - token, err = adal.WaitForUserCompletion(oauthClient, deviceCode) - if err != nil { - return err - } - spToken, err := adal.NewServicePrincipalTokenFromManualToken(*cli.oauthConfig, cli.ClientID, cli.Resource, *token, callback) - if err != nil { - return err - } - spToken.SetAutoRefresh(true) - cli.SaveToken(spToken.Token()) - cli.spToken = spToken - return nil -} - -// SaveToken saves OAuth2 token in the persistent store. -func (cli *Client) SaveToken(token adal.Token) error { - return adal.SaveToken(cli.Store, 0600, token) -} - -// NewHTTPClient returns http.Client with OAuth2 authorization. -func (cli *Client) NewHTTPClient(ctx context.Context) *http.Client { - token := cli.spToken.OAuthToken() - return oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})) -} diff --git a/auth/deviceauth.go b/auth/deviceauth.go new file mode 100644 index 00000000..05567841 --- /dev/null +++ b/auth/deviceauth.go @@ -0,0 +1,121 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "time" + + "golang.org/x/oauth2" +) + +const ( + DeviceCodeGrantType = "urn:ietf:params:oauth:grant-type:device_code" +) + +type DeviceAuth struct { + Code *DeviceCode + TenantID string + ClientID string +} + +type DeviceCode struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURL string `json:"verification_url"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` + Message string `json:"message"` +} + +type DeviceToken struct { + TokenType string `json:"token_type"` + Scope string `json:"scope"` + ExpiresIn int `json:"expires_in"` + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` +} + +type DeviceError struct { + Error string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +func NewDeviceAuth(tenantID, clientID, scope string) (*DeviceAuth, error) { + devicecodeURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/devicecode", tenantID) + res, err := http.PostForm(devicecodeURL, url.Values{"client_id": {clientID}, "scope": {scope}}) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + b, _ := ioutil.ReadAll(res.Body) + return nil, fmt.Errorf("%s: %s", res.Status, string(b)) + } + da := &DeviceAuth{ClientID: clientID, TenantID: tenantID} + dec := json.NewDecoder(res.Body) + err = dec.Decode(&da.Code) + if err != nil { + return nil, err + } + return da, nil +} + +func (da *DeviceAuth) Message() string { + return da.Code.Message +} + +func (da *DeviceAuth) Poll() (*DeviceToken, error) { + tokenURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", da.TenantID) + values := url.Values{ + "client_id": {da.ClientID}, + "grant_type": {DeviceCodeGrantType}, + "device_code": {da.Code.DeviceCode}, + } + interval := da.Code.Interval + if interval == 0 { + interval = 5 + } + for { + time.Sleep(time.Second * time.Duration(interval)) + res, err := http.PostForm(tokenURL, values) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode == http.StatusOK { + dt := &DeviceToken{} + dec := json.NewDecoder(res.Body) + err = dec.Decode(dt) + if err != nil { + return nil, err + } + return dt, nil + } + de := &DeviceError{} + dec := json.NewDecoder(res.Body) + err = dec.Decode(de) + if err != nil { + return nil, err + } + if de.Error != "authorization_pending" { + return nil, fmt.Errorf("%s: %s", de.Error, de.ErrorDescription) + } + } +} + +func (dt *DeviceToken) Token() (*oauth2.Token, error) { + return &oauth2.Token{ + TokenType: dt.TokenType, + AccessToken: dt.AccessToken, + RefreshToken: dt.RefreshToken, + }, nil +} + +func (dt *DeviceToken) Client(ctx context.Context) *http.Client { + return oauth2.NewClient(ctx, dt) +} diff --git a/cmd/msgraph-me/main.go b/cmd/msgraph-me/main.go index 90daf824..50c3a759 100644 --- a/cmd/msgraph-me/main.go +++ b/cmd/msgraph-me/main.go @@ -4,20 +4,18 @@ import ( "context" "encoding/json" "flag" + "fmt" "log" "os" - "github.com/yaegashi/msgraph.go/adal" - + "github.com/yaegashi/msgraph.go/auth" msgraph "github.com/yaegashi/msgraph.go/v1.0" ) const ( - defaultAuthority = "https://login.microsoftonline.com/" - defaultTenantID = "common" - defaultClientID = "45c7f99c-0a94-42ff-a6d8-a8d657229e8c" - defaultResource = "https://graph.microsoft.com" - defaultStore = "token_cache.json" + defaultTenantID = "common" + defaultClientID = "45c7f99c-0a94-42ff-a6d8-a8d657229e8c" + defaultScope = "openid profile user.readwrite files.readwrite.all" ) func dump(o interface{}) { @@ -27,21 +25,25 @@ func dump(o interface{}) { } func main() { - adalClient := &adal.Client{} - flag.StringVar(&adalClient.Authority, "authority", defaultAuthority, "Authority") - flag.StringVar(&adalClient.TenantID, "tenant_id", defaultTenantID, "Tenant ID") - flag.StringVar(&adalClient.ClientID, "client_id", defaultClientID, "Client ID") - flag.StringVar(&adalClient.Resource, "resource", defaultResource, "Resource") - flag.StringVar(&adalClient.Store, "store", defaultStore, "Token cache store path") + var tenantID, clientID string + flag.StringVar(&tenantID, "tenant_id", defaultTenantID, "Tenant ID") + flag.StringVar(&clientID, "client_id", defaultClientID, "Client ID") flag.Parse() - err := adalClient.LoadToken() + da, err := auth.NewDeviceAuth(tenantID, clientID, defaultScope) + if err != nil { + log.Fatal(err) + } + + fmt.Println(da.Message()) + + dt, err := da.Poll() if err != nil { log.Fatal(err) } ctx := context.Background() - cli := adalClient.NewHTTPClient(ctx) + cli := dt.Client(ctx) serv := msgraph.NewService(cli) { @@ -57,8 +59,9 @@ func main() { { s := serv.Me().Drive().Root().Children() - log.Printf("GET %s", s.URL()) - r, err := s.Get() + q := "?$select=name,file,folder,size" + log.Printf("GET %s%s", s.URL(), q) + r, err := s.GetWithPath(q) if err == nil { dump(r) } else { diff --git a/go.mod b/go.mod index 2b37c1cd..fecd87a2 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,4 @@ module github.com/yaegashi/msgraph.go go 1.12 -require ( - github.com/Azure/go-autorest/autorest/adal v0.6.0 - golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 -) +require golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 diff --git a/go.sum b/go.sum index 04f0e8d9..734252e6 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/Azure/go-autorest/autorest v0.9.0 h1:MRvx8gncNaXJqOoLmhNjUAKh33JJF8LyxPhomEtOsjs= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/adal v0.6.0 h1:UCTq22yE3RPgbU/8u4scfnnzuCW6pwQ9n+uBtV78ouo= -github.com/Azure/go-autorest/autorest/adal v0.6.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM= -github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc= -github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= -github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -27,7 +8,6 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7O golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=