|
6 | 6 | # See https://aboutcode.org for more information about AboutCode FOSS projects. |
7 | 7 | # |
8 | 8 |
|
9 | | -import secrets |
| 9 | +from aboutcode.api_auth.auth import APITokenAuthentication |
| 10 | +from aboutcode.api_auth.models import AbstractAPIToken |
| 11 | +from aboutcode.api_auth.models import get_api_token_model |
10 | 12 |
|
11 | | -from django.apps import apps as django_apps |
12 | | -from django.conf import settings |
13 | | -from django.contrib.auth.hashers import check_password |
14 | | -from django.contrib.auth.hashers import make_password |
15 | | -from django.core.exceptions import ImproperlyConfigured |
16 | | -from django.db import models |
17 | | -from django.utils.translation import gettext_lazy as _ |
| 13 | +__version__ = "0.2.0" |
18 | 14 |
|
19 | | -from rest_framework.authentication import TokenAuthentication |
20 | | -from rest_framework.exceptions import AuthenticationFailed |
21 | | - |
22 | | -__version__ = "0.1.0" |
23 | | - |
24 | | - |
25 | | -class AbstractAPIToken(models.Model): |
26 | | - """ |
27 | | - API token using a lookup prefix and PBKDF2 hash for secure verification. |
28 | | -
|
29 | | - The full key is never stored. Only a short plain-text prefix is kept for |
30 | | - DB lookup, and a hashed version of the full key is stored for verification. |
31 | | - The plain key is returned once at generation time and must be stored safely |
32 | | - by the client. |
33 | | - """ |
34 | | - |
35 | | - PREFIX_LENGTH = 8 |
36 | | - |
37 | | - key_hash = models.CharField( |
38 | | - max_length=128, |
39 | | - ) |
40 | | - user = models.OneToOneField( |
41 | | - settings.AUTH_USER_MODEL, |
42 | | - related_name="api_token", |
43 | | - on_delete=models.CASCADE, |
44 | | - ) |
45 | | - prefix = models.CharField( |
46 | | - max_length=PREFIX_LENGTH, |
47 | | - unique=True, |
48 | | - db_index=True, |
49 | | - ) |
50 | | - created = models.DateTimeField( |
51 | | - auto_now_add=True, |
52 | | - db_index=True, |
53 | | - ) |
54 | | - |
55 | | - class Meta: |
56 | | - abstract = True |
57 | | - |
58 | | - def __str__(self): |
59 | | - return f"APIToken {self.prefix}... ({self.user})" |
60 | | - |
61 | | - @classmethod |
62 | | - def generate_key(cls): |
63 | | - """Generate a plain (not encrypted) key.""" |
64 | | - return secrets.token_hex(32) |
65 | | - |
66 | | - @classmethod |
67 | | - def create_token(cls, user): |
68 | | - """Generate a new token for the given user and return the plain key once.""" |
69 | | - plain_key = cls.generate_key() |
70 | | - prefix = plain_key[: cls.PREFIX_LENGTH] |
71 | | - cls.objects.create( |
72 | | - user=user, |
73 | | - prefix=prefix, |
74 | | - key_hash=make_password(plain_key), |
75 | | - ) |
76 | | - return plain_key |
77 | | - |
78 | | - @classmethod |
79 | | - def verify(cls, plain_key): |
80 | | - """Return the token instance if the plain key is valid, None otherwise.""" |
81 | | - if not plain_key: |
82 | | - return |
83 | | - |
84 | | - prefix = plain_key[: cls.PREFIX_LENGTH] |
85 | | - token = cls.objects.filter(prefix=prefix).select_related("user").first() |
86 | | - |
87 | | - if token and check_password(plain_key, token.key_hash): |
88 | | - return token |
89 | | - |
90 | | - @classmethod |
91 | | - def regenerate(cls, user): |
92 | | - """Delete any existing token instance for the user and generate a new one.""" |
93 | | - cls.objects.filter(user=user).delete() |
94 | | - return cls.create_token(user) |
95 | | - |
96 | | - @classmethod |
97 | | - def revoke(cls, user): |
98 | | - """Delete any existing token instance for the user.""" |
99 | | - return cls.objects.filter(user=user).delete() |
100 | | - |
101 | | - |
102 | | -class APITokenAuthentication(TokenAuthentication): |
103 | | - """ |
104 | | - Token authentication using a hashed API token for secure verification. |
105 | | -
|
106 | | - Extends Django REST Framework's TokenAuthentication, replacing the plain-text lookup |
107 | | - with a prefix-based lookup and PBKDF2 hash verification. |
108 | | - """ |
109 | | - |
110 | | - model = None |
111 | | - |
112 | | - def get_model(self): |
113 | | - if self.model is not None: |
114 | | - return self.model |
115 | | - |
116 | | - try: |
117 | | - return django_apps.get_model(settings.API_TOKEN_MODEL) |
118 | | - except (ValueError, LookupError): |
119 | | - raise ImproperlyConfigured("API_TOKEN_MODEL must be of the form 'app_label.model_name'") |
120 | | - |
121 | | - def authenticate_credentials(self, plain_key): |
122 | | - model = self.get_model() |
123 | | - token = model.verify(plain_key) |
124 | | - |
125 | | - if token is None: |
126 | | - raise AuthenticationFailed(_("Invalid token.")) |
127 | | - |
128 | | - if not token.user.is_active: |
129 | | - raise AuthenticationFailed(_("User inactive or deleted.")) |
130 | | - |
131 | | - return (token.user, token) |
| 15 | +__all__ = ["APITokenAuthentication", "AbstractAPIToken", "get_api_token_model"] |
0 commit comments