diff --git a/tests/test_oauth2.py b/tests/test_oauth2.py index 17d9cb701f9a0b7afefe24f1cf6e0057df923594..592f8610caccbbe2277bbe1b1026f8129b674693 100644 --- a/tests/test_oauth2.py +++ b/tests/test_oauth2.py @@ -172,6 +172,16 @@ class TestViews(UffdTestCase): self.assertEqual(r.json['token_type'], 'Bearer') self.assertEqual(r.json['scope'], 'profile') + # Regression test for #114 (OAuth2 token endpoint does not support Basic-Auth) + def test_token_basicauth(self): + r = self.client.post(path=url_for('oauth2.token'), + data={'grant_type': 'authorization_code', 'code': self.get_auth_code(), 'redirect_uri': 'http://localhost:5009/callback'}, + headers={'Authorization': f'Basic dGVzdDp0ZXN0c2VjcmV0'}, follow_redirects=True) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content_type, 'application/json') + self.assertEqual(r.json['token_type'], 'Bearer') + self.assertEqual(r.json['scope'], 'profile') + def test_token_invalid_code(self): r = self.client.post(path=url_for('oauth2.token'), data={'grant_type': 'authorization_code', 'code': 'abcdef', 'redirect_uri': 'http://localhost:5009/callback', 'client_id': 'test', 'client_secret': 'testsecret'}, follow_redirects=True) diff --git a/uffd/oauth2/views.py b/uffd/oauth2/views.py index 3b580aef038a4deff37347ba7584c17475caaf71..57a7e9bfceb1a6f9ff9f46ac54e779f41bb29de3 100644 --- a/uffd/oauth2/views.py +++ b/uffd/oauth2/views.py @@ -1,6 +1,7 @@ import datetime import functools import secrets +import urllib.parse from flask import Blueprint, request, jsonify, render_template, session, redirect, url_for, flash, abort import oauthlib.oauth2 @@ -29,6 +30,16 @@ class UffdRequestValidator(oauthlib.oauth2.RequestValidator): return False def authenticate_client(self, oauthreq, *args, **kwargs): + authorization = oauthreq.extra_credentials.get('authorization') + if authorization: + # From RFC6749 2.3.1: + # Clients in possession of a client password MAY use the HTTP Basic authentication + # scheme as defined in [RFC2617] to authenticate with the authorization server. + # The client identifier is encoded using the "application/x-www-form-urlencoded" + # encoding algorithm per Appendix B, and the encoded value is used as the username + # the client password is encoded using the same algorithm and used as the password. + oauthreq.client_id = urllib.parse.unquote(authorization.username) + oauthreq.client_secret = urllib.parse.unquote(authorization.password) if oauthreq.client_secret is None: return False try: @@ -203,7 +214,8 @@ def authorize(): @bp.route('/token', methods=['GET', 'POST']) def token(): - headers, body, status = server.create_token_response(request.url, request.method, request.form, request.headers) + headers, body, status = server.create_token_response(request.url, request.method, request.form, + request.headers, {'authorization': request.authorization}) return body, status, headers def oauth_required(*scopes):