- 
                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
          
     Merged
      
      
    
  
     Merged
                    Changes from 27 commits
      Commits
    
    
            Show all changes
          
          
            29 commits
          
        
        Select commit
          Hold shift + click to select a range
      
      25d3842
              
                Sketch out initial private methods and service
              
              
                dwyfrequency cf60bb9
              
                Remove unnecessary notes
              
              
                dwyfrequency 3c4e191
              
                Fix some lint issues
              
              
                dwyfrequency 4e84ce3
              
                Fix style guide issues
              
              
                dwyfrequency dc9cbfd
              
                Update code structure
              
              
                dwyfrequency eb1725d
              
                Add pyjwt version to requirments & update code based on comments
              
              
                dwyfrequency c5a25c2
              
                Add app_id key for verified claims dict
              
              
                dwyfrequency aa98697
              
                Add initial test
              
              
                dwyfrequency 7e2259c
              
                Add tests for token headers
              
              
                dwyfrequency 0978778
              
                Add decode token test and notes
              
              
                dwyfrequency 85145e1
              
                Updating requirements for mocks and note in test
              
              
                dwyfrequency 41f93ea
              
                Add verify token test and decode test
              
              
                dwyfrequency 5436d12
              
                Update pytest-mock requirements
              
              
                dwyfrequency 6a4815a
              
                Add tests for error messages
              
              
                dwyfrequency a592256
              
                Update requirements for lifespan cache
              
              
                dwyfrequency 5b94963
              
                update error message and test
              
              
                dwyfrequency 89f29d3
              
                Explicitly pass audience to jwt.decode and update key retrieval
              
              
                dwyfrequency a5290b5
              
                Mock signing key
              
              
                dwyfrequency c46b60b
              
                Update aud check logic and tests
              
              
                dwyfrequency e9148b7
              
                Remove print statement
              
              
                dwyfrequency b732aa6
              
                Update method doc string
              
              
                dwyfrequency 46f22f6
              
                Add test for decode_token error
              
              
                dwyfrequency 2b6c7e7
              
                Catch additional errors and add custom error messages for them
              
              
                dwyfrequency e08f355
              
                Mock out all the common errors
              
              
                dwyfrequency 73edeb3
              
                Updating error messages and tests per comments
              
              
                dwyfrequency 33f93e5
              
                Make jwks_client a class property
              
              
                dwyfrequency 5321203
              
                Add validation for the subject in the JWT payload
              
              
                dwyfrequency 77eb730
              
                Update docs and error message strings
              
              
                dwyfrequency fe30abb
              
                Merge branch 'master' into jd-verifyToken
              
              
                dwyfrequency File filter
Filter by extension
Conversations
          Failed to load comments.   
        
        
          
      Loading
        
  Jump to
        
          Jump to file
        
      
      
          Failed to load files.   
        
        
          
      Loading
        
  Diff view
Diff view
There are no files selected for viewing
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -12,3 +12,4 @@ apikey.txt | |
| htmlcov/ | ||
| .pytest_cache/ | ||
| .vscode/ | ||
| .venv/ | ||
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| # 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 | ||
| _jwks_client = 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 | ||
| # Default lifespan is 300 seconds (5 minutes) so we change it to 21600 seconds (6 hours). | ||
| self._jwks_client = PyJWKClient(self._JWKS_URL, lifespan=21600) | ||
|  | ||
|  | ||
| 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 | ||
| signing_key = self._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) | ||
|  | ||
| 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 has invalid signature..' | ||
|         
                  dwyfrequency marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| ) | ||
| except InvalidAudienceError: | ||
| raise ValueError( | ||
| 'The provided App Check token has incorrect "aud" (audience) claim.' | ||
|         
                  dwyfrequency marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| f'Expected payload to include {self._scoped_project_id}.' | ||
| ) | ||
| except InvalidIssuerError: | ||
| raise ValueError( | ||
| 'The provided App Check token has incorrect "iss" (issuer) claim.' | ||
|         
                  dwyfrequency marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| f'Expected claim to include {self._APP_CHECK_ISSUER}' | ||
| ) | ||
| except ExpiredSignatureError: | ||
| raise ValueError( | ||
| 'The provided App Check token has expired.' | ||
| ) | ||
| except InvalidTokenError as exception: | ||
| raise ValueError( | ||
| f'Decoding App Check token failed. Error: {exception}' | ||
| ) | ||
|  | ||
| audience = payload.get('aud') | ||
| if not isinstance(audience, list) or self._scoped_project_id not in audience: | ||
| raise ValueError('Firebase App Check token has incorrect "aud" (audience) claim.') | ||
| if not payload.get('iss').startswith(self._APP_CHECK_ISSUER): | ||
| raise ValueError('Token does not contain the correct "iss" (issuer).') | ||
| There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we also validate the  example from Node.js:  | ||
| _Validators.check_string( | ||
| 'The provided App Check token "sub" (subject) claim', | ||
| payload.get('sub')) | ||
|  | ||
| return payload | ||
|  | ||
| class _Validators: | ||
| """A collection of data validation utilities. | ||
|  | ||
| Methods provided in this class raise ``ValueErrors`` if any validations fail. | ||
| """ | ||
|  | ||
| @classmethod | ||
| def check_string(cls, label: str, value: Any): | ||
| """Checks if the given value is a string.""" | ||
| if value is None: | ||
| raise ValueError('{0} "{1}" must be a non-empty string.'.format(label, value)) | ||
| if not isinstance(value, str): | ||
| raise ValueError('{0} "{1}" must be a string.'.format(label, value)) | ||
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
              
      
      Oops, something went wrong.
        
    
  
  Add this suggestion to a batch that can be applied as a single commit.
  This suggestion is invalid because no changes were made to the code.
  Suggestions cannot be applied while the pull request is closed.
  Suggestions cannot be applied while viewing a subset of changes.
  Only one suggestion per line can be applied in a batch.
  Add this suggestion to a batch that can be applied as a single commit.
  Applying suggestions on deleted lines is not supported.
  You must change the existing code in this line in order to create a valid suggestion.
  Outdated suggestions cannot be applied.
  This suggestion has been applied or marked resolved.
  Suggestions cannot be applied from pending reviews.
  Suggestions cannot be applied on multi-line comments.
  Suggestions cannot be applied while the pull request is queued to merge.
  Suggestion cannot be applied right now. Please check back later.
  
    
  
    
Uh oh!
There was an error while loading. Please reload this page.