From a626cb429cd46276bf1726ad8293d7dd31b85fe3 Mon Sep 17 00:00:00 2001 From: Pierantonio Fanigliulo Date: Sat, 7 Sep 2024 17:11:55 +0200 Subject: [PATCH] first commit --- .github/workflows/deploy.yml | 22 +++ .gitignore | 28 ++++ LICENSE | 21 +++ README.md | 153 ++++++++++++++++++ pom.xml | 65 ++++++++ .../firebaseappcheck/FirebaseAppCheck.java | 11 ++ .../FirebaseAppCheckAspect.java | 101 ++++++++++++ .../FirebaseAppCheckConfiguration.java | 14 ++ .../FirebaseAppCheckException.java | 13 ++ 9 files changed, 428 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheck.java create mode 100644 src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckAspect.java create mode 100644 src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckConfiguration.java create mode 100644 src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckException.java diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..fbc1c07 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,22 @@ +name: Publish package to GitHub Packages +on: + release: + types: [created] +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Set release version + run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + - uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + - name: Publish package + run: mvn --batch-mode deploy --file pom.xml -Drevision=${{ env.RELEASE_VERSION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60cb4f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +target/ +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +# IDE +.vscode/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..def76f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Pierantonio Fanigliulo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..185bfcc --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# Firebase App Check Spring Boot Starter + +[English version below](#english-version) + +## Versione Italiana + +Questa libreria fornisce un'integrazione semplice di [Firebase App Check](https://firebase.google.com/docs/app-check) per applicazioni Spring Boot. Permette di proteggere facilmente gli endpoint REST utilizzando un'annotazione personalizzata. + +### Caratteristiche + +- Annotazione `@FirebaseAppCheck` per proteggere gli endpoint +- Verifica automatica del token Firebase App Check +- Facile integrazione con progetti Spring Boot esistenti + +### Installazione + +Per utilizzare questa libreria nel tuo progetto, aggiungi le seguenti configurazioni al tuo `pom.xml`: + +1. Aggiungi il repository GitHub Packages: + +```xml + + + github + https://maven.pkg.github.com/pierfani/firebase-appcheck-spring + + +``` + +2. Aggiungi la dipendenza: + +```xml + + it.pierfani + firebase-appcheck-spring-boot-starter + 1.0.0 + +``` + +### Configurazione + +Aggiungi le seguenti proprietà al tuo `application.properties` o `application.yml`: + +```properties +it.pierfani.firebaseappcheck.project-number=YOUR_FIREBASE_PROJECT_NUMBER +it.pierfani.firebaseappcheck.jwks-url=https://firebaseappcheck.googleapis.com/v1/jwks +``` + +### Utilizzo + +Per proteggere un endpoint con Firebase App Check, aggiungi semplicemente l'annotazione `@FirebaseAppCheck` al metodo del controller: + +```java +import it.pierfani.firebaseappcheck.FirebaseAppCheck; + +@RestController +public class ExampleController { + + @GetMapping("/protected-endpoint") + @FirebaseAppCheck + public String protectedEndpoint() { + return "Questo endpoint è protetto da Firebase App Check"; + } +} +``` + +### Gestione degli errori + +La libreria lancia `FirebaseAppCheckException` in caso di errori durante la verifica del token. Puoi gestire questa eccezione nel tuo controller o utilizzando un gestore globale delle eccezioni. + +### Contribuire + +I contributi sono benvenuti! Per favore, apri una issue o una pull request per suggerimenti, bug o miglioramenti. + +### Licenza + +Questo progetto è licenziato sotto la licenza MIT. Vedi il file `LICENSE` per i dettagli. + +--- + +## English Version + +This library provides a simple integration of [Firebase App Check](https://firebase.google.com/docs/app-check) for Spring Boot applications. It allows you to easily protect REST endpoints using a custom annotation. + +### Features + +- `@FirebaseAppCheck` annotation to protect endpoints +- Automatic verification of Firebase App Check token +- Easy integration with existing Spring Boot projects + +### Installation + +To use this library in your project, add the following configurations to your `pom.xml`: + +1. Add the GitHub Packages repository: + +```xml + + + github + https://maven.pkg.github.com/pierfani/firebase-appcheck-spring + + +``` + +2. Add the dependency: + +```xml + + it.pierfani + firebase-appcheck-spring-boot-starter + 1.0.0 + +``` + +### Configuration + +Add the following properties to your `application.properties` or `application.yml`: + +```properties +it.pierfani.firebaseappcheck.project-number=YOUR_FIREBASE_PROJECT_NUMBER +it.pierfani.firebaseappcheck.jwks-url=https://firebaseappcheck.googleapis.com/v1/jwks +``` + +### Usage + +To protect an endpoint with Firebase App Check, simply add the `@FirebaseAppCheck` annotation to the controller method: + +```java +import it.pierfani.firebaseappcheck.FirebaseAppCheck; + +@RestController +public class ExampleController { + + @GetMapping("/protected-endpoint") + @FirebaseAppCheck + public String protectedEndpoint() { + return "This endpoint is protected by Firebase App Check"; + } +} +``` + +### Error Handling + +The library throws `FirebaseAppCheckException` in case of errors during token verification. You can handle this exception in your controller or using a global exception handler. + +### Contributing + +Contributions are welcome! Please open an issue or submit a pull request for any suggestions, bugs, or improvements. + +### License + +This project is licensed under the MIT License. See the `LICENSE` file for details. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..35d6979 --- /dev/null +++ b/pom.xml @@ -0,0 +1,65 @@ + + 4.0.0 + it.pierfani + firebase-appcheck-spring + jar + ${revision} + firebase-appcheck-spring + http://maven.apache.org + + 17 + 17 + 3.0.0 + + + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-aop + ${spring.boot.version} + + + org.springframework.boot + spring-boot-starter-security + ${spring.boot.version} + + + com.auth0 + java-jwt + 4.4.0 + + + com.auth0 + jwks-rsa + 0.22.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 17 + 17 + + + + + + + github + GitHub Packages + https://maven.pkg.github.com/pierfani/firebase-appcheck-spring + + + \ No newline at end of file diff --git a/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheck.java b/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheck.java new file mode 100644 index 0000000..50d4a61 --- /dev/null +++ b/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheck.java @@ -0,0 +1,11 @@ +package it.pierfani.firebaseappcheck; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface FirebaseAppCheck { +} \ No newline at end of file diff --git a/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckAspect.java b/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckAspect.java new file mode 100644 index 0000000..998ddba --- /dev/null +++ b/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckAspect.java @@ -0,0 +1,101 @@ +package it.pierfani.firebaseappcheck; + +import java.security.interfaces.RSAPublicKey; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.JwkProviderBuilder; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; + +import jakarta.servlet.http.HttpServletRequest; + +@Aspect +@Component +public class FirebaseAppCheckAspect { + + private static final String FIREBASE_APP_CHECK_HEADER = "X-Firebase-AppCheck"; + + @Value("${it.pierfani.firebaseappcheck.project-number}") + private String firebaseProjectNumber; + + @Value("${it.pierfani.firebaseappcheck.jwks-url}") + private String firebaseJwksUrl; + + private JwkProvider provider; + + private void initializeProvider() { + if (provider == null) { + provider = new JwkProviderBuilder(firebaseJwksUrl) + .cached(10, 4, TimeUnit.HOURS) + .rateLimited(20, 1, TimeUnit.MINUTES) + .build(); + } + } + + @Around("@annotation(it.pierfani.firebaseappcheck.FirebaseAppCheck)") + public Object checkFirebaseAppCheck(ProceedingJoinPoint joinPoint) throws Throwable { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) + .getRequest(); + String appCheckToken = request.getHeader(FIREBASE_APP_CHECK_HEADER); + + if (appCheckToken == null || appCheckToken.isEmpty()) { + throw new FirebaseAppCheckException("Firebase App Check token is missing"); + } + + try { + verifyToken(appCheckToken); + } catch (FirebaseAppCheckException e) { + throw e; + } catch (Exception e) { + throw new FirebaseAppCheckException("Error verifying Firebase App Check token", e); + } + + return joinPoint.proceed(); + } + + private void verifyToken(String token) { + if (token == null) { + throw new FirebaseAppCheckException("Token is null"); + } + + initializeProvider(); + + try { + DecodedJWT jwt = JWT.decode(token); + RSAPublicKey publicKey = (RSAPublicKey) provider.get(jwt.getKeyId()).getPublicKey(); + + Algorithm algorithm = Algorithm.RSA256(publicKey, null); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer("https://firebaseappcheck.googleapis.com/" + firebaseProjectNumber) + .build(); + + jwt = verifier.verify(token); + + if (jwt.getExpiresAt().before(java.util.Date.from(Instant.now()))) { + throw new FirebaseAppCheckException("Token is expired"); + } + + if (!jwt.getAudience().contains("projects/" + firebaseProjectNumber)) { + throw new FirebaseAppCheckException("Token audience does not match the project"); + } + + } catch (JWTVerificationException e) { + throw new FirebaseAppCheckException("JWT verification failed", e); + } catch (Exception e) { + throw new FirebaseAppCheckException("Error during token verification", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckConfiguration.java b/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckConfiguration.java new file mode 100644 index 0000000..58afbfe --- /dev/null +++ b/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckConfiguration.java @@ -0,0 +1,14 @@ +package it.pierfani.firebaseappcheck; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +@Configuration +@EnableAspectJAutoProxy +public class FirebaseAppCheckConfiguration { + @Bean + public FirebaseAppCheckAspect firebaseAppCheckAspect() { + return new FirebaseAppCheckAspect(); + } +} \ No newline at end of file diff --git a/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckException.java b/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckException.java new file mode 100644 index 0000000..60e4090 --- /dev/null +++ b/src/main/java/it/pierfani/firebaseappcheck/FirebaseAppCheckException.java @@ -0,0 +1,13 @@ +package it.pierfani.firebaseappcheck; + +public class FirebaseAppCheckException extends RuntimeException { + private static final long serialVersionUID = -7043906620236176552L; + + public FirebaseAppCheckException(String message) { + super(message); + } + + public FirebaseAppCheckException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file