First of all, according to Spring doc
, if i want to map user roles to scopes, i should use setCheckUserScopes(true) to DefaultOAuth2RequestFactory. So one way to do this, is injecting my own DefaultOAuth2RequestFactory bean, as doc says:
The AuthorizationServerEndpointsConfigurer allows you to inject a custom OAuth2RequestFactory so you can use that feature to set up a factory if you use #EnableAuthorizationServer.
Then i do
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends
AuthorizationServerConfigurerAdapter {
...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
.tokenServices(tokenServices());
endpoints
.getOAuth2RequestFactory(); // this doesn't return me my own DefaultOAuth2RequestFactory
}
#Bean
#Primary
public OAuth2RequestFactory defaultOAuth2RequestFactory() {
DefaultOAuth2RequestFactory defaultOAuth2RequestFactory = new DefaultOAuth2RequestFactory(
clientDetailsService);
defaultOAuth2RequestFactory.setCheckUserScopes(true);
return defaultOAuth2RequestFactory;
}
}
EDIT
I've overlooked the method requestFactory() from AuthorizationServerEndpointsConfigurer. That was the correct way to pass it to Spring Security. Setting OAuth2RequestFactory bean as primary didn't work. I've deleted some things to focus on the real problem:
After this observation, the actual problem:
as i understand, if the user has authorities A and B, and the app has scope A, then he gets just 'A' scope. But this is not happening. What is really happening is that if app has scope A, and APP (not user) has authorities A and B, then user gets A. But this doesn't make any sense.
This is DefaultOAuth2RequestFactory method that resolve user's scopes:
private Set<String> extractScopes(Map<String, String> requestParameters, String clientId) {
... // I avoid some unimportant lines to not make this post so long
if ((scopes == null || scopes.isEmpty())) {
scopes = clientDetails.getScope();
}
if (checkUserScopes) {
scopes = checkUserScopes(scopes, clientDetails);
}
return scopes;
}
private Set<String> checkUserScopes(Set<String> scopes, ClientDetails clientDetails) {
if (!securityContextAccessor.isUser()) {
return scopes;
}
Set<String> result = new LinkedHashSet<String>();
Set<String> authorities = AuthorityUtils.authorityListToSet(securityContextAccessor.getAuthorities());
for (String scope : scopes) {
if (authorities.contains(scope) || authorities.contains(scope.toUpperCase())
|| authorities.contains("ROLE_" + scope.toUpperCase())) {
result.add(scope);
}
}
return result;
}
Is this a bug? Please tell me if i am wrong. Regards
You need to wire your OAuth2RequestFactory by code something like here.
If the authorities are set by ClientDetailsService then you should be good. If you are looking to map logged-in user authorities I don't have luck there either.
Related
When updating to Spring Security 6, the JSR250 annotation #RolesAllowed on my #RestController doesn't take the defined roleHierarchy into account.
Related to: AccessDecisionVoter Deprecated with Spring Security 6.x
Since Spring Security 6, the AccessDecisionVoter is deprecated and the suggested way, from the thread above, is to "simply expose a expressionHandler". This didn't work for me with JSR250 enabled.
#Bean
public DefaultMethodSecurityExpressionHandler expressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy());
return expressionHandler;
}
#Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "a > b";
roleHierarchy.setHierarchy(hierarchy);
return roleHierarchy;
}
It seems like the created AuthorityAuthorizationManager by Jsr250AuthorizationManagerRegistry.resolveManager for RolesAllowed doesn't take the expressionHandler nor DefaultMethodSecurityExpressionHandler into account.
The AuthorityAuthorizationManager does have a field for a roleHierarchy to be set, but I couldn't figure out how or when this is supposed to be called.
I would have commented on the related post above but unfortunately I don't have the required reputation.
Unfortunately there is no support to add role hierarchy to JSR250 manager. But there is a workaround that basically clones the library's implementation. At this point it makes more sense to drop JSR250 since you will be just replicating the logic from the libs to your code base, but if you want to do it anyway, just follow these instructions:
Create a custom manager that will deal with the JSR250 annotations:
public class Jsr250CustomAuthorizationManager implements AuthorizationManager<MethodInvocation> {
static final AuthorizationManager<MethodInvocation> NULL_MANAGER = (a, o) -> null;
private final RoleHierarchy roleHierarchy;
public Jsr250CustomAuthorizationManager(RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
#Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation methodInvocation) {
var manager = resolveManager(methodInvocation);
return manager.check(authentication, methodInvocation);
}
public AuthorizationManager<MethodInvocation> resolveManager(MethodInvocation methodInvocation) {
if (hasDenyAll(methodInvocation)) {
return (a, o) -> new AuthorizationDecision(false);
}
if (hasPermitAll(methodInvocation)) {
return (a, o) -> new AuthorizationDecision(true);
}
if (hasRolesAllowed(methodInvocation)) {
var rolesAllowed = (RolesAllowed) methodInvocation.getMethod().getAnnotation(RolesAllowed.class);
var manager = AuthorityAuthorizationManager.<MethodInvocation>hasAnyRole(rolesAllowed.value());
manager.setRoleHierarchy(roleHierarchy);
return manager;
}
return NULL_MANAGER;
}
public boolean hasDenyAll(MethodInvocation methodInvocation) {
return methodInvocation.getMethod().getAnnotation(DenyAll.class) != null;
}
public boolean hasPermitAll(MethodInvocation methodInvocation) {
return methodInvocation.getMethod().getAnnotation(PermitAll.class) != null;
}
public boolean hasRolesAllowed(MethodInvocation methodInvocation) {
return methodInvocation.getMethod().getAnnotation(RolesAllowed.class) != null;
}
}
You can just copy and paste the code above into a new class in your code, this is a class that I made myself based on the one that exists in the spring security lib.
expose this new instance as a bean of type Advisor:
#EnableMethodSecurity(jsr250Enabled = true)
public class MethodSecuritytConfiguration {
#Bean
public Advisor jsr250AuthorizationMethodInterceptor() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
var manager = new Jsr250CustomAuthorizationManager(roleHierarchy);
return AuthorizationManagerBeforeMethodInterceptor.jsr250(manager);
}
}
In case you are using #EnableMethodSecurity(jsr250Enabled = true), you will also need to add this configuration to your application.properties:
spring.main.allow-bean-definition-overriding=true
This will allow to override a bean that is defined in the security lib that deals with JSR250 annotations. Because this bean's configuration is hardcoded in the lib wihtout exposing a way to change it, we have to override it altogheter to add the behavior we need. Note that the name of your bean needs to be jsr250AuthorizationMethodInterceptor to override the one (with same name) from the security lib. If you remove the jsr250Enabled configuration from EnableMethodSecurity, then you can name your bean anything you want and remove the configuration from application.properties.
I have a bean which I've declared in my bean config as thus:
#Configuration
public class BeanConfig {
#Bean
public MemberDTO getMemberDTO() {
return new MemberDTO();
}
}
When a user calls my service, I use the username and password they've provided to call the endpoint of a different service to get the user's information:
#Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LogManager.getLogger(CustomAuthenticationProvider.class);
private #Autowired MemberDTO memberDTO;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String loginGeniuneFailMessage = "";
boolean loginGeniuneFail = false;
try {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
String endPoint = credentialsBaseUrl + "/api/login";
HttpResponse<MemberDTO> response_auth = Unirest.get(endPoint)
.basicAuth(username, password)
.header("Accept", "*/*")
.asObject(MemberDTO.class);
int status_auth = response_auth.getStatus();
if (status_auth == 200) {
if (response_auth.getBody() == null) {
LOGGER.info("account validation - could not parse response body to object");
UnirestParsingException ex = response_auth.getParsingError().get();
LOGGER.error("parsing error: ", ex);
} else {
memberDTO = response_auth.getBody();
}
}
...
} catch (Exception ex) {
...
}
}
I want to store the user's information in the memberDTO and use the memberDTO elsewhere in a different component, rather than calling the login API every time:
#Component
public class MemberLogic {
private #Autowired MemberDTO memberDTO;
public ResponseEntity<?> processMemberInformation(WrapperDTO wrapperDTO, BindingResult result) {
if (result.hasFieldErrors()) {
String errors = result.getFieldErrors().stream()
.map(p -> p.getDefaultMessage()).collect(Collectors.joining("\n"));
return ResponseEntity.badRequest().body("An error occured while trying to persist information: " + errors);
}
String name = memberDTO.getName();
...
}
}
The problem now is the "memberDTO.getName()" is returning null, even though this value is being set from the initial API call in CustomAuthenticationProvider.
My questions are: why isn't this working? And is this the best approach to take for something like this?
Thanks.
My questions are: why isn't this working? And is this the best approach to take for something like this?
This doesn't work because Java uses pass-by-value semantics instead of pass-by-reference semantics. What this means is that the statement memberDTO = response_auth.getBody(); does not really make the Spring container start pointing to the MemberDTO returned by response_auth.getBody(). It only makes the memberDTO reference in CustomAuthenticationProvider point to the object in the response. The Spring container still continues to refer to the original MemberDTO object.
One way to fix this would be to define a DAO class that can be used for interacting with DTO instances rather than directly creating a DTO bean :
#Configuration
public class BeanConfig {
#Bean
public MemberDAO getMemberDAO() {
return new MemberDAO();
}
}
CustomAuthenticationProvider can then set the MemberDTO in the MemberDAO by using : memberDAO.setMemberDTO(response_auth.getBody());
Finally, MemberLogic can access the MemberDTO as String name = memberDAO.getMemberDTO().getName();
Note : Instead of returning the MemberDTO from the MemberDAO, the MemberDAO can define a method called getName which extracts the name from the MemberDTO and returns it. (Tell Don't Ask principle). That said and as suggested in the comments, the best practice would be to use a SecurityContext to store the user information.
The problem is, that you can not override a spring bean "content" like this memberDTO = response_auth.getBody(); because it changes only the instance variable for the given bean. (And its also not good because its out of the spring boot context and it overrides only the field dependency for this singleton bean)
You should not use a normal spring bean for holding data (a state). All the spring beans are singleton by default and you could have some concurrency problems.
For this you should use a database, where you write your data or something like a session bean.
I have a rest-endpoint like this:
/users/{userId}/something
I implemented authentification using oauth2.
My WebSecurityConfig looks like this:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll();
}
How can I only allow users to access their own endpoint (eg. User with Id 100 can only access /users/100/something) without beeing able to see another endpoint (like /users/200/something)?
Is this possible?
There are many ways to solve this problem, but i have picked out three solutions to apraoch this problem.
Custom Security Expression
I would recommend a custom security based annotation approach. This would involve implementing a custom security expression, the related expression handler and the method security configuration. The next appraoch is a little bit simpler if this is too much work for you.
public class UserIdentityMethodSecurityExpressionRoot
extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
public UserIdentityMethodSecurityExpressionRoot(Authentication authentication) {
super(authentication);
}
public boolean userIdentity(Long userId) {
User user = ((UserPrincipal) this.getPrincipal()).getUser();
return user.getId() == userId;
}
}
Rest endpoints or service methods can then be annotated with the newly created security expression:
#PreAuthorize("userIdentity(#userId)")
#GetMapping
#ResponseBody
public Resource fineOne(#PathVariable Long userId) {
return resourceService.findOne(id);
}
Please note that the userId must be provided somewhere, either as #PathVariable or #RequestParam. Spring security will then check if the current user has matches the provided userId and returns 403 otherwise.
The full example is available here and has been adapted in this question for your purposes.
SpEL
You can also use SpEL, which is a little bit simpler:
#PreAuthorize("#userId == principal.getId()")
#GetMapping
#ResponseBody
public Resource fineOne(#PathVariable Long userId) {
return resourceService.findOne(id);
}
Other considerations
You can also do all the work yourself and get to a faster result without defining a custom expression using SecurityContextHolder.
public static void checkUserIdentity(Long userId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// user did not provide a token
if(auth == null) {
throw new AccessDeniedException();
}
UserDetails details = (UserDetails) auth.getPrincipal();
if(userId != details.getId()) {
throw new AccessDeniedException();
}
}
And using it like:
#GetMapping
#ResponseBody
public Resource fineOne(#PathVariable Long userId) {
SecurityUtils.checkUserIdentity(userId)
return resourceService.findOne(id);
}
Why does this work? The SecurityContextHolder will inject the current principal if you have setup Spring security correctly. By default, an authentication is bound to the current thread of execution and will be reset if the request has been processed or encounters an exception.
I've implemented Persistent token based Remember Me with Spring Security 3.2.3.RELEASE.
During development and testing I realized the database is filled with tokens for the same username.
When removeUserTokens is called I don't know which of the tokens I need to delete. I guess that the user have multiple tokens, one for each device he's using (Computer, Android, etc...), and if he logs out of one device, I want to delete the token for that device so he stays logged in another device.
Any ideas?
1) create custom PersistentTokenRepository with one additional method:
public class MDJdbcTokenRepository extends JdbcTokenRepositoryImpl {
public void removeTokenBySeries(String series) {
getJdbcTemplate().update("delete from persistent_logins where series = ?", series);
}
}
2) create custom RememeberMeServices
public class MDRememberMeServices extends PersistentTokenBasedRememberMeServices {
private MDJdbcTokenRepository tokenRepository;
public MDRememberMeServices(String key,
UserDetailsService userDetailsService,
MDJdbcTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
setParameter("remember-me");// parameter name in login form
this.tokenRepository = tokenRepository;
}
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
cancelCookie(request, response);
if (authentication != null) {
String rememberMeCookie = extractRememberMeCookie(request);
if(rememberMeCookie != null && rememberMeCookie.length() != 0) {
String[] cookieTokens = decodeCookie(rememberMeCookie);
if (cookieTokens.length == 2) {
String series = cookieTokens[0];
//remove by series
tokenRepository.removeTokenBySeries(series);
}
}
}
}
}
3) add beans
private static String key = "your string here";
#Bean
public MDJdbcTokenRepository persistentTokenRepository() {
MDJdbcTokenRepository db = new MDJdbcTokenRepository();
db.setDataSource(dataSource());
return db;
}
#Bean
public RememberMeServices rememberMeServices() throws Exception {
return new MDRememberMeServices(key, userDetailsService, persistentTokenRepository());
}
4) change your security spring config:
and().rememberMe().key(key).rememberMeServices(rememberMeServices())
The easiest solution is to change the Authentication object you use in such a way that calls to the getName() return "deviceID"+"username" instead of "username".
You could for example hash HTTP header UserAgent and use it as a device ID. The call to PersistentTokenRepository.removeUserTokens will then always only remove tokens related to the particular device.
The value returned from the getName() call can be customized by changing logic in your UserDetailsService implementation, as the Authentication.getName() typically delegates to UserDetails.getUsername() of the object returned from the UserDetailsService.
Other possible solutions will most likely require custom implementation of the org.springframework.security.web.authentication.RememberMeServices interface.
I am also a newbie in this world of Spring but I hope I can give you a tip...
Taking a look at PersistentTokenRepository you can notice that the only way to get the remember-me token is by using the seriesId stored in the cookie of your local browser. That way, when you log out on that device you only remove the "token) (actually the entire entry) of the corresponding seriesId.
So my try is to recommend you to just log out your user in order to delete its associated token.
I really hope that helps!
Default scope for a bean in spring is singleton. However when I have next service defined:
#Service("usersLoggedInService")
public class UsersLoggedInServiceImpl implements UsersLoggedInService {
private Map<Long, String> OnlineUsers = new LinkedHashMap<Long, String>();
#Override
public Map<Long, String> getOnlineUsers() {
return OnlineUsers;
}
#Override
public void setOnlineUsers(Long id, String username) {
OnlineUsers.put(id, username);
}
#Override
public void removeLoggedOutUser(Long id){
if(!OnlineUsers.isEmpty() && OnlineUsers.size()>0)
OnlineUsers.remove(id);
}
}
and using it for login auditing so whenever new user logged in I am adding it to OnlineUsers LinkedHashMap in next way:
usersLoggedInService.setOnlineUsers(user.getId(), user.getUsername());
in one of my service classes. This works fine and I can see the users added in map.
But, when on log out I want to remove the user added in LinkedhashMap and when I check usersLoggedInService.getOnlineUsers() I could see that its empty. I don't understand why.
Logout handler definition:
<logout invalidate-session="true"
logout-url="/logout.htm"
success-handler-ref="myLogoutHandler"/>
And its implementation:
#Component
public class MyLogoutHandler extends SimpleUrlLogoutSuccessHandler {
#Resource(name = "usersLoggedInService")
UsersLoggedInService usersLoggedInService;
#Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
if (authentication != null) {
Object principal = authentication.getPrincipal();
if(principal instanceof User){
User user = (User) principal;
usersLoggedInService.removeLoggedOutUser(user.getId());
}
}
setDefaultTargetUrl("/login");
super.onLogoutSuccess(request, response, authentication);
}
}
Please let me know where the problem is. I don't expect this map to be empty.
-----Updated ------
When new users logged in then I can see all the users already added in LinkedHashmap. This method is inside one of the user service class:
#Service("userService")
public class UserServiceImpl implements UserService {
#Autowired
UsersLoggedInService usersLoggedInService;
#Override
public User getUserDetail() {
Authentication auth = SecurityContextHolder.getContext()
.getAuthentication();
Object principal = auth.getPrincipal();
if(principal instanceof User){
User user = (User) principal;
usersLoggedInService.setOnlineUsers(user.getId(), user.getUsername());
return user;
}
return null;
}
}
when users logged in suppose two users logged in I get Map as {1=user1, 9=user2} but if any of the users logged out then inside onLogoutSuccess() method I get map value as {}. Now if one more user logged in then I get map {1=user1, 9=user2, 3=user3}. So,Map is getting empty inside onLogoutSuccess() method only and it showing populated values everywhere else.
From what you've described it looks like that new instance of service is created for handler.
It might be that default scope of your configuration is not singleton(it's should be easy to check).
Also, could you please try to use #Autowired annotation? There is subtle difference between #Autowire and #Resource from documentation it looks like it shouldn't cause such issue but worth to try anyway:
#Component
public class MyLogoutHandler extends SimpleUrlLogoutSuccessHandler {
#Autowired
private UsersLoggedInsService usersLoggedInService;
// ...
}
-----Update #1 ------
Yeap, try #Autowired with #Qualifier(when I tried that example without qualifier spring created two instances):
#Autowired
#Qualifier("usersLoggedInService")
UsersLoggedInService usersLoggedInService;
----Update #2 ------
Well, I simply copied all your code in sample project and it works on my machine.
Is it possible for you to share your codebase on service like GitHub?
Please show the class that is calling setOnlineUsers. And how and at what place did you check that map is not empty?
Try putting a logger in method onLogoutSuccess and check if you are getting a new or same instance of UsersLoggedInService.
And I am assuming you have a typo in the bean that you are wiring UsersLoggedIn*s*Service. There is an extra 's'