Merge branch 'master' into merge-python3-port
[mediagoblin.git] / mediagoblin / oauth / views.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17 import datetime
18
19 import six
20
21 from oauthlib.oauth1.rfc5849.utils import UNICODE_ASCII_CHARACTER_SET
22 from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
23 AccessTokenEndpoint)
24
25 from mediagoblin.decorators import require_active_login
26 from mediagoblin.tools.translate import pass_to_ugettext
27 from mediagoblin.meddleware.csrf import csrf_exempt
28 from mediagoblin.tools.request import decode_request
29 from mediagoblin.tools.response import (render_to_response, redirect,
30 json_response, render_400,
31 form_response)
32 from mediagoblin.tools.crypto import random_string
33 from mediagoblin.tools.validator import validate_email, validate_url
34 from mediagoblin.oauth.forms import AuthorizeForm
35 from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest
36 from mediagoblin.oauth.tools.request import decode_authorization_header
37 from mediagoblin.oauth.tools.forms import WTFormData
38 from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
39
40 # possible client types
41 CLIENT_TYPES = ["web", "native"] # currently what pump supports
42
43 @csrf_exempt
44 def client_register(request):
45 """ Endpoint for client registration """
46 try:
47 data = decode_request(request)
48 except ValueError:
49 error = "Could not decode data."
50 return json_response({"error": error}, status=400)
51
52 if data is "":
53 error = "Unknown Content-Type"
54 return json_response({"error": error}, status=400)
55
56 if "type" not in data:
57 error = "No registration type provided."
58 return json_response({"error": error}, status=400)
59 if data.get("application_type", None) not in CLIENT_TYPES:
60 error = "Unknown application_type."
61 return json_response({"error": error}, status=400)
62
63 client_type = data["type"]
64
65 if client_type == "client_update":
66 # updating a client
67 if "client_id" not in data:
68 error = "client_id is requried to update."
69 return json_response({"error": error}, status=400)
70 elif "client_secret" not in data:
71 error = "client_secret is required to update."
72 return json_response({"error": error}, status=400)
73
74 client = Client.query.filter_by(
75 id=data["client_id"],
76 secret=data["client_secret"]
77 ).first()
78
79 if client is None:
80 error = "Unauthorized."
81 return json_response({"error": error}, status=403)
82
83 client.application_name = data.get(
84 "application_name",
85 client.application_name
86 )
87
88 client.application_type = data.get(
89 "application_type",
90 client.application_type
91 )
92
93 app_name = ("application_type", client.application_name)
94 if app_name in CLIENT_TYPES:
95 client.application_name = app_name
96
97 elif client_type == "client_associate":
98 # registering
99 if "client_id" in data:
100 error = "Only set client_id for update."
101 return json_response({"error": error}, status=400)
102 elif "access_token" in data:
103 error = "access_token not needed for registration."
104 return json_response({"error": error}, status=400)
105 elif "client_secret" in data:
106 error = "Only set client_secret for update."
107 return json_response({"error": error}, status=400)
108
109 # generate the client_id and client_secret
110 client_id = random_string(22, UNICODE_ASCII_CHARACTER_SET)
111 client_secret = random_string(43, UNICODE_ASCII_CHARACTER_SET)
112 expirey = 0 # for now, lets not have it expire
113 expirey_db = None if expirey == 0 else expirey
114 application_type = data["application_type"]
115
116 # save it
117 client = Client(
118 id=client_id,
119 secret=client_secret,
120 expirey=expirey_db,
121 application_type=application_type,
122 )
123
124 else:
125 error = "Invalid registration type"
126 return json_response({"error": error}, status=400)
127
128 logo_url = data.get("logo_url", client.logo_url)
129 if logo_url is not None and not validate_url(logo_url):
130 error = "Logo URL {0} is not a valid URL.".format(logo_url)
131 return json_response(
132 {"error": error},
133 status=400
134 )
135 else:
136 client.logo_url = logo_url
137
138 client.application_name = data.get("application_name", None)
139
140 contacts = data.get("contacts", None)
141 if contacts is not None:
142 if not isinstance(contacts, six.text_type):
143 error = "Contacts must be a string of space-seporated email addresses."
144 return json_response({"error": error}, status=400)
145
146 contacts = contacts.split()
147 for contact in contacts:
148 if not validate_email(contact):
149 # not a valid email
150 error = "Email {0} is not a valid email.".format(contact)
151 return json_response({"error": error}, status=400)
152
153
154 client.contacts = contacts
155
156 redirect_uris = data.get("redirect_uris", None)
157 if redirect_uris is not None:
158 if not isinstance(redirect_uris, six.text_type):
159 error = "redirect_uris must be space-seporated URLs."
160 return json_response({"error": error}, status=400)
161
162 redirect_uris = redirect_uris.split()
163
164 for uri in redirect_uris:
165 if not validate_url(uri):
166 # not a valid uri
167 error = "URI {0} is not a valid URI".format(uri)
168 return json_response({"error": error}, status=400)
169
170 client.redirect_uri = redirect_uris
171
172
173 client.save()
174
175 expirey = 0 if client.expirey is None else client.expirey
176
177 return json_response(
178 {
179 "client_id": client.id,
180 "client_secret": client.secret,
181 "expires_at": expirey,
182 })
183
184 @csrf_exempt
185 def request_token(request):
186 """ Returns request token """
187 try:
188 data = decode_request(request)
189 except ValueError:
190 error = "Could not decode data."
191 return json_response({"error": error}, status=400)
192
193 if data == "":
194 error = "Unknown Content-Type"
195 return json_response({"error": error}, status=400)
196
197 if not data and request.headers:
198 data = request.headers
199
200 data = dict(data) # mutableifying
201
202 authorization = decode_authorization_header(data)
203
204 if authorization == dict() or u"oauth_consumer_key" not in authorization:
205 error = "Missing required parameter."
206 return json_response({"error": error}, status=400)
207
208 # check the client_id
209 client_id = authorization[u"oauth_consumer_key"]
210 client = Client.query.filter_by(id=client_id).first()
211
212 if client == None:
213 # client_id is invalid
214 error = "Invalid client_id"
215 return json_response({"error": error}, status=400)
216
217 # make request token and return to client
218 request_validator = GMGRequestValidator(authorization)
219 rv = RequestTokenEndpoint(request_validator)
220 tokens = rv.create_request_token(request, authorization)
221
222 # store the nonce & timestamp before we return back
223 nonce = authorization[u"oauth_nonce"]
224 timestamp = authorization[u"oauth_timestamp"]
225 timestamp = datetime.datetime.fromtimestamp(float(timestamp))
226
227 nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
228 nc.save()
229
230 return form_response(tokens)
231
232 @require_active_login
233 def authorize(request):
234 """ Displays a page for user to authorize """
235 if request.method == "POST":
236 return authorize_finish(request)
237
238 _ = pass_to_ugettext
239 token = request.args.get("oauth_token", None)
240 if token is None:
241 # no token supplied, display a html 400 this time
242 err_msg = _("Must provide an oauth_token.")
243 return render_400(request, err_msg=err_msg)
244
245 oauth_request = RequestToken.query.filter_by(token=token).first()
246 if oauth_request is None:
247 err_msg = _("No request token found.")
248 return render_400(request, err_msg)
249
250 if oauth_request.used:
251 return authorize_finish(request)
252
253 if oauth_request.verifier is None:
254 orequest = GMGRequest(request)
255 orequest.resource_owner_key = token
256 request_validator = GMGRequestValidator()
257 auth_endpoint = AuthorizationEndpoint(request_validator)
258 verifier = auth_endpoint.create_verifier(orequest, {})
259 oauth_request.verifier = verifier["oauth_verifier"]
260
261 oauth_request.user = request.user.id
262 oauth_request.save()
263
264 # find client & build context
265 client = Client.query.filter_by(id=oauth_request.client).first()
266
267 authorize_form = AuthorizeForm(WTFormData({
268 "oauth_token": oauth_request.token,
269 "oauth_verifier": oauth_request.verifier
270 }))
271
272 context = {
273 "user": request.user,
274 "oauth_request": oauth_request,
275 "client": client,
276 "authorize_form": authorize_form,
277 }
278
279
280 # AuthorizationEndpoint
281 return render_to_response(
282 request,
283 "mediagoblin/api/authorize.html",
284 context
285 )
286
287
288 def authorize_finish(request):
289 """ Finishes the authorize """
290 _ = pass_to_ugettext
291 token = request.form["oauth_token"]
292 verifier = request.form["oauth_verifier"]
293 oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
294 oauth_request = oauth_request.first()
295
296 if oauth_request is None:
297 # invalid token or verifier
298 err_msg = _("No request token found.")
299 return render_400(request, err_msg)
300
301 oauth_request.used = True
302 oauth_request.updated = datetime.datetime.now()
303 oauth_request.save()
304
305 if oauth_request.callback == "oob":
306 # out of bounds
307 context = {"oauth_request": oauth_request}
308 return render_to_response(
309 request,
310 "mediagoblin/api/oob.html",
311 context
312 )
313
314 # okay we need to redirect them then!
315 querystring = "?oauth_token={0}&oauth_verifier={1}".format(
316 oauth_request.token,
317 oauth_request.verifier
318 )
319
320 return redirect(
321 request,
322 querystring=querystring,
323 location=oauth_request.callback
324 )
325
326 @csrf_exempt
327 def access_token(request):
328 """ Provides an access token based on a valid verifier and request token """
329 data = request.headers
330
331 parsed_tokens = decode_authorization_header(data)
332
333 if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
334 error = "Missing required parameter."
335 return json_response({"error": error}, status=400)
336
337 request.resource_owner_key = parsed_tokens["oauth_consumer_key"]
338 request.oauth_token = parsed_tokens["oauth_token"]
339 request_validator = GMGRequestValidator(data)
340 av = AccessTokenEndpoint(request_validator)
341 tokens = av.create_access_token(request, {})
342 return form_response(tokens)