Skip to content

Commit

Permalink
Add --bind-addr, fixes #2620
Browse files Browse the repository at this point in the history
This commit adds `--bind-addr` flag to tsh login
and TELEPORT_LOGIN_BIND_ADDR environment variable
to set up login bind address for SSO redirect flows.

Usage examples:

```
tsh login  --bind-addr=localhost:3333
tsh login --bind-addr=:3333
tsh login --bind-addr=[::1]:3333
TELEPORT_LOGIN_BIND_ADDR=localhost:7777 tsh login
```

Refactor redirect flow and fix URLs for --bind-addr
  • Loading branch information
klizhentas committed Apr 15, 2019
1 parent 7a11023 commit 074a3db
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 39 deletions.
25 changes: 15 additions & 10 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ type Config struct {
// CheckVersions will check that client version is compatible
// with auth server version when connecting.
CheckVersions bool

// BindAddr is an optional host:port to bind to for SSO redirect flows
BindAddr string
}

// CachePolicy defines cache policy for local clients
Expand Down Expand Up @@ -1799,16 +1802,18 @@ func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType stri
func (tc *TeleportClient) ssoLogin(ctx context.Context, connectorID string, pub []byte, protocol string) (*auth.SSHLoginResponse, error) {
log.Debugf("samlLogin start")
// ask the CA (via proxy) to sign our public key:
response, err := SSHAgentSSOLogin(
ctx,
tc.Config.WebProxyAddr,
connectorID,
pub,
tc.KeyTTL,
tc.InsecureSkipVerify,
loopbackPool(tc.Config.WebProxyAddr),
protocol,
tc.CertificateFormat)
response, err := SSHAgentSSOLogin(SSHLogin{
Context: ctx,
ProxyAddr: tc.Config.WebProxyAddr,
ConnectorID: connectorID,
PubKey: pub,
TTL: tc.KeyTTL,
Insecure: tc.InsecureSkipVerify,
Pool: loopbackPool(tc.Config.WebProxyAddr),
Protocol: protocol,
Compatibility: tc.CertificateFormat,
BindAddr: tc.BindAddr,
})
return response, trace.Wrap(err)
}

Expand Down
96 changes: 70 additions & 26 deletions lib/client/weblogin.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2015 Gravitational, Inc.
Copyright 2015-2019 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -34,6 +34,7 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"

"github.com/gravitational/roundtrip"
"github.com/gravitational/trace"
Expand Down Expand Up @@ -129,9 +130,34 @@ type sealData struct {
Nonce []byte `json:"nonce"`
}

// SSHLogin contains SSH login parameters
type SSHLogin struct {
// Context is an external context
Context context.Context
// ProxyAddr is the target proxy address
ProxyAddr string
// ConnectorID is the OIDC or SAML connector ID to use
ConnectorID string
// PubKey is SSH public key to sign
PubKey []byte
// TTL is requested TTL of the client certificates
TTL time.Duration
// Insecure turns off verification for x509 target proxy
Insecure bool
// Pool is x509 cert pool to use for server certifcate verification
Pool *x509.CertPool
// Protocol is an optional protocol selection
Protocol string
// Compatibility sets compatibility mode for SSH certificates
Compatibility string
// BindAddr is an optional host:port address to bind
// to for SSO login flows
BindAddr string
}

// SSHAgentSSOLogin is used by SSH Agent (tsh) to login using OpenID connect
func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey []byte, ttl time.Duration, insecure bool, pool *x509.CertPool, protocol string, compatibility string) (*auth.SSHLoginResponse, error) {
clt, proxyURL, err := initClient(proxyAddr, insecure, pool)
func SSHAgentSSOLogin(login SSHLogin) (*auth.SSHLoginResponse, error) {
clt, proxyURL, err := initClient(login.ProxyAddr, login.Insecure, login.Pool)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -167,7 +193,7 @@ func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey
})
}

server := httptest.NewServer(makeHandler(func(w http.ResponseWriter, r *http.Request) (*auth.SSHLoginResponse, error) {
handler := makeHandler(func(w http.ResponseWriter, r *http.Request) (*auth.SSHLoginResponse, error) {
if r.URL.Path != "/callback" {
return nil, trace.NotFound("path not found")
}
Expand All @@ -185,10 +211,39 @@ func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey
}

return re, nil
}))
})

redirPath := "/" + uuid.New()
// longURL will be set based on the response from the webserver
var longURL utils.SyncString
mux := http.NewServeMux()
mux.Handle("/callback", handler)
mux.HandleFunc(redirPath, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, longURL.Value(), http.StatusFound)
})
redir := httptest.NewServer(mux)
defer redir.Close()

var server *httptest.Server
if login.BindAddr != "" {
log.Debugf("Binding to %v.", login.BindAddr)
listener, err := net.Listen("tcp", login.BindAddr)
if err != nil {
return nil, trace.Wrap(err, "%v: could not bind to %v, make sure the address is host:port format for ipv4 and [ipv6]:port format for ipv6, and the address is not in use", err, login.BindAddr)
}
server = &httptest.Server{
Listener: listener,
Config: &http.Server{Handler: mux},
}
server.Start()
} else {
server = httptest.NewServer(mux)
}
defer server.Close()

// Encode the secret into "secret_key" parameter.
// redirURL is the short URL presented to the user
redirURL := server.URL + redirPath

u, err := url.Parse(server.URL + "/callback")
if err != nil {
return nil, trace.Wrap(err)
Expand All @@ -197,12 +252,12 @@ func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey
query.Set("secret_key", key.String())
u.RawQuery = query.Encode()

out, err := clt.PostJSON(ctx, clt.Endpoint("webapi", protocol, "login", "console"), SSOLoginConsoleReq{
out, err := clt.PostJSON(login.Context, clt.Endpoint("webapi", login.Protocol, "login", "console"), SSOLoginConsoleReq{
RedirectURL: u.String(),
PublicKey: pubKey,
CertTTL: ttl,
ConnectorID: connectorID,
Compatibility: compatibility,
PublicKey: login.PubKey,
CertTTL: login.TTL,
ConnectorID: login.ConnectorID,
Compatibility: login.Compatibility,
})
if err != nil {
return nil, trace.Wrap(err)
Expand All @@ -213,18 +268,7 @@ func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey
if err != nil {
return nil, trace.Wrap(err)
}

// Start a HTTP server on the client that re-directs to the SAML provider.
// This creates nice short URLs and also works around some platforms (like
// Windows) that truncate long URLs before passing them to the default browser.
redirPath := "/" + uuid.New()
redirMux := http.NewServeMux()
redirMux.HandleFunc(redirPath, func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, re.RedirectURL, http.StatusFound)
})
redir := httptest.NewServer(redirMux)
defer redir.Close()
redirURL := redir.URL + redirPath
longURL.Set(re.RedirectURL)

// If a command was found to launch the browser, create and start it.
var execCmd *exec.Cmd
Expand Down Expand Up @@ -254,7 +298,7 @@ func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey

// Print to screen in-case the command that launches the browser did not run.
fmt.Printf("If browser window does not open automatically, open it by ")
fmt.Printf("clicking on the link:\n %v\n", redirURL)
fmt.Printf("clicking on the link:\n %v\n", utils.ClickableURL(redirURL))

log.Infof("Waiting for response at: %v.", server.URL)

Expand All @@ -268,9 +312,9 @@ func SSHAgentSSOLogin(ctx context.Context, proxyAddr, connectorID string, pubKey
case <-time.After(defaults.CallbackTimeout):
log.Debugf("Timed out waiting for callback after %v.", defaults.CallbackTimeout)
return nil, trace.Wrap(trace.Errorf("timed out waiting for callback"))
case <-ctx.Done():
case <-login.Context.Done():
log.Debugf("Canceled by user.")
return nil, trace.Wrap(ctx.Err())
return nil, trace.Wrap(login.Context.Err())
}
}

Expand Down
46 changes: 46 additions & 0 deletions lib/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ package utils

import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"

"github.com/gravitational/teleport"
Expand Down Expand Up @@ -67,6 +70,49 @@ func ThisFunction() string {
return runtime.FuncForPC(pc[0]).Name()
}

// SyncString is a string value
// that can be concurrently accessed
type SyncString struct {
sync.Mutex
string
}

// Value returns value of the string
func (s *SyncString) Value() string {
s.Lock()
defer s.Unlock()
return s.string
}

// Set sets the value of the string
func (s *SyncString) Set(v string) {
s.Lock()
defer s.Unlock()
s.string = v
}

// ClickableURL fixes address in url to make sure
// it's clickable, e.g. it replaces "undefined" address like
// 0.0.0.0 used in network listeners format with loopback 127.0.0.1
func ClickableURL(in string) string {
out, err := url.Parse(in)
if err != nil {
return in
}
host, port, err := net.SplitHostPort(out.Host)
if err != nil {
return in
}
ip := net.ParseIP(host)
// if address is not an IP, unspecified, e.g. all interfaces 0.0.0.0 or multicast,
// replace with localhost that is clickable
if len(ip) == 0 || ip.IsUnspecified() || ip.IsMulticast() {
out.Host = fmt.Sprintf("127.0.0.1:%v", port)
return out.String()
}
return out.String()
}

// AsBool converts string to bool, in case of the value is empty
// or unknown, defaults to false
func AsBool(v string) bool {
Expand Down
20 changes: 20 additions & 0 deletions lib/utils/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,26 @@ func (s *UtilsSuite) TestVersions(c *check.C) {
}
}

// TestClickableURL tests clickable URL conversions
func (s *UtilsSuite) TestClickableURL(c *check.C) {
testCases := []struct {
info string
in string
out string
}{
{info: "original URL is OK", in: "http://127.0.0.1:3000/hello", out: "http://127.0.0.1:3000/hello"},
{info: "unspecified IPV6", in: "http://[::]:5050/howdy", out: "http://127.0.0.1:5050/howdy"},
{info: "unspecified IPV4", in: "http://0.0.0.0:5050/howdy", out: "http://127.0.0.1:5050/howdy"},
{info: "specified IPV4", in: "http://192.168.1.1:5050/howdy", out: "http://192.168.1.1:5050/howdy"},
{info: "specified IPV6", in: "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:5050/howdy", out: "http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:5050/howdy"},
}
for i, testCase := range testCases {
comment := check.Commentf("test case %v %q", i, testCase.info)
out := ClickableURL(testCase.in)
c.Assert(out, check.Equals, testCase.out, comment)
}
}

// TestParseSessionsURI parses sessions URI
func (s *UtilsSuite) TestParseSessionsURI(c *check.C) {
testCases := []struct {
Expand Down
12 changes: 9 additions & 3 deletions tool/tsh/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ type CLIConf struct {
// format to use with --out to store a fershly retreived certificate
IdentityFormat client.IdentityFileFormat

// BindAddr is an address in the form of host:port to bind to
// during `tsh login` command
BindAddr string

// AuthConnector is the name of the connector to use.
AuthConnector string

Expand Down Expand Up @@ -166,8 +170,9 @@ func main() {
}

const (
clusterEnvVar = "TELEPORT_SITE"
clusterHelp = "Specify the cluster to connect"
clusterEnvVar = "TELEPORT_SITE"
clusterHelp = "Specify the cluster to connect"
bindAddrEnvVar = "TELEPORT_LOGIN_BIND_ADDR"
)

// Run executes TSH client. same as main() but easier to test
Expand Down Expand Up @@ -240,6 +245,7 @@ func Run(args []string, underTest bool) {
// login logs in with remote proxy and obtains a "session certificate" which gets
// stored in ~/.tsh directory
login := app.Command("login", "Log in to a cluster and retrieve the session certificate")
login.Flag("bind-addr", "Address in the form of host:port to bind to for login command webhook").Envar(bindAddrEnvVar).StringVar(&cf.BindAddr)
login.Flag("out", "Identity output").Short('o').AllowDuplicate().StringVar(&cf.IdentityFileOut)
login.Flag("format", fmt.Sprintf("Identity format [%s] or %s (for OpenSSH compatibility)",
client.DefaultIdentityFormat,
Expand Down Expand Up @@ -917,7 +923,7 @@ func makeClient(cf *CLIConf, useProfileLogin bool) (tc *client.TeleportClient, e
if options.StrictHostKeyChecking == false {
c.HostKeyCallback = client.InsecureSkipHostKeyChecking
}

c.BindAddr = cf.BindAddr
return client.NewClient(c)
}

Expand Down

0 comments on commit 074a3db

Please sign in to comment.