Spring security token persistence storage not working - spring

The problem is that the login and all things are working great except the remember me logic. The cookie is not set and there is no rows are inserted in the database.
This is the security configuration class.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* Spring security configurations.
*/
#Configuration
#EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private DataSource dataSource;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// Authorize all requests
.authorizeRequests()
// Allow only admins to access the administration pages
.antMatchers("/admin/**").access("hasRole('ADMIN')")
// Allow any one to access the register and the main pages only alongside
// the resources files that contains css and javascript files
.antMatchers("/resources/**", "/register", "/").permitAll()
// Authenticate any other request
.anyRequest().authenticated()
.and()
// Set up the login form.
.formLogin()
//.successHandler(successHandler())
.loginPage("/login")
.usernameParameter("email").passwordParameter("password")
.permitAll()
.and()
// Enable remember me cookie and persistence storage
.rememberMe()
// Database token repository
.tokenRepository(persistentTokenRepository())
// Valid for 20 days
.tokenValiditySeconds(20 * 24 * 60 * 60)
.rememberMeParameter("remember-me")
.and()
// Log out handler
.logout()
.permitAll()
.and()
// Enable Cross-Site Request Forgery
.csrf();
}
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// Provide database authentication and swl queries to fetch the user's data..
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select email, password, enabled from users where email=?")
.authoritiesByUsernameQuery("select us.email, ur.role from users us, " +
" roles ur where us.role_id=ur.id and us.email=?");
}
}
and this is the database table for token persistence
CREATE TABLE persistent_logins (
username VARCHAR(254) NOT NULL,
series VARCHAR(64) NOT NULL,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL,
PRIMARY KEY (series)
);

Spring Security comes with 2 implementation of PersistentTokenRepository : JdbcTokenRepositoryImpl and InMemoryTokenRepositoryImpl.
I'm using Hibernate in my application, i create a custom implementation using Hibernate instead of using JDBC.
#Repository("tokenRepositoryDao")
#Transactional
public class HibernateTokenRepositoryImpl extends AbstractDao<String, PersistentLogin>
implements PersistentTokenRepository {
static final Logger logger = LoggerFactory.getLogger(HibernateTokenRepositoryImpl.class);
#Override
public void createNewToken(PersistentRememberMeToken token) {
logger.info("Creating Token for user : {}", token.getUsername());
PersistentLogin persistentLogin = new PersistentLogin();
persistentLogin.setUsername(token.getUsername());
persistentLogin.setSeries(token.getSeries());
persistentLogin.setToken(token.getTokenValue());
persistentLogin.setLast_used(token.getDate());
persist(persistentLogin);
}
#Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
logger.info("Fetch Token if any for seriesId : {}", seriesId);
try {
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("series", seriesId));
PersistentLogin persistentLogin = (PersistentLogin) crit.uniqueResult();
return new PersistentRememberMeToken(persistentLogin.getUsername(), persistentLogin.getSeries(),
persistentLogin.getToken(), persistentLogin.getLast_used());
} catch (Exception e) {
logger.info("Token not found...");
return null;
}
}
#Override
public void removeUserTokens(String username) {
logger.info("Removing Token if any for user : {}", username);
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("username", username));
PersistentLogin persistentLogin = (PersistentLogin) crit.uniqueResult();
if (persistentLogin != null) {
logger.info("rememberMe was selected");
delete(persistentLogin);
}
}
#Override
public void updateToken(String seriesId, String tokenValue, Date lastUsed) {
logger.info("Updating Token for seriesId : {}", seriesId);
PersistentLogin persistentLogin = getByKey(seriesId);
persistentLogin.setToken(tokenValue);
persistentLogin.setLast_used(lastUsed);
update(persistentLogin);
}
}

I've reproduced the same issue. Using debug I've checked loginSuccess() method for AbstractRememberMeServices class.
Internal logic was like:
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
this.onLoginSuccess(request, response, successfulAuthentication);
}
}
it turned out that I hadn’t been tagged Remember Me tag while the user was logging in, so I couldn't call onLoginSuccess() method and fell into the block if instead of else.
After I marked the flag, I was able to persist the token and cookies.
Notice: logic can be taken from the answer mentioned by #FuSsA.

Right, so actually, cookie is written in the login success hanlder, so DB persitance error can cause a cookie tofail to be written. My issue was data type on date in persistant table in ms sql:
CREATE TABLE [dbo].[persistent_logins](
--[id] [bigint] IDENTITY(1,1) NOT NULL,
[username] [varchar](64) NOT NULL,
[series] [varchar](64) NOT NULL,
[token] [varchar](64) NOT NULL,
[last_used] [datetime] NOT NULL default CURRENT_TIMESTAMP);
From there SecSecurityConfig is:
.rememberMe()
// Valid for 20 days
.tokenValiditySeconds(20 * 24 * 60 * 60)
.rememberMeParameter("remember-me")
.key("yourPrivateKeyOfChoice")
.tokenRepository(persistentTokenRepository())
.rememberMeServices(rememberMeServices())
...
#Bean
public RememberMeServices rememberMeServices() {
CustomRememberMeServices rememberMeServices = new
CustomRememberMeServices("againThePrivateKey",
userDetailsService, persistentTokenRepository());
return rememberMeServices;
}
#Autowired
#Qualifier("dataSourceEwoForSap")
DriverManagerDataSource dataSource;
#Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setCreateTableOnStartup(false);
db.setDataSource(dataSource);
return db;
}
...then custom remember me has
public CustomRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
super(key, userDetailsService, tokenRepository);
this.tokenRepository = tokenRepository;
this.key = key;
}
#Override
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = ((User) successfulAuthentication.getPrincipal()).getEmail();
logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date());
try {
tokenRepository.createNewToken(persistentToken);
addCookie(persistentToken, request, response);
} catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}

Related

Extend Keycloak token in Spring boot

I'm using Keycloak to secure my Spring boot backend.
Dependencies:
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-2-adapter</artifactId>
<version>12.0.3</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-tomcat7-adapter-dist</artifactId>
<version>12.0.3</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-security-adapter</artifactId>
<version>12.0.3</version>
</dependency>
Security config:
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.cors()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests();
expressionInterceptUrlRegistry = expressionInterceptUrlRegistry.antMatchers("/iam/accounts/promoters*").hasRole("PROMOTER");
expressionInterceptUrlRegistry.anyRequest().permitAll();
}
Everything work fine!
But now I add a new section in keycloak token "roles" and I need to somehow extend keycloak jwt class in my Spring boot and write some code to parse and store the roles information to SecurityContext. Could you Guy please tell me how to archive the goal?
First, extends keycloak AccessToken:
#Data
static class CustomKeycloakAccessToken extends AccessToken {
#JsonProperty("roles")
protected Set<String> roles;
}
Then:
#KeycloakConfiguration
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected KeycloakAuthenticationProvider keycloakAuthenticationProvider() {
return new KeycloakAuthenticationProvider() {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) authentication;
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String role : ((CustomKeycloakAccessToken)((KeycloakPrincipal)token.getPrincipal()).getKeycloakSecurityContext().getToken()).getRoles()) {
grantedAuthorities.add(new KeycloakRole(role));
}
return new KeycloakAuthenticationToken(token.getAccount(), token.isInteractive(), new SimpleAuthorityMapper().mapAuthorities(grantedAuthorities));
}
};
}
/**
* Use NullAuthenticatedSessionStrategy for bearer-only tokens. Otherwise, use
* RegisterSessionAuthenticationStrategy.
*/
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(authenticationManagerBean());
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy());
filter.setRequestAuthenticatorFactory(new SpringSecurityRequestAuthenticatorFactory() {
#Override
public RequestAuthenticator createRequestAuthenticator(HttpFacade facade,
HttpServletRequest request, KeycloakDeployment deployment, AdapterTokenStore tokenStore, int sslRedirectPort) {
return new SpringSecurityRequestAuthenticator(facade, request, deployment, tokenStore, sslRedirectPort) {
#Override
protected BearerTokenRequestAuthenticator createBearerTokenAuthenticator() {
return new BearerTokenRequestAuthenticator(deployment) {
#Override
protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) {
log.debug("Verifying access_token");
if (log.isTraceEnabled()) {
try {
JWSInput jwsInput = new JWSInput(tokenString);
String wireString = jwsInput.getWireString();
log.tracef("\taccess_token: %s", wireString.substring(0, wireString.lastIndexOf(".")) + ".signature");
} catch (JWSInputException e) {
log.errorf(e, "Failed to parse access_token: %s", tokenString);
}
}
try {
TokenVerifier<CustomKeycloakAccessToken> tokenVerifier = AdapterTokenVerifier.createVerifier(tokenString, deployment, true, CustomKeycloakAccessToken.class);
// Verify audience of bearer-token
if (deployment.isVerifyTokenAudience()) {
tokenVerifier.audience(deployment.getResourceName());
}
token = tokenVerifier.verify().getToken();
} catch (VerificationException e) {
log.debug("Failed to verify token");
challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage());
return AuthOutcome.FAILED;
}
if (token.getIssuedAt() < deployment.getNotBefore()) {
log.debug("Stale token");
challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.STALE_TOKEN, "invalid_token", "Stale token");
return AuthOutcome.FAILED;
}
boolean verifyCaller;
if (deployment.isUseResourceRoleMappings()) {
verifyCaller = token.isVerifyCaller(deployment.getResourceName());
} else {
verifyCaller = token.isVerifyCaller();
}
surrogate = null;
if (verifyCaller) {
if (token.getTrustedCertificates() == null || token.getTrustedCertificates().isEmpty()) {
log.warn("No trusted certificates in token");
challenge = clientCertChallenge();
return AuthOutcome.FAILED;
}
// for now, we just make sure Undertow did two-way SSL
// assume JBoss Web verifies the client cert
X509Certificate[] chain = new X509Certificate[0];
try {
chain = exchange.getCertificateChain();
} catch (Exception ignore) {
}
if (chain == null || chain.length == 0) {
log.warn("No certificates provided by undertow to verify the caller");
challenge = clientCertChallenge();
return AuthOutcome.FAILED;
}
surrogate = chain[0].getSubjectDN().getName();
}
log.debug("successful authorized");
return AuthOutcome.AUTHENTICATED;
}
};
}
};
}
});
return filter;
}
}
I didn't understand why do you need extend Keycloak Token. The roles already there are in Keycloak Token. I will try explain how to access it, the Keycloak have two levels for roles, 1) Realm level and 2) Application (Client) level, by default your Keycloak Adapter use realm level, to use application level you need setting the propertie keycloak.use-resource-role-mappings with true in your application.yml
How to create roles in realm
enter image description here
How to creare roles in client
enter image description here
User with roles ADMIN (realm) and ADD_USER (application)
enter image description here
To have access roles you can use KeycloakAuthenticationToken class in your Keycloak Adapter, you can try invoke the following method:
...
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken authenticationToken) {
final AccessToken token = authenticationToken.getAccount().getKeycloakSecurityContext().getToken();
final Set<String> roles = token.getRealmAccess().getRoles();
final Map<String, AccessToken.Access> resourceAccess = token.getResourceAccess();
...
}
...
To protect any router using Spring Security you can use this annotation,  example below:
#PreAuthorize("hasRole('ADMIN')")
#GetMapping("/users")
public ResponseEntity<Object> getUsers(final KeycloakAuthenticationToken token) {
return ResponseEntity.ok(service.getUsers());
}
Obs: The  keycloak.use-resource-role-mappings set up using #PreAuthorize Annotation.  If set to true, #PreAuthorize checks roles in token.getRealmAccess().getRoles(), if false it checks roles in token.getResourceAccess().
If you want add any custom claim in token, let me know that I can explain better.
I put here how I set up my Keycloak Adapter and the properties in my  application.yml:
SecurityConfig.java
...
#KeycloakConfiguration
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
#Value("${project.cors.allowed-origins}")
private String origins = "";
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(keycloakAuthenticationProvider);
}
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Bean
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new NullAuthenticatedSessionStrategy();
}
#Override
protected KeycloakAuthenticationProcessingFilter keycloakAuthenticationProcessingFilter() throws Exception {
KeycloakAuthenticationProcessingFilter filter = new KeycloakAuthenticationProcessingFilter(this.authenticationManagerBean());
filter.setSessionAuthenticationStrategy(this.sessionAuthenticationStrategy());
filter.setAuthenticationFailureHandler((request, response, exception) -> {
response.addHeader("Access-Control-Allow-Origin", origins);
if (!response.isCommitted()) {
response.sendError(401, "Unable to authenticate using the Authorization header");
} else if (200 <= response.getStatus() && response.getStatus() < 300) {
throw new RuntimeException("Success response was committed while authentication failed!", exception);
}
});
return filter;
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
super.configure(http);
http.csrf()
.disable()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "**").permitAll()
.antMatchers("/s/**").authenticated()
.anyRequest().permitAll();
}
}
application.yml
..
keycloak:
enabled: true
auth-server-url: http://localhost:8080/auth
resource: myclient
realm: myrealm
bearer-only: true
principal-attribute: preferred_username
use-resource-role-mappings: true
..

Cannot access to Main Page after using spring-security, although login is successful

I want to add security part to the project and I am using spring security for providing backend security. When I added custom login filter that extends AbstractAuthenticationProcessingFilter of spring security, I got an error about cross origin problem. Now I added http.cors(); to the WebSecurityConfig and I do not get cross origin errors anymore.
I am sending a request to the backend http://localhost:8081/user/sys-role/verifyTargetUrl. Now, the exact error is Uncaught (in promise) Error: Infinite redirect in navigation guard at eval (vue-router.esm-bundler.js?6c02:2913). So somehow frontend vue-router guards find itself in an infinite loop. I will appreciate any of your help.
UPDATE:
It turned out that I don't get the response code as 200 and that causes the infinite loop in vue-router. My question becomes pure spring-security question because there seems to be no issue with vue-router. I send a post request to http://localhost:8081/user/sys-role/verifyTargetUrl but my request does not enter to the PostMapping in backend. It rather enters CustomAuthenticationEntryPoint shown below and sets the code to 504. But in verifyTargetUrl of backend I set it to 200. Besides, onAuthenticationSuccess of CustomAuthenticationSuccessfulHandler is also called in the backend.
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Message msg=new Message();
msg.setCode(504);
msg.setMsg("authenticate fail");
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.toString());
httpServletResponse.getWriter().write(JSON.toJSONString(msg));
}
}
The console of the browser:
config: {url: "http://localhost:8081/user/sys-role/verifyTargetUrl", method: "post", data: "{"userId":1017,"targetUrl":"/Main"}", headers: {…}, transformRequest: Array(1), …} data: {code: 504, msg: "authenticate fail"}
UPDATE 2: More Code
CustomJSONLoginFilter.java
public class CustomJSONLoginFilter extends AbstractAuthenticationProcessingFilter {
private final ISysUserService iUserService;
public CustomJSONLoginFilter(String defaultFilterProcessesUrl, ISysUserService iUserService) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl, HttpMethod.POST.name()));
this.iUserService = iUserService;
}
#Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
JSONObject requestBody= getRequestBody(httpServletRequest);
String username= requestBody.getString("username");
String password= requestBody.getString("password");
// get user info by username
SysUser sysUser= iUserService.getUserInfoByUsername(username);
//verify password
String encorderType=EncryptionAlgorithm.ENCODER_TYPE.get(1);
PasswordEncoder passwordEncoder =EncryptionAlgorithm.ENCODER_MAP.get(encorderType);
System.out.println(passwordEncoder);
System.out.println(sysUser);
System.out.println(password);
if(sysUser==null){
throw new UsernameNotFoundException("can't find userinfo by username:"+username);
}else if(!passwordEncoder.matches(password,sysUser.getPassword())){
throw new BadCredentialsException("password wrong!");
}else{
List<SysRole> list= iUserService.findRolesByUsername(username);
List<SimpleGrantedAuthority> simpleGrantedAuthorities= new ArrayList<SimpleGrantedAuthority>();
Iterator<SysRole> i=list.iterator();
while(i.hasNext()){
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(i.next().getRoleName()));
}
return new UsernamePasswordAuthenticationToken(username,password,simpleGrantedAuthorities);
}
}
private JSONObject getRequestBody(HttpServletRequest request) throws AuthenticationException{
try {
StringBuilder stringBuilder = new StringBuilder();
InputStream inputStream = request.getInputStream();
byte[] bs = new byte[StreamUtils.BUFFER_SIZE];
int len;
while ((len = inputStream.read(bs)) != -1) {
stringBuilder.append(new String(bs, 0, len));
}
return JSON.parseObject(stringBuilder.toString());
} catch (IOException e) {
System.out.println("get request body error.");
}
throw new AuthenticationServiceException("invalid request body");
}
I would not write a custom security but use Spring Security, they have a strong library and has worked it out for you, it is a matter of configuration!
My Aproach was easy implemented! I have a user class where I store
Kotlin Code
var username: String? = null
var password: String? = null
var active: Boolean = false
var confirmationToken: String? = null // email confirmationToken sent # registration and other admin functions
var token: String? = null // If JWT token exist (not NULL or "") then the Networker is logged in with Client!
var roles: String? = null
var permissions: String? = null
ADD CONSTRUCTORS ....
val roleList: List<String>
get() = if (this.roles?.isNotEmpty()!!) {
listOf(*this.roles?.split(",".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()!!)
} else ArrayList()
val permissionList: List<String>
get() = if (this.permissions?.isNotEmpty()!!) {
listOf(*this.permissions?.split(",".toRegex())?.dropLastWhile { it.isEmpty() }?.toTypedArray()!!)
} else ArrayList()
from there I config the securityConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
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.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
#Configuration
#EnableWebSecurity
class SecurityConfiguration(private val userPrincipalDetailService: UserPrincipalDetailService) :
WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authenticationProvider())
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.authorizeRequests()
.antMatchers("/index.html").permitAll()
.antMatchers("/security/**").permitAll()
.antMatchers("/profile/**").authenticated()
.antMatchers("/admin/**").hasRole("ADMIN")
.and().formLogin()
.defaultSuccessUrl("/profile/index", true)
.loginProcessingUrl("/security/login")
.loginPage("/security/login").permitAll()
.usernameParameter("username")
.passwordParameter("password")
.and().logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
.logoutRequestMatcher(AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/security/login")
.and()
.rememberMe().tokenValiditySeconds(2592000) // 2592000 = 30 days in Seconds
.rememberMeParameter("rememberMe")
}
private fun authenticationProvider(): DaoAuthenticationProvider {
val daoAuthenticationProvider = DaoAuthenticationProvider()
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder())
daoAuthenticationProvider.setUserDetailsService(this.userPrincipalDetailService)
return daoAuthenticationProvider
}
#Bean
internal fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
}
If you want to follow a Course in Spring Security - you can follow this one
Spring Boot Security by Romanian Coder

Java spring OAuth2 client (resource server) always returns 'invalid token' error

I have wrote my oven OAuth2 server and I can successfully retrieve access tokens from my server, however when it comes to the client it always returns 'invalid token error'.
I have searched and have read many articles, pages, and links such as the following link:
Spring Security OAuth2 Resource Server Always Returning Invalid Token
Yet I cannot solve the problem, here is my code and I would be grateful I you could help me.
Configuration in authentication server :
#Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
private static final String SERVER_RESOURCE_ID = "oauth2-server";
private static InMemoryTokenStore tokenStore = new InMemoryTokenStore();
#Configuration
#EnableResourceServer
protected static class ResourceServer extends ResourceServerConfigurerAdapter {
#Override
public void configure( ResourceServerSecurityConfigurer resources ) throws Exception{
resources.tokenStore( tokenStore ).resourceId( SERVER_RESOURCE_ID );
}
#Override
public void configure( HttpSecurity http ) throws Exception {
http.requestMatchers().antMatchers("/user").and().authorizeRequests().antMatchers("/me").access("#oauth2.hasScope('read')");
}
} // ResourceServer
#Configuration
#EnableAuthorizationServer
protected static class AuthConfig extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Override
public void configure( ClientDetailsServiceConfigurer clients ) throws Exception {
clients.inMemory()
.withClient( "insert_server" ) // name of client
.secret( "{noop}" + "123456" )
.authorizedGrantTypes( "refresh_token", "password", "client_credentials" )
.scopes( "webclient", "mobileclient" )
.resourceIds( SERVER_RESOURCE_ID )
.and().withClient("rest_server")
.secret( "{noop}" + "123456" )
.authorizedGrantTypes( "refresh_token", "password", "client_credentials" )
.scopes( "webclient", "mobileclient" )
.resourceIds( SERVER_RESOURCE_ID );
//System.out.println( encoder.encode("123456") );
}
#Override
public void configure( AuthorizationServerEndpointsConfigurer endPoints ) throws Exception {
/* endPoints
.authenticationManager( this.authenticationManager )
.userDetailsService( this.userDetailsService ); */
endPoints.authenticationManager( this.authenticationManager )
.tokenStore( tokenStore )
.approvalStoreDisabled();
}
} // AuthConfig
}
Web security class in authentication server:
#Configuration
public class WebSecutiryConfigurer extends WebSecurityConfigurerAdapter {
private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
#Override
#Bean
public AuthenticationManager authenticationManagerBean( ) throws Exception {
return super.authenticationManagerBean();
}
#Override
#Bean
public UserDetailsService userDetailsServiceBean( ) throws Exception {
return super.userDetailsServiceBean();
}
#Override
protected void configure( AuthenticationManagerBuilder auth ) throws Exception{
auth.inMemoryAuthentication()
.withUser( "pswin ")
.password( "{noop}" + "123456" )
.roles( "USER" )
.and().withUser( "admin" ) // username
.password( "{noop}" + "123456" ) // password
.roles( "USER", "ADMIN" ); // roles
}
}
Client Config:
security.oauth2.client.client-id=rest_server
security.oauth2.client.client-secret=123456
security.oauth2.client.access-token-uri=http://localhost:8989/auth/oauth/token
security.oauth2.client.user-authorization-uri=http://localhost:8989/auth/oauth/authorize
security.oauth2.resource.user-info-uri=http://localhost:8989/auth/user
and my controller:
#RestController
#RequestMapping( "/city" )
#EnableResourceServer
public class CityController {
private CityService cityService;
//! Constructor
CityController( CityService cityService ){
this.cityService = cityService;
}
#GetMapping( "/{city_id}" )
public CityDTO getCityByID( #PathVariable Long city_id ){
return new CityDTO( cityService.getByID( city_id ) );
}
#PreAuthorize("#oauth2.hasScope('webclient')")
#GetMapping ( "/" )
public PageDTO getAll( Pageable pageable ){
Page page = this.cityService.getAll( pageable );
return new PageDTO( page.map( ( city ) -> new CityDTO( (City)city ) ) );
}
}
Further details:
Java version: 10
Spring-boot versuin: 2.0.4.RELEASE
Spring-cloud version: Finchley.SR1
As you posted, invalid access token usually happens either 1) token that client sent is different that what oauth2 provider issued OR 2) can be expired if token's TTL is too short. It's hard to know without specific error message but just "invalid token"... but you should be able to find more debugging log in server log ( in spring side ).
One thing you can do is, add breakpoint in OAuth2AuthenticationProcessingFilter#doFilter() since that's where oauth2 check is happening. Debuging step by step should be enough for you to figure out what's wrong with your token.

Spring security returns wrong loggedin user AFTER override UserDetailsContextMapper

I am using spring security with LDAP in a spring boot application.
Everything was working fine before I did below changes in security config as below.
Override UserDetailsContextMapper to set ROLE for the users.
Restrict URLs based on user role.
Below are the code changes to achieve above 2 things.
1. UserDetailsContextMapper:
#Component
public class UserDetailsContextMapperImpl implements UserDetailsContextMapper, Serializable{
private static final long serialVersionUID = 3962976258168853954L;
private static Logger logger = LoggerFactory.getLogger(UserDetailsContextMapperImpl.class);
#SuppressWarnings("rawtypes")
#Override
public UserDetails mapUserFromContext(DirContextOperations ctx, String username, Collection<? extends GrantedAuthority> authority) {
List<GrantedAuthority> mappedAuthorities = new ArrayList<GrantedAuthority>();
try{
Attributes attrListOuter = ctx.getAttributes();
Attribute attrListInner = attrListOuter.get(UserConstant.MEMBER_OF);
logger.info("memberOf: " + attrListInner.getID());
for (NamingEnumeration enumInner = attrListInner.getAll(); enumInner.hasMore();){
String CN = (String)enumInner.next();
logger.info("CN value: " + CN);
if(CN.contains(UserConstant.MASTER_GROUP_PROJ_NAME)){
logger.info("Logged in user is authorized to acccess Rates Toronto application: {}", username );
mappedAuthorities.add(new SimpleGrantedAuthority(UserConstant.ROLE_ABC));// adding ROLE_ABC to APP/LDAP users.
logger.info("User {} Role set as : {}", username, UserConstant.ROLE_ABC );
break;
}else if(CN.contains(UserConstant.GROUP_XYZ)){
mappedAuthorities.add(new SimpleGrantedAuthority(UserConstant.ROLE_XYZ));
logger.info("User {} Role set as : {}", username, UserConstant.ROLE_XYZ );
break;
}
}
if(mappedAuthorities.isEmpty()){
logger.info("Logged in user is NOT authorized to access ABCD application : {}", username );
}
}catch(Exception ex){
logger.info("Exception while mapping UserDetails with LDAP" + ex.getMessage());
}
//Returning Spring Seurity's User object.
return new org.springframework.security.core.userdetails.User(username, "", true, true, true, true, mappedAuthorities);
}
2. Restrict URLs based on user role:
In my websecurity configuration class,
#Configuration
#EnableWebSecurity
public class AbcWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
.......
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/api/**").hasAnyRole("REST","ABC").and().httpBasic().and().formLogin();
http.authorizeRequests().antMatchers("/xyz/**").hasRole("XYZ").and().httpBasic();
http.authorizeRequests().antMatchers("/abc/**").hasRole("ABC").and().httpBasic(); // Users who are accessing the URLS/application via application's UI. ie. Business User.
http.headers().contentTypeOptions().xssProtection().cacheControl().httpStrictTransportSecurity().frameOptions().disable();
http.headers().addHeaderWriter(new StaticHeadersWriter("Cache-Control","no-cache, no-store, max-age=0, must-revalidate"));
http.headers().addHeaderWriter(new StaticHeadersWriter("Expires","0"));
http.csrf().disable();
http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry());
super.configure(http);
}
and ......
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication().userDetailsContextMapper(userDetailsContextMapperImpl).userSearchFilter(SAM_ACC).userSearchBase(base).contextSource().url(url).managerDn(managerDn).managerPassword(password);
auth.inMemoryAuthentication().withUser(restUserName).password(restPassword).roles(restRole);
//This publisher will trigger AuthenticationFailureBadCredentialsEvent (AbstractAuthenticationFailureEvent)
auth.authenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher));
}
After the above changes, getting loggedIn user giving wrong user ( sometimes),
This is not happening every-time, but happening intermittently.
I am using below code to get the current logged in user.
Authentication authentication = securityContextHolder.getContext().getAuthentication();
if (authentication == null)
return null;
org.springframework.security.core.userdetails.User userDetails = (org.springframework.security.core.userdetails.User) authentication.getPrincipal();
String userName = userDetails.getUsername();
I am unable to find where I am missing, any pointers/direction will be very helpful.
It is returning wrong user from already authenticated users.
I am using spring boot 1.2.0 which uses spring security 3.2.5 by default

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;
}
}

Resources