diff --git a/src/jetstream/auth.go b/src/jetstream/auth.go index b72a842a9d..70881b43ad 100644 --- a/src/jetstream/auth.go +++ b/src/jetstream/auth.go @@ -1,835 +1,55 @@ package main import ( - "crypto/rand" - "encoding/base64" - "encoding/json" "errors" "fmt" - "math" "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/cnsis" log "github.com/sirupsen/logrus" "github.com/labstack/echo" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/localusers" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/tokens" - - "golang.org/x/crypto/bcrypt" ) // LoginHookFunc - function that can be hooked into a successful user login type LoginHookFunc func(c echo.Context) error -// UAAAdminIdentifier - The identifier that UAA uses to convey administrative level perms -const UAAAdminIdentifier = "stratos.admin" - -// CFAdminIdentifier - The scope that Cloud Foundry uses to convey administrative level perms -const CFAdminIdentifier = "cloud_controller.admin" - -// SessionExpiresOnHeader Custom header for communicating the session expiry time to clients -const SessionExpiresOnHeader = "X-Cap-Session-Expires-On" - -// ClientRequestDateHeader Custom header for getting date form client -const ClientRequestDateHeader = "X-Cap-Request-Date" - -// XSRFTokenHeader - XSRF Token Header name -const XSRFTokenHeader = "X-Xsrf-Token" - -// XSRFTokenCookie - XSRF Token Cookie name -const XSRFTokenCookie = "XSRF-TOKEN" - -// XSRFTokenSessionName - XSRF Token Session name -const XSRFTokenSessionName = "xsrf_token" - -//LogoutResponse is sent upon user logout. -//It contains a flag to indicate whether or not the user was signed in with SSO -type LogoutResponse struct { - IsSSO bool `json:"isSSO"` -} - -func (p *portalProxy) getUAAIdentityEndpoint() string { - log.Debug("getUAAIdentityEndpoint") - return fmt.Sprintf("%s/oauth/token", p.Config.ConsoleConfig.UAAEndpoint) -} - -func (p *portalProxy) removeEmptyCookie(c echo.Context) { - req := c.Request() - originalCookie := req.Header.Get("Cookie") - cleanCookie := p.EmptyCookieMatcher.ReplaceAllLiteralString(originalCookie, "") - req.Header.Set("Cookie", cleanCookie) -} - -// Get the user name for the specified user -func (p *portalProxy) GetUsername(userid string) (string, error) { - tr, err := p.GetUAATokenRecord(userid) - if err != nil { - return "", err - } - - u, userTokenErr := p.GetUserTokenInfo(tr.AuthToken) - if userTokenErr != nil { - return "", userTokenErr - } - - return u.UserName, nil -} - -// Login via UAA -func (p *portalProxy) initSSOlogin(c echo.Context) error { - if !p.Config.SSOLogin { - err := interfaces.NewHTTPShadowError( - http.StatusNotFound, - "SSO Login is not enabled", - "SSO Login is not enabled") - return err - } - - state := c.QueryParam("state") - if len(state) == 0 { - err := interfaces.NewHTTPShadowError( - http.StatusUnauthorized, - "SSO Login: Redirect state parameter missing", - "SSO Login: Redirect state parameter missing") - return err - } - - if !safeSSORedirectState(state, p.Config.SSOWhiteList) { - err := interfaces.NewHTTPShadowError( - http.StatusUnauthorized, - "SSO Login: Disallowed redirect state", - "SSO Login: Disallowed redirect state") - return err - } - - redirectURL := fmt.Sprintf("%s/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s", p.Config.ConsoleConfig.AuthorizationEndpoint, p.Config.ConsoleConfig.ConsoleClient, url.QueryEscape(getSSORedirectURI(state, state, ""))) - c.Redirect(http.StatusTemporaryRedirect, redirectURL) - return nil -} - -func safeSSORedirectState(state string, whiteListStr string) bool { - if len(whiteListStr) == 0 { - return true; - } - - whiteList := strings.Split(whiteListStr, ",") - if len(whiteList) == 0 { - return true; - } - - for _, n := range whiteList { - if CompareURL(state, n) { - return true - } - } - return false -} - -func getSSORedirectURI(base string, state string, endpointGUID string) string { - baseURL, _ := url.Parse(base) - baseURL.Path = "" - baseURL.RawQuery = "" - baseURLString := strings.TrimRight(baseURL.String(), "?") - - 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 -func (p *portalProxy) ssoLogoutOfUAA(c echo.Context) error { - if !p.Config.SSOLogin { - err := interfaces.NewHTTPShadowError( - http.StatusNotFound, - "SSO Login is not enabled", - "SSO Login is not enabled") - return err - } - - 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 - } - - // 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", ""))) - } else { - redirectURL = "/login?SSO_Message=You+have+been+logged+out" - } - return c.Redirect(http.StatusTemporaryRedirect, redirectURL) -} - -func (p *portalProxy) hasSSOOption(option string) bool { - // Remove all spaces - opts := RemoveSpaces(p.Config.SSOOptions) - - // Split based on ',' - options := strings.Split(opts, ",") - return ArrayContainsString(options, option) -} - -// Callback - invoked after the UAA login flow has completed and during logout -// We use a single callback so this can be whitelisted in the client -func (p *portalProxy) ssoLoginToUAA(c echo.Context) error { - 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 - } - - // 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") - } - _, err := p.doLoginToUAA(c) - if err != nil { - // Send error as query string param - msg := err.Error() - if httpError, ok := err.(interfaces.ErrHTTPShadow); ok { - msg = httpError.UserFacingError - } - if httpError, ok := err.(interfaces.ErrHTTPRequest); ok { - msg = httpError.Response - } - state = fmt.Sprintf("%s/login?SSO_Message=%s", state, url.QueryEscape(msg)) - } - - return c.Redirect(http.StatusTemporaryRedirect, state) -} - -func (p *portalProxy) loginToUAA(c echo.Context) error { - log.Debug("loginToUAA") - - if interfaces.AuthEndpointTypes[p.Config.ConsoleConfig.AuthEndpointType] != interfaces.Remote { - err := interfaces.NewHTTPShadowError( - http.StatusNotFound, - "UAA Login is not enabled", - "UAA Login is not enabled") - return err - } - - resp, err := p.doLoginToUAA(c) - if err != nil { - return err - } - - jsonString, err := json.Marshal(resp) - if err != nil { - return err - } - - // Add XSRF Token - p.ensureXSRFToken(c) - - c.Response().Header().Set("Content-Type", "application/json") - c.Response().Write(jsonString) - - return nil -} - -// Use the appropriate login mechanism -func (p *portalProxy) stratosLoginHandler(c echo.Context) error { - // Local login - if interfaces.AuthEndpointTypes[p.Config.ConsoleConfig.AuthEndpointType] == interfaces.Local { - return p.localLogin(c) - } - - // UAA login - return p.loginToUAA(c) -} - -// Use the appropriate logout mechanism -func (p *portalProxy) stratosLogoutHandler(c echo.Context) error { - return p.logout(c) -} - -func (p *portalProxy) doLoginToUAA(c echo.Context) (*interfaces.LoginRes, error) { - log.Debug("doLoginToUAA") - uaaRes, u, err := p.login(c, p.Config.ConsoleConfig.SkipSSLValidation, p.Config.ConsoleConfig.ConsoleClient, p.Config.ConsoleConfig.ConsoleClientSecret, p.getUAAIdentityEndpoint()) - if err != nil { - // Check the Error - errMessage := "Access Denied" - if httpError, ok := err.(interfaces.ErrHTTPRequest); ok { - // Try and parse the Response into UAA error structure - authError := &interfaces.UAAErrorResponse{} - if err := json.Unmarshal([]byte(httpError.Response), authError); err == nil { - errMessage = authError.ErrorDescription - } - } - - err = interfaces.NewHTTPShadowError( - http.StatusUnauthorized, - errMessage, - "UAA Login failed: %s: %v", errMessage, err) - return nil, err - } - - sessionValues := make(map[string]interface{}) - sessionValues["user_id"] = u.UserGUID - sessionValues["exp"] = u.TokenExpiry - - // Ensure that login disregards cookies from the request - req := c.Request() - req.Header.Set("Cookie", "") - if err = p.setSessionValues(c, sessionValues); err != nil { - return nil, err - } - - err = p.handleSessionExpiryHeader(c) - if err != nil { - return nil, err - } - - _, err = p.saveAuthToken(*u, uaaRes.AccessToken, uaaRes.RefreshToken) - if err != nil { - return nil, err - } - - err = p.ExecuteLoginHooks(c) - if err != nil { - log.Warnf("Login hooks failed: %v", err) - } - - uaaAdmin := strings.Contains(uaaRes.Scope, p.Config.ConsoleConfig.ConsoleAdminScope) - resp := &interfaces.LoginRes{ - Account: u.UserName, - TokenExpiry: u.TokenExpiry, - APIEndpoint: nil, - Admin: uaaAdmin, - } - return resp, nil -} - -func (p *portalProxy) localLogin(c echo.Context) error { - log.Debug("localLogin") - - if interfaces.AuthEndpointTypes[p.Config.ConsoleConfig.AuthEndpointType] != interfaces.Local { - err := interfaces.NewHTTPShadowError( - http.StatusNotFound, - "Local Login is not enabled", - "Local Login is not enabled") - return err - } - - //Perform the login and fetch session values if successful - userGUID, username, err := p.doLocalLogin(c) - if err != nil { - //Login failed, return response. - errMessage := err.Error() - err := interfaces.NewHTTPShadowError( - http.StatusUnauthorized, - errMessage, - "Login failed: %s: %v", errMessage, err) - return err - } - - var expiry int64 - expiry = math.MaxInt64 - - sessionValues := make(map[string]interface{}) - sessionValues["user_id"] = userGUID - sessionValues["exp"] = expiry - - // Ensure that login disregards cookies from the request - req := c.Request() - req.Header.Set("Cookie", "") - if err = p.setSessionValues(c, sessionValues); err != nil { - return err - } - - //Makes sure the client gets the right session expiry time - if err = p.handleSessionExpiryHeader(c); err != nil { - return err - } - - resp := &interfaces.LoginRes{ - Account: username, - TokenExpiry: expiry, - APIEndpoint: nil, - Admin: true, - } - - if jsonString, err := json.Marshal(resp); err == nil { - // Add XSRF Token - p.ensureXSRFToken(c) - c.Response().Header().Set("Content-Type", "application/json") - c.Response().Write(jsonString) - } - - return err -} - -func (p *portalProxy) doLocalLogin(c echo.Context) (string, string, error) { - log.Debug("doLocalLogin") - - username := c.FormValue("username") - password := c.FormValue("password") - - if len(username) == 0 || len(password) == 0 { - return "", username, errors.New("Needs usernameand password") - } - - localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(p.DatabaseConnectionPool) - if err != nil { - log.Errorf("Database error getting repo for Local users: %v", err) - return "", username, err - } - - var scopeOK bool - var hash []byte - var authError error - var localUserScope string - - // Get the GUID for the specified user - guid, err := localUsersRepo.FindUserGUID(username) - if err != nil { - return guid, username, fmt.Errorf("Can not find user") - } - - //Attempt to find the password has for the given user - if hash, authError = localUsersRepo.FindPasswordHash(guid); authError != nil { - authError = fmt.Errorf("User not found.") - //Check the password hash - } else if authError = CheckPasswordHash(password, hash); authError != nil { - authError = fmt.Errorf("Access Denied - Invalid username/password credentials") - } else { - //Ensure the local user has some kind of admin role configured and we check for it here - localUserScope, authError = localUsersRepo.FindUserScope(guid) - scopeOK = strings.Contains(localUserScope, p.Config.ConsoleConfig.LocalUserScope) - if (authError != nil) || (!scopeOK) { - authError = fmt.Errorf("Access Denied - User scope invalid") - } else { - //Update the last login time here if login was successful - loginTime := time.Now() - if updateLoginTimeErr := localUsersRepo.UpdateLastLoginTime(guid, loginTime); updateLoginTimeErr != nil { - log.Error(updateLoginTimeErr) - log.Errorf("Failed to update last login time for user: %s", guid) - } - } - } - return guid, username, authError -} - -//HashPassword accepts a plaintext password string and generates a salted hash -func HashPassword(password string) ([]byte, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) - return bytes, err -} - -//CheckPasswordHash accepts a bcrypt salted hash and plaintext password. -//It verifies the password against the salted hash -func CheckPasswordHash(password string, hash []byte) error { - err := bcrypt.CompareHashAndPassword(hash, []byte(password)) - return err -} - -// Start SSO flow for an Endpoint -func (p *portalProxy) ssoLoginToCNSI(c echo.Context) error { - log.Debug("ssoLoginToCNSI") - 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 { - log.Debug("loginToCNSI") - cnsiGUID := c.FormValue("cnsi_guid") - var systemSharedToken = false - - if len(cnsiGUID) == 0 { - return interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Missing target endpoint", - "Need Endpoint GUID passed as form param") - } - - systemSharedValue := c.FormValue("system_shared") - if len(systemSharedValue) > 0 { - systemSharedToken = systemSharedValue == "true" - } - - resp, err := p.DoLoginToCNSI(c, cnsiGUID, systemSharedToken) - if err != nil { - return err - } - - jsonString, err := json.Marshal(resp) - if err != nil { - return err - } - - c.Response().Header().Set("Content-Type", "application/json") - c.Response().Write(jsonString) - return nil -} - -func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemSharedToken bool) (*interfaces.LoginRes, error) { - - cnsiRecord, err := p.GetCNSIRecord(cnsiGUID) - if err != nil { - return nil, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Requested endpoint not registered", - "No Endpoint registered with GUID %s: %s", cnsiGUID, err) - } - - // Get the User ID since we save the CNSI token against the Console user guid, not the CNSI user guid so that we can look it up easily - userID, err := p.GetSessionStringValue(c, "user_id") - if err != nil { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") - } - - // Register as a system endpoint? - if systemSharedToken { - // User needs to be an admin - user, err := p.GetUAAUser(userID) - if err != nil { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - could not check user") - } - - if !user.Admin { - return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - user is not an administrator") - } - - // We are all good to go - change the userID, so we record this token against the system-shared user and not this specific user - // This is how we identify system-shared endpoint tokens - userID = tokens.SystemSharedUserGuid - } - - // Ask the endpoint type to connect - for _, plugin := range p.Plugins { - endpointPlugin, err := plugin.GetEndpointPlugin() - if err != nil { - // Plugin doesn't implement an Endpoint Plugin interface, skip - continue - } - - endpointType := endpointPlugin.GetType() - if cnsiRecord.CNSIType == endpointType { - tokenRecord, isAdmin, err := endpointPlugin.Connect(c, cnsiRecord, userID) - if err != nil { - if shadowError, ok := err.(interfaces.ErrHTTPShadow); ok { - return nil, shadowError - } - return nil, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Could not connect to the endpoint", - "Could not connect to the endpoint: %s", err) - } - - err = p.setCNSITokenRecord(cnsiGUID, userID, *tokenRecord) - if err != nil { - return nil, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Failed to save Token for endpoint", - "Error occurred: %s", err) - } - - // Validate the connection - some endpoints may want to validate that the connected endpoint - err = endpointPlugin.Validate(userID, cnsiRecord, *tokenRecord) - if err != nil { - // Clear the token - p.ClearCNSIToken(cnsiRecord, userID) - return nil, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Could not connect to the endpoint", - "Could not connect to the endpoint: %s", err) - } - - resp := &interfaces.LoginRes{ - Account: userID, - TokenExpiry: tokenRecord.TokenExpiry, - APIEndpoint: cnsiRecord.APIEndpoint, - Admin: isAdmin, - } - - cnsiUser, ok := p.GetCNSIUserFromToken(cnsiGUID, tokenRecord) - if ok { - // If this is a system shared endpoint, then remove some metadata that should be send back to other users - santizeInfoForSystemSharedTokenUser(cnsiUser, systemSharedToken) - resp.User = cnsiUser - } else { - // Need to record a user - resp.User = &interfaces.ConnectedUser{ - GUID: "Unknown", - Name: "Unknown", - Scopes: []string{"read"}, - Admin: true, - } - } - - return resp, nil - } - } - - return nil, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Endpoint connection not supported", - "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.TokenEndpoint) - if err != nil { - return fmt.Errorf("invalid authorization endpoint URL %s %s", cnsiInfo.TokenEndpoint, err) - } - - if uaaURL.String() == p.GetConfig().ConsoleConfig.UAAEndpoint.String() { // CNSI UAA server matches Console UAA server - uaaToken.LinkedGUID = uaaToken.TokenGUID - err = p.setCNSITokenRecord(theCNSIrecord.GUID, u.UserGUID, uaaToken) - - // Update the endpoint to indicate that SSO Login is okay - repo, dbErr := cnsis.NewPostgresCNSIRepository(p.DatabaseConnectionPool) - if dbErr == nil { - repo.Update(theCNSIrecord.GUID, true) - } - // Return error from the login - return err - } - return fmt.Errorf("the auto-registered endpoint UAA server does not match console UAA server") - } - log.Warn("Could not find current user UAA token") - return err -} - -func santizeInfoForSystemSharedTokenUser(cnsiUser *interfaces.ConnectedUser, isSysystemShared bool) { - if isSysystemShared { - cnsiUser.GUID = tokens.SystemSharedUserGuid - cnsiUser.Scopes = make([]string, 0) - cnsiUser.Name = "system_shared" - } -} - -func (p *portalProxy) ConnectOAuth2(c echo.Context, cnsiRecord interfaces.CNSIRecord) (*interfaces.TokenRecord, error) { - uaaRes, u, _, err := p.FetchOAuth2Token(cnsiRecord, c) - if err != nil { - return nil, err - } - tokenRecord := p.InitEndpointTokenRecord(u.TokenExpiry, uaaRes.AccessToken, uaaRes.RefreshToken, false) - return &tokenRecord, nil -} - -func (p *portalProxy) fetchHTTPBasicToken(cnsiRecord interfaces.CNSIRecord, c echo.Context) (*interfaces.UAAResponse, *interfaces.JWTUserTokenInfo, *interfaces.CNSIRecord, error) { - - uaaRes, u, err := p.loginHTTPBasic(c) - - if err != nil { - return nil, nil, nil, interfaces.NewHTTPShadowError( - http.StatusUnauthorized, - "Login failed", - "Login failed: %v", err) - } - return uaaRes, u, &cnsiRecord, nil -} - -func (p *portalProxy) FetchOAuth2Token(cnsiRecord interfaces.CNSIRecord, c echo.Context) (*interfaces.UAAResponse, *interfaces.JWTUserTokenInfo, *interfaces.CNSIRecord, error) { - endpoint := cnsiRecord.AuthorizationEndpoint - - tokenEndpoint := fmt.Sprintf("%s/oauth/token", endpoint) - - uaaRes, u, err := p.login(c, cnsiRecord.SkipSSLValidation, cnsiRecord.ClientId, cnsiRecord.ClientSecret, tokenEndpoint) - - if err != nil { - if httpError, ok := err.(interfaces.ErrHTTPRequest); ok { - // Try and parse the Response into UAA error structure (p.login only handles UAA requests) - errMessage := "" - authError := &interfaces.UAAErrorResponse{} - if err := json.Unmarshal([]byte(httpError.Response), authError); err == nil { - errMessage = fmt.Sprintf(": %s", authError.ErrorDescription) - } - return nil, nil, nil, interfaces.NewHTTPShadowError( - httpError.Status, - fmt.Sprintf("Could not connect to the endpoint%s", errMessage), - "Could not connect to the endpoint: %s", err) - } - - return nil, nil, nil, interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Login failed", - "Login failed: %v", err) - } - return uaaRes, u, &cnsiRecord, nil -} - -func (p *portalProxy) logoutOfCNSI(c echo.Context) error { - log.Debug("logoutOfCNSI") - - cnsiGUID := c.FormValue("cnsi_guid") - - if len(cnsiGUID) == 0 { - return interfaces.NewHTTPShadowError( - http.StatusBadRequest, - "Missing target endpoint", - "Need CNSI GUID passed as form param") - } - - userGUID, err := p.GetSessionStringValue(c, "user_id") - if err != nil { - return fmt.Errorf("Could not find correct session value: %s", err) - } - - cnsiRecord, err := p.GetCNSIRecord(cnsiGUID) - if err != nil { - return fmt.Errorf("Unable to load CNSI record: %s", err) - } - - // Get the existing token to see if it is connected as a system shared endpoint - tr, ok := p.GetCNSITokenRecord(cnsiGUID, userGUID) - if ok && tr.SystemShared { - // User needs to be an admin - user, err := p.GetUAAUser(userGUID) - if err != nil { - return echo.NewHTTPError(http.StatusUnauthorized, "Can not disconnect System Shared endpoint - could not check user") - } - - if !user.Admin { - return echo.NewHTTPError(http.StatusUnauthorized, "Can not disconnect System Shared endpoint - user is not an administrator") - } - userGUID = tokens.SystemSharedUserGuid - } - - // Clear the token - return p.ClearCNSIToken(cnsiRecord, userGUID) +//LogoutResponse is sent upon user logout. +//It contains a flag to indicate whether or not the user was signed in with SSO +type LogoutResponse struct { + IsSSO bool `json:"isSSO"` } -// Clear the CNSI token -func (p *portalProxy) ClearCNSIToken(cnsiRecord interfaces.CNSIRecord, userGUID string) error { - // If cnsi is cf AND cf is auto-register only clear the entry - p.Config.AutoRegisterCFUrl = strings.TrimRight(p.Config.AutoRegisterCFUrl, "/") - if cnsiRecord.CNSIType == "cf" && p.GetConfig().AutoRegisterCFUrl == cnsiRecord.APIEndpoint.String() { - log.Debug("Setting token record as disconnected") - - tokenRecord := p.InitEndpointTokenRecord(0, "cleared_token", "cleared_token", true) - if err := p.setCNSITokenRecord(cnsiRecord.GUID, userGUID, tokenRecord); err != nil { - return fmt.Errorf("Unable to clear token: %s", err) +//InitStratosAuthService is used to instantiate an Auth service when setting up the portalProxy +func (p *portalProxy) InitStratosAuthService(t interfaces.AuthEndpointType) error { + var auth interfaces.StratosAuth + switch t { + case interfaces.Local: + auth = &localAuth{ + databaseConnectionPool: p.DatabaseConnectionPool, + localUserScope: p.Config.ConsoleConfig.LocalUserScope, + p: p, } - } else { - log.Debug("Deleting Token") - if err := p.deleteCNSIToken(cnsiRecord.GUID, userGUID); err != nil { - return fmt.Errorf("Unable to delete token: %s", err) + case interfaces.Remote: + auth = &uaaAuth{ + databaseConnectionPool: p.DatabaseConnectionPool, + p: p, } + default: + err := fmt.Errorf("Invalid auth endpoint type: %v", t) + return err } - + p.StratosAuthService = auth return nil } -func (p *portalProxy) RefreshUAALogin(username, password string, store bool) error { - log.Debug("RefreshUAALogin") - uaaRes, err := p.getUAATokenWithCreds(p.Config.ConsoleConfig.SkipSSLValidation, username, password, p.Config.ConsoleConfig.ConsoleClient, p.Config.ConsoleConfig.ConsoleClientSecret, p.getUAAIdentityEndpoint()) - if err != nil { - return err - } - - u, err := p.GetUserTokenInfo(uaaRes.AccessToken) - if err != nil { - return err - } - - if store { - _, err = p.saveAuthToken(*u, uaaRes.AccessToken, uaaRes.RefreshToken) - if err != nil { - return err - } - } - - return nil +//GetAuthService gets the auth service from portalProxy via the Auth interface +func (p *portalProxy) GetStratosAuthService() interfaces.StratosAuth { + return p.StratosAuthService } +//login is used for both endpoint and direct UAA login func (p *portalProxy) login(c echo.Context, skipSSLValidation bool, client string, clientSecret string, endpoint string) (uaaRes *interfaces.UAAResponse, u *interfaces.JWTUserTokenInfo, err error) { log.Debug("login") if c.Request().Method == http.MethodGet { @@ -858,553 +78,3 @@ func (p *portalProxy) login(c echo.Context, skipSSLValidation bool, client strin return uaaRes, u, nil } - -func (p *portalProxy) loginHTTPBasic(c echo.Context) (uaaRes *interfaces.UAAResponse, u *interfaces.JWTUserTokenInfo, err error) { - log.Debug("login") - username := c.FormValue("username") - password := c.FormValue("password") - - if len(username) == 0 || len(password) == 0 { - return uaaRes, u, errors.New("Needs username and password") - } - - authString := fmt.Sprintf("%s:%s", username, password) - base64EncodedAuthString := base64.StdEncoding.EncodeToString([]byte(authString)) - - uaaRes.AccessToken = fmt.Sprintf("Basic %s", base64EncodedAuthString) - return uaaRes, u, nil -} - -func (p *portalProxy) logout(c echo.Context) error { - log.Debug("logout") - - p.removeEmptyCookie(c) - - // Remove the XSRF Token from the session - p.unsetSessionValue(c, XSRFTokenSessionName) - - err := p.clearSession(c) - if err != nil { - log.Errorf("Unable to clear session: %v", err) - } - - // Send JSON document - resp := &LogoutResponse{ - IsSSO: p.Config.SSOLogin, - } - - return c.JSON(http.StatusOK, resp) -} - -func (p *portalProxy) getUAATokenWithAuthorizationCode(skipSSLValidation bool, code, client, clientSecret, authEndpoint string, state string, cnsiGUID string) (*interfaces.UAAResponse, error) { - log.Debug("getUAATokenWithAuthorizationCode") - - 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, state, cnsiGUID)) - - return p.getUAAToken(body, skipSSLValidation, client, clientSecret, authEndpoint) -} - -func (p *portalProxy) getUAATokenWithCreds(skipSSLValidation bool, username, password, client, clientSecret, authEndpoint string) (*interfaces.UAAResponse, error) { - log.Debug("getUAATokenWithCreds") - - body := url.Values{} - body.Set("grant_type", "password") - body.Set("username", username) - body.Set("password", password) - body.Set("response_type", "token") - - return p.getUAAToken(body, skipSSLValidation, client, clientSecret, authEndpoint) -} - -func (p *portalProxy) getUAATokenWithRefreshToken(skipSSLValidation bool, refreshToken, client, clientSecret, authEndpoint string, scopes string) (*interfaces.UAAResponse, error) { - log.Debug("getUAATokenWithRefreshToken") - - body := url.Values{} - body.Set("grant_type", "refresh_token") - body.Set("refresh_token", refreshToken) - body.Set("response_type", "token") - - if len(scopes) > 0 { - body.Set("scope", scopes) - } - - return p.getUAAToken(body, skipSSLValidation, client, clientSecret, authEndpoint) -} - -func (p *portalProxy) getUAAToken(body url.Values, skipSSLValidation bool, client, clientSecret, authEndpoint string) (*interfaces.UAAResponse, error) { - log.WithField("authEndpoint", authEndpoint).Debug("getUAAToken") - req, err := http.NewRequest("POST", authEndpoint, strings.NewReader(body.Encode())) - if err != nil { - msg := "Failed to create request for UAA: %v" - log.Errorf(msg, err) - return nil, fmt.Errorf(msg, err) - } - - req.SetBasicAuth(client, clientSecret) - req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) - - var h = p.GetHttpClientForRequest(req, skipSSLValidation) - res, err := h.Do(req) - if err != nil || res.StatusCode != http.StatusOK { - log.Errorf("Error performing http request - response: %v, error: %v", res, err) - return nil, interfaces.LogHTTPError(res, err) - } - - defer res.Body.Close() - - var response interfaces.UAAResponse - - dec := json.NewDecoder(res.Body) - if err = dec.Decode(&response); err != nil { - log.Errorf("Error decoding response: %v", err) - return nil, fmt.Errorf("getUAAToken Decode: %s", err) - } - - return &response, nil -} - -func (p *portalProxy) saveAuthToken(u interfaces.JWTUserTokenInfo, authTok string, refreshTok string) (interfaces.TokenRecord, error) { - log.Debug("saveAuthToken") - - key := u.UserGUID - tokenRecord := interfaces.TokenRecord{ - AuthToken: authTok, - RefreshToken: refreshTok, - TokenExpiry: u.TokenExpiry, - AuthType: interfaces.AuthTypeOAuth2, - } - - err := p.setUAATokenRecord(key, tokenRecord) - if err != nil { - return tokenRecord, err - } - - return tokenRecord, nil -} - -// Helper to initialzie a token record using the specified parameters -func (p *portalProxy) InitEndpointTokenRecord(expiry int64, authTok string, refreshTok string, disconnect bool) interfaces.TokenRecord { - tokenRecord := interfaces.TokenRecord{ - AuthToken: authTok, - RefreshToken: refreshTok, - TokenExpiry: expiry, - Disconnected: disconnect, - AuthType: interfaces.AuthTypeOAuth2, - } - - return tokenRecord -} - -func (p *portalProxy) deleteCNSIToken(cnsiID string, userGUID string) error { - log.Debug("deleteCNSIToken") - - err := p.unsetCNSITokenRecord(cnsiID, userGUID) - if err != nil { - log.Errorf("%v", err) - return err - } - - return nil -} - -func (p *portalProxy) GetUAATokenRecord(userGUID string) (interfaces.TokenRecord, error) { - log.Debug("GetUAATokenRecord") - - tokenRepo, err := tokens.NewPgsqlTokenRepository(p.DatabaseConnectionPool) - if err != nil { - log.Errorf("Database error getting repo for UAA token: %v", err) - return interfaces.TokenRecord{}, err - } - - tr, err := tokenRepo.FindAuthToken(userGUID, p.Config.EncryptionKeyInBytes) - if err != nil { - log.Errorf("Database error finding UAA token: %v", err) - return interfaces.TokenRecord{}, err - } - - return tr, nil -} - -func (p *portalProxy) setUAATokenRecord(key string, t interfaces.TokenRecord) error { - log.Debug("setUAATokenRecord") - - tokenRepo, err := tokens.NewPgsqlTokenRepository(p.DatabaseConnectionPool) - if err != nil { - return fmt.Errorf("Database error getting repo for UAA token: %v", err) - } - - err = tokenRepo.SaveAuthToken(key, t, p.Config.EncryptionKeyInBytes) - if err != nil { - return fmt.Errorf("Database error saving UAA token: %v", err) - } - - return nil -} - -func (p *portalProxy) verifySession(c echo.Context) error { - log.Debug("verifySession") - - sessionExpireTime, err := p.GetSessionInt64Value(c, "exp") - if err != nil { - msg := "Could not find session date" - log.Error(msg) - return echo.NewHTTPError(http.StatusForbidden, msg) - } - - sessionUser, err := p.GetSessionStringValue(c, "user_id") - if err != nil { - msg := "Could not find user_id in Session" - log.Error(msg) - return echo.NewHTTPError(http.StatusForbidden, msg) - } - - if interfaces.AuthEndpointTypes[p.Config.ConsoleConfig.AuthEndpointType] == interfaces.Local { - err = p.verifySessionLocal(c, sessionUser, sessionExpireTime) - } else { - err = p.verifySessionUAA(c, sessionUser, sessionExpireTime) - } - - // Could not verify session - if err != nil { - log.Error(err) - return echo.NewHTTPError(http.StatusForbidden, "Could not verify user") - } - - err = p.handleSessionExpiryHeader(c) - if err != nil { - return err - } - - info, err := p.getInfo(c) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - // Add XSRF Token - p.ensureXSRFToken(c) - - err = c.JSON(http.StatusOK, info) - if err != nil { - return err - } - - return nil -} - -func (p *portalProxy) verifySessionUAA(c echo.Context, sessionUser string, sessionExpireTime int64) error { - tr, err := p.GetUAATokenRecord(sessionUser) - if err != nil { - msg := fmt.Sprintf("Unable to find UAA Token: %s", err) - log.Error(msg, err) - return echo.NewHTTPError(http.StatusForbidden, msg) - } - - // Check if UAA token has expired - if time.Now().After(time.Unix(sessionExpireTime, 0)) { - - // UAA Token has expired, refresh the token, if that fails, fail the request - uaaRes, tokenErr := p.getUAATokenWithRefreshToken(p.Config.ConsoleConfig.SkipSSLValidation, tr.RefreshToken, p.Config.ConsoleConfig.ConsoleClient, p.Config.ConsoleConfig.ConsoleClientSecret, p.getUAAIdentityEndpoint(), "") - if tokenErr != nil { - msg := "Could not refresh UAA token" - log.Error(msg, tokenErr) - return echo.NewHTTPError(http.StatusForbidden, msg) - } - - u, userTokenErr := p.GetUserTokenInfo(uaaRes.AccessToken) - if userTokenErr != nil { - return userTokenErr - } - - if _, err = p.saveAuthToken(*u, uaaRes.AccessToken, uaaRes.RefreshToken); err != nil { - return err - } - sessionValues := make(map[string]interface{}) - sessionValues["user_id"] = u.UserGUID - sessionValues["exp"] = u.TokenExpiry - - if err = p.setSessionValues(c, sessionValues); err != nil { - return err - } - } else { - // Still need to extend the expires_on of the Session - if err = p.setSessionValues(c, nil); err != nil { - return err - } - } - - return nil -} - -func (p *portalProxy) verifySessionLocal(c echo.Context, sessionUser string, sessionExpireTime int64) error { - localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(p.DatabaseConnectionPool) - if err != nil { - log.Errorf("Database error getting repo for Local users: %v", err) - return err - } - - _, err = localUsersRepo.FindPasswordHash(sessionUser) - return err -} - - -// Create a token for XSRF if needed, store it in the session and add the response header for the front-end to pick up -func (p *portalProxy) ensureXSRFToken(c echo.Context) { - token, err := p.GetSessionStringValue(c, XSRFTokenSessionName) - if err != nil || len(token) == 0 { - // Need a new token - tokenBytes, err := generateRandomBytes(32) - if err == nil { - token = base64.StdEncoding.EncodeToString(tokenBytes) - } else { - token = "" - } - sessionValues := make(map[string]interface{}) - sessionValues[XSRFTokenSessionName] = token - p.setSessionValues(c, sessionValues) - } - - if len(token) > 0 { - c.Response().Header().Set(XSRFTokenHeader, token) - } -} - -// See: https://github.com/gorilla/csrf/blob/a8abe8abf66db8f4a9750d76ba95b4021a354757/helpers.go -// generateRandomBytes returns securely generated random bytes. -// It will return an error if the system's secure random number generator fails to function correctly. -func generateRandomBytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - // err == nil only if len(b) == n - if err != nil { - return nil, err - } - - return b, nil - -} - -func (p *portalProxy) handleSessionExpiryHeader(c echo.Context) error { - - // Explicitly tell the client when this session will expire. This is needed because browsers actively hide - // the Set-Cookie header and session cookie expires_on from client side javascript - expOn, err := p.GetSessionValue(c, "expires_on") - if err != nil { - msg := "Could not get session expiry" - log.Error(msg+" - ", err) - return echo.NewHTTPError(http.StatusInternalServerError, msg) - } - c.Response().Header().Set(SessionExpiresOnHeader, strconv.FormatInt(expOn.(time.Time).Unix(), 10)) - - expiry := expOn.(time.Time) - expiryDuration := expiry.Sub(time.Now()) - - // Subtract time now to get the duration add this to the time provided by the client - clientDate := c.Request().Header.Get(ClientRequestDateHeader) - if len(clientDate) > 0 { - clientDateInt, err := strconv.ParseInt(clientDate, 10, 64) - if err == nil { - clientDateInt += int64(expiryDuration.Seconds()) - c.Response().Header().Set(SessionExpiresOnHeader, strconv.FormatInt(clientDateInt, 10)) - } - } - - return nil -} - -func (p *portalProxy) GetStratosUser(userGUID string) (*interfaces.ConnectedUser, error) { - log.Debug("GetStratosUser") - - // If configured for local users, use that instead - // This needs to be refactored - if interfaces.AuthEndpointTypes[p.Config.ConsoleConfig.AuthEndpointType] == interfaces.Local { - return p.getLocalUser(userGUID) - } - - return p.GetUAAUser(userGUID) -} - -func (p *portalProxy) GetUAAUser(userGUID string) (*interfaces.ConnectedUser, error) { - log.Debug("getUAAUser") - - // get the uaa token record - uaaTokenRecord, err := p.GetUAATokenRecord(userGUID) - if err != nil { - msg := "Unable to retrieve UAA token record." - log.Error(msg) - return nil, fmt.Errorf(msg) - } - - // get the scope out of the JWT token data - userTokenInfo, err := p.GetUserTokenInfo(uaaTokenRecord.AuthToken) - if err != nil { - msg := "Unable to find scope information in the UAA Auth Token: %s" - log.Errorf(msg, err) - return nil, fmt.Errorf(msg, err) - } - - // is the user a UAA admin? - uaaAdmin := strings.Contains(strings.Join(userTokenInfo.Scope, ""), p.Config.ConsoleConfig.ConsoleAdminScope) - - // add the uaa entry to the output - uaaEntry := &interfaces.ConnectedUser{ - GUID: userGUID, - Name: userTokenInfo.UserName, - Admin: uaaAdmin, - Scopes: userTokenInfo.Scope, - } - - return uaaEntry, nil -} - -func (p *portalProxy) getLocalUser(userGUID string) (*interfaces.ConnectedUser, error) { - localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(p.DatabaseConnectionPool) - if err != nil { - log.Errorf("Database error getting repo for Local users: %v", err) - return nil, err - } - - user, err := localUsersRepo.FindUser(userGUID) - if err != nil { - return nil, err - } - - var scopes []string - scopes = make([]string, 2) - scopes[0] = user.Scope - scopes[1] = "password.write" - - uaaAdmin := (user.Scope == p.Config.ConsoleConfig.ConsoleAdminScope) - uaaEntry := &interfaces.ConnectedUser{ - GUID: userGUID, - Name: user.Username, - Admin: uaaAdmin, - Scopes: scopes, - } - - return uaaEntry, nil -} - -func (p *portalProxy) GetCNSIUser(cnsiGUID string, userGUID string) (*interfaces.ConnectedUser, bool) { - user, _, ok := p.GetCNSIUserAndToken(cnsiGUID, userGUID) - return user, ok -} - -func (p *portalProxy) GetCNSIUserAndToken(cnsiGUID string, userGUID string) (*interfaces.ConnectedUser, *interfaces.TokenRecord, bool) { - log.Debug("GetCNSIUserAndToken") - - // get the uaa token record - cfTokenRecord, ok := p.GetCNSITokenRecord(cnsiGUID, userGUID) - if !ok { - msg := "Unable to retrieve CNSI token record." - log.Debug(msg) - return nil, nil, false - } - - cnsiUser, ok := p.GetCNSIUserFromToken(cnsiGUID, &cfTokenRecord) - - // If this is a system shared endpoint, then remove some metadata that should not be send back to other users - santizeInfoForSystemSharedTokenUser(cnsiUser, cfTokenRecord.SystemShared) - - return cnsiUser, &cfTokenRecord, ok -} - -func (p *portalProxy) GetCNSIUserFromToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) { - log.Debug("GetCNSIUserFromToken") - - // Custom handler for the Auth type available? - authProvider := p.GetAuthProvider(cfTokenRecord.AuthType) - if authProvider.UserInfo != nil { - return authProvider.UserInfo(cnsiGUID, cfTokenRecord) - } - - // Default - return p.GetCNSIUserFromOAuthToken(cnsiGUID, cfTokenRecord) -} - -func (p *portalProxy) GetCNSIUserFromBasicToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) { - return &interfaces.ConnectedUser{ - GUID: cfTokenRecord.RefreshToken, - Name: cfTokenRecord.RefreshToken, - }, true -} - -func (p *portalProxy) GetCNSIUserFromOAuthToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) { - var cnsiUser *interfaces.ConnectedUser - var scope = []string{} - - // get the scope out of the JWT token data - userTokenInfo, err := p.GetUserTokenInfo(cfTokenRecord.AuthToken) - if err != nil { - msg := "Unable to find scope information in the CNSI UAA Auth Token: %s" - log.Errorf(msg, err) - return nil, false - } - - // add the uaa entry to the output - cnsiUser = &interfaces.ConnectedUser{ - GUID: userTokenInfo.UserGUID, - Name: userTokenInfo.UserName, - Scopes: userTokenInfo.Scope, - } - scope = userTokenInfo.Scope - - // is the user an CF admin? - cnsiRecord, err := p.GetCNSIRecord(cnsiGUID) - if err != nil { - msg := "Unable to load CNSI record: %s" - log.Errorf(msg, err) - return nil, false - } - // TODO should be an extension point - if cnsiRecord.CNSIType == "cf" { - cnsiAdmin := strings.Contains(strings.Join(scope, ""), p.Config.CFAdminIdentifier) - cnsiUser.Admin = cnsiAdmin - } - - return cnsiUser, true -} - -func (p *portalProxy) DoAuthFlowRequest(cnsiRequest *interfaces.CNSIRequest, req *http.Request, authHandler interfaces.AuthHandlerFunc) (*http.Response, error) { - - // get a cnsi token record and a cnsi record - tokenRec, cnsi, err := p.getCNSIRequestRecords(cnsiRequest) - if err != nil { - return nil, fmt.Errorf("Unable to retrieve Endpoint records: %v", err) - } - return authHandler(tokenRec, cnsi) -} - -// Refresh the UAA Token for the user -func (p *portalProxy) RefreshUAAToken(userGUID string) (t interfaces.TokenRecord, err error) { - log.Debug("RefreshUAAToken") - - userToken, err := p.GetUAATokenRecord(userGUID) - if err != nil { - return t, fmt.Errorf("UAA Token info could not be found for user with GUID %s", userGUID) - } - - uaaRes, err := p.getUAATokenWithRefreshToken(p.Config.ConsoleConfig.SkipSSLValidation, userToken.RefreshToken, - p.Config.ConsoleConfig.ConsoleClient, p.Config.ConsoleConfig.ConsoleClientSecret, p.getUAAIdentityEndpoint(), "") - if err != nil { - return t, fmt.Errorf("UAA Token refresh request failed: %v", err) - } - - u, err := p.GetUserTokenInfo(uaaRes.AccessToken) - if err != nil { - return t, fmt.Errorf("Could not get user token info from access token") - } - - u.UserGUID = userGUID - - t, err = p.saveAuthToken(*u, uaaRes.AccessToken, uaaRes.RefreshToken) - if err != nil { - return t, fmt.Errorf("Couldn't save new UAA token: %v", err) - } - - return t, nil -} diff --git a/src/jetstream/auth_test.go b/src/jetstream/auth_test.go index 4b411a06a4..0b3d183154 100644 --- a/src/jetstream/auth_test.go +++ b/src/jetstream/auth_test.go @@ -14,7 +14,7 @@ import ( log "github.com/sirupsen/logrus" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/crypto" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" "github.com/labstack/echo" . "github.com/smartystreets/goconvey/convey" @@ -48,6 +48,11 @@ func TestLoginToUAA(t *testing.T) { pp.Config.ConsoleConfig.UAAEndpoint = uaaURL pp.Config.ConsoleConfig.SkipSSLValidation = true pp.Config.ConsoleConfig.AuthEndpointType = string(interfaces.Remote) + //Init the auth service + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.ConsoleConfig.AuthEndpointType]) + if err != nil { + log.Fatalf("Could not initialise auth service: %v", err) + } mock.ExpectQuery(selectAnyFromTokens). WillReturnRows(expectNoRows()) @@ -55,7 +60,7 @@ func TestLoginToUAA(t *testing.T) { mock.ExpectExec(insertIntoTokens). WillReturnResult(sqlmock.NewResult(1, 1)) - loginErr := pp.loginToUAA(ctx) + loginErr := pp.StratosAuthService.Login(ctx) Convey("Should not fail to login", func() { So(loginErr, ShouldBeNil) @@ -78,7 +83,7 @@ func TestLocalLogin(t *testing.T) { scope := "stratos.admin" //Hash the password - passwordHash, _ := HashPassword(password) + passwordHash, _ := crypto.HashPassword(password) //generate a user GUID userGUID := uuid.NewV4().String() @@ -94,6 +99,11 @@ func TestLocalLogin(t *testing.T) { defer db.Close() pp.Config.ConsoleConfig.AuthEndpointType = string(interfaces.Local) + //Init the auth service + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.ConsoleConfig.AuthEndpointType]) + if err != nil { + log.Fatalf("Could not initialise auth service: %v", err) + } rows := sqlmock.NewRows([]string{"user_guid"}).AddRow(userGUID) mock.ExpectQuery(findUserGUID).WithArgs(username).WillReturnRows(rows) @@ -107,7 +117,7 @@ func TestLocalLogin(t *testing.T) { //Expect exec to update local login time mock.ExpectExec(updateLastLoginTime).WillReturnResult(sqlmock.NewResult(1, 1)) - loginErr := pp.localLogin(ctx) + loginErr := pp.StratosAuthService.Login(ctx) Convey("Should not fail to login", func() { So(loginErr, ShouldBeNil) @@ -130,7 +140,7 @@ func TestLocalLoginWithBadCredentials(t *testing.T) { scope := "stratos.admin" //Hash the password - passwordHash, _ := HashPassword(password) + passwordHash, _ := crypto.HashPassword(password) //generate a user GUID userGUID := uuid.NewV4().String() @@ -147,6 +157,11 @@ func TestLocalLoginWithBadCredentials(t *testing.T) { defer db.Close() pp.Config.ConsoleConfig.AuthEndpointType = string(interfaces.Local) + //Init the auth service + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.ConsoleConfig.AuthEndpointType]) + if err != nil { + log.Fatalf("Could not initialise auth service: %v", err) + } rows := sqlmock.NewRows([]string{"user_guid"}).AddRow(userGUID) mock.ExpectQuery(findUserGUID).WithArgs(username).WillReturnRows(rows) @@ -154,7 +169,7 @@ func TestLocalLoginWithBadCredentials(t *testing.T) { rows = sqlmock.NewRows([]string{"password_hash"}).AddRow(passwordHash) mock.ExpectQuery(findPasswordHash).WithArgs(userGUID).WillReturnRows(rows) - loginErr := pp.localLogin(ctx) + loginErr := pp.StratosAuthService.Login(ctx) Convey("Should fail to login", func() { So(loginErr, ShouldNotBeNil) @@ -175,7 +190,7 @@ func TestLocalLoginWithNoAdminScope(t *testing.T) { password := "localuserpass" //Hash the password - passwordHash, _ := HashPassword(password) + passwordHash, _ := crypto.HashPassword(password) //generate a user GUID userGUID := uuid.NewV4().String() @@ -200,12 +215,17 @@ func TestLocalLoginWithNoAdminScope(t *testing.T) { pp.Config.ConsoleConfig = new(interfaces.ConsoleConfig) pp.Config.ConsoleConfig.LocalUserScope = "stratos.admin" pp.Config.ConsoleConfig.AuthEndpointType = string(interfaces.Local) + //Init the auth service + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.ConsoleConfig.AuthEndpointType]) + if err != nil { + log.Fatalf("Could not initialise auth service: %v", err) + } //The user trying to log in has a non-admin scope rows = sqlmock.NewRows([]string{"scope"}).AddRow(wrongScope) mock.ExpectQuery(findUserScope).WithArgs(userGUID).WillReturnRows(rows) - loginErr := pp.localLogin(ctx) + loginErr := pp.StratosAuthService.Login(ctx) Convey("Should fail to login", func() { So(loginErr, ShouldNotBeNil) @@ -242,8 +262,13 @@ func TestLoginToUAAWithBadCreds(t *testing.T) { pp.Config.ConsoleConfig.UAAEndpoint = uaaURL pp.Config.ConsoleConfig.SkipSSLValidation = true pp.Config.ConsoleConfig.AuthEndpointType = string(interfaces.Remote) + //Init the auth service + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.ConsoleConfig.AuthEndpointType]) + if err != nil { + log.Fatalf("Could not initialise auth service: %v", err) + } - err := pp.loginToUAA(ctx) + err = pp.StratosAuthService.Login(ctx) Convey("Login to UAA should fail", func() { So(err, ShouldNotBeNil) }) @@ -283,6 +308,11 @@ func TestLoginToUAAButCantSaveToken(t *testing.T) { pp.Config.ConsoleConfig.UAAEndpoint = uaaURL pp.Config.ConsoleConfig.SkipSSLValidation = true pp.Config.ConsoleConfig.AuthEndpointType = string(interfaces.Remote) + //Init the auth service + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.ConsoleConfig.AuthEndpointType]) + if err != nil { + log.Fatalf("Could not initialise auth service: %v", err) + } mock.ExpectQuery(selectAnyFromTokens). // WithArgs(mockUserGUID). @@ -292,7 +322,7 @@ func TestLoginToUAAButCantSaveToken(t *testing.T) { mock.ExpectExec(insertIntoTokens). WillReturnError(errors.New("Unknown Database Error")) - loginErr := pp.loginToUAA(ctx) + loginErr := pp.StratosAuthService.Login(ctx) Convey("Should not fail to login", func() { So(loginErr, ShouldNotBeNil) }) @@ -544,7 +574,18 @@ func TestLogout(t *testing.T) { res, _, ctx, pp, db, _ := setupHTTPTest(req) defer db.Close() - pp.logout(ctx) + pp.Config.ConsoleConfig.AuthEndpointType = string(interfaces.Local) + //Init the auth service + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.ConsoleConfig.AuthEndpointType]) + if err != nil { + log.Warnf("%v, defaulting to auth type: remote", err) + err = pp.InitStratosAuthService(interfaces.Remote) + if err != nil { + log.Fatalf("Could not initialise auth service: %v", err) + } + } + + pp.StratosAuthService.Logout(ctx) header := res.Header() setCookie := header.Get("Set-Cookie") diff --git a/src/jetstream/authcnsi.go b/src/jetstream/authcnsi.go new file mode 100644 index 0000000000..b8e00181e4 --- /dev/null +++ b/src/jetstream/authcnsi.go @@ -0,0 +1,485 @@ +package main + +import ( + + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/labstack/echo" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/cnsis" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/tokens" + +) + + +// CFAdminIdentifier - The scope that Cloud Foundry uses to convey administrative level perms +const CFAdminIdentifier = "cloud_controller.admin" + +// Start SSO flow for an Endpoint +func (p *portalProxy) ssoLoginToCNSI(c echo.Context) error { + log.Debug("ssoLoginToCNSI") + 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 { + log.Debug("loginToCNSI") + cnsiGUID := c.FormValue("cnsi_guid") + var systemSharedToken = false + + if len(cnsiGUID) == 0 { + return interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Missing target endpoint", + "Need Endpoint GUID passed as form param") + } + + systemSharedValue := c.FormValue("system_shared") + if len(systemSharedValue) > 0 { + systemSharedToken = systemSharedValue == "true" + } + + resp, err := p.DoLoginToCNSI(c, cnsiGUID, systemSharedToken) + if err != nil { + return err + } + + jsonString, err := json.Marshal(resp) + if err != nil { + return err + } + + c.Response().Header().Set("Content-Type", "application/json") + c.Response().Write(jsonString) + return nil +} + +func (p *portalProxy) DoLoginToCNSI(c echo.Context, cnsiGUID string, systemSharedToken bool) (*interfaces.LoginRes, error) { + + cnsiRecord, err := p.GetCNSIRecord(cnsiGUID) + if err != nil { + return nil, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Requested endpoint not registered", + "No Endpoint registered with GUID %s: %s", cnsiGUID, err) + } + + // Get the User ID since we save the CNSI token against the Console user guid, not the CNSI user guid so that we can look it up easily + userID, err := p.GetSessionStringValue(c, "user_id") + if err != nil { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Could not find correct session value") + } + + // Register as a system endpoint? + if systemSharedToken { + // User needs to be an admin + user, err := p.StratosAuthService.GetUser(userID) + if err != nil { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - could not check user") + } + + if !user.Admin { + return nil, echo.NewHTTPError(http.StatusUnauthorized, "Can not connect System Shared endpoint - user is not an administrator") + } + + // We are all good to go - change the userID, so we record this token against the system-shared user and not this specific user + // This is how we identify system-shared endpoint tokens + userID = tokens.SystemSharedUserGuid + } + + // Ask the endpoint type to connect + for _, plugin := range p.Plugins { + endpointPlugin, err := plugin.GetEndpointPlugin() + if err != nil { + // Plugin doesn't implement an Endpoint Plugin interface, skip + continue + } + + endpointType := endpointPlugin.GetType() + if cnsiRecord.CNSIType == endpointType { + tokenRecord, isAdmin, err := endpointPlugin.Connect(c, cnsiRecord, userID) + if err != nil { + if shadowError, ok := err.(interfaces.ErrHTTPShadow); ok { + return nil, shadowError + } + return nil, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Could not connect to the endpoint", + "Could not connect to the endpoint: %s", err) + } + + err = p.setCNSITokenRecord(cnsiGUID, userID, *tokenRecord) + if err != nil { + return nil, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Failed to save Token for endpoint", + "Error occurred: %s", err) + } + + // Validate the connection - some endpoints may want to validate that the connected endpoint + err = endpointPlugin.Validate(userID, cnsiRecord, *tokenRecord) + if err != nil { + // Clear the token + p.ClearCNSIToken(cnsiRecord, userID) + return nil, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Could not connect to the endpoint", + "Could not connect to the endpoint: %s", err) + } + + resp := &interfaces.LoginRes{ + Account: userID, + TokenExpiry: tokenRecord.TokenExpiry, + APIEndpoint: cnsiRecord.APIEndpoint, + Admin: isAdmin, + } + + cnsiUser, ok := p.GetCNSIUserFromToken(cnsiGUID, tokenRecord) + if ok { + // If this is a system shared endpoint, then remove some metadata that should be send back to other users + santizeInfoForSystemSharedTokenUser(cnsiUser, systemSharedToken) + resp.User = cnsiUser + } else { + // Need to record a user + resp.User = &interfaces.ConnectedUser{ + GUID: "Unknown", + Name: "Unknown", + Scopes: []string{"read"}, + Admin: true, + } + } + + return resp, nil + } + } + + return nil, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Endpoint connection not supported", + "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.TokenEndpoint) + if err != nil { + return fmt.Errorf("invalid authorization endpoint URL %s %s", cnsiInfo.TokenEndpoint, err) + } + + if uaaURL.String() == p.GetConfig().ConsoleConfig.UAAEndpoint.String() { // CNSI UAA server matches Console UAA server + uaaToken.LinkedGUID = uaaToken.TokenGUID + err = p.setCNSITokenRecord(theCNSIrecord.GUID, u.UserGUID, uaaToken) + + // Update the endpoint to indicate that SSO Login is okay + repo, dbErr := cnsis.NewPostgresCNSIRepository(p.DatabaseConnectionPool) + if dbErr == nil { + repo.Update(theCNSIrecord.GUID, true) + } + // Return error from the login + return err + } + return fmt.Errorf("the auto-registered endpoint UAA server does not match console UAA server") + } + log.Warn("Could not find current user UAA token") + return err +} + +func santizeInfoForSystemSharedTokenUser(cnsiUser *interfaces.ConnectedUser, isSysystemShared bool) { + if isSysystemShared { + cnsiUser.GUID = tokens.SystemSharedUserGuid + cnsiUser.Scopes = make([]string, 0) + cnsiUser.Name = "system_shared" + } +} + +func (p *portalProxy) ConnectOAuth2(c echo.Context, cnsiRecord interfaces.CNSIRecord) (*interfaces.TokenRecord, error) { + uaaRes, u, _, err := p.FetchOAuth2Token(cnsiRecord, c) + if err != nil { + return nil, err + } + tokenRecord := p.InitEndpointTokenRecord(u.TokenExpiry, uaaRes.AccessToken, uaaRes.RefreshToken, false) + return &tokenRecord, nil +} + +func (p *portalProxy) FetchOAuth2Token(cnsiRecord interfaces.CNSIRecord, c echo.Context) (*interfaces.UAAResponse, *interfaces.JWTUserTokenInfo, *interfaces.CNSIRecord, error) { + endpoint := cnsiRecord.AuthorizationEndpoint + + tokenEndpoint := fmt.Sprintf("%s/oauth/token", endpoint) + + uaaRes, u, err := p.login(c, cnsiRecord.SkipSSLValidation, cnsiRecord.ClientId, cnsiRecord.ClientSecret, tokenEndpoint) + + if err != nil { + if httpError, ok := err.(interfaces.ErrHTTPRequest); ok { + // Try and parse the Response into UAA error structure (p.login only handles UAA requests) + errMessage := "" + authError := &interfaces.UAAErrorResponse{} + if err := json.Unmarshal([]byte(httpError.Response), authError); err == nil { + errMessage = fmt.Sprintf(": %s", authError.ErrorDescription) + } + return nil, nil, nil, interfaces.NewHTTPShadowError( + httpError.Status, + fmt.Sprintf("Could not connect to the endpoint%s", errMessage), + "Could not connect to the endpoint: %s", err) + } + + return nil, nil, nil, interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Login failed", + "Login failed: %v", err) + } + return uaaRes, u, &cnsiRecord, nil +} + +func (p *portalProxy) logoutOfCNSI(c echo.Context) error { + log.Debug("logoutOfCNSI") + + cnsiGUID := c.FormValue("cnsi_guid") + + if len(cnsiGUID) == 0 { + return interfaces.NewHTTPShadowError( + http.StatusBadRequest, + "Missing target endpoint", + "Need CNSI GUID passed as form param") + } + + userGUID, err := p.GetSessionStringValue(c, "user_id") + if err != nil { + return fmt.Errorf("Could not find correct session value: %s", err) + } + + cnsiRecord, err := p.GetCNSIRecord(cnsiGUID) + if err != nil { + return fmt.Errorf("Unable to load CNSI record: %s", err) + } + + // Get the existing token to see if it is connected as a system shared endpoint + tr, ok := p.GetCNSITokenRecord(cnsiGUID, userGUID) + if ok && tr.SystemShared { + // User needs to be an admin + user, err := p.StratosAuthService.GetUser(userGUID) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "Can not disconnect System Shared endpoint - could not check user") + } + + if !user.Admin { + return echo.NewHTTPError(http.StatusUnauthorized, "Can not disconnect System Shared endpoint - user is not an administrator") + } + userGUID = tokens.SystemSharedUserGuid + } + + // Clear the token + return p.ClearCNSIToken(cnsiRecord, userGUID) +} + +func (p *portalProxy) DoAuthFlowRequest(cnsiRequest *interfaces.CNSIRequest, req *http.Request, authHandler interfaces.AuthHandlerFunc) (*http.Response, error) { + + // get a cnsi token record and a cnsi record + tokenRec, cnsi, err := p.getCNSIRequestRecords(cnsiRequest) + if err != nil { + return nil, fmt.Errorf("Unable to retrieve Endpoint records: %v", err) + } + return authHandler(tokenRec, cnsi) +} + +// Clear the CNSI token +func (p *portalProxy) ClearCNSIToken(cnsiRecord interfaces.CNSIRecord, userGUID string) error { + // If cnsi is cf AND cf is auto-register only clear the entry + p.Config.AutoRegisterCFUrl = strings.TrimRight(p.Config.AutoRegisterCFUrl, "/") + if cnsiRecord.CNSIType == "cf" && p.GetConfig().AutoRegisterCFUrl == cnsiRecord.APIEndpoint.String() { + log.Debug("Setting token record as disconnected") + + tokenRecord := p.InitEndpointTokenRecord(0, "cleared_token", "cleared_token", true) + if err := p.setCNSITokenRecord(cnsiRecord.GUID, userGUID, tokenRecord); err != nil { + return fmt.Errorf("Unable to clear token: %s", err) + } + } else { + log.Debug("Deleting Token") + if err := p.deleteCNSIToken(cnsiRecord.GUID, userGUID); err != nil { + return fmt.Errorf("Unable to delete token: %s", err) + } + } + + return nil +} + +func (p *portalProxy) GetCNSIUser(cnsiGUID string, userGUID string) (*interfaces.ConnectedUser, bool) { + user, _, ok := p.GetCNSIUserAndToken(cnsiGUID, userGUID) + return user, ok +} + +func (p *portalProxy) GetCNSIUserAndToken(cnsiGUID string, userGUID string) (*interfaces.ConnectedUser, *interfaces.TokenRecord, bool) { + log.Debug("GetCNSIUserAndToken") + + // get the uaa token record + cfTokenRecord, ok := p.GetCNSITokenRecord(cnsiGUID, userGUID) + if !ok { + msg := "Unable to retrieve CNSI token record." + log.Debug(msg) + return nil, nil, false + } + + cnsiUser, ok := p.GetCNSIUserFromToken(cnsiGUID, &cfTokenRecord) + + // If this is a system shared endpoint, then remove some metadata that should not be send back to other users + santizeInfoForSystemSharedTokenUser(cnsiUser, cfTokenRecord.SystemShared) + + return cnsiUser, &cfTokenRecord, ok +} + +func (p *portalProxy) GetCNSIUserFromToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) { + log.Debug("GetCNSIUserFromToken") + + // Custom handler for the Auth type available? + authProvider := p.GetAuthProvider(cfTokenRecord.AuthType) + if authProvider.UserInfo != nil { + return authProvider.UserInfo(cnsiGUID, cfTokenRecord) + } + + // Default + return p.GetCNSIUserFromOAuthToken(cnsiGUID, cfTokenRecord) +} + +func (p *portalProxy) GetCNSIUserFromBasicToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) { + return &interfaces.ConnectedUser{ + GUID: cfTokenRecord.RefreshToken, + Name: cfTokenRecord.RefreshToken, + }, true +} + +func (p *portalProxy) GetCNSIUserFromOAuthToken(cnsiGUID string, cfTokenRecord *interfaces.TokenRecord) (*interfaces.ConnectedUser, bool) { + var cnsiUser *interfaces.ConnectedUser + var scope = []string{} + + // get the scope out of the JWT token data + userTokenInfo, err := p.GetUserTokenInfo(cfTokenRecord.AuthToken) + if err != nil { + msg := "Unable to find scope information in the CNSI UAA Auth Token: %s" + log.Errorf(msg, err) + return nil, false + } + + // add the uaa entry to the output + cnsiUser = &interfaces.ConnectedUser{ + GUID: userTokenInfo.UserGUID, + Name: userTokenInfo.UserName, + Scopes: userTokenInfo.Scope, + } + scope = userTokenInfo.Scope + + // is the user an CF admin? + cnsiRecord, err := p.GetCNSIRecord(cnsiGUID) + if err != nil { + msg := "Unable to load CNSI record: %s" + log.Errorf(msg, err) + return nil, false + } + // TODO should be an extension point + if cnsiRecord.CNSIType == "cf" { + cnsiAdmin := strings.Contains(strings.Join(scope, ""), p.Config.CFAdminIdentifier) + cnsiUser.Admin = cnsiAdmin + } + + return cnsiUser, true +} + +// Helper to initialize a token record using the specified parameters +func (p *portalProxy) InitEndpointTokenRecord(expiry int64, authTok string, refreshTok string, disconnect bool) interfaces.TokenRecord { + tokenRecord := interfaces.TokenRecord{ + AuthToken: authTok, + RefreshToken: refreshTok, + TokenExpiry: expiry, + Disconnected: disconnect, + AuthType: interfaces.AuthTypeOAuth2, + } + + return tokenRecord +} + +func (p *portalProxy) deleteCNSIToken(cnsiID string, userGUID string) error { + log.Debug("deleteCNSIToken") + + err := p.unsetCNSITokenRecord(cnsiID, userGUID) + if err != nil { + log.Errorf("%v", err) + return err + } + + return nil +} \ No newline at end of file diff --git a/src/jetstream/authlocal.go b/src/jetstream/authlocal.go new file mode 100644 index 0000000000..d466d1d2fc --- /dev/null +++ b/src/jetstream/authlocal.go @@ -0,0 +1,241 @@ +package main + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "math" + "net/http" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/labstack/echo" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/localusers" +) + +//More fields will be moved into here as global portalProxy struct is phased out +type localAuth struct { + databaseConnectionPool *sql.DB + localUserScope string + consoleAdminScope string + p *portalProxy +} + +//Login provides Local-auth specific Stratos login +func (a *localAuth) Login(c echo.Context) error { + + //This check will remain in until auth is factored down into its own package + if interfaces.AuthEndpointTypes[a.p.Config.ConsoleConfig.AuthEndpointType] != interfaces.Local { + err := interfaces.NewHTTPShadowError( + http.StatusNotFound, + "Local Login is not enabled", + "Local Login is not enabled") + return err + } + + //Perform the login and fetch session values if successful + userGUID, username, err := a.localLogin(c) + + if err != nil { + //Login failed, return response. + errMessage := err.Error() + err := interfaces.NewHTTPShadowError( + http.StatusUnauthorized, + errMessage, + "Login failed: %v", err) + return err + } + + err = a.generateLoginSuccessResponse(c, userGUID, username) + + return err +} + +//Logout provides Local-auth specific Stratos login +func (a *localAuth) Logout(c echo.Context) error { + return a.logout(c) +} + +//GetUsername gets the user name for the specified local user +func (a *localAuth) GetUsername(userid string) (string, error) { + log.Debug("GetUsername") + + localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(a.databaseConnectionPool) + if err != nil { + log.Errorf("Database error getting repo for Local users: %v", err) + return "", err + } + + localUser, err := localUsersRepo.FindUser(userid) + if err != nil { + log.Errorf("Error fetching username for local user %s: %v", userid, err) + return "", err + } + + return localUser.Username, nil +} + +//GetUser gets the user guid for the specified local user +func (a *localAuth) GetUser(userGUID string) (*interfaces.ConnectedUser, error) { + log.Debug("GetUser") + + localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(a.databaseConnectionPool) + if err != nil { + log.Errorf("Database error getting repo for Local users: %v", err) + return nil, err + } + + user, err := localUsersRepo.FindUser(userGUID) + if err != nil { + return nil, err + } + + uaaAdmin := (user.Scope == a.p.Config.ConsoleConfig.ConsoleAdminScope) + + var scopes []string + scopes = make([]string, 2) + scopes[0] = user.Scope + scopes[1] = "password.write" + + connectdUser := &interfaces.ConnectedUser{ + GUID: userGUID, + Name: user.Username, + Admin: uaaAdmin, + Scopes: scopes, + } + + return connectdUser, nil +} + +//VerifySession verifies the session the specified local user, currently just verifies user exists +func (a *localAuth) VerifySession(c echo.Context, sessionUser string, sessionExpireTime int64) error { + localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(a.databaseConnectionPool) + if err != nil { + log.Errorf("Database error getting repo for Local users: %v", err) + return err + } + + _, err = localUsersRepo.FindPasswordHash(sessionUser) + return err +} + +//localLogin verifies local user credentials against our DB +func (a *localAuth) localLogin(c echo.Context) (string, string, error) { + log.Debug("doLocalLogin") + + username := c.FormValue("username") + password := c.FormValue("password") + + if len(username) == 0 || len(password) == 0 { + return "", username, errors.New("Needs usernameand password") + } + + localUsersRepo, err := localusers.NewPgsqlLocalUsersRepository(a.databaseConnectionPool) + if err != nil { + log.Errorf("Database error getting repo for Local users: %v", err) + return "", username, err + } + + var scopeOK bool + var hash []byte + var authError error + var localUserScope string + + // Get the GUID for the specified user + guid, err := localUsersRepo.FindUserGUID(username) + if err != nil { + return guid, username, fmt.Errorf("Access Denied - Invalid username/password credentials") + } + + //Attempt to find the password has for the given user + if hash, authError = localUsersRepo.FindPasswordHash(guid); authError != nil { + authError = fmt.Errorf("Access Denied - Invalid username/password credentials") + //Check the password hash + } else if authError = crypto.CheckPasswordHash(password, hash); authError != nil { + authError = fmt.Errorf("Access Denied - Invalid username/password credentials") + } else { + //Ensure the local user has some kind of admin role configured and we check for it here + localUserScope, authError = localUsersRepo.FindUserScope(guid) + scopeOK = strings.Contains(localUserScope, a.localUserScope) + if (authError != nil) || (!scopeOK) { + authError = fmt.Errorf("Access Denied - User scope invalid") + } else { + //Update the last login time here if login was successful + loginTime := time.Now() + if updateLoginTimeErr := localUsersRepo.UpdateLastLoginTime(guid, loginTime); updateLoginTimeErr != nil { + log.Error(updateLoginTimeErr) + log.Errorf("Failed to update last login time for user: %s", guid) + } + } + } + return guid, username, authError +} + +//generateLoginSuccessResponse +func (a *localAuth) generateLoginSuccessResponse(c echo.Context, userGUID string, username string) error { + log.Debug("generateLoginResponse") + + var err error + var expiry int64 + expiry = math.MaxInt64 + + sessionValues := make(map[string]interface{}) + sessionValues["user_id"] = userGUID + sessionValues["exp"] = expiry + + // Ensure that login disregards cookies from the request + req := c.Request() + req.Header.Set("Cookie", "") + if err = a.p.setSessionValues(c, sessionValues); err != nil { + return err + } + + //Makes sure the client gets the right session expiry time + if err = a.p.handleSessionExpiryHeader(c); err != nil { + return err + } + + resp := &interfaces.LoginRes{ + Account: username, + TokenExpiry: expiry, + APIEndpoint: nil, + Admin: true, + } + + if jsonString, err := json.Marshal(resp); err == nil { + // Add XSRF Token + a.p.ensureXSRFToken(c) + c.Response().Header().Set("Content-Type", "application/json") + c.Response().Write(jsonString) + } + + return err +} + +//logout +func (a *localAuth) logout(c echo.Context) error { + log.Debug("logout") + + a.p.removeEmptyCookie(c) + + // Remove the XSRF Token from the session + a.p.unsetSessionValue(c, XSRFTokenSessionName) + + err := a.p.clearSession(c) + if err != nil { + log.Errorf("Unable to clear session: %v", err) + } + + // Send JSON document + resp := &LogoutResponse{ + IsSSO: a.p.Config.SSOLogin, + } + + return c.JSON(http.StatusOK, resp) +} diff --git a/src/jetstream/authuaa.go b/src/jetstream/authuaa.go new file mode 100644 index 0000000000..f5d7ee3113 --- /dev/null +++ b/src/jetstream/authuaa.go @@ -0,0 +1,611 @@ +package main + +import ( + "database/sql" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/labstack/echo" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/tokens" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/stringutils" +) + +// UAAAdminIdentifier - The identifier that UAA uses to convey administrative level perms +const UAAAdminIdentifier = "stratos.admin" + +//More fields will be moved into here as global portalProxy struct is phased out +type uaaAuth struct { + databaseConnectionPool *sql.DB + p *portalProxy + skipSSLValidation bool +} + +//Login provides UAA-auth specific Stratos login +func (a *uaaAuth) Login(c echo.Context) error { + log.Debug("UAA Login") + //This check will remain in until auth is factored down into its own package + if interfaces.AuthEndpointTypes[a.p.Config.ConsoleConfig.AuthEndpointType] != interfaces.Remote { + err := interfaces.NewHTTPShadowError( + http.StatusNotFound, + "UAA Login is not enabled", + "UAA Login is not enabled") + return err + } + + resp, err := a.p.loginToUAA(c) + if err != nil { + return err + } + + jsonString, err := json.Marshal(resp) + if err != nil { + return err + } + + // Add XSRF Token + a.p.ensureXSRFToken(c) + + c.Response().Header().Set("Content-Type", "application/json") + c.Response().Write(jsonString) + + return nil +} + +//Logout provides UAA-auth specific Stratos login +func (a *uaaAuth) Logout(c echo.Context) error { + return a.logout(c) +} + +//GetUsername gets the user name for the specified UAA user +func (a *uaaAuth) GetUsername(userid string) (string, error) { + tr, err := a.p.GetUAATokenRecord(userid) + if err != nil { + return "", err + } + u, userTokenErr := a.p.GetUserTokenInfo(tr.AuthToken) + if userTokenErr != nil { + return "", userTokenErr + } + return u.UserName, nil +} + +//GetUser gets the user guid for the specified UAA user +func (a *uaaAuth) GetUser(userGUID string) (*interfaces.ConnectedUser, error) { + log.Debug("GetUser") + + // get the uaa token record + uaaTokenRecord, err := a.p.GetUAATokenRecord(userGUID) + if err != nil { + msg := "Unable to retrieve UAA token record." + log.Error(msg) + return nil, fmt.Errorf(msg) + } + + // get the scope out of the JWT token data + userTokenInfo, err := a.p.GetUserTokenInfo(uaaTokenRecord.AuthToken) + if err != nil { + msg := "Unable to find scope information in the UAA Auth Token: %s" + log.Errorf(msg, err) + return nil, fmt.Errorf(msg, err) + } + + // is the user a UAA admin? + uaaAdmin := strings.Contains(strings.Join(userTokenInfo.Scope, ""), a.p.Config.ConsoleConfig.ConsoleAdminScope) + + // add the uaa entry to the output + uaaEntry := &interfaces.ConnectedUser{ + GUID: userGUID, + Name: userTokenInfo.UserName, + Admin: uaaAdmin, + Scopes: userTokenInfo.Scope, + } + + return uaaEntry, nil + +} + +//VerifySession verifies the session the specified UAA user and refreshes the token if necessary +func (a *uaaAuth) VerifySession(c echo.Context, sessionUser string, sessionExpireTime int64) error { + + tr, err := a.p.GetUAATokenRecord(sessionUser) + + if err != nil { + msg := fmt.Sprintf("Unable to find UAA Token: %s", err) + log.Error(msg, err) + return echo.NewHTTPError(http.StatusForbidden, msg) + } + + // Check if UAA token has expired + if time.Now().After(time.Unix(sessionExpireTime, 0)) { + + // UAA Token has expired, refresh the token, if that fails, fail the request + uaaRes, tokenErr := a.p.getUAATokenWithRefreshToken(a.p.Config.ConsoleConfig.SkipSSLValidation, tr.RefreshToken, a.p.Config.ConsoleConfig.ConsoleClient, a.p.Config.ConsoleConfig.ConsoleClientSecret, a.p.getUAAIdentityEndpoint(), "") + if tokenErr != nil { + msg := "Could not refresh UAA token" + log.Error(msg, tokenErr) + return echo.NewHTTPError(http.StatusForbidden, msg) + } + + u, userTokenErr := a.p.GetUserTokenInfo(uaaRes.AccessToken) + if userTokenErr != nil { + return userTokenErr + } + + if _, err = a.p.saveAuthToken(*u, uaaRes.AccessToken, uaaRes.RefreshToken); err != nil { + return err + } + sessionValues := make(map[string]interface{}) + sessionValues["user_id"] = u.UserGUID + sessionValues["exp"] = u.TokenExpiry + + if err = a.p.setSessionValues(c, sessionValues); err != nil { + return err + } + } else { + // Still need to extend the expires_on of the Session + if err = a.p.setSessionValues(c, nil); err != nil { + return err + } + } + + return nil +} + +//logout performs the underlying logout from the UAA endpoint +func (a *uaaAuth) logout(c echo.Context) error { + log.Debug("logout") + + a.p.removeEmptyCookie(c) + + // Remove the XSRF Token from the session + a.p.unsetSessionValue(c, XSRFTokenSessionName) + + err := a.p.clearSession(c) + if err != nil { + log.Errorf("Unable to clear session: %v", err) + } + + // Send JSON document + resp := &LogoutResponse{ + IsSSO: a.p.Config.SSOLogin, + } + + return c.JSON(http.StatusOK, resp) +} + +//loginToUAA performs the underlying login to the UAA endpoint +func (p *portalProxy) loginToUAA(c echo.Context) (*interfaces.LoginRes, error) { + log.Debug("loginToUAA") + uaaRes, u, err := p.login(c, p.Config.ConsoleConfig.SkipSSLValidation, p.Config.ConsoleConfig.ConsoleClient, p.Config.ConsoleConfig.ConsoleClientSecret, p.getUAAIdentityEndpoint()) + var resp *interfaces.LoginRes + if err != nil { + // Check the Error + errMessage := "Access Denied" + if httpError, ok := err.(interfaces.ErrHTTPRequest); ok { + // Try and parse the Response into UAA error structure + authError := &interfaces.UAAErrorResponse{} + if err := json.Unmarshal([]byte(httpError.Response), authError); err == nil { + errMessage = authError.ErrorDescription + } + } + + err = interfaces.NewHTTPShadowError( + http.StatusUnauthorized, + errMessage, + "UAA Login failed: %s: %v", errMessage, err) + + } else { //Login succes + + sessionValues := make(map[string]interface{}) + sessionValues["user_id"] = u.UserGUID + sessionValues["exp"] = u.TokenExpiry + + // Ensure that login disregards cookies from the request + req := c.Request() + req.Header.Set("Cookie", "") + if err = p.setSessionValues(c, sessionValues); err != nil { + return nil, err + } + + err = p.handleSessionExpiryHeader(c) + if err != nil { + return nil, err + } + + _, err = p.saveAuthToken(*u, uaaRes.AccessToken, uaaRes.RefreshToken) + if err != nil { + return nil, err + } + + err = p.ExecuteLoginHooks(c) + if err != nil { + log.Warnf("Login hooks failed: %v", err) + } + + uaaAdmin := strings.Contains(uaaRes.Scope, p.Config.ConsoleConfig.ConsoleAdminScope) + resp = &interfaces.LoginRes{ + Account: u.UserName, + TokenExpiry: u.TokenExpiry, + APIEndpoint: nil, + Admin: uaaAdmin, + } + } + return resp, err +} + +//getUAAIdentityEndpoint gets the token endpoint for the UAA +func (p *portalProxy) getUAAIdentityEndpoint() string { + log.Debug("getUAAIdentityEndpoint") + return fmt.Sprintf("%s/oauth/token", p.Config.ConsoleConfig.UAAEndpoint) +} + +//saveAuthToken stores the UAA token for a given user +func (p *portalProxy) saveAuthToken(u interfaces.JWTUserTokenInfo, authTok string, refreshTok string) (interfaces.TokenRecord, error) { + log.Debug("saveAuthToken") + + key := u.UserGUID + tokenRecord := interfaces.TokenRecord{ + AuthToken: authTok, + RefreshToken: refreshTok, + TokenExpiry: u.TokenExpiry, + AuthType: interfaces.AuthTypeOAuth2, + } + + err := p.setUAATokenRecord(key, tokenRecord) + if err != nil { + return tokenRecord, err + } + + return tokenRecord, nil +} + +//setUAATokenRecord saves the uaa token for the given user, to our store +func (p *portalProxy) setUAATokenRecord(key string, t interfaces.TokenRecord) error { + log.Debug("setUAATokenRecord") + + tokenRepo, err := tokens.NewPgsqlTokenRepository(p.DatabaseConnectionPool) + if err != nil { + return fmt.Errorf("Database error getting repo for UAA token: %v", err) + } + + err = tokenRepo.SaveAuthToken(key, t, p.Config.EncryptionKeyInBytes) + if err != nil { + return fmt.Errorf("Database error saving UAA token: %v", err) + } + + return nil +} + +//RefreshUAALogin refreshes the UAA login and optionally stores the new token +func (p *portalProxy) RefreshUAALogin(username, password string, store bool) error { + log.Debug("RefreshUAALogin") + uaaRes, err := p.getUAATokenWithCreds(p.Config.ConsoleConfig.SkipSSLValidation, username, password, p.Config.ConsoleConfig.ConsoleClient, p.Config.ConsoleConfig.ConsoleClientSecret, p.getUAAIdentityEndpoint()) + if err != nil { + return err + } + + u, err := p.GetUserTokenInfo(uaaRes.AccessToken) + if err != nil { + return err + } + + if store { + _, err = p.saveAuthToken(*u, uaaRes.AccessToken, uaaRes.RefreshToken) + if err != nil { + return err + } + } + + return nil +} + +//getUAATokenWithAuthorizationCode +func (p *portalProxy) getUAATokenWithAuthorizationCode(skipSSLValidation bool, code, client, clientSecret, authEndpoint string, state string, cnsiGUID string) (*interfaces.UAAResponse, error) { + log.Debug("getUAATokenWithAuthorizationCode") + + 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, state, cnsiGUID)) + + return p.getUAAToken(body, skipSSLValidation, client, clientSecret, authEndpoint) +} + +//getUAATokenWithCreds +func (p *portalProxy) getUAATokenWithCreds(skipSSLValidation bool, username, password, client, clientSecret, authEndpoint string) (*interfaces.UAAResponse, error) { + log.Debug("getUAATokenWithCreds") + + body := url.Values{} + body.Set("grant_type", "password") + body.Set("username", username) + body.Set("password", password) + body.Set("response_type", "token") + + return p.getUAAToken(body, skipSSLValidation, client, clientSecret, authEndpoint) +} + +//getUAATokenWithRefreshToken +func (p *portalProxy) getUAATokenWithRefreshToken(skipSSLValidation bool, refreshToken, client, clientSecret, authEndpoint string, scopes string) (*interfaces.UAAResponse, error) { + log.Debug("getUAATokenWithRefreshToken") + + body := url.Values{} + body.Set("grant_type", "refresh_token") + body.Set("refresh_token", refreshToken) + body.Set("response_type", "token") + + if len(scopes) > 0 { + body.Set("scope", scopes) + } + + return p.getUAAToken(body, skipSSLValidation, client, clientSecret, authEndpoint) +} + +//getUAAToken +func (p *portalProxy) getUAAToken(body url.Values, skipSSLValidation bool, client, clientSecret, authEndpoint string) (*interfaces.UAAResponse, error) { + log.WithField("authEndpoint", authEndpoint).Debug("getUAAToken") + req, err := http.NewRequest("POST", authEndpoint, strings.NewReader(body.Encode())) + if err != nil { + msg := "Failed to create request for UAA: %v" + log.Errorf(msg, err) + return nil, fmt.Errorf(msg, err) + } + + req.SetBasicAuth(client, clientSecret) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm) + + var h = p.GetHttpClientForRequest(req, skipSSLValidation) + res, err := h.Do(req) + if err != nil || res.StatusCode != http.StatusOK { + log.Errorf("Error performing http request - response: %v, error: %v", res, err) + return nil, interfaces.LogHTTPError(res, err) + } + + defer res.Body.Close() + + var response interfaces.UAAResponse + + dec := json.NewDecoder(res.Body) + if err = dec.Decode(&response); err != nil { + log.Errorf("Error decoding response: %v", err) + return nil, fmt.Errorf("getUAAToken Decode: %s", err) + } + + return &response, nil +} + +//GetUAATokenRecord fetched the uaa token for the given user, from our store +func (p *portalProxy) GetUAATokenRecord(userGUID string) (interfaces.TokenRecord, error) { + log.Debug("GetUAATokenRecord") + + tokenRepo, err := tokens.NewPgsqlTokenRepository(p.DatabaseConnectionPool) + if err != nil { + log.Errorf("Database error getting repo for UAA token: %v", err) + return interfaces.TokenRecord{}, err + } + + tr, err := tokenRepo.FindAuthToken(userGUID, p.Config.EncryptionKeyInBytes) + if err != nil { + log.Errorf("Database error finding UAA token: %v", err) + return interfaces.TokenRecord{}, err + } + + return tr, nil +} + +//RefreshUAAToken refreshes the UAA Token for the user using the refresh token, then updates our store +func (p *portalProxy) RefreshUAAToken(userGUID string) (t interfaces.TokenRecord, err error) { + log.Debug("RefreshUAAToken") + + userToken, err := p.GetUAATokenRecord(userGUID) + if err != nil { + return t, fmt.Errorf("UAA Token info could not be found for user with GUID %s", userGUID) + } + + uaaRes, err := p.getUAATokenWithRefreshToken(p.Config.ConsoleConfig.SkipSSLValidation, userToken.RefreshToken, + p.Config.ConsoleConfig.ConsoleClient, p.Config.ConsoleConfig.ConsoleClientSecret, p.getUAAIdentityEndpoint(), "") + if err != nil { + err = fmt.Errorf("UAA Token refresh request failed: %v", err) + } else { + u, err := p.GetUserTokenInfo(uaaRes.AccessToken) + if err != nil { + return t, fmt.Errorf("Could not get user token info from access token") + } + + u.UserGUID = userGUID + + t, err = p.saveAuthToken(*u, uaaRes.AccessToken, uaaRes.RefreshToken) + if err != nil { + return t, fmt.Errorf("Couldn't save new UAA token: %v", err) + } + } + return t, err +} + +//SSO +//SSO Login will be refactored at a later date + +// ssoLoginToUAA is a callback invoked after the UAA login flow has completed and during logout +// We use a single callback so this can be whitelisted in the client +func (p *portalProxy) ssoLoginToUAA(c echo.Context) error { + 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 + } + + // 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") + } + _, err := p.loginToUAA(c) + if err != nil { + // Send error as query string param + msg := err.Error() + if httpError, ok := err.(interfaces.ErrHTTPShadow); ok { + msg = httpError.UserFacingError + } + if httpError, ok := err.(interfaces.ErrHTTPRequest); ok { + msg = httpError.Response + } + state = fmt.Sprintf("%s/login?SSO_Message=%s", state, url.QueryEscape(msg)) + } + + return c.Redirect(http.StatusTemporaryRedirect, state) +} + +//ssoLogoutOfUAA performs SSO logout from the UAA +func (p *portalProxy) ssoLogoutOfUAA(c echo.Context) error { + if !p.Config.SSOLogin { + err := interfaces.NewHTTPShadowError( + http.StatusNotFound, + "SSO Login is not enabled", + "SSO Login is not enabled") + return err + } + + 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 + } + + // 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", ""))) + } else { + redirectURL = "/login?SSO_Message=You+have+been+logged+out" + } + return c.Redirect(http.StatusTemporaryRedirect, redirectURL) +} + +//hasSSOOption returns whether or not SSO is enabled +func (p *portalProxy) hasSSOOption(option string) bool { + // Remove all spaces + opts := stringutils.RemoveSpaces(p.Config.SSOOptions) + + // Split based on ',' + options := strings.Split(opts, ",") + return stringutils.ArrayContainsString(options, option) +} + +//initSSOlogin performs SSO Login via UAA +func (p *portalProxy) initSSOlogin(c echo.Context) error { + if !p.Config.SSOLogin { + err := interfaces.NewHTTPShadowError( + http.StatusNotFound, + "SSO Login is not enabled", + "SSO Login is not enabled") + return err + } + + state := c.QueryParam("state") + if len(state) == 0 { + err := interfaces.NewHTTPShadowError( + http.StatusUnauthorized, + "SSO Login: Redirect state parameter missing", + "SSO Login: Redirect state parameter missing") + return err + } + + if !safeSSORedirectState(state, p.Config.SSOWhiteList) { + err := interfaces.NewHTTPShadowError( + http.StatusUnauthorized, + "SSO Login: Disallowed redirect state", + "SSO Login: Disallowed redirect state") + return err + } + + redirectURL := fmt.Sprintf("%s/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s", p.Config.ConsoleConfig.AuthorizationEndpoint, p.Config.ConsoleConfig.ConsoleClient, url.QueryEscape(getSSORedirectURI(state, state, ""))) + c.Redirect(http.StatusTemporaryRedirect, redirectURL) + return nil +} + +func safeSSORedirectState(state string, whiteListStr string) bool { + if len(whiteListStr) == 0 { + return true + } + + whiteList := strings.Split(whiteListStr, ",") + if len(whiteList) == 0 { + return true + } + + for _, n := range whiteList { + if stringutils.CompareURL(state, n) { + return true + } + } + return false +} + +//getSSORedirectURI gets the SSO redirect uri for the given endpoint and state +func getSSORedirectURI(base string, state string, endpointGUID string) string { + baseURL, _ := url.Parse(base) + baseURL.Path = "" + baseURL.RawQuery = "" + baseURLString := strings.TrimRight(baseURL.String(), "?") + + 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 +} + +//HTTP Basic + +//fetchHTTPBasicToken currently unused? +func (p *portalProxy) fetchHTTPBasicToken(cnsiRecord interfaces.CNSIRecord, c echo.Context) (*interfaces.UAAResponse, *interfaces.JWTUserTokenInfo, *interfaces.CNSIRecord, error) { + + uaaRes, u, err := p.loginHTTPBasic(c) + + if err != nil { + return nil, nil, nil, interfaces.NewHTTPShadowError( + http.StatusUnauthorized, + "Login failed", + "Login failed: %v", err) + } + return uaaRes, u, &cnsiRecord, nil +} + +//fetchHTTPBasicToken currently unused? +func (p *portalProxy) loginHTTPBasic(c echo.Context) (uaaRes *interfaces.UAAResponse, u *interfaces.JWTUserTokenInfo, err error) { + log.Debug("login") + username := c.FormValue("username") + password := c.FormValue("password") + + if len(username) == 0 || len(password) == 0 { + return uaaRes, u, errors.New("Needs username and password") + } + + authString := fmt.Sprintf("%s:%s", username, password) + base64EncodedAuthString := base64.StdEncoding.EncodeToString([]byte(authString)) + + uaaRes.AccessToken = fmt.Sprintf("Basic %s", base64EncodedAuthString) + return uaaRes, u, nil +} diff --git a/src/jetstream/repository/crypto/aes.go b/src/jetstream/crypto/aes.go similarity index 100% rename from src/jetstream/repository/crypto/aes.go rename to src/jetstream/crypto/aes.go diff --git a/src/jetstream/repository/crypto/crypto.go b/src/jetstream/crypto/crypto.go similarity index 57% rename from src/jetstream/repository/crypto/crypto.go rename to src/jetstream/crypto/crypto.go index 4dea85b714..2400550cd2 100644 --- a/src/jetstream/repository/crypto/crypto.go +++ b/src/jetstream/crypto/crypto.go @@ -1,11 +1,42 @@ package crypto import ( + "crypto/rand" "fmt" log "github.com/sirupsen/logrus" + + "golang.org/x/crypto/bcrypt" ) +// See: https://github.com/gorilla/csrf/blob/a8abe8abf66db8f4a9750d76ba95b4021a354757/helpers.go +// generateRandomBytes returns securely generated random bytes. +// It will return an error if the system's secure random number generator fails to function correctly. +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + // err == nil only if len(b) == n + if err != nil { + return nil, err + } + + return b, nil + +} + +//HashPassword accepts a plaintext password string and generates a salted hash +func HashPassword(password string) ([]byte, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return bytes, err +} + +//CheckPasswordHash accepts a bcrypt salted hash and plaintext password. +//It verifies the password against the salted hash +func CheckPasswordHash(password string, hash []byte) error { + err := bcrypt.CompareHashAndPassword(hash, []byte(password)) + return err +} + // Note: // When it's time to store the encrypted token in PostgreSQL, it's gets a bit // hairy. The encrypted token is binary data, not really text data, which diff --git a/src/jetstream/repository/crypto/crypto_test.go b/src/jetstream/crypto/crypto_test.go similarity index 100% rename from src/jetstream/repository/crypto/crypto_test.go rename to src/jetstream/crypto/crypto_test.go diff --git a/src/jetstream/info.go b/src/jetstream/info.go index e49bd27105..ecf3d6b7b2 100644 --- a/src/jetstream/info.go +++ b/src/jetstream/info.go @@ -42,7 +42,7 @@ func (p *portalProxy) getInfo(c echo.Context) (*interfaces.Info, error) { return nil, errors.New("Could not find session user_id") } - uaaUser, err := p.GetStratosUser(userGUID) + uaaUser, err := p.StratosAuthService.GetUser(userGUID) if err != nil { return nil, errors.New("Could not load session user data") } diff --git a/src/jetstream/localusers.go b/src/jetstream/localusers.go index 64e9760f54..ef4766cb15 100644 --- a/src/jetstream/localusers.go +++ b/src/jetstream/localusers.go @@ -7,6 +7,7 @@ import ( uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/localusers" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" ) @@ -47,7 +48,7 @@ func (p *portalProxy) AddLocalUser(c echo.Context) (string, error) { //Generate a user GUID and hash the password userGUID := uuid.NewV4().String() - passwordHash, err := HashPassword(password) + passwordHash, err := crypto.HashPassword(password) if err != nil { log.Errorf("Error hashing user password: %v", err) return "", err diff --git a/src/jetstream/localusers_test.go b/src/jetstream/localusers_test.go index f4e167689d..67cabf0b0e 100644 --- a/src/jetstream/localusers_test.go +++ b/src/jetstream/localusers_test.go @@ -10,6 +10,7 @@ import ( uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/localusers" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" . "github.com/smartystreets/goconvey/convey" @@ -168,7 +169,7 @@ func TestFindPasswordHash(t *testing.T) { scope := "stratos.admin" //Hash the password - generatedPasswordHash, _ := HashPassword(password) + generatedPasswordHash, _ := crypto.HashPassword(password) //generate a user GUID userGUID := uuid.NewV4().String() @@ -213,7 +214,7 @@ func TestUpdateLastLoginTime(t *testing.T) { scope := "stratos.admin" //Hash the password - generatedPasswordHash, _ := HashPassword(password) + generatedPasswordHash, _ := crypto.HashPassword(password) //generate a user GUID userGUID := uuid.NewV4().String() diff --git a/src/jetstream/main.go b/src/jetstream/main.go index 5aa1128777..51772e4ce6 100644 --- a/src/jetstream/main.go +++ b/src/jetstream/main.go @@ -33,10 +33,10 @@ import ( uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/cnsis" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/console_config" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/localusers" @@ -504,6 +504,19 @@ func loadPortalConfig(pc interfaces.PortalConfig, env *env.VarSet) (interfaces.P pc.HTTPClientTimeoutMutatingInSecs = pc.HTTPClientTimeoutInSecs } + if len(pc.AuthEndpointType) == 0 { + //Default to "remote" if AUTH_ENDPOINT_TYPE is not set + pc.AuthEndpointType = string(interfaces.Remote) + } else { + val, endpointTypeSupported := interfaces.AuthEndpointTypes[pc.AuthEndpointType] + if endpointTypeSupported { + pc.AuthEndpointType = string(val) + } else { + return pc, fmt.Errorf("AUTH_ENDPOINT_TYPE: %v is not valid. Must be set to local or remote (defaults to remote)", val) + } + } + + log.Debugf("Portal config auth endpoint type initialised to: %v", pc.AuthEndpointType) return pc, nil } @@ -601,6 +614,15 @@ func newPortalProxy(pc interfaces.PortalConfig, dcp *sql.DB, ss HttpSessionStore UserInfo: pp.GetCNSIUserFromBasicToken, }) + err := pp.InitStratosAuthService(interfaces.AuthEndpointTypes[pp.Config.AuthEndpointType]) + if err != nil { + log.Warnf("Defaulting to UAA authentication: %v", err) + err = pp.InitStratosAuthService(interfaces.Remote) + if err != nil { + log.Fatalf("Could not initialise auth service. %v", err) + } + } + // OIDC pp.AddAuthProvider(interfaces.AuthTypeOIDC, interfaces.AuthProvider{ Handler: pp.doOidcFlowRequest, @@ -780,19 +802,16 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { pp.POST("/v1/setup/check", p.setupConsoleCheck) } - pp.POST("/v1/auth/login/uaa", p.stratosLoginHandler) - pp.POST("/v1/auth/logout", p.logout) + loginAuthGroup := pp.Group("/v1/auth") + loginAuthGroup.POST("/login/uaa", p.StratosAuthService.Login) + loginAuthGroup.POST("/logout", p.StratosAuthService.Logout) // SSO Routes will only respond if SSO is enabled - pp.GET("/v1/auth/sso_login", p.initSSOlogin) - pp.GET("/v1/auth/sso_logout", p.ssoLogoutOfUAA) - - // Local User login/logout - // pp.POST("/v1/auth/local_login", p.localLogin) - // pp.POST("/v1/auth/local_logout", p.logout) + loginAuthGroup.GET("/sso_login", p.initSSOlogin) + loginAuthGroup.GET("/sso_logout", p.ssoLogoutOfUAA) // Callback is used by both login to Stratos and login to an Endpoint - pp.GET("/v1/auth/sso_login_callback", p.ssoLoginToUAA) + loginAuthGroup.GET("/sso_login_callback", p.ssoLoginToUAA) // Version info pp.GET("/v1/version", p.getVersions) @@ -811,17 +830,19 @@ func (p *portalProxy) registerRoutes(e *echo.Echo, needSetupMiddleware bool) { e.Use(middlewarePlugin.SessionEchoMiddleware) } + sessionAuthGroup := sessionGroup.Group("/auth") + // Connect to endpoint - sessionGroup.POST("/auth/login/cnsi", p.loginToCNSI) + sessionAuthGroup.POST("/login/cnsi", p.loginToCNSI) // Connect to Enpoint (SSO) - sessionGroup.GET("/auth/login/cnsi", p.ssoLoginToCNSI) + sessionAuthGroup.GET("/login/cnsi", p.ssoLoginToCNSI) // Disconnect endpoint - sessionGroup.POST("/auth/logout/cnsi", p.logoutOfCNSI) + sessionAuthGroup.POST("/logout/cnsi", p.logoutOfCNSI) // Verify Session - sessionGroup.GET("/auth/session/verify", p.verifySession) + sessionAuthGroup.GET("/session/verify", p.verifySession) // CNSI operations sessionGroup.GET("/cnsis", p.listCNSIs) @@ -931,6 +952,7 @@ func getUICustomHTTPErrorHandler(staticDir string, defaultHandler echo.HTTPError // EchoV2DefaultHTTPErrorHandler ensures we get V2 error behaviour // i.e. no wrapping in 'message' JSON object func echoV2DefaultHTTPErrorHandler(err error, c echo.Context) { + code := http.StatusInternalServerError msg := http.StatusText(code) if he, ok := err.(*echo.HTTPError); ok { @@ -954,7 +976,9 @@ func echoV2DefaultHTTPErrorHandler(err error, c echo.Context) { } } - if err != nil { + //Only log if there is a message to log + he, _ := err.(*echo.HTTPError) + if err != nil && he.Message.(string) != "" { c.Logger().Error(err) } } diff --git a/src/jetstream/middleware.go b/src/jetstream/middleware.go index b8e29dd30f..0983457e66 100644 --- a/src/jetstream/middleware.go +++ b/src/jetstream/middleware.go @@ -184,7 +184,7 @@ func (p *portalProxy) adminMiddleware(h echo.HandlerFunc) echo.HandlerFunc { userID, err := p.GetSessionValue(c, "user_id") if err == nil { // check their admin status in UAA - u, err := p.GetStratosUser(userID.(string)) + u, err := p.StratosAuthService.GetUser(userID.(string)) if err != nil { return c.NoContent(http.StatusUnauthorized) } diff --git a/src/jetstream/mock_server_test.go b/src/jetstream/mock_server_test.go index b823f44165..08d35d5e04 100644 --- a/src/jetstream/mock_server_test.go +++ b/src/jetstream/mock_server_test.go @@ -16,7 +16,7 @@ import ( "github.com/labstack/echo" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/crypto" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/tokens" @@ -136,8 +136,10 @@ func setupPortalProxy(db *sql.DB) *portalProxy { SessionStoreSecret: "hiddenraisinsohno!", EncryptionKeyInBytes: mockEncryptionKey, CFAdminIdentifier: CFAdminIdentifier, + AuthEndpointType: "remote", } + pp := newPortalProxy(pc, db, nil, nil, env.NewVarSet()) pp.SessionStore = setupMockPGStore(db) initialisedEndpoint := initCFPlugin(pp) diff --git a/src/jetstream/oauth_requests_test.go b/src/jetstream/oauth_requests_test.go index 926cf00c9b..f08e019b97 100644 --- a/src/jetstream/oauth_requests_test.go +++ b/src/jetstream/oauth_requests_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/crypto" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" . "github.com/smartystreets/goconvey/convey" sqlmock "gopkg.in/DATA-DOG/go-sqlmock.v1" diff --git a/src/jetstream/plugins/cloudfoundryhosting/main.go b/src/jetstream/plugins/cloudfoundryhosting/main.go index 34679db1db..17dcdda63c 100644 --- a/src/jetstream/plugins/cloudfoundryhosting/main.go +++ b/src/jetstream/plugins/cloudfoundryhosting/main.go @@ -124,13 +124,13 @@ func (ch *CFHosting) Init() error { ch.portalProxy.GetConfig().ConsoleConfig = new(interfaces.ConsoleConfig) - //Force auth endpoint type to remote (CF UAA) - ch.portalProxy.GetConfig().ConsoleConfig.AuthEndpointType = "remote" - // We are using the CF UAA - so the Console must use the same Client and Secret as CF ch.portalProxy.GetConfig().ConsoleConfig.ConsoleClient = ch.portalProxy.GetConfig().CFClient ch.portalProxy.GetConfig().ConsoleConfig.ConsoleClientSecret = ch.portalProxy.GetConfig().CFClientSecret + //Set the auth endpoint type for the console + ch.portalProxy.GetConfig().ConsoleConfig.AuthEndpointType = ch.portalProxy.GetConfig().AuthEndpointType + // Ensure that the identifier for an admin is the standard Cloud Foundry one ch.portalProxy.GetConfig().ConsoleConfig.ConsoleAdminScope = ch.portalProxy.GetConfig().CFAdminIdentifier diff --git a/src/jetstream/plugins/userinfo/main.go b/src/jetstream/plugins/userinfo/main.go index 2ebf881e02..5364169b82 100644 --- a/src/jetstream/plugins/userinfo/main.go +++ b/src/jetstream/plugins/userinfo/main.go @@ -54,11 +54,14 @@ func (userInfo *UserInfo) AddSessionGroupRoutes(echoGroup *echo.Group) { // Init performs plugin initialization func (userInfo *UserInfo) Init() error { + return nil } func (userInfo *UserInfo) getProvider(c echo.Context) Provider { - if interfaces.AuthEndpointTypes[userInfo.portalProxy.GetConfig().ConsoleConfig.AuthEndpointType] == interfaces.Local { + + log.Debugf("getUserInfoProvider: %v", userInfo.portalProxy.GetConfig().AuthEndpointType) + if interfaces.AuthEndpointTypes[userInfo.portalProxy.GetConfig().AuthEndpointType] == interfaces.Local { return InitLocalUserInfo(userInfo.portalProxy) } @@ -108,7 +111,6 @@ func (userInfo *UserInfo) userInfo(c echo.Context) error { return nil } - // update the user info for the current user func (userInfo *UserInfo) updateUserInfo(c echo.Context) error { _, err := userInfo.preFlightChecks(c) @@ -142,8 +144,8 @@ func (userInfo *UserInfo) updateUserInfo(c echo.Context) error { "Unable to update user profile", "Unable to update user profile: %v", err, ) - } - + } + c.Response().WriteHeader(http.StatusOK) return nil @@ -182,9 +184,9 @@ func (userInfo *UserInfo) updateUserPassword(c echo.Context) error { "Unable to update user password", "Unable to update user password: %v", err, ) - } - + } + c.Response().WriteHeader(http.StatusOK) return nil -} \ No newline at end of file +} diff --git a/src/jetstream/plugins/userinfo/uaa_user.go b/src/jetstream/plugins/userinfo/uaa_user.go index 711a5c19b4..5fe43afe7a 100644 --- a/src/jetstream/plugins/userinfo/uaa_user.go +++ b/src/jetstream/plugins/userinfo/uaa_user.go @@ -67,7 +67,7 @@ func (userInfo *UaaUserInfo) uaa(target string, body []byte) (int, []byte, error // Now get the URL of the request and remove the path to give the path of the API that is being requested url := fmt.Sprintf("%s/%s", uaaEndpoint, target) - username, err := userInfo.portalProxy.GetUsername(sessionUser) + username, err := userInfo.portalProxy.GetStratosAuthService().GetUsername(sessionUser) if err != nil { return http.StatusInternalServerError, nil, err } diff --git a/src/jetstream/portal_proxy.go b/src/jetstream/portal_proxy.go index 8ad026fcc0..56196d5e72 100644 --- a/src/jetstream/portal_proxy.go +++ b/src/jetstream/portal_proxy.go @@ -22,6 +22,7 @@ type portalProxy struct { EmptyCookieMatcher *regexp.Regexp // Used to detect and remove empty Cookies sent by certain browsers AuthProviders map[string]interfaces.AuthProvider env *env.VarSet + StratosAuthService interfaces.StratosAuth } // HttpSessionStore - Interface for a store that can manage HTTP Sessions @@ -46,4 +47,4 @@ func (p *portalProxy) SetCanPerformMigrations(value bool) { // CanPerformMigrations returns if we can perform Database migrations func (p *portalProxy) CanPerformMigrations() bool { return canPerformMigrations -} \ No newline at end of file +} diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis.go b/src/jetstream/repository/cnsis/pgsql_cnsis.go index 7f32911aeb..2e0d9fbd21 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis.go @@ -6,8 +6,8 @@ import ( "fmt" "net/url" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" log "github.com/sirupsen/logrus" ) diff --git a/src/jetstream/repository/cnsis/pgsql_cnsis_test.go b/src/jetstream/repository/cnsis/pgsql_cnsis_test.go index b097cd7bea..1d66395e19 100644 --- a/src/jetstream/repository/cnsis/pgsql_cnsis_test.go +++ b/src/jetstream/repository/cnsis/pgsql_cnsis_test.go @@ -9,7 +9,7 @@ import ( "gopkg.in/DATA-DOG/go-sqlmock.v1" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/crypto" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" . "github.com/smartystreets/goconvey/convey" ) diff --git a/src/jetstream/repository/interfaces/auth.go b/src/jetstream/repository/interfaces/auth.go new file mode 100644 index 0000000000..6f1e369cf2 --- /dev/null +++ b/src/jetstream/repository/interfaces/auth.go @@ -0,0 +1,12 @@ +package interfaces + +import "github.com/labstack/echo" + +//StratosAuth provides common access to Stratos login/logout functionality +type StratosAuth interface { + Login(c echo.Context) error + Logout(c echo.Context) error + GetUsername(userGUID string) (string, error) + GetUser(userGUID string) (*ConnectedUser, error) + VerifySession(c echo.Context, sessionUser string, sessionExpireTime int64) error +} \ No newline at end of file diff --git a/src/jetstream/repository/interfaces/errors.go b/src/jetstream/repository/interfaces/errors.go index c47939883f..1ceca88170 100644 --- a/src/jetstream/repository/interfaces/errors.go +++ b/src/jetstream/repository/interfaces/errors.go @@ -29,11 +29,16 @@ func NewHTTPError(status int, userFacingError string) error { } func NewHTTPShadowError(status int, userFacingError string, fmtString string, args ...interface{}) error { + //Only set the HTTPError field of the ErrHTTPShadow struct if we have been passed an error message intended for logging + httpErrorMsg := "" + if len(userFacingError) > 0 { + httpErrorMsg = fmt.Sprintf(`{"error":%q}`, userFacingError) + } shadowError := ErrHTTPShadow{ UserFacingError: userFacingError, - HTTPError: echo.NewHTTPError(status, fmt.Sprintf(`{"error":%q}`, userFacingError)), + HTTPError: echo.NewHTTPError(status, httpErrorMsg), } - if len(fmtString) > 0 { + if args != nil { shadowError.LogMessage = fmt.Sprintf(fmtString, args...) } return shadowError diff --git a/src/jetstream/repository/interfaces/portal_proxy.go b/src/jetstream/repository/interfaces/portal_proxy.go index 6862e437ac..2ed2287722 100644 --- a/src/jetstream/repository/interfaces/portal_proxy.go +++ b/src/jetstream/repository/interfaces/portal_proxy.go @@ -14,12 +14,11 @@ type PortalProxy interface { GetHttpClient(skipSSLValidation bool) http.Client GetHttpClientForRequest(req *http.Request, skipSSLValidation bool) http.Client RegisterEndpoint(c echo.Context, fetchInfo InfoFunc) error - DoRegisterEndpoint(cnsiName string, apiEndpoint string, skipSSLValidation bool, clientId string, clientSecret string, ssoAllowed bool, subType string, fetchInfo InfoFunc) (CNSIRecord, error) - GetEndpointTypeSpec(typeName string) (EndpointPlugin, error) // Auth + GetStratosAuthService() StratosAuth ConnectOAuth2(c echo.Context, cnsiRecord CNSIRecord) (*TokenRecord, error) InitEndpointTokenRecord(expiry int64, authTok string, refreshTok string, disconnect bool) TokenRecord @@ -49,11 +48,8 @@ type PortalProxy interface { // UAA Token GetUAATokenRecord(userGUID string) (TokenRecord, error) RefreshUAAToken(userGUID string) (TokenRecord, error) - - GetUsername(userid string) (string, error) RefreshUAALogin(username, password string, store bool) error GetUserTokenInfo(tok string) (u *JWTUserTokenInfo, err error) - GetUAAUser(userGUID string) (*ConnectedUser, error) // Proxy API requests ProxyRequest(c echo.Context, uri *url.URL) (map[string]*CNSIRequest, error) @@ -71,7 +67,6 @@ type PortalProxy interface { // Tokens - lower-level access SaveEndpointToken(cnsiGUID string, userGUID string, tokenRecord TokenRecord) error DeleteEndpointToken(cnsiGUID string, userGUID string) error - AddLoginHook(priority int, function LoginHookFunc) error ExecuteLoginHooks(c echo.Context) error diff --git a/src/jetstream/repository/tokens/pgsql_tokens.go b/src/jetstream/repository/tokens/pgsql_tokens.go index f90629c326..427323ef83 100644 --- a/src/jetstream/repository/tokens/pgsql_tokens.go +++ b/src/jetstream/repository/tokens/pgsql_tokens.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/datastore" - "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" diff --git a/src/jetstream/session.go b/src/jetstream/session.go index 9345d5e045..0564099004 100644 --- a/src/jetstream/session.go +++ b/src/jetstream/session.go @@ -1,21 +1,44 @@ package main import ( + "encoding/base64" "fmt" + "net/http" + "strconv" "time" "github.com/gorilla/sessions" "github.com/labstack/echo" log "github.com/sirupsen/logrus" + + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" ) const ( + + + // XSRFTokenHeader - XSRF Token Header name + XSRFTokenHeader = "X-Xsrf-Token" + // XSRFTokenSessionName - XSRF Token Session name + XSRFTokenSessionName = "xsrf_token" + + // SessionExpiresOnHeader Custom header for communicating the session expiry time to clients + sessionExpiresOnHeader = "X-Cap-Session-Expires-On" + // ClientRequestDateHeader Custom header for getting date form client + clientRequestDateHeader = "X-Cap-Request-Date" + // XSRFTokenCookie - XSRF Token Cookie name + xSRFTokenCookie = "XSRF-TOKEN" // Default cookie name/cookie name prefix jetstreamSessionName = "console-session" jetStreamSessionContextKey = "jetstream-session" jetStreamSessionContextUpdatedKey = "jetstream-session-updated" + ) + + + + // SessionValueNotFound - Error returned when a requested key was not found in the session type SessionValueNotFound struct { msg string @@ -148,3 +171,107 @@ func (p *portalProxy) clearSession(c echo.Context) error { return p.SaveSession(c, session) } + +func (p *portalProxy) removeEmptyCookie(c echo.Context) { + req := c.Request() + originalCookie := req.Header.Get("Cookie") + cleanCookie := p.EmptyCookieMatcher.ReplaceAllLiteralString(originalCookie, "") + req.Header.Set("Cookie", cleanCookie) +} + +func (p *portalProxy) handleSessionExpiryHeader(c echo.Context) error { + + // Explicitly tell the client when this session will expire. This is needed because browsers actively hide + // the Set-Cookie header and session cookie expires_on from client side javascript + expOn, err := p.GetSessionValue(c, "expires_on") + if err != nil { + msg := "Could not get session expiry" + log.Error(msg+" - ", err) + return echo.NewHTTPError(http.StatusInternalServerError, msg) + } + c.Response().Header().Set(sessionExpiresOnHeader, strconv.FormatInt(expOn.(time.Time).Unix(), 10)) + + expiry := expOn.(time.Time) + expiryDuration := expiry.Sub(time.Now()) + + // Subtract time now to get the duration add this to the time provided by the client + clientDate := c.Request().Header.Get(clientRequestDateHeader) + if len(clientDate) > 0 { + clientDateInt, err := strconv.ParseInt(clientDate, 10, 64) + if err == nil { + clientDateInt += int64(expiryDuration.Seconds()) + c.Response().Header().Set(sessionExpiresOnHeader, strconv.FormatInt(clientDateInt, 10)) + } + } + + return nil +} + +// Create a token for XSRF if needed, store it in the session and add the response header for the front-end to pick up +func (p *portalProxy) ensureXSRFToken(c echo.Context) { + token, err := p.GetSessionStringValue(c, XSRFTokenSessionName) + if err != nil || len(token) == 0 { + // Need a new token + tokenBytes, err := crypto.GenerateRandomBytes(32) + if err == nil { + token = base64.StdEncoding.EncodeToString(tokenBytes) + } else { + token = "" + } + sessionValues := make(map[string]interface{}) + sessionValues[XSRFTokenSessionName] = token + p.setSessionValues(c, sessionValues) + } + + if len(token) > 0 { + c.Response().Header().Set(XSRFTokenHeader, token) + } +} + +func (p *portalProxy) verifySession(c echo.Context) error { + log.Debug("verifySession") + + sessionExpireTime, err := p.GetSessionInt64Value(c, "exp") + if err != nil { + msg := "Could not find session date" + log.Error(msg) + return echo.NewHTTPError(http.StatusForbidden, msg) + } + + sessionUser, err := p.GetSessionStringValue(c, "user_id") + if err != nil { + msg := "Could not find user_id in Session" + log.Error(msg) + return echo.NewHTTPError(http.StatusForbidden, msg) + } + + err = p.StratosAuthService.VerifySession(c, sessionUser, sessionExpireTime) + + // Could not verify session + if err != nil { + log.Error(err) + err = echo.NewHTTPError(http.StatusForbidden, "Could not verify user") + + } else { + + err = p.handleSessionExpiryHeader(c) + if err != nil { + return err + } + + info, err := p.getInfo(c) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + // Add XSRF Token + p.ensureXSRFToken(c) + + err = c.JSON(http.StatusOK, info) + if err != nil { + return err + } + } + + return err +} diff --git a/src/jetstream/setup_console.go b/src/jetstream/setup_console.go index 95b133f779..9a67fff3e3 100644 --- a/src/jetstream/setup_console.go +++ b/src/jetstream/setup_console.go @@ -14,6 +14,7 @@ import ( uuid "github.com/satori/go.uuid" log "github.com/sirupsen/logrus" + "github.com/cloudfoundry-incubator/stratos/src/jetstream/crypto" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/console_config" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" "github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces/config" @@ -200,7 +201,12 @@ func (p *portalProxy) initialiseConsoleConfig(envLookup *env.VarSet) (*interface } } else if val == interfaces.Remote { // Auth endpoint type is set to "remote", so need to load local user config vars - // Nothing to do + // Default authorization endpoint to be UAA endpoint + if consoleConfig.AuthorizationEndpoint == nil { + // No Authorization endpoint + consoleConfig.AuthorizationEndpoint = consoleConfig.UAAEndpoint + log.Infof("Using UAA Endpoint for Auth Endpoint: %s", consoleConfig.AuthorizationEndpoint) + } } else { //Auth endpoint type has been set to an invalid value return consoleConfig, errors.New("AUTH_ENDPOINT_TYPE must be set to either \"local\" or \"remote\"") @@ -209,13 +215,6 @@ func (p *portalProxy) initialiseConsoleConfig(envLookup *env.VarSet) (*interface return consoleConfig, errors.New("AUTH_ENDPOINT_TYPE not found") } - // Default authorization endpoint to be UAA endpoint - if consoleConfig.AuthorizationEndpoint == nil { - // No Authorization endpoint - consoleConfig.AuthorizationEndpoint = consoleConfig.UAAEndpoint - log.Infof("Using UAA Endpoint for Auth Endpoint: %s", consoleConfig.AuthorizationEndpoint) - } - return consoleConfig, nil } @@ -250,7 +249,7 @@ func initialiseLocalUsersConfiguration(consoleConfig *interfaces.ConsoleConfig, userGUID := uuid.NewV4().String() password := localUserPassword - passwordHash, err := HashPassword(password) + passwordHash, err := crypto.HashPassword(password) if err != nil { log.Errorf("Unable to initialise Stratos local user due to: %+v", err) return err diff --git a/src/jetstream/utils.go b/src/jetstream/stringutils/utils.go similarity index 98% rename from src/jetstream/utils.go rename to src/jetstream/stringutils/utils.go index 6d5ef4ab3a..2ea9baa5cb 100644 --- a/src/jetstream/utils.go +++ b/src/jetstream/stringutils/utils.go @@ -1,4 +1,4 @@ -package main +package stringutils import ( "strings" diff --git a/src/jetstream/utils_test.go b/src/jetstream/stringutils/utils_test.go similarity index 97% rename from src/jetstream/utils_test.go rename to src/jetstream/stringutils/utils_test.go index ea4b882f92..a5caeb2e09 100644 --- a/src/jetstream/utils_test.go +++ b/src/jetstream/stringutils/utils_test.go @@ -1,4 +1,4 @@ -package main +package stringutils import ( "testing"