oauth2.py 8.4 KB

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