How can I display the current logged in User with Spring Boot Thymeleaf? - spring

I am trying to display the details of the current user however I keep getting errors. I tried accessing the authenticated user from the template but that did not work as I was getting this error:
Method getFirstName() cannot be found on org.springframework.security.core.userdetails.User type
I was trying to get the information from a controller and then saving it in a string and passsing the string to a template but that wasn't working either.
Here is my SecurityConfig class:
#Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private UserService userService;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(
"/registration",
"/js/**",
"/css/**",
"/img/**",
"/webjars/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.invalidateHttpSession(true)
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout")
.permitAll();
}
#Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
#Bean
public DaoAuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
auth.setUserDetailsService(userService);
auth.setPasswordEncoder(passwordEncoder());
return auth;
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
Here is my UserService Class:
public interface UserService extends UserDetailsService {
User findByEmailAddress(String emailAddress);
// User findByFirstName(String firstName);
User save(UserRegistrationDto registration);
}
Here is my UserServiceImpl class:
#Service
public class UserServiceImpl implements UserService {
#Autowired
private UserRepository userRepository;
#Autowired
private BCryptPasswordEncoder passwordEncoder;
#Override
public UserDetails loadUserByUsername(String emailAddress) throws
UsernameNotFoundException {
User user = userRepository.findByEmailAddress(emailAddress);
if (user == null){
throw new UsernameNotFoundException("Invalid username or
password.");
}
return new
org.springframework.security.core.userdetails.User(user.getEmailAddress(),
user.getPassword(),
mapRolesToAuthorities(user.getRoles()));
}
public User findByEmailAddress(String emailAddress){
return userRepository.findByEmailAddress(emailAddress);
}
public User save(UserRegistrationDto registration){
User user = new User();
user.setFirstName(registration.getFirstName());
user.setSurname(registration.getSurname());
user.setEmailAddress(registration.getEmailAddress());
user.setPassword(passwordEncoder.encode(registration.getPassword()));
user.setRoles(Arrays.asList(new Role("ROLE_USER")));
return userRepository.save(user);
}
private Collection<? extends GrantedAuthority>
mapRolesToAuthorities(Collection<Role> roles){
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
}
Here is some code from the template class where I'm trying to get the information:
th:text ="${#authentication.getPrincipal().getFirstName()}">
th:text ="${#authentication.getPrincipal().getUser().getFirstName()}">
This is the login controller. The parts I have commented out was another way I was trying to get the current users details:
#Controller
//#RequestMapping("/login")
public class MainController {
// #GetMapping("/")
// public String root() {
// return "userProfile1";
// }
#GetMapping("/login")
public String login(Model model) {
return "login";
}
// #GetMapping
// public String displayUserAccount(#ModelAttribute("user") #Valid
UserRegistrationDto userDto, BindingResult result, Model model) {
//
//
// model.addAttribute("firstName", ((UserRegistrationDto)
auth).getEmailAddress());
//
// model.addAttribute("emailAddress", userDto.getEmailAddress());
// model.addAttribute("firstName", userDto.getFirstName());
// model.addAttribute("surname", userDto.getSurname());
// model.addAttribute("age", userDto.getAge());
// model.addAttribute("gender", userDto.getGender());
// model.addAttribute("dob", userDto.getDob());
// // return "redirect:/registration?success";
// return "userProfile1";
//
// }
#ResponseBody
public String currentUserName(Authentication auth) {
((UserRegistrationDto) auth).getEmailAddress();
return "userProfile1";
}
}
This is all over the place I'm sorry! Thanks so much for anyone who helps :D

You can use Thymeleaf extras for display authenticated user details.
Thymeleaf Extras Springsecurity4
<div th:text="${#authentication.name} ></div>

The problem is here:
return new
org.springframework.security.core.userdetails.User(user.getEmailAddress(),
user.getPassword(),
mapRolesToAuthorities(user.getRoles()));
You lose the reference to your User entity. Change it to:
return user;
For this to work, you need to update your User entity to implement UserDetails interface:
public class User implements UserDetails {
// some new methods to implement
}
Then, your Thymleaf code should work. Another way of getting the firstName would be:
<span th:text="${#request.userPrincipal.principal.firstName}"></span>

I figured out how to fix my problem.
I created this method in a controller:
#Autowired
UserRepository userR;
#GetMapping
public String currentUser(#ModelAttribute("user") #Valid UserRegistrationDto userDto, BindingResult result, Model model) {
Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication();
String email = loggedInUser.getName();
User user = userR.findByEmailAddress(email);
String firstname = user.getFirstName();
model.addAttribute("firstName", firstname);
model.addAttribute("emailAddress", email);
return "userProfile1"; //this is the name of my template
}
and then I added this line of code in my html template:
Email: th:text="${emailAddress}"

Reference (4. Spring Security Dialect):
https://www.thymeleaf.org/doc/articles/springsecurity.html
Add dependencies pom.xml
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
and the view (Thymeleaf):
<div sec:authorize="isAuthenticated()">
Authenticated user roles:
Logged user: <span sec:authentication="name"></span> |
Roles: <span sec:authentication="principal.authorities"></span>
</div>
I hope you serve them

You can get the username attribute easily from your Principal class.
#GetMapping(value = "/")
public String index(#AuthenticationPrincipal MyUserPrincipal principal) {
String username = principal.getUsername();
//Do whatever you want here
return "index";
}
However, if you want more details than the ones inside Principal class then you need to explicitly define them in your principal class:
public int getId() {
return member.getId();
}
So now you can invoke it directly:
#GetMapping(value = "/")
public String index(#AuthenticationPrincipal MyUserPrincipal principal) {
int userId = principal.getId();
//Do whatever you want here
return "index";
}
You will need to import the following:
import org.springframework.security.core.annotation.AuthenticationPrincipal;
If you only want to get a Principal class attribute directly from Thymeleaf then you can alternatively do the following:
<span sec:authentication="principal.username">Username</span>

Related

Requests coming back as 401 unauthorized

I recently asked a question very similar to this one but instead of 401 the error I was getting was 403 (Forbbiden), but I changed the entire code so I decided to post a new one specific to this code and this problem.
I'm trying to create an user logic to my project (for the first time ever) but it has been impossible to implement any kind of security measure. I've been stuck in this for days so if anyone knows where I'm wrong I'd be grateful!
this is my code:
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/users/create", "/users/create/**").permitAll()
.and()
.httpBasic();
}
}
#Data
#Component
public class CreateUserRoleDTO {
private Integer idUser;
private List<Integer> idsRoles;
public CreateUserRoleDTO() {
super();
}
public CreateUserRoleDTO(Integer idUser, List<Integer> idsRoles) {
super();
this.idUser = idUser;
this.idsRoles = idsRoles;
}
public Integer getIdUser() {
return idUser;
}
public void setIdUser(Integer idUser) {
this.idUser = idUser;
}
public List<Integer> getIdsRoles() {
return idsRoles;
}
public void setIdsRoles(List<Integer> idsRoles) {
this.idsRoles = idsRoles;
}
}
#Service
public class CreateRoleUserService {
#Autowired
private UserRepository repo;
#Autowired
private CreateUserRoleDTO createUserRoleDTO;
public Users execute(CreateUserRoleDTO createUserRoleDTO) {
Optional<Users> userExists=repo.findById(createUserRoleDTO.getIdUser());
List<Roles> roles=new ArrayList<>();
if (userExists.isEmpty()) {
throw new Error("User does not exist");
}
roles=createUserRoleDTO.getIdsRoles().stream().map(role -> {
return new Roles(role);
}).collect(Collectors.toList());
Users user=userExists.get();
user.setRole(roles);
repo.save(user);
return user;
}
#Entity
#Table(name="users_table")
public class Users implements Serializable{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column(unique=true)
private String login;
#Column(unique=true)
private String email;
private String password;
#ManyToMany
private List<Roles> role;
}
(plus the getters and setters and constructors)
data.sql:
INSERT INTO `ROLES`(`ID`, `NAME`) VALUES(1, 'USER');
INSERT INTO `ROLES`(`ID`,`NAME`) VALUES(2, 'ADMIN');
-> the code runs fine, it even gives me the security password, the problem appears when I try to make any kind of requests.
The entire code if I've left anything out: https://github.com/vitoriaacarvalho/backend-challenge-very-useful-tools-to-remember-
An authentication configuration is missing in your SecurityConfig. For example, try adding the following to your configure method:
http.httpBasic();
Additionally, your security configuration is missing a default authorization rule, so authentication is not actually required. You can try adding .anyRequest().authenticated() to test this out.
Here's a configuration which uses the lambda syntax available in the DSL and is ready to be upgraded to Spring Security 6:
#Configuration
#EnableWebSecurity
#EnableMethodSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.antMatchers("/users/create", "/users/create/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
// Disable CSRF for testing.
// TODO: Delete the following line and learn about CSRF!
http.csrf().disable();
return http.build();
}
#Bean // Automatically injected into Spring Security
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// Note: We don't configure a UserDetailsService since it is already
// annotated #Service and therefore already published as an #Bean.
}
Unfortunately, I also spotted a few other mistakes in your application that made it not work.
It looks like you have a mistake in the JPQL used to query the user for the UserDetailsService. The WHERE clause should be where u.login = :username (add a u.).
You also have the if-statement inverted as well. When throwing a UsernameNotFoundException (a better exception than Error for this case), it would look like:
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = repo.findByUsernameFetchRoles(username);
if (user == null) {
throw new UsernameNotFoundException("User does not exist!");
}
return UserPrincipal.create(user);
}
Lastly, the constructor of your Users class was not assigning user data from the user parameter. It should be:
public UserPrincipal(Users user) {
this.login = user.getLogin();
this.password = user.getPassword();
...
}
With those changes, authentication works and you're on your way to learning Spring Security!

Customise user login behaviour in OAuth based B2B multi tenant Spring Boot application using Spring Security

I am working on a Spring Boot application, which has two tenants as of now: tenant1 and tenant2 (in the future, I will add more tenants). Each of the tenants has its own authentication providers.
In order to achieve the same, as of now, I have made the following changes in my application:
config changes are as follows:
spring.security.oauth2.client.registration.tenant1.client-id=abcd
spring.security.oauth2.client.registration.tenant1.client-authentication-method=basic
spring.security.oauth2.client.registration.tenant1.authorization-grant-type=authorization_code
myapp.oauth2.path=https://external.authorization.server/services/oauth2/
spring.security.oauth2.client.provider.tenant1.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.tenant1.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.tenant1.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.tenant1.user-name-attribute=name
spring.security.oauth2.client.registration.tenant2.client-id=efgh
spring.security.oauth2.client.registration.tenant2.client-secret=secret
spring.security.oauth2.client.registration.tenant2.client-authentication-method=basic
spring.security.oauth2.client.registration.tenant2.authorization-grant-type=authorization_code
spring.security.oauth2.client.provider.tenant2.token-uri=${myapp.oauth2.path}token
spring.security.oauth2.client.provider.tenant2.authorization-uri=${myapp.oauth2.path}authorize
spring.security.oauth2.client.provider.tenant2.user-info-uri=${myapp.oauth2.path}userinfo
spring.security.oauth2.client.provider.tenant2.user-name-attribute=name
As of now, I am fetching client secrets for both tenants from Vault, so I had to define the OAuth2 configuration as follows:
#EnableConfigurationProperties(OAuth2ClientProperties.class)
#Conditional(ClientsConfiguredCondition.class)
#Configuration
public class OAuth2Configuration {
static final String OAUTH2_CLIENT_SECRET_KEY = "oauth2_client_secret";
private static final Logger log = LoggerFactory.getLogger(OAuth2Configuration.class);
private static final String OAUTH2_REGISTRATION_MISSING =
"oAuth2 registration properties are missing";
private final ApplicationSecretProvider applicationSecretProvider;
private final Map<String, ClientAuthenticationMethod> clientAuthenticationMethodMap =
new HashMap<>();
private final String authenticationMethod;
public OAuth2Configuration(
#Value("${spring.security.oauth2.client.registration.tenant1.client-authentication-method}")
final String authenticationMethod,
final ApplicationSecretProvider applicationSecretProvider) {
this.authenticationMethod = authenticationMethod;
this.applicationSecretProvider = applicationSecretProvider;
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.POST.getValue(), ClientAuthenticationMethod.POST);
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.BASIC.getValue(), ClientAuthenticationMethod.BASIC);
this.clientAuthenticationMethodMap
.put(ClientAuthenticationMethod.NONE.getValue(), ClientAuthenticationMethod.NONE);
}
#Bean
public InMemoryClientRegistrationRepository getClientRegistrationRepository(
OAuth2ClientProperties properties) {
List<ClientRegistration> registrations = new ArrayList<>(
OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(properties).values());
//We will have only one client registered for oAuth
if (CollectionUtils.isEmpty(registrations)) {
log.error(OAUTH2_REGISTRATION_MISSING);
throw new IllegalStateException(OAUTH2_REGISTRATION_MISSING);
}
ClientRegistration registration = registrations.get(0);
ClientRegistration.Builder builder = ClientRegistration.withClientRegistration(registration);
ClientAuthenticationMethod clientAuthenticationMethod =
getClientAuthenticationMethod(authenticationMethod);
ClientRegistration completeRegistration = builder
.clientSecret(applicationSecretProvider.getSecretForKey(OAUTH2_CLIENT_SECRET_KEY))
.clientAuthenticationMethod(clientAuthenticationMethod)
.build();
ClientRegistration testRegistration = registrations.get(1);
return new InMemoryClientRegistrationRepository(List.of(completeRegistration, testRegistration));
}
protected ClientAuthenticationMethod getClientAuthenticationMethod(String grantType) {
ClientAuthenticationMethod retValue = clientAuthenticationMethodMap.get(grantType);
if (retValue == null) {
return ClientAuthenticationMethod.NONE;
}
return retValue;
}
}
Then I extended DefaultOAuth2UserService in order to save user details in my application as follows:
#Component
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private UserRepository userRepository;
private AuthorityRepository authRepository;
#Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Autowired
public void setAuthorityRepository(AuthorityRepository
authorityRepository) {
this.authorityRepository = authorityRepository;
}
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
DefaultOAuth2User oAuth2User = (DefaultOAuth2User) super.loadUser(userRequest);
Collection<GrantedAuthority> authorities = new HashSet<>(oAuth2User.getAuthorities());
Map<String, Object> attributes = oAuth2User.getAttributes();
...
return new DefaultOAuth2User(authorities, oAuth2User.getAttributes(), userNameAttributeName);
}
}
Security configuration is as follows:
#EnableWebSecurity
#Import(SecurityProblemSupport.class)
#ConditionalOnProperty(
value = "myapp.authentication.type",
havingValue = "oauth",
matchIfMissing = true
)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customoAuth2UserService;
public SecurityConfiguration(CustomOAuth2UserService customoAuth2UserService) {
this.customoAuth2UserService = customoAuth2UserService;
}
public void configure(HttpSecurity http) throws Exception {
http
.csrf()
.authorizeRequests()
.antMatchers("/login**").permitAll()
.antMatchers("/manage/**").permitAll()
.antMatchers("/api/auth-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/info").permitAll()
.antMatchers("/management/prometheus").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.anyRequest().authenticated()
//.and().oauth2ResourceServer().jwt()
.and()
//.and()
.oauth2Login()
.redirectionEndpoint()
.baseUri("/oauth2**")
.and()
.failureUrl("/api/redirectToHome")
.userInfoEndpoint().userService(customoAuth2UserService);
http.cors().disable();
}
}
With this on /login page, users from both the tenants are able to see both the login links.
I have following question in this regard:
(1) Instead of showing multiple links on the login page, would like to have a common entry page for all users, where users can enter the email and based on tenant id (derived from email), user can be redirected to the appropriate authentication provider and post successful authentication, authenticated user details can be saved in application database as done in CustomOAuth2UserService. How would I achieve this?
In this regard, I have gone through several articles/posts but did not get any concrete idea regarding what changes should I do in the existing code base to achieve this.

Spring Security OAuth2 - Getting a custom principal from the Authentication object?

I am currently trying to get a custom UserInformation object to come back when I try to access the principal field from the Authentication object (Authentication.getPrincipal()) when using OAuth2. I am enabling OAuth2 in the WebSecurityConfingAdapter by adding the .oauth2Login() property:
#Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2Login();
}
}
When I add this property and try to access the principal from the controller, it says that the type of the principal is DefaultOidcUser.
#RestController
public class OAuthController {
#GetMapping("/getPrincipal")
public String authenticate(Authentication authenticate) {
return "PRINCIPAL CLASS: " + authenticate.getPrincipal().getClass().getName();
}
}
Is there a way I can have the Authentication principal return a custom object (not an OidcUser)? I tried to write a custom OidcService, but it still needs to return an OidcUser.
Here are the dependencies I am pulling in:
spring-boot-starter-security: 2.2.4-RELEASE
spring-security-oauth2-client: 5.2.1-RELEASE
spring-security-oauth2-jose: 5.2.1-RELEASE
Below are the custom OidcServer and UserInformation object I have so far:
#Configuration
public class CustomOIDCUserService extends OidcUserService {
#Override
public OidcUser loadUser(OidcUserRequest oidcUserRequestst) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(oidcUserRequestst);
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
//Add roles to mappedAuthorities
UserInformation userInfo = new UserInformation(new DefaultOidcUser(mappedAuthorities,
oidcUser.getIdToken(),
oidcUser.getUserInfo()));
//Initialize other fields from oidcUser
return userInfo;
}
}
public class UserInformation implements OidcUser, UserDetails {
private OidcUser oidcUser;
String name;
String email;
public UserInformation(OidcUser oidcUser) {
this.oidcUser = oidcUser;
}
//Setting constructors, getters, and setters
Thank you!

OAuth2 Client Principal do not have GrantedAuthorities when authenticated by Other Custom Authorization Server (SpringBoot2 & OAuth2)

i'm using Spring Boot2 as Framework and Thymeleaf as template engine.
in my authorization server, i added user 'admin' as 'ROLE_ADMIN'.
but in Client Application, when i loged in as 'admin' and print Authentication Object from SecurityContextHolder.getContext().getAuthentication(), Granted Authorities property has only 'ROLE_USER'.
following is my authorization server config.
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password(passwordEncoder().encode("123")).roles("USER", "ADMIN");
auth
.inMemoryAuthentication()
.withUser("user").password(passwordEncoder().encode("123")).roles("USER");
}
and following is Authentication Object from SecurityContextHolder.getContext().getAuthentication()'s logging code.
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println(auth.isAuthenticated());
System.out.println(auth.getAuthorities());
System.out.println(auth.getPrincipal());
and result is
// isAuthenticated()
true
// getAuthorites()
[ROLE_USER]
// getPrincipal()
Name: [admin], Granted Authorities: [ROLE_USER], User Attributes: [authorities=[{authority=ROLE_ADMIN}, {authority=ROLE_USER}], ...
following is my thymeleaf code.
<div sec:authorize="isAuthenticated()">
Text visible only to authenticated users.
<!-- Principal name -->
Authenticated username:
<div sec:authentication="name"></div>
<div sec:authorize="hasRole('USER')">Text visible to user.</div>
<!-- i cant see this message -->
<div sec:authorize="hasRole('ADMIN')">Text visible to admin.</div>
Authenticated user roles:
<!-- print '[ROLE_USER]' only -->
<div sec:authentication="principal.authorities"></div>
</div>
<div sec:authorize="!isAuthenticated()">Text visible only to
unauthenticated users.
</div>
so, i want to access Principal.UserAttributes.authorities in thymeleaf.
i'm refering OAuth2AuthenticationToken, OAuth2User.getAttributes() and DefaultOAuth2User.toString()
how can i do this?
I solved.
In Authorization Server, i configed like this.
AuthorizationServer WebSecurityConfigurerAdapter config
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password(passwordEncoder().encode("123")).roles("USER", "ADMIN").authorities("USER", "ADMIN");
auth
.inMemoryAuthentication()
.withUser("user").password(passwordEncoder().encode("123")).roles("USER");
}
...
}
and following is my Resource Server's /me mapping controller
ResourceServer /me mapped Controller
#RestController
public class UserController {
#RequestMapping("/me")
public Principal user(Principal principal) {
return principal;
}
}
and following is my Client's WebSecurityConfigurerAdapter config
Client WebSecurityConfigurerAdapter config
#Configuration
#EnableOAuth2Client
public class WebSecurityConfigurerAdapterImpl extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/home", "/error", "/webjars/**", "/resources/**", "/login**").permitAll()
.anyRequest().authenticated()
.and().oauth2Login();
}
and in Client's Controller, i logged like this.
logging Principal in Client Controller
#GetMapping("")
public String git1() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println(auth.getPrincipal());
/** Thymeleaf using this **/
Object authenticationProperty = AuthUtils.getAuthenticationProperty(auth, "principal.attributes['authorities']");
System.out.println(authenticationProperty.toString());
return VIEW_PATH + "git1";
}
and following is the result
Name: [admin], Granted Authorities: [ROLE_USER], User Attributes: [authorities=[{authority=USER}, {authority=ADMIN}], details={remoteAddress=127.0.0.1, sessionId=null, tokenValue=82a7a532-a31e-4d0a-bd83-f15a9cbea3bc, tokenType=Bearer, decodedDetails=null}, authenticated=true, userAuthentication={authorities=[{authority=USER}, {authority=ADMIN}], details=null, authenticated=true, principal=admin, credentials=N/A, name=admin}, oauth2Request={clientId=foo, scope=[read], requestParameters={client_id=foo}, resourceIds=[], authorities=[], approved=true, refresh=false, redirectUri=null, responseTypes=[], extensions={}, refreshTokenRequest=null, grantType=null}, clientOnly=false, principal=admin, credentials=, name=admin]
[{authority=USER}, {authority=ADMIN}]
as you can see, i added 'ROLE_USER' and 'ROLE_ADMIN' Authorities in Authorization Server.
in Resource Server's Principal Object granted both 'ROLE_ADMIN' and 'ROLE_USER'.
but in Client's Principal Object doesn't granted 'ROLE_ADMIN'. there is 'ROLE_USER' Only.
and Principal.atttibutes['authorities'] has 'USER', 'ADMIN'.
as #Rahil Husain said, there is DefaultOAuth2UserService and this service grant 'ROLE_USER' only to OAuth2User Object.
first, i added CustomAuthoritiesExtractor via #Componenet annotation (#Bean too.) to Client.
but this doesn't working in my projects.
so, i implemented CustomOAuth2User and CustomOAuth2UserService.
like this.
CustomOAuth2User
public class CustomOAuth2User implements OAuth2User {
private List<GrantedAuthority> authorities;
private Map<String, Object> attributes;
private String name;
public CustomOAuth2User(List<GrantedAuthority> authorities, Map<String, Object> attributes) {
this.authorities = authorities;
this.attributes = attributes;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
#Override
public Map<String, Object> getAttributes() {
if (this.attributes == null) {
this.attributes = new HashMap<>();
this.attributes.put("name", this.getName());
}
return attributes;
}
#Override
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
and following is CustomOAuth2UserService
CustomOAuth2UserService
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
#Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
AuthoritiesExtractor authoritiesExtractor = new CustomAuthoritiesExtractor();
List<GrantedAuthority> grantedAuthorityList = authoritiesExtractor.extractAuthorities(oAuth2User.getAttributes());
CustomOAuth2User customOAuth2User = new CustomOAuth2User(grantedAuthorityList, oAuth2User.getAttributes());
customOAuth2User.setName(oAuth2User.getName());
return customOAuth2User;
}
}
and following is my CustomAuthoritiesExtractor. this class not used as #Bean or #Component. directly used in CustomOAuth2Service for mapping CustomOAuth2User object's authorities
CustomAuthoritiesExtractor
public class CustomAuthoritiesExtractor implements AuthoritiesExtractor {
#Override
public List<GrantedAuthority> extractAuthorities(Map<String, Object> map) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(asAuthorities(map));
}
private String asAuthorities(Map<String, Object> map) {
List<String> authorities = new ArrayList<>();
List<LinkedHashMap<String, String>> authz =
(List<LinkedHashMap<String, String>>) map.get("authorities");
for (LinkedHashMap<String, String> entry : authz) {
authorities.add(entry.get("authority"));
}
return String.join(",", authorities);
}
}
and final, i changed Client's endpoint to using my CustomOAuth2User and CustomOAuth2UserService.
so, i changed Client's WebSecurityConfigurerAdapter config like this.
#Configuration
#EnableOAuth2Client
public class WebSecurityConfigurerAdapterImpl extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/home", "/error", "/webjars/**", "/resources/**", "/login**").permitAll()
.anyRequest().authenticated()
.and().oauth2Login()
/** add this config**/
.userInfoEndpoint()
.customUserType(CustomOAuth2User.class, "teemo")
.userService(this.oauth2UserService());
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
return new CustomOAuth2UserService();
}
and following is my thymeleaf.
thymeleaf
<div sec:authorize="isAuthenticated()">
Text visible only to authenticated users.
Authenticated username:
<div sec:authentication="name"></div>
<div sec:authorize="hasRole('USER')">hasRole('USER')</div>
<div sec:authorize="hasRole('ROLE_USER')">hasRole('ROLE_USER')</div>
<div sec:authorize="hasRole('ADMIN')">hasRole('ADMIN')</div>
<div sec:authorize="hasRole('ROLE_ADMIN')">hasRole('ROLE_ADMIN')</div>
<!-- TRUE -->
<div sec:authorize="hasAuthority('USER')">hasAuthority('USER')</div>
<div sec:authorize="hasAuthority('ROLE_USER')">hasAuthority('ROLE_USER')</div>
<!-- TRUE -->
<div sec:authorize="hasAuthority('ADMIN')">hasAuthority('ADMIN')</div>
<div sec:authorize="hasAuthority('ROLE_ADMIN')">hasAuthority('ROLE_ADMIN')</div>
</div>
<div sec:authorize="!isAuthenticated()">Text visible only to
unauthenticated users.
</div>
and following is the result.
Text visible only to authenticated users. Authenticated username:
admin
hasAuthority('USER')
hasAuthority('ADMIN')
anyone who digging like me, i hope help this question and answers.
but i don't know this is de facto-standard way.
just.. working now.
Use #authentication Object
<div th:text="${#authentication.principal.something}">
The value of the "name" property of the authentication object should appear here.
</div>
Example:
<img th:if="${#authentication.principal.image}"
class="img-circle" th:src="${#authentication.principal.image}"
width="100" height="100" alt="place-holder" />
But Add this dependency first
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>-latest-version-here-</version>
</dependency>
As it does not come with thymeleaf-starter in spring boot
You can pass the Principal to your controller as argument, like
public String myController(Principal principal) {
...
}
You will also have to map the authorities to granted Authorities by yourself, e.g. using the AuthoritiesExtractor Interface from Spring, here is an example: Link from Baeldung

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