Guest User

A script to generate a user API key on a Discourse site

a guest
Aug 22nd, 2023
215
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 3.63 KB | Source Code | 0 0
  1. # A script to generate a user API key on a Discourse site.
  2. # Authorized by author on https://shuiyuan.sjtu.edu.cn/t/topic/123808 to use this code under MIT license.
  3.  
  4. import base64
  5. import json
  6. import secrets
  7. import urllib.parse
  8. import uuid
  9. import webbrowser
  10. from collections.abc import Iterable
  11. from dataclasses import dataclass
  12.  
  13. import requests
  14. from cryptography.hazmat.primitives import serialization
  15. from cryptography.hazmat.primitives.asymmetric import padding, rsa
  16.  
  17. # From: https://github.com/discourse/discourse/blob/main/app/models/user_api_key_scope.rb
  18. ALL_SCOPES = [
  19.     'read',
  20.     'write',
  21.     'message_bus',
  22.     'push',
  23.     'one_time_password',
  24.     'notifications',
  25.     'session_info',
  26.     'bookmarks_calendar',
  27.     'user_status',
  28. ]
  29. DEFAULT_SCOPES = ['read']
  30.  
  31.  
  32. @dataclass
  33. class UserApiKeyPayload:
  34.     key: str
  35.     nonce: str
  36.     push: bool
  37.     api: int
  38.  
  39.  
  40. @dataclass
  41. class UserApiKeyRequestResult:
  42.     client_id: str
  43.     payload: UserApiKeyPayload
  44.  
  45.  
  46. # Ref:
  47. # https://meta.discourse.org/t/user-api-keys-specification/48536
  48. # https://github.com/discourse/discourse/blob/main/app/controllers/user_api_keys_controller.rb
  49. def generate_user_api_key(
  50.     site_url_base: str,
  51.     application_name: str, *,
  52.     client_id: str | None = None,
  53.     scopes: Iterable[str] | None = None,
  54. ) -> UserApiKeyRequestResult:
  55.     # Generate RSA key pair.
  56.     private_key = rsa.generate_private_key(
  57.         public_exponent=65537,
  58.         key_size=4096,
  59.     )
  60.     public_key = private_key.public_key()
  61.     public_key_pem = public_key.public_bytes(
  62.         encoding=serialization.Encoding.PEM,
  63.         format=serialization.PublicFormat.SubjectPublicKeyInfo,
  64.     ).decode('ascii')
  65.  
  66.     # Generate a random client ID if not provided.
  67.     client_id_to_use = str(uuid.uuid4()) if client_id is None else client_id
  68.     nonce = secrets.token_urlsafe(32)
  69.  
  70.     # Validate scopes.
  71.     scopes_list = DEFAULT_SCOPES if scopes is None else list(scopes)
  72.     if not set(scopes_list) <= set(ALL_SCOPES):
  73.         raise ValueError('Invalid scopes')
  74.  
  75.     # Build request URL and open in browser.
  76.     params_dict: dict[str, str] = {
  77.         'application_name': application_name,
  78.         'client_id': client_id_to_use,
  79.         'scopes': ','.join(scopes_list),
  80.         'public_key': public_key_pem,
  81.         'nonce': nonce,
  82.     }
  83.     params_str = '&'.join(f'{k}={urllib.parse.quote(v)}' for k, v in params_dict.items())
  84.     webbrowser.open(f'{site_url_base}/user-api-key/new?{params_str}')
  85.  
  86.     # Receive, decrypt and check response payload from server.
  87.     enc_payload = input('Paste the response payload here: ')
  88.     dec_payload = UserApiKeyPayload(**json.loads(private_key.decrypt(
  89.         base64.b64decode(enc_payload),
  90.         padding.PKCS1v15(),
  91.     )))
  92.     if dec_payload.nonce != nonce:
  93.         raise ValueError('Nonce mismatch')
  94.  
  95.     # Return client ID and response payload.
  96.     return UserApiKeyRequestResult(
  97.         client_id=client_id_to_use,
  98.         payload=dec_payload,
  99.     )
  100.  
  101.  
  102. def test_user_api_key(site_url_base: str, key: str) -> None:
  103.     # Get the current session information from the Discourse site.
  104.     r = requests.get(
  105.         f'{site_url_base}/session/current.json',
  106.         headers={'User-Api-Key': key},
  107.         timeout=5,
  108.     )
  109.     # Expect some results.
  110.     print(r.json())
  111.  
  112.  
  113. def main() -> None:
  114.     site_url_base = 'https://meta.discourse.org'
  115.     # Generate a user API key and test it.
  116.     result = generate_user_api_key(site_url_base, 'Sample Discourse App')
  117.     print(result)  # Store this somewhere
  118.     test_user_api_key(site_url_base, result.payload.key)
  119.  
  120.  
  121. if __name__ == '__main__':
  122.     main()
  123.  
Add Comment
Please, Sign In to add comment