Building an OAuth2 Authentication & Authorization Server with Spring Security from Scratch
This tutorial walks through why OAuth2 is needed, its advantages, the project layout, Maven dependencies, YAML configurations, Java security and JWT setup, resource‑server integration, testing steps, and a side‑by‑side comparison of the four OAuth2 grant types, all with complete code examples.
After finishing a microservices series, readers asked for a clear guide on authentication and authorization. This article builds a unified auth server using Spring Security and OAuth2, explains the problems with traditional Session authentication, and highlights the benefits of a stateless, cross‑domain, extensible JWT‑based solution.
Project Structure
spring-security-oauth2-ep01/
├── auth-server/ # authentication & authorization center
│ ├── pom.xml
│ └── src/main/java/com/teaching/auth/
│ ├── AuthServerApplication.java
│ ├── config/
│ │ ├── AuthorizationServerConfig.java
│ │ ├── SecurityConfig.java
│ │ └── JwtConfig.java
│ └── service/
│ └── UserDetailsServiceImpl.java
├── resource-server/ # example resource service (order API)
│ ├── pom.xml
│ └── src/main/java/com/teaching/resource/
│ ├── ResourceServerApplication.java
│ ├── config/ResourceServerConfig.java
│ └── controller/OrderController.java
├── scripts/generate-ep01.sh
└── README.mdAuth Server Dependencies
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
</parent>
<groupId>com.teaching</groupId>
<artifactId>auth-server</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OAuth2 Authorization Server (Spring’s deprecated core, now a separate module) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.3.0</version>
</dependency>
<!-- JWT support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- Optional persistence for client details -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>Auth Server YAML Configuration
server:
port: 8080
spring:
application:
name: auth-server
datasource:
url: jdbc:mysql://localhost:3306/oauth2_db
username: root
password: root123
jpa:
hibernate:
ddl-auto: update
show-sql: true
logging:
level:
org.springframework.security: DEBUGSecurity Configuration (Java)
package com.teaching.auth.config;
import com.teaching.auth.service.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/.well-known/jwks.json").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll())
.csrf(csrf -> csrf.disable());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}JWT Configuration
package com.teaching.auth.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration
public class JwtConfig {
@Bean
public KeyPair keyPair() {
try {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
return generator.generateKeyPair();
} catch (Exception e) {
throw new RuntimeException("Failed to generate key pair", e);
}
}
@Bean
public RSAKey rsaKey(KeyPair keyPair) {
return new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
.privateKey((RSAPrivateKey) keyPair.getPrivate())
.keyID(UUID.randomUUID().toString())
.build();
}
@Bean
public JWKSource<SecurityContext> jwkSource(RSAKey rsaKey) {
return new ImmutableJWKSet<>(new JWKSet(rsaKey));
}
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
}
@Bean
public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
}Authorization Server Core Configuration
package com.teaching.auth.config;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import java.time.Duration;
import java.util.UUID;
@Configuration
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
http.oauth2ResourceServer(resource -> resource.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("teaching-client")
.clientSecret("{noop}teaching-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.redirectUri("http://localhost:8081/login/oauth2/code/teaching")
.redirectUri("http://localhost:8080/authorized")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
return new InMemoryRegisteredClientRepository(publicClient);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder()
.issuer("http://localhost:8080")
.build();
}
}UserDetailsService Implementation
package com.teaching.auth.service;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final PasswordEncoder passwordEncoder;
public UserDetailsServiceImpl(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// Simulated test users; real projects should query a database
if ("user".equals(username)) {
return User.builder()
.username("user")
.password(passwordEncoder.encode("123456"))
.roles("USER")
.build();
}
if ("admin".equals(username)) {
return User.builder()
.username("admin")
.password(passwordEncoder.encode("admin123"))
.roles("ADMIN", "USER")
.build();
}
throw new UsernameNotFoundException("User not found: " + username);
}
}Auth Server Main Class
package com.teaching.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
System.out.println("✅ Auth server started!");
System.out.println(" http://localhost:8080");
}
}Resource Server Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>Resource Server YAML
server:
port: 8081
spring:
application:
name: resource-server
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080
jwk-set-uri: http://localhost:8080/oauth2/jwks
logging:
level:
org.springframework.security: DEBUGResource Server Security Configuration
package com.teaching.resource.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
}Resource Server Controller
package com.teaching.resource.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/order")
public class OrderController {
@GetMapping("/public/info")
public Map<String, Object> publicInfo() {
Map<String, Object> result = new HashMap<>();
result.put("message", "Public info, no authentication required");
result.put("timestamp", System.currentTimeMillis());
return result;
}
@GetMapping("/list")
@PreAuthorize("hasAuthority('SCOPE_message.read')")
public Map<String, Object> listOrders(Authentication authentication) {
Map<String, Object> result = new HashMap<>();
result.put("message", "Order list");
result.put("user", authentication.getName());
result.put("authorities", authentication.getAuthorities());
return result;
}
@PostMapping("/create")
@PreAuthorize("hasAuthority('SCOPE_message.write')")
public Map<String, Object> createOrder(Principal principal) {
Map<String, Object> result = new HashMap<>();
result.put("message", "Order created successfully");
result.put("user", principal.getName());
result.put("orderId", System.currentTimeMillis());
return result;
}
@GetMapping("/user/info")
public Map<String, Object> getUserInfo() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Map<String, Object> result = new HashMap<>();
result.put("username", auth.getName());
result.put("authorities", auth.getAuthorities());
result.put("details", auth.getDetails());
return result;
}
}Testing the Setup
Start both services with mvn spring-boot:run in their respective directories. Obtain an access token using the password grant:
curl -X POST http://localhost:8080/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=teaching-client" \
-d "client_secret=teaching-secret" \
-d "username=user" \
-d "password=123456" \
-d "scope=message.read message.write"The response contains access_token, refresh_token, token type and expiry.
Use the token to call protected endpoints:
# List orders
curl http://localhost:8081/api/order/list \
-H "Authorization: Bearer $TOKEN"
# Create an order
curl -X POST http://localhost:8081/api/order/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"
# Get user info
curl http://localhost:8081/api/order/user/info \
-H "Authorization: Bearer $TOKEN"Four OAuth2 Grant Types Compared
Authorization Code – best for server‑side web apps; highest security (★★★★★).
Password – suitable for trusted clients such as first‑party mobile apps; moderate security (★★★).
Client Credentials – used for service‑to‑service calls; lower security (★★).
Implicit (deprecated) – for pure front‑end apps; lowest security (★).
Code Retrieval
💡 Reply with “OAuth2 第一期脚本” to receive the full code‑generation script for this episode.
Next Episode Preview
Spring Security + OAuth2 Authentication (Part 2): Deep dive into JWT structure, custom claims, token enhancement, and refresh‑token mechanisms.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Coder Trainee
Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
