Skip to content

Research for Issue #577

Joshua Douglas edited this page Oct 6, 2023 · 5 revisions

Introduce API Endpoint Authentication

Introduce secure API endpoint authentication, so that only registered users can access our API. The selected authentication approach should be compatible with our current use of AWS Cognito, and should be integrated with the frontend app. We should not break any existing authentication code, such as the google OAuth.

Requirements

  • Add authentication to all endpoints that expose user data.
    • Endpoints must validate the identity of a user before responding to a request.
    • This PR will not include role base access, so all users will have the same level of access for now.
    • The authentication method should be compatible with AWS Cognito & our current authentication approach
  • Use connexion to require authentication by updating the OpenAPI specification
  • Design a system that allows developers to easily access and develop our api & app
  • Integrate the authentication changes with the frontend app, if necessary
  • Update existing test cases so they continue to work with our new changes
  • Add new test cases to verify that endpoint access is denied when using unauthenticated HTTP requests

Research Questions

What is the difference between Authorization and Authentication?

Json Web Tokens (JWTs)

What is a Json Web Token?

JWTs provide a URL safe way for transmitting claims between two clients. JWTs are encoded (not encrypted!) and digitally signed JSON messages that have three components:

  1. A JSON body/claim.
    • This can be any JSON you want to send.
    • JWT payloads are typically called 'claims' since we can verify the sender's identity
  2. A header with detailed metadata
    • The header is typically handled by JWT libraries
    • The header metadata is defined by the RFC standard, and contains info about the signing algorithm
  3. A signature, that can be used to authenticate the sender
    • The JSON body and header are 'signed' by using a private key to encrypt the two concatenated messages
    • The body and payload are base64 encoded before signing, to ensure the message is URL safe
    • When using an asymmetric algorithm like RS256 the public key can be used to decrypt the message
      • i.e. private key owner signs, everybody can decrypt the message
    • When using a symmetric algorithm like HS256 the private key can be used to encrypt and decrypt the message
      • i.e. The private key owner(s), and private key owner(s), can validate that a private key holder sent the message
      • This is often used with strategies like Diffie-Hellman key exchange, which can be used to negotiate a common private key between server & client
    • This ensures that the owner of the key sent the message. The message is validated by comparing the decoded body and header to the decrypted signature. If the key is correct and the data was not modified, then these two values will match
    • The signature also guarantees that neither the header or body where modified while the message was in transit, since any change to their data would change the resulting signature

A JWT looks like this: [Base64URL-encoded Header].[Base64URL-encoded Payload].[Signature]

No public key is needed to decode the header or payload, so JWTs are not used to transmit sensitive data. They are, however, used to transmit ephemeral (time-sensitive) credentials if used in conjunction with HTTPS connections. The idea is that encrypted HTTPS communications are very hard to intercept and decrypt/hack on short time-scales, so unique credentials with a short timespans are considered to be safe to transmit with this setup.

How are JWTs used in API Auth?

The JWT serves as a temporary, typically timestamped credential. The server can verify that it issued the JWT by using its private or public key. The standard JWT workflow looks like this. Once the token is expired the user will need to sign back in by providing their username & password.

graph TD;
    A[Client] -->|1. Login | B[Server]
    B -->|2. Generate JWT| B
    B -->|3. Return JWT| A
    A -->|4. Store JWT Locally| A
    A -->|5. Send Request<br>w/JWT in Header| B
    B -->|6. Verify JWT| B
    B -->|7. Token Valid?| C{Decision}
    C -->|8a. Yes| D[Perform Requested<br>Action]
    C -->|8b. No| E[Return Error]
    D -->|9a. HTTP Response| A
    E -->|9b. Error Response| A
Loading

JWTs are typically used by REST APIs to allow stateless access to authenticated user data. A user can be authenticated without first performing a user lookup, because the service only requires the private or public key to authenticate the JWT. Once authenticated, the JWT can be designed to store the essential user information required by the request to minimize database lookups.

The client never reads or modifies the JWT. They only store it and send it back to the server with each request.

How does connexion use JWTs for authentication?

To enable JWT authentication, add a security schema to the OpenAPI spec and register a x-bearerInfoFunc function.

The connexion library enforces JWT authentication for the specified endpoints by invoking the x-bearerInfoFunc function before invoking the endpoint. The HTTP request will exit early with an 401 authentication error if the x-bearerInfoFunc raises an unhandled exception or if it returns None

# jose is a 3rd party library with 
# cryptography implementations
from jose import JWTError, jwt

def example_x-bearerInfoFunc(token: str) -> dict:
  '''
  JWT authentication methods accept the JWT token str
  and expects a dictionary containing the JWT payload
  as a dictionary. This JWT payload will be sent to
  the endpoint handler as a `token_info` positional
  parameter

  Return None if the authentication failed.
  '''
  try:
    token_body = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
  except:
    # option 1 Raise an exception
    # If you do this, then you need to register a custom
    # handler for that exception type with connexion, to 
    # return a 401. Otherwise a 5xx will be returned
    # option 2, just pass None. connexion will return 401 err
    return None

Compare Asymmetric vs Symmetric JWT approaches

If a single service is both issuing a token and verifying it then a symmetric algorithm is easier to setup and manage. The service only requires a single private key, and no public key distribution is required.

If multiple services need to verify, or if you require 3rd party services to use the JWTs for authentication, then an asymmetric approach is preferred. Private keys should not be shared with 3rd party services and sharing private keys amongst multiple internal services is challenging to do securely and generally not recommended.

Show private/public key PEM format

JWT libraries expect private and public keys to follow PEM formatting specifications.

Private key:

-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBA...
...Remaining Key
-----END RSA PRIVATE KEY-----

Public key:

-----BEGIN PUBLIC KEY-----
MIGfMA0...
...Remaining key
-----END PUBLIC KEY-----

The key format follow a strict standard, so external libraries should be used when reading and writing PEM keys.

AWS Cognito

How does AWS Cognito work?

The call to botoClient.get_user returns this. I wonder how much overlap this has with the decoded JWT. If the overlap is high enough we can avoid an API call here.

{
  'Username': 'd343da66-9646-4cae-b2e0-f00a636a0087', 
  'UserAttributes': [
    {'Name': 'sub', 'Value': 'd343da66-9646-4cae-b2e0-f00a636a0087'},
    {'Name': 'email_verified', 'Value': 'true'},
    {'Name': 'email', 'Value': '9mm4err7@duck.com'}
  ], 
  'ResponseMetadata': 
    {
      'RequestId': '174771f8-9b16-4784-a4c8-35ebb6bcd566', 
      'HTTPStatusCode': 200, 
      'HTTPHeaders': {...}, 
      'RetryAttempts': 0
    }
}

What information does a AWS signin request return?

Here is an example response from a successful AWS Cognito sign in. The response will include the user's JWT and the user info.

{
  "token": 
  "eyJraWQiOiJLclpCb0JIQlZPYXJtSmJ1aWN0VnRQdVI5dkZSMCtUYkplOWV2U2hjeUVRPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJkMzQzZGE2Ni05NjQ2LTRjYWUtYjJlMC1mMDBhNjM2YTAwODciLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0xLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMV90WTEyT1E4OXkiLCJjbGllbnRfaWQiOiI1cXJscDVpaGV1N3BsMGJnMTZmbjRkdGRsayIsIm9yaWdpbl9qdGkiOiJmMzc5MzljNy04OWUzLTQzOGMtODAwOC0xMzBiODVmMGExMzkiLCJldmVudF9pZCI6IjQyNzA4NTExLTg3MzctNDNlNi04OTMwLTY4ZmUyZTljNTkxMSIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE2OTMwMzQyMDUsImV4cCI6MTY5MzAzNjAwNCwiaWF0IjoxNjkzMDM0MjA1LCJqdGkiOiI1ZDA2MDBlNC04Mzg0LTQyNDYtOWYyZS0wNTcxNmNkOWQxNmYiLCJ1c2VybmFtZSI6ImQzNDNkYTY2LTk2NDYtNGNhZS1iMmUwLWYwMGE2MzZhMDA4NyJ9.LkvIZEDflgE7jIjWE7AWHJ6Oc_5IMLZE03KK6mHvNzmvWthX9X_hUDEnF3SsTWd7ySUPktj9n7XkE3OxN2XpyrD9cmQSVI7S3tTt-dMCDh8yKTeUxhqGqR5eOCCyZelloSv64MUMtJ9-raiWECq8rz77jHOaX18oWI4RIvUOQHer5_-OvT715SsA1wEXGm3zzUPa5tcK8OWZP7pay4u_Qf77yH7s1ThsttXwrUQ0E84Qc4xKAgkL5GUnuOffkTj3vph1gEMb1Tkw12ruYZl5WK2fFMirrqTGacfr9cTLaYFi9TGSlM_lYwjNndb61pUz-rGikaNinGfPRo6zDEgXwA",
  "user": {
    "email": "9mm4err7@duck.com"
  }
}

How does AWS Cognito Sign JWTs?

Using the asymmetrical RSA256 algorithm. The private key used for signing is determined by the region and user pool Id. Anyone can validate the issued JWTs by using their public keys.

They provide instructions for validating their JWTs, on their docs.

Where can you find AWS Cognito JWT Public Keys?

According to their docs public keys can be found by Region and userPoolId at https://cognito-idp.<Region>.amazonaws.com/<userPoolId>/.well-known/jwks.json>.

This json file will contain a collection of keys with different local key Ids (kid). The JWTs issued by AWS Cognito each have a unique kid so you can always verify that you are using the correct public key.

Can you cache public keys in your app?

Yes. You can request the keys once and store them in memory, however these keys are regularly rotated so we would need to periodically refresh our cache.

AWS recommends updating your local key cache any time you receive a token that contains the correct issuer (https://cognito-idp.<region>.amazonaws.com/<userPoolId>) but a kid missing from your cache.

Does python have a secure JWT library we can use?

The python standard library does not have a JWT library. We should avoid implementing any low-level JWT validation algorithms because they algorithms are susceptible to security exploits. The validation algorithm we use should be reviewed by a security expert, and periodically updated as new exploits are revealed. As a result, this is the perfect use case for a 3rd party library.

The best library seems to be PyJWT. This library is widely used (e.g. the popular flask-jws library uses it), is maintained by a security expert, is regularly updated, and is widely recommended online.

The top contender to PyJWT is python-jose. This library also seems secure and well maintained, but it includes much more than the implementation of RFC 7519 standard. We'll favor the smaller library here.

How to install pyjwt

Decoding token pyjwt requires the optional cryptographic dependency, so:

pip install pyjwt[crypto]

How does our current authentication system work?

The authentication strategies are slightly different between the frontend and backend.

Backend API

Most of the API endpoints are unauthenticated. This is not currently a problem since the unauthenticated endpoints don't contain sensitive data, yet. If you navigate to http://dev.homeunite.us/api/ui you can interact with most endpoints w/o authentication.

The user GET and DELETE methods are authenticated using Json Web Tokens.

The backend OpenAPI spec has a single JWT security scheme, applied to four endpoints.

  securitySchemes:
    jwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      x-bearerInfoFunc: openapi_server.controllers.security_controller.requires_auth
  /auth/user: # An example secured endpoint
    get:
      description: Gets current user
      operationId: user
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: "../../openapi.yaml#/components/schemas/ApiResponse"
          description: successful operation
      tags:
        - auth
      x-openapi-router-controller: openapi_server.controllers.auth_controller
      security:
        - jwt: ["secret"]

This is our current authentication method. It performs authentication by making an authenticated AWS Cognito API call. The API call will fail by throwing an exception if the token is invalid or expired, so this works as an authentication method.

def requires_auth(token):
    # Check if token is valid
    try:
        # Get user info from token
        userInfo = userClient.get_user(
            AccessToken=token
        )
        return userInfo

    # handle any errors
    except Exception as e:
        code = e.response['Error']['Code']
        message = e.response['Error']['Message']
        raise AuthError({
                    "code": code, 
                    "message": message
                }, 401)

Frontend App

Describe how the frontend app prevents you from navigating to authenticated urls. Does the frontend app already store JWTs?

Describe the existing backend authentication endpoints

Connexion JWT Authentication Method

security_controller.requires_auth

This method is explained above. The HTTP request will exit early with an 401 authentication error if this method raises an unhandled exception or if it returns None.

Sign-in Methods

auth_controller.signin

Post request at /auth/signin/

Use a username and password provided within the JSON body to request a JWT from AWS Cognito. Redirect the user to create a new password if AWS Cognito requires it. Return some basic user data (current just the user email) and the JWT if the authentication succeeds.

auth_controller.token

POST /auth/token

This endpoint is used during OAuth to exchange authorization codes for a JWT. "Authorization codes" are returned by successful AWS OAuth authentication requests, and are meant to be used with the amazon cognito /oauth2/token endpoint to retrieve the final JWT.

Current the frontend app is calling the auth/token endpoint directly, with the authorization code returned by AWS cognito.

auth_controller.google

Get request at /auth/google/

The OpenAPI spec does not specify this, but the google endpoint expects a redirect_uri query parameter.

The google indirectly signs in users using the AWS Cognito OAuth 2.0 endpoint. The redirect method actually returns a 302 status code HTTP response that instructs the client browser to do the redirect.

def google():
    client_id = current_app.config['COGNITO_CLIENT_ID']
    root_url = current_app.root_url
    redirect_uri = request.args['redirect_uri']
    print(f"{cognito_client_url}/oauth2/authorize?client_id={client_id}&response_type=code&scope=email+openid+profile+phone+aws.cognito.signin.user.admin&redirect_uri={root_url}{redirect_uri}&identity_provider=Google")

    return redirect(f"{cognito_client_url}/oauth2/authorize?client_id={client_id}&response_type=code&scope=email+openid+profile+phone+aws.cognito.signin.user.admin&redirect_uri={root_url}{redirect_uri}&identity_provider=Google")

Note: We specify auth/signin as the Oauth redirect_uri, but the auth/signin endpoint is never invoked during this process. The frontend app receives a response in the form http://localhost:4040/signin?code=3afbdddc-590b-47a5-9f7a-524fbdd1b326, but the frontend app simply extracts the code query parameter value and ignores the signin redirect URI.

sequenceDiagram
    actor client
    participant API1 as /auth/google()
    participant API2 as /auth/token()
    participant AWS1 as amazoncognito.com/oauth2/authorize
    client->>API1 : ?redirect_uri="/signin"
    API1->>client : 302: redirect to AWS
    client->>AWS1 : ?client_id=xx&redirect_uri=homeunite.us/signin&...
    AWS1->>client : 302: redirect to /signin/?code=AUTH_CODE
    client->>API2 : POST {code=AUTH_CODE}
    API2->>client : JWT
Loading

Sign up methods

Both of these methods do the same thing, despite having different names.

auth_controller.signUpHost auth_controller.signUpCoordinator

The signup methods add the user email to the local database, and use the AWS congito sign_up endpoint.

def signUpHost():  # noqa: E501
    """Signup a new Host
    """
    if connexion.request.is_json:
        body = connexion.request.get_json()

    secret_hash = current_app.calc_secret_hash(body['email'])

    # Signup user
    with DataAccessLayer.session() as session:
        user = User(email=body['email'])
        session.add(user)
        try:
            session.commit()
        except IntegrityError:
            session.rollback()
            raise AuthError({
                "message": "A user with this email already exists."
            }, 422)

    try:
        response = current_app.boto_client.sign_up(
          ClientId=current_app.config['COGNITO_CLIENT_ID'],
          SecretHash=secret_hash,
          Username=body['email'],
          Password=body['password'],
          ClientMetadata={
              'url': current_app.root_url
          }
        )

        return response

    except botocore.exceptions.ClientError as error:
        match error.response['Error']['Code']:
            case 'UsernameExistsException': 
                msg = "A user with this email already exists."
                raise AuthError({  "message": msg }, 400)
            case 'NotAuthorizedException':
                msg = "User is already confirmed."
                raise AuthError({  "message": msg }, 400)
            case 'InvalidPasswordException':
                msg = "Password did not conform with policy"
                raise AuthError({  "message": msg }, 400)
            case 'TooManyRequestsException':
                msg = "Too many requests made. Please wait before trying again."
                raise AuthError({  "message": msg }, 400)
            case _:
                msg = "An unexpected error occurred."
                raise AuthError({  "message": msg }, 400)
    except botocore.excepts.ParameterValidationError as error:
        msg = f"The parameters you provided are incorrect: {error}"
        raise AuthError({"message": msg}, 500)
    

def signUpCoordinator():  # noqa: E501
    """Signup a new Coordinator
    """
    # Exact duplicate of signUpHost()

Unnecessary Endpoints

auth_controller.private

This appears to be a test endpoint that just returns a canned response. The frontend does define a query for it, but it is never used.

Unauthenticated Account Management Endpoints

These endpoints do not require authentication.

Signout user

Signout user and invalidate the JWT w/AWS.

Uses AWS GlobalSignOut endpoint.

Guest Invite Endpoints

auth_controller.invite admin_controller.initial_sign_in_reset_password

These is used by the frontend app 'invite guest' feature. I tried using the feature with a local build but got the following error:

{'Error': 
{'Message': 
  "CustomMessage failed with error Cannot read properties of undefined (reading 'url').", 'Code': 'UserLambdaValidationException'
}, 
'ResponseMetadata': 
  {
    'RequestId': '415a3ff4-620a-4c4a-8fe8-cf988574e829', 
    'HTTPStatusCode': 400, 'HTTPHeaders': {...}, 'RetryAttempts': 0
  }, 
'message': "CustomMessage failed with error Cannot read properties of undefined (reading 'url')."}

Session Management

auth_controller.current_session auth_controller.refresh

Both of these methods call the AWS InitiateAuth endpoint, using the REFRESH_TOKEN authentication flow.

The refresh token is provided by AWS during sign-in. Our API application stores the refresh token within the flask session during the signin and token methods.

The frontend app will use the refresh endpoint if it encounters an authentication error, in an attempt to gracefully recover the session.

Is it worth adding the complexity of JWT validation, instead of simply checking if AWS Cognito API calls fail?

I believe so. Checking for AWS Cognito API call exceptions does work, but it is very inflexible. If we don't have any way of validating JWTs ourselves, then we are restricted to only using JWTs. This means that we can only ever authenticate endpoints if we have a user entry within the AWS Cognito server. In a production environment this is not a significant restriction, but it is problematic for development and testing requirements.

Development test environments should be well isolated from the production user account database, to avoid commingling throw-away test accounts with real user accounts. Dev environments should also be very predictable and free from API rate limits, since we may run a very large number of tests in a short period of time. We could technically create a development-only AWS Cognito account, but issuing and validating our own JWTs while in development provides much more flexibility.

What are options for introducing authentication with a testable design?

Of the four options, option 3 seems to be the best option. This does require introducing mock logic to our application, but using moto should make this easy and we can use the application configuration to make sure that the mock class is never used in a production environment.

As a precaution, we could strip the moto library from the production build altogether to guarantee that there is no leakage.

Option 1: (Do nothing) Use the Real Cognito Service

Pros:

  • Easy to do. No additional implementation work
  • Nothing is mocked, so our tests will properly check the integration will the real AWS service

Cons:

  • To run the app locally all developers need the AWS cognito credentials
    • This is not necessary. In the best case only our deployment script would need the real credentials.
  • Test & development users will commingle with production users
    • This can be mitigated by using a development user pool, but this would require adding a developer specific configuration
  • Slows down the test suite since our tests would be making real API calls
  • Requires networking, so tests can't run offline.
  • Our tests would need to respect the cognito API limits
    • The limits are quite high, so this is probably not a real issue

Option 2: Bypass Authentication for Testing

Pros:

  • Easy to implement
  • Removes networking calls from test environment
  • Simple to understand and maintain

Cons:

  • Does not test the actual authentication process.
  • Risk of leaking the bypass mechanism, although this can be mitigated with good practices.
  • Would prohibit testing of more advanced authentication features, such as role based access

Option 3: Mock AWS Cognito

Instead of simply bypassing authentication, we can mock the cognito idp client using the moto mocking library. Most of the AWS cognito features are implemented, and it works by running the AWS Cognito locally.

If we use a develop/production configuration variable we can cleanly isolate the mock client from the production client.

Pros:

  • No need to interact with the real AWS Cognito service.
  • Faster than hitting the real AWS endpoint.
  • Since the AWS cognito features are mocked, we can test all our authentication logic
  • No risk of commingling real and test user accounts

Cons:

  • Care needs to be taken to ensure that the mock class code does not leak into production code
  • If we try to use a AWS Cognito service not implemented by moto then we will need to implement the mocking ourselves
    • moto currently implements all the features we use, so this is not a problem currently

Option 4: Introduce a second Development / Admin Authentication System

We could develop a parallel authentication system that provides full JWT authentication, without the use of AWS Cognito. It would essentially requiring us to issue our own signed JWTs

Pros:

  • Allows you to test the full authentication and authorization stack, minus the AWS cognito portion
  • Do not commingle development and production user account
  • No need to disable any production code. The development authentication system will be a valid authentication pathway that can be maintained in the code.

Cons:

  • Lots of work. Need to implement and maintain another system.
  • Adds a lot of complexity to the application. Would also require managing additional user accounts locally. We may also decide that it is best to hide this from the public API.
  • Need to ensure parity with the Cognito-based system.

Can you apply auth to all endpoints, and opt-out the few that don't require authentication?

Yes. You can specify the global security globally by setting the security field at the root level. Endpoints that don't require authentication can 'opt-out' by setting an empty security field.

# OpenApi.yaml
components:
  securitySchemes:
    jwt:
      type: http
      scheme: bearer
      bearerFormat: JWT
      x-bearerInfoFunc: openapi_server.controllers.security_controller.requires_auth
security:
  - jwt: [] # By default endpoints will require authentication
# example opt-out endpoint
  /public-endpoint:
    get:
      security: []  # This endpoint doesn't require authentication

How does OAuth work?

We rely on AWS cognito to use OAuth2. More detail can be found on the AWS cognito authorization endpoint.

What matters for us is the OAuth endpoint returns a authorization code if the authorization was successful. This authorization code can be exchanged for a user JWT using the AWS token endpoint.

This means that we can utilize JWT authentication everywhere - regardless of the original source of the token (user/pass or OAuth).

Can moto mock all of the AWS Cognito endpoints we rely on?

We rely on these AWS congito endpoints:

Supported by Moto boto client method AWS Cognito endpoint
? oauth/authorize
? oauth/token
y initiate_auth InitiateAuth
y admin_get_user AdminCreateUser
y global_sign_out GlobalSignOut
y confirm_sign_up ConfirmSignUp
n resend_confirmation_code ResendConfirmationCode
y forgot_password ForgotPassword
y confirm_forgot_password ConfirmForgotPassword
y sign_up SignUp
y get_user GetUser
y respond_to_auth_challenge RespondToAuthChallenge
n delete_user DeleteUser

How do you use moto to mock cognito?

General Solution Plan

Current leading plan is to mock AWS cognito while running the application in develop mode.

Implementation Questions

How do create a local db that is synchronized with AWS Cognito?

Not entirely sure this is necessary yet. But if we do need to use a local database to lookup user emails, then we will need to ensure that the local db is synchronized with the AWS cognito.

Show to get public keys from AWS Cognito

We could make a request and validate the returned json ourselves, but pyjwt includes a client that can do this for us.

If the public cannot find any public keys, or if it doesn't find the public key with the corresponding kid after doing a cache refresh then it will throw a PyJWKClientError. We should report the failure to find any keys as a critical 5xx server error that needs to be fixed. If we do find some keys but none of them match the kid, then it is possible that we've just received a bad authentication token, and we should return a standard authentication failed error.

from jwt import PyJWKClient

key_url = f'https://cognito-idp.{region}.amazonaws.com/{user_pool_id}/.well-known/jwks.json'
## PyJWKClient has logic to refresh the key cache each time
## a token with an unknown kid is encountered. Also, AWS 
## does not rotate their keys every day, so there is no 
## need to refresh our cache on a frequent time interval
one_day_sec = 60*60*24
key_client = PyJWKClient(key_url, lifespan=one_hour_sec)
# This is not a string. It is actually a 
public_key = key_client.get_signing_key_from_jwt(token)

Show how to decode and verify a JWT

The PyJWT library makes this easy, especially if we retrieve the public key using PyJWKClient because the client will convert raw public key strings to PEM key format.

import jwt

def validate_jwt(encoded_jwt: str, public_key):
  '''
  Decode the encoded_jwt using a PEM public key. The 
  public key must use the proper PEM public key format
  in order to be properly recognized. 
  '''
    try:
        # Decode the token using the secret and algorithms specified
        decoded = jwt.decode(encoded_jwt, public_key, algorithms=['RS256'])
        print("Token is valid!")
        print("Decoded payload:", decoded)
    except jwt.ExpiredSignatureError:
        # Send Expired Error (should probably register
        # these exception types with connexion)
    except jwt.InvalidTokenError:
        # Send Invalid Error

Show the HUU public key endpoint

... We should mirror the AWS public keystore format. This is a well known format and the PyJWTClient can understand it.

We should be able to rotate our private key at any time, but key rotation should only be available as an admin user with terminal access to the server.

{"keys":
[{"alg":"RS256","e":"AQAB","kid":"JX1Ctjmlej6wQG+5yECO9hqw+RkyyJcP8c0/SRejmtw=","kty":"RSA","n":"64mcqYxVhAHyt06Z3lI-oQGxaP0fuNvtdCoiW9MvUSHqaVpVU0lilU5juHjWiSpmIWBmB_vWCiTJXnEUGpdPSc52AVqUiOTKyomcyBP6ay1Ec6O6BVOEzQnxrDu2ohLW0--dBwHsr9GeZPqxfOqK9jWiQJEi0-CsjmSzWUTJIMSdm5MXlQ-TipDuIWbhPd7tZoP_0XJOtCvAsLXnUYS8O-Cgo3aS6PtyfWeZAUYl_tBACgwLwNTWWLiaqDp1DQJfAl_1bnHaHNbyhRy-cXUhhjBbmwM7DAFWx8UytCekV4mf486BDPUsWIWSDzDh2X2OZkuleGwJz0S0YLGk7Hqtjw","use":"sig"} //, other keys...
]}

Show how to mock the boto delete_user method

moto does not currently support this method, so we need to implement the mocking ourselves.

Clone this wiki locally