├── .github ├── ISSUE_TEMPLATE │ ├── 1_bug.yml │ └── 2_feature_request.yml └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── docs └── .gitignore ├── example ├── .gitignore ├── README.md ├── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ ├── main │ ├── kotlin │ │ ├── Application.kt │ │ └── plugins │ │ │ ├── AppCheck.kt │ │ │ ├── HTTP.kt │ │ │ ├── Routing.kt │ │ │ ├── Security.kt │ │ │ └── Serialization.kt │ └── resources │ │ └── logback.xml │ └── test │ └── kotlin │ └── ApplicationTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── library ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── net │ │ └── freshplatform │ │ └── ktor_server │ │ └── firebase_app_check │ │ ├── FirebaseAppCheck.kt │ │ ├── FirebaseAppCheckExceptions.kt │ │ ├── configurations │ │ ├── FirebaseAppCheckPluginConfiguration.kt │ │ └── FirebaseAppCheckSecureStrategy.kt │ │ ├── service │ │ ├── FirebaseAppCheckTokenVerifierService.kt │ │ └── jwt │ │ │ └── DecodedJwt.kt │ │ └── utils │ │ ├── ApplicationCallExtensions.kt │ │ └── FirebaseAppCheckResponses.kt │ ├── jvmMain │ └── kotlin │ │ └── net │ │ └── freshplatform │ │ └── ktor_server │ │ └── firebase_app_check │ │ ├── FirebaseAppCheckTokenVerifierServiceImpl.kt │ │ └── service │ │ └── FirebaseAppCheckTokenVerifierService.jvm.kt │ ├── jvmTest │ └── kotlin │ │ └── net │ │ └── freshplatform │ │ └── ktor_server │ │ └── firebase_app_check │ │ ├── ApplicationTest.kt │ │ ├── FirebaseAppCheckTokenVerifierServiceMock.kt │ │ └── TestConstants.kt │ └── nativeMain │ └── kotlin │ └── net │ └── freshplatform │ └── ktor_server │ └── firebase_app_check │ └── service │ └── FirebaseAppCheckTokenVerifierService.native.kt ├── scripts └── before-push.sh └── settings.gradle.kts /.github/ISSUE_TEMPLATE/1_bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: | 3 | You found a bug in the library causing your application to crash or 4 | throw an exception, a feature is buggy, unexpected behavior or something looks wrong. 5 | labels: 'bug' 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thank you for using the library! 11 | 12 | - type: checkboxes 13 | attributes: 14 | label: Is there an existing issue for this? 15 | options: 16 | - label: I have searched the [existing issues](https://github.com/freshplatform/ktor-server-firebase-app-check/issues) 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Steps to reproduce 21 | description: Please tell us exactly how to reproduce the problem you are running into. 22 | placeholder: | 23 | 1. ... 24 | 2. ... 25 | 3. ... 26 | validations: 27 | required: true 28 | - type: textarea 29 | attributes: 30 | label: Expected results 31 | description: Please tell us what is expected to happen. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: Actual results 37 | description: Please tell us what is actually happening. 38 | validations: 39 | required: true 40 | - type: textarea 41 | attributes: 42 | label: Code sample 43 | description: | 44 | Please create a minimal reproducible sample that shows the problem 45 | and attach it below between the lines with the backticks. 46 | 47 | To create it, use the [Ktor project generator](https://start.ktor.io/) 48 | 49 | Note: Please do not upload screenshots of text. Instead, use code blocks 50 | or the above mentioned ways to upload your code sample. 51 | value: | 52 |
Code sample 53 | 54 | ```kotlin 55 | [Paste your code here] 56 | ``` 57 | 58 |
59 | validations: 60 | required: true 61 | - type: textarea 62 | attributes: 63 | label: Screenshots or Video 64 | description: | 65 | Upload any screenshots or video of the bug if applicable. 66 | value: | 67 |
68 | Screenshots / Video demonstration 69 | 70 | [Upload media here] 71 | 72 |
73 | - type: textarea 74 | attributes: 75 | label: Logs 76 | description: | 77 | Include the full logs of the commands you are running between the lines 78 | with the backticks below. 79 | 80 | If the logs are too large to be uploaded to GitHub, you may upload 81 | them as a `txt` file or use online tools like https://pastebin.com to 82 | share it. 83 | 84 | Note: Please do not upload screenshots of text. Instead, use code blocks 85 | or the above mentioned ways to upload logs. 86 | value: | 87 |
Logs 88 | 89 | ```console 90 | [Paste your logs here] 91 | ``` 92 | 93 |
94 | - type: textarea 95 | attributes: 96 | label: Gradle version output (optional) 97 | description: | 98 | Please provide the full output of running `gradle --version` 99 | if the issue is related on how the library use Gradle 100 | value: | 101 |
Doctor output 102 | 103 | ```console 104 | [Paste your output here] 105 | ``` 106 | 107 |
-------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest a new idea. 3 | labels: 'enhancement' 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for using the library! 9 | 10 | - type: checkboxes 11 | attributes: 12 | label: Is there an existing issue for this? 13 | description: Please search to see if an issue already exists for this feature request or proposal. 14 | options: 15 | - label: I have searched the [existing issues](https://github.com/freshplatform/ktor-server-firebase-app-check/issues) 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Use case 20 | description: | 21 | Please tell us the problem you are running into that led to you wanting 22 | a new feature. 23 | 24 | Is your feature request related to a problem? Please give a clear and 25 | concise description of what the problem is. 26 | 27 | Describe the alternative solutions you've considered. Is there already a solution that solves this? 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Proposal 33 | description: | 34 | Briefly but precisely describe what you would like the library to be able to do. 35 | 36 | Consider attaching something showing what you are imagining: 37 | * images 38 | * videos 39 | * code samples 40 | validations: 41 | required: true -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | tests: 11 | name: Build and test 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 5 14 | 15 | steps: 16 | - name: Clone Repository 17 | uses: actions/checkout@v4 18 | 19 | # - name: Validate Gradle Wrapper 20 | # uses: gradle/actions/wrapper-validation@v3 21 | 22 | - name: Setup JDK 17 23 | uses: actions/setup-java@v4 24 | with: 25 | java-version: 17 26 | distribution: 'adopt' 27 | # For more info: https://github.com/gradle/actions/blob/main/docs/setup-gradle.md#incompatibility-with-other-caching-mechanisms 28 | # cache: gradle 29 | 30 | - name: Setup Gradle 31 | uses: gradle/actions/setup-gradle@v3 32 | with: 33 | validate-wrappers: true 34 | cache-disabled: false 35 | 36 | - name: Make sure the `./gradlew` is executable 37 | run: chmod +x ./gradlew 38 | 39 | - name: Build & Test with Gradle 40 | run: ./gradlew build --stacktrace 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | bin/ 16 | !**/src/main/**/bin/ 17 | !**/src/test/**/bin/ 18 | 19 | ### IntelliJ IDEA ### 20 | .idea 21 | *.iws 22 | *.iml 23 | *.ipr 24 | out/ 25 | !**/src/main/**/out/ 26 | !**/src/test/**/out/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | .DS_Store -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | [//]: # (## [next]) 6 | ## 0.0.6-dev 7 | * Update to support ktor 3 8 | * Update dependencies: 9 | * kotlin = "2.1.10" 10 | ktor = "3.1.1" 11 | * Move the dependencies's versions to catalog 12 | 13 | ## 0.0.5-dev 14 | * Update the versions to use kotlin `1.9.21` and ktor `2.3.6` 15 | * Prepare the library to be a KMP Library by using the Kotlin Multiplatform Gradle plugin 16 | and add the source sets (`jvmMain` and `commonMain`) 17 | * Share some 18 | * Update `README` by add a status section 19 | * Fix a few typos in the docs 20 | * Update the `build.gradle.kts` to make it less specific to JVM, and remove unused gradle ktor plugin 21 | * Fix the `group` of the dependency 22 | * Update GitHub Main workflow 23 | * Add `before-push.sh` script 24 | * Move the repo owner from `freshtechtips` to `freshplatform` 25 | 26 | ## 0.0.4-dev 27 | * The library is now dev state 28 | * Improve the tests 29 | * Fix typos 30 | 31 | ## 0.0.3-experimental 32 | * **Breaking Change**: Now you don't need to pass the configuration class as a value, just add the properties directly 33 | * **Breaking change**: The `FirebaseAppCheckPlugin` has been moved to the root `kotlin` folder 34 | * Separate the `src` to a module 35 | * Include the `example` in the `settings.gradle.kts` 36 | * Update the `build.gradle.kts` of the `example` 37 | 38 | ## 0.0.2-experimental 39 | * Rename the folder `examples` to `example` and use only one example project 40 | * Use latest version of kotlin `1.9.20` 41 | * Use the latest version of Gradle 42 | 43 | ## 0.0.1-experimental 44 | 45 | * initial experimental release. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | The contributions are more than welcome!
4 | This project will be better with the open-source community help 5 | 6 | There are no guidelines for now. 7 | This page will be updated in the future. 8 | 9 | ## Steps to contributing 10 | 11 | You will need GitHub account as well as git installed and configured with your GitHub account on your machine 12 | 13 | 1. Fork the repository in GitHub 14 | 2. clone the forked repository using `git` 15 | 3. Add the `upstream` repository using: 16 | ``` 17 | git remote add upstream git@github.com:freshplatform/ktor-server-firebase-app-check.git 18 | ``` 19 | 4. Open the project with your favorite IDE, we suggest using [IntelliJ IDEA Community Edition](https://www.jetbrains.com/idea/download/) 20 | 5. Sync the project with Gradle 21 | 6. Create a new git branch and switch to it using: 22 | ``` 23 | git checkout -b your-branch-name 24 | ``` 25 | The `your-branch-name` is your choice 26 | 7. Make your changes 27 | 8. Test them in the [example](./example) and add changes in necessary 28 | 9. Mention the new changes in the [CHANGELOG.md](./CHANGELOG.md) in the next block 29 | 10. When you are done to send your pull request, run: 30 | ``` 31 | git add . 32 | git commit -m "Your commit message" 33 | git push origin your-branch-name 34 | ``` 35 | this will push the new branch to your forked repository 36 | 11. Now you can send your pull request either by following the link that you will get in the command line or open your 37 | forked repository. You will find an option to send the pull request, you can also 38 | open the [Pull Requests](https://github.com/freshplatform/ktor-server-firebase-app-check/pulls) tab and send new pull request 39 | 12. Please wait for the review, and we might ask you to make more changes, then run: 40 | ``` 41 | git add . 42 | git commit -m "Your new commit message" 43 | git push origin your-branch-name 44 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Fresh Platform 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase App Check for Ktor server 2 | 3 | A Ktor server plugin for configuring [Firebase App Check](https://firebase.google.com/products/app-check) easily and with simplicity. 4 | It is **not affiliated** with Firebase or Google and may not be suitable for production use **yet**. 5 | 6 | [//]: # (Note: this repository name might be changed to [ktor-server-guardian](https://github.com/freshplatform/ktor-server-guardian)) 7 | 8 | feel free to share your opinion in the discussions 9 | 10 | [//]: # ([![Build Status](https://travis-ci.org/freshplatform/ktor-server-firebase-app-check.svg?branch=main)](https://travis-ci.org/freshplatform/ktor-server-firebase-app-check)) 11 | [![](https://jitpack.io/v/freshplatform/ktor-server-firebase-app-check.svg)](https://jitpack.io/#freshplatform/ktor-server-firebase-app-check) 12 | [![CI](https://github.com/freshplatform/ktor-server-firebase-app-check/actions/workflows/main.yml/badge.svg)](https://github.com/freshplatform/ktor-server-firebase-app-check/actions/workflows/main.yml) 13 | 14 | ## Table of Contents 15 | 16 | - [About](#about) 17 | - [Installation](#installation) 18 | - [Usage](#usage) 19 | - [Status](#status) 20 | - [Features](#features) 21 | - [Contributing](#contributing) 22 | - [Acknowledgments](#acknowledgments) 23 | 24 | ## About 25 | 26 | Protection for your app’s data and users 27 | App Check is an additional layer of security that helps protect 28 | access to your services by attesting that incoming traffic is 29 | coming from your app, and blocking traffic that 30 | doesn't have valid credentials. It helps protect your backend from abuse, 31 | such as billing fraud, phishing, app impersonation, and data poisoning. 32 | 33 | Broad platform support that you can tailor to your needs 34 | App Check supports Android, iOS, and Web out of the box. For customers who want to support more platforms, 35 | you can integrate your own attestation provider with App Check's custom capabilities. 36 | 37 | Firebase support integrates with a custom backend, the supported SDKs are Node.js, Python, and Go 38 | 39 | This is a ktor plugin that saves you some time to integrate it with your ktor backend 40 | 41 | [![Firebase app check video](https://i.imgur.com/asvY9tu.png)](https://youtu.be/LFz8qdF7xg4?si=V8SJRrkrHdCDZBKU) 42 | 43 | ## Installation 44 | 45 | Use this section to describe how to install your project. For example: 46 | 47 | 1. [Create a new ktor project](https://start.ktor.io/) or use existing one if you already have. 48 | 2. Add jitpack repository to your `build.gradle.kts`: 49 | ```groovy 50 | repositories { 51 | mavenCentral() 52 | maven { 53 | name = "jitpack" 54 | setUrl("https://jitpack.io") 55 | } 56 | } 57 | ``` 58 | 3. Add the dependency: 59 | ```groovy 60 | dependencies { 61 | implementation("com.github.freshtechtips:ktor-server-firebase-app-check:") // use the latest version above 62 | } 63 | 64 | ``` 65 | 4. Configure and install the plugin in the application module, 66 | Pass the following environment variables, 67 | go to your Firebase project settings, in the general tab 68 | 69 | * `FIREBASE_PROJECT_NUMBER` from the Project ID 70 | * `FIREBASE_PROJECT_ID` from the Project number 71 | 72 | ```kotlin 73 | install(FirebaseAppCheckPlugin) { 74 | firebaseProjectNumber = System.getenv("FIREBASE_PROJECT_NUMBER") 75 | firebaseProjectId = System.getenv("FIREBASE_PROJECT_ID") 76 | isShouldVerifyToken = true 77 | secureStrategy = FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes 78 | pluginMessagesBuilder = { configuration -> 79 | // Example of override a response message 80 | FirebaseAppCheckMessages( 81 | configuration, 82 | appCheckIsNotDefinedResponse = mapOf( 83 | "error" to "${configuration.firebaseAppCheckHeaderName} is required" 84 | ), 85 | ) 86 | } 87 | } 88 | ``` 89 | 90 | By default, the plugin runs the app check only when the development is false. 91 | You can override this bypass `isShouldVerifyToken = true` in the configuration 92 | 93 | ## Usage 94 | 95 | You might want to read the [Firebase App Check documentation](https://firebase.google.com/docs/app-check) 96 | 97 | Here's how to use the library: 98 | 99 | * First make sure to use the desired secure strategy in the plugin configuration when you install it, if you want to secure the whole api and the app, 100 | or just specific routes by surrounding them with `protectedRouteWithAppCheck { }` 101 | 102 | 103 | * Secure your routes (optional) if you are 104 | 105 | `secureStrategy = FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes,` 106 | ```kotlin 107 | routing { 108 | get("/") { 109 | call.respondText("Hello World! this route is not using app firebase app check") 110 | } 111 | protectRouteWithAppCheck { 112 | route("/products") { 113 | get("/1") { 114 | call.respondText { "Product 1, Firebase app check" } 115 | } 116 | get("/2") { 117 | call.respondText { "Product 2, Firebase app check" } 118 | } 119 | } 120 | } 121 | get("/test") { 122 | call.respondText { "Tis get test doesn't use firebase app check!" } 123 | } 124 | protectRouteWithAppCheck { 125 | post("/test") { 126 | call.respondText { "Tis post test is protected!" } 127 | } 128 | } 129 | } 130 | ``` 131 | * Send the request and set the header `X-Firebase-AppCheck` with the app check token (jwt). 132 | you can get the token from the Firebase app by checking SDK in the client apps (Android, iOS, macOS, Web) 133 | Or if you just want to test real quick [try this expired token](https://pastebin.com/za2wW8cP). 134 | (please notice to succeed in the test, you must generate the app token from the client app that uses the same project) 135 | 136 | Token for testing purposes: 137 | ``` 138 | eyJraWQiOiJ2Yy1sVEEiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxOjgwMjA4OTE0MjU1OTphbmRyb2lkOjI2ZDhjMDA3ZGVkMDNmODQyYTg4MmEiLCJhdWQiOlsicHJvamVjdHNcLzgwMjA4OTE0MjU1OSIsInByb2plY3RzXC9teW5vdGVzLWViNzE3Il0sInByb3ZpZGVyIjoiZGVidWciLCJpc3MiOiJodHRwczpcL1wvZmlyZWJhc2VhcHBjaGVjay5nb29nbGVhcGlzLmNvbVwvODAyMDg5MTQyNTU5IiwiZXhwIjoxNjk3MTM0NDg3LCJpYXQiOjE2OTcxMzA4ODcsImp0aSI6InZLZERfNTRhQ2tzVmpHV0xBN3d1TjZmWlFUQWRYZzRBWGJhYVBzRUZDV0EifQ.H_LGsCe5I-Z2uAgYU1isDmxQ-6PecdmjEqvkrZp9AWthNhsiMdlVYjUe2DaSmt3lhIlwCJyCh2YooOLvSlFAvdx5n__kB5O5C9Fw-Vw-zjSTOAi6lNB0hi8OEkIJhNgw2b_UipeVFd1I6ICkCdV93Ewr-clv-eDeMIg_b8vr3w6HtypZDVu3hAl6BjfxY9r7cm5eBmHGnOxwb1-flSKRJdBmrh4Bm0_imaDPSHw_rUwCUXHOAM-QfdQ-D4C15L_IJH4X6kT7nm8GMj47rQjr1d6CQZbW3xoIsTJvnpreOR1xyiHZiLydj1cwPt6r2DfmjRL6-tFs2u8c72CcoqQ4hhsJE9ZSk1BHXpnGw6t5PLPWmk-K7wCrn49U20SYsbOGzyMmwPs-nRyYL3QeV00brlaQWFN7pnjquYHtgJZgkVZlIe1Hh_8mBzTSLygc3-0Xw3FKf1X6p_jOyyN7Qi3Wf5GHvBdp_sYyuBtXMYVwhKQ56lYBX3waLP0KHSiDiDUW 139 | ``` 140 | 141 | ## Status 142 | 143 | Is this library ready for production use?? 144 | The short answer is yes, the long one is no, because we do have 145 | plans to change too many things that are not related to how the logic 146 | of Firebase App Check works, the logic will likely still be the same 147 | 148 | It will only update when the official one updates. 149 | What could be changes then? 150 | only cosmetic changes to change the way to use the library, 151 | for example, we might change the library name to `KotlinGuardian` or `KtorGuardian` 152 | and add support for different providers to use instead of firebase alone, add more features 153 | and things, so we expect to do too many breaking changes but other than that 154 | 155 | The library should work just fine as long it follows the official docs of firebase 156 | 157 | ## Features 158 | List the key features of the library 159 | 160 | ```markdown 161 | ## Features 162 | 163 | — Easy to use and customizable 164 | - Different secure strategies 165 | - Caching and rate limiting for the public key of the Firebase app check 166 | - Handle different errors 167 | ``` 168 | 169 | ## Contributing 170 | 171 | We welcome contributions! 172 | 173 | Please follow these guidelines when contributing to this project. 174 | See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. 175 | 176 | ## Acknowledgments 177 | 178 | - Thanks to [Firebase](https://firebase.google.com/) 179 | for updating the [documentation](https://firebase.google.com/docs/app-check/custom-resource-backend#other) 180 | and showing us how to contact their APIs 181 | - Thanks to JetBrains for Kotlin, IntelliJ IDEA Community Edition, and Ktor for server 182 | - Thanks to the open-source community 183 | - Thanks for [Auth0](https://developer.auth0.com/) and [Jwt.io](https://jwt.io/) for the JWT libraries 184 | and the debugger 185 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) apply false 3 | alias(libs.plugins.kotlin.multiplatform) apply false 4 | alias(libs.plugins.kotlinx.serialization) apply false 5 | } 6 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreshKernel/ktor-server-firebase-app-check/657d28f586b022eaf0a489e25abf2182c75bdbb1/docs/.gitignore -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | bin/ 16 | !**/src/main/**/bin/ 17 | !**/src/test/**/bin/ 18 | 19 | ### IntelliJ IDEA ### 20 | .idea 21 | *.iws 22 | *.iml 23 | *.ipr 24 | out/ 25 | !**/src/main/**/out/ 26 | !**/src/test/**/out/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | .DS_Store -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Firebase App Check Example 2 | 3 | A simple project that showcases how to use the Firebase App Check 4 | library 5 | 6 | ## Table of Contents 7 | 8 | - [Installation](#installation) 9 | 10 | ## Installation 11 | 1. Clone the repository 12 | 2. Make sure you have Java 11 as a minimum, 17 is recommended 13 | 3. If you want to try this example, you have two options. Either 14 | go to build.gradle.kts and remove the `mavenLocal()` 15 | or if you already publish the library to your mavenLocal then 16 | remove the jitpack repository 17 | 4. Pass the following environment variables, 18 | go to your firebase project settings, in general tab 19 | 20 | * `FIREBASE_PROJECT_NUMBER` from the Project ID 21 | * `FIREBASE_PROJECT_ID` from the Project number 22 | 5. The routes: 23 | 24 | Get `/`: Unprotected route 25 | 26 | Get `/products`: Protected routes with two products `/1` and `/2` 27 | 28 | Get `/test`: Unprotected route 29 | 30 | Post `/test`: Protected route 31 | 6. Send request and set the header `X-Firebase-AppCheck` with the app check token (jwt). 32 | you can get the token from firebase app to check sdk in the client apps (Android, iOS, macOS, Web) 33 | Or if you just want to test real quick [try this expired token](https://pastebin.com/za2wW8cP). 34 | (please notice the success of the test requires valid setup, 35 | you must generate the app token from client app that uses the same project) 36 | 37 | Token for testing purposes: 38 | ``` 39 | eyJraWQiOiJ2Yy1sVEEiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxOjgwMjA4OTE0MjU1OTphbmRyb2lkOjI2ZDhjMDA3ZGVkMDNmODQyYTg4MmEiLCJhdWQiOlsicHJvamVjdHNcLzgwMjA4OTE0MjU1OSIsInByb2plY3RzXC9teW5vdGVzLWViNzE3Il0sInByb3ZpZGVyIjoiZGVidWciLCJpc3MiOiJodHRwczpcL1wvZmlyZWJhc2VhcHBjaGVjay5nb29nbGVhcGlzLmNvbVwvODAyMDg5MTQyNTU5IiwiZXhwIjoxNjk3MTM0NDg3LCJpYXQiOjE2OTcxMzA4ODcsImp0aSI6InZLZERfNTRhQ2tzVmpHV0xBN3d1TjZmWlFUQWRYZzRBWGJhYVBzRUZDV0EifQ.H_LGsCe5I-Z2uAgYU1isDmxQ-6PecdmjEqvkrZp9AWthNhsiMdlVYjUe2DaSmt3lhIlwCJyCh2YooOLvSlFAvdx5n__kB5O5C9Fw-Vw-zjSTOAi6lNB0hi8OEkIJhNgw2b_UipeVFd1I6ICkCdV93Ewr-clv-eDeMIg_b8vr3w6HtypZDVu3hAl6BjfxY9r7cm5eBmHGnOxwb1-flSKRJdBmrh4Bm0_imaDPSHw_rUwCUXHOAM-QfdQ-D4C15L_IJH4X6kT7nm8GMj47rQjr1d6CQZbW3xoIsTJvnpreOR1xyiHZiLydj1cwPt6r2DfmjRL6-tFs2u8c72CcoqQ4hhsJE9ZSk1BHXpnGw6t5PLPWmk-K7wCrn49U20SYsbOGzyMmwPs-nRyYL3QeV00brlaQWFN7pnjquYHtgJZgkVZlIe1Hh_8mBzTSLygc3-0Xw3FKf1X6p_jOyyN7Qi3Wf5GHvBdp_sYyuBtXMYVwhKQ56lYBX3waLP0KHSiDiDUW 40 | ``` -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.jvm) 3 | application 4 | alias(libs.plugins.kotlinx.serialization) 5 | } 6 | 7 | group = "net.freshplatform" 8 | version = "0.0.1" 9 | 10 | application { 11 | mainClass.set("ApplicationKt") 12 | 13 | val isDevelopment: Boolean = project.ext.has("development") 14 | applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") 15 | } 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | implementation(libs.ktor.server.core) 23 | implementation(libs.ktor.server.auth.jvm) 24 | implementation(libs.ktor.server.auth.jwt) 25 | implementation(libs.ktor.server.host) 26 | implementation(libs.ktor.server.status.pages) 27 | implementation(libs.ktor.server.compression) 28 | implementation(libs.ktor.server.content.negotiation) 29 | implementation(libs.ktor.server.netty) 30 | implementation(libs.ktor.serialization.kotlinx.json) 31 | implementation(libs.ktor.server.caching.headers) 32 | implementation(libs.logback.classic) 33 | 34 | testImplementation(libs.ktor.server.test.jvm) 35 | testImplementation(libs.kotlin.test) 36 | implementation(project(":library")) 37 | } -------------------------------------------------------------------------------- /example/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreshKernel/ktor-server-firebase-app-check/657d28f586b022eaf0a489e25abf2182c75bdbb1/example/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /example/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /example/src/main/kotlin/Application.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.server.application.Application 2 | import io.ktor.server.application.serverConfig 3 | import io.ktor.server.engine.connector 4 | import io.ktor.server.engine.embeddedServer 5 | import io.ktor.server.netty.Netty 6 | import plugins.configureHTTP 7 | import plugins.configureRouting 8 | import plugins.configureSecurity 9 | import plugins.configureSerialization 10 | 11 | fun main() { 12 | embeddedServer( 13 | factory = Netty, 14 | rootConfig = serverConfig { 15 | developmentMode = true 16 | watchPaths = listOf("classes", "resources") 17 | module(Application::module) 18 | }, 19 | configure = { 20 | connector { 21 | host = "0.0.0.0" 22 | port = 12345 23 | } 24 | } 25 | ).start(wait = true) 26 | } 27 | 28 | fun Application.module() { 29 | configureHTTP() 30 | configureSerialization() 31 | configureSecurity() 32 | configureRouting() 33 | } 34 | -------------------------------------------------------------------------------- /example/src/main/kotlin/plugins/AppCheck.kt: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import io.ktor.server.application.* 4 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckPlugin 5 | import net.freshplatform.ktor_server.firebase_app_check.configurations.FirebaseAppCheckSecureStrategy 6 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckTokenVerifierServiceImpl 7 | import net.freshplatform.ktor_server.firebase_app_check.utils.FirebaseAppCheckMessages 8 | 9 | fun Application.configureAppCheck() { 10 | install(FirebaseAppCheckPlugin) { 11 | firebaseProjectNumber = System.getenv("FIREBASE_PROJECT_NUMBER") 12 | ?: throw MissingEnvironmentVariableException("FIREBASE_PROJECT_NUMBER") 13 | firebaseProjectId = System.getenv("FIREBASE_PROJECT_ID") 14 | ?: throw MissingEnvironmentVariableException("FIREBASE_PROJECT_ID") 15 | isShouldVerifyToken = true 16 | serviceImpl = FirebaseAppCheckTokenVerifierServiceImpl() 17 | secureStrategy = FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes 18 | pluginMessagesBuilder = { configuration -> 19 | // Example of override a response message 20 | FirebaseAppCheckMessages( 21 | configuration, 22 | appCheckIsNotDefinedResponse = mapOf( 23 | "error" to "${configuration.firebaseAppCheckHeaderName} is required" 24 | ), 25 | ) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /example/src/main/kotlin/plugins/HTTP.kt: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import io.ktor.http.* 4 | import io.ktor.http.content.* 5 | import io.ktor.server.application.* 6 | import io.ktor.server.plugins.cachingheaders.* 7 | import io.ktor.server.plugins.compression.* 8 | 9 | fun Application.configureHTTP() { 10 | install(Compression) { 11 | gzip { 12 | priority = 1.0 13 | } 14 | deflate { 15 | priority = 10.0 16 | minimumSize(1024) // condition 17 | } 18 | } 19 | install(CachingHeaders) { 20 | options { _, outgoingContent -> 21 | when (outgoingContent.contentType?.withoutParameters()) { 22 | ContentType.Text.CSS -> CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60)) 23 | else -> null 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/src/main/kotlin/plugins/Routing.kt: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import io.ktor.http.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.plugins.statuspages.* 6 | import io.ktor.server.response.* 7 | import io.ktor.server.routing.* 8 | import net.freshplatform.ktor_server.firebase_app_check.utils.protectRouteWithAppCheck 9 | 10 | class MissingEnvironmentVariableException(variableName: String) : 11 | RuntimeException("The required environment variable '$variableName' is missing.") 12 | 13 | fun Application.configureRouting() { 14 | install(StatusPages) { 15 | exception { call, cause -> 16 | call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError) 17 | } 18 | } 19 | 20 | routing { 21 | get("/") { 22 | call.respondText("Hello World! this route is not using app firebase app check") 23 | } 24 | protectRouteWithAppCheck { 25 | route("/products") { 26 | get("/1") { 27 | call.respondText { "Product 1, Firebase app check" } 28 | } 29 | get("/2") { 30 | call.respondText { "Product 2, Firebase app check" } 31 | } 32 | } 33 | } 34 | route("/products") { 35 | get("/3") { 36 | call.respondText { "Product 2, Firebase app check is not required." } 37 | } 38 | } 39 | get("/test") { 40 | call.respondText { "This get /test doesn't use firebase app check!" } 41 | } 42 | protectRouteWithAppCheck { 43 | post("/test") { 44 | call.respondText { "This post /test is protected!" } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/src/main/kotlin/plugins/Security.kt: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import io.ktor.server.application.* 4 | 5 | fun Application.configureSecurity() { 6 | configureAppCheck() 7 | } -------------------------------------------------------------------------------- /example/src/main/kotlin/plugins/Serialization.kt: -------------------------------------------------------------------------------- 1 | package plugins 2 | 3 | import io.ktor.serialization.kotlinx.json.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.plugins.contentnegotiation.* 6 | import io.ktor.server.response.* 7 | import io.ktor.server.routing.* 8 | 9 | fun Application.configureSerialization() { 10 | install(ContentNegotiation) { 11 | json() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/test/kotlin/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.server.testing.* 2 | import kotlin.test.Test 3 | 4 | class ApplicationTest { 5 | @Test 6 | fun testRoot() = testApplication { 7 | // application { 8 | // configureRouting() 9 | // } 10 | // client.get("/").apply { 11 | // assertEquals(HttpStatusCode.OK, status) 12 | // assertEquals("Hello World!", bodyAsText()) 13 | // } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.10" 3 | ktor = "3.1.1" 4 | 5 | library = "0.0.6-dev" 6 | 7 | # Example 8 | logback = "1.5.4" 9 | 10 | # Library 11 | auth0-java-jwt = "4.4.0" 12 | auth0-java-jwksRsa = "0.22.1" 13 | kotlinx-coroutines = "1.10.1" 14 | 15 | [libraries] 16 | # Library 17 | ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } 18 | ktor-server-test-jvm = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } 19 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 20 | kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 21 | # Example 22 | ktor-server-auth-jvm = { module = "io.ktor:ktor-server-auth-jvm", version.ref = "ktor" } 23 | ktor-server-auth-jwt = { module = "io.ktor:ktor-server-auth-jwt-jvm", version.ref = "ktor" } 24 | ktor-server-host = { module = "io.ktor:ktor-server-host-common-jvm", version.ref = "ktor" } 25 | ktor-server-status-pages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" } 26 | ktor-server-compression = { module = "io.ktor:ktor-server-compression", version.ref = "ktor" } 27 | ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation-jvm", version.ref = "ktor" } 28 | ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" } 29 | ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json-jvm", version.ref = "ktor" } 30 | ktor-server-caching-headers = { module = "io.ktor:ktor-server-caching-headers-jvm", version.ref = "ktor" } 31 | logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } 32 | 33 | # JVM 34 | auth0-java-jwt = { module = "com.auth0:java-jwt", version.ref = "auth0-java-jwt" } 35 | auth0-java-jwksRsa = { module = "com.auth0:jwks-rsa", version.ref = "auth0-java-jwksRsa" } 36 | 37 | [plugins] 38 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 39 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 40 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FreshKernel/ktor-server-firebase-app-check/657d28f586b022eaf0a489e25abf2182c75bdbb1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | before_install: 4 | - ./gradlew build -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.multiplatform) 3 | id("maven-publish") 4 | } 5 | 6 | group = "net.freshplatform" 7 | version = libs.versions.library.get() 8 | description = 9 | "A Ktor server plugin for configuring Firebase App Check easily and with simplicity. It is not affiliated with Firebase or Google and may not be suitable for production use yet." 10 | 11 | kotlin { 12 | jvm() 13 | 14 | sourceSets { 15 | val commonMain by getting { 16 | dependencies { 17 | implementation(libs.ktor.server.core) 18 | implementation(libs.kotlinx.coroutines) 19 | } 20 | } 21 | val commonTest by getting { 22 | dependencies { 23 | implementation(libs.kotlin.test) 24 | } 25 | } 26 | val jvmMain by getting { 27 | dependencies { 28 | implementation(libs.ktor.server.core) 29 | implementation(libs.auth0.java.jwt) 30 | implementation(libs.auth0.java.jwksRsa) 31 | } 32 | } 33 | val jvmTest by getting { 34 | dependencies { 35 | implementation(libs.ktor.server.test.jvm) 36 | } 37 | } 38 | } 39 | } 40 | 41 | //publishing { 42 | // 43 | // val jitpackGroupId = "com.github.freshplatform" 44 | // 45 | // publications { 46 | // create("jitpack") { 47 | // from(components["java"]) 48 | // 49 | // groupId = jitpackGroupId 50 | // artifactId = "ktor-server-firebase-app-check" 51 | // version = project.version.toString() 52 | // } 53 | // } 54 | // 55 | // repositories { 56 | // maven { 57 | // name = "jitpack" 58 | // setUrl("https://jitpack.io") 59 | // content { includeGroup(jitpackGroupId) } 60 | // } 61 | // } 62 | //} 63 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/FirebaseAppCheck.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check 2 | 3 | import io.ktor.server.application.ApplicationCallPipeline 4 | import io.ktor.server.application.BaseApplicationPlugin 5 | import io.ktor.server.application.call 6 | import io.ktor.server.request.uri 7 | import io.ktor.util.AttributeKey 8 | import net.freshplatform.ktor_server.firebase_app_check.configurations.FirebaseAppCheckPluginConfiguration 9 | import net.freshplatform.ktor_server.firebase_app_check.configurations.FirebaseAppCheckSecureStrategy 10 | import net.freshplatform.ktor_server.firebase_app_check.service.FirebaseAppCheckTokenVerifierServiceImpl 11 | import net.freshplatform.ktor_server.firebase_app_check.utils.verifyAppTokenRequest 12 | 13 | /** 14 | * A Ktor server plugin for configuring Firebase App Check easily and with simplicity. 15 | * It is not affiliated with Firebase or Google and may not be suitable for production use yet. 16 | * This plugin is designed to facilitate the setup of Firebase App Check within a Ktor application. 17 | * It requires the following configurations: `firebaseProjectNumber` and `firebaseProjectId`. 18 | * 19 | * @param config The configuration object that holds Firebase App Check settings. 20 | */ 21 | class FirebaseAppCheckPlugin( 22 | internal val config: FirebaseAppCheckPluginConfiguration 23 | ) { 24 | companion object Plugin : 25 | BaseApplicationPlugin { 26 | override val key: AttributeKey 27 | get() = AttributeKey("FirebaseAppCheck") 28 | 29 | override fun install( 30 | pipeline: ApplicationCallPipeline, 31 | configure: FirebaseAppCheckPluginConfiguration.() -> Unit 32 | ): FirebaseAppCheckPlugin { 33 | val configuration = FirebaseAppCheckPluginConfiguration( 34 | serviceImpl = FirebaseAppCheckTokenVerifierServiceImpl() 35 | ) 36 | .apply(configure) 37 | require(configuration.firebaseProjectNumber.isNotBlank()) { 38 | "The firebase project number should not be blank." 39 | } 40 | require(configuration.firebaseProjectId.isNotBlank()) { 41 | "The firebase project id should not be blank." 42 | } 43 | 44 | val isShouldVerifyToken = configuration.isShouldVerifyToken(pipeline.developmentMode) 45 | val secureStrategy = configuration.secureStrategy 46 | if (isShouldVerifyToken && secureStrategy !is FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes) { 47 | pipeline.intercept(ApplicationCallPipeline.Call) { 48 | 49 | when (secureStrategy) { 50 | FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes -> { 51 | return@intercept 52 | } 53 | 54 | FirebaseAppCheckSecureStrategy.ProtectAll -> { 55 | // Do nothing (don't return) 56 | } 57 | 58 | is FirebaseAppCheckSecureStrategy.ProtectRoutesByPaths -> { 59 | val uri = call.request.uri 60 | if (!secureStrategy.routesPaths.contains(uri)) { 61 | return@intercept 62 | } 63 | } 64 | } 65 | 66 | 67 | call.verifyAppTokenRequest() 68 | } 69 | } 70 | return FirebaseAppCheckPlugin(configuration) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/FirebaseAppCheckExceptions.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check 2 | 3 | /** 4 | * A custom exception class for Firebase App Check-related errors. 5 | * 6 | * @param message The exception message describing the error. 7 | */ 8 | open class FirebaseAppCheckException( 9 | override val message: String 10 | ): Exception(message) 11 | 12 | /** 13 | * Enum that defines possible error types when fetching a public key for Firebase App Check. 14 | */ 15 | enum class FirebaseAppCheckFetchPublicKeyErrorType { 16 | SigningKeyNotFound, 17 | NetworkError, 18 | RateLimitReached, 19 | JwkError, 20 | UnknownError 21 | } 22 | 23 | /** 24 | * An exception class for errors related to fetching a public key for Firebase App Check. 25 | * 26 | * @param message The exception message describing the error. 27 | * @param errorType The specific error type related to the exception. 28 | */ 29 | class FirebaseAppCheckFetchPublicKeyException( 30 | override val message: String, 31 | val errorType: FirebaseAppCheckFetchPublicKeyErrorType 32 | ): FirebaseAppCheckException(message) 33 | 34 | /** 35 | * Enum that defines possible error types when verifying a JWT for Firebase App Check. 36 | */ 37 | enum class FirebaseAppCheckVerifyJwtErrorType { 38 | TokenExpired, 39 | GenericJwtVerificationError, 40 | TokenIsNotValid, 41 | HeaderTypeIsNotJwt, 42 | TokenAlgorithmIsNotCorrect, 43 | TokenSignatureVerificationInvalid, 44 | TokenMissingClaim, 45 | TokenIncorrectClaim, 46 | UnknownError, 47 | } 48 | 49 | /** 50 | * An exception class for errors related to verifying a JWT for Firebase App Check. 51 | * 52 | * @param message The exception message describing the error. 53 | * @param errorType The specific error type related to the exception. 54 | */ 55 | class FirebaseAppCheckVerifyJwtException( 56 | override val message: String, 57 | val errorType: FirebaseAppCheckVerifyJwtErrorType 58 | ): FirebaseAppCheckException(message) -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/configurations/FirebaseAppCheckPluginConfiguration.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check.configurations 2 | 3 | import io.ktor.http.* 4 | import io.ktor.server.application.* 5 | import io.ktor.server.response.* 6 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckFetchPublicKeyErrorType 7 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckFetchPublicKeyErrorType.* 8 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckFetchPublicKeyException 9 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckVerifyJwtErrorType 10 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckVerifyJwtErrorType.* 11 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckVerifyJwtException 12 | import net.freshplatform.ktor_server.firebase_app_check.service.FirebaseAppCheckTokenVerifierService 13 | import net.freshplatform.ktor_server.firebase_app_check.service.jwt.DecodedJwt 14 | import net.freshplatform.ktor_server.firebase_app_check.utils.FirebaseAppCheckMessages 15 | import net.freshplatform.ktor_server.firebase_app_check.utils.protectRouteWithAppCheck 16 | 17 | /** 18 | * Configuration class for Firebase App Check plugin. 19 | * It defines the settings required to configure Firebase App Check for a Ktor application. 20 | * @property firebaseProjectId The Firebase project ID (required). 21 | * @property firebaseProjectNumber The Firebase project number (required). 22 | * @property isShouldVerifyToken If set, overrides the default behavior of when to verify tokens. 23 | * By default, the firebase app check run only if the development mode of ktor server is false 24 | * * if you want to override this behavior, please pass a value to [isShouldVerifyToken] 25 | * @property firebaseAppCheckHeaderName The name of the header used to pass the Firebase App Check token. 26 | * it's already set by default, but if you want to change it, for some reason, you can pass the value you want 27 | * @property firebaseAppCheckApiBaseUrl The base URL for Firebase App Check API. 28 | * it's already set by default, but if you want to change it, for some reason, you can pass the value you want 29 | * @property firebaseAppCheckPublicJwtSetUrl The URL for fetching the public JWT set. 30 | * it's already set by default, but if you want to change it, for some reason, you can pass the value you want 31 | * @property secureStrategy The strategy used to secure specific routes. by default, it uses 32 | * [FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes] 33 | * if you want to secure the whole app use [FirebaseAppCheckSecureStrategy.ProtectAll] for all the requests 34 | * if you want a specific routes in the app use [FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes] 35 | * and then protect the routes in the routing by surround them with [protectRouteWithAppCheck] 36 | * if you want to protect routes by the path of the route as string use 37 | * [FirebaseAppCheckSecureStrategy.ProtectRoutesByPaths] 38 | * @property additionalSecurityCheck A function to perform additional security checks on the decoded JWT. 39 | * @property afterSecurityCheck A function to execute after performing security checks. 40 | * @property errorBuilder A function to build responses for different error scenarios. 41 | */ 42 | class FirebaseAppCheckPluginConfiguration( 43 | var firebaseProjectId: String = "", 44 | var firebaseProjectNumber: String = "", 45 | var isShouldVerifyToken: Boolean? = null, 46 | var firebaseAppCheckHeaderName: String = "X-Firebase-AppCheck", 47 | var firebaseAppCheckApiBaseUrl: String = "https://firebaseappcheck.googleapis.com", 48 | var firebaseAppCheckPublicKeyUrl: String = "${firebaseAppCheckApiBaseUrl}/v1/jwks", 49 | var secureStrategy: FirebaseAppCheckSecureStrategy = FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes, 50 | var additionalSecurityCheck: suspend (decodedJwt: DecodedJwt) -> Boolean = { 51 | true 52 | }, 53 | var afterSecurityCheck: suspend (decodedJwt: DecodedJwt) -> Unit = {}, 54 | // var consumeTheTokenAfterUsingIt: Boolean = false 55 | var errorBuilder: suspend (e: Exception, call: ApplicationCall, pluginConfig: FirebaseAppCheckPluginConfiguration) -> Unit 56 | = { e, call, pluginConfig -> 57 | val pluginMessages = pluginConfig.pluginMessagesBuilder(pluginConfig) 58 | when (e) { 59 | is FirebaseAppCheckVerifyJwtException -> { 60 | when (e.errorType) { 61 | TokenExpired -> { 62 | call.respond( 63 | status = HttpStatusCode.Unauthorized, 64 | message = pluginMessages.tokenExpiredResponse 65 | ) 66 | } 67 | 68 | GenericJwtVerificationError -> { 69 | call.respond( 70 | status = HttpStatusCode.InternalServerError, 71 | message = pluginMessages.genericJwtVerificationErrorResponse 72 | ) 73 | } 74 | 75 | TokenIsNotValid -> { 76 | call.respond( 77 | status = HttpStatusCode.Unauthorized, 78 | message = pluginMessages.tokenIsNotValidResponse 79 | ) 80 | } 81 | 82 | HeaderTypeIsNotJwt -> { 83 | call.respond( 84 | status = HttpStatusCode.Unauthorized, 85 | message = pluginMessages.headerTypeIsNotJwtResponse 86 | ) 87 | } 88 | 89 | TokenAlgorithmIsNotCorrect -> { 90 | call.respond( 91 | status = HttpStatusCode.Unauthorized, 92 | message = pluginMessages.tokenAlgorithmIsNotCorrectResponse 93 | ) 94 | } 95 | 96 | TokenSignatureVerificationInvalid -> { 97 | call.respond( 98 | status = HttpStatusCode.Unauthorized, 99 | message = pluginMessages.tokenSignatureVerificationInvalidResponse 100 | ) 101 | } 102 | 103 | TokenMissingClaim -> { 104 | call.respond( 105 | status = HttpStatusCode.Unauthorized, 106 | message = pluginMessages.tokenMissingClaimResponse 107 | ) 108 | } 109 | 110 | TokenIncorrectClaim -> { 111 | call.respond( 112 | status = HttpStatusCode.Unauthorized, 113 | message = pluginMessages.tokenIncorrectClaimResponse 114 | ) 115 | } 116 | 117 | FirebaseAppCheckVerifyJwtErrorType.UnknownError -> { 118 | call.respond( 119 | status = HttpStatusCode.Unauthorized, 120 | message = pluginMessages.verifyJwtUnhandledExceptionResponse 121 | ) 122 | } 123 | } 124 | } 125 | 126 | is FirebaseAppCheckFetchPublicKeyException -> { 127 | when (e.errorType) { 128 | SigningKeyNotFound -> { 129 | call.respond( 130 | status = HttpStatusCode.NotFound, 131 | message = pluginMessages.signingKeyNotFoundResponse 132 | ) 133 | } 134 | 135 | NetworkError -> { 136 | call.respond( 137 | status = HttpStatusCode.BadGateway, 138 | message = pluginMessages.networkErrorResponse 139 | ) 140 | } 141 | 142 | RateLimitReached -> { 143 | call.respond( 144 | status = HttpStatusCode.TooManyRequests, 145 | message = pluginMessages.rateLimitReachedResponse 146 | ) 147 | } 148 | 149 | JwkError -> { 150 | call.respond( 151 | status = HttpStatusCode.InternalServerError, 152 | message = pluginMessages.jwkErrorResponse 153 | ) 154 | } 155 | 156 | FirebaseAppCheckFetchPublicKeyErrorType.UnknownError -> { 157 | call.respond( 158 | status = HttpStatusCode.InternalServerError, 159 | message = pluginMessages.fetchPublicKeyUnknownErrorResponse 160 | ) 161 | } 162 | } 163 | } 164 | // You can also handle it using the StatusPage ktor server plugin. 165 | // but we shouldn't handle all the exceptions in that case 166 | else -> { 167 | call.respond( 168 | status = HttpStatusCode.InternalServerError, 169 | message = pluginMessages.unknownErrorResponse 170 | ) 171 | } 172 | } 173 | }, 174 | var pluginMessagesBuilder: (pluginConfiguration: FirebaseAppCheckPluginConfiguration) -> FirebaseAppCheckMessages = { 175 | FirebaseAppCheckMessages( 176 | pluginConfiguration = it 177 | ) 178 | }, 179 | var serviceImpl: FirebaseAppCheckTokenVerifierService, 180 | ) { 181 | 182 | /** 183 | * Determines whether token verification should be performed based on the environment (developmentMode). 184 | * 185 | * @param isDevMode A boolean indicating whether dev mode is enabled or not. 186 | * @return `true` if token verification should be performed; otherwise, `false`. 187 | */ 188 | fun isShouldVerifyToken(isDevMode: Boolean): Boolean = 189 | isShouldVerifyToken ?: !isDevMode 190 | 191 | } -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/configurations/FirebaseAppCheckSecureStrategy.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check.configurations 2 | 3 | import net.freshplatform.ktor_server.firebase_app_check.utils.protectRouteWithAppCheck 4 | 5 | /** 6 | * A sealed class that defines different strategies for securing routes with Firebase App Check. 7 | * If you want to secure the whole app use [FirebaseAppCheckSecureStrategy.ProtectAll] for all the requests 8 | * if you want a specific routes in the app use [FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes] 9 | * * and then protect the routes in the routing by surround them with [protectRouteWithAppCheck] 10 | * if you want to protect routes by the path of the route as string use 11 | * [FirebaseAppCheckSecureStrategy.ProtectRoutesByPaths] 12 | */ 13 | sealed class FirebaseAppCheckSecureStrategy { 14 | data object ProtectAll : FirebaseAppCheckSecureStrategy() 15 | data class ProtectRoutesByPaths( 16 | val routesPaths: List 17 | ) : FirebaseAppCheckSecureStrategy() 18 | 19 | data object ProtectSpecificRoutes : FirebaseAppCheckSecureStrategy() 20 | } -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/service/FirebaseAppCheckTokenVerifierService.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check.service 2 | 3 | import net.freshplatform.ktor_server.firebase_app_check.service.jwt.DecodedJwt 4 | import kotlin.time.Duration 5 | import kotlin.time.Duration.Companion.hours 6 | 7 | 8 | /** 9 | * Configuration data class for fetching Firebase App Check public keys. This class 10 | * encapsulates various configurations, including cache and rate limiting settings. 11 | * 12 | * @param cacheConfiguration Configuration for the cache of public keys. 13 | * @param rateLimitedConfig Configuration for rate limiting of key fetch requests. 14 | */ 15 | data class FetchFirebaseAppCheckPublicKeyConfig( 16 | val cacheConfiguration: FetchFirebaseAppCheckPublicKeyCacheConfig = FetchFirebaseAppCheckPublicKeyCacheConfig(), 17 | val rateLimitedConfig: FetchFirebaseAppCheckPublicKeyRateLimitedConfig = FetchFirebaseAppCheckPublicKeyRateLimitedConfig(), 18 | ) 19 | 20 | /** 21 | * Configuration data class for the cache of public keys. This class defines the size 22 | * of the cache and the duration for keys to expire within the cache. 23 | * 24 | * @param cacheSize The size of the cache for public keys. 25 | * @param expiresIn The duration for public keys to expire in the cache. 26 | */ 27 | data class FetchFirebaseAppCheckPublicKeyCacheConfig( 28 | val cacheSize: Long = 10, 29 | val expiresIn: Duration = 24.hours, 30 | ) 31 | 32 | /** 33 | * Configuration data class for rate limiting of key fetch requests. 34 | * 35 | * @param enabled 36 | */ 37 | data class FetchFirebaseAppCheckPublicKeyRateLimitedConfig( 38 | val enabled: Boolean = true 39 | ) 40 | 41 | /** 42 | * Object containing functions for fetching and verifying Firebase App Check tokens. 43 | */ 44 | interface FirebaseAppCheckTokenVerifierService { 45 | 46 | /** 47 | * Suspended function to verify a Firebase App Check token. 48 | * 49 | * @param firebaseAppCheckTokenJwt The JWT string to be verified. 50 | * @param firebaseProjectId The Firebase project ID. 51 | * @param firebaseProjectNumber The Firebase project number. 52 | * @param issuerBaseUrl The base URL of the Firebase App Check issuer. 53 | * @return The verified Decoded JWT. 54 | */ 55 | suspend fun verifyFirebaseAppCheckToken( 56 | firebaseAppCheckTokenJwt: String, 57 | firebaseProjectId: String, 58 | firebaseProjectNumber: String, 59 | issuerBaseUrl: String, 60 | publicKeyUrl: String 61 | ): DecodedJwt 62 | } 63 | 64 | expect class FirebaseAppCheckTokenVerifierServiceImpl(): FirebaseAppCheckTokenVerifierService -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/service/jwt/DecodedJwt.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check.service.jwt 2 | 3 | 4 | data class DecodedJwt( 5 | val token: String, 6 | ) -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/utils/ApplicationCallExtensions.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check.utils 2 | 3 | import io.ktor.http.HttpStatusCode 4 | import io.ktor.server.application.ApplicationCall 5 | import io.ktor.server.application.ApplicationCallPipeline 6 | import io.ktor.server.application.call 7 | import io.ktor.server.application.plugin 8 | import io.ktor.server.request.header 9 | import io.ktor.server.response.respond 10 | import io.ktor.server.routing.Route 11 | import io.ktor.server.routing.RouteSelector 12 | import io.ktor.server.routing.RouteSelectorEvaluation 13 | import io.ktor.server.routing.RoutingResolveContext 14 | import io.ktor.server.routing.application 15 | import io.ktor.server.routing.intercept 16 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckPlugin 17 | import net.freshplatform.ktor_server.firebase_app_check.configurations.FirebaseAppCheckSecureStrategy 18 | 19 | /** 20 | * A suspended function that verifies an incoming Firebase App Check token for an [ApplicationCall]. 21 | * This function performs comprehensive verification checks on the provided token to ensure its validity and security. 22 | * 23 | * It also handles exceptions by default and forwards them to the error builder, along with request information. 24 | */ 25 | suspend fun ApplicationCall.verifyAppTokenRequest() { 26 | val pluginConfig = application.plugin(FirebaseAppCheckPlugin).config 27 | val call = this 28 | val pluginMessages = pluginConfig.pluginMessagesBuilder(pluginConfig) 29 | val firebaseAppCheckToken = call.request.header(pluginConfig.firebaseAppCheckHeaderName) 30 | if (firebaseAppCheckToken == null) { 31 | call.respond( 32 | status = HttpStatusCode.Unauthorized, 33 | message = pluginMessages.appCheckIsNotDefinedResponse, 34 | ) 35 | return 36 | } 37 | 38 | if (firebaseAppCheckToken.isBlank()) { 39 | call.respond( 40 | status = HttpStatusCode.Unauthorized, 41 | message = pluginMessages.appCheckIsEmptyResponse 42 | ) 43 | return 44 | } 45 | 46 | try { 47 | 48 | val verifiedJwt = pluginConfig.serviceImpl.verifyFirebaseAppCheckToken( 49 | firebaseProjectId = pluginConfig.firebaseProjectId, 50 | firebaseProjectNumber = pluginConfig.firebaseProjectNumber, 51 | firebaseAppCheckTokenJwt = firebaseAppCheckToken, 52 | issuerBaseUrl = pluginConfig.firebaseAppCheckApiBaseUrl, 53 | publicKeyUrl = pluginConfig.firebaseAppCheckPublicKeyUrl 54 | ) 55 | // Optional: Check that the token’s subject matches your app’s App ID. 56 | val isShouldContinue = pluginConfig.additionalSecurityCheck(verifiedJwt) 57 | if (!isShouldContinue) { 58 | call.respond( 59 | status = HttpStatusCode.Unauthorized, 60 | message = pluginMessages.appCheckConditionFalseResponse, 61 | ) 62 | return 63 | } 64 | pluginConfig.afterSecurityCheck(verifiedJwt) 65 | // if (pluginConfig.consumeTheTokenAfterUsingIt) { 66 | // try { 67 | // val response = httpClient.post( 68 | // "${pluginConfig.firebaseAppCheckApiBaseUrl}/v1beta/projects/${pluginConfig.firebaseProjectId}" + 69 | // ":verifyAppCheckToken", 70 | // ) { 71 | // contentType(ContentType.Application.Json) 72 | // setBody( 73 | // mapOf( 74 | // "app_check_token" to jwt.token 75 | // ) 76 | // ) 77 | // } 78 | // call.respond(response.bodyAsText()) 79 | // } catch (e: Exception) { 80 | // call.respond(e.toString()) 81 | // } 82 | // } 83 | } catch (e: Exception) { 84 | pluginConfig.errorBuilder(e, call, pluginConfig) 85 | } 86 | } 87 | 88 | /** 89 | * Configures a route to be protected by Firebase App Check using a specific secure strategy. 90 | * 91 | * By using [FirebaseAppCheckSecureStrategy.ProtectSpecificRoutes], this function ensures that only the defined 92 | * route and its associated handlers are protected by Firebase App Check. 93 | * 94 | * Example: 95 | * ``` 96 | get("/test") { 97 | call.respondText { "Tis get test doesn't use firebase app check!" } 98 | } 99 | protectRouteWithAppCheck { 100 | post("/test") { 101 | call.respondText { "Tis post test is protected!" } 102 | } 103 | } 104 | * ``` 105 | * 106 | * @param build A lambda that specifies the route and its handlers to be protected. 107 | */ 108 | fun Route.protectRouteWithAppCheck( 109 | build: Route.() -> Unit 110 | ) { 111 | val configuration = application.plugin(FirebaseAppCheckPlugin).config 112 | 113 | val protectedRoute = createChild(ProtectedRouteSelector()) 114 | val isShouldVerifyToken = configuration.isShouldVerifyToken(application.developmentMode) 115 | 116 | if (isShouldVerifyToken) { 117 | protectedRoute.intercept(ApplicationCallPipeline.Call) { _ -> 118 | call.verifyAppTokenRequest() 119 | } 120 | } 121 | protectedRoute.build() 122 | } 123 | 124 | class ProtectedRouteSelector : RouteSelector() { 125 | override suspend fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation { 126 | return RouteSelectorEvaluation.Transparent 127 | } 128 | 129 | override fun toString(): String = "protected" 130 | } -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/utils/FirebaseAppCheckResponses.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check.utils 2 | 3 | import net.freshplatform.ktor_server.firebase_app_check.configurations.FirebaseAppCheckPluginConfiguration 4 | 5 | /** 6 | * Data class containing messages related to Firebase App Check. 7 | * Messages include responses for scenarios like missing or empty App Check tokens. 8 | */ 9 | data class FirebaseAppCheckMessages( 10 | val pluginConfiguration: FirebaseAppCheckPluginConfiguration, 11 | val appCheckIsNotDefinedResponse: Any = "App check should be defined in the header: ${pluginConfiguration.firebaseAppCheckHeaderName}.", 12 | val appCheckIsEmptyResponse: Any = "App check in the header('${pluginConfiguration.firebaseAppCheckHeaderName}') should not be empty.", 13 | val appCheckConditionFalseResponse: Any = "App check token is not verified.", 14 | val tokenExpiredResponse: Any = "This firebase app check token is expired.", 15 | val genericJwtVerificationErrorResponse: Any = "Unknown error while verifying the firebase app check token. It's when we're trying to verify the jwt.", 16 | val tokenIsNotValidResponse: Any = "The firebase app check token is invalid.", 17 | val headerTypeIsNotJwtResponse: Any = "The type of this token in the header is not equal to 'jwt'.", 18 | val tokenAlgorithmIsNotCorrectResponse: Any = "The type of this algorithm in the token is not RSA256.", 19 | val tokenSignatureVerificationInvalidResponse: Any = "The token signature is invalid.", 20 | val tokenMissingClaimResponse: Any = "There are missing claims, make sure the kid exists and the token is valid.", 21 | val tokenIncorrectClaimResponse: Any = "There are incorrect claims, make sure the kid exists and the token is valid.", 22 | val verifyJwtUnhandledExceptionResponse: Any = "Unhandled exception while verifying the token.", 23 | val signingKeyNotFoundResponse: Any = "Can't find the signing key from Firebase App Check API.", 24 | val networkErrorResponse: Any = "There was a network error while fetching the public key from Firebase App Check.", 25 | val rateLimitReachedResponse: Any = "The limit has been reached. The Firebase App Check API no longer takes requests from us for now.", 26 | val jwkErrorResponse: Any = "Unknown error while getting the public key from Firebase App Check API. It's related to JWK.", 27 | val fetchPublicKeyUnknownErrorResponse: Any = "Unknown error while getting the public key from Firebase App Check API. It's related to JWK.", 28 | val unknownErrorResponse: Any = "Unknown while run the firebase app check feature.", 29 | ) 30 | -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/FirebaseAppCheckTokenVerifierServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check 2 | 3 | import com.auth0.jwk.* 4 | import com.auth0.jwt.JWT 5 | import com.auth0.jwt.algorithms.Algorithm 6 | import com.auth0.jwt.exceptions.* 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import net.freshplatform.ktor_server.firebase_app_check.service.FetchFirebaseAppCheckPublicKeyConfig 10 | import net.freshplatform.ktor_server.firebase_app_check.service.FirebaseAppCheckTokenVerifierService 11 | import net.freshplatform.ktor_server.firebase_app_check.service.jwt.DecodedJwt 12 | import java.net.URL 13 | import java.security.PublicKey 14 | import java.security.interfaces.RSAPublicKey 15 | import kotlin.time.toJavaDuration 16 | 17 | class FirebaseAppCheckTokenVerifierServiceImpl : FirebaseAppCheckTokenVerifierService { 18 | override suspend fun verifyFirebaseAppCheckToken( 19 | firebaseAppCheckTokenJwt: String, 20 | firebaseProjectId: String, 21 | firebaseProjectNumber: String, 22 | issuerBaseUrl: String, 23 | publicKeyUrl: String 24 | ): DecodedJwt { 25 | val publicKey = fetchFirebaseAppCheckPublicKey( 26 | jwtString = firebaseAppCheckTokenJwt, 27 | url = publicKeyUrl, 28 | config = FetchFirebaseAppCheckPublicKeyConfig() 29 | ) 30 | val verifiedJwt = verifyFirebaseAppCheckToken( 31 | firebaseProjectId = firebaseProjectId, 32 | firebaseProjectNumber = firebaseProjectNumber, 33 | jwtString = firebaseAppCheckTokenJwt, 34 | publicKey = publicKey, 35 | issuerBaseUrl = issuerBaseUrl 36 | ) 37 | return verifiedJwt 38 | } 39 | private suspend fun fetchFirebaseAppCheckPublicKey( 40 | jwtString: String, 41 | url: String, 42 | config: FetchFirebaseAppCheckPublicKeyConfig 43 | ): PublicKey { 44 | return withContext(Dispatchers.IO) { 45 | try { 46 | val cacheConfig = config.cacheConfiguration 47 | val rateLimitedConfig = config.rateLimitedConfig 48 | val jwkProvider = 49 | JwkProviderBuilder(URL(url)) 50 | .cached( 51 | cacheConfig.cacheSize, 52 | cacheConfig.expiresIn.toJavaDuration(), 53 | ) 54 | .rateLimited( 55 | rateLimitedConfig.enabled 56 | ) 57 | .build() 58 | 59 | val decodedJwt = JWT.decode(jwtString) 60 | val kid = decodedJwt.getHeaderClaim("kid").asString() 61 | jwkProvider.get(kid).publicKey 62 | } catch (e: SigningKeyNotFoundException) { 63 | throw FirebaseAppCheckFetchPublicKeyException( 64 | message = e.message.toString(), 65 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.SigningKeyNotFound 66 | ) 67 | } catch (e: NetworkException) { 68 | throw FirebaseAppCheckFetchPublicKeyException( 69 | message = e.message.toString(), 70 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.NetworkError 71 | ) 72 | } catch (e: RateLimitReachedException) { 73 | throw FirebaseAppCheckFetchPublicKeyException( 74 | message = e.message.toString(), 75 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.RateLimitReached 76 | ) 77 | } catch (e: JwkException) { 78 | throw FirebaseAppCheckFetchPublicKeyException( 79 | message = e.message.toString(), 80 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.JwkError 81 | ) 82 | } catch (e: JWTDecodeException) { 83 | throw FirebaseAppCheckVerifyJwtException( 84 | message = e.message.toString(), 85 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIsNotValid 86 | ) 87 | } catch (e: AlgorithmMismatchException) { 88 | throw FirebaseAppCheckVerifyJwtException( 89 | message = e.message.toString(), 90 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenAlgorithmIsNotCorrect 91 | ) 92 | } catch (e: SignatureVerificationException) { 93 | throw FirebaseAppCheckVerifyJwtException( 94 | message = e.message.toString(), 95 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenSignatureVerificationInvalid 96 | ) 97 | } catch (e: MissingClaimException) { 98 | throw FirebaseAppCheckVerifyJwtException( 99 | message = e.message.toString(), 100 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenMissingClaim 101 | ) 102 | } catch (e: IncorrectClaimException) { 103 | throw FirebaseAppCheckVerifyJwtException( 104 | message = e.message.toString(), 105 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIncorrectClaim 106 | ) 107 | } catch (e: JWTVerificationException) { 108 | throw FirebaseAppCheckVerifyJwtException( 109 | message = e.message.toString(), 110 | errorType = FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError 111 | ) 112 | } catch (e: Exception) { 113 | throw FirebaseAppCheckFetchPublicKeyException( 114 | message = e.message.toString(), 115 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.UnknownError 116 | ) 117 | } 118 | } 119 | } 120 | 121 | private suspend fun verifyFirebaseAppCheckToken( 122 | jwtString: String, 123 | publicKey: PublicKey, 124 | firebaseProjectId: String, 125 | firebaseProjectNumber: String, 126 | issuerBaseUrl: String 127 | ): DecodedJwt { 128 | return withContext(Dispatchers.IO) { 129 | try { 130 | val verifier = JWT 131 | .require(Algorithm.RSA256(publicKey as RSAPublicKey, null)) 132 | .withAnyOfAudience("projects/${firebaseProjectNumber}", "projects/${firebaseProjectId}") 133 | .withIssuer("${issuerBaseUrl}/${firebaseProjectNumber}") 134 | .build() 135 | val decodedJwt = verifier.verify(jwtString) 136 | val tokenHeader = decodedJwt.getHeaderClaim("typ").asString() 137 | if (tokenHeader != "JWT") { 138 | throw FirebaseAppCheckVerifyJwtException( 139 | message = "The token header of value $tokenHeader is not equal to 'JWT'", 140 | errorType = FirebaseAppCheckVerifyJwtErrorType.HeaderTypeIsNotJwt 141 | ) 142 | } 143 | DecodedJwt(decodedJwt.token) 144 | } catch (e: TokenExpiredException) { 145 | throw FirebaseAppCheckVerifyJwtException( 146 | message = e.message.toString(), 147 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenExpired 148 | ) 149 | } catch (e: JWTDecodeException) { 150 | throw FirebaseAppCheckVerifyJwtException( 151 | message = e.message.toString(), 152 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIsNotValid 153 | ) 154 | } catch (e: AlgorithmMismatchException) { 155 | throw FirebaseAppCheckVerifyJwtException( 156 | message = e.message.toString(), 157 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenAlgorithmIsNotCorrect 158 | ) 159 | } catch (e: SignatureVerificationException) { 160 | throw FirebaseAppCheckVerifyJwtException( 161 | message = e.message.toString(), 162 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenSignatureVerificationInvalid 163 | ) 164 | } catch (e: MissingClaimException) { 165 | throw FirebaseAppCheckVerifyJwtException( 166 | message = e.message.toString(), 167 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenMissingClaim 168 | ) 169 | } catch (e: IncorrectClaimException) { 170 | throw FirebaseAppCheckVerifyJwtException( 171 | message = e.message.toString(), 172 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIncorrectClaim 173 | ) 174 | } catch (e: JWTVerificationException) { 175 | throw FirebaseAppCheckVerifyJwtException( 176 | message = e.message.toString(), 177 | errorType = FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError 178 | ) 179 | } catch (e: Exception) { 180 | throw FirebaseAppCheckVerifyJwtException( 181 | message = e.message.toString(), 182 | errorType = FirebaseAppCheckVerifyJwtErrorType.UnknownError 183 | ) 184 | } 185 | } 186 | } 187 | 188 | } -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/service/FirebaseAppCheckTokenVerifierService.jvm.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check.service 2 | 3 | import com.auth0.jwk.* 4 | import com.auth0.jwt.JWT 5 | import com.auth0.jwt.algorithms.Algorithm 6 | import com.auth0.jwt.exceptions.* 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.withContext 9 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckFetchPublicKeyErrorType 10 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckFetchPublicKeyException 11 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckVerifyJwtErrorType 12 | import net.freshplatform.ktor_server.firebase_app_check.FirebaseAppCheckVerifyJwtException 13 | import net.freshplatform.ktor_server.firebase_app_check.service.jwt.DecodedJwt 14 | import java.net.URL 15 | import java.security.PublicKey 16 | import java.security.interfaces.RSAPublicKey 17 | import kotlin.time.toJavaDuration 18 | 19 | actual class FirebaseAppCheckTokenVerifierServiceImpl : FirebaseAppCheckTokenVerifierService { 20 | override suspend fun verifyFirebaseAppCheckToken( 21 | firebaseAppCheckTokenJwt: String, 22 | firebaseProjectId: String, 23 | firebaseProjectNumber: String, 24 | issuerBaseUrl: String, 25 | publicKeyUrl: String 26 | ): DecodedJwt { 27 | val publicKey = fetchFirebaseAppCheckPublicKey( 28 | jwtString = firebaseAppCheckTokenJwt, 29 | url = publicKeyUrl, 30 | config = FetchFirebaseAppCheckPublicKeyConfig() 31 | ) 32 | val verifiedJwt = verifyFirebaseAppCheckToken( 33 | firebaseProjectId = firebaseProjectId, 34 | firebaseProjectNumber = firebaseProjectNumber, 35 | jwtString = firebaseAppCheckTokenJwt, 36 | publicKey = publicKey, 37 | issuerBaseUrl = issuerBaseUrl 38 | ) 39 | return verifiedJwt 40 | } 41 | private suspend fun fetchFirebaseAppCheckPublicKey( 42 | jwtString: String, 43 | url: String, 44 | config: FetchFirebaseAppCheckPublicKeyConfig 45 | ): PublicKey { 46 | return withContext(Dispatchers.IO) { 47 | try { 48 | val cacheConfig = config.cacheConfiguration 49 | val rateLimitedConfig = config.rateLimitedConfig 50 | val jwkProvider = 51 | JwkProviderBuilder(URL(url)) 52 | .cached( 53 | cacheConfig.cacheSize, 54 | cacheConfig.expiresIn.toJavaDuration(), 55 | ) 56 | .rateLimited( 57 | rateLimitedConfig.enabled 58 | ) 59 | .build() 60 | 61 | val decodedJwt = JWT.decode(jwtString) 62 | val kid = decodedJwt.getHeaderClaim("kid").asString() 63 | jwkProvider.get(kid).publicKey 64 | } catch (e: SigningKeyNotFoundException) { 65 | throw FirebaseAppCheckFetchPublicKeyException( 66 | message = e.message.toString(), 67 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.SigningKeyNotFound 68 | ) 69 | } catch (e: NetworkException) { 70 | throw FirebaseAppCheckFetchPublicKeyException( 71 | message = e.message.toString(), 72 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.NetworkError 73 | ) 74 | } catch (e: RateLimitReachedException) { 75 | throw FirebaseAppCheckFetchPublicKeyException( 76 | message = e.message.toString(), 77 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.RateLimitReached 78 | ) 79 | } catch (e: JwkException) { 80 | throw FirebaseAppCheckFetchPublicKeyException( 81 | message = e.message.toString(), 82 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.JwkError 83 | ) 84 | } catch (e: JWTDecodeException) { 85 | throw FirebaseAppCheckVerifyJwtException( 86 | message = e.message.toString(), 87 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIsNotValid 88 | ) 89 | } catch (e: AlgorithmMismatchException) { 90 | throw FirebaseAppCheckVerifyJwtException( 91 | message = e.message.toString(), 92 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenAlgorithmIsNotCorrect 93 | ) 94 | } catch (e: SignatureVerificationException) { 95 | throw FirebaseAppCheckVerifyJwtException( 96 | message = e.message.toString(), 97 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenSignatureVerificationInvalid 98 | ) 99 | } catch (e: MissingClaimException) { 100 | throw FirebaseAppCheckVerifyJwtException( 101 | message = e.message.toString(), 102 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenMissingClaim 103 | ) 104 | } catch (e: IncorrectClaimException) { 105 | throw FirebaseAppCheckVerifyJwtException( 106 | message = e.message.toString(), 107 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIncorrectClaim 108 | ) 109 | } catch (e: JWTVerificationException) { 110 | throw FirebaseAppCheckVerifyJwtException( 111 | message = e.message.toString(), 112 | errorType = FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError 113 | ) 114 | } catch (e: Exception) { 115 | throw FirebaseAppCheckFetchPublicKeyException( 116 | message = e.message.toString(), 117 | errorType = FirebaseAppCheckFetchPublicKeyErrorType.UnknownError 118 | ) 119 | } 120 | } 121 | } 122 | 123 | private suspend fun verifyFirebaseAppCheckToken( 124 | jwtString: String, 125 | publicKey: PublicKey, 126 | firebaseProjectId: String, 127 | firebaseProjectNumber: String, 128 | issuerBaseUrl: String 129 | ): DecodedJwt { 130 | return withContext(Dispatchers.IO) { 131 | try { 132 | val verifier = JWT 133 | .require(Algorithm.RSA256(publicKey as RSAPublicKey, null)) 134 | .withAnyOfAudience("projects/${firebaseProjectNumber}", "projects/${firebaseProjectId}") 135 | .withIssuer("${issuerBaseUrl}/${firebaseProjectNumber}") 136 | .build() 137 | val decodedJwt = verifier.verify(jwtString) 138 | val tokenHeader = decodedJwt.getHeaderClaim("typ").asString() 139 | if (tokenHeader != "JWT") { 140 | throw FirebaseAppCheckVerifyJwtException( 141 | message = "The token header of value $tokenHeader is not equal to 'JWT'", 142 | errorType = FirebaseAppCheckVerifyJwtErrorType.HeaderTypeIsNotJwt 143 | ) 144 | } 145 | DecodedJwt(decodedJwt.token) 146 | } catch (e: TokenExpiredException) { 147 | throw FirebaseAppCheckVerifyJwtException( 148 | message = e.message.toString(), 149 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenExpired 150 | ) 151 | } catch (e: JWTDecodeException) { 152 | throw FirebaseAppCheckVerifyJwtException( 153 | message = e.message.toString(), 154 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIsNotValid 155 | ) 156 | } catch (e: AlgorithmMismatchException) { 157 | throw FirebaseAppCheckVerifyJwtException( 158 | message = e.message.toString(), 159 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenAlgorithmIsNotCorrect 160 | ) 161 | } catch (e: SignatureVerificationException) { 162 | throw FirebaseAppCheckVerifyJwtException( 163 | message = e.message.toString(), 164 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenSignatureVerificationInvalid 165 | ) 166 | } catch (e: MissingClaimException) { 167 | throw FirebaseAppCheckVerifyJwtException( 168 | message = e.message.toString(), 169 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenMissingClaim 170 | ) 171 | } catch (e: IncorrectClaimException) { 172 | throw FirebaseAppCheckVerifyJwtException( 173 | message = e.message.toString(), 174 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIncorrectClaim 175 | ) 176 | } catch (e: JWTVerificationException) { 177 | throw FirebaseAppCheckVerifyJwtException( 178 | message = e.message.toString(), 179 | errorType = FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError 180 | ) 181 | } catch (e: Exception) { 182 | throw FirebaseAppCheckVerifyJwtException( 183 | message = e.message.toString(), 184 | errorType = FirebaseAppCheckVerifyJwtErrorType.UnknownError 185 | ) 186 | } 187 | } 188 | } 189 | 190 | } -------------------------------------------------------------------------------- /library/src/jvmTest/kotlin/net/freshplatform/ktor_server/firebase_app_check/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check 2 | 3 | import io.ktor.client.request.* 4 | import io.ktor.client.statement.* 5 | import io.ktor.http.* 6 | import io.ktor.server.application.* 7 | import io.ktor.server.response.* 8 | import io.ktor.server.routing.* 9 | import io.ktor.server.testing.* 10 | import net.freshplatform.ktor_server.firebase_app_check.configurations.FirebaseAppCheckPluginConfiguration 11 | import net.freshplatform.ktor_server.firebase_app_check.service.FirebaseAppCheckTokenVerifierService 12 | import net.freshplatform.ktor_server.firebase_app_check.utils.FirebaseAppCheckMessages 13 | import net.freshplatform.ktor_server.firebase_app_check.utils.protectRouteWithAppCheck 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | import kotlin.test.assertFailsWith 17 | import kotlin.test.fail 18 | 19 | val firebaseAppCheckTokenVerifierService: FirebaseAppCheckTokenVerifierService by lazy { 20 | FirebaseAppCheckTokenVerifierServiceMock() 21 | } 22 | 23 | class ApplicationTest { 24 | @Test 25 | fun testRoot() = testApplication { 26 | val pluginConfiguration = FirebaseAppCheckPluginConfiguration( 27 | firebaseProjectNumber = TestConstants.FIREBASE_PROJECT_NUMBER, 28 | firebaseProjectId = TestConstants.FIREBASE_PROJECT_ID, 29 | serviceImpl = firebaseAppCheckTokenVerifierService, 30 | isShouldVerifyToken = true 31 | ) 32 | val messages = FirebaseAppCheckMessages( 33 | pluginConfiguration = pluginConfiguration, 34 | ) 35 | install(FirebaseAppCheckPlugin) { 36 | firebaseProjectId = pluginConfiguration.firebaseProjectId 37 | firebaseProjectNumber = pluginConfiguration.firebaseProjectNumber 38 | serviceImpl = pluginConfiguration.serviceImpl 39 | isShouldVerifyToken = pluginConfiguration.isShouldVerifyToken 40 | } 41 | routing { 42 | get("/") { 43 | call.respondText( 44 | text = TestConstants.APP_CHECK_NOT_REQUIRED_MSG, 45 | ) 46 | } 47 | protectRouteWithAppCheck { 48 | route("/products") { 49 | get("/1") { 50 | call.respondText( 51 | text = TestConstants.APP_CHECK_REQUIRED_MSG, 52 | ) 53 | } 54 | get("/2") { 55 | call.respondText( 56 | text = TestConstants.APP_CHECK_REQUIRED_MSG, 57 | ) 58 | } 59 | } 60 | } 61 | route("/products") { 62 | get("/3") { 63 | call.respondText( 64 | text = TestConstants.APP_CHECK_NOT_REQUIRED_MSG, 65 | ) 66 | } 67 | } 68 | get("/test") { 69 | call.respondText( 70 | text = TestConstants.APP_CHECK_NOT_REQUIRED_MSG, 71 | ) 72 | } 73 | protectRouteWithAppCheck { 74 | post("/test") { 75 | call.respondText( 76 | text = TestConstants.APP_CHECK_REQUIRED_MSG, 77 | ) 78 | } 79 | } 80 | } 81 | val jwtString = TestConstants.TOKEN_OF_THE_PROJECT 82 | try { 83 | firebaseAppCheckTokenVerifierService.verifyFirebaseAppCheckToken( 84 | firebaseAppCheckTokenJwt = jwtString, 85 | firebaseProjectId = pluginConfiguration.firebaseProjectId, 86 | firebaseProjectNumber = pluginConfiguration.firebaseProjectNumber, 87 | issuerBaseUrl = pluginConfiguration.firebaseAppCheckApiBaseUrl, 88 | publicKeyUrl = pluginConfiguration.firebaseAppCheckPublicKeyUrl 89 | ) 90 | } catch (e: Exception) { 91 | fail("Test failed while verify the firebase app check token: $e") 92 | } 93 | 94 | 95 | val verifiedJwtWithDifferentProjectId = assertFailsWith { 96 | firebaseAppCheckTokenVerifierService.verifyFirebaseAppCheckToken( 97 | firebaseAppCheckTokenJwt = jwtString, 98 | publicKeyUrl = pluginConfiguration.firebaseAppCheckPublicKeyUrl, 99 | firebaseProjectId = pluginConfiguration.firebaseProjectId, 100 | firebaseProjectNumber = "32132312123", 101 | issuerBaseUrl = pluginConfiguration.firebaseAppCheckApiBaseUrl 102 | ) 103 | } 104 | assertEquals( 105 | FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError, 106 | verifiedJwtWithDifferentProjectId.errorType 107 | ) 108 | 109 | val verifiedJwtWithDifferentProjectNumber = assertFailsWith { 110 | firebaseAppCheckTokenVerifierService.verifyFirebaseAppCheckToken( 111 | firebaseAppCheckTokenJwt = jwtString, 112 | firebaseProjectId = pluginConfiguration.firebaseProjectId, 113 | firebaseProjectNumber = "32132312123", 114 | issuerBaseUrl = pluginConfiguration.firebaseAppCheckApiBaseUrl, 115 | publicKeyUrl = pluginConfiguration.firebaseAppCheckPublicKeyUrl 116 | ) 117 | } 118 | assertEquals( 119 | FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError, 120 | verifiedJwtWithDifferentProjectNumber.errorType 121 | ) 122 | 123 | val invalidJwtException = assertFailsWith { 124 | firebaseAppCheckTokenVerifierService.verifyFirebaseAppCheckToken( 125 | firebaseAppCheckTokenJwt = "eyInvalidJwt", 126 | publicKeyUrl = pluginConfiguration.firebaseAppCheckPublicKeyUrl, 127 | firebaseProjectId = pluginConfiguration.firebaseProjectId, 128 | firebaseProjectNumber = pluginConfiguration.firebaseProjectNumber, 129 | issuerBaseUrl = pluginConfiguration.firebaseAppCheckApiBaseUrl 130 | ) 131 | } 132 | 133 | assertEquals(FirebaseAppCheckVerifyJwtErrorType.TokenIsNotValid, invalidJwtException.errorType) 134 | 135 | client.get("/").apply { 136 | assertEquals(HttpStatusCode.OK, status) 137 | assertEquals(TestConstants.APP_CHECK_NOT_REQUIRED_MSG, bodyAsText()) 138 | } 139 | 140 | (1..2).forEach { productNumber -> 141 | client.get("/products/${productNumber}").apply { 142 | assertEquals(HttpStatusCode.Unauthorized, status) 143 | assertEquals(messages.appCheckIsNotDefinedResponse, bodyAsText()) 144 | } 145 | client.get("/products/${productNumber}") { 146 | headers { 147 | header(pluginConfiguration.firebaseAppCheckHeaderName, "Bearer ${TestConstants.TOKEN_OF_THE_PROJECT}") 148 | } 149 | }.apply { 150 | assertEquals(HttpStatusCode.Unauthorized, status) 151 | assertEquals(messages.tokenIsNotValidResponse, bodyAsText()) 152 | } 153 | 154 | client.get("/products/${productNumber}") { 155 | headers { 156 | header(pluginConfiguration.firebaseAppCheckHeaderName, TestConstants.TOKEN_OF_THE_PROJECT) 157 | } 158 | }.apply { 159 | assertEquals(HttpStatusCode.OK, status) 160 | assertEquals(TestConstants.APP_CHECK_REQUIRED_MSG, bodyAsText()) 161 | } 162 | } 163 | client.get("/products/3").apply { 164 | assertEquals(HttpStatusCode.OK, status) 165 | assertEquals(TestConstants.APP_CHECK_NOT_REQUIRED_MSG, bodyAsText()) 166 | } 167 | 168 | client.get("/test").apply { 169 | assertEquals(HttpStatusCode.OK, status) 170 | assertEquals(TestConstants.APP_CHECK_NOT_REQUIRED_MSG, bodyAsText()) 171 | } 172 | 173 | client.post("/test").apply { 174 | assertEquals(HttpStatusCode.Unauthorized, status) 175 | assertEquals(messages.appCheckIsNotDefinedResponse, bodyAsText()) 176 | } 177 | 178 | client.post("/test") { 179 | headers { 180 | header(pluginConfiguration.firebaseAppCheckHeaderName, "Bearer ${TestConstants.TOKEN_OF_THE_PROJECT}") 181 | } 182 | }.apply { 183 | assertEquals(HttpStatusCode.Unauthorized, status) 184 | assertEquals(messages.tokenIsNotValidResponse, bodyAsText()) 185 | } 186 | 187 | client.post("/test") { 188 | headers { 189 | header(pluginConfiguration.firebaseAppCheckHeaderName, TestConstants.TOKEN_OF_THE_PROJECT) 190 | } 191 | }.apply { 192 | assertEquals(HttpStatusCode.OK, status) 193 | assertEquals(TestConstants.APP_CHECK_REQUIRED_MSG, bodyAsText()) 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /library/src/jvmTest/kotlin/net/freshplatform/ktor_server/firebase_app_check/FirebaseAppCheckTokenVerifierServiceMock.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.exceptions.JWTDecodeException 5 | import net.freshplatform.ktor_server.firebase_app_check.service.FirebaseAppCheckTokenVerifierService 6 | import net.freshplatform.ktor_server.firebase_app_check.service.jwt.DecodedJwt 7 | import java.security.PublicKey 8 | 9 | private class PublicKeyMock : PublicKey { 10 | override fun getAlgorithm(): String { 11 | return "RS256" 12 | } 13 | 14 | override fun getFormat(): String { 15 | return "JWT" 16 | } 17 | 18 | override fun getEncoded(): ByteArray { 19 | return byteArrayOf() 20 | } 21 | } 22 | 23 | class FirebaseAppCheckTokenVerifierServiceMock : FirebaseAppCheckTokenVerifierService { 24 | 25 | override suspend fun verifyFirebaseAppCheckToken( 26 | firebaseAppCheckTokenJwt: String, 27 | firebaseProjectId: String, 28 | firebaseProjectNumber: String, 29 | issuerBaseUrl: String, 30 | publicKeyUrl: String 31 | ): DecodedJwt { 32 | try { 33 | val verified = JWT.decode(firebaseAppCheckTokenJwt) 34 | if (verified.audience.first() != "projects/$firebaseProjectNumber") { 35 | throw FirebaseAppCheckVerifyJwtException( 36 | "The ${verified.audience.first()} is not equal to projects/$firebaseProjectNumber", 37 | errorType = FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError 38 | ) 39 | } 40 | if (verified.audience[1] != "projects/$firebaseProjectId") { 41 | throw FirebaseAppCheckVerifyJwtException( 42 | "The ${verified.audience[1]} is not equal to projects/$firebaseProjectId", 43 | errorType = FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError 44 | ) 45 | } 46 | if (verified.issuer != "https://firebaseappcheck.googleapis.com/$firebaseProjectNumber") { 47 | throw FirebaseAppCheckVerifyJwtException( 48 | "The ${verified.issuer} is not equal to projects/$firebaseProjectNumber", 49 | errorType = FirebaseAppCheckVerifyJwtErrorType.GenericJwtVerificationError 50 | ) 51 | } 52 | return DecodedJwt(verified.token) 53 | } catch (e: JWTDecodeException) { 54 | throw FirebaseAppCheckVerifyJwtException( 55 | "Token is not valid: $e", 56 | errorType = FirebaseAppCheckVerifyJwtErrorType.TokenIsNotValid 57 | ) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /library/src/jvmTest/kotlin/net/freshplatform/ktor_server/firebase_app_check/TestConstants.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check 2 | 3 | object TestConstants { 4 | const val FIREBASE_PROJECT_ID = "mynotes-eb717" 5 | const val FIREBASE_PROJECT_NUMBER = "802089142559" 6 | const val TOKEN_OF_THE_PROJECT = "eyJraWQiOiJ2Yy1sVEEiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxOjgwMjA4OTE0MjU1OTphbmRyb2lkOjI2ZDhjMDA3ZGVkMDNmODQyYTg4MmEiLCJhdWQiOlsicHJvamVjdHNcLzgwMjA4OTE0MjU1OSIsInByb2plY3RzXC9teW5vdGVzLWViNzE3Il0sInByb3ZpZGVyIjoiZGVidWciLCJpc3MiOiJodHRwczpcL1wvZmlyZWJhc2VhcHBjaGVjay5nb29nbGVhcGlzLmNvbVwvODAyMDg5MTQyNTU5IiwiZXhwIjoxNjk3MTM0NDg3LCJpYXQiOjE2OTcxMzA4ODcsImp0aSI6InZLZERfNTRhQ2tzVmpHV0xBN3d1TjZmWlFUQWRYZzRBWGJhYVBzRUZDV0EifQ.H_LGsCe5I-Z2uAgYU1isDmxQ-6PecdmjEqvkrZp9AWthNhsiMdlVYjUe2DaSmt3lhIlwCJyCh2YooOLvSlFAvdx5n__kB5O5C9Fw-Vw-zjSTOAi6lNB0hi8OEkIJhNgw2b_UipeVFd1I6ICkCdV93Ewr-clv-eDeMIg_b8vr3w6HtypZDVu3hAl6BjfxY9r7cm5eBmHGnOxwb1-flSKRJdBmrh4Bm0_imaDPSHw_rUwCUXHOAM-QfdQ-D4C15L_IJH4X6kT7nm8GMj47rQjr1d6CQZbW3xoIsTJvnpreOR1xyiHZiLydj1cwPt6r2DfmjRL6-tFs2u8c72CcoqQ4hhsJE9ZSk1BHXpnGw6t5PLPWmk-K7wCrn49U20SYsbOGzyMmwPs-nRyYL3QeV00brlaQWFN7pnjquYHtgJZgkVZlIe1Hh_8mBzTSLygc3-0Xw3FKf1X6p_jOyyN7Qi3Wf5GHvBdp_sYyuBtXMYVwhKQ56lYBX3waLP0KHSiDiDUW" 7 | 8 | const val APP_CHECK_REQUIRED_MSG = "App Check for this route is required." 9 | const val APP_CHECK_NOT_REQUIRED_MSG = "This route doesn't requires App Check" 10 | } -------------------------------------------------------------------------------- /library/src/nativeMain/kotlin/net/freshplatform/ktor_server/firebase_app_check/service/FirebaseAppCheckTokenVerifierService.native.kt: -------------------------------------------------------------------------------- 1 | package net.freshplatform.ktor_server.firebase_app_check.service 2 | 3 | import net.freshplatform.ktor_server.firebase_app_check.service.jwt.DecodedJwt 4 | 5 | actual class FirebaseAppCheckTokenVerifierServiceImpl : FirebaseAppCheckTokenVerifierService { 6 | override suspend fun verifyFirebaseAppCheckToken( 7 | firebaseAppCheckTokenJwt: String, 8 | firebaseProjectId: String, 9 | firebaseProjectNumber: String, 10 | issuerBaseUrl: String, 11 | publicKeyUrl: String 12 | ): DecodedJwt { 13 | TODO("Not yet implemented") 14 | } 15 | } -------------------------------------------------------------------------------- /scripts/before-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Build with Gradle..." 4 | echo "" 5 | ./gradlew build -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "FirebaseAppCheck" 2 | 3 | include(":library") 4 | include(":example") 5 | 6 | pluginManagement { 7 | repositories { 8 | mavenCentral() 9 | gradlePluginPortal() 10 | } 11 | } 12 | 13 | dependencyResolutionManagement { 14 | repositories { 15 | mavenCentral() 16 | google() 17 | gradlePluginPortal() 18 | } 19 | } --------------------------------------------------------------------------------