In this post, I will show how we can build user management and authentication with Okta SDK and Spring Boot.
Introduction
As part of any application, developers have to be careful how they build authentication. Despite we are using Form-Based authentication for a long time, it is not the most secure one. In this post, I plan to show Form-Based authentication where users are not necessarily authenticated by validating their encrypted password against the password stored in a database. If you want to learn more about Spring Security with different authentication flows, I recently released a book Simplifying Spring Security. You can buy the book here.
Okta is an identity provider. It’s an application that provides user management and authentication with different protocols.
Okta SDK APIs
Okta offers two libraries okta-sdk-java
and okta-auth-java
for user management APIs and authentication.
Are these libraries right for you? This depends on your use case. Okta also offers okta-spring-boot-starter
library to use okta for different OAuth flows in your Spring Boot Application. We will not be using this library in this demo.
You can find more details about these libraries here and here.
Include these libraries in your project as follows:
implementation 'com.okta.authn.sdk:okta-authn-sdk-api:2.0.1'
runtimeOnly 'com.okta.authn.sdk:okta-authn-sdk-impl:2.0.1'
runtimeOnly 'com.okta.sdk:okta-sdk-httpclient:3.0.1'
User Management with Okta SDK in Spring Boot Application
In this demo, I have a sample application of To-Do List. When a user launches the application, user will see a login screen. It has Sign up option. If the user doesn’t exist in the application, the user will have to create an account.
On Sign-up page, when a user enters the “Submit” button, we will save the user in our database and then call Okta SDK API to create the user on Okta side.
To achieve this, we need Okta Client.
@Bean
public Client client()
{
Client clientConfig =
Clients.builder().setOrgUrl("https://oktadomainurl").setClientCredentials(new TokenClientCredentials(secret))
.build();
return clientConfig;
}
As you see above, we are creating a client that we will use to call Okta API. The `secret` is the API token you will be able to find in Okta admin UI. If you don’t find it, either you don’t have admin privileges or you have not created the token yet. There is another way to create this client with an access token.
@Bean
public Client client()
{
Client clientConfig =
Clients.builder().setOrgUrl("https://oktadomainurl")
.setAuthorizationMode(AuthorizationMode.PRIVATE_KEY).setClientId("{clientId}")
.setScopes(new HashSet<>(Arrays.asList("okta.users.read", "okta.apps.read")))
.setPrivateKey("/path/to/yourPrivateKey.pem")
return clientConfig;
}
The advantage of this Client configuration is that you don’t need to know API access token created based on admin privileges.
Now on my Controller side, I will use this client to create user in Okta as below:
UserDto userDto = new UserDto();
userDto.setEmail(email);
userDto.setFirstName(firstname);
userDto.setLastName(lastname);
userDto.setPassword(encodedPassword);
userDto.setRole("ADMIN");
userDto.setEnabled(true);
UserDto returnedUser = usersManager.createUser(userDto);
LOGGER.info("Create the user in Okta");
User oktaUser = UserBuilder.instance().setEmail(returnedUser.getEmail())
.setFirstName(returnedUser.getFirstName())
.setLastName(returnedUser.getLastName())
.buildAndCreate(client);
That covers the user management part. You can similarly call GET
or DELETE
API to manage users.
User Authentication
Now comes the critical part of authentication. In many enterprise applications, when using third-party identity provides, the trouble always comes with user data synchronization. Both applications need to store user data.
For authentication, we will need authenticationClient
bean. This client will allow us to call Okta API for authentication.
@Bean
public AuthenticationClient authenticationClient()
{
AuthenticationClient authenticationClient =
AuthenticationClients.builder()
.setOrgUrl("https://oktadomainurl")
.build();
return authenticationClient;
}
In our security config, I will override the form-based login with a custom login page.
@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity.authorizeRequests()
.antMatchers("/js/**","/css/**","/img/**").permitAll()
.antMatchers("/signup","/forgotpassword").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
}
As you see in above code, I am using customAuthenticationProvider
, this provider will use authenticationClient
to authentcate with Okta. This AuthenticationProvider will look like below:
package com.betterjavacode.sss.todolist.clients;
import com.betterjavacode.sss.todolist.security.AuthenticationStateHandler;
import com.okta.authn.sdk.client.AuthenticationClient;
import com.okta.authn.sdk.resource.AuthenticationResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider
{
private static final Logger LOGGER = LoggerFactory.getLogger(CustomAuthenticationProvider.class);
@Autowired
private AuthenticationClient authenticationClient;
@Autowired
private AuthenticationStateHandler authenticationStateHandler;
@Override
public Authentication authenticate (Authentication authentication) throws AuthenticationException
{
String username = authentication.getName();
String password = authentication.getCredentials().toString();
String relayState = "/index";
AuthenticationResponse authnResponse = null;
try
{
LOGGER.info("Going to connect to Okta");
authnResponse = authenticationClient.authenticate(username, password.toCharArray(),
relayState,
authenticationStateHandler);
}
catch(com.okta.authn.sdk.AuthenticationException e)
{
LOGGER.error("Unable to authentcate the user", e);
}
if(authnResponse != null)
{
final List grantedAuths = new ArrayList<>();
grantedAuths.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
final UserDetails principal = new User(username, password, grantedAuths);
final Authentication authen = new UsernamePasswordAuthenticationToken(principal,
password, grantedAuths);
return authen;
}
else
{
LOGGER.info("Unable to authenticate");
return null;
}
}
@Override
public boolean supports (Class<?> authentication)
{
return true;
}
}
We use authenticationClient
to call authenticate method. AuthenticationStateHandler
basically handles the status authentication. The implementation of this handle is as below:
package com.betterjavacode.sss.todolist.security;
import com.okta.authn.sdk.AuthenticationStateHandlerAdapter;
import com.okta.authn.sdk.resource.AuthenticationResponse;
import com.okta.commons.lang.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class AuthenticationStateHandler extends AuthenticationStateHandlerAdapter
{
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationStateHandler.class);
@Override
public void handleUnknown (AuthenticationResponse unknownResponse)
{
// TO DO
}
@Override
public void handleSuccess (AuthenticationResponse successResponse)
{
if (Strings.hasLength(successResponse.getSessionToken()))
{
LOGGER.info("Login successful");
String relayState = successResponse.getRelayState();
String dest = relayState != null ? relayState : "/";
}
}
}
That’s all. This covers user authentication. Remember this is still form based authentication where you are entering user credentials on your custom login page and behind the screen calling Okta API to authenticate.
In my book, Simplifying Spring Security, I have also added the demo for login with Okta OAuth.
Conclusion
In this post, I showed how to use Okta SDK for user management and authentication with the Spring Boot application. If you have any questions, feel free to send me an email by subscribing to my blog here.