Skip to content

Commit f373af0

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

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
@@ -8,9 +8,12 @@ import (
88
"crypto"
99
"crypto/tls"
1010
"crypto/x509"
11+
"encoding/json"
1112
"errors"
1213
"fmt"
14+
"io/ioutil"
1315
"net/http"
16+
"net/url"
1417
"strings"
1518

1619
"github.com/hashicorp/cap/jwt"
@@ -168,6 +171,91 @@ func (b *jwtAuthBackend) config(ctx context.Context, s logical.Storage) (*jwtCon
168171
return config, nil
169172
}
170173

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

459547
ParsedJWTPubKeys []crypto.PublicKey `json:"-"`
548+
// These are looked up from OIDCDiscoveryURL when needed
549+
OIDCDeviceAuthURL string `json:"-"`
550+
OIDCTokenURL string `json:"-"`
460551
}
461552

462553
const (

0 commit comments

Comments
 (0)