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