github.com/vmware/govmomi@v0.51.0/sts/client.go (about) 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 sts 6 7 import ( 8 "context" 9 "crypto/tls" 10 "errors" 11 "fmt" 12 "net/url" 13 "time" 14 15 internalhelpers "github.com/vmware/govmomi/internal" 16 "github.com/vmware/govmomi/lookup" 17 "github.com/vmware/govmomi/lookup/types" 18 "github.com/vmware/govmomi/sts/internal" 19 "github.com/vmware/govmomi/vim25" 20 "github.com/vmware/govmomi/vim25/soap" 21 vim25types "github.com/vmware/govmomi/vim25/types" 22 ) 23 24 const ( 25 Namespace = "oasis:names:tc:SAML:2.0:assertion" 26 basePath = "/sts" 27 Path = basePath + "/STSService" 28 SystemPath = basePath + "/system-STSService/sdk" 29 ) 30 31 // Client is a soap.Client targeting the STS (Secure Token Service) API endpoint. 32 type Client struct { 33 *soap.Client 34 35 RoundTripper soap.RoundTripper 36 } 37 38 func getEndpointURL(ctx context.Context, c *vim25.Client) string { 39 // Services running on vCenter can bypass lookup service using the 40 // system-STSService path. This avoids the need to lookup the system domain. 41 if usingSidecar := internalhelpers.UsingEnvoySidecar(c); usingSidecar { 42 return fmt.Sprintf("http://%s%s", c.URL().Host, SystemPath) 43 } 44 return getEndpointURLFromLookupService(ctx, c) 45 } 46 47 func getEndpointURLFromLookupService(ctx context.Context, c *vim25.Client) string { 48 filter := &types.LookupServiceRegistrationFilter{ 49 ServiceType: &types.LookupServiceRegistrationServiceType{ 50 Product: "com.vmware.cis", 51 Type: "cs.identity", 52 }, 53 EndpointType: &types.LookupServiceRegistrationEndpointType{ 54 Protocol: "wsTrust", 55 Type: "com.vmware.cis.cs.identity.sso", 56 }, 57 } 58 59 return lookup.EndpointURL(ctx, c, Path, filter) 60 } 61 62 // NewClient returns a client targeting the STS API endpoint. 63 // The Client.URL will be set to that of the Lookup Service's endpoint registration, 64 // as the SSO endpoint can be external to vCenter. If the Lookup Service is not available, 65 // URL defaults to Path on the vim25.Client.URL.Host. 66 func NewClient(ctx context.Context, c *vim25.Client) (*Client, error) { 67 68 url := getEndpointURL(ctx, c) 69 sc := c.Client.NewServiceClient(url, Namespace) 70 71 return &Client{sc, sc}, nil 72 } 73 74 // RoundTrip dispatches to the RoundTripper field. 75 func (c *Client) RoundTrip(ctx context.Context, req, res soap.HasFault) error { 76 // Drop any operationID header, not used by STS 77 ctx = context.WithValue(ctx, vim25types.ID{}, "") 78 return c.RoundTripper.RoundTrip(ctx, req, res) 79 } 80 81 // TokenRequest parameters for issuing a SAML token. 82 // At least one of Userinfo or Certificate must be specified. 83 // When `TokenRequest.Certificate` is set, the `tls.Certificate.PrivateKey` field must be set as it is required to sign the request. 84 // When the `tls.Certificate.Certificate` field is not set, the request Assertion header is set to that of the TokenRequest.Token. 85 // Otherwise `tls.Certificate.Certificate` is used as the BinarySecurityToken in the request. 86 type TokenRequest struct { 87 Userinfo *url.Userinfo // Userinfo when set issues a Bearer token 88 Certificate *tls.Certificate // Certificate when set issues a HoK token 89 Lifetime time.Duration // Lifetime is the token's lifetime, defaults to 10m 90 Renewable bool // Renewable allows the issued token to be renewed 91 Delegatable bool // Delegatable allows the issued token to be delegated (e.g. for use with ActAs) 92 ActAs bool // ActAs allows to request an ActAs token based on the passed Token. 93 Token string // Token for Renew request or Issue request ActAs identity or to be exchanged. 94 KeyType string // KeyType for requested token (if not set will be decucted from Userinfo and Certificate options) 95 KeyID string // KeyID used for signing the requests 96 } 97 98 func (c *Client) newRequest(req TokenRequest, kind string, s *Signer) (internal.RequestSecurityToken, error) { 99 if req.Lifetime == 0 { 100 req.Lifetime = 5 * time.Minute 101 } 102 103 created := time.Now().UTC() 104 rst := internal.RequestSecurityToken{ 105 TokenType: c.Namespace, 106 RequestType: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/" + kind, 107 SignatureAlgorithm: internal.SHA256, 108 Lifetime: &internal.Lifetime{ 109 Created: created.Format(internal.Time), 110 Expires: created.Add(req.Lifetime).Format(internal.Time), 111 }, 112 Renewing: &internal.Renewing{ 113 Allow: req.Renewable, 114 // /wst:RequestSecurityToken/wst:Renewing/@OK 115 // "It NOT RECOMMENDED to use this as it can leave you open to certain types of security attacks. 116 // Issuers MAY restrict the period after expiration during which time the token can be renewed. 117 // This window is governed by the issuer's policy." 118 OK: false, 119 }, 120 Delegatable: req.Delegatable, 121 KeyType: req.KeyType, 122 } 123 124 if req.KeyType == "" { 125 // Deduce KeyType based on Certificate nad Userinfo. 126 if req.Certificate == nil { 127 if req.Userinfo == nil { 128 return rst, errors.New("one of TokenRequest Certificate or Userinfo is required") 129 } 130 rst.KeyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer" 131 } else { 132 rst.KeyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey" 133 // For HOK KeyID is required. 134 if req.KeyID == "" { 135 req.KeyID = newID() 136 } 137 } 138 } 139 140 if req.KeyID != "" { 141 rst.UseKey = &internal.UseKey{Sig: req.KeyID} 142 s.keyID = rst.UseKey.Sig 143 } 144 145 return rst, nil 146 } 147 148 func (s *Signer) setLifetime(lifetime *internal.Lifetime) error { 149 var err error 150 if lifetime != nil { 151 s.Lifetime.Created, err = time.Parse(internal.Time, lifetime.Created) 152 if err == nil { 153 s.Lifetime.Expires, err = time.Parse(internal.Time, lifetime.Expires) 154 } 155 } 156 return err 157 } 158 159 // Issue is used to request a security token. 160 // The returned Signer can be used to sign SOAP requests, such as the SessionManager LoginByToken method and the RequestSecurityToken method itself. 161 // One of TokenRequest Certificate or Userinfo is required, with Certificate taking precedence. 162 // When Certificate is set, a Holder-of-Key token will be requested. Otherwise, a Bearer token is requested with the Userinfo credentials. 163 // See: http://docs.oasis-open.org/ws-sx/ws-trust/v1.4/errata01/os/ws-trust-1.4-errata01-os-complete.html#_Toc325658937 164 func (c *Client) Issue(ctx context.Context, req TokenRequest) (*Signer, error) { 165 s := &Signer{ 166 Certificate: req.Certificate, 167 keyID: req.KeyID, 168 Token: req.Token, 169 user: req.Userinfo, 170 } 171 172 rst, err := c.newRequest(req, "Issue", s) 173 if err != nil { 174 return nil, err 175 } 176 177 if req.ActAs { 178 rst.ActAs = &internal.Target{ 179 Token: req.Token, 180 } 181 } 182 183 header := soap.Header{ 184 Security: s, 185 Action: fmt.Sprintf(`"%s"`, rst.Action()), 186 } 187 188 res, err := internal.Issue(c.WithHeader(ctx, header), c, &rst) 189 if err != nil { 190 return nil, err 191 } 192 193 s.Token = res.RequestSecurityTokenResponse.RequestedSecurityToken.Assertion 194 195 return s, s.setLifetime(res.RequestSecurityTokenResponse.Lifetime) 196 } 197 198 // Renew is used to request a security token renewal. 199 func (c *Client) Renew(ctx context.Context, req TokenRequest) (*Signer, error) { 200 s := &Signer{ 201 Certificate: req.Certificate, 202 } 203 204 rst, err := c.newRequest(req, "Renew", s) 205 if err != nil { 206 return nil, err 207 } 208 209 if req.Token == "" { 210 return nil, errors.New("TokenRequest Token is required") 211 } 212 213 rst.RenewTarget = &internal.Target{Token: req.Token} 214 215 header := soap.Header{ 216 Security: s, 217 Action: rst.Action(), 218 } 219 220 res, err := internal.Renew(c.WithHeader(ctx, header), c, &rst) 221 if err != nil { 222 return nil, err 223 } 224 225 s.Token = res.RequestedSecurityToken.Assertion 226 227 return s, s.setLifetime(res.Lifetime) 228 }