- 
                Notifications
    You must be signed in to change notification settings 
- Fork 344
feat: Add function to verify an App Check token #642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 24 commits
25d3842
              cf60bb9
              3c4e191
              4e84ce3
              dc9cbfd
              eb1725d
              c5a25c2
              aa98697
              7e2259c
              0978778
              85145e1
              41f93ea
              5436d12
              6a4815a
              a592256
              5b94963
              89f29d3
              a5290b5
              c46b60b
              e9148b7
              b732aa6
              46f22f6
              2b6c7e7
              e08f355
              73edeb3
              33f93e5
              5321203
              77eb730
              fe30abb
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -12,3 +12,4 @@ apikey.txt | |
| htmlcov/ | ||
| .pytest_cache/ | ||
| .vscode/ | ||
| .venv/ | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,146 @@ | ||||||
| # Copyright 2022 Google Inc. | ||||||
| # | ||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| # you may not use this file except in compliance with the License. | ||||||
| # You may obtain a copy of the License at | ||||||
| # | ||||||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||||||
| # | ||||||
| # Unless required by applicable law or agreed to in writing, software | ||||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
| # See the License for the specific language governing permissions and | ||||||
| # limitations under the License. | ||||||
|  | ||||||
| """Firebase App Check module.""" | ||||||
|  | ||||||
| from typing import Any, Dict | ||||||
| import jwt | ||||||
| from jwt import PyJWKClient, ExpiredSignatureError, InvalidTokenError | ||||||
| from jwt import InvalidAudienceError, InvalidIssuerError, InvalidSignatureError | ||||||
| from firebase_admin import _utils | ||||||
|  | ||||||
| _APP_CHECK_ATTRIBUTE = '_app_check' | ||||||
|  | ||||||
| def _get_app_check_service(app) -> Any: | ||||||
| return _utils.get_app_service(app, _APP_CHECK_ATTRIBUTE, _AppCheckService) | ||||||
|  | ||||||
| def verify_token(token: str, app=None) -> Dict[str, Any]: | ||||||
| """Verifies a Firebase App Check token. | ||||||
|  | ||||||
| Args: | ||||||
| token: A token from App Check. | ||||||
| app: An App instance (optional). | ||||||
|  | ||||||
| Returns: | ||||||
| Dict[str, Any]: A token's decoded claims if the App Check token | ||||||
|         
                  dwyfrequency marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||||||
|  | ||||||
| Raises: | ||||||
| ValueError: If ``project_id``, headers, or the decoded token payload is invalid. | ||||||
|         
                  dwyfrequency marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||||||
| """ | ||||||
| return _get_app_check_service(app).verify_token(token) | ||||||
|  | ||||||
| class _AppCheckService: | ||||||
| """Service class that implements Firebase App Check functionality.""" | ||||||
|  | ||||||
| _APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/' | ||||||
| _JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1/jwks' | ||||||
| _project_id = None | ||||||
| _scoped_project_id = None | ||||||
|  | ||||||
| def __init__(self, app): | ||||||
|         
                  dwyfrequency marked this conversation as resolved.
              Show resolved
            Hide resolved | ||||||
| # Validate and store the project_id to validate the JWT claims | ||||||
| self._project_id = app.project_id | ||||||
| if not self._project_id: | ||||||
| raise ValueError( | ||||||
| 'Project ID is required to access App Check service. Either set the ' | ||||||
| 'projectId option, or use service account credentials. Alternatively, set the ' | ||||||
| 'GOOGLE_CLOUD_PROJECT environment variable.') | ||||||
|         
                  dwyfrequency marked this conversation as resolved.
              Show resolved
            Hide resolved | ||||||
| self._scoped_project_id = 'projects/' + app.project_id | ||||||
|  | ||||||
|  | ||||||
| def verify_token(self, token: str) -> Dict[str, Any]: | ||||||
| """Verifies a Firebase App Check token.""" | ||||||
| _Validators.check_string("app check token", token) | ||||||
|  | ||||||
| # Obtain the Firebase App Check Public Keys | ||||||
| # Note: It is not recommended to hard code these keys as they rotate, | ||||||
| # but you should cache them for up to 6 hours. | ||||||
|         
                  dwyfrequency marked this conversation as resolved.
              Show resolved
            Hide resolved | ||||||
| # Default lifespan is 300 seconds (5 minutes) so we change it to 21600 seconds (6 hours). | ||||||
| jwks_client = PyJWKClient(self._JWKS_URL, lifespan=21600) | ||||||
|          | ||||||
| signing_key = jwks_client.get_signing_key_from_jwt(token) | ||||||
| self._has_valid_token_headers(jwt.get_unverified_header(token)) | ||||||
| verified_claims = self._decode_and_verify(token, signing_key.key) | ||||||
|  | ||||||
| # The token's subject will be the app ID, you may optionally filter against | ||||||
| # an allow list | ||||||
|          | ||||||
| verified_claims['app_id'] = verified_claims.get('sub') | ||||||
| return verified_claims | ||||||
|  | ||||||
| def _has_valid_token_headers(self, headers: Any) -> None: | ||||||
| """Checks whether the token has valid headers for App Check.""" | ||||||
| # Ensure the token's header has type JWT | ||||||
| if headers.get('typ') != 'JWT': | ||||||
| raise ValueError("The provided App Check token has an incorrect type header") | ||||||
| # Ensure the token's header uses the algorithm RS256 | ||||||
| algorithm = headers.get('alg') | ||||||
| if algorithm != 'RS256': | ||||||
| raise ValueError( | ||||||
| 'The provided App Check token has an incorrect algorithm. ' | ||||||
|         
                  dwyfrequency marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||||||
| f'Expected RS256 but got {algorithm}.' | ||||||
| ) | ||||||
|  | ||||||
| def _decode_and_verify(self, token: str, signing_key: str): | ||||||
| """Decodes and verifies the token from App Check.""" | ||||||
| payload = {} | ||||||
| try: | ||||||
| payload = jwt.decode( | ||||||
| token, | ||||||
| signing_key, | ||||||
| algorithms=["RS256"], | ||||||
| audience=self._scoped_project_id | ||||||
| ) | ||||||
| except InvalidSignatureError: | ||||||
| raise ValueError( | ||||||
| 'The provided App Check token signature cannot be verified.' | ||||||
|          | ||||||
| 'The provided App Check token signature cannot be verified.' | |
| 'The provided App Check token has invalid signature.' | 
        
          
              
                  dwyfrequency marked this conversation as resolved.
              
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
              
                  dwyfrequency marked this conversation as resolved.
              
              
                Outdated
          
            Show resolved
            Hide resolved
        
              
          
              
                Outdated
          
        
      There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 'The provided App Check token signature has expired.' | |
| 'The provided App Check token has expired.' | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also validate the sub claim?
example from Node.js:
} else if (typeof payload.sub !== 'string') {
      errorMessage = 'The provided App Check token has no "sub" (subject) claim.';
    } else if (payload.sub === '') {
      errorMessage = 'The provided App Check token has an empty string "sub" (subject) claim.';
    }
    ```
Uh oh!
There was an error while loading. Please reload this page.