What I have currently
I'm currently implementing an OIDC Resource Provider for my company. They use their intern OIDC servers, which I managed to work with by following this example: https://github.com/jgrandja/oauth2login-demo/tree/linkedin
I'm now able to retrieve user information from the Authorization Server, like that:
#RestController
#RequestMapping("/some/route")
public class SomeController {
#GetMapping("/some/route")
public ResponseEntity<?> getSomething(#RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
String userInfoEndpointUri = authorizedClient.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUri();
Map userAttributes = this.webClient
.get()
.uri(userInfoEndpointUri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(Map.class)
.block();
String firstName = (String) userAttributes.get("first_name");
String lastName = (String) userAttributes.get("last_name");
...
}
}
What I'd like
I am now searching for a solution to map the userAttributes to an Object prior to
the controller method call, so that I get e.g.:
#GetMapping("/some/route")
public ResponseEntity<?> getSomething(MyCostumUserBean user) {
String firstName = user.getFirstName();
String lastName = user.getLastName();
...
}
I read something about the ChannelInterceptor and HandlerInterceptor and also the PrincipalExtractor and AuthoritiesExtractor.
The problem is, that I am just learning the Spring framework and these possibilities are overwhelming me.
It would be a plus if that method would allow some validation and would immediately respond with Error codes if the validation fails.
After that is achieved, I would like to add additional information to MyCostumUserBean from another server, which I send the identity of the current session's user to and receive e.g. Role/Permissions of that user.
I tried to put it in a picture:
Question
What is the proper / by the Spring Framework intended way to deal with that? How do I achieve that?
Extra: Is it secure to rely on OAuth2AuthorizedClient.getPrincipalName()? Or can that be faked by an user, by faking the Cookie/Token?
I think you are asking the way to configure the success handler, or a filter which can check the user attributes.
If this is what you are asking, There are many ways to do it.
For examples:
Use User scope check:(need to assign the scope to the user in advance.)
#ResponseBody
#GetMapping("/some/route")
public String getSomeThing(#RegisteredOAuth2AuthorizedClient("custom") OAuth2AuthorizedClient authorizedClient) {
Set<String> scopes = authorizedClient.getAccessToken()
.getScopes();
if (scopes.contains("users:read")) {
} else if (scopes.contains("users:read")) {
return " page 1";
} else {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Forbidden.");
}
}
You can put some logic in the successHandler:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**")
.permitAll().and()
.formLogin()
.successHandler(successHandler());
}
#Bean
public CustomSuccessHandler successHandler() {
return new CustomSuccessHandler();
}
If you want to apply a filter for your Security Chians:
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
...
.and()
.addFilterBefore(getBeforeAuthenticationFilter(), CustomBeforeAuthenticationFilter.class)
.formLogin()
.loginPage()
.permitAll()
...
}
public UsernamePasswordAuthenticationFilter getBeforeAuthenticationFilter() throws Exception {
CustomBeforeAuthenticationFilter filter = new CustomBeforeAuthenticationFilter();
....
return filter;
}
}
You can also achieve the same purpose by using a Customizing Filter Chains, by give the different order and the relative login in it.
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapterForUserGroup1 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...;
}
}
#Configuration
#Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapterForUserGroup2 extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
...;
}
}
Related
Imagine the following (hypothetical) data structure
endpoint | username | password
users admin 123
info george awd
data magnus e4
this means that every endpoint requires different credentials and no one username/password combo can log in to every endpoint. I am looking for a way to make this scalable in our Spring MVC project when adding more endpoints. We could use roles and hardcore this into the config class but the endpoints and login combinations vary for every customer installation
Given the following SecurityConfiguration with LookupAuthenticationService being the class that looks up the username/password data in the database
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String[] ENDPOINT_LIST = {
"/rest/**"
};
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(ENDPOINT_LIST)
.authenticated()
.and()
.httpBasic();
}
#Autowired
protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
#Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService());
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
protected UserDetailsService userDetailsService() {
return new LookupAuthenticationService(passwordEncoder());
}
}
The ideal situation would be if LookupAuthenticationService has access to the request so we know which endpoint to fetch but I guess this is only possible when working with individual Filters
The possibilities I've found so far are:
Add a WebSecurityConfigurerAdapter and multiple UserDetailsServer specific per endpoint -> lots of code
Add a HandlerInterceptor per endpoint -> lots of code
AuthenticationManagerResolver returning a different AuthenticationManager based on pathInfo?
Any input how to best resolve this issue would be appreciated
You can have a table where you map endpoints to rules, like so:
pattern
authority
/users/**
ROLE_ADMIN
/info/**
ROLE_USER
/another/**
ROLE_ANOTHER
And instead of assigning a user to an endpoint, you assign a role to the users. With this in place, you can create an AuthorizationManager which is going to protect your endpoints based on the request path.
#Component
public class AccessRuleAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final AccessRuleRepository rules;
private RequestMatcherDelegatingAuthorizationManager delegate;
public AccessRuleAuthorizationManager(AccessRuleRepository rules) {
this.rules = rules;
}
#Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
return this.delegate.check(authentication, object.getRequest());
}
#EventListener
void applyRules(ApplicationReadyEvent event) {
Builder builder = builder();
for (AccessRule rule : this.rules.findAll()) {
builder.add(
new AntPathRequestMatcher(rule.getPattern()),
AuthorityAuthorizationManager.hasAuthority(rule.getAuthority())
);
}
this.delegate = builder.build();
}
}
And, in your SecurityConfiguration you simply do this:
#Autowired
private AccessRuleAuthorizationManager access;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) ->
authz.anyRequest().access(this.access)
)
.httpBasic(Customizer.withDefaults());
}
I recommend you to take a look at this repository and watch the presentation from the repository's description. The last steps of the presentation was adding the custom AuthorizationManager, and there's a great explanation about it.
I am trying to implement SpringSecurity mechanism on this little project, that will limit interactions with the URL of the request by roles.
I have two roles USER and ADMIN, USER can see the items, but not add or delete them, while ADMIN can do both.
Now the problem, the requests from USER role and even unauthenticated users to create/delete/read an item are allowed somehow. It seems to me that my application is not configured correctly somewhere.
SecurityConfig:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}12345").roles("USER").and()
.withUser("admin").password("{noop}12345").roles("ADMIN");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests()
.antMatchers("api/**").hasRole("ADMIN")
.antMatchers("api/items", "api/items/").hasRole("USER")
.anyRequest().authenticated()
.and().csrf().disable().headers().frameOptions().disable();
}
}
Controller:
#RestController
public class ItemController {
#Autowired
private ItemService itemService;
#GetMapping("/api/items")
public List<Item> getItems() {
return itemService.getAllItems();
}
#PostMapping(value = "/api/addItem",consumes = {"application/json"},produces = {"application/json"})
#ResponseBody
public Item addItem(#RequestBody Item item) {
itemService.addItem(item);
return item;
}
#DeleteMapping(value = "api/deleteItem/{id}")
#ResponseBody
public String deleteItem(#PathVariable int id) {
itemService.deleteItem(id);
return "Deleted";
}
}
I am sending requests to the following URL's:
http://localhost:8080/api/items // GET
http://localhost:8080/api/addItem // POST
http://localhost:8080/api/deleteItem/4 // DELETE
Have you tried adding slashes to your antmatcher patterns, such as:
antMatchers("/api/**").hasRole("ADMIN")
The Spring documentation mentions:
Note: a pattern and a path must both be absolute or must both be relative in order for the two to match. Therefore it is recommended that users of this implementation to sanitize patterns in order to prefix them with "/" as it makes sense in the context in which they're used.
Furthermore, Spring security uses the first match of all the matching rules expressed. I would recommend reordering the matchers from most-specific to less-specific as otherwise a call to api/items will be matched by the api/** matcher instead of being matched by the api/items matcher.
.antMatchers("/api/items", "api/items/").hasRole("USER")
.antMatchers("/api/**").hasRole("ADMIN")
On top of #GetMapping or #PostMapping you can add following annotation to manage Role based access
#PreAuthorize("hasAnyRole('ROLE_ADMIN')")
#PostMapping(value = "/api/addItem",consumes = {"application/json"},produces = {"application/json"})
#ResponseBody
public Item addItem(#RequestBody Item item) {
itemService.addItem(item);
return item;
}
In Spring MVC with Spring Security, is it possible to achieve this?
#Override WebSecurityConfigurerAdapter.configure(HttpSecurity)
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.mvcMatchers("/users/{authentication.principal.username}").hasAnyRole(ADMIN, MANAGER)
.antMatchers("/users/**").hasRole(ADMIN)
.anyRequest().authorized()
...
}
/users/** is a restricted area and should be accessible by admins only. But managers should still be able to see their own profile (/users/user_with_manager_role), and only their own profile, not those of any other users (regardless of their role).
Solution
I've found a solution in Andrew's answer. My Code now looks like this:
WebSecurityConfigurerAdapter
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true) // added this annotation
public class SecurityConfig extends WebSecurityConfigurerAdapter
#Override WebSecurityConfigurerAdapter.configure(HttpSecurity)
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
// removed /users handling
.anyRequest().authorized()
...
}
UsersController
#Controller
#RequestMapping("/users")
public class UsersController
{
#GetMapping("{username}")
#PreAuthorize("authentication.principal.username == #username) || hasRole('ADMIN')")
public String usersGet(#PathVariable("username") String username)
{
// do something with username, for example get a User object from a JPA repository
return "user";
}
}
I'm afraid it's not possible: when this configuration is being set up, it has no info about {authentication.principal.username} which will be resolved at some point in future.
But Spring gives you a bunch of built-in method security expressions you can annotate your methods with.
Starting from a simple expression like #PreAuthorize("hasRole('ADMIN')"), you might end up with a custom one:
#XMapping(path = "/users/{username}")
#PreAuthorize("#yourSecurityService.isMyPage(authentication.principal, #username)")
public void yourControllerMethod(#PathVariable String username);
#yourSecurityService.isMyPage(authentication.principal, #username) refers to your #Service method public boolean isMyPage(Principal, String).
How about something like this:
#Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/myself").hasAnyRole(ADMIN, MANAGER)
.antMatchers("/users/**").hasRole(ADMIN)
.anyRequest().hasAnyRole(ADMIN, MANAGER)
...
}
#RequestMapping(value = "/myself", method = RequestMethod.GET)
public Profile getMyself() {
// return the profile of the loged in user
}
With this manager and admins can get their own profile and admins can also request other profiles with /users/{username}.
My main objective is to store the client-id of the each user, once they login with google. This github repo contains most of what I needed till now. The two main files of concern are OAuthSecurityConfig.java and UserRestController.java.
When I navigate to /user, the Principal contains all the details I need on the user. Thus I can use the following snippets to get the data I need:
Authentication a = SecurityContextHolder.getContext().getAuthentication();
String clientId = ((OAuth2Authentication) a).getOAuth2Request().getClientId();
I can then store the clientId in a repo
User user = new User(clientId);
userRepository.save(user);
The problem with this is that users do not have to navigate to /user. Thus, one can navigate to /score/user1 without being registered.
This API is meant to be a backend for an android application in the future, so a jquery redirect to /user would be insecure and would not work.
Things I have tried:
Attempt 1
I created the following class:
#Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
#Autowired
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("User %s does not exist!", username));
}
return new UserRepositoryUserDetails(user);
}
}
and overrode the WebSecurityConfigurerAdapterwith:
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
Both overridden methods are not called when a user logs in (I checked with a System.out.println)
Attempt 2
I tried adding .userDetailsService(customUserDetailsService)
to:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// Starts authorizing configurations.
.authorizeRequests()
// Do not require auth for the "/" and "/index.html" URLs
.antMatchers("/", "/**.html", "/**.js").permitAll()
// Authenticate all remaining URLs.
.anyRequest().fullyAuthenticated()
.and()
.userDetailsService(customUserDetailsService)
// Setting the logout URL "/logout" - default logout URL.
.logout()
// After successful logout the application will redirect to "/" path.
.logoutSuccessUrl("/")
.permitAll()
.and()
// Setting the filter for the URL "/google/login".
.addFilterAt(filter(), BasicAuthenticationFilter.class)
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
Both methods were still not called, and I don't feel like I am any closer to the solution. Any help will be greatly appreciated.
The way to go here is to provide a custom OidcUserService and override the loadUser() method because Google login is based on OpenId Connect.
First define a model class to hold the extracted data, something like this:
public class GoogleUserInfo {
private Map<String, Object> attributes;
public GoogleUserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public String getId() {
return (String) attributes.get("sub");
}
public String getName() {
return (String) attributes.get("name");
}
public String getEmail() {
return (String) attributes.get("email");
}
}
Then create the custom OidcUserService with the loadUser() method which first calls the provided framework implementiation and then add your own logic for persisting the user data you need, something like this:
#Service
public class CustomOidcUserService extends OidcUserService {
#Autowired
private UserRepository userRepository;
#Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
try {
return processOidcUser(userRequest, oidcUser);
} catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex.getCause());
}
}
private OidcUser processOidcUser(OidcUserRequest userRequest, OidcUser oidcUser) {
GoogleUserInfo googleUserInfo = new GoogleUserInfo(oidcUser.getAttributes());
// see what other data from userRequest or oidcUser you need
Optional<User> userOptional = userRepository.findByEmail(googleUserInfo.getEmail());
if (!userOptional.isPresent()) {
User user = new User();
user.setEmail(googleUserInfo.getEmail());
user.setName(googleUserInfo.getName());
// set other needed data
userRepository.save(user);
}
return oidcUser;
}
}
And register the custom OidcUserService in the security configuration class:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomOidcUserService customOidcUserService;
#Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.oidcUserService(customOidcUserService);
}
}
Mode detailed explanation can be found in the documentation:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2login-advanced-oidc-user-service
In case of some one else is stuck with this, my solution was to create a custom class extending from
OAuth2ClientAuthenticationProcessingFilter and then override the successfulAuthentication method to get the user authentication details and save it to my database.
Example (kotlin):
On your ssoFilter method (if you followed this tutorial https://spring.io/guides/tutorials/spring-boot-oauth2) or wharever you used to register your ouath clients, change the use of
val googleFilter = Auth2ClientAuthenticationProcessingFilter("/login/google");
for your custom class
val googleFilter = CustomAuthProcessingFilter("login/google")
and of course declare the CustomAuthProcessingFilter class
class CustomAuthProcessingFilter(defaultFilterProcessesUrl: String?)
: OAuth2ClientAuthenticationProcessingFilter(defaultFilterProcessesUrl) {
override fun successfulAuthentication(request: HttpServletRequest?, response: HttpServletResponse?, chain: FilterChain?, authResult: Authentication?) {
super.successfulAuthentication(request, response, chain, authResult)
// Check if user is authenticated.
if (authResult === null || !authResult.isAuthenticated) {
return
}
// Use userDetails to grab the values you need like socialId, email, userName, etc...
val userDetails: LinkedHashMap<*, *> = userAuthentication.details as LinkedHashMap<*, *>
}
}
You can listen to AuthenticationSuccessEvent. For example:
#Bean
ApplicationListener<AuthenticationSuccessEvent> doSomething() {
return new ApplicationListener<AuthenticationSuccessEvent>() {
#Override
void onApplicationEvent(AuthenticationSuccessEvent event){
OAuth2Authentication authentication = (OAuth2Authentication) event.authentication;
// get required details from OAuth2Authentication instance and proceed further
}
};
}
I have implemented Spring Security for a RESTful web service project. It has Request Mappings with same url patterns but with different Request Method types.
#RequestMapping(value = "/charity/accounts", method = RequestMethod.POST)
public AccountResponseDto createAccount(HttpServletResponse response, #RequestBody AccountRequestDto requestDTO) {
// some logics here
}
#RequestMapping(value = "/charity/accounts", method = RequestMethod.GET)
public AccountResponseDto getAccount(HttpServletResponse response) {
// some logics here
}
#RequestMapping(value = "/charity/accounts", method = RequestMethod.PUT)
public void updateAccount(HttpServletResponse response, #RequestBody AccountRequestDto requestDTO){
// some logics here
}
Currently all of these methods require Authorization to execute, but I need to remove authorization for createAccount(...) method. Are there annotation based solutions?
Note: I need a solution that will not effect to do changes for url patterns, as it will impact in many other modules.
That's why we have roles,authorizations..first we can define who can GET/PUT/POST and grant authorities to the user accordingly.
Then we can annotate as #Secured("ROLE_ADMIN") on GET/PUT/POST methods accordingly.
To unsecure GET, you can add #PreAuthorize("isAnonymous()") or #Secured("MY_CUSTOM_ANONYM_ROLE")
Below is a sample configuration which would permit requests for signup and about:
#EnableWebSecurity
#Configuration
public class CustomWebSecurityConfigurerAdapter extends
WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
auth
.inMemoryAuthentication()
.withUser("user") // #1
.password("password")
.roles("USER")
.and()
.withUser("admin") // #2
.password("password")
.roles("ADMIN","USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeUrls()
.antMatchers("/signup","/about").permitAll();
}
}
You can refer Spring Security Java Config for detailed information.
A suggestion on your Controller. If all requests prefixed with /charity to be handled by CharityController, you can map requests in the below way:
#Controller
#RequestMapping(value="/charity")
class CharityController {
#RequestMapping(value = "/accounts", method = RequestMethod.GET)
public AccountResponseDto getAccount(HttpServletResponse response){
}
}
Update
The following should work for you.
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(HttpMethod.POST, new String [] {"/charity/accounts", "/charity/people"}).permitAll();
}