Skip to content

Commit 3fce815

Browse files
committed
Add device flow
1 parent d5693f9 commit 3fce815

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
@@ -148,18 +148,20 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
148148
var pollInterval string
149149
var interval int
150150
var state string
151+
var userCode string
151152
var listener net.Listener
152153

153154
if secret != nil {
154155
pollInterval, _ = secret.Data["poll_interval"].(string)
155156
state, _ = secret.Data["state"].(string)
157+
userCode, _ = secret.Data["user_code"].(string)
156158
}
157-
if callbackMode == "direct" {
159+
if callbackMode != "client" {
158160
if state == "" {
159-
return nil, errors.New("no state returned in direct callback mode")
161+
return nil, errors.New("no state returned in " + callbackMode + " callback mode")
160162
}
161163
if pollInterval == "" {
162-
return nil, errors.New("no poll_interval returned in direct callback mode")
164+
return nil, errors.New("no poll_interval returned in " + callbackMode + " callback mode")
163165
}
164166
interval, err = strconv.Atoi(pollInterval)
165167
if err != nil {
@@ -215,6 +217,31 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
215217
// authorization is pending, try again
216218
}
217219
}
220+
if userCode != "" {
221+
fmt.Fprintf(os.Stderr, "When prompted, enter code %s\n\n", userCode)
222+
}
223+
224+
if callbackMode != "client" {
225+
data := map[string]interface{}{
226+
"state": state,
227+
"client_nonce": clientNonce,
228+
}
229+
pollUrl := fmt.Sprintf("auth/%s/oidc/poll", mount)
230+
for {
231+
time.Sleep(time.Duration(interval) * time.Second)
232+
233+
secret, err := c.Logical().Write(pollUrl, data)
234+
if err == nil {
235+
return secret, nil
236+
}
237+
if strings.HasSuffix(err.Error(), "slow_down") {
238+
interval *= 2
239+
} else if !strings.HasSuffix(err.Error(), "authorization_pending") {
240+
return nil, err
241+
}
242+
// authorization is pending, try again
243+
}
244+
}
218245

219246
// Start local server
220247
go func() {
@@ -373,8 +400,9 @@ Configuration:
373400
Vault role of type "OIDC" to use for authentication.
374401
375402
%s=<string>
376-
Mode of callback: "direct" for direct connection to Vault or "client"
377-
for connection to command line client (default: client).
403+
Mode of callback: "direct" for direct connection to Vault, "client"
404+
for connection to command line client, or "device" for device flow
405+
which has no callback (default: client).
378406
379407
%s=<string>
380408
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)