From 583caecf02ae625ebeab7eed0d052c028dddad7d Mon Sep 17 00:00:00 2001 From: ryanfkeepers Date: Wed, 1 Nov 2023 17:03:52 -0600 Subject: [PATCH] poc for obtaining a device token --- src/cmd/delegated copy/delegated.go | 73 +++++++++++++ src/cmd/device/device.go | 126 +++++++++++++++++++++ src/pkg/services/m365/api/access.go | 164 +++++++++++++++++++++++++--- 3 files changed, 346 insertions(+), 17 deletions(-) create mode 100644 src/cmd/delegated copy/delegated.go create mode 100644 src/cmd/device/device.go diff --git a/src/cmd/delegated copy/delegated.go b/src/cmd/delegated copy/delegated.go new file mode 100644 index 000000000..c44c5c622 --- /dev/null +++ b/src/cmd/delegated copy/delegated.go @@ -0,0 +1,73 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/alcionai/corso/src/cli" + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/observe" + "github.com/alcionai/corso/src/pkg/count" + "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +// The root-level command. +// `corso [] [] [...]` +var cmd = &cobra.Command{ + Use: "delegated", + Short: "delegated token POC", + RunE: getToken, + PersistentPreRunE: cli.PreRun, +} + +func main() { + cli.AddSupportFlags(cmd) + ctx, log := cli.SeedCtx() + + defer func() { + observe.Flush(ctx) // flush the progress bars + + _ = log.Sync() // flush all logs in the buffer + }() + + if err := cmd.ExecuteContext(ctx); err != nil { + os.Exit(1) + } +} + +func getToken(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + _, details, err := utils.GetAccountAndConnect( + ctx, + cmd, + path.ExchangeService) + if err != nil { + return Only(ctx, err) + } + + creds, err := details.Repo.Account.M365Config() + if err != nil { + return Only(ctx, err) + } + + ac, err := api.NewClient( + creds, + details.Opts, + count.New()) + if err != nil { + return Only(ctx, err) + } + + da, err := ac.Access().GetDelegatedToken(ctx) + if err != nil { + return Only(ctx, err) + } + + PrettyJSON(ctx, da) + + return nil +} diff --git a/src/cmd/device/device.go b/src/cmd/device/device.go new file mode 100644 index 000000000..c610e16f2 --- /dev/null +++ b/src/cmd/device/device.go @@ -0,0 +1,126 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/alcionai/corso/src/cli" + . "github.com/alcionai/corso/src/cli/print" + "github.com/alcionai/corso/src/cli/utils" + "github.com/alcionai/corso/src/internal/observe" + "github.com/alcionai/corso/src/pkg/count" + "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +// The root-level command. +// `corso [] [] [...]` +var cmd = &cobra.Command{ + Use: "device", + Short: "device token POC", + RunE: help, + PersistentPreRunE: cli.PreRun, +} + +var request = &cobra.Command{ + Use: "request", + Short: "request device token POC", + RunE: requestToken, +} + +var get = &cobra.Command{ + Use: "get", + Short: "get device token POC", + RunE: getToken, +} + +func main() { + cli.AddSupportFlags(cmd) + ctx, log := cli.SeedCtx() + + defer func() { + observe.Flush(ctx) // flush the progress bars + + _ = log.Sync() // flush all logs in the buffer + }() + + cmd.AddCommand(request) + cmd.AddCommand(get) + + if err := cmd.ExecuteContext(ctx); err != nil { + os.Exit(1) + } +} + +func help(cmd *cobra.Command, args []string) error { + return cmd.Help() +} + +func requestToken(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + _, details, err := utils.GetAccountAndConnect( + ctx, + cmd, + path.ExchangeService) + if err != nil { + return Only(ctx, err) + } + + creds, err := details.Repo.Account.M365Config() + if err != nil { + return Only(ctx, err) + } + + ac, err := api.NewClient( + creds, + details.Opts, + count.New()) + if err != nil { + return Only(ctx, err) + } + + da, err := ac.Access().RequestDeviceToken(ctx) + if err != nil { + return Only(ctx, err) + } + + PrettyJSON(ctx, da) + + return nil +} + +func getToken(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + + _, details, err := utils.GetAccountAndConnect( + ctx, + cmd, + path.ExchangeService) + if err != nil { + return Only(ctx, err) + } + + creds, err := details.Repo.Account.M365Config() + if err != nil { + return Only(ctx, err) + } + + ac, err := api.NewClient( + creds, + details.Opts, + count.New()) + if err != nil { + return Only(ctx, err) + } + + da, err := ac.Access().GetDeviceToken(ctx, args[0]) + if err != nil { + return Only(ctx, err) + } + + PrettyJSON(ctx, da) + + return nil +} diff --git a/src/pkg/services/m365/api/access.go b/src/pkg/services/m365/api/access.go index 4cee23657..94921e716 100644 --- a/src/pkg/services/m365/api/access.go +++ b/src/pkg/services/m365/api/access.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/httputil" "strings" "github.com/alcionai/clues" @@ -68,24 +69,50 @@ func (c Access) GetToken( return nil } -type delegatedAccess struct { - TokenType string `json:"token_type"` - Scope string `json:"scope"` - ExpiresIn string `json:"expires_in"` - ExpiresOn string `json:"expires_on"` - NotBefore string `json:"not_before"` - Resource string `json:"resource"` - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` +type delegatedResp struct { + AccessToken string `json:"access_token,omitempty"` + Devicecode string `json:"device_code,omitempty"` + ExpiresIn string `json:"expires_in,omitempty"` + ExpiresOn string `json:"expires_on,omitempty"` + Interval string `json:"interval,omitempty"` + Message string `json:"message,omitempty"` + NotBefore string `json:"not_before,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + Resource string `json:"resource,omitempty"` + Scope string `json:"scope,omitempty"` + TokenType string `json:"token_type,omitempty"` + UserCode string `json:"user_code,omitempty"` + VerificationURI string `json:"verification_uri,omitempty"` } -func (da delegatedAccess) MinimumPrintable() any { +func (da delegatedResp) MinimumPrintable() any { + return da +} + +type deviceResp struct { + AccessToken string `json:"access_token,omitempty"` + Devicecode string `json:"device_code,omitempty"` + ExpiresIn int `json:"expires_in,omitempty"` + ExpiresOn string `json:"expires_on,omitempty"` + IDToken string `json:"id_token,omitempty"` + Interval int `json:"interval,omitempty"` + Message string `json:"message,omitempty"` + NotBefore string `json:"not_before,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + Resource string `json:"resource,omitempty"` + Scope string `json:"scope,omitempty"` + TokenType string `json:"token_type,omitempty"` + UserCode string `json:"user_code,omitempty"` + VerificationURI string `json:"verification_uri,omitempty"` +} + +func (da deviceResp) MinimumPrintable() any { return da } func (c *Access) GetDelegatedToken( ctx context.Context, -) (delegatedAccess, error) { +) (delegatedResp, error) { var ( //nolint:lll // https://dzone.com/articles/getting-access-token-for-microsoft-graph-using-oau @@ -110,21 +137,124 @@ func (c *Access) GetDelegatedToken( resp, err := c.Post(ctx, rawURL, headers, body) if err != nil { - return delegatedAccess{}, graph.Stack(ctx, err) + return delegatedResp{}, graph.Stack(ctx, err) } if resp.StatusCode == http.StatusBadRequest { - return delegatedAccess{}, clues.New("incorrect tenant or credentials") + return delegatedResp{}, clues.New("incorrect tenant or credentials") } if resp.StatusCode/100 == 4 || resp.StatusCode/100 == 5 { - return delegatedAccess{}, clues.New("non-2xx response: " + resp.Status) + return delegatedResp{}, clues.New("non-2xx response: " + resp.Status) } defer resp.Body.Close() - var da delegatedAccess - err = json.NewDecoder(resp.Body).Decode(&da) + var ar delegatedResp + err = json.NewDecoder(resp.Body).Decode(&ar) - return da, clues.Wrap(err, "undecodable body").WithClues(ctx).OrNil() + return ar, clues.Wrap(err, "undecodable resp body").WithClues(ctx).OrNil() +} + +func (c *Access) RequestDeviceToken( + ctx context.Context, +) (deviceResp, error) { + var ( + //nolint:lll + // https://dzone.com/articles/getting-access-token-for-microsoft-graph-using-oau + rawURL = fmt.Sprintf( + "https://login.microsoftonline.com/%s/oauth2/v2.0/devicecode", + c.Credentials.AzureTenantID) + headers = map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + body = strings.NewReader(fmt.Sprintf( + "client_id=%s&client_secret=%s&scope=%s", + c.Credentials.AzureClientID, + c.Credentials.AzureClientSecret, + "user.read openid profile offline_access")) + ) + + resp, err := c.Post(ctx, rawURL, headers, body) + if err != nil { + return deviceResp{}, graph.Stack(ctx, err) + } + + if resp.StatusCode == http.StatusBadRequest { + return deviceResp{}, clues.New("incorrect tenant or credentials") + } + + if resp.StatusCode/100 == 4 || resp.StatusCode/100 == 5 { + return deviceResp{}, clues.New("non-2xx response: " + resp.Status) + } + + defer resp.Body.Close() + + var ar deviceResp + err = json.NewDecoder(resp.Body).Decode(&ar) + + return ar, clues.Wrap(err, "undecodable resp body").WithClues(ctx).OrNil() +} + +func (c *Access) GetDeviceToken( + ctx context.Context, + deviceCode string, +) (deviceResp, error) { + var ( + //nolint:lll + // https://dzone.com/articles/getting-access-token-for-microsoft-graph-using-oau + rawURL = fmt.Sprintf( + "https://login.microsoftonline.com/%s/oauth2/v2.0/token", + c.Credentials.AzureTenantID) + headers = map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + body = strings.NewReader(fmt.Sprintf( + "grant_type=urn:ietf:params:oauth:grant-type:device_code"+ + "&client_id=%s"+ + "&client_secret=%s"+ + "&device_code=%s", + c.Credentials.AzureClientID, + c.Credentials.AzureClientSecret, + deviceCode)) + ) + + fmt.Printf("\n-----\ndc %q\n-----\n", deviceCode) + + resp, err := c.Post(ctx, rawURL, headers, body) + if err != nil { + err = graph.Stack(ctx, err) + fmt.Printf("\n-----\nERROR %+v\n-----\n", clues.ToCore(err)) + + return deviceResp{}, err + } + + if resp.StatusCode == http.StatusBadRequest { + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + return deviceResp{}, clues.Wrap(err, "dumping http response") + } + + fmt.Printf("\n-----\nresp %+v\n-----\n", string(respDump)) + + return deviceResp{}, clues.New("incorrect tenant or credentials") + } + + if resp.StatusCode/100 == 4 || resp.StatusCode/100 == 5 { + respDump, err := httputil.DumpResponse(resp, true) + if err != nil { + return deviceResp{}, clues.Wrap(err, "dumping http response") + } + + fmt.Printf("\n-----\nresp %+v\n-----\n", string(respDump)) + + return deviceResp{}, clues.New("non-2xx response: " + resp.Status) + } + + defer resp.Body.Close() + + var ar deviceResp + err = json.NewDecoder(resp.Body).Decode(&ar) + + return ar, clues.Wrap(err, "undecodable resp body").WithClues(ctx).OrNil() }