Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ dependencies {
compile group: 'org.glassfish.jersey.media', name: 'jersey-media-json-jackson', version: jerseyVersion
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security'
compile group: 'org.springframework.security', name: 'spring-security-config'
compile group: 'org.opensaml', name: 'opensaml', version: '2.6.4'
compileOnly group: 'org.projectlombok', name: 'lombok', version: lombokVersion
compileOnly group: 'org.springframework.boot', name: 'spring-boot-devtools', version: springBootVersion
testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: springBootVersion
Expand Down
8 changes: 8 additions & 0 deletions front-end/src/api/login.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,11 @@ export function getUserInfo(token) {
params: { token }
})
}

export function loginByToken(token) {
return request({
headers: { 'Content-Type': 'application/json' },
url: '/pulsar-manager/login?token=' + token,
method: 'get'
})
}
23 changes: 23 additions & 0 deletions front-end/src/store/modules/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { removeCsrfToken } from '@/utils/csrfToken'
import { Message } from 'element-ui'
import { setTenant, removeTenant } from '../../utils/tenant'
import { getUserInfo } from '@/api/users'
import { loginByToken } from '../../api/login'

const user = {
state: {
Expand Down Expand Up @@ -149,6 +150,28 @@ const user = {
// resolve()
// })
})
},

LoginByToken({ commit }, token) {
return new Promise((resolve, reject) => {
loginByToken(token).then(response => {
if (response.data.hasOwnProperty('error') && response.data.error.length >= 0) {
Message({
message: 'Invalid Access Token Provided',
type: 'error',
duration: 5 * 1000
})
reject('login error')
}
commit('SET_TOKEN', response.headers.token)
setToken(response.headers.token)
setName(response.headers.username)
setTenant(response.headers.tenant)
resolve()
}).catch(error => {
reject(error)
})
})
}
}
}
Expand Down
16 changes: 16 additions & 0 deletions front-end/src/views/login/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ export default {
mounted() {
window.addEventListener('message', this.handleMessage)
},
beforeMount() {
if (location.search.includes('token')) {
this.handleLoginByToken()
}
},
methods: {
showPwd() {
if (this.passwordType === 'password') {
Expand Down Expand Up @@ -158,6 +163,17 @@ export default {
}
})
},
handleLoginByToken() {
const urlParams = new URLSearchParams(window.location.search)
const token = (urlParams.get('token'))
this.$store.dispatch('LoginByToken', token).then(() => {
this.loading = false
window.location = '/'
this.$router.push({ path: '/' })
}).catch(() => {
this.loading = false
})
},
afterQRScan() {
// const hash = window.location.hash.slice(1)
// const hashObj = getQueryObject(hash)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,25 @@ public ResponseEntity<Map<String, Object>> logout() {
jwtService.removeToken(request.getSession().getId());
return ResponseEntity.ok(result);
}

@RequestMapping(value = "/login", method = RequestMethod.GET)
public ResponseEntity<Map<String, Object>> loginByAccessToken(
HttpServletRequest request) {
String token = request.getParameter("token");
Optional<UserInfoEntity> userInfoEntity = usersRepository.findByAccessToken(token);
Map<String, Object> result = Maps.newHashMap();
if (!userInfoEntity.isPresent())
{
result.put("error", "No user found with the access token");
return ResponseEntity.ok(result);
}
HttpHeaders headers = new HttpHeaders();
if (userInfoEntity.isPresent()){
UserInfoEntity user = userInfoEntity.get();
headers.add("token",token);
headers.add("username", user.getName());
headers.add("tenant", user.getName());
}
return new ResponseEntity<>(result, headers, HttpStatus.OK);
}
}
131 changes: 131 additions & 0 deletions src/main/java/org/apache/pulsar/manager/controller/SSOController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pulsar.manager.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.util.Asserts;
import org.apache.pulsar.manager.entity.RoleBindingEntity;
import org.apache.pulsar.manager.entity.RoleBindingRepository;
import org.apache.pulsar.manager.entity.RolesRepository;
import org.apache.pulsar.manager.entity.UserInfoEntity;
import org.apache.pulsar.manager.entity.UsersRepository;
import org.apache.pulsar.manager.service.JwtService;
import org.apache.pulsar.manager.utils.SAMLParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping(value = "/pulsar-manager/saml")
@Api(description = "Calling the request below this class does not require authentication because " +
"the user has not logged in yet.")
@Validated
public class SSOController {


@Autowired
private UsersRepository usersRepository;

@Autowired
private JwtService jwtService;

@Resource
private RolesRepository rolesRepository;

@Resource
private RoleBindingRepository roleBindingRepository;

@Value("${sso.redirect.host}")
private String redirectHost;

@Value("${sso.redirect.port}")
private int redirectPort;

@Value("${sso.redirect.scheme}")
private String redirectUrlScheme;

@Value("${sso.certificate}")
private String certificate;

@Value("${sso.whitelisted.domain}")
private String whiteListedDomain;

@ApiOperation(value = "Verify SAML Response")
@ApiResponses({
@ApiResponse(code = 200, message = "ok"),
@ApiResponse(code = 404, message = "Not found"),
@ApiResponse(code = 500, message = "Internal server error")
})
@RequestMapping(value = "/sso", method = RequestMethod.POST)
@ResponseBody
public void loginByIDP(HttpServletRequest request, HttpServletResponse response) throws Exception {
String samlResponse = request.getParameter("SAMLResponse");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest to check the call is a SAML response before using it.

Moreover, lets be neutral regarding the sso method here? The config and api endpoint are generic. The implementation assumes SAML. We should check the type of auth and switch to a branch of code (factory?) which gives us (authenticated / not authenticated) as a result.

SAMLParser samlParser = new SAMLParser(certificate);
Map<String,String> userDetails = samlParser.getUserDetailsFromSAMLResponse(samlResponse);
Asserts.check(StringUtils.contains(userDetails.get("email"),whiteListedDomain),"Login with domain not supported");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest to check the email format in string(using InternetAddress()?) in addition to the domain.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering what we will achieve with the email format validation , the email is extracted from SAML Response which is served through IDP directly.

Copy link

@shiv4289 shiv4289 Dec 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Lets drop this.

Optional<UserInfoEntity> userInfoEntity = usersRepository.findByEmail(userDetails.get("email"));
if (!userInfoEntity.isPresent()) {
UserInfoEntity userEntity = new UserInfoEntity();
userEntity.setName(getUsername(userDetails.get("email")));
userEntity.setEmail(userDetails.get("email"));
userEntity.setPassword(DigestUtils.sha256Hex("pulsar"));
usersRepository.save(userEntity);
userInfoEntity = usersRepository.findByEmail(userDetails.get("email"));
long superUserRoleID = rolesRepository.findByRoleFlag(0).get().getRoleId();
RoleBindingEntity roleBindingEntity = new RoleBindingEntity();
roleBindingEntity.setRoleId(superUserRoleID);
roleBindingEntity.setUserId(userEntity.getUserId());
roleBindingEntity.setName("Role created after sso signin");
roleBindingRepository.save(roleBindingEntity);

}
UserInfoEntity user = userInfoEntity.get();
String userAccount = user.getName();
String password = user.getPassword();
String token = jwtService.toToken(userAccount + password + System.currentTimeMillis());
user.setAccessToken(token);
usersRepository.update(user);
jwtService.setToken(request.getSession().getId(), token);
URI uri = new URIBuilder().setScheme(redirectUrlScheme).setHost(redirectHost).setPort(redirectPort).setPath("/login")
.addParameter("token",token)
.build();
response.setStatus(302);
response.sendRedirect(uri.toString());
}

private String getUsername(String email) {
String userName = StringUtils.split(email,"@")[0];
return StringUtils.replace(userName,".","");
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,9 @@ public void update(UserInfoEntity userInfoEntity) {
public void delete(String name) {
this.usersMapper.delete(name);
}

@Override
public Optional<UserInfoEntity> findByEmail(String email) {
return Optional.ofNullable(this.usersMapper.findByEmail(email));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,12 @@ public interface UsersRepository {
* @param name username
*/
void delete(String name);

/**
* Get a user information by email.
* @param email The email
* @return UserInfoEntity
*/
Optional<UserInfoEntity> findByEmail(String email);

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public void addInterceptors(InterceptorRegistry registry) {
.excludePathPatterns("/doc.html")
// BKVM
.excludePathPatterns("/bkvm")
.excludePathPatterns("/pulsar-manager/saml/**")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

our endpoint is /sso right? Should this be /pulsar-manager/sso/**

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also why ** ? can we do with just /sso without ** ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

our endpoint is /saml/sso @shiv4289
here /saml is mapped at controller level and ** to exclude all method in that controller from authentication.
This makes me rethink to change the excludePathPatterns as /pulsar-manager/saml/sso to make only one method accessible without authentication.

;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,10 @@ public interface UsersMapper {

@Delete("DELETE FROM users WHERE name=#{name}")
void delete(String name);

@Select("SELECT access_token AS accessToken, user_id AS userId, name, description, email," +
"phone_number AS phoneNumber, location, company, expire, password " +
"FROM users " +
"WHERE email = #{email}")
UserInfoEntity findByEmail(String email);
}
Loading