Skip to content

Commit 8f7fbc3

Browse files
committed
govc: add session.login -jwt option
Fixes #3041 Signed-off-by: Doug MacEachern <[email protected]>
1 parent 7396b62 commit 8f7fbc3

File tree

3 files changed

+99
-8
lines changed

3 files changed

+99
-8
lines changed

cli/session/login.go

+34-8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package session
1919
import (
2020
"bytes"
2121
"context"
22+
"encoding/base64"
2223
"errors"
2324
"flag"
2425
"fmt"
@@ -33,6 +34,7 @@ import (
3334
"github.com/vmware/govmomi/cli/flags"
3435
"github.com/vmware/govmomi/session"
3536
"github.com/vmware/govmomi/sts"
37+
"github.com/vmware/govmomi/vapi/authentication"
3638
"github.com/vmware/govmomi/vapi/rest"
3739
"github.com/vmware/govmomi/vim25"
3840
"github.com/vmware/govmomi/vim25/methods"
@@ -45,6 +47,7 @@ type login struct {
4547

4648
clone bool
4749
issue bool
50+
jwt string
4851
renew bool
4952
long bool
5053
vapi bool
@@ -69,6 +72,7 @@ func (cmd *login) Register(ctx context.Context, f *flag.FlagSet) {
6972

7073
f.BoolVar(&cmd.clone, "clone", false, "Acquire clone ticket")
7174
f.BoolVar(&cmd.issue, "issue", false, "Issue SAML token")
75+
f.StringVar(&cmd.jwt, "jwt", "", "Exchange SAML token for JWT audience")
7276
f.BoolVar(&cmd.renew, "renew", false, "Renew SAML token")
7377
f.BoolVar(&cmd.vapi, "r", false, "REST login")
7478
f.DurationVar(&cmd.life, "lifetime", time.Minute*10, "SAML token lifetime")
@@ -103,6 +107,7 @@ The session.login command can be used to:
103107
- Login using a vCenter Extension certificate
104108
- Issue a SAML token
105109
- Renew a SAML token
110+
- Exchange a SAML token for a JSON Web Token (JWT)
106111
- Login using a SAML token
107112
- Impersonate a user
108113
- Avoid passing credentials to other govc commands
@@ -124,6 +129,7 @@ Examples:
124129
token=$(govc session.login -u host -cert user.crt -key user.key -issue -token "$bearer")
125130
govc session.login -u host -cert user.crt -key user.key -token "$token"
126131
token=$(govc session.login -u host -cert user.crt -key user.key -renew -lifetime 24h -token "$token")
132+
govc session.login -jwt vmware-tes:vc:nsxd-v2:nsx -token "$token"
127133
# HTTP requests
128134
govc session.login -r -X GET /api/vcenter/namespace-management/clusters | jq .
129135
govc session.login -r -X POST /rest/vcenter/cluster/modules <<<'{"spec": {"cluster": "domain-c9"}}'`
@@ -210,6 +216,22 @@ func (cmd *login) issueToken(ctx context.Context, vc *vim25.Client) (string, err
210216
return s.Token, nil
211217
}
212218

219+
func (cmd *login) exchangeTokenJWT(ctx context.Context, c *rest.Client) (string, error) {
220+
spec := authentication.TokenIssueSpec{
221+
Audience: cmd.jwt,
222+
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
223+
RequestedTokenType: "urn:ietf:params:oauth:token-type:id_token",
224+
SubjectToken: base64.StdEncoding.EncodeToString([]byte(cmd.token)),
225+
SubjectTokenType: "urn:ietf:params:oauth:token-type:saml2",
226+
}
227+
228+
info, err := authentication.NewManager(c).Issue(ctx, spec)
229+
if err != nil {
230+
return "", err
231+
}
232+
return info.AccessToken, nil
233+
}
234+
213235
func (cmd *login) loginByToken(ctx context.Context, c *vim25.Client) error {
214236
header := soap.Header{
215237
Security: &sts.Signer{
@@ -340,6 +362,8 @@ func (cmd *login) Run(ctx context.Context, f *flag.FlagSet) error {
340362
case cmd.issue:
341363
cmd.Session.LoginSOAP = nologinSOAP
342364
cmd.Session.LoginREST = nologinREST
365+
case cmd.jwt != "":
366+
cmd.Session.LoginSOAP = nologinSOAP
343367
}
344368

345369
c, err := cmd.Client()
@@ -349,6 +373,14 @@ func (cmd *login) Run(ctx context.Context, f *flag.FlagSet) error {
349373

350374
r := &ticketResult{cmd: cmd}
351375

376+
var rc *rest.Client
377+
if cmd.vapi || cmd.jwt != "" {
378+
rc, err = cmd.RestClient()
379+
if err != nil {
380+
return err
381+
}
382+
}
383+
352384
switch {
353385
case cmd.clone:
354386
m := session.NewManager(c)
@@ -362,14 +394,8 @@ func (cmd *login) Run(ctx context.Context, f *flag.FlagSet) error {
362394
return err
363395
}
364396
return cmd.WriteResult(r)
365-
}
366-
367-
var rc *rest.Client
368-
if cmd.vapi {
369-
rc, err = cmd.RestClient()
370-
if err != nil {
371-
return err
372-
}
397+
case cmd.jwt != "":
398+
r.Token, err = cmd.exchangeTokenJWT(ctx, rc)
373399
}
374400

375401
if f.NArg() == 1 {

govc/USAGE.md

+3
Original file line numberDiff line numberDiff line change
@@ -5471,6 +5471,7 @@ The session.login command can be used to:
54715471
- Login using a vCenter Extension certificate
54725472
- Issue a SAML token
54735473
- Renew a SAML token
5474+
- Exchange a SAML token for a JSON Web Token (JWT)
54745475
- Login using a SAML token
54755476
- Impersonate a user
54765477
- Avoid passing credentials to other govc commands
@@ -5492,6 +5493,7 @@ Examples:
54925493
token=$(govc session.login -u host -cert user.crt -key user.key -issue -token "$bearer")
54935494
govc session.login -u host -cert user.crt -key user.key -token "$token"
54945495
token=$(govc session.login -u host -cert user.crt -key user.key -renew -lifetime 24h -token "$token")
5496+
govc session.login -jwt vmware-tes:vc:nsxd-v2:nsx -token "$token"
54955497
# HTTP requests
54965498
govc session.login -r -X GET /api/vcenter/namespace-management/clusters | jq .
54975499
govc session.login -r -X POST /rest/vcenter/cluster/modules <<<'{"spec": {"cluster": "domain-c9"}}'
@@ -5503,6 +5505,7 @@ Options:
55035505
-cookie= Set HTTP cookie for an existing session
55045506
-extension= Extension name
55055507
-issue=false Issue SAML token
5508+
-jwt= Exchange SAML token for JWT audience
55065509
-l=false Output session cookie
55075510
-lifetime=10m0s SAML token lifetime
55085511
-r=false REST login

vapi/authentication/authentication.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// © Broadcom. All Rights Reserved.
2+
// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package authentication
6+
7+
import (
8+
"context"
9+
"net/http"
10+
11+
"github.com/vmware/govmomi/vapi/rest"
12+
)
13+
14+
// Manager extends rest.Client, adding authentication related methods.
15+
type Manager struct {
16+
*rest.Client
17+
}
18+
19+
// NewManager creates a new Manager instance with the given client.
20+
func NewManager(client *rest.Client) *Manager {
21+
return &Manager{
22+
Client: client,
23+
}
24+
}
25+
26+
type TokenIssueSpec struct {
27+
SubjectToken string `json:"subject_token"`
28+
SubjectTokenType string `json:"subject_token_type"`
29+
GrantType string `json:"grant_type"`
30+
ActorToken string `json:"actor_token,omitempty"`
31+
ActorTokenType string `json:"actor_token_type,omitempty"`
32+
RequestedTokenType string `json:"requested_token_type,omitempty"`
33+
Resource string `json:"resource,omitempty"`
34+
Scope string `json:"scope,omitempty"`
35+
Audience string `json:"audience,omitempty"`
36+
}
37+
38+
type TokenInfo struct {
39+
AccessToken string `json:"access_token"`
40+
TokenType string `json:"token_type"`
41+
ExpiresIn int `json:"expires_in,omitempty"`
42+
IssuedTokenType string `json:"issued_token_type,omitempty"`
43+
RefreshToken string `json:"refresh_token,omitempty"`
44+
Scope string `json:"scope,omitempty"`
45+
}
46+
47+
func (c *Manager) Issue(ctx context.Context, token TokenIssueSpec) (*TokenInfo, error) {
48+
url := c.Resource("/vcenter/tokenservice/token-exchange")
49+
50+
var res TokenInfo
51+
52+
spec := struct {
53+
Spec TokenIssueSpec `json:"spec"`
54+
}{Spec: token}
55+
56+
err := c.Do(ctx, url.Request(http.MethodPost, spec), &res)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
return &res, nil
62+
}

0 commit comments

Comments
 (0)