Skip to content

Commit

Permalink
Teleport Connect allow SSO hostname (#48035)
Browse files Browse the repository at this point in the history
* Add SSO provider URLs to ping response.

* Add SSOHostname to profile.

* Add SSOHostname to teleterm cluster.

* Add SSO hostname to connect's proxy host allow list.

* Fix lint.

* Address comments.

* Document use of SSOHostname in the profile.

* Use sso host, not hostname.

* Ping with connector.

* Resolve comments.
  • Loading branch information
Joerger authored Nov 5, 2024
1 parent ecd5500 commit 0927b6f
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 133 deletions.
6 changes: 6 additions & 0 deletions api/client/webclient/webclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@ type SAMLSettings struct {
Display string `json:"display"`
// SingleLogoutEnabled is whether SAML SLO (single logout) is enabled for this auth connector.
SingleLogoutEnabled bool `json:"singleLogoutEnabled,omitempty"`
// SSO is the URL of the identity provider's SSO service.
SSO string
}

// OIDCSettings contains the Name and Display string for OIDC.
Expand All @@ -503,6 +505,8 @@ type OIDCSettings struct {
Name string `json:"name"`
// Display is the display name for the connector.
Display string `json:"display"`
// Issuer URL is the endpoint of the provider
IssuerURL string
}

// GithubSettings contains the Name and Display string for Github connector.
Expand All @@ -511,6 +515,8 @@ type GithubSettings struct {
Name string `json:"name"`
// Display is the connector display name
Display string `json:"display"`
// EndpointURL is the endpoint URL.
EndpointURL string
}

// DeviceTrustSettings holds cluster-wide device trust settings that are liable
Expand Down
5 changes: 5 additions & 0 deletions api/profile/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ type Profile struct {

// SSHDialTimeout is the timeout value that should be used for SSH connections.
SSHDialTimeout time.Duration `yaml:"ssh_dial_timeout,omitempty"`

// SSOHost is the host of the SSO provider used to log in. Clients can check this value, along
// with WebProxyAddr, to determine if a webpage is safe to open. Currently used by Teleport
// Connect in the proxy host allow list.
SSOHost string `yaml:"sso_host,omitempty"`
}

// Copy returns a shallow copy of p, or nil if p is nil.
Expand Down
229 changes: 120 additions & 109 deletions gen/proto/go/teleport/lib/teleterm/v1/cluster.pb.go

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion gen/proto/ts/teleport/lib/teleterm/v1/cluster_pb.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 29 additions & 2 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,9 @@ type Config struct {
// HasTouchIDCredentialsFunc allows tests to override touchid.HasCredentials.
// If nil touchid.HasCredentials is used.
HasTouchIDCredentialsFunc func(rpID, user string) bool

// SSOHost is the host of the SSO provider used to log in.
SSOHost string
}

// CachePolicy defines cache policy for local clients
Expand Down Expand Up @@ -885,6 +888,8 @@ func (c *Config) LoadProfile(ps ProfileStore, proxyAddr string) error {
c.PIVSlot = profile.PIVSlot
c.SAMLSingleLogoutEnabled = profile.SAMLSingleLogoutEnabled
c.SSHDialTimeout = profile.SSHDialTimeout
c.SSOHost = profile.SSOHost

c.AuthenticatorAttachment, err = parseMFAMode(profile.MFAMode)
if err != nil {
return trace.BadParameter("unable to parse mfa mode in user profile: %v.", err)
Expand Down Expand Up @@ -935,6 +940,7 @@ func (c *Config) Profile() *profile.Profile {
PIVSlot: c.PIVSlot,
SAMLSingleLogoutEnabled: c.SAMLSingleLogoutEnabled,
SSHDialTimeout: c.SSHDialTimeout,
SSOHost: c.SSOHost,
}
}

Expand Down Expand Up @@ -4299,7 +4305,9 @@ You may use the --skip-version-check flag to bypass this check.
// cached, there is no need to do this test again.
tc.TLSRoutingConnUpgradeRequired = client.IsALPNConnUpgradeRequired(ctx, tc.WebProxyAddr, tc.InsecureSkipVerify)

tc.applyAuthSettings(pr.Auth)
if err := tc.applyAuthSettings(pr.Auth); err != nil {
return nil, trace.Wrap(err)
}

tc.lastPing = pr

Expand Down Expand Up @@ -4578,7 +4586,7 @@ func (tc *TeleportClient) applyProxySettings(proxySettings webclient.ProxySettin

// applyAuthSettings updates configuration changes based on the advertised
// authentication settings, overriding existing fields in tc.
func (tc *TeleportClient) applyAuthSettings(authSettings webclient.AuthenticationSettings) {
func (tc *TeleportClient) applyAuthSettings(authSettings webclient.AuthenticationSettings) error {
tc.LoadAllCAs = authSettings.LoadAllCAs

// If PIVSlot is not already set, default to the server setting.
Expand All @@ -4590,6 +4598,25 @@ func (tc *TeleportClient) applyAuthSettings(authSettings webclient.Authenticatio
if authSettings.PrivateKeyPolicy != "" && !authSettings.PrivateKeyPolicy.IsSatisfiedBy(tc.PrivateKeyPolicy) {
tc.PrivateKeyPolicy = authSettings.PrivateKeyPolicy
}

var ssoURL *url.URL
var err error
switch {
case authSettings.SAML != nil:
ssoURL, err = url.Parse(authSettings.SAML.SSO)
case authSettings.OIDC != nil:
ssoURL, err = url.Parse(authSettings.OIDC.IssuerURL)
case authSettings.Github != nil:
ssoURL, err = url.Parse(authSettings.Github.EndpointURL)
}
if err != nil {
return trace.Wrap(err)
}
if ssoURL != nil {
tc.SSOHost = ssoURL.Host
}

return nil
}

// AddTrustedCA adds a new CA as trusted CA for this client, used in tests
Expand Down
2 changes: 2 additions & 0 deletions lib/client/client_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ func (s *Store) ReadProfileStatus(profileName string) (*ProfileStatus, error) {
// Set ValidUntil to now to show that the keys are not available.
ValidUntil: time.Now(),
SAMLSingleLogoutEnabled: profile.SAMLSingleLogoutEnabled,
SSOHost: profile.SSOHost,
}, nil
}
return nil, trace.Wrap(err)
Expand All @@ -217,6 +218,7 @@ func (s *Store) ReadProfileStatus(profileName string) (*ProfileStatus, error) {
SiteName: profile.SiteName,
KubeProxyAddr: profile.KubeProxyAddr,
SAMLSingleLogoutEnabled: profile.SAMLSingleLogoutEnabled,
SSOHost: profile.SSOHost,
IsVirtual: !onDisk,
})
}
Expand Down
5 changes: 5 additions & 0 deletions lib/client/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ type ProfileStatus struct {
// SAMLSingleLogoutEnabled is whether SAML SLO (single logout) is enabled, this can only be true if this is a SAML SSO session
// using an auth connector with a SAML SLO URL configured.
SAMLSingleLogoutEnabled bool

// SSOHost is the host of the SSO provider used to log in.
SSOHost string
}

// profileOptions contains fields needed to initialize a profile beyond those
Expand All @@ -255,6 +258,7 @@ type profileOptions struct {
KubeProxyAddr string
IsVirtual bool
SAMLSingleLogoutEnabled bool
SSOHost string
}

// profileStatueFromKeyRing returns a ProfileStatus for the given key ring and options.
Expand Down Expand Up @@ -375,6 +379,7 @@ func profileStatusFromKeyRing(keyRing *KeyRing, opts profileOptions) (*ProfileSt
IsVirtual: opts.IsVirtual,
AllowedResourceIDs: allowedResourceIDs,
SAMLSingleLogoutEnabled: opts.SAMLSingleLogoutEnabled,
SSOHost: opts.SSOHost,
}, nil
}

Expand Down
1 change: 1 addition & 0 deletions lib/teleterm/apiserver/handler/handler_clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func newAPIRootCluster(cluster *clusters.Cluster) *api.Cluster {
Roles: loggedInUser.Roles,
ActiveRequests: loggedInUser.ActiveRequests,
},
SsoHost: cluster.SSOHost,
}

if cluster.GetProfileStatusError() != nil {
Expand Down
2 changes: 2 additions & 0 deletions lib/teleterm/clusters/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ type Cluster struct {
clusterClient *client.TeleportClient
// clock is a clock for time-related operations
clock clockwork.Clock
// SSOHost is the host of the SSO provider used to log in.
SSOHost string
}

type ClusterWithDetails struct {
Expand Down
10 changes: 6 additions & 4 deletions lib/teleterm/clusters/cluster_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,13 @@ func (c *Cluster) LocalLogin(ctx context.Context, user, password, otpToken strin

// SSOLogin logs in a user to the Teleport cluster using supported SSO provider
func (c *Cluster) SSOLogin(ctx context.Context, providerType, providerName string) error {
// Get the ping response for the given auth connector.
c.clusterClient.AuthConnector = providerName

if _, err := c.updateClientFromPingResponse(ctx); err != nil {
return trace.Wrap(err)
}

c.clusterClient.AuthConnector = providerName

if err := c.login(ctx, c.ssoLogin(providerType, providerName)); err != nil {
return trace.Wrap(err)
}
Expand All @@ -115,12 +116,13 @@ func (c *Cluster) SSOLogin(ctx context.Context, providerType, providerName strin

// PasswordlessLogin processes passwordless logins for this cluster.
func (c *Cluster) PasswordlessLogin(ctx context.Context, stream api.TerminalService_LoginPasswordlessServer) error {
// Get the ping response for the given auth connector.
c.clusterClient.AuthConnector = constants.PasswordlessConnector

if _, err := c.updateClientFromPingResponse(ctx); err != nil {
return trace.Wrap(err)
}

c.clusterClient.AuthConnector = constants.PasswordlessConnector

if err := c.login(ctx, c.passwordlessLogin(stream)); err != nil {
return trace.Wrap(err)
}
Expand Down
1 change: 1 addition & 0 deletions lib/teleterm/clusters/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ func (s *Storage) fromProfile(profileName, leafClusterName string) (*Cluster, *c
}
if status != nil {
cluster.status = *status
cluster.SSOHost = status.SSOHost
}

return cluster, clusterClient, trace.Wrap(err)
Expand Down
14 changes: 10 additions & 4 deletions lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -1306,8 +1306,9 @@ func oidcSettings(connector types.OIDCConnector, cap types.AuthPreference) webcl
return webclient.AuthenticationSettings{
Type: constants.OIDC,
OIDC: &webclient.OIDCSettings{
Name: connector.GetName(),
Display: connector.GetDisplay(),
Name: connector.GetName(),
Display: connector.GetDisplay(),
IssuerURL: connector.GetIssuerURL(),
},
// Local fallback / MFA.
SecondFactor: types.LegacySecondFactorFromSecondFactors(cap.GetSecondFactors()),
Expand All @@ -1326,6 +1327,10 @@ func samlSettings(connector types.SAMLConnector, cap types.AuthPreference) webcl
Name: connector.GetName(),
Display: connector.GetDisplay(),
SingleLogoutEnabled: connector.GetSingleLogoutURL() != "",
// Note that we get the connector's primary SSO field, not the MFA SSO field.
// These two values are often unique, but should have the same host prefix
// (e.g. https://dev-813354.oktapreview.com) in reasonable, functional setups.
SSO: connector.GetSSO(),
},
// Local fallback / MFA.
SecondFactor: types.LegacySecondFactorFromSecondFactors(cap.GetSecondFactors()),
Expand All @@ -1341,8 +1346,9 @@ func githubSettings(connector types.GithubConnector, cap types.AuthPreference) w
return webclient.AuthenticationSettings{
Type: constants.Github,
Github: &webclient.GithubSettings{
Name: connector.GetName(),
Display: connector.GetDisplay(),
Name: connector.GetName(),
Display: connector.GetDisplay(),
EndpointURL: connector.GetEndpointURL(),
},
// Local fallback / MFA.
SecondFactor: types.LegacySecondFactorFromSecondFactors(cap.GetSecondFactors()),
Expand Down
2 changes: 2 additions & 0 deletions proto/teleport/lib/teleterm/v1/cluster.proto
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ message Cluster {
// profile_status_error is set if there was an error when reading the profile.
// This allows the app to be usable, when one or more profiles cannot be read.
string profile_status_error = 12;
// sso_host is the host of the SSO provider used to log in.
string sso_host = 13;
}

// ShowResources tells if the cluster can show requestable resources on the resources page.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,22 +77,32 @@ export function manageRootClusterProxyHostAllowList({

allowList.clear();
for (const rootCluster of rootClusters) {
if (!rootCluster.proxyHost) {
continue;
if (rootCluster.proxyHost) {
let browserProxyHost: string;
try {
browserProxyHost = proxyHostToBrowserProxyHost(rootCluster.proxyHost);
allowList.add(browserProxyHost);
} catch (error) {
logger.error(
'Ran into an error when converting proxy host to browser proxy host',
error
);
}
}

let browserProxyHost: string;
try {
browserProxyHost = proxyHostToBrowserProxyHost(rootCluster.proxyHost);
} catch (error) {
logger.error(
'Ran into an error when converting proxy host to browser proxy host',
error
);
continue;
// Allow the SSO host for SSO login/mfa redirects.
if (rootCluster.ssoHost) {
let browserSsoHost: string;
try {
browserSsoHost = proxyHostToBrowserProxyHost(rootCluster.ssoHost);
allowList.add(browserSsoHost);
} catch (error) {
logger.error(
'Ran into an error when converting sso host to browser sso host',
error
);
}
}

allowList.add(browserProxyHost);
}
};

Expand Down
2 changes: 2 additions & 0 deletions web/packages/teleterm/src/services/tshd/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const makeRootCluster = (
proxyVersion: '11.1.0',
showResources: ShowResources.REQUESTABLE,
profileStatusError: '',
ssoHost: 'example.auth0.com',
...props,
});

Expand All @@ -107,6 +108,7 @@ export const makeLeafCluster = (
proxyVersion: '',
profileStatusError: '',
showResources: ShowResources.UNSPECIFIED,
ssoHost: 'example.auth0.com',
...props,
});

Expand Down

0 comments on commit 0927b6f

Please sign in to comment.