-
Notifications
You must be signed in to change notification settings - Fork 222
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
73178c7
commit e50e857
Showing
8 changed files
with
406 additions
and
6 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"net" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/spf13/cobra" | ||
"go.flipt.io/flipt/internal/cmd/cloud" | ||
"go.flipt.io/flipt/internal/cmd/util" | ||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
type cloudCommand struct { | ||
url string | ||
} | ||
|
||
type cloudAuth struct { | ||
Token string `json:"token"` | ||
} | ||
|
||
func newCloudCommand() *cobra.Command { | ||
cloud := &cloudCommand{} | ||
|
||
cmd := &cobra.Command{ | ||
Use: "cloud", | ||
Short: "Interact with Flipt Cloud", | ||
} | ||
|
||
cmd.PersistentFlags().StringVarP(&cloud.url, "url", "u", "https://flipt.cloud", "Flipt Cloud URL") | ||
|
||
authCmd := &cobra.Command{ | ||
Use: "auth [flags]", | ||
Short: "Authenticate with Flipt Cloud", | ||
RunE: cloud.auth, | ||
Args: cobra.NoArgs, | ||
} | ||
|
||
cmd.AddCommand(authCmd) | ||
return cmd | ||
} | ||
|
||
func (c *cloudCommand) auth(cmd *cobra.Command, args []string) error { | ||
done := make(chan struct{}) | ||
const callbackURL = "http://localhost:8080/cloud/auth/callback" | ||
|
||
ok, err := util.PromptConfirm("Open browser to authenticate with Flipt Cloud?", false) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if !ok { | ||
return nil | ||
} | ||
|
||
flow, err := cloud.InitFlow() | ||
if err != nil { | ||
return fmt.Errorf("initializing flow: %w", err) | ||
} | ||
|
||
var ( | ||
g errgroup.Group | ||
ctx, cancel = context.WithCancel(cmd.Context()) | ||
) | ||
|
||
defer cancel() | ||
|
||
cloudAuthFile := filepath.Join(userConfigDir, "cloud.json") | ||
|
||
g.Go(func() error { | ||
tok, err := flow.Wait(ctx) | ||
if err != nil { | ||
return fmt.Errorf("waiting for token: %w", err) | ||
} | ||
|
||
cloudAuth := cloudAuth{ | ||
Token: tok, | ||
} | ||
|
||
cloudAuthBytes, err := json.Marshal(cloudAuth) | ||
if err != nil { | ||
return fmt.Errorf("marshalling cloud auth token: %w", err) | ||
} | ||
|
||
if err := os.WriteFile(cloudAuthFile, cloudAuthBytes, 0600); err != nil { | ||
return fmt.Errorf("writing cloud auth token: %w", err) | ||
} | ||
|
||
fmt.Println("\n✅ Authenticated with Flipt Cloud!\nYou can now run commands that require cloud authentication.") | ||
|
||
return nil | ||
}) | ||
|
||
g.Go(func() error { | ||
if err := flow.StartServer(nil); !errors.Is(err, net.ErrClosed) { | ||
return fmt.Errorf("starting server: %w", err) | ||
} | ||
close(done) | ||
return nil | ||
}) | ||
|
||
g.Go(func() error { | ||
select { | ||
case <-done: | ||
cancel() | ||
case <-ctx.Done(): | ||
if err := flow.Close(); !errors.Is(err, context.Canceled) { | ||
return err | ||
} | ||
} | ||
return nil | ||
}) | ||
|
||
browserParams := cloud.BrowserParams{ | ||
RedirectURL: callbackURL, | ||
} | ||
|
||
g.Go(func() error { | ||
url, err := flow.BrowserURL(fmt.Sprintf("%s/login/device", c.url), browserParams) | ||
if err != nil { | ||
return fmt.Errorf("creating browser URL: %w", err) | ||
} | ||
return util.OpenBrowser(url) | ||
}) | ||
|
||
return g.Wait() | ||
} |
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package cloud | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net" | ||
"net/http" | ||
) | ||
|
||
// TokenResponse represents the token received by the local server's callback handler. | ||
type TokenResponse struct { | ||
Token string | ||
State string | ||
} | ||
|
||
// bindLocalServer initializes a LocalServer that will listen on a randomly available TCP port. | ||
func bindLocalServer() (*localServer, error) { | ||
listener, err := net.Listen("tcp4", "127.0.0.1:0") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &localServer{ | ||
listener: listener, | ||
resultChan: make(chan TokenResponse, 1), | ||
}, nil | ||
} | ||
|
||
type localServer struct { | ||
CallbackPath string | ||
WriteSuccessHTML func(w io.Writer) | ||
|
||
resultChan chan (TokenResponse) | ||
listener net.Listener | ||
} | ||
|
||
func (s *localServer) Port() int { | ||
return s.listener.Addr().(*net.TCPAddr).Port | ||
} | ||
|
||
func (s *localServer) Close() error { | ||
return s.listener.Close() | ||
} | ||
|
||
func (s *localServer) Serve() error { | ||
return http.Serve(s.listener, s) | ||
} | ||
|
||
func (s *localServer) Wait(ctx context.Context) (TokenResponse, error) { | ||
select { | ||
case <-ctx.Done(): | ||
return TokenResponse{}, ctx.Err() | ||
case code := <-s.resultChan: | ||
return code, nil | ||
} | ||
} | ||
|
||
// ServeHTTP implements http.Handler. | ||
func (s *localServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
if s.CallbackPath != "" && r.URL.Path != s.CallbackPath { | ||
w.WriteHeader(404) | ||
return | ||
} | ||
defer func() { | ||
_ = s.Close() | ||
}() | ||
|
||
params := r.URL.Query() | ||
s.resultChan <- TokenResponse{ | ||
Token: params.Get("token"), | ||
State: params.Get("state"), | ||
} | ||
|
||
w.Header().Add("content-type", "text/html") | ||
if s.WriteSuccessHTML != nil { | ||
s.WriteSuccessHTML(w) | ||
} else { | ||
defaultSuccessHTML(w) | ||
} | ||
} | ||
|
||
func defaultSuccessHTML(w io.Writer) { | ||
fmt.Fprintf(w, "<p>You may now close this page and return to the client app.</p>") | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
package cloud | ||
|
||
import ( | ||
"context" | ||
"crypto/rand" | ||
"encoding/hex" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net/url" | ||
) | ||
|
||
// Flow holds the state for the steps of our OAuth-like Web Application flow. | ||
type Flow struct { | ||
server *localServer | ||
state string | ||
} | ||
|
||
// InitFlow creates a new Flow instance by detecting a locally available port number. | ||
func InitFlow() (*Flow, error) { | ||
server, err := bindLocalServer() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
state, _ := randomString(20) | ||
|
||
return &Flow{ | ||
server: server, | ||
state: state, | ||
}, nil | ||
} | ||
|
||
// BrowserParams are GET query parameters for initiating the web flow. | ||
type BrowserParams struct { | ||
RedirectURL string | ||
} | ||
|
||
// BrowserURL appends GET query parameters to baseURL and returns the url that the user should | ||
// navigate to in their web browser. | ||
func (f *Flow) BrowserURL(baseURL string, params BrowserParams) (string, error) { | ||
ru, err := url.Parse(params.RedirectURL) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
ru.Host = fmt.Sprintf("%s:%d", ru.Hostname(), f.server.Port()) | ||
f.server.CallbackPath = ru.Path | ||
|
||
q := url.Values{} | ||
q.Set("redirect_url", ru.String()) | ||
q.Set("state", f.state) | ||
|
||
return fmt.Sprintf("%s?%s", baseURL, q.Encode()), nil | ||
} | ||
|
||
// StartServer starts the localhost server and blocks until it has received the web redirect. The | ||
// writeSuccess function can be used to render a HTML page to the user upon completion. | ||
func (f *Flow) StartServer(writeSuccess func(io.Writer)) error { | ||
f.server.WriteSuccessHTML = writeSuccess | ||
return f.server.Serve() | ||
} | ||
|
||
func (f *Flow) Close() error { | ||
return f.server.Close() | ||
} | ||
|
||
// WaitOptions specifies parameters to exchange the access token for. | ||
type WaitOptions struct { | ||
// ClientSecret is the app client secret value. | ||
ClientSecret string | ||
} | ||
|
||
// Wait blocks until the browser flow has completed and returns the access token. | ||
func (f *Flow) Wait(ctx context.Context) (string, error) { | ||
resp, err := f.server.Wait(ctx) | ||
if err != nil { | ||
return "", err | ||
} | ||
if resp.State != f.state { | ||
return "", errors.New("state mismatch") | ||
} | ||
|
||
return resp.Token, nil | ||
} | ||
|
||
func randomString(length int) (string, error) { | ||
b := make([]byte, length/2) | ||
_, err := rand.Read(b) | ||
if err != nil { | ||
return "", err | ||
} | ||
return hex.EncodeToString(b), nil | ||
} |
Oops, something went wrong.