| /* |
| Copyright (c) 2018 VMware, Inc. All Rights Reserved. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package sts |
| |
| import ( |
| "context" |
| "crypto/tls" |
| "errors" |
| "net/url" |
| "time" |
| |
| "github.com/vmware/govmomi/lookup" |
| "github.com/vmware/govmomi/lookup/types" |
| "github.com/vmware/govmomi/sts/internal" |
| "github.com/vmware/govmomi/vim25" |
| "github.com/vmware/govmomi/vim25/soap" |
| ) |
| |
| const ( |
| Namespace = "oasis:names:tc:SAML:2.0:assertion" |
| Path = "/sts/STSService" |
| ) |
| |
| // Client is a soap.Client targeting the STS (Secure Token Service) API endpoint. |
| type Client struct { |
| *soap.Client |
| } |
| |
| // NewClient returns a client targeting the STS API endpoint. |
| // The Client.URL will be set to that of the Lookup Service's endpoint registration, |
| // as the SSO endpoint can be external to vCenter. If the Lookup Service is not available, |
| // URL defaults to Path on the vim25.Client.URL.Host. |
| func NewClient(ctx context.Context, c *vim25.Client) (*Client, error) { |
| filter := &types.LookupServiceRegistrationFilter{ |
| ServiceType: &types.LookupServiceRegistrationServiceType{ |
| Product: "com.vmware.cis", |
| Type: "sso:sts", |
| }, |
| EndpointType: &types.LookupServiceRegistrationEndpointType{ |
| Protocol: "wsTrust", |
| Type: "com.vmware.cis.cs.identity.sso", |
| }, |
| } |
| |
| url := lookup.EndpointURL(ctx, c, Path, filter) |
| sc := c.Client.NewServiceClient(url, Namespace) |
| |
| return &Client{sc}, nil |
| } |
| |
| // TokenRequest parameters for issuing a SAML token. |
| // At least one of Userinfo or Certificate must be specified. |
| type TokenRequest struct { |
| Userinfo *url.Userinfo // Userinfo when set issues a Bearer token |
| Certificate *tls.Certificate // Certificate when set issues a HoK token |
| Lifetime time.Duration // Lifetime is the token's lifetime, defaults to 10m |
| Renewable bool // Renewable allows the issued token to be renewed |
| Delegatable bool // Delegatable allows the issued token to be delegated (e.g. for use with ActAs) |
| ActAs bool // ActAs allows to request an ActAs token based on the passed Token. |
| Token string // Token for Renew request or Issue request ActAs identity or to be exchanged. |
| KeyType string // KeyType for requested token (if not set will be decucted from Userinfo and Certificate options) |
| KeyID string // KeyID used for signing the requests |
| } |
| |
| func (c *Client) newRequest(req TokenRequest, kind string, s *Signer) (internal.RequestSecurityToken, error) { |
| if req.Lifetime == 0 { |
| req.Lifetime = 5 * time.Minute |
| } |
| |
| created := time.Now().UTC() |
| rst := internal.RequestSecurityToken{ |
| TokenType: c.Namespace, |
| RequestType: "http://docs.oasis-open.org/ws-sx/ws-trust/200512/" + kind, |
| SignatureAlgorithm: internal.SHA256, |
| Lifetime: &internal.Lifetime{ |
| Created: created.Format(internal.Time), |
| Expires: created.Add(req.Lifetime).Format(internal.Time), |
| }, |
| Renewing: &internal.Renewing{ |
| Allow: req.Renewable, |
| // /wst:RequestSecurityToken/wst:Renewing/@OK |
| // "It NOT RECOMMENDED to use this as it can leave you open to certain types of security attacks. |
| // Issuers MAY restrict the period after expiration during which time the token can be renewed. |
| // This window is governed by the issuer's policy." |
| OK: false, |
| }, |
| Delegatable: req.Delegatable, |
| KeyType: req.KeyType, |
| } |
| |
| if req.KeyType == "" { |
| // Deduce KeyType based on Certificate nad Userinfo. |
| if req.Certificate == nil { |
| if req.Userinfo == nil { |
| return rst, errors.New("one of TokenRequest Certificate or Userinfo is required") |
| } |
| rst.KeyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/Bearer" |
| } else { |
| rst.KeyType = "http://docs.oasis-open.org/ws-sx/ws-trust/200512/PublicKey" |
| // For HOK KeyID is required. |
| if req.KeyID == "" { |
| req.KeyID = newID() |
| } |
| } |
| } |
| |
| if req.KeyID != "" { |
| rst.UseKey = &internal.UseKey{Sig: req.KeyID} |
| s.keyID = rst.UseKey.Sig |
| } |
| |
| return rst, nil |
| } |
| |
| func (s *Signer) setLifetime(lifetime *internal.Lifetime) error { |
| var err error |
| if lifetime != nil { |
| s.Lifetime.Created, err = time.Parse(internal.Time, lifetime.Created) |
| if err == nil { |
| s.Lifetime.Expires, err = time.Parse(internal.Time, lifetime.Expires) |
| } |
| } |
| return err |
| } |
| |
| // Issue is used to request a security token. |
| // The returned Signer can be used to sign SOAP requests, such as the SessionManager LoginByToken method and the RequestSecurityToken method itself. |
| // One of TokenRequest Certificate or Userinfo is required, with Certificate taking precedence. |
| // When Certificate is set, a Holder-of-Key token will be requested. Otherwise, a Bearer token is requested with the Userinfo credentials. |
| // See: http://docs.oasis-open.org/ws-sx/ws-trust/v1.4/errata01/os/ws-trust-1.4-errata01-os-complete.html#_Toc325658937 |
| func (c *Client) Issue(ctx context.Context, req TokenRequest) (*Signer, error) { |
| s := &Signer{ |
| Certificate: req.Certificate, |
| keyID: req.KeyID, |
| Token: req.Token, |
| user: req.Userinfo, |
| } |
| |
| rst, err := c.newRequest(req, "Issue", s) |
| if err != nil { |
| return nil, err |
| } |
| |
| if req.ActAs { |
| rst.ActAs = &internal.Target{ |
| Token: req.Token, |
| } |
| } |
| |
| header := soap.Header{ |
| Security: s, |
| Action: rst.Action(), |
| } |
| |
| res, err := internal.Issue(c.WithHeader(ctx, header), c, &rst) |
| if err != nil { |
| return nil, err |
| } |
| |
| s.Token = res.RequestSecurityTokenResponse.RequestedSecurityToken.Assertion |
| |
| return s, s.setLifetime(res.RequestSecurityTokenResponse.Lifetime) |
| } |
| |
| // Renew is used to request a security token renewal. |
| func (c *Client) Renew(ctx context.Context, req TokenRequest) (*Signer, error) { |
| s := &Signer{ |
| Certificate: req.Certificate, |
| } |
| |
| rst, err := c.newRequest(req, "Renew", s) |
| if err != nil { |
| return nil, err |
| } |
| |
| if req.Token == "" { |
| return nil, errors.New("TokenRequest Token is required") |
| } |
| |
| rst.RenewTarget = &internal.Target{Token: req.Token} |
| |
| header := soap.Header{ |
| Security: s, |
| Action: rst.Action(), |
| } |
| |
| res, err := internal.Renew(c.WithHeader(ctx, header), c, &rst) |
| if err != nil { |
| return nil, err |
| } |
| |
| s.Token = res.RequestedSecurityToken.Assertion |
| |
| return s, s.setLifetime(res.Lifetime) |
| } |