Firebase Authentication-Spring Security Integration
Firebase Authentication is a service that allows easy user authentication with email/password, social providers, and phone numbers. On the other hand, Spring Security is a powerful authentication and access-control framework for securing Spring-based applications. Let’s delve into understanding how Spring Security integrates with Firebase Authentication for secure and seamless user management.
1. Introduction
1.1 What is Firebase?
Firebase is a comprehensive platform developed by Google for building and managing mobile and web applications. It provides a wide range of services, including real-time databases, cloud storage, and analytics, but one of its most notable features is its robust authentication service. Firebase Authentication simplifies the process of user authentication by supporting various methods, such as email/password, phone authentication, and popular social login providers like Google, Facebook, and Twitter. This makes it an excellent choice for modern applications requiring secure and user-friendly authentication.
1.2 What is Spring Security?
Spring Security is a powerful and customizable authentication and access-control framework used in Java applications. Part of the Spring Framework ecosystem, Spring Security provides comprehensive security features to protect applications against common security threats, such as unauthorized access, CSRF attacks, and more. With support for multiple authentication methods, including OAuth2, LDAP, JWT, and custom authentication providers, Spring Security is widely used in enterprise applications to ensure robust security standards.
1.3 Firebase authentication with Spring security
Combining Firebase Authentication with Spring Security allows developers to create secure applications that leverage Firebase’s authentication capabilities while benefiting from Spring Security’s extensive authorization and security features. By integrating Firebase with Spring Security, developers can manage user authentication seamlessly, implement role-based access control, and protect sensitive resources effectively in their applications.
2. Setting up the Project
We’ll start by creating a Spring Boot project with the necessary dependencies. In the pom.xml
file, add the following dependencies:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.google.firebase</groupId> <artifactId>firebase-admin</artifactId> <version>8.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
3. Generating the authentication file in Firebase?
- Go to the Firebase Console: Visit https://console.firebase.google.com/ and sign in with your Google account.
- Select Your Project: If you already have a Firebase project, select it from the dashboard. If not, create a new project by clicking “Add Project” and following the setup steps.
- Open Project Settings: In your Firebase project dashboard, click on the gear icon next to “Project Overview” in the top-left corner. Select
Project settings
from the dropdown menu. - Go to the Service Accounts Tab: In the Project Settings page, navigate to the
Service accounts
tab. This tab contains information and options for setting up Firebase Admin SDK for server-side code. - Generate a New Private Key: Under the “Firebase Admin SDK” section, you’ll see a button labeled
Generate new private key
. Click this button. - Confirm Key Creation: A dialog box will appear asking if you want to generate a new private key. Click
Generate
to proceed. This will download theserviceAccountKey.json
file to your computer. - Save the File Securely: Place the
serviceAccountKey.json
file in a secure location within your project directory. Avoid committing this file to source control (e.g., Git) as it contains sensitive information.
4. Creating Users in Firebase Authentication
Firebase allows you to create and manage users through its console or programmatically.
4.1 Creating a configuration class
First, initialize Firebase by adding a configuration class that loads your service account JSON key file:
import com.google.auth.oauth2.GoogleCredentials; import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.FileInputStream; import java.io.IOException; @Configuration public class FirebaseConfig { @Bean public FirebaseApp initializeFirebase() throws IOException { FileInputStream serviceAccount = new FileInputStream("path/to/your/serviceAccountKey.json"); FirebaseOptions options = FirebaseOptions.builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .build(); return FirebaseApp.initializeApp(options); } }
4.1.1 Code Explanation
This code defines a configuration class in a Spring Boot application to initialize and configure Firebase. The class is annotated with @Configuration
, indicating that it is a Spring configuration class. This allows it to define beans and configurations to be managed within the Spring application context.
Inside the FirebaseConfig
class, a method initializeFirebase
is defined, annotated with @Bean
. The @Bean
annotation instructs Spring to manage the returned object from this method as a bean, making it available for dependency injection throughout the application. The initializeFirebase
method is responsible for setting up the Firebase application instance with necessary configurations.
Within the initializeFirebase
method, a FileInputStream
is used to read the Firebase service account key file, specified by "path/to/your/serviceAccountKey.json"
. This JSON file contains credentials required to authenticate with Firebase, so it must be replaced with the actual path to your Firebase service account key.
After opening the service account file, a FirebaseOptions
object is created using a builder pattern. The setCredentials
method is called on this builder, where the service account credentials are loaded and passed. This sets up the authentication for Firebase, enabling it to perform server-side operations securely.
Finally, the method calls FirebaseApp.initializeApp(options)
, which initializes the Firebase application with the specified options. This FirebaseApp
instance is returned from the method, allowing it to be injected wherever needed in the application for Firebase services.
4.2 Creating new users
Once Firebase is initialized, we can create new users:
import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.UserRecord; import org.springframework.stereotype.Service; @Service public class FirebaseUserService { public UserRecord createUser(String email, String password) throws Exception { UserRecord.CreateRequest request = new UserRecord.CreateRequest() .setEmail(email) .setPassword(password); return FirebaseAuth.getInstance().createUser(request); } }
4.2.1 Code Explanation
This code defines a Spring service class called FirebaseUserService
that provides functionality for creating a new user in Firebase Authentication. The class is annotated with @Service
, which designates it as a service component in the Spring application. This allows Spring to manage it as a singleton bean, making it accessible for dependency injection throughout the application.
The main functionality of this class is provided by the createUser
method, which takes two parameters: email
and password
. These parameters represent the email and password of the user that will be created in Firebase. The method is declared to throw an Exception
to handle any potential errors that might occur while interacting with Firebase.
Within the createUser
method, an instance of UserRecord.CreateRequest
is created and configured with the provided email and password. This is done using a builder-style method where setEmail
and setPassword
are called on the CreateRequest
object. This configuration prepares a request object that specifies the details of the user to be created.
Next, the FirebaseAuth.getInstance().createUser(request)
method is called. FirebaseAuth.getInstance()
retrieves the Firebase Authentication instance, and createUser
sends the request to Firebase to create the new user. If successful, this method returns a UserRecord
object representing the newly created user.
5. Implementing User Login Functionality
Firebase provides a token upon successful login that we can validate on the backend.
Here’s how the client-side login might look in JavaScript:
firebase.auth().signInWithEmailAndPassword(email, password) .then((userCredential) => { userCredential.user.getIdToken().then((idToken) => { // Send idToken to the backend }); }) .catch((error) => { console.error("Error signing in:", error); });
5.1 Code Explanation
This code demonstrates how to use Firebase Authentication to sign in a user with an email and password. The method firebase.auth().signInWithEmailAndPassword(email, password)
is called, where email
and password
are the credentials provided by the user for authentication. This method initiates the sign-in process and returns a promise, which either resolves with a userCredential
object on successful login or rejects with an error if authentication fails.
Upon successful login, the then
method is executed, where a userCredential
object is passed to the callback function. The userCredential
object contains information about the authenticated user, including the user’s ID token. Within this callback, userCredential.user.getIdToken()
is called, which retrieves the ID token for the signed-in user. This ID token is essential for backend authentication, as it verifies the user’s identity.
The ID token is then available in another promise that resolves with the idToken
variable. Typically, this idToken
would be sent to the backend server (e.g., through an HTTP request) to allow the server to verify the user’s identity and grant access to protected resources.
If there is an error during the sign-in process, the catch
method is triggered, which handles any errors that may occur, such as invalid credentials or network issues. In this example, the error is logged to the console using console.error
to help identify the issue.
6. Exchanging Refresh Tokens for New ID Tokens
ID tokens from Firebase expire periodically, so it’s essential to handle token renewal on the client side. Firebase provides a refresh token mechanism to help you get a new ID token without re-authenticating.
firebase.auth().currentUser.getIdToken(true).then((idToken) => { // Use the new ID token }).catch((error) => { console.error("Error refreshing token:", error); });
6.1 Code Explanation
This code snippet demonstrates how to refresh a user’s Firebase ID token to ensure they have a valid authentication token. Firebase ID tokens have a limited lifespan, so refreshing the token periodically is necessary for continued authentication in applications with long-lived sessions.
The code begins with firebase.auth().currentUser.getIdToken(true)
, which attempts to retrieve a fresh ID token for the currently signed-in user. The getIdToken
method accepts a boolean parameter. Passing true
forces the refresh of the ID token, ensuring that the application receives a new and valid token even if the previous one is still valid but close to expiration.
This method returns a promise that resolves with the refreshed ID token, represented by the idToken
variable in the then
callback function. The refreshed idToken
can be used as the new authentication token to securely communicate with the backend or access protected resources, providing proof of the user’s identity and keeping the session active.
If there is an error during the token refresh process, the catch
block is executed. In this example, the catch
method logs any errors to the console using console.error
, which helps to identify and debug issues related to token refreshing, such as network problems or expired sessions.
7. Integrating With Spring Security
Next, we’ll add a filter to validate Firebase ID tokens with each request. This filter will extract the token from the Authorization header, validate it with Firebase, and then set the authenticated user in the Spring Security context.
7.1 Firebase Authentication Filter
Create a filter class, FirebaseAuthenticationFilter
:
import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import javax.servlet.FilterChain; import javax.servlet.Filter; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.IOException; public class FirebaseAuthenticationFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String token = getBearerTokenFromRequest(request); if (token != null) { FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(decodedToken.getUid(), null, Collections.emptyList()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } chain.doFilter(request, response); } private String getBearerTokenFromRequest(ServletRequest request) { String authorization = ((HttpServletRequest) request).getHeader("Authorization"); if (authorization != null && authorization.startsWith("Bearer ")) { return authorization.substring(7); } return null; } }
7.1.1 Code Explanation
This code defines a custom filter, FirebaseAuthenticationFilter
, which is implemented to integrate Firebase Authentication with Spring Security. The purpose of this filter is to intercept incoming HTTP requests, extract and verify Firebase authentication tokens, and then set the authenticated user in the Spring Security context if the token is valid. This filter class implements the Filter
interface, allowing it to be registered in the Spring Security filter chain.
The main functionality is in the doFilter
method, which is overridden from the Filter
interface. This method intercepts each HTTP request and processes it before it reaches other parts of the application. Inside doFilter
, the getBearerTokenFromRequest
method is called to extract the bearer token from the HTTP request’s Authorization header. If the Authorization header contains a token starting with “Bearer “, this method retrieves the token part by removing the “Bearer ” prefix; otherwise, it returns null
.
If a valid token is present, the code proceeds to verify it with Firebase. FirebaseAuth.getInstance().verifyIdToken(token)
verifies the extracted token with Firebase, returning a FirebaseToken
object if the token is valid. The FirebaseToken
object contains authenticated user information, including the user’s unique ID (UID).
Next, a UsernamePasswordAuthenticationToken
object is created to represent the authenticated user within Spring Security. The UID from the FirebaseToken
is used as the principal (the unique user identifier), and null
is set for credentials as password verification is managed by Firebase. An empty list of authorities is provided, but you could customize this to include specific user roles if needed. Additionally, authentication.setDetails
is called to set request-specific details, such as the remote address, using a WebAuthenticationDetailsSource
object.
Once the UsernamePasswordAuthenticationToken
is created, it is set in the SecurityContextHolder
, which stores the authentication information in the current security context. This allows Spring Security to recognize the user as authenticated and grants access to secured resources based on their session.
Finally, the filter chain continues by calling chain.doFilter(request, response)
, passing the request and response to the next filter or servlet in the chain. If the token is invalid or missing, the filter simply passes the request through without setting an authenticated user in the security context.
7.2 Configuring Spring Security
Finally, integrate the filter into Spring Security’s configuration by creating a SecurityConfig
class:
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .addFilterBefore(new FirebaseAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } }
7.2.1 Code Explanation
This code defines a Spring Security configuration class, SecurityConfig
, that extends WebSecurityConfigurerAdapter
. This class is responsible for configuring security settings in the Spring Boot application, particularly to integrate Firebase Authentication with Spring Security.
The class is annotated with @Configuration
and @EnableWebSecurity
. The @Configuration
annotation designates this class as a configuration class, enabling it to define and manage beans for the Spring application context. The @EnableWebSecurity
annotation enables Spring Security’s web security support, allowing custom security configurations to be applied to HTTP requests.
The main security configuration is defined in the overridden configure
method, which accepts an HttpSecurity
object. This method customizes the security settings for HTTP requests in the application. Here, several configurations are applied to manage authentication and the security filter chain:
First, csrf().disable()
disables CSRF (Cross-Site Request Forgery) protection for simplicity. CSRF protection is often disabled in APIs and mobile applications where the server and client are separate and the frontend handles authentication securely. However, enabling CSRF is recommended for web applications where CSRF attacks are a risk.
Next, authorizeRequests().anyRequest().authenticated()
configures authorization for all incoming HTTP requests. The anyRequest().authenticated()
directive ensures that every request requires authentication. This means that only authenticated users can access any endpoint in the application, helping protect sensitive resources from unauthorized access.
Finally, the addFilterBefore
method is used to add a custom filter, FirebaseAuthenticationFilter
, into the security filter chain. This filter is inserted before UsernamePasswordAuthenticationFilter
, ensuring that Firebase token verification occurs early in the filter chain. By verifying the Firebase token, FirebaseAuthenticationFilter
sets up the Spring Security context with the authenticated user, allowing Spring Security to control access based on the verified identity.
8. Run the code
Execute the following command to run the application and it should be running locally on the following endpoint – http://localhost:8080
.
mvn spring-boot:run
Use the frontend call to authenticate the users and obtain an ID token from the Firebase. Once done include the token in the Authorization header to further play with the application.
9. Conclusion
In this guide, we integrated Firebase Authentication with Spring Security in a Spring Boot application. By using Firebase tokens, we were able to authenticate users and secure requests effectively. This setup provides a robust, scalable authentication solution that takes advantage of Firebase’s identity management capabilities and Spring Security’s access control.
Further improvements could include role-based access control, additional security configurations, and error handling for token validation failures. With this foundation, your application is now ready to securely manage user authentication using Firebase and Spring Security.