Skip to main content

Package to create general API for 2factor checkers.

Project description

WebCase 2factor API

Package to create general API for 2factor checkers.

Installation

pip install wc-django-2factor

In settings.py:

INSTALLED_APPS += [
    # Dependencies:
    'pxd_admin_extensions',
    'django_jsonform',
    'wcd_settings',

    # 2Factor itself.
    'wcd_2factor',
]

WCD_2FACTOR = {
    # It will be empty by default:
    'METHODS': [
        # Simple builtin 2factor method.
        # Used to work like user secret confirmation.
        # But mostly serves as an example.
        'wcd_2factor.builtins.dummy.DUMMY_METHOD_DESCRIPTOR',
    ],
    # Custom json encoder
    'JSON_ENCODER': 'wcd_2factor.utils.types.EnvoyerJSONEncoder',
}

Usage

Confirmer

Service for confirmation state management.

from wcd_2factor.confirmer import default_confirmer, Backend, Confirmer
from wcd_2factor.registries import method_config_registry
from wcd_2factor.models import MethodConfig, UserConfig, ConfirmationState

# Default registry

# Use:
default confirmer
# Or create another.
confirmer = Confirmer(method_config_registry)

# List of all available method keys:
default_confirmer.get_methods()

# List of all active `MethodConfig` instances:
default_confirmer.get_method_configs()

# List of all user `UserConfig` configurations:
default_confirmer.get_user_configs(
    # Provide user instance:
    user=user or None,
    # Or identifier:
    user_id=user.pk or None
)


# Creates backend for some method config or `None` if there is no such:
default_confirmer.make_backend(
    # Optional, if `user_config` will be provided, since it also has a
    # relation to a MethodConfig.
    method_config=MethodConfig() or None,
    # Optional, since confirmation could be done just using the MethodConfig 
    # by itself.
    user_config=UserConfig() or None,
    # Optional context to be passed to the backend.
    context={} or None,
    # Whether should raise an exception if backend could not be created.
    # For example when there is no registered method.
    should_raise=False,
)


# Method to change user confirmation.
# Id will check if the changes are significant enough to request a 
# confirmation from user.
# If it does - `make_confirmation` - will be a callable to create new 
# `ConfirmationState` instance. Else `None` will be returned.
instance = UserConfig()
make_confirmation = default_confirmer.change_user_config(
    # Current instance to apply changes to.
    instance,
    # New configuration object. Either a pydantic object or dataclass or just a 
    # simple dict, that will be internally converted to a pydantic object.
    DTO() or dict(),
    # If you already have an initialized backend, method could use it 
    # instead of creating a new one:
    backend=Backend() or None,
    # Optional method config object. If, for example `user_config` instance 
    # doesen't have one attached yet,
    method_config=MethodConfig() or None,
    # Optional context to be passed to the backend's method.
    context={} or None,
)
# Don't forget to save your configuration instance.
# It will not be saved by this method.
instance.save()

if make_confirmation is not None:
    confirmation: ConfirmationState = make_confirmation()

  
# Requesting any type of confirmation:
confirmation: ConfirmationState = default_confirmer.request_confirmation(
    # Optional, if `user_config` will be provided, since it also has a
    # relation to a MethodConfig.
    method_config=MethodConfig() or None,
    # Optional, since confirmation could be done just using the MethodConfig 
    # by itself.
    user_config=UserConfig() or None,
    # If you already have an initialized backend, method could use it 
    # instead of creating a new one:
    backend=Backend() or None,
    # User provided state.
    # It depends on backend what kind of parameters should and should not be 
    # present.
    # In most cases if `used_config` provided - no additional information 
    # required at all.
    state={} or None,
    # Optional context to be passed to the backend's method.
    context={} or None,
)


# If you have user data to confirm some `ConfirmationState` run this:
confirmation: ConfirmationState = default_confirmer.confirm(
    # Either identifier:
    id=uuid4() or None,
    # Or confirmation object itself must be provided:
    confirmation=confirmation or None,
    # User passed data, that confirms that user have control over the 
    # "second factor":
    data={} or None,
    # If you already have an initialized backend, method could use it 
    # instead of creating a new one:
    backend=Backend() or None,
    # Optional context to be passed to the backend's method.
    context={} or None,
)
# Method might return state, event when confirmation process failed for some 
# reason.
# So check the confirmation before using it:
if not confirmation.is_available():
    raise ValueError('Confirmation failed.')


# Checks if confirmation is confirmed and available to use:
available, optional_confirmation = default_confirmer.check(
    # Either identifier:
    id=uuid4() or None,
    # Or confirmation object itself must be provided:
    confirmation=confirmation or None,
    # Optional context to be passed to the backend's method.
    context={} or None,
)
# In some cases method might return None instead of confirmation object.
# That happens when confirmation was already used, or there were none at all.
if not available:
    raise ValueError('Confirmation unavailable.')


# And the last one.
# ConfirmationState object is a "one-time" password to perform some action.
# So after usage it will be deleted from the database.
used, optional_confirmation = default_confirmer.use(
    # Either identifier:
    id=uuid4() or None,
    # Or confirmation object itself must be provided:
    confirmation=confirmation or None,
    # Optional context to be passed to the backend's method.
    context={} or None,
)
# But you will still have object returned if `used` was true.
# You might need to do something with it afterwards.
if not used:
    raise ValueError('Confirmation failed.')

Registry and custom Backends

Registry is a simple dict with some additional methods to register new confirmation methods.

For every method that could be used in your application MethodConfigDescriptor should be defined and added to registry.

For example:

from wcd_2factor.registries import (
    method_config_registry, Registry,
    MethodConfigDescriptor, DTO,
)

# This is a default method's registry. 
# It will be autopopulated with descriptors from 
# django_settings.WCD_2FACTOR['METHODS'].
method_config_registry

# But nothing stops you from creating your own registry.
my_registry = Registry()

# And after that you may add descriptors to it.
MY_METHOD_DESCRIPTOR = my_registry.register(MethodConfigDescriptor(
    # Unique method key.
    key='my_method',
    # Verbose method name.
    verbose_name=gettext_lazy('My Method'),
    # Backend class is required, since it will be used to execute every
    # `Confirmer` method.
    backend_class=Backend,
    # Other data object classes and schemas are optional:
    # MethodConfig pydantic class.
    # Configuration model for MethodConfig.
    config_global_dto=BaseModel or None,
    # JSONSchema for that configuration.
    config_global_schema=BaseModel.model_json_schema() or None,
    # Configuration model for UserConfig.
    config_user_dto=BaseModel or None,
    # JSONSchema for that configuration.
    config_user_schema=BaseModel.model_json_schema() or None,
))

But descriptor is only a simple definition with and additional configuration inside.

All the work with message sending and request confirmation are on your Backend implementation.

from wcd_2factor.confirmer import Backend
from wcd_2factor.registries import DTO, MethodConfigDescriptor
from wcd_2factor.models import ConfirmationState, UserConfig


class YourBackend(Backend):
    method_config: YourMethodDTO
    user_config: Optional[YourUserDTO]

    # Method that checks if user configuration changed.
    # And if this change is significant enough to request a confirmation.
    def change_user_config(
        self,
        # New configuration to check for changes.
        new: YourMethodDTO,
        context: Optional[dict] = None,
    ) -> Tuple[bool, Optional[dict]]:
        # Pseudocode:

        if (
            self.user_config is None
            or
            self.user_config != new
        ):
            # Then user configuration changed and confirmation with
            # some "state" should be created to confirm the change.
            return True, {'some': 'state'}

        # Otherwise - do nothing.
        return False, None

    # This is method for all confirmation requests creation.
    # Whether it's for user confirmation or not, with empty `self.user_config` 
    # and only `self.method_config` available or "fully configured"".
    def request_confirmation(
        self,
        # User or application provided state.
        state: dict,
        context: Optional[dict] = None,
    ) -> Tuple[ConfirmationState.Status, dict]:
        return ConfirmationState.Status.IN_PROCESS, {
            **state,
            'some_confirmation_token_to_check': 'value',
        }

    # To confirm saved confirmation state, `Confirmer` will call this method.
    def confirm(
        self,
        # State from `ConfirmationState` object.
        state: dict,
        # User-provided data to validate against.
        user_data: Any,
        context: Optional[dict] = None,
    ) -> bool:
        # Return True if user provided something that is somehow valid 
        # against the stored state.
        # User will never have access to the ConfigurationState data from 
        # your `request_confirmation` object.
        # At least he should not.
        return (
            state.get('some_confirmation_token_to_check')
            ==
            user_data.get('validation_token')
        )

Frontend/DRF

Library has an API implmentation based on DjangoRestFramework.

It is available in wcd_2factor.contrib.dtf module.

In urls.py:

from wcd_2factor.contrib.drf.views import make_urlpatterns as twofactor_make_urlpatterns


urlpatters = [
  # ...
  path(
    'api/v1/auth/2factor/',
    include(
      (twofactor_make_urlpatterns(), 'wcd_2factor'),
      namespace='2factor',
    )
  ),
]

And after the /api/v1/auth/2factor/ you will have several endpoints:

Method configurations

GET: method-config/list/active/ - List of active method configs.

User configurations

GET: user-config/own/list/ - List of user's configurations.

POST: user-config/own/create/ - Creating a new user configuration.

{
  // Selected global method config.
  "method_config_id": 1,
  // Configuration data. 
  "config": {"email": "ad@ad.com"},
  // 2Factor method could be deactivated by user.
  "is_active": false,
  // Setting some method as a default.
  "is_default": false,
}

POST: user-config/own/confirm/ - Confirming unconfirmed user configuration.

{
  // User config id.
  "id": 1,
  // Unconfirmed confirmation identifier.
  "confirmation_id": "uuid-confirmation-identifier-0000",
  // Data to confirm with.
  "data": {"code": "some"},
}

PUT: user-config/own/<int:pk>/update/ - Updating user configuration.

{
  // Configuration data. 
  "config": {"email": "ad@ad.com"},
  // 2Factor method could be deactivated by user.
  "is_active": false,
  // Setting some method as a default.
  "is_default": false,
}

DELETE: user-config/own/<int:pk>/destroy/ - Deletes user configuration.

Confirmation

POST: confirmation/request/ - Creating a new confirmation.

{
  // One of `method_config_id` or `user_config_id` must be provided:
  // Selected global method config. For example if user doesn't have it's own.
  "method_config_id": 1,
  // Selected user configuration method.
  "user_config_id": 1,
  // Additional data, if required. For example an "email" to confirm.
  "data": {"some": "data"},
}

Result will have a confirmation identifier. So on the frontend side it should be saved to confirm later.

GET: confirmation/{confirmation_id}/check/ - Will return current ConfirmationState status.

POST: confirmation/confirm/ - Method to confirm previously created "request".

{
  // ConfirmationState identifier.
  "id": "uuid-confirmation-identifier-0000",
  // User data, that backend will use to validate the confirmation.
  "data": {"code": "confirmation-000-code"},
}

It will return the same data as previous requests, but this time confirmation status will be confirmed, or not if confirmation failed.

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[0.2.0]

Changed

  • Total rewrite. See docs.

[0.1.7]

Added

  • Default to confirmation states admin list.
  • New django unified JSONField support.

[0.1.6]

Added

  • Translation strings.

[0.1.3]

Added

  • Admin search ui for confirmation state model.

[0.1.1]

Added

  • DEBUG_CODE_RESPONSE setting. It adds generated 'code' field to a request confirmation response for easier debug.

[0.1.0]

Initial version.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wc_django_2factor-0.2.0.tar.gz (43.4 kB view details)

Uploaded Source

File details

Details for the file wc_django_2factor-0.2.0.tar.gz.

File metadata

  • Download URL: wc_django_2factor-0.2.0.tar.gz
  • Upload date:
  • Size: 43.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.13.0

File hashes

Hashes for wc_django_2factor-0.2.0.tar.gz
Algorithm Hash digest
SHA256 459483ad65da0d71ac85799174e4b4659213dd30bc5dd4b6489453b45b8e2dc0
MD5 c7c8ddb22a6b140f48294d6e1df1fbe1
BLAKE2b-256 dbb088ef244b26ce0239f7bd37efc98dddb2eef8537ee3e36279f786b33e8a2d

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page