Fixes for small bugs
[mediagoblin.git] / mediagoblin / oauth / views.py
CommitLineData
c840cb66 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
405aa45a 17import datetime
060a7a7b 18import urllib
4990b47c 19
20238f54
BP
20import six
21
32ff6f4d 22from oauthlib.oauth1.rfc5849.utils import UNICODE_ASCII_CHARACTER_SET
617bff18
JT
23from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
24 AccessTokenEndpoint)
42dbb26a 25
617bff18 26from mediagoblin.decorators import require_active_login
d41c6a53 27from mediagoblin.tools.translate import pass_to_ugettext
4990b47c 28from mediagoblin.meddleware.csrf import csrf_exempt
786bbd79 29from mediagoblin.tools.request import decode_request
42dbb26a 30from mediagoblin.tools.response import (render_to_response, redirect,
2b60a56c 31 json_response, render_400,
32 form_response)
4990b47c 33from mediagoblin.tools.crypto import random_string
c33a34d4 34from mediagoblin.tools.validator import validate_email, validate_url
005181b1 35from mediagoblin.oauth.forms import AuthorizeForm
36from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest
37from mediagoblin.oauth.tools.request import decode_authorization_header
38from mediagoblin.oauth.tools.forms import WTFormData
617bff18 39from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
c840cb66 40
41# possible client types
c5eb24b8 42CLIENT_TYPES = ["web", "native"] # currently what pump supports
c840cb66 43
4990b47c 44@csrf_exempt
c840cb66 45def client_register(request):
46 """ Endpoint for client registration """
d41c6a53 47 try:
42dbb26a 48 data = decode_request(request)
d41c6a53 49 except ValueError:
50 error = "Could not decode data."
51 return json_response({"error": error}, status=400)
52
53 if data is "":
54 error = "Unknown Content-Type"
55 return json_response({"error": error}, status=400)
c840cb66 56
4990b47c 57 if "type" not in data:
d41c6a53 58 error = "No registration type provided."
59 return json_response({"error": error}, status=400)
c5eb24b8 60 if data.get("application_type", None) not in CLIENT_TYPES:
d41c6a53 61 error = "Unknown application_type."
62 return json_response({"error": error}, status=400)
42dbb26a 63
54fbbf09 64 client_type = data["type"]
65
66 if client_type == "client_update":
67 # updating a client
68 if "client_id" not in data:
d41c6a53 69 error = "client_id is requried to update."
70 return json_response({"error": error}, status=400)
54fbbf09 71 elif "client_secret" not in data:
d41c6a53 72 error = "client_secret is required to update."
73 return json_response({"error": error}, status=400)
54fbbf09 74
d41c6a53 75 client = Client.query.filter_by(
42dbb26a 76 id=data["client_id"],
d41c6a53 77 secret=data["client_secret"]
78 ).first()
54fbbf09 79
c33a34d4 80 if client is None:
d41c6a53 81 error = "Unauthorized."
82 return json_response({"error": error}, status=403)
83
84 client.application_name = data.get(
42dbb26a 85 "application_name",
d41c6a53 86 client.application_name
87 )
88
89 client.application_type = data.get(
90 "application_type",
91 client.application_type
92 )
54fbbf09 93
763e300d 94 app_name = ("application_type", client.application_name)
c5eb24b8 95 if app_name in CLIENT_TYPES:
763e300d 96 client.application_name = app_name
763e300d 97
54fbbf09 98 elif client_type == "client_associate":
99 # registering
100 if "client_id" in data:
d41c6a53 101 error = "Only set client_id for update."
102 return json_response({"error": error}, status=400)
54fbbf09 103 elif "access_token" in data:
d41c6a53 104 error = "access_token not needed for registration."
105 return json_response({"error": error}, status=400)
54fbbf09 106 elif "client_secret" in data:
d41c6a53 107 error = "Only set client_secret for update."
108 return json_response({"error": error}, status=400)
54fbbf09 109
c33a34d4 110 # generate the client_id and client_secret
32ff6f4d
JT
111 client_id = random_string(22, UNICODE_ASCII_CHARACTER_SET)
112 client_secret = random_string(43, UNICODE_ASCII_CHARACTER_SET)
c33a34d4 113 expirey = 0 # for now, lets not have it expire
114 expirey_db = None if expirey == 0 else expirey
42dbb26a
RE
115 application_type = data["application_type"]
116
c33a34d4 117 # save it
118 client = Client(
42dbb26a
RE
119 id=client_id,
120 secret=client_secret,
c33a34d4 121 expirey=expirey_db,
86ba4168 122 application_type=application_type,
d41c6a53 123 )
c33a34d4 124
125 else:
d41c6a53 126 error = "Invalid registration type"
127 return json_response({"error": error}, status=400)
c33a34d4 128
670cdef7
JT
129 logo_uri = data.get("logo_uri", client.logo_url)
130 if logo_uri is not None and not validate_url(logo_uri):
131 error = "Logo URI {0} is not a valid URI.".format(logo_uri)
d41c6a53 132 return json_response(
42dbb26a 133 {"error": error},
d41c6a53 134 status=400
135 )
c33a34d4 136 else:
670cdef7 137 client.logo_url = logo_uri
42dbb26a 138
405aa45a 139 client.application_name = data.get("application_name", None)
c33a34d4 140
86ba4168 141 contacts = data.get("contacts", None)
c33a34d4 142 if contacts is not None:
20238f54 143 if not isinstance(contacts, six.text_type):
d41c6a53 144 error = "Contacts must be a string of space-seporated email addresses."
145 return json_response({"error": error}, status=400)
c33a34d4 146
147 contacts = contacts.split()
148 for contact in contacts:
149 if not validate_email(contact):
150 # not a valid email
d41c6a53 151 error = "Email {0} is not a valid email.".format(contact)
152 return json_response({"error": error}, status=400)
42dbb26a
RE
153
154
c33a34d4 155 client.contacts = contacts
156
86ba4168 157 redirect_uris = data.get("redirect_uris", None)
158 if redirect_uris is not None:
20238f54 159 if not isinstance(redirect_uris, six.text_type):
d41c6a53 160 error = "redirect_uris must be space-seporated URLs."
617bff18 161 return json_response({"error": error}, status=400)
c33a34d4 162
86ba4168 163 redirect_uris = redirect_uris.split()
c33a34d4 164
86ba4168 165 for uri in redirect_uris:
c33a34d4 166 if not validate_url(uri):
167 # not a valid uri
d41c6a53 168 error = "URI {0} is not a valid URI".format(uri)
169 return json_response({"error": error}, status=400)
c33a34d4 170
86ba4168 171 client.redirect_uri = redirect_uris
c33a34d4 172
42dbb26a 173
4990b47c 174 client.save()
c840cb66 175
c33a34d4 176 expirey = 0 if client.expirey is None else client.expirey
177
4990b47c 178 return json_response(
179 {
d41c6a53 180 "client_id": client.id,
181 "client_secret": client.secret,
182 "expires_at": expirey,
4990b47c 183 })
d41c6a53 184
d41c6a53 185@csrf_exempt
186def request_token(request):
187 """ Returns request token """
188 try:
42dbb26a 189 data = decode_request(request)
d41c6a53 190 except ValueError:
191 error = "Could not decode data."
192 return json_response({"error": error}, status=400)
193
2b60a56c 194 if not data and request.headers:
195 data = request.headers
42dbb26a 196
2b60a56c 197 data = dict(data) # mutableifying
405aa45a 198
2b60a56c 199 authorization = decode_authorization_header(data)
d41c6a53 200
2b60a56c 201 if authorization == dict() or u"oauth_consumer_key" not in authorization:
405aa45a 202 error = "Missing required parameter."
2b60a56c 203 return json_response({"error": error}, status=400)
405aa45a 204
d41c6a53 205 # check the client_id
2b60a56c 206 client_id = authorization[u"oauth_consumer_key"]
d41c6a53 207 client = Client.query.filter_by(id=client_id).first()
89d5b44e 208
209 if client == None:
d41c6a53 210 # client_id is invalid
211 error = "Invalid client_id"
212 return json_response({"error": error}, status=400)
213
e9bb5879 214 # make request token and return to client
2b60a56c 215 request_validator = GMGRequestValidator(authorization)
d41c6a53 216 rv = RequestTokenEndpoint(request_validator)
405aa45a 217 tokens = rv.create_request_token(request, authorization)
d41c6a53 218
cfe7054c 219 # store the nonce & timestamp before we return back
220 nonce = authorization[u"oauth_nonce"]
221 timestamp = authorization[u"oauth_timestamp"]
89d5b44e 222 timestamp = datetime.datetime.fromtimestamp(float(timestamp))
cfe7054c 223
224 nc = NonceTimestamp(nonce=nonce, timestamp=timestamp)
225 nc.save()
226
2b60a56c 227 return form_response(tokens)
405aa45a 228
42dbb26a 229@require_active_login
d41c6a53 230def authorize(request):
231 """ Displays a page for user to authorize """
405aa45a 232 if request.method == "POST":
233 return authorize_finish(request)
42dbb26a 234
d41c6a53 235 _ = pass_to_ugettext
236 token = request.args.get("oauth_token", None)
237 if token is None:
238 # no token supplied, display a html 400 this time
405aa45a 239 err_msg = _("Must provide an oauth_token.")
d41c6a53 240 return render_400(request, err_msg=err_msg)
241
405aa45a 242 oauth_request = RequestToken.query.filter_by(token=token).first()
243 if oauth_request is None:
244 err_msg = _("No request token found.")
245 return render_400(request, err_msg)
42dbb26a 246
405aa45a 247 if oauth_request.used:
248 return authorize_finish(request)
42dbb26a 249
405aa45a 250 if oauth_request.verifier is None:
786bbd79 251 orequest = GMGRequest(request)
24e12cb1 252 orequest.resource_owner_key = token
405aa45a 253 request_validator = GMGRequestValidator()
254 auth_endpoint = AuthorizationEndpoint(request_validator)
255 verifier = auth_endpoint.create_verifier(orequest, {})
256 oauth_request.verifier = verifier["oauth_verifier"]
257
fd703bb4 258 oauth_request.actor = request.user.id
405aa45a 259 oauth_request.save()
260
261 # find client & build context
262 client = Client.query.filter_by(id=oauth_request.client).first()
263
264 authorize_form = AuthorizeForm(WTFormData({
265 "oauth_token": oauth_request.token,
266 "oauth_verifier": oauth_request.verifier
267 }))
268
269 context = {
270 "user": request.user,
271 "oauth_request": oauth_request,
272 "client": client,
273 "authorize_form": authorize_form,
274 }
275
276
d41c6a53 277 # AuthorizationEndpoint
405aa45a 278 return render_to_response(
279 request,
280 "mediagoblin/api/authorize.html",
281 context
282 )
42dbb26a 283
405aa45a 284
285def authorize_finish(request):
286 """ Finishes the authorize """
287 _ = pass_to_ugettext
288 token = request.form["oauth_token"]
289 verifier = request.form["oauth_verifier"]
290 oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
291 oauth_request = oauth_request.first()
42dbb26a 292
405aa45a 293 if oauth_request is None:
294 # invalid token or verifier
295 err_msg = _("No request token found.")
296 return render_400(request, err_msg)
297
298 oauth_request.used = True
299 oauth_request.updated = datetime.datetime.now()
300 oauth_request.save()
301
302 if oauth_request.callback == "oob":
303 # out of bounds
304 context = {"oauth_request": oauth_request}
305 return render_to_response(
306 request,
307 "mediagoblin/api/oob.html",
308 context
309 )
310
311 # okay we need to redirect them then!
312 querystring = "?oauth_token={0}&oauth_verifier={1}".format(
313 oauth_request.token,
314 oauth_request.verifier
315 )
316
060a7a7b
JT
317 # It's come from the OAuth headers so it'll be encoded.
318 redirect_url = urllib.unquote(oauth_request.callback)
319
405aa45a 320 return redirect(
321 request,
322 querystring=querystring,
060a7a7b 323 location=redirect_url
405aa45a 324 )
d41c6a53 325
326@csrf_exempt
327def access_token(request):
42dbb26a 328 """ Provides an access token based on a valid verifier and request token """
2b60a56c 329 data = request.headers
405aa45a 330
42dbb26a 331 parsed_tokens = decode_authorization_header(data)
2b60a56c 332
333 if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
334 error = "Missing required parameter."
405aa45a 335 return json_response({"error": error}, status=400)
336
24e12cb1 337 request.resource_owner_key = parsed_tokens["oauth_consumer_key"]
2b60a56c 338 request.oauth_token = parsed_tokens["oauth_token"]
339 request_validator = GMGRequestValidator(data)
86ee2d1a
JT
340
341 # Check that the verifier is valid
342 verifier_valid = request_validator.validate_verifier(
343 token=request.oauth_token,
344 verifier=parsed_tokens["oauth_verifier"]
345 )
346 if not verifier_valid:
347 error = "Verifier code or token incorrect"
348 return json_response({"error": error}, status=401)
349
2b60a56c 350 av = AccessTokenEndpoint(request_validator)
351 tokens = av.create_access_token(request, {})
352 return form_response(tokens)