diff --git a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.html b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.html
index 53e4802fd5..bac7f84c63 100644
--- a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.html
+++ b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.html
@@ -10,7 +10,7 @@
1">
- {{ auth.name }}
+ {{ auth.name }}
@@ -22,6 +22,9 @@
+
+
You will be redirected to the Single Sign-On UI for this endpoint and returned to Stratos upon completion.
+
Share this endpoint connection with other users
diff --git a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.scss b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.scss
index aa87805676..51c77b3a11 100644
--- a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.scss
+++ b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.scss
@@ -1,5 +1,6 @@
.connection-dialog {
$dialog-padding: 24px;
+ width: 300px;
&__content {
display: flex;
flex-direction: column;
diff --git a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.ts b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.ts
index 4025ae0396..90717bf2bc 100644
--- a/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.ts
+++ b/src/frontend/app/features/endpoints/connect-endpoint-dialog/connect-endpoint-dialog.component.ts
@@ -52,6 +52,12 @@ export class ConnectEndpointDialogComponent implements OnDestroy {
},
types: new Array('cf', 'metrics')
},
+ {
+ name: 'Single sign-on',
+ value: 'sso',
+ form: {},
+ types: new Array('cf')
+ },
];
private hasAttemptedConnect: boolean;
@@ -201,6 +207,7 @@ export class ConnectEndpointDialogComponent implements OnDestroy {
submit() {
this.hasAttemptedConnect = true;
const { guid, authType, authValues, systemShared } = this.endpointForm.value;
+
this.store.dispatch(new ConnectEndpoint(
this.data.guid,
this.data.type,
diff --git a/src/frontend/app/features/endpoints/endpoints-page/endpoints-page.component.ts b/src/frontend/app/features/endpoints/endpoints-page/endpoints-page.component.ts
index 779a4528b8..86d5a0c5ef 100644
--- a/src/frontend/app/features/endpoints/endpoints-page/endpoints-page.component.ts
+++ b/src/frontend/app/features/endpoints/endpoints-page/endpoints-page.component.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
import { EndpointsService } from '../../../core/endpoints.service';
import {
@@ -6,8 +6,12 @@ import {
} from '../../../shared/components/list/list-types/endpoint/endpoints-list-config.service';
import { ListConfig } from '../../../shared/components/list/list.component.types';
import { CurrentUserPermissions } from '../../../core/current-user-permissions.config';
-import { of } from 'rxjs';
-
+import { Subscription, } from 'rxjs';
+import { queryParamMap } from '../../../core/auth-guard.service';
+import { delay, first, map, filter } from 'rxjs/operators';
+import { Store } from '@ngrx/store';
+import { AppState } from '../../../store/app-state';
+import { ShowSnackBar } from '../../../store/actions/snackBar.actions';
@Component({
selector: 'app-endpoints-page',
@@ -19,7 +23,38 @@ import { of } from 'rxjs';
}]
})
-export class EndpointsPageComponent {
+export class EndpointsPageComponent implements OnDestroy, OnInit {
public canRegisterEndpoint = CurrentUserPermissions.ENDPOINT_REGISTER;
- constructor(public endpointsService: EndpointsService) { }
+ constructor(public endpointsService: EndpointsService, public store: Store ) { }
+
+ sub: Subscription;
+
+ ngOnInit(): void {
+ const params = queryParamMap();
+ if (params['cnsi_guid']) {
+ const guid = params['cnsi_guid'];
+ window.history.pushState({}, '', '/endpoints');
+ this.sub = this.endpointsService.endpoints$.pipe(
+ delay(0),
+ filter(ep => !!ep[guid]),
+ map(ep => {
+ const endpoint = ep[guid];
+ if (endpoint.connectionStatus === 'connected') {
+ this.store.dispatch(new ShowSnackBar(`Connected ${endpoint.name}`));
+ } else {
+ this.store.dispatch(new ShowSnackBar(`A problem occurred connecting endpoint ${endpoint.name}`));
+ }
+ }),
+ first(),
+ ).subscribe();
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.sub) {
+ this.sub.unsubscribe();
+ }
+ }
}
+
+
diff --git a/src/frontend/app/store/effects/endpoint.effects.ts b/src/frontend/app/store/effects/endpoint.effects.ts
index 9ec9827b8a..f5efaa7d6f 100644
--- a/src/frontend/app/store/effects/endpoint.effects.ts
+++ b/src/frontend/app/store/effects/endpoint.effects.ts
@@ -89,6 +89,16 @@ export class EndpointsEffect {
@Effect() connectEndpoint$ = this.actions$.ofType(CONNECT_ENDPOINTS).pipe(
mergeMap(action => {
const actionType = 'update';
+
+ // Special-case SSO login - redirect to the back-end
+ if (action.authType === 'sso') {
+ const loc = window.location.protocol + '//' + window.location.hostname +
+ (window.location.port ? ':' + window.location.port : '');
+ const ssoUrl = '/pp/v1/auth/login/cnsi?guid=' + action.guid + '&state=' + encodeURIComponent(loc);
+ window.location.assign(ssoUrl);
+ return [];
+ }
+
const apiAction = this.getEndpointUpdateAction(action.guid, action.type, EndpointsEffect.connectingKey);
const params: HttpParams = new HttpParams({
fromObject: {
diff --git a/src/jetstream/auth.go b/src/jetstream/auth.go
index 34d864b8d0..773d9ea13a 100644
--- a/src/jetstream/auth.go
+++ b/src/jetstream/auth.go
@@ -98,17 +98,22 @@ func (p *portalProxy) initSSOlogin(c echo.Context) error {
return err
}
- redirectURL := fmt.Sprintf("%s/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s", p.Config.ConsoleConfig.UAAEndpoint, p.Config.ConsoleConfig.ConsoleClient, url.QueryEscape(getSSORedirectURI(state, state)))
+ redirectURL := fmt.Sprintf("%s/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s", p.Config.ConsoleConfig.UAAEndpoint, p.Config.ConsoleConfig.ConsoleClient, url.QueryEscape(getSSORedirectURI(state, state, "")))
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
return nil
}
-func getSSORedirectURI(base string, state string) string {
+func getSSORedirectURI(base string, state string, endpointGUID string) string {
baseURL, _ := url.Parse(base)
baseURL.Path = ""
baseURL.RawQuery = ""
baseURLString := strings.TrimRight(baseURL.String(), "?")
- return fmt.Sprintf("%s/pp/v1/auth/sso_login_callback?state=%s", baseURLString, url.QueryEscape(state))
+
+ returnURL := fmt.Sprintf("%s/pp/v1/auth/sso_login_callback?state=%s", baseURLString, url.QueryEscape(state))
+ if len(endpointGUID) > 0 {
+ returnURL = fmt.Sprintf("%s&guid=%s", returnURL, endpointGUID)
+ }
+ return returnURL
}
// Logout of the UAA
@@ -125,7 +130,7 @@ func (p *portalProxy) ssoLogoutOfUAA(c echo.Context) error {
// Redirect to the UAA to logout of the UAA session as well (if configured to do so), otherwise redirect back to the UI login page
var redirectURL string
if p.hasSSOOption("logout") {
- redirectURL = fmt.Sprintf("%s/logout.do?client_id=%s&redirect=%s", p.Config.ConsoleConfig.UAAEndpoint, p.Config.ConsoleConfig.ConsoleClient, url.QueryEscape(getSSORedirectURI(state, "logout")))
+ redirectURL = fmt.Sprintf("%s/logout.do?client_id=%s&redirect=%s", p.Config.ConsoleConfig.UAAEndpoint, p.Config.ConsoleConfig.ConsoleClient, url.QueryEscape(getSSORedirectURI(state, "logout", "")))
} else {
redirectURL = "/login?SSO_Message=You+have+been+logged+out"
}
@@ -153,6 +158,13 @@ func (p *portalProxy) ssoLoginToUAA(c echo.Context) error {
return err
}
+ // We use the same callback URL for both UAA and endpoint login
+ // Check if it is an endpoint login and dens to the right handler
+ endpointGUID := c.QueryParam("guid")
+ if len(endpointGUID) > 0 {
+ return p.ssoLoginToCNSI(c)
+ }
+
if state == "logout" {
return c.Redirect(http.StatusTemporaryRedirect, "/login?SSO_Message=You+have+been+logged+out")
}
@@ -242,6 +254,65 @@ func (p *portalProxy) doLoginToUAA(c echo.Context) (*interfaces.LoginRes, error)
return resp, nil
}
+// Start SSO flow for an Endpoint
+func (p *portalProxy) ssoLoginToCNSI(c echo.Context) error {
+ log.Debug("loginToCNSI")
+ endpointGUID := c.QueryParam("guid")
+ if len(endpointGUID) == 0 {
+ return interfaces.NewHTTPShadowError(
+ http.StatusBadRequest,
+ "Missing target endpoint",
+ "Need Endpoint GUID passed as form param")
+ }
+
+ _, err := p.GetSessionStringValue(c, "user_id")
+ if err != nil {
+ return echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value")
+ }
+
+ state := c.QueryParam("state")
+ if len(state) == 0 {
+ err := interfaces.NewHTTPShadowError(
+ http.StatusUnauthorized,
+ "SSO Login: State parameter missing",
+ "SSO Login: State parameter missing")
+ return err
+ }
+
+ cnsiRecord, err := p.GetCNSIRecord(endpointGUID)
+ if err != nil {
+ return interfaces.NewHTTPShadowError(
+ http.StatusBadRequest,
+ "Requested endpoint not registered",
+ "No Endpoint registered with GUID %s: %s", endpointGUID, err)
+ }
+
+ // Check if this is first time in the flow, or via the callback
+ code := c.QueryParam("code")
+
+ if len(code) == 0 {
+ // First time around
+ // Use the standard SSO Login Callback endpoint, so this can be whitelisted for Stratos and Endpoint login
+ returnURL := getSSORedirectURI(state, state, endpointGUID)
+ redirectURL := fmt.Sprintf("%s/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s",
+ cnsiRecord.AuthorizationEndpoint, cnsiRecord.ClientId, url.QueryEscape(returnURL))
+ c.Redirect(http.StatusTemporaryRedirect, redirectURL)
+ return nil
+ }
+
+ // Callback
+ _, err = p.DoLoginToCNSI(c, endpointGUID, false)
+ status := "ok"
+ if err != nil {
+ status = "fail"
+ }
+
+ // Take the user back to Stratos on the endpoints page
+ redirect := fmt.Sprintf("/endpoints?cnsi_guid=%s&status=%s", endpointGUID, status)
+ c.Redirect(http.StatusTemporaryRedirect, redirect)
+ return nil
+}
+
// Connect to the given Endpoint
// Note, an admin user can connect an endpoint as a system endpoint to share it with others
func (p *portalProxy) loginToCNSI(c echo.Context) error {
@@ -525,7 +596,9 @@ func (p *portalProxy) login(c echo.Context, skipSSLValidation bool, client strin
if c.Request().Method() == http.MethodGet {
code := c.QueryParam("code")
state := c.QueryParam("state")
- uaaRes, err = p.getUAATokenWithAuthorizationCode(skipSSLValidation, code, client, clientSecret, endpoint, state)
+ // If this is login for a CNSI, then the redirect URL is slightly different
+ cnsiGUID := c.QueryParam("guid")
+ uaaRes, err = p.getUAATokenWithAuthorizationCode(skipSSLValidation, code, client, clientSecret, endpoint, state, cnsiGUID)
} else {
username := c.FormValue("username")
password := c.FormValue("password")
@@ -584,7 +657,7 @@ func (p *portalProxy) logout(c echo.Context) error {
return c.JSON(http.StatusOK, resp)
}
-func (p *portalProxy) getUAATokenWithAuthorizationCode(skipSSLValidation bool, code, client, clientSecret, authEndpoint string, state string) (*UAAResponse, error) {
+func (p *portalProxy) getUAATokenWithAuthorizationCode(skipSSLValidation bool, code, client, clientSecret, authEndpoint string, state string, cnsiGUID string) (*UAAResponse, error) {
log.Debug("getUAATokenWithCreds")
body := url.Values{}
@@ -592,7 +665,7 @@ func (p *portalProxy) getUAATokenWithAuthorizationCode(skipSSLValidation bool, c
body.Set("code", code)
body.Set("client_id", client)
body.Set("client_secret", clientSecret)
- body.Set("redirect_uri", getSSORedirectURI(state, state))
+ body.Set("redirect_uri", getSSORedirectURI(state, state, cnsiGUID))
return p.getUAAToken(body, skipSSLValidation, client, clientSecret, authEndpoint)
}
diff --git a/src/jetstream/main.go b/src/jetstream/main.go
index 615d985fa1..7667b998de 100644
--- a/src/jetstream/main.go
+++ b/src/jetstream/main.go
@@ -677,10 +677,12 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, addSetupMiddleware *setupMidd
// Only add SSO routes if SSO Login is enabled
if p.Config.SSOLogin {
pp.GET("/v1/auth/sso_login", p.initSSOlogin)
- pp.GET("/v1/auth/sso_login_callback", p.ssoLoginToUAA)
pp.GET("/v1/auth/sso_logout", p.ssoLogoutOfUAA)
}
+ // Callback is use dby both login to Stratos and login to an Endpoint
+ pp.GET("/v1/auth/sso_login_callback", p.ssoLoginToUAA)
+
// Version info
pp.GET("/v1/version", p.getVersions)
@@ -698,10 +700,13 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, addSetupMiddleware *setupMidd
e.Use(middlewarePlugin.SessionEchoMiddleware)
}
- // Connect to CF cluster
+ // Connect to endpoint
sessionGroup.POST("/auth/login/cnsi", p.loginToCNSI)
- // Disconnect CF cluster
+ // Connect to Enpoint (SSO)
+ sessionGroup.GET("/auth/login/cnsi", p.ssoLoginToCNSI)
+
+ // Disconnect endpoint
sessionGroup.POST("/auth/logout/cnsi", p.logoutOfCNSI)
// Verify Session
diff --git a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts
index bec29b1ff8..8bea0da95b 100644
--- a/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts
+++ b/src/test-e2e/endpoints/endpoints-connect-e2e.spec.ts
@@ -54,7 +54,7 @@ describe('Endpoints', () => {
expect(ctrls['authtype']).toBeDefined();
expect(ctrls['username']).toBeDefined();
expect(ctrls['password']).toBeDefined();
- expect(ctrls['authtype'].value).toEqual('creds');
+ expect(ctrls['authtype'].text).toEqual('Username and Password');
expect(ctrls['username'].text).toEqual('');
expect(ctrls['password'].text).toEqual('');
});