First Commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.misc import consteq
|
||||
|
||||
|
||||
class ApiClient(models.Model):
|
||||
_name = 'api.client'
|
||||
_description = 'API Client for JWT Authentication'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(string='Application Name', required=True)
|
||||
active = fields.Boolean(default=True)
|
||||
client_id = fields.Char(
|
||||
string='Client ID',
|
||||
required=True,
|
||||
default=lambda self: secrets.token_urlsafe(24),
|
||||
copy=False,
|
||||
readonly=True,
|
||||
)
|
||||
client_secret = fields.Char(
|
||||
string='New Client Secret',
|
||||
copy=False,
|
||||
store=False,
|
||||
help='Shown only before saving. Use Regenerate Secret to rotate it.',
|
||||
)
|
||||
client_secret_hash = fields.Char(string='Client Secret Hash', copy=False, readonly=True)
|
||||
user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Odoo User',
|
||||
required=True,
|
||||
domain=[('active', '=', True)],
|
||||
help='Odoo user mapped to JWT tokens issued for this client.',
|
||||
)
|
||||
token_ttl = fields.Integer(string='Token Lifetime (seconds)', default=7200, required=True)
|
||||
last_used = fields.Datetime(readonly=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('client_id_unique', 'unique(client_id)', 'Client ID must be unique.'),
|
||||
('token_ttl_positive', 'check(token_ttl > 0)', 'Token lifetime must be positive.'),
|
||||
]
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
generated_secrets = []
|
||||
for vals in vals_list:
|
||||
plain_secret = vals.pop('client_secret', None) or self._generate_secret()
|
||||
vals['client_secret_hash'] = self._hash_secret(plain_secret)
|
||||
generated_secrets.append(plain_secret)
|
||||
records = super().create(vals_list)
|
||||
for record, plain_secret in zip(records, generated_secrets):
|
||||
record.client_secret = plain_secret
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
if 'client_secret' in vals:
|
||||
plain_secret = vals.pop('client_secret')
|
||||
if plain_secret:
|
||||
vals['client_secret_hash'] = self._hash_secret(plain_secret)
|
||||
return super().write(vals)
|
||||
|
||||
def action_regenerate_secret(self):
|
||||
for client in self:
|
||||
plain_secret = self._generate_secret()
|
||||
client.write({'client_secret': plain_secret})
|
||||
client.client_secret = plain_secret
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Client secret regenerated',
|
||||
'message': 'Copy the new secret from the form before leaving this page.',
|
||||
'type': 'warning',
|
||||
'sticky': True,
|
||||
},
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _generate_secret(self):
|
||||
return secrets.token_urlsafe(48)
|
||||
|
||||
@api.model
|
||||
def _hash_secret(self, secret):
|
||||
if not secret:
|
||||
raise UserError('Client secret cannot be empty.')
|
||||
iterations = 260000
|
||||
salt = secrets.token_bytes(16)
|
||||
digest = hashlib.pbkdf2_hmac('sha256', secret.encode(), salt, iterations)
|
||||
return '$'.join((
|
||||
'pbkdf2_sha256',
|
||||
str(iterations),
|
||||
base64.urlsafe_b64encode(salt).decode(),
|
||||
base64.urlsafe_b64encode(digest).decode(),
|
||||
))
|
||||
|
||||
def _check_secret(self, secret):
|
||||
self.ensure_one()
|
||||
try:
|
||||
scheme, iterations, salt, expected = self.client_secret_hash.split('$', 3)
|
||||
if scheme != 'pbkdf2_sha256':
|
||||
return False
|
||||
digest = hashlib.pbkdf2_hmac(
|
||||
'sha256',
|
||||
secret.encode(),
|
||||
base64.urlsafe_b64decode(salt.encode()),
|
||||
int(iterations),
|
||||
)
|
||||
return consteq(base64.urlsafe_b64encode(digest).decode(), expected)
|
||||
except Exception:
|
||||
return False
|
||||
Reference in New Issue
Block a user