-
Notifications
You must be signed in to change notification settings - Fork 0
/
sshlogin.go
217 lines (189 loc) · 5.88 KB
/
sshlogin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
package web
import (
"crypto/x509"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os/exec"
"runtime"
"time"
log "github.com/Sirupsen/logrus"
"github.com/gravitational/roundtrip"
"github.com/gravitational/trace"
"github.com/mailgun/lemma/secret"
)
const (
// HTTPS is https prefix
HTTPS = "https"
// WSS is secure web sockets prefix
WSS = "wss"
)
type sealData struct {
Value []byte `json:"value"`
Nonce []byte `json:"nonce"`
}
// SSHAgentOIDCLogin is used by SSH Agent to login using OpenID connect
func SSHAgentOIDCLogin(proxyAddr, connectorID string, pubKey []byte, ttl time.Duration, insecure bool, pool *x509.CertPool) (*SSHLoginResponse, error) {
clt, proxyURL, err := initClient(proxyAddr, insecure, pool)
if err != nil {
return nil, trace.Wrap(err)
}
// create one time encoding secret that we will use to verify
// callback from proxy that is received over untrusted channel (HTTP)
keyBytes, err := secret.NewKey()
if err != nil {
return nil, trace.Wrap(err)
}
decryptor, err := secret.New(&secret.Config{KeyBytes: keyBytes})
if err != nil {
return nil, trace.Wrap(err)
}
waitC := make(chan *SSHLoginResponse, 1)
errorC := make(chan error, 1)
proxyURL.Path = "/web/msg/error/login_failed"
redirectErrorURL := proxyURL.String()
proxyURL.Path = "/web/msg/info/login_success"
redirectSuccessURL := proxyURL.String()
makeHandler := func(fn func(http.ResponseWriter, *http.Request) (*SSHLoginResponse, error)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response, err := fn(w, r)
if err != nil {
if trace.IsNotFound(err) {
http.NotFound(w, r)
return
}
errorC <- err
http.Redirect(w, r, redirectErrorURL, http.StatusFound)
return
}
waitC <- response
http.Redirect(w, r, redirectSuccessURL, http.StatusFound)
})
}
server := httptest.NewServer(makeHandler(func(w http.ResponseWriter, r *http.Request) (*SSHLoginResponse, error) {
if r.URL.Path != "/callback" {
return nil, trace.NotFound("path not found")
}
encrypted := r.URL.Query().Get("response")
if encrypted == "" {
return nil, trace.BadParameter("missing required query parameters in %v", r.URL.String())
}
var encryptedData *secret.SealedBytes
err := json.Unmarshal([]byte(encrypted), &encryptedData)
if err != nil {
return nil, trace.BadParameter("failed to decode response in %v", r.URL.String())
}
out, err := decryptor.Open(encryptedData)
if err != nil {
return nil, trace.BadParameter("failed to decode response: in %v, err: %v", r.URL.String(), err)
}
var re *SSHLoginResponse
log.Infof("callback got response: %v", r.URL.String())
err = json.Unmarshal([]byte(out), &re)
if err != nil {
return nil, trace.BadParameter("failed to decode response: in %v, err: %v", r.URL.String(), err)
}
return re, nil
}))
defer server.Close()
u, err := url.Parse(server.URL + "/callback")
if err != nil {
return nil, trace.Wrap(err)
}
query := u.Query()
query.Set("secret", secret.KeyToEncodedString(keyBytes))
u.RawQuery = query.Encode()
out, err := clt.PostJSON(clt.Endpoint("webapi", "oidc", "login", "console"), oidcLoginConsoleReq{
RedirectURL: u.String(),
PublicKey: pubKey,
CertTTL: ttl,
ConnectorID: connectorID,
})
if err != nil {
return nil, trace.Wrap(err)
}
var re *oidcLoginConsoleResponse
err = json.Unmarshal(out.Bytes(), &re)
if err != nil {
return nil, trace.Wrap(err)
}
fmt.Printf("If browser window does not open automatically, open it by clicking on the link:\n %v\n", re.RedirectURL)
var command = "sensible-browser"
if runtime.GOOS == "darwin" {
command = "open"
}
path, err := exec.LookPath(command)
if err == nil {
exec.Command(path, re.RedirectURL).Start()
}
log.Infof("waiting for response on %v", server.URL)
select {
case err := <-errorC:
log.Infof("got error: ", err)
return nil, trace.Wrap(err)
case response := <-waitC:
log.Infof("got response: ", err)
return response, nil
case <-time.After(60 * time.Second):
log.Infof("got timeout ")
return nil, trace.Wrap(trace.Errorf("timeout waiting for callback"))
}
}
// SSHAgentLogin issues call to web proxy and receives temp certificate
// if credentials are valid
//
// proxyAddr must be specified as host:port
func SSHAgentLogin(proxyAddr, user, password, hotpToken string, pubKey []byte, ttl time.Duration, insecure bool, pool *x509.CertPool) (*SSHLoginResponse, error) {
clt, _, err := initClient(proxyAddr, insecure, pool)
if err != nil {
return nil, trace.Wrap(err)
}
re, err := clt.PostJSON(clt.Endpoint("webapi", "ssh", "certs"), createSSHCertReq{
User: user,
Password: password,
HOTPToken: hotpToken,
PubKey: pubKey,
TTL: ttl,
})
if err != nil {
return nil, trace.Wrap(err)
}
var out *SSHLoginResponse
err = json.Unmarshal(re.Bytes(), &out)
if err != nil {
return nil, trace.Wrap(err)
}
return out, nil
}
func initClient(proxyAddr string, insecure bool, pool *x509.CertPool) (*webClient, *url.URL, error) {
// validate proxyAddr:
host, port, err := net.SplitHostPort(proxyAddr)
if err != nil || host == "" || port == "" {
if err != nil {
log.Error(err)
}
return nil, nil, trace.BadParameter("'%v' is not a valid proxy address", proxyAddr)
}
proxyAddr = "https://" + net.JoinHostPort(host, port)
u, err := url.Parse(proxyAddr)
if err != nil {
return nil, nil, trace.BadParameter("'%v' is not a valid proxy address", proxyAddr)
}
var opts []roundtrip.ClientParam
if pool != nil {
// use custom set of trusted CAs
opts = append(opts, roundtrip.HTTPClient(newClientWithPool(pool)))
} else if insecure {
// skip https cert verification, oh no!
fmt.Printf("WARNING: You are using insecure connection to SSH proxy %v\n", proxyAddr)
opts = append(opts, roundtrip.HTTPClient(newInsecureClient()))
}
clt, err := newWebClient(proxyAddr, opts...)
if err != nil {
return nil, nil, trace.Wrap(err)
}
return clt, u, nil
}