You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
7.3 KiB

  1. # -*- coding: utf-8 -*-
  2. # Copyright 2016 SYLEAM
  3. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
  4. import logging
  5. from datetime import datetime, timedelta
  6. from openerp import models, api, fields, exceptions, _
  7. _logger = logging.getLogger(__name__)
  8. try:
  9. from oauthlib.oauth2.rfc6749.tokens import random_token_generator
  10. except ImportError:
  11. _logger.debug('Cannot `import oauthlib`.')
  12. try:
  13. import jwt
  14. except ImportError:
  15. _logger.debug('Cannot `import jwt`.')
  16. try:
  17. from cryptography.hazmat.backends import default_backend
  18. from cryptography.hazmat.primitives.asymmetric.ec import \
  19. EllipticCurvePrivateKey
  20. from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
  21. from cryptography.hazmat.primitives.serialization import \
  22. Encoding, PublicFormat, PrivateFormat, NoEncryption, \
  23. load_pem_private_key
  24. from cryptography.hazmat.primitives.asymmetric import rsa, ec
  25. except ImportError:
  26. _logger.debug('Cannot `import cryptography`.')
  27. class OAuthProviderClient(models.Model):
  28. _inherit = 'oauth.provider.client'
  29. CRYPTOSYSTEMS = {
  30. 'ES': EllipticCurvePrivateKey,
  31. 'RS': RSAPrivateKey,
  32. 'PS': RSAPrivateKey,
  33. }
  34. token_type = fields.Selection(selection_add=[('jwt', 'JSON Web Token')])
  35. jwt_scope_id = fields.Many2one(
  36. comodel_name='oauth.provider.scope', string='Data Scope',
  37. domain=[('model_id.model', '=', 'res.users')],
  38. help='Scope executed to add some user\'s data in the token.')
  39. jwt_algorithm = fields.Selection(selection=[
  40. ('HS256', 'HMAC using SHA-256 hash algorithm'),
  41. ('HS384', 'HMAC using SHA-384 hash algorithm'),
  42. ('HS512', 'HMAC using SHA-512 hash algorithm'),
  43. ('ES256', 'ECDSA signature algorithm using SHA-256 hash algorithm'),
  44. ('ES384', 'ECDSA signature algorithm using SHA-384 hash algorithm'),
  45. ('ES512', 'ECDSA signature algorithm using SHA-512 hash algorithm'),
  46. ('RS256', 'RSASSA-PKCS1-v1_5 signature algorithm using SHA-256 hash '
  47. 'algorithm'),
  48. ('RS384', 'RSASSA-PKCS1-v1_5 signature algorithm using SHA-384 hash '
  49. 'algorithm'),
  50. ('RS512', 'RSASSA-PKCS1-v1_5 signature algorithm using SHA-512 hash '
  51. 'algorithm'),
  52. ('PS256', 'RSASSA-PSS signature using SHA-256 and MGF1 padding with '
  53. 'SHA-256'),
  54. ('PS384', 'RSASSA-PSS signature using SHA-384 and MGF1 padding with '
  55. 'SHA-384'),
  56. ('PS512', 'RSASSA-PSS signature using SHA-512 and MGF1 padding with '
  57. 'SHA-512'),
  58. ], string='Algorithm', help='Algorithm used to sign the JSON Web Token.')
  59. jwt_private_key = fields.Text(
  60. string='Private Key',
  61. help='Private key used for the JSON Web Token generation.')
  62. jwt_public_key = fields.Text(
  63. string='Public Key', compute='_compute_jwt_public_key',
  64. help='Public key used for the JSON Web Token generation.')
  65. @api.multi
  66. def _load_private_key(self):
  67. """ Load the client's private key into a cryptography's object instance
  68. """
  69. return load_pem_private_key(
  70. str(self.jwt_private_key),
  71. password=None,
  72. backend=default_backend(),
  73. )
  74. @api.multi
  75. @api.constrains('jwt_algorithm', 'jwt_private_key')
  76. def _check_jwt_private_key(self):
  77. """ Check if the private key's type matches the selected algorithm
  78. This check is only performed for asymetric algorithms
  79. """
  80. for client in self:
  81. algorithm_prefix = client.jwt_algorithm[:2]
  82. if client.jwt_private_key and \
  83. algorithm_prefix in self.CRYPTOSYSTEMS:
  84. private_key = client._load_private_key()
  85. if not isinstance(
  86. private_key, self.CRYPTOSYSTEMS[algorithm_prefix]):
  87. raise exceptions.ValidationError(
  88. _('The private key doesn\'t fit the selected '
  89. 'algorithm!'))
  90. @api.multi
  91. def generate_private_key(self):
  92. """ Generate a private key for ECDSA and RSA algorithm clients """
  93. for client in self:
  94. algorithm_prefix = client.jwt_algorithm[:2]
  95. if algorithm_prefix == 'ES':
  96. key = ec.generate_private_key(
  97. curve=ec.SECT283R1,
  98. backend=default_backend(),
  99. )
  100. elif algorithm_prefix in ('RS', 'PS'):
  101. key = rsa.generate_private_key(
  102. public_exponent=65537, key_size=2048,
  103. backend=default_backend(),
  104. )
  105. else:
  106. raise exceptions.UserError(
  107. _('You can only generate private keys for asymetric '
  108. 'algorithms!'))
  109. client.jwt_private_key = key.private_bytes(
  110. encoding=Encoding.PEM,
  111. format=PrivateFormat.TraditionalOpenSSL,
  112. encryption_algorithm=NoEncryption(),
  113. )
  114. @api.multi
  115. def _compute_jwt_public_key(self):
  116. """ Compute the public key associated to the client's private key
  117. This is only done for asymetric algorithms
  118. """
  119. for client in self:
  120. if client.jwt_private_key and \
  121. client.jwt_algorithm[:2] in self.CRYPTOSYSTEMS:
  122. private_key = client._load_private_key()
  123. client.jwt_public_key = private_key.public_key().public_bytes(
  124. Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
  125. else:
  126. client.jwt_public_key = False
  127. @api.model
  128. def _generate_jwt_payload(self, request):
  129. """ Generate a payload containing data from the client """
  130. utcnow = datetime.utcnow()
  131. data = {
  132. 'exp': utcnow + timedelta(seconds=request.expires_in),
  133. 'nbf': utcnow,
  134. 'iss': 'Odoo',
  135. 'aud': request.client.identifier,
  136. 'iat': utcnow,
  137. 'user_id': request.client.generate_user_id(request.odoo_user),
  138. }
  139. if request.client.jwt_scope_id:
  140. # Sudo as the token's user to execute the scope's filter with that
  141. # user's rights
  142. scope = request.client.jwt_scope_id.sudo(user=request.odoo_user)
  143. scope_data = scope.get_data_for_model(
  144. 'res.users', res_id=request.odoo_user.id)
  145. # Remove the user id in scope data
  146. del scope_data['id']
  147. data.update(scope_data)
  148. return data
  149. @api.multi
  150. def get_oauth2_server(self, validator=None, **kwargs):
  151. """ Add a custom JWT token generator in the server's arguments """
  152. self.ensure_one()
  153. def jwt_generator(request):
  154. """ Generate a JSON Web Token using a custom payload from the client
  155. """
  156. payload = self._generate_jwt_payload(request)
  157. return jwt.encode(
  158. payload,
  159. request.client.jwt_private_key,
  160. algorithm=request.client.jwt_algorithm,
  161. )
  162. # Add the custom generator only if none is already defined
  163. if self.token_type == 'jwt' and 'token_generator' not in kwargs:
  164. kwargs['token_generator'] = jwt_generator
  165. kwargs['refresh_token_generator'] = random_token_generator
  166. return super(OAuthProviderClient, self).get_oauth2_server(
  167. validator=validator, **kwargs)