Skip to content

Commit

Permalink
Merge pull request #111 from digitalocean/auth-init
Browse files Browse the repository at this point in the history
Introduce `doctl auth init`
  • Loading branch information
bryanl authored Jul 25, 2016
2 parents a57555c + 269c545 commit e17a1be
Show file tree
Hide file tree
Showing 14 changed files with 287 additions and 439 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ dist
public
docs/content

out/doctl
/out
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ You can download the archive from your browser, or copy its URL and retrieve it
cd ~
# OS X
curl -L https://github.com/digitalocean/doctl/releases/download/v1.1.0/doctl-1.1.0-darwin-10.6-amd64.tar.gz | tar xz
curl -L https://github.com/digitalocean/doctl/releases/download/v1.4.0/doctl-1.4.0-darwin-10.6-amd64.tar.gz | tar xz
# linux (with wget)
wget -qO- https://github.com/digitalocean/doctl/releases/download/v1.1.0/doctl-1.1.0-linux-amd64.tar.gz | tar xz
wget -qO- https://github.com/digitalocean/doctl/releases/download/v1.4.0/doctl-1.4.0-linux-amd64.tar.gz | tar xz
# linux (with curl)
curl -L https://github.com/digitalocean/doctl/releases/download/v1.1.0/doctl-1.1.0-linux-amd64.tar.gz | tar xz
curl -L https://github.com/digitalocean/doctl/releases/download/v1.4.0/doctl-1.4.0-linux-amd64.tar.gz | tar xz
```

Move the `doctl` binary to somewhere in your path. For example:
Expand All @@ -79,14 +79,18 @@ go get github.com/digitalocean/doctl/cmd/doctl

## Initialization

To automatically retrieve your access token from DigitalOcean, run `doctl auth login`. This process will authenticate
you with DigitalOcean and retrieve an access token. If your shell does not have access to a web browser
(because of a remote Linux shell with no DISPLAY environment variable or you've specified the CLIAUTH=1 flag), `doctl`
will give you a link for offline authentication.
To use `doctl`, a DigitalOcean access token is required. [Generate](https://cloud.digitalocean.com/settings/api/tokens)
a new token and run `doctl auth init`, or set the environment variable, `DIGITALOCEAN_ACCESS_TOKEN`, with your new
token.

## Configuration

By default, `doctl` will load a configuration file from `$HOME/.doctlcfg` if found.
By default, `doctl` will load a configuration file from `$XDG_CONFIG_HOME/doctl/config.yaml` if found. If
the `XDG_CONFIG_HOME` environment variable is not, the path will default to `$HOME/.config/doctl/config.yaml` on
Unix like systems, and `%APPDATA%/doctl/config/config.yaml` on Windows.

The configuration file has changed locations in recent versions, and a warning will be displayed if your configuration
exists at the legacy location.

### Configuration OPTIONS

Expand Down Expand Up @@ -143,4 +147,4 @@ repository is required.
[tutorial]: https://www.digitalocean.com/community/tutorials/how-to-use-doctl-the-official-digitalocean-command-line-client
[doctl-releases]: https://github.com/digitalocean/doctl/releases
[windows-release]: https://github.com/digitalocean/doctl/releases/download/v1.1.0/doctl-1.1.0-windows-4.0-amd64.zip
[windows-release]: https://github.com/digitalocean/doctl/releases/download/v1.4.0/doctl-1.4.0-windows-4.0-amd64.zip
200 changes: 22 additions & 178 deletions commands/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,13 @@ package commands

import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"runtime"
"strings"

"golang.org/x/crypto/ssh/terminal"

"github.com/bryanl/doit-server"
"github.com/bryanl/webbrowser"
"github.com/gorilla/websocket"
"github.com/satori/go.uuid"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -46,7 +38,7 @@ var retrieveUserTokenFunc = func() (string, error) {
}

reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter token: ")
fmt.Print("DigitalOcean access token: ")
return reader.ReadString('\n')
}

Expand All @@ -71,187 +63,39 @@ func Auth() *Command {
},
}

CmdBuilder(cmd, RunAuthLogin, "login", "login to DigitalOcean account", Writer, docCategories("account"))
cmdBuilderWithInit(cmd, RunAuthInit, "init", "initialize configuration", Writer, false, docCategories("auth"))

return cmd
}

// RunAuthLogin runs auth login. It communicates with doit-server to perform auth.
func RunAuthLogin(c *CmdConfig) error {
dsa := newDoitServerAuth()

ac, err := dsa.retrieveAuthCredentials()
if err != nil {
return err
}

token, err := dsa.initAuth(ac)
if err != nil {
return err
}

viper.Set("access-token", token)

fmt.Println("updated access token")

return nil
}

type doitServerAuth struct {
url string
browserOpen func(u string) error
isCLI func() bool
monitorAuth func(u string, ac *doitserver.AuthCredentials) (*doitserver.TokenResponse, error)
}

func newDoitServerAuth() *doitServerAuth {
return &doitServerAuth{
url: "http://doit-server.apps.pifft.com",
browserOpen: func(u string) error {
return webbrowser.Open(u, webbrowser.NewTab, true)
},
isCLI: func() bool {
return (runtime.GOOS == "linux" && os.Getenv("DISPLAY") == "") || os.Getenv("CLIAUTH") != ""
},
monitorAuth: monitorAuthWS,
}
}

func (dsa *doitServerAuth) initAuth(ac *doitserver.AuthCredentials) (string, error) {
if dsa.isCLI() {
return dsa.initAuthCLI(ac)
}

return dsa.initAuthBrowser(ac)
}

func (dsa *doitServerAuth) initAuthCLI(ac *doitserver.AuthCredentials) (string, error) {
u, err := dsa.createAuthURL(ac, keyPair{k: "cliauth", v: "1"})
if err != nil {
return "", err
}

fmt.Printf("Visit the following URL in your browser: %s\n", u)

return retrieveUserTokenFunc()
}

func (dsa *doitServerAuth) initAuthBrowser(ac *doitserver.AuthCredentials) (string, error) {
u, err := dsa.createAuthURL(ac)
if err != nil {
return "", err
}

err = dsa.browserOpen(u)
if err != nil {
return "", err
}

tr, err := dsa.monitorAuth(dsa.url, ac)
if err != nil {
return "", err
}

return tr.AccessToken, nil
}

func (dsa *doitServerAuth) retrieveAuthCredentials() (*doitserver.AuthCredentials, error) {
u, err := url.Parse(dsa.url)
if err != nil {
return nil, err
}

u.Path = "/token"
v := u.Query()
v.Set("id", uuid.NewV4().String())
u.RawQuery = v.Encode()

r, err := http.Get(u.String())
if err != nil {
return nil, err
}
defer r.Body.Close()
if err != nil {
return nil, err
}

body, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, err
}

if r.StatusCode != 200 {
return nil, errors.New("it's broke, Jim")
}

var m doitserver.AuthCredentials
err = json.Unmarshal(body, &m)
if err != nil {
return nil, err
}

return &m, nil
}

type keyPair struct {
k, v string
}

func (dsa *doitServerAuth) createAuthURL(ac *doitserver.AuthCredentials, kps ...keyPair) (string, error) {
authURL, err := url.Parse(dsa.url)
// RunAuthInit initializes the doctl config. Configuration is stored in $XDG_CONFIG_HOME/doctl. On Unix, if
// XDG_CONFIG_HOME is not set, use $HOME/.config. On Windows use %APPDATA%/doctl/config.
func RunAuthInit(c *CmdConfig) error {
in, err := retrieveUserTokenFunc()
if err != nil {
return "", err
}

authURL.Path = "/auth/digitalocean"

q := authURL.Query()
q.Set("id", ac.ID)
q.Set("cs", ac.CS)

for _, kp := range kps {
q.Set(kp.k, kp.v)
return fmt.Errorf("unable to read DigitalOcean access token: %s", err)
}

authURL.RawQuery = q.Encode()
token := strings.TrimSpace(in)

return authURL.String(), nil
viper.Set("access-token", string(token))

}
fmt.Fprintln(c.Out)
fmt.Fprint(c.Out, "Validating token: ")

func monitorAuthWS(serverURL string, ac *doitserver.AuthCredentials) (*doitserver.TokenResponse, error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
// need to initial the godo client since we've changed the configuration.
if err := c.initServices(c); err != nil {
return fmt.Errorf("unable to initialize DigitalOcean API client with new token: %s", err)
}

switch u.Scheme {
case "http":
u.Scheme = "ws"
case "https":
u.Scheme = "wss"
default:
return nil, &UnknownSchemeError{Scheme: u.Scheme}
if _, err := c.Account().Get(); err != nil {
fmt.Fprintln(c.Out, "invalid token")
fmt.Fprintln(c.Out)
return fmt.Errorf("unable to use supplied token to access API: %s", err)
}

u.Path = "/status"

conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return nil, err
}

err = conn.WriteJSON(ac)
if err != nil {
return nil, err
}

var tr doitserver.TokenResponse

err = conn.ReadJSON(&tr)
if err != nil {
return nil, err
}
fmt.Fprintln(c.Out, "OK")
fmt.Fprintln(c.Out)

return &tr, nil
return writeConfig()
}
Loading

0 comments on commit e17a1be

Please sign in to comment.