Spring Boot OAuht2 Reason for /oauth2/authorization/{id}
Do you know the reason why redirect URL should be http://localhost:8080/login/oauth2/code/google when we setup Authorization Server in the …
Let’s implement oauth2 practical implementation with Spring Boot using PostgreSQL and Thymeleaf. In this application:
At the end our clients will be able to login our web application both Basic Authentication (default security layer implemented by Spring Security) and OAuth2 (using Google Authorization)
There are three urls in our web application:
public interface Urls {
String INDEX = "/";
String LOGGED_IN_PAGE = "/home";
String REGISTER_PAGE = "/register";
}
/
=> If user(s) is already logged-in, we will show user’s profile page, otherwise we will show login page./register
=> We will show register form to store user via traditional way.In the login page, there will be also sign in with Google button:
Here is the register page:
Here are the required dependencies for this project:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Quick note: You should also need to change the following properties:
spring.datasource.url=jdbc:postgresql://localhost:5432/test spring.datasource.username=postgres spring.datasource.password=1234
We have only one controller:
@Controller
public class OAuth2Controller {
private static final Logger logger = LoggerFactory.getLogger(OAuth2Controller.class);
@GetMapping(value = Urls.INDEX)
public String getIndexPage() {
if (CheckUserAuthentication.isUserAuthenticated()) {
return "logged_in_users";
}
return "not_logged_in_page";
}
@GetMapping(value = Urls.LOGGED_IN_PAGE)
public String loggedInUserPage(Model model) {
SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication currentUser = securityContext.getAuthentication();
SecureUser loggedInUser = (SecureUser) currentUser.getPrincipal();
logger.info("Logged-in user: {}", loggedInUser);
return "logged_in_users";
}
@GetMapping(value = Urls.REGISTER_PAGE)
public String openLoginPage() {
if (CheckUserAuthentication.isUserAuthenticated()) {
return "logged_in_users";
}
return "register_page";
}
}
Also we are going to store user information in the table called users
:
@Table(name = "users")
@Entity
public class UserDTO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Enumerated(EnumType.STRING)
@Column(name = "provider")
private Provider provider;
@Column(name = "email")
private String email;
@Column(name = "role")
private String role;
}
If you only need to see the code, here is the github link
We will first implement OAuth2, then we are going to update with Basic Authentication. Then, let’s start with oAuth2:
If you recall my previous blogs about Spring Security, I already drew the big picture of the spring security. Basically we need to do three things:
But for OAuth2, we don’t need to define passwordEncoder.
Let’s create configuration class called OAuth2Configuration
:
@Configuration
public class OAuth2Configuration {
private static final String CLIENT_SECRET = "your_client_secreut";
private static final String CLIENT_ID = "your_client_id";
// ...
@Bean
public ClientRegistrationRepository clientRepository() {
ClientRegistration google = googleClientRegistration();
return new InMemoryClientRegistrationRepository(google);
}
private ClientRegistration googleClientRegistration() {
return CommonOAuth2Provider.GOOGLE
.getBuilder("google")
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.build();
}
}
With this configuration, we are basically saying that: “Hey Spring boot if you get any request with this url: /oauth2/authorization/google
, redirects that user to the Google Sign-In Page with the client secret and client id (provided by google api console)”
While we were implementing basic authentication, Spring Security forced us to create object with type UserDetails
. Same thing will be applied for the OAuth2 login.
Spring Security forces us to return object with type of OAuth2User
if we are going to login with oauth2.
Spring Security forces us to return object with type of OidcUser
(which extends OAuth2User
) if we are going to login with oauth2 with OpenID Connect 1.0.
What is OpenID Connect?
OpenID Connect 1.0 is a simple identity layer on top of the OAuth 2.0 protocol.
At the end OAuth2UserService
should return OAuth2User
or OidcUser
.
OAuth2 UserService will apply the followings:
Because Google supports OpenID 1.0 identity layer. Our method should implement OAuth2UserService
with also support openID as well. Spring Security provides a class called OidcUserService
for openId connect 1.0 provider’s.
Here is the class for openId (we are going to override loadUser
method):
// An implementation of an {OAuth2UserService} that supports OpenID Connect 1.0 Provider's.
public class OidcUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
}
}
Here is the our oidcServer (every time user allows to login with Google, loadUser()
method will be called):
@Service
public class MyOidcUserService extends OidcUserService {
private static final Logger logger = LoggerFactory.getLogger(MyOidcService.class);
@Autowired
private UserService userService;
// Our OAuth2UserService will apply:
/**
* Get the information from the google, such as gmail adress (+)
*
* Check whether gmail address is in the database (+)
* If user isn't defined in the database:
* Create a new record for that user
* Save the new user to the database
* Return appropriate object
* If user is already defined in the database:
* Return appropriate object
*/
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
OidcUser oidcUser = super.loadUser(userRequest);
try {
return userService.findUser(userRequest, oidcUser);
} catch (Exception ex) {
logger.error("Error", ex);
throw new OAuth2AuthenticationException(ex.getMessage());
}
}
}
In this line: OidcUser oidcUser = super.loadUser(userRequest);
, we are returning OAuth2User
after obtaining the user attributes from the provider(s) (attributes can be gmail address etc..)
Then, userService.findUser(userRequest, oidcUser)
, we will check this user is our db or not, then we will return object which implements OidcUser
.
Before diving into userService
, let’s first register MyOidcServer
in the security configuration.
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private static final String CLIENT_SECRET = "your_client_secreut";
private static final String CLIENT_ID = "your_client_id";
@Autowired
private MyOidcService myOidcService;
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.oauth2Login()
.defaultSuccessUrl(Urls.LOGGED_IN_PAGE, true)
.userInfoEndpoint().oidcUserService(myOidcService);
}
}
userInfoEndpoint()
returns configurer to register our oidcService.
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
@Autowired
private UserRepository userRepository;
// ...
@Transactional
public SecureUser findUser(OidcUserRequest userRequest, OidcUser oidcUser) {
GoogleOAuth2Request googleOAuth2Request = new GoogleOAuth2Request(userRequest.getClientRegistration().getRegistrationId(), oidcUser.getAttributes());
Optional<UserDTO> userInDb = findByUsername(googleOAuth2Request.getEmail());
if (userInDb.isEmpty()) {
logger.info("New user from the google with email: {}", googleOAuth2Request.getEmail());
UserDTO userDTO = createNewUser(googleOAuth2Request.getEmail(), Provider.GOOGLE);
saveNewUser(userDTO);
return SecureUser.createUser(oidcUser, userDTO);
} else {
return SecureUser.createUser(oidcUser, userInDb.get());
}
}
}
We have two providers:
public enum Provider {
LOCAL("local"), GOOGLE("google");
// ...
}
GoogleOAuth2Request
is a kind of utility method to obtain email from oauth2 provider:
public class GoogleOAuth2Request {
private String registrationId;
private Map<String, Object> attributes; // attributes from Google AuthServer
public GoogleOAuth2Request(String registrationId, Map<String, Object> attributes) {
this.registrationId = registrationId;
this.attributes = attributes;
}
public String getId() {
return (String) attributes.get("sub");
}
public String getName() {
return (String) attributes.get("name");
}
public String getEmail() {
// returns gmail
return (String) attributes.get("email");
}
public String getImageUrl() {
return (String) attributes.get("picture");
}
}
SecureUser
is an implementation of OidcUser
:
public interface OidcUser extends OAuth2User, IdTokenClaimAccessor {
Map<String, Object> getClaims();
OidcUserInfo getUserInfo();
OidcIdToken getIdToken();
}
public class SecureUser implements OidcUser {
// ...
public static SecureUser createUser(OidcUser oidcUser, UserDTO userDTO) {
SecureUser myOidcUser = new SecureUser();
myOidcUser.setClaims(oidcUser.getClaims());
myOidcUser.setUserInfo(oidcUser.getUserInfo());
myOidcUser.setIdToken(oidcUser.getIdToken());
myOidcUser.setAttributes(oidcUser.getAttributes());
SimpleGrantedAuthority readAuthority = new SimpleGrantedAuthority(userDTO.getRole());
myOidcUser.setAuthorities(Collections.singleton(readAuthority));
myOidcUser.setName(userDTO.getEmail());
return myOidcUser;
}
// ...
}
We must also check the Provider, if User is already defined in the database. Because same username(same emails) may also try to login with Google and Local
Now if we click Login with Google button, we will be redirected to the Google Sign in Page, after all we will be redirected to the logged_in page in our spring application:
Our OAuth2 implementation is fully functional. Let’s add form based authentication.
Here are the steps to implement form based authentication: (I am not going into detail why we need a new UserDetailsService etc.. You can find the reason from my previous blogs)
But before applying these steps, we first need to add password field in the table and the entity.
First add new column called password:
ALTER TABLE users ADD COLUMN password varchar(100);
Second update the user entity:
@Table(name = "users")
@Entity
public class UserDTO {
// ...
// new field
@Column(name = "password")
private String password;
}
We should create another service but in this time, this service must implement UserDetailsService
:
public class MyLocalUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserDTO> userInDb = userService.findByUsername(username);
if (userInDb.isEmpty()) {
throw new UsernameNotFoundException("User with this username: " + username + " not found");
}
return SecureUser.createFromBasicAuthentication(userInDb.get());
}
}
Because we should return UserDetails
, then our SecureUser
object must also implement UserDetails
interface
public class SecureUser implements OidcUser, UserDetails {
private Map<String, Object> claims;
private OidcUserInfo userInfo;
private OidcIdToken idToken;
private Map<String, Object> attributes;
private Collection<SimpleGrantedAuthority> authorities;
private String name;
// Common for OAuth2 and BasicAuthentication
private String provider;
// Fields for UserDetails
private String password;
private String username;
public static SecureUser createFromBasicAuthentication(UserDTO userDTO) {
SecureUser basicAuth = new SecureUser();
basicAuth.setProvider(Provider.LOCAL.name);
basicAuth.setUsername(userDTO.getEmail());
basicAuth.setPassword(userDTO.getPassword());
return basicAuth;
}
// ...
}
We should create a bean from our custom user details service. Then Spring Boot can be able to use our implementation instead of the default one:
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ...
@Bean
public UserDetailsService userDetailsService() {
return new MyLocalUserDetailsService();
}
}
Because we are implementing custom user details service. We must also create password encoder as well.
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ...
@Bean
public UserDetailsService userDetailsService() {
return new MyLocalUserDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
// permit specific urls, (js, css, etc...)
http.authorizeRequests().antMatchers(permittedUrls()).permitAll();
// anything than the permitted urls must be protected
http.authorizeRequests().anyRequest().authenticated();
// configure logout functionality,
// after logout has occurred, redirects user to the "/"
http.logout(logout -> {
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
logout.logoutSuccessUrl(Urls.INDEX);
logout.deleteCookies("JSESSIONID");
logout.invalidateHttpSession(true);
logout.clearAuthentication(true);
});
// oauth2 configuration,
// after successful login, redirects user to the "/home"
http.oauth2Login()
.defaultSuccessUrl(Urls.LOGGED_IN_PAGE, true)
.userInfoEndpoint().oidcUserService(myOidcService);
// Setup form login authentication
http.formLogin().loginPage(Urls.INDEX).successForwardUrl(Urls.LOGGED_IN_PAGE);
}
}
I have created new user from register form. If I run select query for users
table:
select * from users;
-[ RECORD 1 ]----------------------------------------------------------
id | 1
email | mehmetozanguven@gmail.com
provider | GOOGLE
role | READ
password |
-[ RECORD 2 ]----------------------------------------------------------
id | 2
email | ozan@ozan.com
provider | LOCAL
role | READ
password | $2a$10$A9rodo18nsrI.gX5D7O1L.JOi4/s48.THHC3K3vm1YlG59hDV19NW
As you can see we have two records one with google and another with local(with the encoded password way)
Finally here is the /home
page via local provider:
In this blog, we implemented both OAuth2 and form based login in the same application. We implemented both authentication processes step by step. Both authentication processes force us to return specific type of user. In OAuth2, Spring security forces us to return user object with type of OidcUser. In form based authentication, Spring Security forces us to return user object with UserDetails.
For OAuth2:
OidcUserService
base class for that).userInfoEndpoint().oidcUserService(myOidcService);
For Login form based authentication:
UserDetailsService
)Wait for the next blog …
Do you know the reason why redirect URL should be http://localhost:8080/login/oauth2/code/google when we setup Authorization Server in the …
In this article, we will implement a basic single sign on application using Spring boot. We will use Thymeleaf for html pages A single sign-on (SSO) …