With OAuth 2.0, we used Facebook and GitHub to demonstrate how the user can authenticate on authorization servers to gain access to your web application.
But, what do you do when your company tells you that they want you to develop a login page that allows a user to use OAuth 2.0 for Facebook but also allows the user to register and create an account hosted by the company’s database using OAuth 2.0? Well, in this case, you can’t use Facebook or another vendor’s authorization server, so it sounds like you would need to create your own authorization server for the user to authenticate to, right?
Can we do that?
Absolutely! It requires building a resource and an authorization server for the company’s OAuth 2.0 login.
Review OAuth 2.0 workflow with Resource and Authorization servers
Let’s go back over the OAuth 2.0 workflow so you can see it from the perspective of the resource and authorization server. Since you are going to be configuring your own server, pay close attention to how the workflow is managed.
You, the user, wish to log in to your bank to check your balance. A user refers to the person logging into the app.
You use the client, a web application that connects you to your bank, to log in with your username and password.
The client sends an authorization request to your bank’s authorization server with an OpenID Connect scope. Remember that an authorization server is where the user’s identity is authenticated and authorization is set up.
Your bank sends you a pop-up asking you if you want the client to access your bank information. You click Yes. (This is just like with Facebook and Google when you authorize the app to gain access to your information).
Now the bank’s authorization server sends an authorization grant to your client app. An authorization grant is a term for the initial request for an access token from the authorization server to the client. This authorization grant uses a temporary code. Your client app sends it back to the authorization server in exchange for an access token. An access token contains your authorization data and some user data. It is used to validate that you have authorized on the authorization server. An access token, id_token, and refresh token are sent back to the client app. The id_token is encrypted with Base64 and a private key on the authorization server and sent with OpenID Connect user data from the authorization server to validate authentication on the client.
The refresh token is sent with the access token to validate the creation of a new token after the access token has expired.
Now you use the client to request your balance information. The client sends an HTTP request to the resource server with the access token. Remember that the resource server is able to access the data user’s data, not the authorization server.
The resource server validates the access token, and sends the balance information to your client so you can view it.
Let’s look at how to configure an authorization and resource server to perform all of these functions.
Configure Your Authorization Server
Since you all are pros, I will go over the libraries you need to add for an authorization server in one go. First, you want to import these libraries:
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
Next, add these methods to your authorization server class. I am calling it LoginAuthorizationServer. You can create a new class called LoginAuthorizationServer.java in a new package for your authorization server.
Add @EnableAuthorizationServer
to your authorization server’s configuration file to designate this class as the authorization server. Extend AuthorizationServerConfigurerAdapter for your LoginAuthorizationServer class to configure how your authorization server will work.
So far, you should have this:
@Configuration
@EnableAuthorizationServer
public class LoginAuthorizationServer extends AuthorizationServerConfigurerAdapter{
}
First, if your client web app credentials from the IdP’s OAuth 2.0 registration are coming via an HTTPS request, assign them to String variables called clientId and clientSecret using the @Value("${security.jwt.*}")
annotation because these values should be derived from parsing a JWT web token containing the HTTPS request.
@Configuration
@EnableAuthorizationServer
public class LoginAuthorizationServer extends AuthorizationServerConfigurerAdapter{
@Value("${security.jwt.client-id}")
private String clientId;
@Value("${security.jwt.client-secret}")
private String clientSecret;
}
When configuring the security rules for your authorization server, you need to configure rules for what to do with the JWT HTTPS request from the client web app.
First, validate the client using the credentials with the
withClient()
method.Next, you see the
authorizedGrantTypes()
method that determines what is required based on what the request and OAuth 2.0 registration information contains.The authorization_code option requires a client ID, client secret and redirect URI to grant an access token to the client web app. A redirect URI is important because it allows the client web app to validate that the token was received in a specific location.
Other options are with a password, which only requires a password and implicit, which works with an OAuth 2.0 workflow best for single-page applications (SPA).
The most secure and recommended grant type for our client web app is authorization_code. Use refresh_token if you want the client to get a refresh token when their access token expires.
The
redirect URI
is https://mybankapp/login/oauth/vault, so after the user logs in successfully, they are redirected back to the client app at this URL.As you can see, this is where your client ID and client secret go to authenticate your client web app on the authorization server. If you want to retrieve the client ID and client secret from a database, you would use
clients.jdbc()
. If you are using OIDC, setclients.scopes("openid")
.ClientDetailsServiceConfigurer is a class that holds all of the information required to create a user session. In this case, that is the clientId, client secret, scope, redirect URI, etc. In the configuration, you can designate where this session information is stored.
Here is the configuration on how the authorization server will grant tokens:
@Override public void configure(ClientDetailsServiceConfigurerclients) throwsException { clients .withClient(clientId).secret(passwordEncoder.encode(clientSecret)) .authorizedGrantTypes("authorization_code", "refresh_token") .redirectUris(“https://mybankapp/login/oauth/vault”); }
Configure your authorization server endpoints so they know what to do with the tokens. The
tokenServices()
: You need theaccessTokenConverter()
to convert your JWT encoded tokens to a character string form to be saved as a key file.
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurerendpoints) throwsException {
endpoints
.authenticationManager(this.authenticationManager)
.tokenServices(tokenServices())
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter());
}
Configure Token Management on Your Authorization Server
Lastly, you have to configure what your authorization server will do with your JWT tokens. The TokenStore class is used as a central repository for all of your tokens for retrieval in various methods:
Create an Instance of TokenStore
@Bean
public TokenStoretokenStore() {
if (tokenStore==null) {
tokenStore=newJwtTokenStore(jwtAccessTokenConverter());
}
return tokenStore;
}
The @Bean
set as @Primary
gives the following method preference. This is important because authorization and resource servers are set up with a configurable order of operations.
Using the DefaultTokenServices class, use a tokenServices() method to define your token service. This class is compatible with the authorization and resource server. Here, you can add all of the default token handling methods required to support your JWT access token and refresh token on your authorization server and resource server.
@Bean
@Primary
public DefaultTokenServicestokenServices() {
DefaultTokenServices defaultTokenServices=newDefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
The tokenServices()
calls tokenStore()
to create a new JWT access token.
@Bean
public TokenStoretokenStore() {
return newJwtTokenStore(accessTokenConverter());
}
The accessTokenConverter()
method class is called to encrypt your access token. If it is called to verify your access token with a symmetric key, then it will decrypt the token.
@Bean
public JwtAccessTokenConverteraccessTokenConverter() {
JwtAccessTokenConverter converter=newJwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
Set your signing key in the @Value variable
signingKey
here:
@Value("${security.jwt.signingKey}")
private String signingKey;
Here is all of the code for your authorization server:
@Configuration
@EnableAuthorizationServer
public class LoginAuthorizationServer extends AuthorizationServerConfigurerAdapter
{
@Value("${security.jwt.client-id}")
private String clientId;
@Value("${security.jwt.client-secret}")
private String clientSecret;
@Value("${security.jwt.signingKey}")
private String signingKey;
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(this.authenticationManager)
.tokenServices(tokenServices())
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter());
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.withClient(clientId).secret(passwordEncoder.encode(clientSecret))
.authorizedGrantTypes("authorization_code", "refresh_token")
.redirectUris(“https://bankapp.com/login”);
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(signingKey);
return converter;
}
@Bean
public TokenStore tokenStore() {
if (tokenStore == null) {
tokenStore = new JwtTokenStore(jwtAccessTokenConverter());
}
return tokenStore;
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
}
Configure Your Resource Server
Now you need to configure your resource server, which is used to verify the access token and validate the client's authentication with the access token. It also handles secure client data that the authorization server does not have access too. You see, the main difference between the authorization and resource server is that:
The authorization server handles the authorization and token handling.
The resource server is like an API server that validates the access token when it comes from the client. After validation, it also has access to secure resources like requested client data.
Here’s an example:
You click on a button to look at your account balance from your client app. Your client app sends your access token to the resource server to validate it. Once validated, it retrieves the account balance and sends it back to the client app.
The resource server receives an HTTP request from the client application with the access token.
Now create a new class called OAuth2ResourceServer.java in a package for your resource server. This way, you can have your authorization server and resource server in the same application. Whichever way you decide to do it depends on your style. It does not affect the functionality of your servers.
Next add @EnableResourceServer
to designate this configuration file as a resource server.
Extend OAuth2ResourceServer with the ResourceServerConfigurerAdapter, which is used to identify this class to hold the ResourceServerSecurityConfigurer class methods:
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
}
First, implement the ResourceServerSecurityConfigurer class used to implement the security filter chain for your resource server. It includes various methods that can be used to pull in your resource server specific properties. These methods can be found here.
The ResourceServerSecurityConfigurer pulls in the JWT access token from an HTTP request that has to be validated using the configure()
method. In this case, the method, tokenServices()
sets off the tokenStore()
method to create a new repository for your access token.
@Override public void configure(ResourceServerSecurityConfigurerconfig){ config.tokenServices(tokenServices()); }
Let's add the HttpSecurity class to create an endpoint so your resource server can check and verify that the tokens are authenticated and authorized.
This is how your resource server should look:
@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(tokenServices());
}
@Override
public void configure(HttpSecurity http) throws Exception
{
http.tokenKeyAccess("isAnonymous()|| hasRole('USER')")
.checkTokenAccess("hasRole('USER')");
}
}
This is just a small example of how to set up these servers. It is really meant to help you understand some of the classes and methods associated with the OAuth 2.0 workflow on the server-side. Depending on your coding style, there are some configurations that will work better for you. For more information on how to set up your authorization and resource server, check out this documentation.
Optional Research
There are many ways you can configure your authorization server and resource server, depending on your coding style and requirements. Two typical configurations include adding OpenID Connect ID Token handling and validating client credentials from a JDBC database.
If you need to add either of those options to your server, you will get some helpful tips in this section.
Configure Your Authorization Server to Use OpenID Connect
Lastly, let’s talk about the endpoints in the security filter chain. The endpoints are the locations where tokens are sent. It is at these endpoints that the default authorization server methods access tokens like the access, ID token, and refresh token to create an OAuth 2.0 workflow.
The TokenEnhancer class goes beyond default token handling when working with OpenID Connect’s ID token. The extra claims from OIDC are added to this class.
A new access token is created as an instance of the TokenEnhancerChain class.
The token is encrypted using the
accessTokenConverter()
method.The designated endpoints access the tokenStore for the token.
It encrypts the token with
accessTokenConverter()
.The tokenEnhancer class adds the custom claims from the OIDC scope.
The authenticationManager validates the information.
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Collections.singletonList(accessTokenConverter));
endpoints.tokenStore(tokenStore)
.accessTokenConverter(accessTokenConverter)
.tokenEnhancer(enhancerChain)
.authenticationManager(authenticationManager);
}
Add the TokenEnhancer class for the custom configuration for additional claims that are added to the access token. In our case, add two claims to the OIDC scope:
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
Create a TokenEnhancer class for the custom claims that you added with OIDC:
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(
OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put(
"organization", authentication.getName() + randomAlphabetic(4));
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(
additionalInfo);
return accessToken;
}
}
Configure Your Authorization Server to Use a Database (Optional)
You have a few options when adding a database for your user credentials. Not only is Spring Boot set up to use Java Database Connectivity (JDBC) through an API with a database of your choosing, it has support to connect with cloud databases like Google Cloud.
In this example, I provide some sample code to set up a simple JDBC connection to an SQL-based database like MongoDB.
The setup for the authorization server is similar with a few differences below. Start by adding the spring-jdbc dependency to your Spring Boot web application.
The code shows credentials saved in memory instead of in a database. If you are assigned to use a database for login, Spring Security and OAuth 2.0 uses the ClientDetailsServiceConfigurer class to designate its configuration for JDBC using the jdbc()
method and the DataStore class.
@Override
public void configure(finalClientDetailsServiceConfigurerclients) throwsException {
clients.jdbc(dataSource());
}
Spring Boot makes things easier by allowing you to use the DataSourceBuilder class to build a standard data store.
@Bean
@ConfigurationProperties("app.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
You also have to configure your database with attributes for all session values using the DataSource class. The JDBC API requires a driver to create a connection from your Spring Boot web application to the database. Here is some sample code on setting up an instance of that driver called dataSource()
, and setting its properties.
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.user"));
dataSource.setPassword(env.getProperty("jdbc.pass"));
return dataSource;
}
Below is a sample structure for the attributes for all session information in SQL. You can connect these attributes with the ClientDetailsServiceConfigurer configuration class that was set up in the authorization server. As you may remember, this method configures the user session information.
create table oauth_client_details (
client_id VARCHAR(256) PRIMARY KEY,
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
autoapprove VARCHAR(256)
);
Setting up your authorization and resource server can be a daunting task. In the course, we set up a client for OAuth 2.0 and OIDC. This is a reference with an explanation, including significant sections of code to research before you have to set up both ends: the client and the authorization/resource server.
There are a few things to remember to secure your authorization/resource server:
Work with an API request tool like Postman to look at what is coming in and out with the HTTP requests. This allows you to visualize how everything works and make sure it's doing what you expect.
When you set up your OAuth 2.0 client using localhost, you won't be able to validate the receipt of your tokens without setting up your Auth server with an IP address. Both cannot work on localhost at once.
Setting up your authorization server using secure programming is especially important.
Ensure that all your transmission to/from your client are HTTPS.
Secure your authorization grant, access, ID, and refresh token with HTTPS transmission, encryption, and a secure endpoint delivery.
It's time to wrap it up now.
We started with an introduction to Spring Security, and its default login page. Then moved on to secure that login with OAuth 2.0 and added OIDC. You learned how to create test methods based on Spring Security and customize Spring Security error pages. You saw different ways to customize Spring Security's default protections against CSRF and CORS attacks as well as session hijacking. Lastly, you learned how to set up an authorization and resource server for a custom OAuth 2.0 client/server setup!
Now take this last quiz and we will call it a wrap!