Skip to content

Commit 1db39e6

Browse files
committed
Add device flow
Signed-off-by: Dave Dykstra <[email protected]>
1 parent 9c29af1 commit 1db39e6

File tree

5 files changed

+409
-78
lines changed

5 files changed

+409
-78
lines changed

cli.go

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,18 +151,20 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
151151
var pollInterval string
152152
var interval int
153153
var state string
154+
var userCode string
154155
var listener net.Listener
155156

156157
if secret != nil {
157158
pollInterval, _ = secret.Data["poll_interval"].(string)
158159
state, _ = secret.Data["state"].(string)
160+
userCode, _ = secret.Data["user_code"].(string)
159161
}
160-
if callbackMode == "direct" {
162+
if callbackMode != "client" {
161163
if state == "" {
162-
return nil, errors.New("no state returned in direct callback mode")
164+
return nil, errors.New("no state returned in " + callbackMode + " callback mode")
163165
}
164166
if pollInterval == "" {
165-
return nil, errors.New("no poll_interval returned in direct callback mode")
167+
return nil, errors.New("no poll_interval returned in " + callbackMode + " callback mode")
166168
}
167169
interval, err = strconv.Atoi(pollInterval)
168170
if err != nil {
@@ -199,7 +201,11 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
199201
}
200202
fmt.Fprintf(os.Stderr, "Waiting for OIDC authentication to complete...\n")
201203

202-
if callbackMode == "direct" {
204+
if userCode != "" {
205+
fmt.Fprintf(os.Stderr, "When prompted, enter code %s\n\n", userCode)
206+
}
207+
208+
if callbackMode != "client" {
203209
data := map[string]interface{}{
204210
"state": state,
205211
"client_nonce": clientNonce,
@@ -212,7 +218,9 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
212218
if err == nil {
213219
return secret, nil
214220
}
215-
if !strings.HasSuffix(err.Error(), "authorization_pending") {
221+
if strings.HasSuffix(err.Error(), "slow_down") {
222+
interval *= 2
223+
} else if !strings.HasSuffix(err.Error(), "authorization_pending") {
216224
return nil, err
217225
}
218226
// authorization is pending, try again
@@ -376,8 +384,9 @@ Configuration:
376384
Vault role of type "OIDC" to use for authentication.
377385
378386
%s=<string>
379-
Mode of callback: "direct" for direct connection to Vault or "client"
380-
for connection to command line client (default: client).
387+
Mode of callback: "direct" for direct connection to Vault, "client"
388+
for connection to command line client, or "device" for device flow
389+
which has no callback (default: client).
381390
382391
%s=<string>
383392
Optional address to bind the OIDC callback listener to in client callback

path_config.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import (
99
"crypto/tls"
1010
"crypto/x509"
1111
"encoding/asn1"
12+
"encoding/json"
1213
"errors"
1314
"fmt"
15+
"io/ioutil"
1416
"net/http"
17+
"net/url"
1518
"strings"
1619

1720
"github.com/hashicorp/cap/jwt"
@@ -174,6 +177,91 @@ func (b *jwtAuthBackend) config(ctx context.Context, s logical.Storage) (*jwtCon
174177
return config, nil
175178
}
176179

180+
func contactIssuer(ctx context.Context, uri string, data *url.Values, ignoreBad bool) ([]byte, error) {
181+
var req *http.Request
182+
var err error
183+
if data == nil {
184+
req, err = http.NewRequest("GET", uri, nil)
185+
} else {
186+
req, err = http.NewRequest("POST", uri, strings.NewReader(data.Encode()))
187+
}
188+
if err != nil {
189+
return nil, err
190+
}
191+
if data != nil {
192+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
193+
}
194+
195+
client, ok := ctx.Value(oauth2.HTTPClient).(*http.Client)
196+
if !ok {
197+
client = http.DefaultClient
198+
}
199+
resp, err := client.Do(req.WithContext(ctx))
200+
if err != nil {
201+
return nil, nil
202+
}
203+
defer resp.Body.Close()
204+
205+
body, err := ioutil.ReadAll(resp.Body)
206+
if err != nil {
207+
return nil, nil
208+
}
209+
210+
if resp.StatusCode != http.StatusOK && (!ignoreBad || resp.StatusCode != http.StatusBadRequest) {
211+
return nil, fmt.Errorf("%s: %s", resp.Status, body)
212+
}
213+
214+
return body, nil
215+
}
216+
217+
// Discover the device_authorization_endpoint URL and store it in the config
218+
// This should be in coreos/go-oidc but they don't yet support device flow
219+
// At the same time, look up token_endpoint and store it as well
220+
// Returns nil on success, otherwise returns an error
221+
func (b *jwtAuthBackend) configDeviceAuthURL(ctx context.Context, s logical.Storage) error {
222+
config, err := b.config(ctx, s)
223+
if err != nil {
224+
return err
225+
}
226+
227+
b.l.Lock()
228+
defer b.l.Unlock()
229+
230+
if config.OIDCDeviceAuthURL != "" {
231+
if config.OIDCDeviceAuthURL == "N/A" {
232+
return fmt.Errorf("no device auth endpoint url discovered")
233+
}
234+
return nil
235+
}
236+
237+
caCtx, err := b.createCAContext(b.providerCtx, config.OIDCDiscoveryCAPEM)
238+
if err != nil {
239+
return errwrap.Wrapf("error creating context for device auth: {{err}}", err)
240+
}
241+
242+
issuer := config.OIDCDiscoveryURL
243+
244+
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
245+
body, err := contactIssuer(caCtx, wellKnown, nil, false)
246+
if err != nil {
247+
return errwrap.Wrapf("error reading issuer config: {{err}}", err)
248+
}
249+
250+
var daj struct {
251+
DeviceAuthURL string `json:"device_authorization_endpoint"`
252+
TokenURL string `json:"token_endpoint"`
253+
}
254+
err = json.Unmarshal(body, &daj)
255+
if err != nil || daj.DeviceAuthURL == "" {
256+
b.cachedConfig.OIDCDeviceAuthURL = "N/A"
257+
return fmt.Errorf("no device auth endpoint url discovered")
258+
}
259+
260+
b.cachedConfig.OIDCDeviceAuthURL = daj.DeviceAuthURL
261+
b.cachedConfig.OIDCTokenURL = daj.TokenURL
262+
return nil
263+
}
264+
177265
func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
178266
config, err := b.config(ctx, req.Storage)
179267
if err != nil {
@@ -502,6 +590,9 @@ type jwtConfig struct {
502590
UnsupportedCriticalCertExtensions []string `json:"unsupported_critical_cert_extensions"`
503591

504592
ParsedJWTPubKeys []crypto.PublicKey `json:"-"`
593+
// These are looked up from OIDCDiscoveryURL when needed
594+
OIDCDeviceAuthURL string `json:"-"`
595+
OIDCTokenURL string `json:"-"`
505596
}
506597

507598
const (

0 commit comments

Comments
 (0)