Skip to content

Commit 08044b7

Browse files
committed
Adds device flow
1 parent f0d46e8 commit 08044b7

File tree

5 files changed

+418
-75
lines changed

5 files changed

+418
-75
lines changed

cli.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,20 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
122122
var pollInterval string
123123
var interval int
124124
var state string
125+
var userCode string
125126
var listener net.Listener
126127

127128
if secret != nil {
128129
pollInterval, _ = secret.Data["poll_interval"].(string)
129130
state, _ = secret.Data["state"].(string)
131+
userCode, _ = secret.Data["user_code"].(string)
130132
}
131-
if callbackMode == "direct" {
133+
if callbackMode != "client" {
132134
if state == "" {
133-
return nil, errors.New("no state returned in direct callback mode")
135+
return nil, errors.New("no state returned in " + callbackMode + " callback mode")
134136
}
135137
if pollInterval == "" {
136-
return nil, errors.New("no poll_interval returned in direct callback mode")
138+
return nil, errors.New("no poll_interval returned in " + callbackMode + " callback mode")
137139
}
138140
interval, err = strconv.Atoi(pollInterval)
139141
if err != nil {
@@ -186,6 +188,31 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
186188
// authorization is pending, try again
187189
}
188190
}
191+
if userCode != "" {
192+
fmt.Fprintf(os.Stderr, "When prompted, enter code %s\n\n", userCode)
193+
}
194+
195+
if callbackMode != "client" {
196+
data := map[string]interface{}{
197+
"state": state,
198+
"client_nonce": clientNonce,
199+
}
200+
pollUrl := fmt.Sprintf("auth/%s/oidc/poll", mount)
201+
for {
202+
time.Sleep(time.Duration(interval) * time.Second)
203+
204+
secret, err := c.Logical().Write(pollUrl, data)
205+
if err == nil {
206+
return secret, nil
207+
}
208+
if strings.HasSuffix(err.Error(), "slow_down") {
209+
interval *= 2
210+
} else if !strings.HasSuffix(err.Error(), "authorization_pending") {
211+
return nil, err
212+
}
213+
// authorization is pending, try again
214+
}
215+
}
189216

190217
// Start local server
191218
go func() {
@@ -377,8 +404,9 @@ Configuration:
377404
Vault role of type "OIDC" to use for authentication.
378405
379406
callbackmode=<string>
380-
Mode of callback: "direct" for direct connection to Vault or "client"
381-
for connection to command line client (default: client).
407+
Mode of callback: "direct" for direct connection to Vault, "client"
408+
for connection to command line client, or "device" for device flow
409+
which has no callback (default: client).
382410
383411
listenaddress=<string>
384412
Optional address to bind the OIDC callback listener to in client callback

path_config.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import (
55
"crypto"
66
"crypto/tls"
77
"crypto/x509"
8+
"encoding/json"
89
"errors"
10+
"fmt"
11+
"io/ioutil"
912
"net/http"
13+
"net/url"
1014
"strings"
1115

1216
"github.com/hashicorp/cap/jwt"
@@ -151,6 +155,91 @@ func (b *jwtAuthBackend) config(ctx context.Context, s logical.Storage) (*jwtCon
151155
return config, nil
152156
}
153157

158+
func contactIssuer(ctx context.Context, uri string, data *url.Values, ignoreBad bool) ([]byte, error) {
159+
var req *http.Request
160+
var err error
161+
if data == nil {
162+
req, err = http.NewRequest("GET", uri, nil)
163+
} else {
164+
req, err = http.NewRequest("POST", uri, strings.NewReader(data.Encode()))
165+
}
166+
if err != nil {
167+
return nil, nil
168+
}
169+
if data != nil {
170+
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
171+
}
172+
173+
client, ok := ctx.Value(oauth2.HTTPClient).(*http.Client)
174+
if !ok {
175+
client = http.DefaultClient
176+
}
177+
resp, err := client.Do(req.WithContext(ctx))
178+
if err != nil {
179+
return nil, nil
180+
}
181+
defer resp.Body.Close()
182+
183+
body, err := ioutil.ReadAll(resp.Body)
184+
if err != nil {
185+
return nil, nil
186+
}
187+
188+
if resp.StatusCode != http.StatusOK && (!ignoreBad || resp.StatusCode != http.StatusBadRequest) {
189+
return nil, fmt.Errorf("%s: %s", resp.Status, body)
190+
}
191+
192+
return body, nil
193+
}
194+
195+
// Discover the device_authorization_endpoint URL and store it in the config
196+
// This should be in coreos/go-oidc but they don't yet support device flow
197+
// At the same time, look up token_endpoint and store it as well
198+
// Returns nil on success, otherwise returns an error
199+
func (b *jwtAuthBackend) configDeviceAuthURL(ctx context.Context, s logical.Storage) (error) {
200+
config, err := b.config(ctx, s)
201+
if err != nil {
202+
return err
203+
}
204+
205+
b.l.Lock()
206+
defer b.l.Unlock()
207+
208+
if config.OIDCDeviceAuthURL != "" {
209+
if config.OIDCDeviceAuthURL == "N/A" {
210+
return fmt.Errorf("no device auth endpoint url discovered")
211+
}
212+
return nil
213+
}
214+
215+
caCtx, err := b.createCAContext(b.providerCtx, config.OIDCDiscoveryCAPEM)
216+
if err != nil {
217+
return errwrap.Wrapf("error creating context for device auth: {{err}}", err)
218+
}
219+
220+
issuer := config.OIDCDiscoveryURL
221+
222+
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
223+
body, err := contactIssuer(caCtx, wellKnown, nil, false)
224+
if err != nil {
225+
return errwrap.Wrapf("error reading issuer config: {{err}}", err)
226+
}
227+
228+
var daj struct {
229+
DeviceAuthURL string `json:"device_authorization_endpoint"`
230+
TokenURL string `json:"token_endpoint"`
231+
}
232+
err = json.Unmarshal(body, &daj)
233+
if err != nil || daj.DeviceAuthURL == "" {
234+
b.cachedConfig.OIDCDeviceAuthURL = "N/A"
235+
return fmt.Errorf("no device auth endpoint url discovered")
236+
}
237+
238+
b.cachedConfig.OIDCDeviceAuthURL = daj.DeviceAuthURL
239+
b.cachedConfig.OIDCTokenURL = daj.TokenURL
240+
return nil
241+
}
242+
154243
func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
155244
config, err := b.config(ctx, req.Storage)
156245
if err != nil {
@@ -405,6 +494,9 @@ type jwtConfig struct {
405494
NamespaceInState bool `json:"namespace_in_state"`
406495

407496
ParsedJWTPubKeys []crypto.PublicKey `json:"-"`
497+
// These are looked up from OIDCDiscoveryURL when needed
498+
OIDCDeviceAuthURL string `json:"-"`
499+
OIDCTokenURL string `json:"-"`
408500
}
409501

410502
const (

0 commit comments

Comments
 (0)