Implementing Authentication & Authorization with Spring Security
Topics to be Covered¶
- Implementing Authentication and Authorization with Spring Security
- Hands-on implementation using the **Spring Security framework and OAuth2 for token-based security**
Token Validation in ProductService¶
Token Validation Workflow in a Secured Service¶
-
Token Generation: Tokens are generated in the
UserService
during the authentication process. This token is essential for accessing protected resources across services. -
Token Usage in ProductService:
- Whenever a user interacts with the
ProductService
, they must send the token (usually as a header in the HTTP request). - The
ProductService
will receive this token and proceed to validate it before processing the request.
- Whenever a user interacts with the
-
Token Verification Process:
- The first step for
ProductService
is to validate the token. This ensures that the token is legitimate and has not expired. - If token verification fails, the system should deny access by returning an HTTP 403 Forbidden status.
- If token verification succeeds, the user is granted access to the requested resources.
- The first step for
-
Role-Based Authorization:
- Beyond token verification, the service can also check the role associated with the user (e.g., admin, regular user).
- This is essential for determining access levels. For instance:
- Admins may be allowed to view and manage all products.
- Regular users may have restricted access to certain details.
- Based on the user's role, the system can enforce role-specific policies such as showing only relevant information or restricting certain actions.
Code Example: Token Validation in AuthenticationCommons
¶
The code below shows how AuthenticationCommons
handles token validation using an HTTP call to the user service.
package com.scaler.productservicedecmwfeve.commons;
import com.scaler.productservicedecmwfeve.dtos.UserDto;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class AuthenticationCommons {
private RestTemplate restTemplate;
public AuthenticationCommons(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public UserDto validateToken(String token) {
// Send an HTTP POST request to the UserService for token validation
ResponseEntity<UserDto> userDtoResponse = restTemplate.postForEntity(
"http://localhost:8181/users/validate/" + token,
null, // No body needed for validation
UserDto.class // Expecting a UserDto object as a response
);
// If no user is found, return null, which implies invalid token
if (userDtoResponse.getBody() == null) {
return null;
}
// If validation is successful, return the UserDto
return userDtoResponse.getBody();
}
}
- The
validateToken
method sends a request to the user service to validate the token. If the user service returns a valid user (UserDto
), access is allowed; otherwise, it denies the request.
Code Example: Role-Based Authorization in ProductController
¶
Here, we demonstrate how ProductController
uses the validated token to enforce role-based access.
public ResponseEntity<List<Product>> getAllProducts() {
// Validate the token using AuthenticationCommons
UserDto userDto = authenticationCommons.validateToken(token);
// If the token is invalid, return 403 Forbidden
if (userDto == null) {
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
}
// Check if the user is an admin
boolean isAdmin = false;
for (Role role : userDto.getRoles()) {
if (role.getName().equals("ADMIN")) {
isAdmin = true;
break;
}
}
// If the user is not an admin, return 401 Unauthorized
if (!isAdmin) return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
// Fetch all products
List<Product> products = productService.getAllProducts();
// Modify the product titles as per business logic (optional)
List<Product> finalProducts = new ArrayList<>();
for (Product p : products) {
p.setTitle("Hello " + p.getTitle()); // Example modification
finalProducts.add(p);
}
// Return the modified list of products
return new ResponseEntity<>(finalProducts, HttpStatus.OK);
}
In this example:
- Step 1: Validate the token using
AuthenticationCommons
. - Step 2: If validation fails, return
403 Forbidden
. - Step 3: If validation succeeds, check if the user is an admin.
- Step 4: If the user is not an admin, return
401 Unauthorized
; otherwise, proceed with fetching and modifying the products. - Step 5: Return the list of modified products with an
HTTP 200 OK
status.
Implementing Authentication and Authorization Using Spring Security¶
Why Use Spring Security?¶
- Implementing custom token validation, login, signup, and role management can lead to complex, repetitive code.
- Spring Security provides a standardized and robust way to handle all these functionalities with less effort.
- It comes with built-in support for common security concerns like token validation, role-based access, OAuth2, and more.
Introduction to OAuth2¶
OAuth2 is an industry-standard protocol for authorization. It enables applications to obtain limited access to user accounts on an HTTP service, such as Facebook or Google.
-
Key Components of OAuth2:
- User: The person or system accessing the application.
- Authorization Server: The system responsible for authenticating the user and issuing access tokens.
- Resource Server: The server that contains the resources being accessed (e.g., ProductService).
- Application: The application (client) that the user interacts with, which requests authorization on their behalf.
Spring Security Authorization Server¶
- Spring Security provides built-in support for OAuth2, including an Authorization Server implementation.
- You can implement your own Authorization Server to handle user authentication and issue tokens.
- Refer to this Github Repository for a detailed implementation.
For a detailed setup guide, follow the Spring Authorization Server Documentation.
Spring Security Configuration¶
To enable Spring Security in your project, you need to follow a few steps:
Dependency Setup¶
First, include the relevant dependencies in your pom.xml
file to add Spring Security and OAuth2 support.
Application Configuration: application.properties
¶
Add the following configuration properties in application.properties
. These properties define the client credentials, authentication methods, and redirection URIs needed for OAuth2.
# OAuth2 Client Configuration
spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-id=oidc-client
spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-secret={noop}secret
spring.security.oauth2.authorizationserver.client.oidc-client.registration.client-authentication-methods[0]=client_secret_basic
spring.security.oauth2.authorizationserver.client.oidc-client.registration.authorization-grant-types[0]=authorization_code
spring.security.oauth2.authorizationserver.client.oidc-client.registration.authorization-grant-types[1]=refresh_token
spring.security.oauth2.authorizationserver.client.oidc-client.registration.redirect-uris[0]=http://127.0.0.1:8080/login/oauth2/code/oidc-client
spring.security.oauth2.authorizationserver.client.oidc-client.registration.post-logout-redirect-uris[0]=http://127.0.0.1:8080/
spring.security.oauth2.authorizationserver.client.oidc-client.registration.scopes[0]=openid
spring.security.oauth2.authorizationserver.client.oidc-client.registration.scopes[1]=profile
spring.security.oauth2.authorizationserver.client.oidc-client.require-authorization-consent=true
This configuration includes:
- Client ID and Secret: Credentials for identifying the client application.
- Grant Types: Defines the authorization grant types supported (e.g.,
authorization_code
,refresh_token
). - Redirect URIs: URIs where the user is redirected after login or logout.
- Scopes: Permissions granted to the client (e.g.,
openid
,profile
).
Spring Security Configuration: SecurityConfig.java
¶
Next, configure Spring Security in SecurityConfig.java
. This file will handle security filters, user details management, and the OAuth2 server setup.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
// Apply default security settings for OAuth2 Authorization Server
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated() // All requests require authentication
)
.formLogin(Customizer.withDefaults()); // Use form-based login
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
// In-memory user details for testing purposes
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oidc-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/oidc-client")
.postLogoutRedirectUri("http://127.0.0.1:8080/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
// Generate RSA keys for JWT signing
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private 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;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
// Create default authorization server settings
return AuthorizationServerSettings.builder().build();
}
}
This configuration does the following:
- Security Filters: Configures security filters for handling OAuth2 tokens and user login.
- User Management: Sets up a simple in-memory user service for authentication.
- OAuth2 Server: Sets up OAuth2 authorization, including client registration and RSA key management for JWT tokens.
Running the Application¶
After completing the configuration:
- Run the Spring Boot application.
- Access the login page at
http://localhost:8181/login
. - You can log in using the credentials
user/password
. - By default, user details are stored in memory, so they will be lost when the application is restarted.
Using OAuth2 in Postman¶
Postman supports OAuth2 out of the box, making it an excellent tool for testing OAuth2-based authentication systems.
Steps to Use OAuth2 in Postman:¶
- Configure the OAuth2 details:
- Auth URL: The authorization URL from your OAuth2 setup.
- Token URL: The URL where the token is issued.
- Client ID and Client Secret: These values should match those configured in
SecurityConfig.java
. - Redirect URI: The URI to which users will be redirected after successful login.
- Scopes: Specify the scopes for the OAuth2 token (e.g.,
openid
,profile
).
- Generate Token: After filling in the details, Postman will generate an access token that you can use for further requests.
- Making Requests: Once the token is generated, include it in the
Authorization
header for requests to your protected API.
Persisting OAuth2 Details in a Database¶
By default, the OAuth2 authorization details (e.g., tokens, client credentials) are stored in memory. However, in a production application, these details should be persisted in a database.
Implementing OAuth2 with JPA¶
Follow the steps outlined in the Spring Authorization Server Documentation to configure Spring Authorization Server with JPA.
Database Schema for OAuth2¶
Create tables in the database to store authorization details:
CREATE TABLE authorization (
id VARCHAR(255) NOT NULL,
registered_client_id VARCHAR(255) NULL,
principal_name VARCHAR(255) NULL,
authorization_grant_type VARCHAR(255) NULL,
authorized_scopes TEXT NULL,
attributes TEXT NULL,
state TEXT NULL,
authorization_code_value TEXT NULL,
authorization_code_issued_at datetime NULL,
authorization_code_expires_at datetime NULL,
authorization_code_metadata VARCHAR(255) NULL,
access_token_value TEXT NULL,
access_token_issued_at datetime NULL,
access_token_expires_at datetime NULL,
access_token_metadata VARCHAR(255) NULL,
access_token_type VARCHAR(255) NULL,
access_token_scopes TEXT NULL,
refresh_token_value TEXT NULL,
refresh_token_issued_at datetime NULL,
refresh_token_expires_at datetime NULL,
refresh_token_metadata TEXT NULL,
oidc_id_token_value TEXT NULL,
oidc_id_token_issued_at datetime NULL,
oidc_id_token_expires_at datetime NULL,
oidc_id_token_metadata TEXT NULL,
oidc_id_token_claims TEXT NULL,
user_code_value TEXT NULL,
user_code_issued_at datetime NULL,
user_code_expires_at datetime NULL,
user_code_metadata TEXT NULL,
device_code_value TEXT NULL,
device_code_issued_at datetime NULL,
device_code_expires_at datetime NULL,
device_code_metadata TEXT NULL,
CONSTRAINT pk_authorization PRIMARY KEY (id)
);
CREATE TABLE authorization_consent (
registered_client_id VARCHAR(255) NOT NULL,
principal_name VARCHAR(255) NOT NULL,
authorities VARCHAR(1000) NULL,
CONSTRAINT pk_authorizationconsent PRIMARY KEY (registered_client_id, principal_name)
);
CREATE TABLE client (
id VARCHAR(255) NOT NULL,
client_id VARCHAR(255) NULL,
client_id_issued_at datetime NULL,
client_secret VARCHAR(255) NULL,
client_secret_expires_at datetime NULL,
client_name VARCHAR(255) NULL,
client_authentication_methods VARCHAR(1000) NULL,
authorization_grant_types VARCHAR(1000) NULL,
redirect_uris VARCHAR(1000) NULL,
post_logout_redirect_uris VARCHAR(1000) NULL,
scopes VARCHAR(1000) NULL,
client_settings VARCHAR(2000) NULL,
token_settings VARCHAR(2000) NULL,
CONSTRAINT pk_client PRIMARY KEY (id)
);
This schema provides a persistent store for OAuth2 tokens and client registrations.
Implementing JPA Models and Repositories¶
Once the database schema is set up, create the corresponding JPA models and repositories to interact with the database. Use the Spring Authorization Server documentation as a reference for mapping these entities correctly.