Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSO Login Support #2491

Merged
merged 11 commits into from
Jun 21, 2018
83 changes: 76 additions & 7 deletions src/backend/app-core/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ func (p *portalProxy) GetUsername(userid string) (string, error) {
return u.UserName, nil
}

func (p *portalProxy) initSSOlogin(c echo.Context) error {
state := c.QueryParam("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)))
c.Redirect(http.StatusTemporaryRedirect, redirectUrl)

return nil
}

func getSSORedirectUri(state string) string {
return fmt.Sprintf("%s/pp/v1/auth/sso_login_callback?state=%s", state, url.QueryEscape(state))
}

func (p *portalProxy) loginToUAA(c echo.Context) error {
log.Debug("loginToUAA")

Expand Down Expand Up @@ -130,7 +142,7 @@ func (p *portalProxy) loginToUAA(c echo.Context) error {
uaaAdmin := strings.Contains(uaaRes.Scope, p.Config.ConsoleConfig.ConsoleAdminScope)

resp := &interfaces.LoginRes{
Account: c.FormValue("username"),
Account: u.UserName,
TokenExpiry: u.TokenExpiry,
APIEndpoint: nil,
Admin: uaaAdmin,
Expand All @@ -140,6 +152,10 @@ func (p *portalProxy) loginToUAA(c echo.Context) error {
return err
}

if c.Request().Method() == http.MethodGet {
state := c.QueryParam("state")
return c.Redirect(http.StatusTemporaryRedirect, state)
}
// Add XSRF Token
p.ensureXSRFToken(c)

Expand Down Expand Up @@ -267,6 +283,41 @@ func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemShare
"Endpoint connection not supported")
}

func (p *portalProxy) DoLoginToCNSIwithConsoleUAAtoken(c echo.Context, theCNSIrecord interfaces.CNSIRecord) error {
userID, err := p.GetSessionStringValue(c, "user_id")
if err != nil {
return errors.New("could not find correct session value")
}
uaaToken, err := p.GetUAATokenRecord(userID)
if err == nil { // Found the user's UAA token
u, err := p.GetUserTokenInfo(uaaToken.AuthToken)
if err != nil {
return errors.New("could not parse current user UAA token")
}
cfEndpointSpec, _ := p.GetEndpointTypeSpec("cf")
cnsiInfo, _, err := cfEndpointSpec.Info(theCNSIrecord.APIEndpoint.String(), true)
if err != nil {
log.Fatal("Could not get the info for Cloud Foundry", err)
return err
}

uaaUrl, err := url.Parse(cnsiInfo.AuthorizationEndpoint)
if err != nil {
return fmt.Errorf("invalid authorization endpoint URL %s %s", cnsiInfo.AuthorizationEndpoint, err)
}

if uaaUrl.String() == p.GetConfig().ConsoleConfig.UAAEndpoint.String() { // CNSI UAA server matches Console UAA server
err = p.setCNSITokenRecord(theCNSIrecord.GUID, u.UserGUID, uaaToken)
return err
} else {
return fmt.Errorf("the auto-registered endpoint UAA server does not match console UAA server")
}
} else {
log.Warn("Could not find current user UAA token")
return err
}
}

func santizeInfoForSystemSharedTokenUser(cnsiUser *interfaces.ConnectedUser, isSysystemShared bool) {
if isSysystemShared {
cnsiUser.GUID = tokens.SystemSharedUserGuid
Expand Down Expand Up @@ -408,14 +459,19 @@ func (p *portalProxy) RefreshUAALogin(username, password string, store bool) err

func (p *portalProxy) login(c echo.Context, skipSSLValidation bool, client string, clientSecret string, endpoint string) (uaaRes *UAAResponse, u *interfaces.JWTUserTokenInfo, err error) {
log.Debug("login")
username := c.FormValue("username")
password := c.FormValue("password")
if c.Request().Method() == http.MethodGet {
code := c.QueryParam("code")
state := c.QueryParam("state")
uaaRes, err = p.getUAATokenWithAuthorizationCode(skipSSLValidation, code, client, clientSecret, endpoint, state)
} else {
username := c.FormValue("username")
password := c.FormValue("password")

if len(username) == 0 || len(password) == 0 {
return uaaRes, u, errors.New("Needs username and password")
if len(username) == 0 || len(password) == 0 {
return uaaRes, u, errors.New("Needs username and password")
}
uaaRes, err = p.getUAATokenWithCreds(skipSSLValidation, username, password, client, clientSecret, endpoint)
}

uaaRes, err = p.getUAATokenWithCreds(skipSSLValidation, username, password, client, clientSecret, endpoint)
if err != nil {
return uaaRes, u, err
}
Expand Down Expand Up @@ -460,6 +516,19 @@ func (p *portalProxy) logout(c echo.Context) error {
return err
}

func (p *portalProxy) getUAATokenWithAuthorizationCode(skipSSLValidation bool, code, client, clientSecret, authEndpoint string, state string) (*UAAResponse, error) {
log.Debug("getUAATokenWithCreds")

body := url.Values{}
body.Set("grant_type", "authorization_code")
body.Set("code", code)
body.Set("client_id", client)
body.Set("client_secret", clientSecret)
body.Set("redirect_uri", getSSORedirectUri(state))

return p.getUAAToken(body, skipSSLValidation, client, clientSecret, authEndpoint)
}

func (p *portalProxy) getUAATokenWithCreds(skipSSLValidation bool, username, password, client, clientSecret, authEndpoint string) (*UAAResponse, error) {
log.Debug("getUAATokenWithCreds")

Expand Down
2 changes: 1 addition & 1 deletion src/backend/app-core/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func LoadConfigFile(path string) error {
line = strings.TrimSpace(line)
if strings.Index(line, "#") != 0 {
// Not a comment
keyValue := strings.Split(line, "=")
keyValue := strings.SplitN(line, "=",2)
if len(keyValue) == 2 {
loadedConfig[keyValue[0]] = keyValue[1]
}
Expand Down
2 changes: 1 addition & 1 deletion src/backend/app-core/glide.lock

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

3 changes: 3 additions & 0 deletions src/backend/app-core/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,9 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, addSetupMiddleware *setupMidd
pp.POST("/v1/auth/login/uaa", p.loginToUAA)
pp.POST("/v1/auth/logout", p.logout)

pp.GET("/v1/auth/sso_login", p.initSSOlogin)
pp.GET("/v1/auth/sso_login_callback", p.loginToUAA)

// Version info
pp.GET("/v1/version", p.getVersions)

Expand Down
11 changes: 8 additions & 3 deletions src/backend/app-core/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ const cfSessionCookieName = "JSESSIONID"

const StratosDomainHeader = "x-stratos-domain"

func handleSessionError(err error, doNotLog bool) error {
func handleSessionError(config interfaces.PortalConfig, c echo.Context, err error, doNotLog bool) error {
// Add header so front-end knows SSO login is enabled
if config.SSOLogin {
c.Response().Header().Set("x-stratos-sso-login", "true")
}

if strings.Contains(err.Error(), "dial tcp") {
return interfaces.NewHTTPShadowError(
http.StatusServiceUnavailable,
Expand Down Expand Up @@ -60,7 +65,7 @@ func (p *portalProxy) sessionMiddleware(h echo.HandlerFunc) echo.HandlerFunc {
// Tell the frontend what the Cookie Domain is so it can check if sessions will work
c.Response().Header().Set(StratosDomainHeader, p.Config.CookieDomain)
}
return handleSessionError(err, isVerify)
return handleSessionError(p.Config, c, err, isVerify)
}
}

Expand Down Expand Up @@ -132,7 +137,7 @@ func (p *portalProxy) adminMiddleware(h echo.HandlerFunc) echo.HandlerFunc {
}
}

return handleSessionError(err, false)
return handleSessionError(p.Config, c, err, false)
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/backend/app-core/repository/interfaces/portal_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type PortalProxy interface {

RefreshOAuthToken(skipSSLValidation bool, cnsiGUID, userGUID, client, clientSecret, tokenEndpoint string) (t TokenRecord, err error)
DoLoginToCNSI(c echo.Context, cnsiGUID string, systemSharedToken bool) (*LoginRes, error)
DoLoginToCNSIwithConsoleUAAtoken(c echo.Context, theCNSIrecord CNSIRecord) (error)

// Expose internal portal proxy records to extensions
GetCNSIRecord(guid string) (CNSIRecord, error)
GetCNSIRecordByEndpoint(endpoint string) (CNSIRecord, error)
Expand Down
1 change: 1 addition & 0 deletions src/backend/app-core/repository/interfaces/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ type PortalConfig struct {
EncryptionKeyFilename string `configName:"ENCRYPTION_KEY_FILENAME"`
EncryptionKey string `configName:"ENCRYPTION_KEY"`
AutoRegisterCFUrl string `configName:"AUTO_REG_CF_URL"`
SSOLogin bool `configName:"SSO_LOGIN"`
CookieDomain string `configName:"COOKIE_DOMAIN"`
CFAdminIdentifier string
CloudFoundryInfo *CFInfo
Expand Down
6 changes: 6 additions & 0 deletions src/backend/cloudfoundry/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ func (c *CloudFoundrySpecification) cfLoginHook(context echo.Context) error {
log.Infof("No, user should not auto-connect to auto-registered cloud foundry %s (previsouly disoconnected). ", cfAPI)
} else {
log.Infof("Yes, user should auto-connect to auto-registered cloud foundry %s.", cfAPI)
err = c.portalProxy.DoLoginToCNSIwithConsoleUAAtoken(context, cfCnsi) // no need to login twice
if err != nil {
log.Warnf("Could not use console UAA token to login to auto-registered endpoint: %s", err.Error())
_, err = c.portalProxy.DoLoginToCNSI(context, cfCnsi.GUID, false)
return err
}
_, err := c.portalProxy.DoLoginToCNSI(context, cfCnsi.GUID, false)
return err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
<mat-card class="login__card">
<app-stratos-title></app-stratos-title>
<div class="login__body">
<div class="login__form-outer">
<div class="login__form-outer" [ngClass]="{'login-sso': ssoLogin}">
<form class="login__form" name="loginForm" (ngSubmit)="login()" #loginForm="ngForm">
<mat-form-field>
<mat-form-field *ngIf="!ssoLogin">
<input matInput required [(ngModel)]="username" name="username" placeholder="Username">
</mat-form-field>
<mat-form-field>
<mat-form-field *ngIf="!ssoLogin">
<input matInput required type="password" [(ngModel)]="password" name="password" placeholder="Password">
</mat-form-field>
<button class="login__submit" color="primary" *ngIf="!loggedIn" type="submit" mat-button mat-raised-button [disabled]="!loginForm.valid">Login</button>
<button class="login__submit" color="primary" *ngIf="!loggedIn" type="submit" mat-button mat-raised-button [disabled]="!ssoLogin && !loginForm.valid">Login</button>
</form>
</div>
<div class="login__loading">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
height: 180px;
opacity: 1;
transition: height $time $cubic 100ms, opacity $time * 2 $cubic $time + 50ms;
&.login-sso {
height: auto;
}
}
&__loading {
display: none;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export class LoginPageComponent implements OnInit, OnDestroy {
verifying: boolean;
error: boolean;

ssoLogin: boolean;

busy$: Observable<boolean>;

redirect: RouterRedirect;
Expand All @@ -43,6 +45,7 @@ export class LoginPageComponent implements OnInit, OnDestroy {
subscription: Subscription;

ngOnInit() {
this.ssoLogin = false;
this.store.dispatch(new VerifySession());
const auth$ = this.store.select(s => ({ auth: s.auth, endpoints: s.endpoints }));
this.busy$ = auth$.pipe(
Expand Down Expand Up @@ -73,6 +76,16 @@ export class LoginPageComponent implements OnInit, OnDestroy {
}

login() {
if (this.ssoLogin) {
const returnUrl = encodeURI(window.location.protocol + '//' + window.location.hostname +
(window.location.port ? ':' + window.location.port : ''));
window.open('/pp/v1/auth/sso_login?state=' + returnUrl , '_self');
this.busy$ = new Observable<boolean>((observer) => {
observer.next(true);
observer.complete();
});
return;
}
this.message = '';
this.store.dispatch(new Login(this.username, this.password));
}
Expand All @@ -92,6 +105,7 @@ export class LoginPageComponent implements OnInit, OnDestroy {
this.loggedIn = auth.loggedIn;
this.loggingIn = auth.loggingIn;
this.verifying = auth.verifying;
this.ssoLogin = auth.sessionData && auth.sessionData.isSSOLogin;

// Upgrade in progress
if (auth.sessionData && auth.sessionData.upgradeInProgress) {
Expand All @@ -116,7 +130,7 @@ export class LoginPageComponent implements OnInit, OnDestroy {

// auth.sessionData will be populated if user has been redirected here after attempting to access a protected page without
// a valid session
this.error = auth.error && (!auth.sessionData || !auth.sessionData.valid);
this.error = auth.error && (!auth.sessionData || !auth.sessionData.valid) && !this.ssoLogin;

if (this.error) {
if (auth.error && auth.errorResponse && auth.errorResponse === 'Invalid session') {
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/app/store/actions/auth.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export class VerifiedSession implements Action {
}

export class InvalidSession implements Action {
constructor(public uaaError: boolean = false, public upgradeInProgress = false, public domainMismatch = false) { }
constructor(public uaaError: boolean = false, public upgradeInProgress = false,
public domainMismatch = false, public isSSOLogin = false) { }
type = SESSION_INVALID;
}

Expand Down
4 changes: 3 additions & 1 deletion src/frontend/app/store/effects/auth.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
const SETUP_HEADER = 'stratos-setup-required';
const UPGRADE_HEADER = 'retry-after';
const DOMAIN_HEADER = 'x-stratos-domain';
const SSO_HEADER = 'x-stratos-sso-login';

@Injectable()
export class AuthEffect {
Expand Down Expand Up @@ -83,14 +84,15 @@ export class AuthEffect {
catchError((err, caught) => {
let setupMode = false;
let isUpgrading = false;
const isSSO = err.headers.has(SSO_HEADER);
if (err.status === 503) {
setupMode = err.headers.has(SETUP_HEADER);
isUpgrading = err.headers.has(UPGRADE_HEADER);
}

// Check for cookie domain mismatch with the requesting URL
const isDomainMismatch = this.isDomainMismatch(err.headers);
return action.login ? [new InvalidSession(setupMode, isUpgrading, isDomainMismatch)] : [new ResetAuth()];
return action.login ? [new InvalidSession(setupMode, isUpgrading, isDomainMismatch, isSSO)] : [new ResetAuth()];
}));
}));

Expand Down
2 changes: 1 addition & 1 deletion src/frontend/app/store/reducers/auth.reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function authReducer(state: AuthState = defaultState, action): AuthState
return {
...state,
sessionData: { valid: false, uaaError: action.uaaError, upgradeInProgress: action.upgradeInProgress,
domainMismatch: action.domainMismatch, sessionExpiresOn: null },
domainMismatch: action.domainMismatch, isSSOLogin: action.isSSOLogin, sessionExpiresOn: null },
verifying: false
};
case RouterActions.GO:
Expand Down
1 change: 1 addition & 0 deletions src/frontend/app/store/types/auth.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface SessionData {
valid: boolean;
uaaError?: boolean;
upgradeInProgress?: boolean;
isSSOLogin?: boolean;
sessionExpiresOn: number;
domainMismatch?: boolean;
}