Compare commits

...

3 Commits

Author SHA1 Message Date
ryanfkeepers
583caecf02 poc for obtaining a device token 2023-11-01 17:03:52 -06:00
ryanfkeepers
af804fad20 remove unused fields 2023-11-01 14:49:00 -06:00
ryanfkeepers
0e83120428 basic auth delegated token poc 2023-11-01 11:20:50 -06:00
8 changed files with 507 additions and 19 deletions

View File

@ -7,6 +7,7 @@ import (
"strings"
"github.com/spf13/cobra"
"go.uber.org/zap"
"golang.org/x/exp/slices"
"github.com/alcionai/corso/src/cli/backup"
@ -33,10 +34,10 @@ var corsoCmd = &cobra.Command{
Short: "Free, Secure, Open-Source Backup for M365.",
Long: `Free, Secure, and Open-Source Backup for Microsoft 365.`,
RunE: handleCorsoCmd,
PersistentPreRunE: preRun,
PersistentPreRunE: PreRun,
}
func preRun(cc *cobra.Command, args []string) error {
func PreRun(cc *cobra.Command, args []string) error {
if err := config.InitFunc(cc, args); err != nil {
return err
}
@ -104,6 +105,13 @@ func CorsoCommand() *cobra.Command {
return c
}
func AddSupportFlags(cmd *cobra.Command) {
config.AddConfigFlags(cmd)
logger.AddLoggingFlags(cmd)
observe.AddProgressBarFlags(cmd)
print.AddOutputFlag(cmd)
}
// BuildCommandTree builds out the command tree used by the Corso library.
func BuildCommandTree(cmd *cobra.Command) {
// want to order flags explicitly
@ -111,11 +119,8 @@ func BuildCommandTree(cmd *cobra.Command) {
flags.AddRunModeFlag(cmd, true)
cmd.Flags().BoolP("version", "v", false, "current version info")
cmd.PersistentPreRunE = preRun
config.AddConfigFlags(cmd)
logger.AddLoggingFlags(cmd)
observe.AddProgressBarFlags(cmd)
print.AddOutputFlag(cmd)
cmd.PersistentPreRunE = PreRun
AddSupportFlags(cmd)
flags.AddGlobalOperationFlags(cmd)
cmd.SetUsageTemplate(indentExamplesTemplate(corsoCmd.UsageTemplate()))
@ -132,14 +137,20 @@ func BuildCommandTree(cmd *cobra.Command) {
// Running Corso
// ------------------------------------------------------------------------------------------
// Handle builds and executes the cli processor.
func Handle() {
func SeedCtx() (context.Context, *zap.SugaredLogger) {
//nolint:forbidigo
ctx := config.Seed(context.Background())
ctx, log := logger.Seed(ctx, logger.PreloadLoggingFlags(os.Args[1:]))
ctx = print.SetRootCmd(ctx, corsoCmd)
ctx = observe.SeedObserver(ctx, print.StderrWriter(ctx), observe.PreloadFlags())
return ctx, log
}
// Handle builds and executes the cli processor.
func Handle() {
ctx, log := SeedCtx()
BuildCommandTree(corsoCmd)
defer func() {

View File

@ -98,17 +98,16 @@ func configureAccount(
// M365 is a helper for aggregating m365 secrets and credentials.
func GetM365(m365Cfg account.M365Config) credentials.M365 {
AzureClientID := str.First(
creds := credentials.GetM365()
creds.AzureClientID = str.First(
flags.AzureClientIDFV,
os.Getenv(credentials.AzureClientID),
creds.AzureClientID,
m365Cfg.AzureClientID)
AzureClientSecret := str.First(
creds.AzureClientSecret = str.First(
flags.AzureClientSecretFV,
os.Getenv(credentials.AzureClientSecret),
creds.AzureClientSecret,
m365Cfg.AzureClientSecret)
return credentials.M365{
AzureClientID: AzureClientID,
AzureClientSecret: AzureClientSecret,
}
return creds
}

View File

@ -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 <command> [<subcommand>] [<service>] [<flag>...]`
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
}

View File

@ -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 <command> [<subcommand>] [<service>] [<flag>...]`
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
}

126
src/cmd/device/device.go Normal file
View File

@ -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 <command> [<subcommand>] [<service>] [<flag>...]`
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
}

View File

@ -21,6 +21,8 @@ const (
keyAzureClientID = "azure_clientid"
keyAzureClientSecret = "azure_clientSecret"
keyAzureTenantID = "azure_tenantid"
keyAzureUsername = "azure_username"
keyAzureUserPassword = "azure_userPassword"
)
// StringConfig transforms a m365Config struct into a plain
@ -31,6 +33,8 @@ func (c M365Config) StringConfig() (map[string]string, error) {
keyAzureClientID: c.AzureClientID,
keyAzureClientSecret: c.AzureClientSecret,
keyAzureTenantID: c.AzureTenantID,
keyAzureUsername: c.AzureUsername,
keyAzureUserPassword: c.AzureUserPassword,
}
return cfg, c.validate()
@ -52,6 +56,8 @@ func (a Account) M365Config() (M365Config, error) {
c.AzureClientID = a.Config[keyAzureClientID]
c.AzureClientSecret = a.Config[keyAzureClientSecret]
c.AzureTenantID = a.Config[keyAzureTenantID]
c.AzureUsername = a.Config[keyAzureUsername]
c.AzureUserPassword = a.Config[keyAzureUserPassword]
}
return c, c.validate()

View File

@ -10,12 +10,16 @@ import (
const (
AzureClientID = "AZURE_CLIENT_ID"
AzureClientSecret = "AZURE_CLIENT_SECRET"
AzureUsername = "AZURE_USERNAME"
AzureUserPassword = "AZURE_USER_PASSWORD"
)
// M365 aggregates m365 credentials from flag and env_var values.
type M365 struct {
AzureClientID string
AzureClientSecret string
AzureUsername string
AzureUserPassword string
}
// M365 is a helper for aggregating m365 secrets and credentials.
@ -24,10 +28,14 @@ func GetM365() M365 {
// var AzureClientID, AzureClientSecret string
AzureClientID := os.Getenv(AzureClientID)
AzureClientSecret := os.Getenv(AzureClientSecret)
AzureUserPassword := os.Getenv(AzureUserPassword)
AzureUsername := os.Getenv(AzureUsername)
return M365{
AzureClientID: AzureClientID,
AzureClientSecret: AzureClientSecret,
AzureUsername: AzureUsername,
AzureUserPassword: AzureUserPassword,
}
}

View File

@ -2,8 +2,10 @@ package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httputil"
"strings"
"github.com/alcionai/clues"
@ -15,8 +17,8 @@ import (
// controller
// ---------------------------------------------------------------------------
func (c Client) Access() Access {
return Access{c}
func (c Client) Access() *Access {
return &Access{c}
}
// Access is an interface-compliant provider of the client.
@ -66,3 +68,193 @@ func (c Access) GetToken(
return nil
}
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 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,
) (delegatedResp, 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/token",
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"+
"&resource=https://graph.microsoft.com"+
"&grant_type=password"+
"&username=%s"+
"&password=%s",
c.Credentials.AzureClientID,
c.Credentials.AzureClientSecret,
c.Credentials.AzureUsername,
c.Credentials.AzureUserPassword))
)
resp, err := c.Post(ctx, rawURL, headers, body)
if err != nil {
return delegatedResp{}, graph.Stack(ctx, err)
}
if resp.StatusCode == http.StatusBadRequest {
return delegatedResp{}, clues.New("incorrect tenant or credentials")
}
if resp.StatusCode/100 == 4 || resp.StatusCode/100 == 5 {
return delegatedResp{}, clues.New("non-2xx response: " + resp.Status)
}
defer resp.Body.Close()
var ar delegatedResp
err = json.NewDecoder(resp.Body).Decode(&ar)
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()
}