ReLive27
Posted on February 14, 2023
Overview
OAuth 2.0 is the industry standard authorization protocol. OAuth 2.0 focuses on simplicity for client developers, while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices.
The OAuth authorization server is responsible for authenticating users and issuing access tokens containing user data and appropriate access policies.
Below we will use Spring Authorization Server to build a simple authorization server.
💡 Note: If you don’t want to read till the end, you can view the source code here.Don’t forget to give a star to the project if you like it!
OAuth2 authorization server implementation
Let's start with the OAuth2 authorization server configuration implementation.
Maven dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.3.1</version>
</dependency>
Configuration
First let's configure the database connection information through application.yml
.
spring:
application:
name: auth-server
datasource:
druid:
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/integrated_oauth?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: <<username>> # modify username
password: <<password>> # change Password
Then we create an AuthorizationServerConfig
configuration class, in this class we will create the specific beans required by the OAuth2 authorization server. The first one will be the client service repository, where we create a client using the RegisteredClient builder type and persist it to the database.
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("relive-client")
.clientSecret("{noop}relive-client")
.clientAuthenticationMethods(s -> {
s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
})
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-client-authorization-code")
.scope("message.read")
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.requireProofKey(false)
.build())
.tokenSettings(TokenSettings.builder()
.idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
.accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
.refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
.reuseRefreshTokens(true)
.build())
.build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
return registeredClientRepository;
}
The properties we configure are:
- id--RegisteredClient unique id
- clientId--client identifier
- clientSecret--client secret
- clientAuthenticationMethods--the authentication method the client may use. Supported values are
client_secret_basic
,client_secret_post
,private_key_jwt
,client_secret_jwt
, andnone
- authorizationGrantTypes--the types of grants the client can use. Supported values are
authorization_code
,client_credentials
andrefresh_token
- redirectUris--client has registered redirect URI
- scopes--The ranges that clients are allowed to request
clientSettings--client Custom Settings
tokenSettings--custom settings for OAuth2 tokens issued to clients
Next let's configure the central component OAuth2AuthorizationService that stores new authorizations and queries existing ones.
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
For the authorization "consent" of an OAuth2 authorization request, Spring provides OAuth2AuthorizationConsentService components for storing new authorization consents and querying existing authorization consents.
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
Next let's create a bean, configure the OAuth2 authorization service with other default configurations, and use it to redirect the request to the login page for unauthenticated authorization requests.
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.exceptionHandling(exceptions -> exceptions.
authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))).build();
}
Every authorization server needs a signing key for tokens, let's generate an RSA key:
final class KeyGeneratorUtils {
private KeyGeneratorUtils() {
}
static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
}
public final class Jwks {
private Jwks() {
}
public static RSAKey generateRsa() {
KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
return new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
}
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
RSAKey rsaKey = Jwks.generateRsa();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
After processing the signing key of the token, the authorization server also needs an issuer URL, which we can create through ProviderSettings:
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer("http://127.0.0.1:8080")
.build();
}
Finally we will enable the Spring Security security configuration class to secure our service.
@EnableWebSecurity
@Configuration
public class DefaultSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin(withDefaults())
return http.build();
}
//...
}
Here authorizeRequests.anyRequest().authenticated() makes all requests require authentication and provides Form-based authentication.
We also need to define the user information used by the test, the following creates a memory-based user information repository.
@Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
Resource server implementation
Now we will create a resource server, the API interface in the service will only allow requests authenticated by the OAuth2 authorization server.
Maven dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>2.6.7</version>
</dependency>
Configuration
First let's configure the service port via application.yml.
server:
port: 8090
Next, for OAuth2 security configuration, we need to use the issuerUri set by the previous authorization server in ProviderSettings.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://127.0.0.1:8080
The resource server will use this Uri to further configure itself, discover the public key of the authorization server, and pass in the JwtDecoder used to verify the JWT. A consequence of this process is that the authorization server must start and receive requests for the resource server to start successfully.
If the resource server must be able to start independently of the authorization server, then jwk-set-uri
can be provided. This will be our further property to add in the OAuth2 security configuration:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://127.0.0.1:8080
jwk-set-uri: http://127.0.0.1:8080/oauth2/jwks
Now that we can set up the Spring Security security configuration, every request to the service resource should be authorized and have the appropriate permissions:
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/resource/test/**")
.and()
.authorizeRequests()
.mvcMatchers("/resource/test/**")
.access("hasAuthority('SCOPE_message.read')")
.and()
.oauth2ResourceServer()
.jwt();
return http.build();
}
}
Finally, we will create a REST controller that will return the jwt claims information.
@RestController
public class ResourceServerTestController {
@GetMapping("/resource/test")
public Map<String, Object> getArticles(@AuthenticationPrincipal Jwt jwt) {
return jwt.getClaims();
}
}
OAuth2 client
Now we want to create a client, which first requests authorization from the authorization server to obtain an access token, and then accesses the corresponding resource on the resource server.
Maven dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.3.9</version>
</dependency>
Configuration
First of all, we will configure the client's access port 8070 in application.yml
.
server:
port: 8070
Next we'll define the configuration properties for the OAuth2 client:
spring:
security:
oauth2:
client:
registration:
messaging-client-authorization-code:
provider: client-provider
client-id: relive-client
client-secret: relive-client
authorization-grant-type: authorization_code
redirect-uri: "http://127.0.0.1:8070/login/oauth2/code/{registrationId}"
scope: message.read
client-name: messaging-client-authorization-code
provider:
client-provider:
authorization-uri: http://127.0.0.1:8080/oauth2/authorize
token-uri: http://127.0.0.1:8080/oauth2/token
Now let's create a WebClient instance to perform HTTP requests to the resource server:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
WebClient
adds an OAuth2 authorization filter, which requires OAuth2AuthorizedClientManager
as a dependency. Only the authorization code and refresh token are configured here, and other modes can be added if necessary:
@Bean
OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
.builder()
.authorizationCode()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
Finally, we'll configure the Spring Security security configuration:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
//Easy to test, open permissions
authorizeRequests.anyRequest().permitAll()
)
.oauth2Client(withDefaults());
return http.build();
}
Here we release all client API permissions, but in actual situations, client services require authentication. The OAuth2 protocol itself is an authorization protocol and does not care about the specific form of authentication. You can add simple forms authentication.
Access resource list
Finally, we create a controller where we will use the previously configured WebClient to make HTTP requests to our resource server:
@RestController
public class ClientTestController {
@Autowired
private WebClient webClient;
@GetMapping(value = "/client/test")
public Map<String, Object> getArticles(@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code") OAuth2AuthorizedClient authorizedClient) {
return this.webClient
.get()
.uri("http://127.0.0.1:8090/resource/test")
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(Map.class)
.block();
}
}
In the above example, we use the @RegisteredOAuth2AuthorizedClient annotation to bind OAuth2AuthorizedClient, and trigger the OAuth2 authorization code mode process to obtain an access token.
Conclusion
This example mainly demonstrates the secure communication between two services using the OAuth2 protocol, especially in complex Internet scenarios, where client services and resource services are provided by different platforms. OAuth2 is very good at obtaining the user's entrusted decision, In many ways, it is simpler and safer than other solutions.
The source code used in this article is available on GitHub.
You might want to read on to the next one:
- Customize the OAuth2 Authorization Consent Page
- Spring Security Persistent OAuth2 Client
- Spring Security OAuth2 Client Credentials Grant
- Authorization Code Flow with Proof Key for Code Exchange (PKCE)
- Spring Security OAuth2 Login
Thanks for reading!
Posted on February 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.