|
2 | 2 | # -*- coding: utf-8 -*- |
3 | 3 |
|
4 | 4 |
|
| 5 | +import datetime |
5 | 6 | import io |
6 | | -import os, shutil, time, datetime |
7 | | -from urllib.parse import urlsplit |
| 7 | +import logging # pyflakes:ignore |
| 8 | +import os |
| 9 | +import requests |
| 10 | +import requests_mock |
| 11 | +import shutil |
| 12 | +import time |
| 13 | +import urllib |
| 14 | + |
| 15 | +from .factories import OidClientRecordFactory |
| 16 | +from Cryptodome.PublicKey import RSA |
| 17 | +from oic import rndstr |
| 18 | +from oic.oic import Client as OidClient |
| 19 | +from oic.oic.message import RegistrationResponse, AuthorizationResponse |
| 20 | +from oic.utils.authn.client import CLIENT_AUTHN_METHOD |
| 21 | +from oidc_provider.models import RSAKey |
8 | 22 | from pyquery import PyQuery |
9 | 23 | from unittest import skipIf |
| 24 | +from urllib.parse import urlsplit |
10 | 25 |
|
11 | 26 | import django.contrib.auth.views |
12 | 27 | from django.urls import reverse as urlreverse |
|
19 | 34 | from ietf.group.models import Group, Role, RoleName |
20 | 35 | from ietf.ietfauth.htpasswd import update_htpasswd_file |
21 | 36 | from ietf.mailinglists.models import Subscribed |
| 37 | +from ietf.meeting.factories import MeetingFactory |
22 | 38 | from ietf.person.factories import PersonFactory, EmailFactory |
23 | 39 | from ietf.person.models import Person, Email, PersonalApiKey |
24 | 40 | from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory |
25 | 41 | from ietf.review.models import ReviewWish, UnavailablePeriod |
| 42 | +from ietf.stats.models import MeetingRegistration |
26 | 43 | from ietf.utils.decorators import skip_coverage |
27 | 44 | from ietf.utils.mail import outbox, empty_outbox, get_payload_text |
28 | 45 | from ietf.utils.test_utils import TestCase, login_testing_unauthorized |
@@ -647,4 +664,135 @@ def test_send_apikey_report(self): |
647 | 664 | self.assertIn("API key usage", mail['subject']) |
648 | 665 | self.assertIn(" %s times" % count, body) |
649 | 666 | self.assertIn(date, body) |
650 | | - |
| 667 | + |
| 668 | + |
| 669 | +class OpenIDConnectTests(TestCase): |
| 670 | + def request_matcher(self, request): |
| 671 | + method, url = str(request).split(None, 1) |
| 672 | + response = requests.Response() |
| 673 | + if method == 'GET': |
| 674 | + r = self.client.get(request.path) |
| 675 | + elif method == 'POST': |
| 676 | + data = dict(urllib.parse.parse_qsl(request.text)) |
| 677 | + extra = request.headers |
| 678 | + for key in [ 'Authorization', ]: |
| 679 | + if key in request.headers: |
| 680 | + extra['HTTP_%s'%key.upper()] = request.headers[key] |
| 681 | + r = self.client.post(request.path, data=data, **extra) |
| 682 | + else: |
| 683 | + raise ValueError('Unexpected method: %s' % method) |
| 684 | + response = requests.Response() |
| 685 | + response.status_code = r.status_code |
| 686 | + response.raw = r |
| 687 | + response.url = url |
| 688 | + response.request = request |
| 689 | + response._content = r.content |
| 690 | + response.encoding = 'utf-8' |
| 691 | + for (k,v) in r.items(): |
| 692 | + response.headers[k] = v |
| 693 | + return response |
| 694 | + |
| 695 | + def test_oidc_code_auth(self): |
| 696 | + |
| 697 | + key = RSA.generate(2048) |
| 698 | + RSAKey.objects.create(key=key.exportKey('PEM').decode('utf8')) |
| 699 | + |
| 700 | + r = self.client.get('/') |
| 701 | + host = r.wsgi_request.get_host() |
| 702 | + |
| 703 | + redirect_uris = [ |
| 704 | + 'https://foo.example.com/', |
| 705 | + ] |
| 706 | + oid_client_record = OidClientRecordFactory(_redirect_uris='\n'.join(redirect_uris), ) |
| 707 | + |
| 708 | + with requests_mock.Mocker() as mock: |
| 709 | + pass |
| 710 | + mock._adapter.add_matcher(self.request_matcher) |
| 711 | + |
| 712 | + # Get a client |
| 713 | + client = OidClient(client_authn_method=CLIENT_AUTHN_METHOD) |
| 714 | + |
| 715 | + # Get provider info |
| 716 | + client.provider_config( 'http://%s/api/openid' % host) |
| 717 | + |
| 718 | + # No registration step -- we only supported this out-of-band |
| 719 | + |
| 720 | + # Set shared client/provider information in the client |
| 721 | + client_reg = RegistrationResponse( client_id= oid_client_record.client_id, |
| 722 | + client_secret= oid_client_record.client_secret) |
| 723 | + client.store_registration_info(client_reg) |
| 724 | + |
| 725 | + # Get a user for which we want to get access |
| 726 | + person = PersonFactory() |
| 727 | + RoleFactory(name_id='chair', person=person) |
| 728 | + meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) |
| 729 | + MeetingRegistration.objects.create( |
| 730 | + meeting=meeting, person=person, first_name=person.first_name(), last_name=person.last_name(), email=person.email()) |
| 731 | + |
| 732 | + # Get access authorisation |
| 733 | + session = {} |
| 734 | + session["state"] = rndstr() |
| 735 | + session["nonce"] = rndstr() |
| 736 | + args = { |
| 737 | + "client_id": client.client_id, |
| 738 | + "response_type": "code", |
| 739 | + "scope": ['openid', 'profile', 'email', 'roles', 'registration', ], |
| 740 | + "nonce": session["nonce"], |
| 741 | + "redirect_uri": redirect_uris[0], |
| 742 | + "state": session["state"] |
| 743 | + } |
| 744 | + auth_req = client.construct_AuthorizationRequest(request_args=args) |
| 745 | + auth_url = auth_req.request(client.authorization_endpoint) |
| 746 | + r = self.client.get(auth_url, follow=True) |
| 747 | + self.assertEqual(r.status_code, 200) |
| 748 | + login_url, __ = r.redirect_chain[-1] |
| 749 | + self.assertTrue(login_url.startswith(urlreverse('ietf.ietfauth.views.login'))) |
| 750 | + |
| 751 | + # Do login |
| 752 | + username = person.user.username |
| 753 | + r = self.client.post(login_url, {'username':username, 'password':'%s+password'%username}, follow=True) |
| 754 | + self.assertContains(r, 'Request for Permission') |
| 755 | + q = PyQuery(r.content) |
| 756 | + forms = q('form[action="/api/openid/authorize"]') |
| 757 | + self.assertEqual(len(forms), 1) |
| 758 | + |
| 759 | + # Authorize the client to access account information |
| 760 | + data = {'allow': 'Authorize'} |
| 761 | + for input in q('form[action="/api/openid/authorize"] input[type="hidden"]'): |
| 762 | + name = input.get("name") |
| 763 | + value = input.get("value") |
| 764 | + data[name] = value |
| 765 | + r = self.client.post(urlreverse('oidc_provider:authorize'), data) |
| 766 | + |
| 767 | + # Check authorization returns |
| 768 | + self.assertEqual(r.status_code, 302) |
| 769 | + location = r['Location'] |
| 770 | + self.assertTrue(location.startswith(redirect_uris[0])) |
| 771 | + self.assertIn('state=%s'%data['state'], location) |
| 772 | + |
| 773 | + # Extract the grant code |
| 774 | + params = client.parse_response(AuthorizationResponse, info=urllib.parse.urlsplit(location).query, sformat="urlencoded") |
| 775 | + |
| 776 | + # Use grant code to get access token |
| 777 | + access_token_info = client.do_access_token_request(state=params['state'], |
| 778 | + authn_method='client_secret_basic') |
| 779 | + |
| 780 | + for key in ['access_token', 'refresh_token', 'token_type', 'expires_in', 'id_token']: |
| 781 | + self.assertIn(key, access_token_info) |
| 782 | + for key in ['iss', 'sub', 'aud', 'exp', 'iat', 'auth_time', 'nonce', 'at_hash']: |
| 783 | + self.assertIn(key, access_token_info['id_token']) |
| 784 | + |
| 785 | + # Get userinfo, check keys present |
| 786 | + userinfo = client.do_user_info_request(state=params["state"], scope=args['scope']) |
| 787 | + for key in [ 'email', 'family_name', 'given_name', 'meeting', 'name', 'roles', ]: |
| 788 | + self.assertIn(key, userinfo) |
| 789 | + |
| 790 | + r = client.do_end_session_request(state=params["state"], scope=args['scope']) |
| 791 | + self.assertEqual(r.status_code, 302) |
| 792 | + self.assertEqual(r.headers["Location"], urlreverse('ietf.ietfauth.views.login')) |
| 793 | + |
| 794 | + # The pyjwkent.jwt and oic.utils.keyio modules have had problems with calling |
| 795 | + # logging.debug() instead of logger.debug(), which results in setting a root |
| 796 | + # handler, causing later logging to become visible even if that wasn't intended. |
| 797 | + # Fail here if that happens. |
| 798 | + self.assertEqual(logging.root.handlers, []) |
0 commit comments