Additional parameter on spring-authorization-server - java

What I'm going to do is like this.
post additional parameter when user submit on consent page.
send posted additional parameter to redirect uri.
I've checked through AuthorizationRequestConverter that OAuth2AuthorizationCodeRequestAuthenticationToken has the parameter which user has uploaded.
But I don't know why OAuth2Authorization doesn't save additional parameters on Database.
Cause of above I cant' reach to additional parameters and can't post data to redirect URI.
I've referred to this repository.
sjohnr/spring-authorization-server
Here's my auth server configuration
AuthorizationServerConfig
#Bean
#Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
authorizationServerConfigurer
.authorizationEndpoint(authorizationEndPoint ->
authorizationEndPoint
.consentPage(CUSTOM_CONSENT_PAGE_URI)
.authorizationRequestConverter(customAuthorizationRequestConverter())
.authorizationResponseHandler(authorizationResponseHandler())
)
.withObjectPostProcessor(new ObjectPostProcessor<OAuth2AuthorizationCodeRequestAuthenticationProvider>() {
#Override
public <O extends OAuth2AuthorizationCodeRequestAuthenticationProvider> O postProcess(O object) {
object.setAuthenticationValidatorResolver(createDefaultAuthenticationValidatorResolver());
return object;
}
});
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequest ->
authorizeRequest.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
return http.build();
}
private AuthenticationConverter customAuthorizationRequestConverter() {
final OAuth2AuthorizationCodeRequestAuthenticationConverter delegate = new OAuth2AuthorizationCodeRequestAuthenticationConverter();
return (request) -> {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
(OAuth2AuthorizationCodeRequestAuthenticationToken) delegate.convert(request);
return authorizationCodeRequestAuthentication;
};
}
// ... validator and authorizationResponseHandler are same as reference
Authentication Provider
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
#RequiredArgsConstructor
#Component
#Slf4j
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserMapper userMapper;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
User user = getUser(username);
validatePassword(username, user.getPassword(), password);
return new UsernamePasswordAuthenticationToken(username, password, user.getAuthorities());
}
private User getUser(String username) {
User user = userMapper.fetchUserByUsername(username);
if (user == null)
throw new BusinessException("oauth.user.0100", HttpStatus.NOT_FOUND);
return user;
}
private void validatePassword(String username, String userPassword, String reqPassword) {
if(StringUtils.equals(userPassword, userMapper.fetchEncryptPassword(reqPassword)))
return;
userMapper.updateFailedLoginCount(username);
throw new BusinessException("oauth.user.0100", HttpStatus.NOT_FOUND);
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}

Related

How do I get user details in controller of Spring Boot OIDC app?

I have adapted the code from here to call a MitreID OIDC server.
My controller:
public final String home(Principal p) {
final String username = SecurityContextHolder.getContext().getAuthentication().getName();
...
returns null and is null for all userdetails.
I have also tried:
public final String home(#AuthenticationPrincipal OpenIdConnectUserDetails user) {
final String username = user.getUsername();
and
#RequestMapping(value = "/username", method = RequestMethod.GET)
#ResponseBody
public String currentUserNameSimple(HttpServletRequest request) {
Principal principal = request.getUserPrincipal();
return "username: " + principal.getName();
}
Everything is null but the authentication is returning an access and user token.
My security config is:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private OAuth2RestTemplate restTemplate;
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**");
}
#Bean
public OpenIdConnectFilter myFilter() {
final OpenIdConnectFilter filter = new OpenIdConnectFilter("/openid_connect_login");
filter.setRestTemplate(restTemplate);
return filter;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.addFilterAfter(new OAuth2ClientContextFilter(), AbstractPreAuthenticatedProcessingFilter.class)
.addFilterAfter(myFilter(), OAuth2ClientContextFilter.class)
.httpBasic().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/openid_connect_login"))
.and()
.authorizeRequests()
.antMatchers("/","/index*").permitAll()
.anyRequest().authenticated()
;
// #formatter:on
}
}
So why can my controller not access the userdetails?
EDIT: as requested, OpenIdConnectFilter:
package org.baeldung.security;
import java.io.IOException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.UrlJwkProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
public class OpenIdConnectFilter extends AbstractAuthenticationProcessingFilter {
#Value("${oidc.clientId}")
private String clientId;
#Value("${oidc.issuer}")
private String issuer;
#Value("${oidc.jwkUrl}")
private String jwkUrl;
public OAuth2RestOperations restTemplate;
public OpenIdConnectFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
setAuthenticationManager(new NoopAuthenticationManager());
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
logger.info("ewd here: b " );
try {
accessToken = restTemplate.getAccessToken();
} catch (final OAuth2Exception e) {
throw new BadCredentialsException("Could not obtain access token", e);
}
try {
logger.info("ewd access token: " + accessToken);
final String idToken = accessToken.getAdditionalInformation().get("id_token").toString();
String kid = JwtHelper.headers(idToken)
.get("kid");
final Jwt tokenDecoded = JwtHelper.decodeAndVerify(idToken, verifier(kid));
final Map<String, String> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
verifyClaims(authInfo);
final OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken);
logger.info("ewd user token: " + tokenDecoded);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (final Exception e) {
throw new BadCredentialsException("Could not obtain user details from token", e);
}
}
public void verifyClaims(Map claims) {
int exp = (int) claims.get("exp");
Date expireDate = new Date(exp * 1000L);
Date now = new Date();
if (expireDate.before(now) || !claims.get("iss").equals(issuer) || !claims.get("aud").equals(clientId)) {
throw new RuntimeException("Invalid claims");
}
}
private RsaVerifier verifier(String kid) throws Exception {
JwkProvider provider = new UrlJwkProvider(new URL(jwkUrl));
Jwk jwk = provider.get(kid);
return new RsaVerifier((RSAPublicKey) jwk.getPublicKey());
}
public void setRestTemplate(OAuth2RestTemplate restTemplate2) {
restTemplate = restTemplate2;
}
private static class NoopAuthenticationManager implements AuthenticationManager {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
throw new UnsupportedOperationException("No authentication should be done with this AuthenticationManager");
}
}
}
In the tutorial you refer to, Google OpenId Connect is used. This service also returns the extra scope email variable that is read in:
OpenIdConnectUserDetails.class
public OpenIdConnectUserDetails(Map<String, String> userInfo, OAuth2AccessToken token) {
this.userId = userInfo.get("sub");
this.username = userInfo.get("email");
this.token = token;
}
Without knowing the specific configuration of your MitreID OIDC server maybe the openId server is not returning the email variable
I am a new auth0 user and had the same issue, trying to get some User information in the Spring Controller. I am able to access the Claims from the token using this code.
#GetMapping
public List<Task> getTasks(AuthenticationJsonWebToken authentication) {
logger.debug("getTasks called.");
DecodedJWT jwt = JWT.decode(authentication.getToken());
Map<String, Claim> claims = jwt.getClaims();
for (Object key: claims.keySet()) {
logger.debug("key: {}, value: {}", key.toString(), claims.get(key).asString());
}
return taskRepository.findAll();
}
Hope this helps.
If you need username, you can get it from JwtAuthenticationToken object as below:
#GetMapping("/home")
public String home(JwtAuthenticationToken user) {
String name = user.getName();
If you need some other information from user's profile, you can call your auth server's /userinfo endpoint with the access token as below:
This will fetch info only if you had included profile scope in your authorize call.
#GetMapping("/home")
public String home(JwtAuthenticationToken user) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, "Bearer "+user.getToken().getTokenValue());
HttpEntity entity = new HttpEntity(headers);
ResponseEntity<Map> userinfo = template.exchange("https://your-auth-server/default/v1/userinfo", HttpMethod.GET, entity, Map.class);
String name = (String) userinfo.getBody().get("given_name");
You can retrieve all profile attributes from this response.
For Auth0, I was able to get user information in two ways:
First one is using JwtAuthenticationToken directly on the controller as shown below.
#GetMapping("/info")
public void users(JwtAuthenticationToken token) {
System.out.println(token.getName());
System.out.println(token.getTokenAttributes().get("name"));
}
Here, token.getName() prints the user id from auth0 and token.getTokenAttributes() returns a map from which we can retrieve any information that we need. For example to print user name, we can use token.getTokenAttributes().get("name").
Casting Authentication object to JwtAuthenticationToken:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
JwtAuthenticationToken token = (JwtAuthenticationToken) authentication;
String username = token.getTokenAttributes().get("name");
you can retrieve the user or profile related properties that were defined when creating the okta oidc application through the OidcUser class, which can be used with the AuthenticationPrincipal annotation.Follow below steps
**My Controller:**
#GetMapping("/user")
public User user(#AuthenticationPrincipal OidcUser oidcUser) {
System.out.println("oidcUser :: " + oidcUser + "\n\n");
User user = new User();
System.out.println("Attributes :: " + oidcUser.getAttributes() + "\n\n");
user.setFirstName(oidcUser.getAttribute("given_name"));
user.setLastName(oidcUser.getAttribute("family_name"));
user.setName(oidcUser.getAttribute("name"));
user.setPreferred_username(oidcUser.getAttribute("preferred_username"));
user.setEmail(oidcUser.getAttribute("email"));
user.setGroups(getGroupsFromCurrentUser());
System.out.println(user.toString() + "\n\n");
return user;
}
private List<String> getGroupsFromCurrentUser() {
List<String> groups = new ArrayList<>();
Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
System.out.println("\n\n"+authorities+"\n\n");
for (GrantedAuthority auth : authorities) {
groups.add(auth.getAuthority());
}
System.out.println("\n\n"+"groups :: "+groups+"\n\n");
return groups;
}

Selection based AuthProvider Spring Security

I have configured Spring security to work with both LDAP and DB based login. First it tries to login via LDAP and if required permissions is not there then it takes to username/password entry page.
<security:http auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
<security:custom-filter ref="customPreAuthFilter" position="PRE_AUTH_FILTER"/> // This is for LDAP
<security:custom-filter ref="customAuthFilter" position="FORM_LOGIN_FILTER"/> // This is for DB Based
/** intercept urls
**/
</security:http>
I want to add a new screen on the top and user need to select between the two button LDAP or username/pass. How do I proceed?
The data to be accessed is the same url i.e. /home but both ldap and DB users should be able to access.
If you look at code in UsernamePasswordAuthenticationFilter there is setDetails method.
from docs:
Provided so that subclasses may configure what is put into the
authentication request's details property.
Idea from here
Provision to change ldap/Ad provider url at run time
You can set the details like authtype here and use it authentication provider, But to achieve the things you would lik adds little more work.
Adding details and hope it helps.
CustomUsernamePasswordAuthenticationFilter.java
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
#Component
public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(CustomUsernamePasswordAuthenticationFilter.class);
#Autowired
#Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
// TODO Auto-generated method stub
super.setAuthenticationManager(authenticationManager);
}
#Autowired
#Override
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
super.setAuthenticationDetailsSource(authenticationDetailsSource);
}
#Override
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
String authType = request.getParameter("authType");
logger.info("authType {} ",authType);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
}
But this is not sufficient you would need to extend WebAuthenticationDetails.
Reason is WebAuthenticationDetails provides only remote IP address and sessionId so, to add authType we need to extend this class.
You have to extend WebAuthenticationDetailsSource to return CustomAuthenticationDetails as shown below.
CustomAuthenticationDetails.java
public class CustomAuthenticationDetails extends WebAuthenticationDetails{
private final String authType;
public CustomAuthenticationDetails(HttpServletRequest request) {
super(request);
authType = request.getParameter("authType");
}
public String getAuthType() {
return authType;
}
}
CustomWebAuthenticationDetailsSource.java
public class CustomWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
#Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new CustomAuthenticationDetails(context);
}
}
Please note these classes for demo purpose only.
Need to autowire actual authentication providers in these classes.
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.stereotype.Component;
#Component
public class AuthenicationProviderJdbcLdapImpl implements AuthenticationProvider{
// you need to autowire jdbc auth provider
#Autowired(required = false)
DaoAuthenticationProvider authenticationProvider;
//you need to autowire ldap auth provider
#Autowired(required = false)
LdapAuthenticationProvider ldapAuthenticationProvider;
protected static class User{
private final String userId;
private final String password;
public User(String userId,String password) {
this.userId = userId;
this.password = password;
}
public String getUserId() {
return userId;
}
public String getPassword() {
return password;
}
#Override
public String toString() {
return "User [userId=" + userId + ", password=" + password + "]";
}
}
private final static Logger logger = LoggerFactory.getLogger(AuthenicationProviderJdbcLdapImpl.class);
private static final List<User> users1 = Arrays.asList(new User("admin1", "password"),new User("admin2", "password"));
private static final List<User> users2 = Arrays.asList(new User("admin3", "password"),new User("admin4", "password"));
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
CustomAuthenticationDetails details = (CustomAuthenticationDetails) authentication.getDetails();
String authType = details.getAuthType();
logger.info("authType {}",authType);
if("jdbc".equalsIgnoreCase(authType)) {
logger.info("perfrom jdbc authentication");
//perform your authentication using jdbc
//the below is just for explaination
return performAuthentication(authentication, users1);
}else if("ldap".equalsIgnoreCase(authType)) {
logger.info("perfrom ldap authentication");
//perform your authentication using ldap
//the below is just for explaination
return performAuthentication(authentication, users2);
}
return null;
}
private Authentication performAuthentication(Authentication authentication,List<User> users) {
String credential = (String) authentication.getCredentials();
String userId = authentication.getName();
for(User user: users) {
if(user.getUserId().equals(userId)&& user.getPassword().equals(credential)) {
authentication = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(),authentication.getAuthorities());
return authentication;
}
}
return null;
}
#Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
If you would need to redirect different login page (not sure, if you have the requirement) you register AuthenticationFailureHandler shown in security config. Here it is redirected to login and login1 based on condition.
http.failureHandler(new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String whichPage = request.getParameter("whichPage");
System.out.println("inside login failure handler "+whichPage);
if("login1".equals(whichPage)) {
response.sendRedirect(request.getContextPath() +"/login1");
}else {
response.sendRedirect(request.getContextPath() +"/login");
}
}
})
WebSecurityConfig.java
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserDetailsService userDetailsService;
#Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
AuthenticationSuccessHandler authenticationSuccessHandler;
#Autowired()
AuthenicationProviderJdbcImpl authenicationProviderJdbcImpl;
#Autowired()
AuthenicationProviderLdapImpl authenicationProviderLdapImpl;
#Autowired
CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAt(customUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http
.authorizeRequests()
.antMatchers("/resources/**", "/registration","/login1").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()//.successHandler(authenticationSuccessHandler)
.failureHandler(new AuthenticationFailureHandler() {
#Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String whichPage = request.getParameter("whichPage");
System.out.println("inside login failure handler "+whichPage);
if("login1".equals(whichPage)) {
response.sendRedirect(request.getContextPath() +"/login1");
}else {
response.sendRedirect(request.getContextPath() +"/login");
}
}
})
.and()
.logout()
.permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenicationProviderLdapImpl).authenticationProvider(authenicationProviderJdbcImpl);
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
/*auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder());*/
}
}
The below is from logs when authType = jdbc or authType=ldap
login called
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] stomUsernamePasswordAuthenticationFilter : authType jdbc
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] c.t.a.AuthenicationProviderJdbcLdapImpl : authType jdbc
2018-11-23 17:45:25.606 INFO 7232 --- [nio-8080-exec-6] c.t.a.AuthenicationProviderJdbcLdapImpl : perfrom jdbc authentication
login called
login1 called
login1 called
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] stomUsernamePasswordAuthenticationFilter : authType ldap
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] c.t.a.AuthenicationProviderJdbcLdapImpl : authType ldap
2018-11-23 17:45:42.435 INFO 7232 --- [nio-8080-exec-5] c.t.a.AuthenicationProviderJdbcLdapImpl : perfrom ldap authentication returning true in ldap

Spring Security: Custom UserDetailsService not being called (using Auth0 authentication)

I'm new to the Spring framework, so I apologize in advance for any gaping holes in my understanding.
I'm using Auth0 to secure my API, which works perfectly. My setup & config is the same as the suggested setup in the Auth0 documentation:
// SecurityConfig.java
#Configuration
#EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// auth0 config vars here
#Override
protected void configure(HttpSecurity http) {
JwtWebSecurityConfigurer
.forRS256(apiAudience, issuer)
.configure(http)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/public").permitAll()
.antMatchers(HttpMethod.GET, "/api/private").authenticated();
}
}
With this setup, the spring security principal is being set to the userId (sub) from the jwt token: auth0|5b2b.... However, instead of just the userId, I want it set to the matching user (from my database). My question is how to do that.
What I've tried
I've tried implementing a custom database-backed UserDetailsService that I copied from this tutorial.
However, it's not getting called regardless of how I try to add it to my conf. I've tried adding it several different ways with no effect:
// SecurityConfig.java (changes only)
// My custom userDetailsService, overriding the loadUserByUsername
// method from Spring Framework's UserDetailsService.
#Autowired
private MyUserDetailsService userDetailsService;
protected void configure(HttpSecurity http) {
http.userDetailsService(userDetailsService); // Option 1
http.authenticationProvider(authenticationProvider()); // Option 2
JwtWebSecurityConfigurer
[...] // The rest unchanged from above
}
#Override // Option 3 & 4: Override the following method
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider()); // Option 3
auth.userDetailsService(userDetailsService); // Option 4
}
#Bean // Needed for Options 2 or 4
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
return authProvider;
}
Unfortunately none of the similar "userDetails not being called" questions have helped me due to me needing to combine it with Auth0 authentication.
I'm not positive that I'm on the right path with this. It seems strange to me that I can't find any documentation from Auth0 on this extremely common use case, so maybe I'm missing something obvious.
PS: Not sure if relevant, but the following is always logged during init.
Jun 27, 2018 11:25:22 AM com.test.UserRepository initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.
EDIT 1:
Based on Ashish451's answer, I tried copying his CustomUserDetailsService, and added the following to my SecurityConfig:
#Autowired
private CustomUserDetailsService userService;
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( userService );
}
Unfortunately with those changes, CustomUserDetailsService is still not being called.
EDIT 2:
Output when adding the logging method suggested by #Norberto Ritzmann:
Jul 04, 2018 3:49:22 PM com.test.repositories.UserRepositoryImpl initDao
INFO: No authentication manager set. Reauthentication of users when changing passwords will not be performed.
Jul 04, 2018 3:49:22 PM com.test.runner.JettyRunner testUserDetailsImpl
INFO: UserDetailsService implementation: com.test.services.CustomUserDetailsService
Looking at your Adapter code you are generating JWT token in configure itself.
Am not sure whats apiAudience, issuer but it generated sub of JWT I guess.
Your issue is that you want to change JWT sub as per your Database.
I have recently implemented JWT security in Spring Boot Application.
And I am setting UserName after fetching it from Database.
I have added code with pkg info for clarity.
// My adapter class. Its same as your's except one thing that I have added a Filter to it. In this Filter I am authenticating JWT token. This filter will be called each time a Secured Rest URL is fired.
import java.nio.charset.StandardCharsets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import com.dev.myapp.jwt.model.CustomUserDetailsService;
import com.dev.myapp.security.RestAuthenticationEntryPoint;
import com.dev.myapp.security.TokenAuthenticationFilter;
import com.dev.myapp.security.TokenHelper;
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Autowired
private CustomUserDetailsService jwtUserDetailsService; // Get UserDetail bu UserName
#Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint; // Handle any exception during Authentication
#Bean
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
// Binds User service for User and Password Query from Database with Password Encryption
#Autowired
public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exception {
auth.userDetailsService( jwtUserDetailsService )
.passwordEncoder( passwordEncoder() );
}
#Autowired
TokenHelper tokenHelper; // Contains method for JWT key Generation, Validation and many more...
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS ).and()
.exceptionHandling().authenticationEntryPoint( restAuthenticationEntryPoint ).and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(new TokenAuthenticationFilter(tokenHelper, jwtUserDetailsService), BasicAuthenticationFilter.class);
http.csrf().disable();
}
// Patterns to ignore from JWT security check
#Override
public void configure(WebSecurity web) throws Exception {
// TokenAuthenticationFilter will ignore below paths
web.ignoring().antMatchers(
HttpMethod.POST,
"/auth/login"
);
web.ignoring().antMatchers(
HttpMethod.GET,
"/",
"/assets/**",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
);
}
}
// User Service to get User Details
#Transactional
#Repository
public class CustomUserDetailsService implements UserDetailsService {
protected final Log LOGGER = LogFactory.getLog(getClass());
#Autowired
private UserRepo userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User uu = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username));
} else {
return user;
}
}
}
// Unauthorized access handler
#Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
// This is invoked when user tries to access a secured REST resource without supplying any credentials
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
// Filter Chain for Validating JWT Token
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.filter.OncePerRequestFilter;
public class TokenAuthenticationFilter extends OncePerRequestFilter {
protected final Log logger = LogFactory.getLog(getClass());
private TokenHelper tokenHelper;
private UserDetailsService userDetailsService;
public TokenAuthenticationFilter(TokenHelper tokenHelper, UserDetailsService userDetailsService) {
this.tokenHelper = tokenHelper;
this.userDetailsService = userDetailsService;
}
#Override
public void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws IOException, ServletException {
String username;
String authToken = tokenHelper.getToken(request);
logger.info("AuthToken: "+authToken);
if (authToken != null) {
// get username from token
username = tokenHelper.getUsernameFromToken(authToken);
logger.info("UserName: "+username);
if (username != null) {
// get user
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (tokenHelper.validateToken(authToken, userDetails)) {
// create authentication
TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
authentication.setToken(authToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // Adding Token in Security COntext
}
}else{
logger.error("Something is wrong with Token.");
}
}
chain.doFilter(request, response);
}
}
// TokenBasedAuthentication class
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
public class TokenBasedAuthentication extends AbstractAuthenticationToken {
private static final long serialVersionUID = -8448265604081678951L;
private String token;
private final UserDetails principle;
public TokenBasedAuthentication( UserDetails principle ) {
super( principle.getAuthorities() );
this.principle = principle;
}
public String getToken() {
return token;
}
public void setToken( String token ) {
this.token = token;
}
#Override
public boolean isAuthenticated() {
return true;
}
#Override
public Object getCredentials() {
return token;
}
#Override
public UserDetails getPrincipal() {
return principle;
}
}
// Helper class for JWT generation and Validation Logic
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.dev.myapp.common.TimeProvider;
import com.dev.myapp.entity.User;
#Component
public class TokenHelper {
protected final Log LOGGER = LogFactory.getLog(getClass());
#Value("${app.name}") // reading details from property file added in Class path
private String APP_NAME;
#Value("${jwt.secret}")
public String SECRET;
#Value("${jwt.licenseSecret}")
public String LICENSE_SECRET;
#Value("${jwt.expires_in}")
private int EXPIRES_IN;
#Value("${jwt.mobile_expires_in}")
private int MOBILE_EXPIRES_IN;
#Value("${jwt.header}")
private String AUTH_HEADER;
#Autowired
TimeProvider timeProvider; // return current time. Basically Deployment time.
private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;
// Generate Token based on UserName. You can Customize this
public String generateToken(String username) {
String audience = generateAudience();
return Jwts.builder()
.setIssuer( APP_NAME )
.setSubject(username)
.setAudience(audience)
.setIssuedAt(timeProvider.now())
.setExpiration(generateExpirationDate())
.signWith( SIGNATURE_ALGORITHM, SECRET )
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
User user = (User) userDetails;
final String username = getUsernameFromToken(token);
final Date created = getIssuedAtDateFromToken(token);
return (
username != null &&
username.equals(userDetails.getUsername())
);
}
// If Token is valid will extract all claim else throw appropriate error
private Claims getAllClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
LOGGER.error("Could not get all claims Token from passed token");
claims = null;
}
return claims;
}
private Date generateExpirationDate() {
long expiresIn = EXPIRES_IN;
return new Date(timeProvider.now().getTime() + expiresIn * 1000);
}
}
For this Log
No authentication manager set. Reauthentication of users when changing passwords
Since you have not implemented a methods with Name loadUserByUsername. You are getting this log.
Edit 1:
I am using Filter Chain just to Validate Token and add User in Security Context which will be extracted from Token....
Am using JWT and you are using AuthO, only Implementation is Different. Am added full implementation for a complete work flow.
You focus on implementing authenticationManagerBean and configureGlobal from WebSecurityConfig class to use UserService.
and TokenBasedAuthentication class implementation.
Other things you can skip.
Maybe this is an spring-boot context initialization issue, meaning the #Autowired annotation cannot be resolved during the initialization of the Configuration class.
You could try the #ComponentScan() annotation on top of your Configuration class and load your MyUserDetailsService explicitly. (see: https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-configuration-classes.html#using-boot-importing-configuration). Having done this I would recommend the following in your Configuration class:
#Autowired
private MyUserDetailsService userService;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
Hope this can help you out.
I ended up asking Auth0 support about this, and they say that it's currently not possible to modify the principal without modifying the library source.
They provide an alternative approach, however, which is to use a JWT validation library (e.g. https://github.com/auth0/java-jwt) instead of their Spring Security API SDK.
My solution will be to modify my code to work with just the token as principal.
You could extend JwtAuthenticationProvider with overridden authenticate method which will put your user into Authentication object:
Using springboot 2.1.7.RELEASE
Auth0 deps: com.auth0:auth0:1.14.2, com.auth0:auth0-spring-security-api:1.2.5, com.auth0:jwks-rsa:0.8.3
Note: some errors in the following code snippets might exist as I've transformed kotlin code by hand into java
Configure SecurityConfig as usual but pass modified authentication provider:
#Autowired UserService userService;
...
#Override
protected void configure(HttpSecurity http) {
// same thing used in usual method `JwtWebSecurityConfigurer.forRS256(String audience, String issuer)`
JwkProvider jwkProvider = JwkProviderBuilder(issuer).build()
// provider deduced from existing default one
Auth0UserAuthenticationProvider authenticationProvider = new Auth0UserAuthenticationProvider(userService, jwkProvider, issuer, audience)
JwtWebSecurityConfigurer
.forRS256(apiAudience, issuer, authenticationProvider)
.configure(http)
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/api/public").permitAll()
.antMatchers(HttpMethod.GET, "/api/private").authenticated();
}
Extend default JwtAuthenticationProvider which is usually used in method JwtWebSecurityConfigurer.forRS256(String audience, String issuer)
public class Auth0UserAuthenticationProvider extends JwtAuthenticationProvider {
private final UserService userService;
public (UserService userService, JwkProvider jwkProvider, String issuer, String audience) {
super(jwkProvider, issuer, audience);
this.userService = userService;
}
/**
* Intercept Authentication object before it is set in context
*/
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication jwtAuth = super.authenticate(authentication);
// Use your service and get user details here
User user = userService.getDetailsOrWhatever(jwtAuth.getPrincipal().toString());
// TODO implement following class which merges Auth0 provided details with your user
return new MyAuthentication(jwtAuth, user);
}
}
Implement your own MyAuthentication.class which will override getDetails() and return the actual user instead of decoded token given by Auth0 library.
Afterwards user will be available in
SecurityContextHolder.getContext().getAuthentication().getDetails();
This is what I ended up doing if you want to just stick with using spring libraries. You can change Auth0UserDetailsService to query user details from any source.
public class Auth0JwtAuthenticationProvider implements AuthenticationProvider {
#Autowired
ApplicationContext context;
#Autowired
#Qualifier("auth0UserDetailsService")
UserDetailsService auth0UserDetailsService;
private JwtAuthenticationProvider jwtAuthenticationProvider;
public Auth0JwtAuthenticationProvider() {
}
#PostConstruct
public void postConstruct() {
jwtAuthenticationProvider = new JwtAuthenticationProvider(context.getBean(JwtDecoder.class));
}
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication token = jwtAuthenticationProvider.authenticate(authentication);
((AbstractAuthenticationToken) token).setDetails(auth0UserDetailsService.loadUserByUsername(authentication.getName()));
return token;
}
#Override
public boolean supports(Class<?> authentication) {
return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication);
}
}
And for UserDetailsService I am getting user details using a service.
public class Auth0UserDetailsService implements UserDetailsService {
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
RestTemplate restTemplate = new RestTemplate();
.....
ResponseEntity<Auth0UserDetails> responseEntity = restTemplate.exchange(url, HttpMethod.GET, entity, Auth0UserDetails.class);
return responseEntity.getBody();
}
.....
}
And UserDetails will have all details you need.
public class Auth0UserDetails implements UserDetails {
#JsonProperty("name")
private String userName;
#JsonProperty("created_at")
private LocalDateTime createdAt;
#JsonProperty("email")
private String email;
#JsonProperty("email_verified")
private boolean isEmailVerified;
#JsonProperty("nickname")
private String nickName;
#JsonProperty("picture")
private String pictureURL;
#JsonProperty("updated_at")
private LocalDateTime updatedAt;
#JsonProperty("user_id")
private String userID;
...
//Getters and Setter and overridden methods.
}
#EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
#Bean
public Auth0JwtAuthenticationProvider auth0JwtAuthenticationProvider() {
return new Auth0JwtAuthenticationProvider();
}
#Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authenticationProvider(auth0JwtAuthenticationProvider()).authorizeRequests().
....
.oauth2ResourceServer().jwt();
}}

How to configure waffle in spring using java configuration

I have been struggling to get waffle to work with spring 4.2.5 using spring java configuration. And I thought I might as well help others in the same situation.
We use a custom preWaffle and postWaffle filter to authenticate that the user exists in our database after it has been validated via waffles NTLM protocol.
We also have methods for authorization of a user actions using the EnableGlobalMethodSecurity annotation.
To get this working in spring java configuration was trouble some to say the least. You can find our solution in the answer below. I hope it will help.
SpringConfiguration.java
// ... imports
#Configuration
#EnableWebMvc
#EnableScheduling
#PropertySources({
#PropertySource("classpath:app.properties")
// ... Properties sources
})
#EnableTransactionManagement
#ComponentScan(basePackages = "com.our.package")
public class SpringConfiguration extends WebMvcConfigurerAdapter {
// Our Spring configuration ...
}
SecurityConfiguration.java
// ... imports
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, proxyTargetClass = true)
#Order(1)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter{
// Authentication manager configuration
#Autowired
private WindowsAuthenticationProviderWrapper authProvider;
#Autowired
private AuthenticationManagerBuilder auth;
#Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider);
}
#Bean
public AuthenticationManager authenticationManager() throws Exception {
return auth.getObject();
}
// Waffle configuration
#Bean
public Filter customPreAuthSecurityFilter() {
return new CustomPreAuthSecurityFilter();
}
#Bean
public Filter customNegotiateSecurityFilter() {
return new CustomNegotiateSecurityFilter();
}
#Bean
public WindowsAuthProviderImpl waffleAuthProvider(){
return new WindowsAuthProviderImpl();
}
#Bean(name="negotiateSecurityFilterProvider")
#Autowired
public NegotiateSecurityFilterProvider negotiateSecurityFilterProvider(){
NegotiateSecurityFilterProvider bean = new NegotiateSecurityFilterProvider(waffleAuthProvider());
List<String> protocols = new ArrayList<>();
protocols.add("Negotiate");
bean.setProtocols(protocols);
return bean;
}
#Bean
public BasicSecurityFilterProvider basicSecurityFilterProvider(){
return new BasicSecurityFilterProvider(waffleAuthProvider());
}
#Bean(name="waffleSecurityFilterProviderCollection")
#Autowired
public waffle.servlet.spi.SecurityFilterProviderCollection negotiateSecurityFilterProviderCollection() {
final List<SecurityFilterProvider> lsp = new ArrayList<>();
lsp.add(negotiateSecurityFilterProvider());
lsp.add(basicSecurityFilterProvider());
return new waffle.servlet.spi.SecurityFilterProviderCollection(lsp.toArray(new SecurityFilterProvider[]{}));
}
#Bean(name="negotiateSecurityFilterEntryPoint")
#Autowired
public waffle.spring.NegotiateSecurityFilterEntryPoint negotiateSecurityFilterEntryPoint() {
final waffle.spring.NegotiateSecurityFilterEntryPoint ep = new waffle.spring.NegotiateSecurityFilterEntryPoint();
ep.setProvider(negotiateSecurityFilterProviderCollection());
return ep;
}
#Bean(name="negotiateSecurityFilter")
#Autowired
public waffle.spring.NegotiateSecurityFilter waffleNegotiateSecurityFilter(){
waffle.spring.NegotiateSecurityFilter bean = new waffle.spring.NegotiateSecurityFilter();
bean.setRoleFormat("both");
bean.setPrincipalFormat("fqn");
bean.setAllowGuestLogin(false);
bean.setProvider(negotiateSecurityFilterProviderCollection());
return bean;
}
// Static Mappings
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/assets/**");
}
// Security filter chain
// The custom filters can be removed if you only use waffle
// but this is how we added them
#Override
protected void configure(HttpSecurity http) throws Exception {
// A user needs to have the role user and has to be authenticated
http.exceptionHandling()
.authenticationEntryPoint(negotiateSecurityFilterEntryPoint()).and()
.addFilterBefore(customPreAuthSecurityFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(waffleNegotiateSecurityFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(customNegotiateSecurityFilter(), BasicAuthenticationFilter.class)
.authorizeRequests().anyRequest().fullyAuthenticated();
}
}
To be able to autowire the waffle authProvider I created the following wrapperclass.
WindowsAuthenticationProviderWrapper.java
// ... imports
// This class purpose is only to make the Windows authentication provider autowireable in spring.
#Component
public class WindowsAuthenticationProviderWrapper extends WindowsAuthenticationProvider{}
As requested (Some code has been stripped due to security risks).
CustomPreAuthFilter.java
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* This filter removes the excess negoatiate header sent by IE. If the client
* has already authenticated, strip the Authorization header from the request.
*/
public class CustomPreAuthSecurityFilter extends GenericFilterBean {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
SecurityContext sec = SecurityContextHolder.getContext();
HttpServletRequest req = (HttpServletRequest) servletRequest;
if(sec != null && sec.getAuthentication() != null) {
req = new CustomServletRequestWrapper(req);
}
try {
filterChain.doFilter(req, servletResponse);
} catch (RuntimeException e) {
sendUnauthorized((HttpServletResponse) servletResponse);
}
}
private void sendUnauthorized(HttpServletResponse response) throws IOException {
logger.warn("error logging in user");
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
CustomNegotiateSecurityFilter.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import waffle.servlet.WindowsPrincipal;
import waffle.spring.WindowsAuthenticationToken;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
/**
* Handle post NTLM authentication against system database
*/
public class CustomNegotiateSecurityFilter extends GenericFilterBean {
#Autowired
private UserDAO userDAO;
#Autowired
Environment env;
private static final Logger LOGGER = LoggerFactory.getLogger(CustomNegotiateSecurityFilter.class);
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
SecurityContext sec = SecurityContextHolder.getContext();
Authentication authentication = sec.getAuthentication();
// Continue filter chain if we are anonymously authenticated or if DB authentication has already happened.
if (authentication != null && authentication.getClass() == WindowsAuthenticationToken.class) {
// The user is Authenticated with NTLM but needs to be checked against the DB.
User user;
try {
// fetch user from DB ...
} catch (Exception e) {
// The could not be found in the DB.
sendUnauthorized(response);
return;
}
// The user was found in the DB.
WindowsPrincipal principal = (WindowsPrincipal)authentication.getPrincipal();
final CustomAuthenticationToken token = new CustomAuthenticationToken(principal); // This class extends WindowsAuthenticationToken
// add roles to token ...
sec.setAuthentication(token);
}
chain.doFilter(request, response);
}
private void sendUnauthorized(HttpServletResponse response) throws IOException {
logger.warn("Could not log in user");
SecurityContextHolder.clearContext();
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
private void addRoleToAuthentication(WindowsAuthenticationToken authentication, String role) {
for(GrantedAuthority authority : authentication.getAuthorities()) {
if(authority.getAuthority().equals(role)) {
return;
}
}
authentication.getAuthorities().add(new SimpleGrantedAuthority(role));
}
}
EDIT
For those who asked about here is one implementation. CustomServletRequestWrapper:
class CustomServletRequestWrapper extends HttpServletRequestWrapper {
public CustomServletRequestWrapper(HttpServletRequest request) {
super(request);
}
public String getHeader(String name) {
if(name.equals("Authorization"))
return null;
String header = super.getHeader(name);
return (header != null) ? header : super.getParameter(name); // Note: you can't use getParameterValues() here.
}
public Enumeration getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
names.addAll(Collections.list(super.getParameterNames()));
names.remove("Authorization");
return Collections.enumeration(names);
}
}
If you need more information don't hessitate to ask.

JHipster ldap Authentication

Hey Overfloweens and JHipsters,
I've recently come to the conclusion that I want to try to link up my JHipster security to protocol to an ldap server to verify authentication across my work directory that already has all the employee passwords and usernames. However, I want to continue to use JHipster's in-built token system which uses Spring-mvc. I know how to do the ldap server apart from JHipster, but I'm unclear how to modify the SecurityConfiguration.java file to make this a reality. Any advice would be much appreciate.
Security Configuration file:
package com.comcast.castit.config;
import javax.inject.Inject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
import org.springframework.security.web.authentication.RememberMeServices;
import com.comcast.castit.security.AjaxAuthenticationFailureHandler;
import com.comcast.castit.security.AjaxAuthenticationSuccessHandler;
import com.comcast.castit.security.AjaxLogoutSuccessHandler;
import com.comcast.castit.security.AuthoritiesConstants;
import com.comcast.castit.security.Http401UnauthorizedEntryPoint;
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Inject
private Environment env;
#Inject
private AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler;
#Inject
private AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler;
#Inject
private AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler;
#Inject
private Http401UnauthorizedEntryPoint authenticationEntryPoint;
#Inject
private UserDetailsService userDetailsService;
#Inject
private RememberMeServices rememberMeServices;
#Bean
public PasswordEncoder passwordEncoder() {
return new StandardPasswordEncoder();
}
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(
passwordEncoder());
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/bower_components/**")
.antMatchers("/fonts/**").antMatchers("/images/**")
.antMatchers("/scripts/**").antMatchers("/styles/**")
.antMatchers("/views/**").antMatchers("/i18n/**")
.antMatchers("/swagger-ui/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint).and()
.rememberMe().rememberMeServices(rememberMeServices)
.key(env.getProperty("jhipster.security.rememberme.key")).and()
.formLogin().loginProcessingUrl("/app/authentication")
.successHandler(ajaxAuthenticationSuccessHandler)
.failureHandler(ajaxAuthenticationFailureHandler)
.usernameParameter("j_username")
.passwordParameter("j_password").permitAll().and().logout()
.logoutUrl("/app/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler)
.deleteCookies("JSESSIONID").permitAll().and().csrf().disable()
.headers().frameOptions().disable().authorizeRequests()
.antMatchers("/app/rest/register").permitAll()
.antMatchers("/app/rest/activate").permitAll()
.antMatchers("/app/rest/authenticate").permitAll()
.antMatchers("/app/rest/logs/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/app/**").authenticated()
.antMatchers("/metrics/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/health/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/dump/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/shutdown/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/beans/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/info/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/autoconfig/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/env/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/trace/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/api-docs/**")
.hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/protected/**").authenticated();
}
#EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true)
private static class GlobalSecurityConfiguration extends
GlobalMethodSecurityConfiguration {
}
}
The default authentication mechanism uses a "UserDetailsService" implementation, it should be called "com.comcast.castit.security.UserDetailsService" in your project.
This code has a simple "loadUserByUsername" that fetches a user according to his login and gets his authorities.
For your needs, you should change this part -> this will not impact the rest of your application, which is good (Spring Security is well designed for that)
There is a tutorial on using LDAP with Spring Security / Spring Boot, you can check it out here: https://spring.io/guides/gs/authenticating-ldap/
Of course it would be better if we had a specific documentation for JHipster, so if you succeed and have time, your feedback would be most welcomed!
In SecurityConfiguration.java change configureGlobal function to:
#Inject
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
/*auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());*/
auth.ldapAuthentication()
.userSearchBase("ou=Users")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=Groups")
.groupSearchFilter("member={0}")
.contextSource()
.url("ldap://127.0.0.1:10389/o=myorganisation");
}
I have commented out the the previous code.
Then your application will authenticate with your ldap server. It will still check the database for user details and if the user doesn't exist in users table you will have problems like :
userRepository.findOneByLogin(login) that searches for a user in database using the existing username.
But the authentication will happen with your ldap credentials.
Remove the following Code Snippet From the SecurityConfig.java
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return (AuthenticationManager) ldapAuthenticationManager;
}
And Then Create an LDAPAuthenticationManager class
package com.digitronic.isda.security;
import org.springframework.ldap.core.AuthenticationSource;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.authentication.BindAuthenticator;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.stereotype.Component;
#Component("authenticationManagerBean")
public class LDAPAuthenticationManager implements AuthenticationManager {
LdapAuthenticationProvider provider = null;
#Override
public Authentication authenticate(Authentication arg0)
throws AuthenticationException {
return provider.authenticate(arg0);
}
LDAPAuthenticationManager() {
DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(
"ldap://127.0.0.1:389");
contextSource.setUserDn("test.com\\Administrator");
contextSource.setCacheEnvironmentProperties(true);
try {
contextSource.afterPropertiesSet();
} catch (Exception e) {
e.printStackTrace();
}
contextSource.setPassword("asdasdasdjBj,K");
LdapContextSource ldapSrc = new LdapContextSource();
ldapSrc.setUrl("ldap://127.0.0.1:389");
ldapSrc.setUserDn("test.com\\Administrator");
ldapSrc.setPassword("asdasdasdjBj,K");
ldapSrc.setAnonymousReadOnly(false);
ldapSrc.setCacheEnvironmentProperties(true);
try {
ldapSrc.afterPropertiesSet();
} catch (Exception e) {
e.printStackTrace();
}
ldapSrc.setAuthenticationSource(new AuthenticationSource() {
#Override
public String getPrincipal() {
// TODO Auto-generated method stub
return "test.com\\Administrator";
}
#Override
public String getCredentials() {
// TODO Auto-generated method stub
return "asdasdasdjBj,K";
}
});
FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(
"cn=Users,dc=digitronic,dc=lan", "(sAMAccountName={0})",
ldapSrc);
BindAuthenticator bindAuth = new BindAuthenticator(contextSource);
bindAuth.setUserSearch(userSearch);
provider = new LdapAuthenticationProvider(bindAuth);
}
}
If you need an UserDetailsContextMapper (For Authorities) add this:
provider.setUserDetailsContextMapper(new UserDetailsContextMapper() {
#Override
public void mapUserToContext(UserDetails user, DirContextAdapter ctx) {
// TODO Auto-generated method stub
}
#Override
public UserDetails mapUserFromContext(DirContextOperations ctx,
String username,
Collection<? extends GrantedAuthority> authorities) {
User anwender = userRepository.findOneByAnwender(username);
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(
"ROLE_ADMIN");
grantedAuthorities.add(grantedAuthority);
return new org.springframework.security.core.userdetails.User(
username, "1", grantedAuthorities);
}
});
In order to maintain the JHipster's in-built token system with LDAP I've taken a different approach.
Whenever a user tries to login I search his credentials in the LDAP server and in case it exists, a new user is created in the local database with its LDAP attributes and then all the functionalities work normally.
The following class is used just to get the context of your LDAP server and the user attributes.
public class LDAPHelper {
private static LDAPHelper instance = null;
protected LDAPHelper() {
// Exists only to defeat instantiation
}
public static LDAPHelper getInstance() {
if (instance == null) {
instance = new LDAPHelper();
}
return instance;
}
public boolean validateLogin(String username, String password) {
return (getLdapContext(username, password) != null);
}
private LdapContext getLdapContext(String username, String password) {
Hashtable<String, String> env = new Hashtable<String, String>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put("com.sun.jndi.ldap.read.timeout", "120000");
env.put(Context.SECURITY_AUTHENTICATION, "Simple");
env.put(Context.SECURITY_PRINCIPAL, "VF-ROOT\\" + username);
env.put(Context.SECURITY_CREDENTIALS, password);
env.put(Context.PROVIDER_URL, "*ldap url*");
System.out.println(username);
try {
return new InitialLdapContext(env, null);
} catch (NamingException e) {
return null;
}
}
public User getUserAttributes(String username, String password)
throws NamingException { ... }
In the AccountResource I changed the register method to search the user in the LDAP server and, if the user exists, I add him to the local database if he hasn't been added yet.
You call this method before the login to make sure that only the users that are in the LDAP server can login in your application.
#RequestMapping(value = "/register", method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE,
MediaType.TEXT_PLAIN_VALUE })
#Timed
public ResponseEntity<?> registerAccount(#Valid #RequestBody ManagedUserDTO managedUserDTO,
HttpServletRequest request) {
HttpHeaders textPlainHeaders = new HttpHeaders();
textPlainHeaders.setContentType(MediaType.TEXT_PLAIN);
// user exists in LDAP server
if (LDAPHelper.getInstance().validateLogin(managedUserDTO.getLogin(), managedUserDTO.getPassword())) {
// user was already created in the local database
if (userRepository.findOneByLogin(managedUserDTO.getLogin().toLowerCase()).isPresent()) {
return new ResponseEntity<>("user exists in database", textPlainHeaders, HttpStatus.OK);
} else {
try {
User userAux = LDAPHelper.getInstance().getUserAttributes(managedUserDTO.getLogin(), managedUserDTO.getPassword());
User user = userService.createUserInformation(managedUserDTO.getLogin(), managedUserDTO.getPassword(), userAux.getFirstName(),
userAux.getLastName(), userAux.getEmail(), managedUserDTO.getLangKey());
} catch (NamingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return new ResponseEntity<>("user created in database", textPlainHeaders, HttpStatus.OK);
}
} else {
return new ResponseEntity<>("user does not exist in ldap", textPlainHeaders, HttpStatus.UNAUTHORIZED);
}
}

Categories