Merge branch 'auth_docs'
[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
4990b47c 18
617bff18
JT
19from oauthlib.oauth1 import (RequestTokenEndpoint, AuthorizationEndpoint,
20 AccessTokenEndpoint)
005181b1 21
617bff18 22from mediagoblin.decorators import require_active_login
d41c6a53 23from mediagoblin.tools.translate import pass_to_ugettext
4990b47c 24from mediagoblin.meddleware.csrf import csrf_exempt
786bbd79 25from mediagoblin.tools.request import decode_request
405aa45a 26from mediagoblin.tools.response import (render_to_response, redirect,
2b60a56c 27 json_response, render_400,
28 form_response)
4990b47c 29from mediagoblin.tools.crypto import random_string
c33a34d4 30from mediagoblin.tools.validator import validate_email, validate_url
005181b1 31from mediagoblin.oauth.forms import AuthorizeForm
32from mediagoblin.oauth.oauth import GMGRequestValidator, GMGRequest
33from mediagoblin.oauth.tools.request import decode_authorization_header
34from mediagoblin.oauth.tools.forms import WTFormData
617bff18 35from mediagoblin.db.models import NonceTimestamp, Client, RequestToken
c840cb66 36
37# possible client types
38client_types = ["web", "native"] # currently what pump supports
39
4990b47c 40@csrf_exempt
c840cb66 41def client_register(request):
42 """ Endpoint for client registration """
d41c6a53 43 try:
44 data = decode_request(request)
45 except ValueError:
46 error = "Could not decode data."
47 return json_response({"error": error}, status=400)
48
49 if data is "":
50 error = "Unknown Content-Type"
51 return json_response({"error": error}, status=400)
c840cb66 52
4990b47c 53 if "type" not in data:
d41c6a53 54 error = "No registration type provided."
55 return json_response({"error": error}, status=400)
56 if data.get("application_type", None) not in client_types:
57 error = "Unknown application_type."
58 return json_response({"error": error}, status=400)
54fbbf09 59
60 client_type = data["type"]
61
62 if client_type == "client_update":
63 # updating a client
64 if "client_id" not in data:
d41c6a53 65 error = "client_id is requried to update."
66 return json_response({"error": error}, status=400)
54fbbf09 67 elif "client_secret" not in data:
d41c6a53 68 error = "client_secret is required to update."
69 return json_response({"error": error}, status=400)
54fbbf09 70
d41c6a53 71 client = Client.query.filter_by(
72 id=data["client_id"],
73 secret=data["client_secret"]
74 ).first()
54fbbf09 75
c33a34d4 76 if client is None:
d41c6a53 77 error = "Unauthorized."
78 return json_response({"error": error}, status=403)
79
80 client.application_name = data.get(
81 "application_name",
82 client.application_name
83 )
84
85 client.application_type = data.get(
86 "application_type",
87 client.application_type
88 )
54fbbf09 89
763e300d 90 app_name = ("application_type", client.application_name)
91 if app_name in client_types:
92 client.application_name = app_name
763e300d 93
54fbbf09 94 elif client_type == "client_associate":
95 # registering
96 if "client_id" in data:
d41c6a53 97 error = "Only set client_id for update."
98 return json_response({"error": error}, status=400)
54fbbf09 99 elif "access_token" in data:
d41c6a53 100 error = "access_token not needed for registration."
101 return json_response({"error": error}, status=400)
54fbbf09 102 elif "client_secret" in data:
d41c6a53 103 error = "Only set client_secret for update."
104 return json_response({"error": error}, status=400)
54fbbf09 105
c33a34d4 106 # generate the client_id and client_secret
107 client_id = random_string(22) # seems to be what pump uses
108 client_secret = random_string(43) # again, seems to be what pump uses
109 expirey = 0 # for now, lets not have it expire
110 expirey_db = None if expirey == 0 else expirey
86ba4168 111 application_type = data["application_type"]
112
c33a34d4 113 # save it
114 client = Client(
115 id=client_id,
116 secret=client_secret,
117 expirey=expirey_db,
86ba4168 118 application_type=application_type,
d41c6a53 119 )
c33a34d4 120
121 else:
d41c6a53 122 error = "Invalid registration type"
123 return json_response({"error": error}, status=400)
c33a34d4 124
125 logo_url = data.get("logo_url", client.logo_url)
126 if logo_url is not None and not validate_url(logo_url):
d41c6a53 127 error = "Logo URL {0} is not a valid URL.".format(logo_url)
128 return json_response(
129 {"error": error},
130 status=400
131 )
c33a34d4 132 else:
133 client.logo_url = logo_url
405aa45a 134
135 client.application_name = data.get("application_name", None)
c33a34d4 136
86ba4168 137 contacts = data.get("contacts", None)
c33a34d4 138 if contacts is not None:
139 if type(contacts) is not unicode:
d41c6a53 140 error = "Contacts must be a string of space-seporated email addresses."
141 return json_response({"error": error}, status=400)
c33a34d4 142
143 contacts = contacts.split()
144 for contact in contacts:
145 if not validate_email(contact):
146 # not a valid email
d41c6a53 147 error = "Email {0} is not a valid email.".format(contact)
148 return json_response({"error": error}, status=400)
c33a34d4 149
150
151 client.contacts = contacts
152
86ba4168 153 redirect_uris = data.get("redirect_uris", None)
154 if redirect_uris is not None:
155 if type(redirect_uris) is not unicode:
d41c6a53 156 error = "redirect_uris must be space-seporated URLs."
617bff18 157 return json_response({"error": error}, status=400)
c33a34d4 158
86ba4168 159 redirect_uris = redirect_uris.split()
c33a34d4 160
86ba4168 161 for uri in redirect_uris:
c33a34d4 162 if not validate_url(uri):
163 # not a valid uri
d41c6a53 164 error = "URI {0} is not a valid URI".format(uri)
165 return json_response({"error": error}, status=400)
c33a34d4 166
86ba4168 167 client.redirect_uri = redirect_uris
c33a34d4 168
169
4990b47c 170 client.save()
c840cb66 171
c33a34d4 172 expirey = 0 if client.expirey is None else client.expirey
173
4990b47c 174 return json_response(
175 {
d41c6a53 176 "client_id": client.id,
177 "client_secret": client.secret,
178 "expires_at": expirey,
4990b47c 179 })
d41c6a53 180
d41c6a53 181@csrf_exempt
182def request_token(request):
183 """ Returns request token """
184 try:
185 data = decode_request(request)
186 except ValueError:
187 error = "Could not decode data."
188 return json_response({"error": error}, status=400)
189
405aa45a 190 if data == "":
d41c6a53 191 error = "Unknown Content-Type"
192 return json_response({"error": error}, status=400)
193
2b60a56c 194 if not data and request.headers:
195 data = request.headers
196
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
89d5b44e 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
405aa45a 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)
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)
246
247 if oauth_request.used:
248 return authorize_finish(request)
249
250 if oauth_request.verifier is None:
786bbd79 251 orequest = GMGRequest(request)
405aa45a 252 request_validator = GMGRequestValidator()
253 auth_endpoint = AuthorizationEndpoint(request_validator)
254 verifier = auth_endpoint.create_verifier(orequest, {})
255 oauth_request.verifier = verifier["oauth_verifier"]
256
257 oauth_request.user = request.user.id
258 oauth_request.save()
259
260 # find client & build context
261 client = Client.query.filter_by(id=oauth_request.client).first()
262
263 authorize_form = AuthorizeForm(WTFormData({
264 "oauth_token": oauth_request.token,
265 "oauth_verifier": oauth_request.verifier
266 }))
267
268 context = {
269 "user": request.user,
270 "oauth_request": oauth_request,
271 "client": client,
272 "authorize_form": authorize_form,
273 }
274
275
d41c6a53 276 # AuthorizationEndpoint
405aa45a 277 return render_to_response(
278 request,
279 "mediagoblin/api/authorize.html",
280 context
281 )
282
283
284def authorize_finish(request):
285 """ Finishes the authorize """
286 _ = pass_to_ugettext
287 token = request.form["oauth_token"]
288 verifier = request.form["oauth_verifier"]
289 oauth_request = RequestToken.query.filter_by(token=token, verifier=verifier)
290 oauth_request = oauth_request.first()
d41c6a53 291
405aa45a 292 if oauth_request is None:
293 # invalid token or verifier
294 err_msg = _("No request token found.")
295 return render_400(request, err_msg)
296
297 oauth_request.used = True
298 oauth_request.updated = datetime.datetime.now()
299 oauth_request.save()
300
301 if oauth_request.callback == "oob":
302 # out of bounds
303 context = {"oauth_request": oauth_request}
304 return render_to_response(
305 request,
306 "mediagoblin/api/oob.html",
307 context
308 )
309
310 # okay we need to redirect them then!
311 querystring = "?oauth_token={0}&oauth_verifier={1}".format(
312 oauth_request.token,
313 oauth_request.verifier
314 )
315
316 return redirect(
317 request,
318 querystring=querystring,
319 location=oauth_request.callback
320 )
d41c6a53 321
322@csrf_exempt
323def access_token(request):
324 """ Provides an access token based on a valid verifier and request token """
2b60a56c 325 data = request.headers
405aa45a 326
2b60a56c 327 parsed_tokens = decode_authorization_header(data)
328
329 if parsed_tokens == dict() or "oauth_token" not in parsed_tokens:
330 error = "Missing required parameter."
405aa45a 331 return json_response({"error": error}, status=400)
332
2b60a56c 333
334 request.oauth_token = parsed_tokens["oauth_token"]
335 request_validator = GMGRequestValidator(data)
336 av = AccessTokenEndpoint(request_validator)
337 tokens = av.create_access_token(request, {})
338 return form_response(tokens)
1e2675b0 339