Implement Firebase custom tokens with Spring security
OK, Let me start with a little disclaimer —
You don’t have to use Spring Security to implement Firebase custom tokens. You could use any Authentication server to authorize a rest API. That is all we will be doing here. However, I would like to show it under the purview of spring security.
Problem statement —
You have an existing application that already uses firebase for authentication. Suddenly, you have to authenticate users against one more authentication server — typically an in-house server.
You got two options —
- Start coding a new component that will authenticate users with both firebase and a custom auth server.
- You can just use the existing firebase.
Of course — you would want to go with just firebase. It is easy, has a fluent and battle tested API and your application is supposedly already using it.
Moreover, if you have multiple apps already using firebase and each of them needs this new functionality. All you have to add are a few lines of code and you are good to go..!
If you want to really know how many lines — here is an example from firebase web SDK —
I would love to explain this example but we talking about something else. Feel free to check out the firebase docs for the same.
firebase.auth().signInWithCustomToken(token)
.then((userCredential) => {
// Signed in
var user = userCredential.user;
// ...
})
.catch((error) => {
var errorCode = error.code;
var errorMessage = error.message;
// ...
});
Steps to Implement Firebase custom tokens
Initialize a new Spring boot security application
I have already created a post that explains how to create an in-memory username and password authentication. That is all we need.
Add Firebase Admin SDK to the project
You can use one of the below depending on the build system.
build.gradle
dependencies {
implementation 'com.google.firebase:firebase-admin:8.0.1'
}
pom.xml
<dependency>
<groupId>com.google.firebase</groupId>
<artifactId>firebase-admin</artifactId>
<version>8.0.1</version>
</dependency>
Create a service account with firebase
You would require a service account to proceed further. I would suggest checking out this document to set up a service account.
Once you are done with the service account, download the firebase config JSON file.
You have two options to authorize via service accounts —
- Add an environment variable
GOOGLE_APPLICATION_CREDENTIALS
and point to the JSON file. This is recommended approach and we will be using the same one. - Explicitly specify the path to the JSON file.
Initialize the Firebase Admin SDK
We will create a Spring configuration bean and initialize the firebase Admin SDK in @PostConstruct
.
@Configuration
public class FirebaseConfig {
@Bean
public FirebaseAuth firebaseAuth(){
return FirebaseAuth.getInstance();
}
@PostConstruct
public void initializeFirebaseApp() throws IOException {
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.getApplicationDefault())
.setServiceAccountId(SERVICE_ACCOUNT_ID)
.build();
FirebaseApp.initializeApp(options);
}
}
Create the custom token
We will create a rest API that will return a token minted via the FirebaseAuth
Object.
FirebaseAuth.createCustomToken(uid)
method is used to create a custom token for a UID. The UID should uniquely identify a user. Optionally, we can add additional claims to this token via the overloaded createCustomToken
method. 👇🏼
createCustomToken(
@NonNull String uid, @Nullable Map<String, Object> developerClaims){}
Spring security Authentication
interface provides Authentication.getName()
method which represents the username of the current user. we can safely use it for the uid.
We will also add grantedAuthorities
to the token via claims.
@RestController
public class TokenController {
@Autowired
private FirebaseAuth firebaseAuth;
@GetMapping("/token")
public Map<String, String> getToken() throws FirebaseAuthException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
//joining elements of collections as comma seperated string
String authorities = authentication.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
System.out.println(authorities);
String customToken = firebaseAuth.createCustomToken("andrew", Collections.singletonMap("authorities", authorities));
return Collections.singletonMap("token", customToken);
}
}
Finally — Configure Spring Security
The API created to fetch the token should be secured so that only logged in user should be able to fetch a new token.
anyRequest().authenticated()
will authenticate all the requests. We will be using http basic authentication.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.cors().configurationSource(new CorsConfigurationSource() {
@Nullable
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
return config;
}
})
.and()
.authorizeRequests()
.anyRequest().authenticated();
}
}
Usage:
Assuming the authentication server is at localhost:8080. we can use the below request to fetch the token.
curl --location --request GET 'http://localhost:8080/token' \
--header 'Authorization: Basic dGVzdFVzZXIxOnBhc3N3b3Jk' \
This request will return a JWT token which can be used for authentication via the firebase client.