|
| 1 | +# Firebase And Spring Boot |
| 2 | + |
| 3 | +This repository is an example of how to use Firebase with Springboot to provide authentication services. |
| 4 | + |
| 5 | +## Configuring Firebase |
| 6 | + |
| 7 | +Before we start enable the SignIn Methods. I enable Email/Password and Google and create a user. You can simply add a user using email/password. |
| 8 | + |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | +### Configuring Firebase in the SpringBoot Project: |
| 13 | + |
| 14 | +To configure the firebase in the spring boot project first download the google-services.json file from your Firebase Project setting. |
| 15 | + |
| 16 | +Go to Project Setting -> Service Account-> Scroll down and click on create and then click on Generate new Private Key |
| 17 | + |
| 18 | +This will generate and download Private Key to access the Firebase Admin SDK |
| 19 | + |
| 20 | +The code snippets are also provided for different language |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | +Now rename the private key to firebase_config.json and paste in the resource folder of your spring boot project. This is the location where we access our private key. You can also store on other location or environment variable. For this tutorial, we will store in the resource folder |
| 25 | + |
| 26 | + |
| 27 | + |
| 28 | +## Configuring POM.xml |
| 29 | + |
| 30 | +These files are already in place but lets point them out |
| 31 | + |
| 32 | +``` |
| 33 | +<dependency> |
| 34 | + <groupId>com.google.firebase</groupId> |
| 35 | + <artifactId>firebase-admin</artifactId> |
| 36 | + <version>8.1.0</version> |
| 37 | +</dependency> |
| 38 | +<dependency> |
| 39 | + <groupId>org.springframework.boot</groupId> |
| 40 | + <artifactId>spring-boot-starter-security</artifactId> |
| 41 | +</dependency> |
| 42 | +``` |
| 43 | + |
| 44 | +## Configuring SpringBoot Project to add Security |
| 45 | + |
| 46 | +We created a filter to check every request for the JWT bearer token in the request. |
| 47 | + |
| 48 | +For this, we are made a **OncePerRequestFilter**. **Filter** base class that aims to guarantee a **single** execution **per request** dispatch. |
| 49 | + |
| 50 | + |
| 51 | +``` |
| 52 | +@Component |
| 53 | +public class SecurityFilter extends OncePerRequestFilter { |
| 54 | +... |
| 55 | +
|
| 56 | + @Override |
| 57 | + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) |
| 58 | + throws ServletException, IOException { |
| 59 | + verifyToken(request); |
| 60 | + filterChain.doFilter(request, response); |
| 61 | + } |
| 62 | +
|
| 63 | + private void verifyToken(HttpServletRequest request) { |
| 64 | + String session = null; |
| 65 | + FirebaseToken decodedToken = null; |
| 66 | + CredentialType type = null; |
| 67 | + boolean strictServerSessionEnabled = securityProps.getFirebaseProps().isEnableStrictServerSession(); |
| 68 | + Cookie sessionCookie = cookieUtils.getCookie("session"); |
| 69 | + String token = securityService.getBearerToken(request); |
| 70 | + try { |
| 71 | + if (sessionCookie != null) { |
| 72 | + session = sessionCookie.getValue(); |
| 73 | + decodedToken = FirebaseAuth.getInstance().verifySessionCookie(session, |
| 74 | + securityProps.getFirebaseProps().isEnableCheckSessionRevoked()); |
| 75 | + type = CredentialType.SESSION; |
| 76 | + } else if (!strictServerSessionEnabled) { |
| 77 | + if (token != null && !token.equalsIgnoreCase("undefined")) { |
| 78 | + decodedToken = FirebaseAuth.getInstance().verifyIdToken(token); |
| 79 | + type = CredentialType.ID_TOKEN; |
| 80 | + } |
| 81 | + } |
| 82 | + } catch (FirebaseAuthException e) { |
| 83 | + e.printStackTrace(); |
| 84 | + logger.error("Firebase Exception:: ", e.getLocalizedMessage()); |
| 85 | + } |
| 86 | + FireBaseUser user = firebaseTokenToUserDto(decodedToken); |
| 87 | + if (user != null) { |
| 88 | + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, |
| 89 | + new Credentials(type, decodedToken, token, session), null); |
| 90 | + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); |
| 91 | + SecurityContextHolder.getContext().setAuthentication(authentication); |
| 92 | + } |
| 93 | + } |
| 94 | +
|
| 95 | +... |
| 96 | +
|
| 97 | +
|
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +In this filter, we are first checking is session-based authentication if the session cookie is available then we are verifying the session cookie and fetching the details, and set to SecurityContextHolder. If the session cookie is not available then we are using Bearer token. |
| 102 | + |
| 103 | + |
| 104 | +``` |
| 105 | +@Service |
| 106 | +public class SecurityService { |
| 107 | +
|
| 108 | +... |
| 109 | +
|
| 110 | + public User getUser() { |
| 111 | + User userPrincipal = null; |
| 112 | + SecurityContext securityContext = SecurityContextHolder.getContext(); |
| 113 | + Object principal = securityContext.getAuthentication().getPrincipal(); |
| 114 | + if (principal instanceof User) { |
| 115 | + userPrincipal = ((User) principal); |
| 116 | + } |
| 117 | + return userPrincipal; |
| 118 | + } |
| 119 | +
|
| 120 | + public Credentials getCredentials() { |
| 121 | + SecurityContext securityContext = SecurityContextHolder.getContext(); |
| 122 | + return (Credentials) securityContext.getAuthentication().getCredentials(); |
| 123 | + } |
| 124 | +
|
| 125 | + public boolean isPublic() { |
| 126 | + return securityProps.getAllowedPublicApis().contains(httpServletRequest.getRequestURI()); |
| 127 | + } |
| 128 | +
|
| 129 | + public String getBearerToken(HttpServletRequest request) { |
| 130 | + String bearerToken = null; |
| 131 | + String authorization = request.getHeader("Authorization"); |
| 132 | + if (StringUtils.hasText(authorization) && authorization.startsWith("Bearer ")) { |
| 133 | + bearerToken = authorization.substring(7); |
| 134 | + } |
| 135 | + return bearerToken; |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | + |
| 140 | +SecurityService to decode Bearer token from a request and fetching Principle from SecurityContextHolder. |
| 141 | + |
| 142 | +``` |
| 143 | +@Configuration |
| 144 | +public class SecurityConfig{ |
| 145 | +
|
| 146 | + private ObjectMapper objectMapper; |
| 147 | + private SecurityProperties restSecProps; |
| 148 | + public SecurityFilter tokenAuthenticationFilter; |
| 149 | +
|
| 150 | + @Autowired |
| 151 | + public SecurityConfig(ObjectMapper objectMapper, SecurityProperties restSecProps, SecurityFilter tokenAuthenticationFilter){ |
| 152 | + this.objectMapper = objectMapper; |
| 153 | + this.restSecProps = restSecProps; |
| 154 | + this.tokenAuthenticationFilter = tokenAuthenticationFilter; |
| 155 | + } |
| 156 | +
|
| 157 | +
|
| 158 | +
|
| 159 | + @Bean |
| 160 | + public AuthenticationEntryPoint restAuthenticationEntryPoint() { |
| 161 | + return (httpServletRequest, httpServletResponse, e) -> { |
| 162 | + Map<String, Object> errorObject = new HashMap<>(); |
| 163 | + int errorCode = 401; |
| 164 | + errorObject.put("message", "Unauthorized access of protected resource, invalid credentials"); |
| 165 | + errorObject.put("error", HttpStatus.UNAUTHORIZED); |
| 166 | + errorObject.put("code", errorCode); |
| 167 | + errorObject.put("timestamp", new Timestamp(new Date().getTime())); |
| 168 | + httpServletResponse.setContentType("application/json;charset=UTF-8"); |
| 169 | + httpServletResponse.setStatus(errorCode); |
| 170 | + httpServletResponse.getWriter().write(objectMapper.writeValueAsString(errorObject)); |
| 171 | + }; |
| 172 | + } |
| 173 | +
|
| 174 | + @Bean |
| 175 | + public CorsConfigurationSource corsConfigurationSource() { |
| 176 | + CorsConfiguration configuration = new CorsConfiguration(); |
| 177 | + configuration.setAllowedOrigins(restSecProps.getAllowedOrigins()); |
| 178 | + configuration.setAllowedMethods(restSecProps.getAllowedMethods()); |
| 179 | + configuration.setAllowedHeaders(restSecProps.getAllowedHeaders()); |
| 180 | + configuration.setAllowCredentials(restSecProps.isAllowCredentials()); |
| 181 | + configuration.setExposedHeaders(restSecProps.getExposedHeaders()); |
| 182 | + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); |
| 183 | + source.registerCorsConfiguration("/**", configuration); |
| 184 | + return source; |
| 185 | + } |
| 186 | +
|
| 187 | + @Bean |
| 188 | + public SecurityFilterChain configure(HttpSecurity http) throws Exception { |
| 189 | + http.cors().configurationSource(corsConfigurationSource()).and().csrf().disable().formLogin().disable() |
| 190 | + .httpBasic().disable().exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint()) |
| 191 | + .and().authorizeRequests() |
| 192 | + .antMatchers(restSecProps.getAllowedPublicApis().toArray(String[]::new)).permitAll() |
| 193 | + .antMatchers(HttpMethod.OPTIONS, "/**").permitAll().anyRequest().authenticated().and() |
| 194 | + .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) |
| 195 | + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); |
| 196 | + return http.build(); |
| 197 | + } |
| 198 | +
|
| 199 | + @Bean |
| 200 | + public WebSecurityCustomizer webSecurityCustomizer() { |
| 201 | + return (web) -> web.ignoring().antMatchers("/images/**", "/js/**", "/webjars/**"); |
| 202 | + } |
| 203 | +} |
| 204 | +
|
| 205 | +``` |
| 206 | + |
| 207 | +Spring Security WebSecurityConfigurerAdapter to configure Cors, SecurityFilter, and AuthenticationEntryPoint for changing the default HTML Unauthorised page to API based 401 response. |
| 208 | + |
| 209 | +``` |
| 210 | +security: |
| 211 | +... |
| 212 | + allowed-origins: |
| 213 | + - https://${DOMAIN} |
| 214 | + - http://localhost:3000 |
| 215 | + allowed-methods: |
| 216 | + - GET |
| 217 | + - POST |
| 218 | + - PUT |
| 219 | + - PATCH |
| 220 | + - DELETE |
| 221 | + - OPTIONS |
| 222 | + allowed-headers: |
| 223 | + - Authorization |
| 224 | + - Origin |
| 225 | + - Content-Type |
| 226 | + - Accept |
| 227 | + - Accept-Encoding |
| 228 | + - Accept-Language |
| 229 | + - Access-Control-Allow-Origin |
| 230 | + - Access-Control-Allow-Headers |
| 231 | + - Access-Control-Request-Method |
| 232 | + - X-Requested-With |
| 233 | + - X-Auth-Token |
| 234 | + - X-Xsrf-Token |
| 235 | + - Cache-Control |
| 236 | + - Id-Token |
| 237 | + allowed-public-apis: |
| 238 | + - /favicon.ico |
| 239 | + - /session/login |
| 240 | + - /public/** |
| 241 | + exposed-headers: |
| 242 | + - X-Xsrf-Token |
| 243 | +``` |
| 244 | + |
| 245 | +**application.yml** file to change the properties of headers, public domains, etc |
| 246 | + |
| 247 | +To get a token use this url: |
| 248 | + |
| 249 | +``` |
| 250 | +https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={webapikey} |
| 251 | +``` |
| 252 | + |
| 253 | +with body: |
| 254 | + |
| 255 | +``` |
| 256 | +{ |
| 257 | + |
| 258 | + "password":"12345678", |
| 259 | + "returnSecureToken":true |
| 260 | +} |
| 261 | +``` |
0 commit comments