authnzerver.actions.apikey_nosession module

This contains functions to drive API key related auth actions.

API keys generated by this module do not require existing user sessions, so are useful for backend API services. They are shorter-lived than API keys tied to sessions, so come with a refresh token, which can be used to fetch a new no-session API key.

The workflow to use here is:

  1. Hit the login endpoint. Make sure the endpoint uses XSRF protection. For Tornado generated pages, this is fairly easy: use xsrf_form_html() in a template to add a form field for the token. An AJAX call can pick this up from the form and send it in the POST request as the _xsrf parameter. For an SPA, this is more difficult. For a Tornado app, the static HTML of the SPA endpoint can be served using a StaticFileHandler that makes sure to set the _xsrf cookie (session-only scoped) by calling self.xsrf_cookie in the initialize() or prepare() method (this sets the cookie as a side-effect). That way, any response back from the SPA endpoint will have the _xsrf cookie (this is not HttpOnly), and the POST request can then read the cookie and send it back as verification in the request header or POST request params.
  2. After login is verified, run the issue_apikey() function below. This returns an API key and a refresh token. The API key should have an expiry no longer than 15 minutes (or about the time needed to process a single API call). The refresh token has a longer expiry time (no longer than 24 hours or maybe the actual session lifetime) to allow for the user coming back and having to fetch a new API key. The refresh token is effectively a password, and the authnzerver stores it as such, generating a random 32-byte token, then using Argon2-ID to hash and store it in the DB. To verify it, we run the Argon2-ID hash as we do for passwords.
  3. For SPAs, send back the API key and its expiry date in the response body, and set the refresh token as an HttpOnly, Secure cookie, with the TTL set to the expiry date of the refresh token. For API-only calls, the refresh token will have to be sent in the JSON response body. HTTPS is required in all cases.
  4. The client can then use the no-session API key as normal until it expires. The verification of the API key itself can take place statelessly if it is decrypted correctly, has not expired, and the the claims in the decrypted key dict match the API endpoint’s requirements. If further verification is required, the frontend can call verify_apikey(), which will check the API key against the one stored in the DB.
  5. A bit before the API key expires, the client can hit an refresh-api-key endpoint on the frontend that is dedicated to refreshing the API key. The refresh token is presented in the cookie to the endpoint so the refresh request can be authenticated.
  6. Use the refresh_apikey() function to refresh the API key. The refresh token presented is verified, and if that passes, a new API key + its expiry is sent back to the client, and a NEW refresh token MUST ALSO be set as an HttpOnly cookie if the client is an SPA (send back the refresh token in the response body if not).
  7. To enforce logout or account deletion or lock, the API key and the refresh token are deleted from the apikeys_nosession table by the revoke_apikey() or revoke_all_apikeys() function below. The refresh token cookie MUST also be deleted from the client if it hits the logout/delete endpoint successfully.

References

authnzerver.actions.apikey_nosession.issue_apikey(payload: dict, raiseonfail: bool = False, override_authdb_path: str = None, override_permissions_json: str = None, config: types.SimpleNamespace = None) → dict[source]

Issues a new API key.

This version does not require a session.

Parameters:
  • payload (dict) –

    The payload dict must have the following keys:

    • issuer: str, the entity that will be designated as the API key issuer
    • audience: str, the service this API key is being issued for
    • subject: str, the specific API endpoint API key is being issued for
    • apiversion: int or str, the API version that the API key is valid for
    • expires_seconds: int, the number of seconds after which the API key expires
    • not_valid_before: float or int, the amount of seconds after utcnow() when the API key becomes valid
    • user_id: int, the user ID of the user requesting the API key
    • user_role: str, the user role of the user requesting the API key
    • ip_address: str, the IP address to tie the API key to
    • refresh_expires: int, the number of seconds after which the API key’s refresh token expires
    • refresh_nbf: float or int, the amount of seconds after utcnow() after which the refresh token becomes valid
  • raiseonfail (bool) – If True, will raise an Exception if something goes wrong.
  • override_authdb_path (str or None) – If given as a str, is the alternative path to the auth DB.
  • override_permissions_json (str or None) – If given as a str, is the alternative path to the permissions JSON to use. This is used to check if the user_id is allowed to actually request an API key.
  • config (SimpleNamespace object or None) – An object containing systemwide config variables as attributes. This is useful when the wrapping function needs to pass in some settings directly from environment variables.
Returns:

The dict returned is of the form:

{'success': True or False,
 'apikey': apikey dict,
 'expires': expiry datetime in ISO format,
 'refresh_token': refresh token str,
 'refresh_token_expires': expiry of refresh token in ISO format,
 'messages': list of str messages if any}

Return type:

dict

Notes

API keys are tied to an IP address, user ID, and role.

This function will return a dict with all the API key information. This entire dict should be serialized to JSON, encrypted and time-stamp signed by the frontend as the final “API key”, and finally sent back to the client.

authnzerver.actions.apikey_nosession.refresh_apikey(payload: dict, raiseonfail: bool = False, override_authdb_path: str = None, override_permissions_json: str = None, config: types.SimpleNamespace = None)[source]

Refreshes a no-session API key.

Requires a refresh token.

Parameters:
  • payload (dict) –

    This dict contains the following keys:

    • apikey_dict: the decrypted and verified API key info dict from the frontend.
    • user_id: the user ID of the person revoking this key. Only superusers or staff can revoke an API key that doesn’t belong to them.
    • user_role: the user ID of the person revoking this key. Only superusers or staff can revoke an API key that doesn’t belong to them.
    • refresh_token: the refresh token needed to refresh the API key
    • ip_address: the current IP address of the user
    • expires_seconds: int, the number of seconds after which the API key expires
    • not_valid_before: float or int, the amount of seconds after utcnow() when the API key becomes valid
    • refresh_expires: int, the number of seconds after which the API key’s refresh token expires
    • refresh_nbf: float or int, the amount of seconds after utcnow() after which the refresh token becomes valid
  • raiseonfail (bool) – If True, will raise an Exception if something goes wrong.
  • override_authdb_path (str or None) – If given as a str, is the alternative path to the auth DB.
  • override_permissions_json (str or None) – If given as a str, is the alternative path to the permissions JSON to use. This is used to check if the user_id is allowed to actually refresh (“delete” then “create”) an API key.
  • config (SimpleNamespace object or None) – An object containing systemwide config variables as attributes. This is useful when the wrapping function needs to pass in some settings directly from environment variables.
Returns:

The dict returned is of the form:

{'success': True if API key was revoked and False otherwise,
 'messages': list of str messages if any}

Return type:

dict

authnzerver.actions.apikey_nosession.revoke_all_apikeys(payload: dict, raiseonfail: bool = False, override_authdb_path: str = None, override_permissions_json: str = None, config: types.SimpleNamespace = None) → dict[source]

Revokes an API key.

This does not require a session, but does require a current valid and unexpired API key to revoke all API keys belonging to the specified user.

Parameters:
  • payload (dict) –

    This dict contains the following keys:

    • apikey_dict: the decrypted and verified API key info dict from the frontend.
    • user_id: the user ID of the person revoking this key. Only superusers or staff can revoke an API key that doesn’t belong to them.
    • user_role: the user ID of the person revoking this key. Only superusers or staff can revoke an API key that doesn’t belong to them.
  • raiseonfail (bool) – If True, will raise an Exception if something goes wrong.
  • override_authdb_path (str or None) – If given as a str, is the alternative path to the auth DB.
  • override_permissions_json (str or None) – If given as a str, is the alternative path to the permissions JSON to use. This is used to check if the user_id is allowed to actually revoke (“delete”) an API key.
  • config (SimpleNamespace object or None) – An object containing systemwide config variables as attributes. This is useful when the wrapping function needs to pass in some settings directly from environment variables.
Returns:

The dict returned is of the form:

{'success': True if API key was revoked and False otherwise,
 'messages': list of str messages if any}

Return type:

dict

authnzerver.actions.apikey_nosession.revoke_apikey(payload: dict, raiseonfail: bool = False, override_authdb_path: str = None, override_permissions_json: str = None, config: types.SimpleNamespace = None)[source]

Revokes an API key.

This does not require a session.

Parameters:
  • payload (dict) –

    This dict contains the following keys:

    • apikey_dict: the decrypted and verified API key info dict from the frontend.
    • user_id: the user ID of the person revoking this key. Only superusers or staff can revoke an API key that doesn’t belong to them.
    • user_role: the user ID of the person revoking this key. Only superusers or staff can revoke an API key that doesn’t belong to them.
  • raiseonfail (bool) – If True, will raise an Exception if something goes wrong.
  • override_authdb_path (str or None) – If given as a str, is the alternative path to the auth DB.
  • override_permissions_json (str or None) – If given as a str, is the alternative path to the permissions JSON to use. This is used to check if the user_id is allowed to actually revoke (“delete”) an API key.
  • config (SimpleNamespace object or None) – An object containing systemwide config variables as attributes. This is useful when the wrapping function needs to pass in some settings directly from environment variables.
Returns:

The dict returned is of the form:

{'success': True if API key was revoked and False otherwise,
 'messages': list of str messages if any}

Return type:

dict

authnzerver.actions.apikey_nosession.verify_apikey(payload: dict, raiseonfail: bool = False, override_authdb_path: str = None, override_permissions_json: str = None, config: types.SimpleNamespace = None) → dict[source]

Checks if an API key is valid.

This version does not require a session.

Parameters:
  • payload (dict) –

    This dict contains a single key:

    • apikey_dict: the decrypted and verified API key info dict from the frontend.
    • user_id: the user ID of the person wanting to verify this key.
    • user_role: the user role of the person wanting to verify this key.
  • raiseonfail (bool) – If True, will raise an Exception if something goes wrong.
  • override_authdb_path (str or None) – If given as a str, is the alternative path to the auth DB.
  • override_permissions_json (str or None) – If given as a str, is the alternative path to the permissions JSON to use. This is used to check if the user_id is allowed to actually verify (“read”) an API key.
  • config (SimpleNamespace object or None) – An object containing systemwide config variables as attributes. This is useful when the wrapping function needs to pass in some settings directly from environment variables.
Returns:

The dict returned is of the form:

{'success': True if API key is OK and False otherwise,
 'messages': list of str messages if any}

Return type:

dict