oauth2.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. from django import forms
  2. from django.contrib import messages
  3. from django.utils.translation import gettext, gettext_lazy as _
  4. from ....admin.forms import YesNoSwitch
  5. from .base import ChangeSettingsForm
  6. OAUTH2_OPTIONAL_FIELDS = (
  7. "oauth2_token_extra_headers",
  8. "oauth2_user_extra_headers",
  9. "oauth2_send_welcome_email",
  10. "oauth2_json_avatar_path",
  11. )
  12. class ChangeOAuth2SettingsForm(ChangeSettingsForm):
  13. settings = [
  14. "enable_oauth2_client",
  15. "oauth2_provider",
  16. "oauth2_client_id",
  17. "oauth2_client_secret",
  18. "oauth2_scopes",
  19. "oauth2_login_url",
  20. "oauth2_token_url",
  21. "oauth2_token_extra_headers",
  22. "oauth2_json_token_path",
  23. "oauth2_user_url",
  24. "oauth2_user_method",
  25. "oauth2_user_token_location",
  26. "oauth2_user_token_name",
  27. "oauth2_user_extra_headers",
  28. "oauth2_send_welcome_email",
  29. "oauth2_json_id_path",
  30. "oauth2_json_name_path",
  31. "oauth2_json_email_path",
  32. "oauth2_json_avatar_path",
  33. ]
  34. enable_oauth2_client = YesNoSwitch(
  35. label=_("Enable OAuth2 client"),
  36. help_text=_(
  37. "Enabling OAuth2 will make login option redirect users to the OAuth provider "
  38. "configured below. It will also disable option to register on forum, "
  39. "change username, email or password, as those features will be delegated "
  40. "to the 3rd party site."
  41. ),
  42. )
  43. oauth2_provider = forms.CharField(
  44. label=_("Provider name"),
  45. help_text=_("Name of the OAuth 2 provider to be displayed by interface."),
  46. max_length=255,
  47. required=False,
  48. )
  49. oauth2_client_id = forms.CharField(
  50. label=_("Client ID"),
  51. max_length=200,
  52. required=False,
  53. )
  54. oauth2_client_secret = forms.CharField(
  55. label=_("Client Secret"),
  56. max_length=200,
  57. required=False,
  58. )
  59. oauth2_scopes = forms.CharField(
  60. label=_("Scopes"),
  61. help_text=_("List of scopes to request from provider, separated with spaces."),
  62. max_length=500,
  63. required=False,
  64. )
  65. oauth2_login_url = forms.URLField(
  66. label=_("Login form URL"),
  67. help_text=_(
  68. "Address to login form on provider's server that users will be "
  69. "redirected to."
  70. ),
  71. max_length=500,
  72. required=False,
  73. )
  74. oauth2_token_url = forms.URLField(
  75. label=_("Access token retrieval URL"),
  76. help_text=_(
  77. "URL that will be called after user completes the login process "
  78. "and authorization code is sent back to your site. This URL "
  79. "is expected to take this code and return the access token that "
  80. "will be next used to retrieve user data."
  81. ),
  82. max_length=500,
  83. required=False,
  84. )
  85. oauth2_token_extra_headers = forms.CharField(
  86. label=_("Extra HTTP headers in token request"),
  87. help_text=_(
  88. "List of extra headers to include in a HTTP request made to retrieve "
  89. 'the access token. Example header is "Header-name: value". Specify each '
  90. "header on separate line."
  91. ),
  92. widget=forms.Textarea(attrs={"rows": 4}),
  93. max_length=500,
  94. required=False,
  95. )
  96. oauth2_json_token_path = forms.CharField(
  97. label=_("JSON path to access token"),
  98. help_text=_(
  99. "Name of key containing the access token in JSON returned by the provider "
  100. 'If token is nested, use period (".") for path, eg: "result.token" '
  101. 'will retrieve the token from "token" key nested in "result".'
  102. ),
  103. max_length=500,
  104. required=False,
  105. )
  106. oauth2_user_url = forms.URLField(
  107. label=_("User data URL"),
  108. max_length=500,
  109. required=False,
  110. )
  111. oauth2_user_method = forms.ChoiceField(
  112. label=_("Request method"),
  113. choices=[
  114. ("POST", "POST"),
  115. ("GET", "GET"),
  116. ],
  117. widget=forms.RadioSelect(),
  118. )
  119. oauth2_user_token_location = forms.ChoiceField(
  120. label=_("Access token location"),
  121. choices=[
  122. ("QUERY", _("Query string")),
  123. ("HEADER", _("HTTP header")),
  124. ("HEADER_BEARER", _("HTTP header (Bearer)")),
  125. ],
  126. widget=forms.RadioSelect(),
  127. )
  128. oauth2_user_token_name = forms.CharField(
  129. label=_("Access token name"),
  130. max_length=200,
  131. required=False,
  132. )
  133. oauth2_user_extra_headers = forms.CharField(
  134. label=_("Extra HTTP headers in user request"),
  135. help_text=_(
  136. "List of extra headers to include in a HTTP request made to retrieve "
  137. 'the user profile. Example header is "Header-name: value". Specify each '
  138. "header on separate line."
  139. ),
  140. widget=forms.Textarea(attrs={"rows": 4}),
  141. max_length=500,
  142. required=False,
  143. )
  144. oauth2_send_welcome_email = YesNoSwitch(
  145. label=_("Send a welcoming e-mail to users on their first sign-ons"),
  146. required=False,
  147. )
  148. oauth2_json_id_path = forms.CharField(
  149. label=_("User ID path"),
  150. max_length=200,
  151. required=False,
  152. )
  153. oauth2_json_name_path = forms.CharField(
  154. label=_("User name path"),
  155. max_length=200,
  156. required=False,
  157. )
  158. oauth2_json_email_path = forms.CharField(
  159. label=_("User e-mail path"),
  160. max_length=200,
  161. required=False,
  162. )
  163. oauth2_json_avatar_path = forms.CharField(
  164. label=_("User avatar URL path"),
  165. help_text=_("Optional, leave empty to don't download avatar from provider."),
  166. max_length=200,
  167. required=False,
  168. )
  169. def clean_oauth2_scopes(self):
  170. # Remove duplicates and extra spaces, keep order of scopes
  171. clean_scopes = []
  172. for scope in self.cleaned_data["oauth2_scopes"].split():
  173. scope = scope.strip()
  174. if scope and scope not in clean_scopes:
  175. clean_scopes.append(scope)
  176. return " ".join(clean_scopes) or None
  177. def clean_oauth2_token_extra_headers(self):
  178. return clean_headers(self.cleaned_data["oauth2_token_extra_headers"])
  179. def clean_oauth2_user_extra_headers(self):
  180. return clean_headers(self.cleaned_data["oauth2_user_extra_headers"])
  181. def clean(self):
  182. data = super().clean()
  183. if not data.get("enable_oauth2_client"):
  184. return data
  185. required_data = [data[key] for key in data if key not in OAUTH2_OPTIONAL_FIELDS]
  186. if not all(required_data):
  187. data["enable_oauth2_client"] = False
  188. messages.error(
  189. self.request,
  190. gettext(
  191. "You need to complete the configuration before you will be able to "
  192. "enable OAuth 2 on your site."
  193. ),
  194. )
  195. return data
  196. def clean_headers(headers_value):
  197. clean_headers = {}
  198. for header in headers_value.splitlines():
  199. header = header.strip()
  200. if not header:
  201. continue
  202. if ":" not in header:
  203. raise forms.ValidationError(
  204. gettext(
  205. '"%(header)s" is not a valid header. '
  206. 'It\'s missing a colon (":").'
  207. )
  208. % {"header": header},
  209. )
  210. name, value = [part.strip() for part in header.split(":", 1)]
  211. if not name:
  212. raise forms.ValidationError(
  213. gettext(
  214. '"%(header)s" is not a valid header. '
  215. 'It\'s missing a header name before the colon (":").'
  216. )
  217. % {"header": header},
  218. )
  219. if name in clean_headers:
  220. raise forms.ValidationError(
  221. gettext('"%(header)s" header is entered more than once.')
  222. % {"header": name},
  223. )
  224. if not value:
  225. raise forms.ValidationError(
  226. gettext(
  227. '"%(header)s" is not a valid header. '
  228. 'It\'s missing a header value after the colon (":").'
  229. )
  230. % {"header": header},
  231. )
  232. clean_headers[name] = value
  233. return "\n".join([f"{key}: {value}" for key, value in clean_headers.items()])