Spring security authentication based on request parameter - spring

The application I'm working on already has Spring Security to handle form based authentication. Now the requirement is to login a user programmatically via an external service if a token is found in one of the request parameters.
In other words, if a particular request parameter, say "token", exists, it needs to call an external service with that token to verify if it's a valid token. If it is then the user will be logged in.
I can't figure out how and where to "trigger" or "hook on to" Spring Security to check this parameter and make the verification then authenticate the user when appropriate since there is no login form. I thought there should be something in Spring Security that can be extended or customized to do this?
I looked through Spring Security documentation and wonder if AbstractPreAuthenticatedProcessingFilter is the right thing to start with?

I have a similar setup in my application. Here are the basic elements as far as I can tell:
You need to create an AuthenticationProvider like so:
public class TokenAuthenticationProvider implements AuthenticationProvider {
#Autowired private SomeService userSvc;
#Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
if (auth.isAuthenticated())
return auth;
String token = auth.getCredentials().toString();
User user = userSvc.validateApiAuthenticationToken(token);
if (user != null) {
auth = new PreAuthenticatedAuthenticationToken(user, token);
auth.setAuthenticated(true);
logger.debug("Token authentication. Token: " + token + "; user: " + user.getDisplayName());
} else
throw new BadCredentialsException("Invalid token " + token);
return auth;
}
}
You also need to create a Filter to turn the custom parameter into an authentication token:
public class AuthenticationTokenFilter implements Filter {
#Override
public void init(FilterConfig fc) throws ServletException {
}
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain fc) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.getContext();
if (context.getAuthentication() != null && context.getAuthentication().isAuthenticated()) {
// do nothing
} else {
Map<String,String[]> params = req.getParameterMap();
if (!params.isEmpty() && params.containsKey("auth_token")) {
String token = params.get("auth_token")[0];
if (token != null) {
Authentication auth = new TokenAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
}
fc.doFilter(req, res);
}
#Override
public void destroy() {
}
class TokenAuthentication implements Authentication {
private String token;
private TokenAuthentication(String token) {
this.token = token;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<GrantedAuthority>(0);
}
#Override
public Object getCredentials() {
return token;
}
#Override
public Object getDetails() {
return null;
}
#Override
public Object getPrincipal() {
return null;
}
#Override
public boolean isAuthenticated() {
return false;
}
#Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
}
#Override
public String getName() {
// your custom logic here
}
}
}
You need to create beans for these:
<beans:bean id="authTokenFilter" class="com.example.security.AuthenticationTokenFilter" scope="singleton" />
<beans:bean id="tokenAuthProvider" class="com.example.security.TokenAuthenticationProvider" />
Finally, you need to wire these beans into your security config (adjust accordingly):
<sec:http >
<!-- other configs here -->
<sec:custom-filter ref="authTokenFilter" after="BASIC_AUTH_FILTER" /> <!-- or other appropriate filter -->
</sec:http>
<sec:authentication-manager>
<!-- other configs here -->
<sec:authentication-provider ref="tokenAuthProvider" />
</sec:authentication-manager>
There might be another way, but this definitely works (using Spring Security 3.1 at the moment).

If you use Spring MVC controller or service, where targe request parameter is passed, then you can use #PreAuthorize Spring security annotation.
Say, you have some Spring service that can check passed token and perform authentication if passed token is valid one:
#Service("authenticator")
class Authenticator {
...
public boolean checkTokenAndAuthenticate(Object token) {
...
//check token and if it is invalid return "false"
...
//if token is valid then perform programmatically authentication and return "true"
}
...
}
Then, with Spring security #PreAuthorize annotation you can do this it next way:
...
#PreAuthorize("#authenticator.checkTokenAndAuthenticate(#token)")
public Object methodToBeChecked(Object token) { ... }
...
Also, you should enable Spring security annotations by and add spring-security-aspects to POM (or jar to classpath).

Related

Using JWT with #PathVariable but only allow access url for spesific user

I am creating simple Rest social media application with Spring Boot. I use JWT for authentication in application.
In my mobile application when users register, i am getting some information from users and create account and profile of the user.
By the way, you can see (simplified) database object of account and profile. I use Mongo DB for database.
account:
{
“_id”: “b6164102-926e-47d8-b9ff-409c44dc47c0“,
“email”: “xxx#yy.com”
….
}
profile:
{
“_id”: “35b06171-c16a-4559-90f3-df81ace6d64a“,
“accountId”: “b6164102-926e-47d8-b9ff-409c44dc47c0”,
profileImages: [
{
“imageId”: “1431b0bc-feb7-436d-9d3a-7b9094547bf6”,
“imageLink”: “https://this_is_some_link_to_image.com
}
….
]
….
}
When user login to app, i add accountId to JWT and then in my mobile app i call below endpoint to get profile information of user. I take accountId from jwt and find profile of that account id.
#GetMapping("/profiles")
public ResponseEntity<BaseResponse> getUserProfile(#AuthenticationPrincipal AccountId accountId) {
var query = new Query(accountId);

 var presenter = new GetUserProfilePresenter();


 useCase.execute(query, presenter);


 return presenter.getViewModel();
}
In the app, users can upload photo to their profile using below endpoint;
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#PathVariable("profileId") UUID profileId, #RequestParam("image") MultipartFile image) throws IOException {

......
}
Everything works fine but the problem is someone can use their token to call this url with another person’s profileId. Because profileId is not a hidden id. In my mobile app users can shuffle and see other users profile using below url.
This url is accessible by any authenticated users.
#GetMapping(path = "/profiles/{profileId}")
public ResponseEntity<BaseResponse> getProfile(#PathVariable("profileId") UUID profileId) {
......
}
Now, my question is how can i make "/profiles/{profileId}/images" this url is only accesible for user of this profile without changing path format.
For exampe;
User A - Profile Id = XXX
User B - Profile Id = YYY
I want that if User A calls this url with own JWT Token, uploads image only to own profile not another one profile.
I have come up with some solutions but these solutions cause me to change the url path;
Solution 1:
I can use accountId in the jwt. Find profile of user with this accountId so that, every call to this url guaranteed upload image only to profile of token user.
But this solution change url path like below because i dont need to get any profileId from path.
#PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public ResponseEntity<BaseResponse> uploadProfileImage(

 #AuthenticationPrincipal AccountId accountId, #RequestParam("image") MultipartFile image) throws IOException {


 ......
}
Solution 2:
This is very similar to first solution only different is when i create jwt for user. I will put profileId of user to inside of JWT. So when the user calls the url i will get profileId from jwt and put inside of Authentication object. And in the controller i will get this profileId for using to find profile of user then upload image to this spesific profile.
But also, this solution change url path format because i dont need to get profileId from url path.
So if i back to my main question. What is the best practices and solutions for these kinda problems and situations?
~~~EDIT~~~
For those whose wonder, i didn't change my path. Actually i implemented solution 1 with a twist.
Now i use accountId from JWT and profileId at the same time so when i want to find a profile of exactly that user i search the database using accountId and profileId together.
With this change, i didn't need to change other paths.
For example; (GET) /profiles/{profileId} this path still meaningful for all authenticated users.
But (POST) /profiles/{profileId}/images this path only meaningful for that spesific (owner of token) user.
By the way, i starts paths with "api/admin/**" prefix for my admin role operations.
Final code (Controller);
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal AccountId accountId,
#PathVariable("profileId") UUID profileId,
#RequestParam("image") MultipartFile image) throws IOException {
....
}
Final code (Repository);
#Repository
public interface ProfileJpaRepository extends MongoRepository<ProfileDto, String> {
Optional<ProfileDto> findByAccountId(String accountId);
Optional<ProfileDto> findByIdAndAccountId(String profileId, String accountId);
}
The best practice to handle this kind of scenarios is to have two endpoints, each needing different kind of permissions:
"/profiles/{profileId}/images" will be available for admins, so that if an admin wants to change another user's profile image, they can do so by calling this endpoint.
"/profiles/images" will be responsible for changing the most generic users with the lowest privileges.
So, in both scenarios you need to extract the AccountId from the JWT and you should not get the AccountId from the user directly, unless for administration purposes where you check the privileges to authorize the user.
Now, the best way to implement such a system, is to use Spring Security and to create a custom AuthenticationToken, then to customize AbstractUserDetailsAuthenticationProvider, * AbstractAuthenticationProcessingFilter* and UsernamePasswordAuthenticationToken.
After doing so, you can then configure Spring to use the custom provider for authentication.
UsernamePasswordAuthenticationToken
public class JwtAuthenticationToken extends UsernamePasswordAuthenticationToken {
private Payload payload; // Payload can be any model class that encapsulates the payload of the JWT.
private boolean creationAllowed;
public JwtAuthenticationToken(String jwtToken) throws Exception {
super(null, jwtToken);
// Verify JWT and get the payload
this.payload = // set the payload
}
public JwtAuthenticationToken(String principal, JwtAuthenticationToken authToken, Collection<? extends GrantedAuthority> authorities) {
super(principal, authToken.getCredentials(), authorities);
this.payload = authToken.payload;
authToken.eraseCredentials(); // not sure if this is needed
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public Payload getPayload() {
return this.firebaseToken;
}
public boolean isCreationAllowed() {
return creationAllowed;
}
public void setCreationAllowed(boolean creationAllowed) {
this.creationAllowed = creationAllowed;
}
}
AbstractUserDetailsAuthenticationProvider
#Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
#Autowired
AppUserService appUserService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(JwtAuthenticationToken.class, authentication, () ->
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only JwtAuthenticationToken is supported")
);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) authentication;
String principal;
try {
principal = jwtAuthToken.getPayload().getEmail(); // Here I'm using email as the user identifier, this can be anything, for example AccountId
} catch (RuntimeException re) {
throw new AuthenticationException("Could not extract user's email address.");
}
AppUser user = (AppUser) this.retrieveUser(principal, jwtAuthToken);
return this.createSuccessAuthentication(principal, jwtAuthToken, user);
}
#Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
JwtAuthenticationToken result = new JwtAuthenticationToken((String) principal, (JwtAuthenticationToken) authentication, user.getAuthorities());
result.setDetails(user);
return result;
}
#Override
public UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
UserDetails userDetails = appUserService.loadUserByUsername(s);
JwtAuthenticationToken jwtAuthToken = (JwtAuthenticationToken) usernamePasswordAuthenticationToken;
if (userDetails != null)
return userDetails; // You need to create an UserDetails which will be set by the framework to the Security Context as the authenticated user, this will be useful later when you want to check the privileges.
else
throw new AuthenticationException("Creating the user details is not allowed.");
}
#Override
protected void additionalAuthenticationChecks(final UserDetails d, final UsernamePasswordAuthenticationToken auth) {
// Nothing to do
}
#Override
public boolean supports(Class<?> authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
}
}
AbstractAuthenticationProcessingFilter
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
super("/**"); // The path that this filter needs to process, use "/**" to make sure all paths must be proessed.
}
#Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
return true; // Here I am returning true to require authentication for all requests.
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String authorization = request.getHeader("Authorization");
if (authorization == null || !authorization.startsWith("Bearer "))
throw new AuthenticationException("No JWT token found in request headers");
String authToken = authorization.substring(7);
JwtAuthenticationToken token = new JwtAuthenticationToken(authToken);
return getAuthenticationManager().authenticate(token);
}
#Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
super.successfulAuthentication(request, response, chain, authResult);
// Authentication process succeed, filtering the request in.
// As this authentication is in HTTP header, after success we need to continue the request normally
// and return the response as if the resource was not secured at all
chain.doFilter(request, response);
}
#Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
super.unsuccessfulAuthentication(request, response, failed);
// Authentication process failed, filtering the request out.
}
}
UserDetails
public class AppUser implements UserDetails {
// A class to be used as a container for user details, you can add more details specific to your application here.
}
Finally, you need to configure Spring boot to use this classes:
SecurityConfig
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final RequestMatcher PUBLIC_URLS = new OrRequestMatcher(
// -- public paths, for example: swagger ui paths
new AntPathRequestMatcher("/swagger-ui.html"),
new AntPathRequestMatcher("/swagger-resources/**"),
new AntPathRequestMatcher("/v2/api-docs"),
new AntPathRequestMatcher("/webjars/**")
);
private JwtAuthenticationProvider provider;
public SecurityConfig(JwtAuthenticationProvider provider) {
this.provider = provider;
}
#Override
public void configure(final WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS) // Allowing browser pre-flight
.requestMatchers(PUBLIC_URLS);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling()
// this entry point handles when you request a protected page and you are not yet authenticated
//.defaultAuthenticationEntryPointFor(forbiddenEntryPoint(), PROTECTED_URLS)
.authenticationEntryPoint(forbiddenEntryPoint())
.and()
.authenticationProvider(this.provider)
.addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class)
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
}
#Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
final JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
filter.setAuthenticationManager(this.authenticationManager());
filter.setAuthenticationSuccessHandler(this.successHandler());
filter.setAuthenticationFailureHandler(this.failureHandler());
return filter;
}
#Bean
JwtAuthenticationSuccessHandler successHandler() {
return new JwtAuthenticationSuccessHandler();
}
#Bean
JwtAuthenticationFailureHandler failureHandler() {
return new JwtAuthenticationFailureHandler();
}
/**
* Disable Spring boot automatic filter registration.
*/
#Bean
FilterRegistrationBean disableAutoRegistration(JwtAuthenticationFilter filter) {
final FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
#Bean
AuthenticationEntryPoint forbiddenEntryPoint() {
return new HttpStatusEntryPoint(FORBIDDEN);
}
}
AuthenticationFailureHandler
public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
Map<String, Object> data = new HashMap<>();
data.put("exception", e.getMessage());
httpServletResponse.getOutputStream().println(objectMapper.writeValueAsString(data));
}
}
AuthenticationSuccessHandler
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
}
}
OKAY!
Now that you have implemented the security correctly, you can access user details and privileges from anywhere using the last piece:
UserDetailsService
#Service
public class AppUserService implements UserDetailsService {
#Autowired
private AppUserRepository appUserRepository;
public AppUser getCurrentAppUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null)
return (AppUser) authentication.getDetails();
return null;
}
public String getCurrentPrincipal() {
return (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
#Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
Optional<AppUser> appUserOptional = this.appUserRepository.findByEmailsContains(new EmailEntity(s)); // This should be changed in your case if you are using something like AccountId
appUserOptional.ifPresent(AppUser::loadAuthorities);
return appUserOptional.orElse(null);
}
}
Great.
Let's see how to use it in your Controllers:
#PostMapping(path = "/profiles/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(#RequestParam("image") MultipartFile image) throws IOException {

AppUser user = this.appUserService.getCurrentAppUser();
Long id = user.getAccountId(); // Or profile id or any other identifier that you needed and extracted from the JWT after verification.
// set the profile picture.
// save changes of repository and return.
}
For admin purposes:
#PreAuthorize ("hasRole('ROLE_ADMIN')")
#PostMapping(path = "/profiles/{profileId}/images", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#PathVariable("profileId") UUID profileId, #RequestParam("image") MultipartFile image) throws IOException {

AppUser user = this.appUserService.getCurrentAppUser();
// set the profile picture using profileId parameter
// save changes of repository and return.
}
The only remaining task is to assign the ROLE_ADMIN to the right user when loading it from the database. To do this, there are a lot of different approaches and it totally depends on your requirements. Overall, you can save a role in the database and relate it to a specific user and simply load it using an Entity.
Let's get few things right here , I am assuming that you have like two entities - Account and Profile and you wish to upload/update new profile image using same API -
#PostMapping(path = "/profiles/{profileId}/images
If ADMIN role , update profile image for #PathVariable("profileId") OR if USER role update their own profile image using #PathVariable("profileId") and not any other Profile entity image using ProfileId if current user is authenticated.
Please check this link for Role-Permission Authentication
Spring Boot : Custom Role - Permission Authorization using SpEL
User Principal
#Getter
#Setter
#Builder
public class UserPrincipal implements UserDetails {
/**
* Generated Serial ID
*/
private static final long serialVersionUID = -8983688752985468522L;
private Long id;
private String email;
private String password;
private Collection<? extends GrantedAuthority> authorities;
private Collection<? extends GrantedAuthority> permissions;
public static UserPrincipal createUserPrincipal(Account account) {
if (userDTO != null) {
List<GrantedAuthority> authorities = userDTO.getRoles().stream().filter(Objects::nonNull)
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
List<GrantedAuthority> permissions = account.getRoles().stream().filter(Objects::nonNull)
.map(Role::getPermissions).flatMap(Collection::stream)
.map(permission -> new SimpleGrantedAuthority(permissionDTO.getName().name()))
.collect(Collectors.toList());
return UserPrincipal.builder()
.id(account.getId())
.email(account.getEmail())
.authorities(authorities)
.permissions(permissions)
.build();
}
return null;
}
AuthenticationFilter
public class AuthTokenFilter extends OncePerRequestFilter {
#Autowired
private JwtUtils jwtUtils;
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwtToken = getJwtTokenFromHttpRequest(request);
if (StringUtils.isNotBlank(jwtToken) && jwtUtils.validateToken(jwtToken)) {
Long accountId = jwtUtils.getAccountIdFromJwtToken(jwtToken);
UserDetails userDetails = customUserDetailsService.loadUserByUserId(accountId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails
.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
}
filterChain.doFilter(request, response);
}
private String getJwtTokenFromHttpRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (!StringUtils.isEmpty(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
AuthUtil
#UtilityClass
public class AuthUtils {
public boolean isAdmin(UserPrincipal userPrincipal){
if(CollectionUtils.isNotEmpty(userPrincipal.getAuthorities())){
return userPrincipal.getRoles().stream()
.filter(Objects::nonNull)
.map(GrantedAuthority::getName)
.anyMatch(role -> role.equals("ROLE_ADMIN"));
}
return false;
}
}
Profile Service
#Service
public class ProfileService {
#Autowired
private ProfileRepository profileRepository;
public Boolean validateProfileIdForAccountId(Integer profileId, Long accountId) throws NotOwnerException,NotFoundException {
Profile profile = profileRepository.findByAccountId(profileId,accountId);
if(profile == null){
throw new NotFoundException("Profile does not exists for this account");
} else if(profile.getId() != profileId){
throw new NotOwnerException();
}
return true;
}
}
ProfileController
#PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER')")
#PostMapping(path = "/profiles/{profileId}/images", consumes =
MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<BaseResponse> uploadProfileImage(
#AuthenticationPrincipal UserPrincipal currentUser,
#PathVariable("profileId") UUID profileId,
#RequestParam("image") MultipartFile image) throws IOException {
if(!AuthUtils.isAdmin(currentUser)){
profileService.validateProfileIdForAccountId(profileId, currentUser.getId());
}
}
Now you can validate whether the #PathVariable("profileId") does indeed belong to the authenticated CurrentUser, you are also checking if the CurrentUser is ADMIN.
You can also add & check any specific permission for ROLES for facilitating UPLOAD/UPDATE
#PreAuthorize("hasAnyRole('ROLE_ADMIN','ROLE_USER') or hasPermission('UPDATE')")

spring boot login check implement way

My way of implementing login check
#Configuration
public class ViewControllerImpl implements WebMvcConfigurer {
#Bean
public WebMvcConfigurer webMvcConfigurer(){
WebMvcConfigurer adapter = new WebMvcConfigurer() {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new InterceptorConfig()).addPathPatterns("/**")
.excludePathPatterns("/login", "/", "/session", "/static/**");
}
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
WebMvcConfigurer.super.addResourceHandlers(registry);
}
};
return adapter;
}
}
public class InterceptorConfig implements HandlerInterceptor
{
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
Object loginStatus = request.getSession().getAttribute("loginStatus");
if(loginStatus == "success"){
return true;
}
else {
// request.getRequestDispatcher("login").forward(request, response);
response.sendRedirect("/login");
return false;
}
}
}
After the username and password are successfully verified,I will store a loginstatus in the session.
if loginstatus is success, it means that you have logged in.
Is this code implementation safe?
Do I need to use spring security?
Like #m-deinum already said you should really just utilize Spring-Security. There is a super quick tutorial on how to do this here: https://spring.io/guides/gs/securing-web/
You will start to learn it with BasicAuth and an InMemoryDB for user and password storage. You will end up with an application that returns a status Code 200 for logged in user or a status code 401 for a denied access.
From there on it is easy to extend your application to user user and password stored in a custom db or even OAuth.
Have no fear using it - its easy and fun :)

Spring Security - Adding a custom filter for Authorization

we are implementing some api.
The authentification is done in the Front, and it send us a Bearer in the authorization Header.
We have created a method with validates the token (it calls some rest services xxx ) and another method with returns a list of roles that the user has .
Is it possible to create a filter that will do this verification ??
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
#Override
protected void doFilter(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain) throws IOException, ServletException {
System.out.println("IN FILTER");
String header = req.getHeader("Authorization");
if (header == null || !header.startsWith("Bearer")) {
chain.doFilter(req, res);
return;
}
String token = request.getHeader(HEADER_STRING);
if (validateMyToken(token)) {
List<String> mygroups = getMyGroups(token);
} else {
throw new InvalidTokenException("Token is not Ok :(");
}
#HOW DO I PUT MY GROUPS, SOMEWHERE FOR VERIFICATION ???
}
}
And use it in my controller :
#RestController
#RequestMapping(value = "/applications/")
public class TestController {
#PreAuthorize("hasRole('ROLE_ADMIN')")
#GetMapping(value = "/test")
public String getContacts() {
System.out.println("IN CONTROLLER");
return "toto";
}
}
I know I have to do something in WebSecurity, but don't know how
You need to create a custom UserDetails implementation https://docs.spring.io/spring-security/site/docs/4.2.6.RELEASE/apidocs/org/springframework/security/core/userdetails/UserDetails.html . After that create a proper Authorities collection according to your List<String> mygroups = getMyGroups(token);. If you want to secure methods with roles just mark them with #Secured / #RolesAllowed or just like you wrote with #PreAuthorize("hasRole('ROLE_ADMIN')")

Two factor authentication with spring security oauth2

I'm looking for ideas how to implement two factor authentication (2FA) with spring security OAuth2. The requirement is that the user needs two factor authentication only for specific applications with sensitive information. Those webapps have their own client ids.
One idea that popped in my mind would be to "mis-use" the scope approval page to force the user to enter the 2FA code/PIN (or whatever).
Sample flows would look like this:
Accessing apps without and with 2FA
User is logged out
User accesses app A which does not require 2FA
Redirect to OAuth app, user logs in with username and password
Redirected back to app A and user is logged in
User accesses app B which also does not require 2FA
Redirect to OAuth app, redirect back to app B and user is directly logged in
User accesses app S which does require 2FA
Redirect to OAuth app, user needs to additionally provide the 2FA token
Redirected back to app S and user is logged in
Directly accessing app with 2FA
User is logged out
User accesses app S which does require 2FA
Redirect to OAuth app, user logs in with username and password, user needs to additionally provide the 2FA token
Redirected back to app S and user is logged in
Do you have other ideas how to apporach this?
So this is how two factor authentication has been implemented finally:
A filter is registered for the /oauth/authorize path after the spring security filter:
#Order(200)
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
#Override
protected void afterSpringSecurityFilterChain(ServletContext servletContext) {
FilterRegistration.Dynamic twoFactorAuthenticationFilter = servletContext.addFilter("twoFactorAuthenticationFilter", new DelegatingFilterProxy(AppConfig.TWO_FACTOR_AUTHENTICATION_BEAN));
twoFactorAuthenticationFilter.addMappingForUrlPatterns(null, false, "/oauth/authorize");
super.afterSpringSecurityFilterChain(servletContext);
}
}
This filter checks if the user hasn't already authenticated with a 2nd factor (by checking if the ROLE_TWO_FACTOR_AUTHENTICATED authority isn't available) and creates an OAuth AuthorizationRequest which is put into the session. The user is then redirected to the page where he has to enter the 2FA code:
/**
* Stores the oauth authorizationRequest in the session so that it can
* later be picked by the {#link com.example.CustomOAuth2RequestFactory}
* to continue with the authoriztion flow.
*/
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
#Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (AuthenticationUtil.isAuthenticated() && !AuthenticationUtil.hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authenticatoin. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
// redirect the the page where the user needs to enter the two factor authentiation code
redirectStrategy.sendRedirect(request, response,
ServletUriComponentsBuilder.fromCurrentContextPath()
.path(TwoFactorAuthenticationController.PATH)
.toUriString());
return;
} else {
request.getSession().removeAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
}
}
filterChain.doFilter(request, response);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
}
The TwoFactorAuthenticationController that handles entering the 2FA-code adds the authority ROLE_TWO_FACTOR_AUTHENTICATED if the code was correct and redirects the user back to the /oauth/authorize endpoint.
#Controller
#RequestMapping(TwoFactorAuthenticationController.PATH)
public class TwoFactorAuthenticationController {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
public static final String PATH = "/secure/two_factor_authentication";
#RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session, ....) {
if (AuthenticationUtil.isAuthenticatedWithAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.info("User {} already has {} authority - no need to enter code again", ROLE_TWO_FACTOR_AUTHENTICATED);
throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
LOG.warn("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
throw ....;
}
return ....; // Show the form to enter the 2FA secret
}
#RequestMapping(method = RequestMethod.POST)
public String auth(....) {
if (userEnteredCorrect2FASecret()) {
AuthenticationUtil.addAuthority(ROLE_TWO_FACTOR_AUTHENTICATED);
return "forward:/oauth/authorize"; // Continue with the OAuth flow
}
return ....; // Show the form to enter the 2FA secret again
}
}
A custom OAuth2RequestFactory retrieves the previously saved AuthorizationRequest from the session if available and returns that or creates a new one if none can be found in the session.
/**
* If the session contains an {#link AuthorizationRequest}, this one is used and returned.
* The {#link com.example.TwoFactorAuthenticationFilter} saved the original AuthorizationRequest. This allows
* to redirect the user away from the /oauth/authorize endpoint during oauth authorization
* and show him e.g. a the page where he has to enter a code for two factor authentication.
* Redirecting him back to /oauth/authorize will use the original authorizationRequest from the session
* and continue with the oauth authorization.
*/
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
#Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
return authorizationRequest;
}
}
return super.createAuthorizationRequest(authorizationParameters);
}
}
This custom OAuth2RequestFactory is set to the authorization server like:
<bean id="customOAuth2RequestFactory" class="com.example.CustomOAuth2RequestFactory">
<constructor-arg index="0" ref="clientDetailsService" />
</bean>
<!-- Configures the authorization-server and provides the /oauth/authorize endpoint -->
<oauth:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
user-approval-handler-ref="approvalStoreUserApprovalHandler" redirect-resolver-ref="redirectResolver"
authorization-request-manager-ref="customOAuth2RequestFactory">
<oauth:authorization-code authorization-code-services-ref="authorizationCodeServices"/>
<oauth:implicit />
<oauth:refresh-token />
<oauth:client-credentials />
<oauth:password />
</oauth:authorization-server>
When using java config you can create a TwoFactorAuthenticationInterceptor instead of the TwoFactorAuthenticationFilter and register it with an AuthorizationServerConfigurer with
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig implements AuthorizationServerConfigurer {
...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.addInterceptor(twoFactorAuthenticationInterceptor())
...
.requestFactory(customOAuth2RequestFactory());
}
#Bean
public HandlerInterceptor twoFactorAuthenticationInterceptor() {
return new TwoFactorAuthenticationInterceptor();
}
}
The TwoFactorAuthenticationInterceptor contains the same logic as the TwoFactorAuthenticationFilter in its preHandle method.
I couldn't make the accepted solution work. I have been working on this for a while, and finally I wrote my solution by using the ideas explained here and on this thread "null client in OAuth2 Multi-Factor Authentication"
Here is the GitHub location for the working solution for me:
https://github.com/turgos/oauth2-2FA
I appreciate if you share your feedback in case you see any issues or better approach.
Below you can find the key configuration files for this solution.
AuthorizationServerConfig
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private ClientDetailsService clientDetailsService;
#Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient("ClientId")
.secret("secret")
.authorizedGrantTypes("authorization_code")
.scopes("user_info")
.authorities(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED)
.autoApprove(true);
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.requestFactory(customOAuth2RequestFactory());
}
#Bean
public DefaultOAuth2RequestFactory customOAuth2RequestFactory(){
return new CustomOAuth2RequestFactory(clientDetailsService);
}
#Bean
public FilterRegistrationBean twoFactorAuthenticationFilterRegistration(){
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(twoFactorAuthenticationFilter());
registration.addUrlPatterns("/oauth/authorize");
registration.setName("twoFactorAuthenticationFilter");
return registration;
}
#Bean
public TwoFactorAuthenticationFilter twoFactorAuthenticationFilter(){
return new TwoFactorAuthenticationFilter();
}
}
CustomOAuth2RequestFactory
public class CustomOAuth2RequestFactory extends DefaultOAuth2RequestFactory {
private static final Logger LOG = LoggerFactory.getLogger(CustomOAuth2RequestFactory.class);
public static final String SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME = "savedAuthorizationRequest";
public CustomOAuth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
#Override
public AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters) {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(false);
if (session != null) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
if (authorizationRequest != null) {
session.removeAttribute(SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
LOG.debug("createAuthorizationRequest(): return saved copy.");
return authorizationRequest;
}
}
LOG.debug("createAuthorizationRequest(): create");
return super.createAuthorizationRequest(authorizationParameters);
}
}
WebSecurityConfig
#EnableResourceServer
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends WebSecurityConfigurerAdapter {
#Autowired
CustomDetailsService customDetailsService;
#Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/webjars/**");
web.ignoring().antMatchers("/css/**","/fonts/**","/libs/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception { // #formatter:off
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize", "/secure/two_factor_authentication","/exit", "/resources/**")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin().loginPage("/login")
.permitAll();
} // #formatter:on
#Override
#Autowired // <-- This is crucial otherwise Spring Boot creates its own
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth//.parentAuthenticationManager(authenticationManager)
// .inMemoryAuthentication()
// .withUser("demo")
// .password("demo")
// .roles("USER");
auth.userDetailsService(customDetailsService).passwordEncoder(encoder());
}
}
TwoFactorAuthenticationFilter
public class TwoFactorAuthenticationFilter extends OncePerRequestFilter {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationFilter.class);
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private OAuth2RequestFactory oAuth2RequestFactory;
//These next two are added as a test to avoid the compilation errors that happened when they were not defined.
public static final String ROLE_TWO_FACTOR_AUTHENTICATED = "ROLE_TWO_FACTOR_AUTHENTICATED";
public static final String ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED = "ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED";
#Autowired
public void setClientDetailsService(ClientDetailsService clientDetailsService) {
oAuth2RequestFactory = new DefaultOAuth2RequestFactory(clientDetailsService);
}
private boolean twoFactorAuthenticationEnabled(Collection<? extends GrantedAuthority> authorities) {
return authorities.stream().anyMatch(
authority -> ROLE_TWO_FACTOR_AUTHENTICATION_ENABLED.equals(authority.getAuthority())
);
}
private Map<String, String> paramsFromRequest(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
params.put(entry.getKey(), entry.getValue()[0]);
}
return params;
}
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Check if the user hasn't done the two factor authentication.
if (isAuthenticated() && !hasAuthority(ROLE_TWO_FACTOR_AUTHENTICATED)) {
AuthorizationRequest authorizationRequest = oAuth2RequestFactory.createAuthorizationRequest(paramsFromRequest(request));
/* Check if the client's authorities (authorizationRequest.getAuthorities()) or the user's ones
require two factor authentication. */
if (twoFactorAuthenticationEnabled(authorizationRequest.getAuthorities()) ||
twoFactorAuthenticationEnabled(SecurityContextHolder.getContext().getAuthentication().getAuthorities())) {
// Save the authorizationRequest in the session. This allows the CustomOAuth2RequestFactory
// to return this saved request to the AuthenticationEndpoint after the user successfully
// did the two factor authentication.
request.getSession().setAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME, authorizationRequest);
LOG.debug("doFilterInternal(): redirecting to {}", TwoFactorAuthenticationController.PATH);
// redirect the the page where the user needs to enter the two factor authentication code
redirectStrategy.sendRedirect(request, response,
TwoFactorAuthenticationController.PATH
);
return;
}
}
LOG.debug("doFilterInternal(): without redirect.");
filterChain.doFilter(request, response);
}
public boolean isAuthenticated(){
return SecurityContextHolder.getContext().getAuthentication().isAuthenticated();
}
private boolean hasAuthority(String checkedAuthority){
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
authority -> checkedAuthority.equals(authority.getAuthority())
);
}
}
TwoFactorAuthenticationController
#Controller
#RequestMapping(TwoFactorAuthenticationController.PATH)
public class TwoFactorAuthenticationController {
private static final Logger LOG = LoggerFactory.getLogger(TwoFactorAuthenticationController.class);
public static final String PATH = "/secure/two_factor_authentication";
#RequestMapping(method = RequestMethod.GET)
public String auth(HttpServletRequest request, HttpSession session) {
if (isAuthenticatedWithAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED)) {
LOG.debug("User {} already has {} authority - no need to enter code again", TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
//throw ....;
}
else if (session.getAttribute(CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME) == null) {
LOG.debug("Error while entering 2FA code - attribute {} not found in session.", CustomOAuth2RequestFactory.SAVED_AUTHORIZATION_REQUEST_SESSION_ATTRIBUTE_NAME);
//throw ....;
}
LOG.debug("auth() HTML.Get");
return "loginSecret"; // Show the form to enter the 2FA secret
}
#RequestMapping(method = RequestMethod.POST)
public String auth(#ModelAttribute(value="secret") String secret, BindingResult result, Model model) {
LOG.debug("auth() HTML.Post");
if (userEnteredCorrect2FASecret(secret)) {
addAuthority(TwoFactorAuthenticationFilter.ROLE_TWO_FACTOR_AUTHENTICATED);
return "forward:/oauth/authorize"; // Continue with the OAuth flow
}
model.addAttribute("isIncorrectSecret", true);
return "loginSecret"; // Show the form to enter the 2FA secret again
}
private boolean isAuthenticatedWithAuthority(String checkedAuthority){
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream().anyMatch(
authority -> checkedAuthority.equals(authority.getAuthority())
);
}
private boolean addAuthority(String authority){
Collection<SimpleGrantedAuthority> oldAuthorities = (Collection<SimpleGrantedAuthority>)SecurityContextHolder.getContext().getAuthentication().getAuthorities();
SimpleGrantedAuthority newAuthority = new SimpleGrantedAuthority(authority);
List<SimpleGrantedAuthority> updatedAuthorities = new ArrayList<SimpleGrantedAuthority>();
updatedAuthorities.add(newAuthority);
updatedAuthorities.addAll(oldAuthorities);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
SecurityContextHolder.getContext().getAuthentication().getPrincipal(),
SecurityContextHolder.getContext().getAuthentication().getCredentials(),
updatedAuthorities)
);
return true;
}
private boolean userEnteredCorrect2FASecret(String secret){
/* later on, we need to pass a temporary secret for each user and control it here */
/* this is just a temporary way to check things are working */
if(secret.equals("123"))
return true;
else;
return false;
}
}

Spring Security authentication via URL

I have a Spring MVC app that uses Spring Security and form based login for authorization/authentication.
Now I want to add a special URL that includes a token that should be accessible without additional information because the token is unique to a user:
http://myserver.com/special/5f6be0c0-87d7-11e2-9e96-0800200c9a66/text.pdf
How do I need to configure Spring Security to use that token for user authentication?
You need to define your custom pre auth filter.
In security app context within http tag:
<custom-filter position="PRE_AUTH_FILTER" ref="preAuthTokenFilter" />
Then define your filter bean (and its properties approprietly):
<beans:bean class="com.yourcompany.PreAuthTokenFilter"
id="preAuthTokenFilter">
<beans:property name="authenticationDetailsSource" ref="authenticationDetailsSource" />
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
</beans:bean>
Create your custom filter extended from GenericFilterBean
public class PreAuthTokenFilter extends GenericFilterBean {
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager;
private AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
#Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
String token = getTokenFromHeader(request);//your method
if (StringUtils.isNotEmpty(token)) {
/* get user entity from DB by token, retrieve its username and password*/
if (isUserTokenValid(/* some args */)) {
try {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authResult);
} catch (AuthenticationException e) {
}
}
}
chain.doFilter(request, response);
}
/*
other methods
*/
If you don't want or cannot retrieve a password, you need to create your own AbstractAuthenticationToken which will receive only username as param (principal) and use it instead of UsernamePasswordAuthenticationToken:
public class PreAuthToken extends AbstractAuthenticationToken {
private final Object principal;
public PreAuthToken(Object principal) {
super(null);
super.setAuthenticated(true);
this.principal = principal;
}
#Override
public Object getCredentials() {
return "";
}
#Override
public Object getPrincipal() {
return principal;
}
}
You can provide a custom PreAuthenticatedProcessingFilter and PreAuthenticatedAuthenticationProvider. See Pre-Authentication Scenarios chapter for details.
I ran into this problem, and solved it using a custom implementation of the Spring Security RembereMe Service infrastructure. Here is what you need to do.
Define your own Authentication object
public class LinkAuthentication extends AbstractAuthenticationToken
{
#Override
public Object getCredentials()
{
return null;
}
#Override
public Object getPrincipal()
{
return the prncipal that that is passed in via the constructor
}
}
Define
public class LinkRememberMeService implements RememberMeServices, LogoutHandler
{
/**
* It might appear that once this method is called and returns an authentication object, that authentication should be finished and the
* request should proceed. However, spring security does not work that way.
*
* Once this method returns a non null authentication object, spring security still wants to run it through its authentication provider
* which, is totally brain dead on the part of Spring this, is why there is also a
* LinkAuthenticationProvider
*
*/
#Override
public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response)
{
String accessUrl = ServletUtils.getApplicationUrl(request, "/special/");
String requestUrl = request.getRequestURL().toString();
if (requestUrl.startsWith(accessUrl))
{
// take appart the url extract the token, find the user details object
// and return it.
LinkAuthentication linkAuthentication = new LinkAuthentication(userDetailsInstance);
return linkAuthentication;
} else
{
return null;
}
}
#Override
public void loginFail(HttpServletRequest request, HttpServletResponse response)
{
}
#Override
public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication)
{
}
#Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
{
}
}
public class LinkAuthenticationProvider implements AuthenticationProvider
{
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
// Spring Security is totally brain dead and over engineered
return authentication;
}
#Override
public boolean supports(Class<?> authentication)
{
return LinkAuthentication.class.isAssignableFrom(authentication);
}
}
Hack up the rest rest of your spring security xml to define a custom authentication provider, and the custom remember me service.
P.S. if you do base64 encoding of the GUID in your URL it will be a few characters shorter. You can use the Apache commons codec base64 binary encoder / decoder to do safer url links.
public static String toBase64Url(UUID uuid)
{
return Base64.encodeBase64URLSafeString(toBytes(uuid));
}

Resources