girder-oauth 5.0.20.dev2__tar.gz → 5.0.20.dev6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/PKG-INFO +1 -1
  2. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/cilogon.py +12 -11
  3. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/microsoft.py +2 -1
  4. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth.egg-info/PKG-INFO +1 -1
  5. girder_oauth-5.0.20.dev6/girder_oauth.egg-info/scm_version.json +8 -0
  6. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/plugin_tests/oauth_test.py +251 -16
  7. girder_oauth-5.0.20.dev2/girder_oauth.egg-info/scm_version.json +0 -8
  8. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/MANIFEST.in +0 -0
  9. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/__init__.py +0 -0
  10. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/__init__.py +0 -0
  11. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/base.py +0 -0
  12. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/bitbucket.py +0 -0
  13. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/box.py +0 -0
  14. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/github.py +0 -0
  15. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/globus.py +0 -0
  16. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/google.py +0 -0
  17. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/providers/linkedin.py +0 -0
  18. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/rest.py +0 -0
  19. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/settings.py +0 -0
  20. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/web_client/dist/girder-plugin-oauth.umd.cjs +0 -0
  21. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth/web_client/dist/style.css +0 -0
  22. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth.egg-info/SOURCES.txt +0 -0
  23. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth.egg-info/dependency_links.txt +0 -0
  24. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth.egg-info/entry_points.txt +0 -0
  25. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth.egg-info/not-zip-safe +0 -0
  26. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth.egg-info/requires.txt +0 -0
  27. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth.egg-info/scm_file_list.json +0 -0
  28. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/girder_oauth.egg-info/top_level.txt +0 -0
  29. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/plugin.cmake +0 -0
  30. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/plugin_tests/__init__.py +0 -0
  31. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/pyproject.toml +0 -0
  32. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/setup.cfg +0 -0
  33. {girder_oauth-5.0.20.dev2 → girder_oauth-5.0.20.dev6}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: girder-oauth
3
- Version: 5.0.20.dev2
3
+ Version: 5.0.20.dev6
4
4
  Summary: Allow users to login via supported OAuth2 providers.
5
5
  Home-page: http://girder.readthedocs.io/en/latest/plugins.html#oauth-login
6
6
  Author: Kitware, Inc.
@@ -1,3 +1,4 @@
1
+ import urllib.parse
1
2
  import warnings
2
3
 
3
4
  import requests
@@ -29,15 +30,14 @@ class CILogon(ProviderBase):
29
30
 
30
31
  redirectUri = '/'.join((getApiUrl(), 'oauth', 'cilogon', 'callback'))
31
32
 
32
- url = (
33
- f'{cls._AUTHORITY}/authorize'
34
- f'?client_id={clientId}'
35
- f'&response_type=code'
36
- f'&scope={" ".join(cls._AUTH_SCOPES)}'
37
- f'&redirect_uri={redirectUri}'
38
- f'&state={state}'
39
- )
40
- return url
33
+ query = urllib.parse.urlencode({
34
+ 'client_id': clientId,
35
+ 'response_type': 'code',
36
+ 'scope': ' '.join(cls._AUTH_SCOPES),
37
+ 'redirect_uri': redirectUri,
38
+ 'state': state,
39
+ })
40
+ return f'{cls._AUTHORITY}/authorize?{query}'
41
41
 
42
42
  def getToken(self, code):
43
43
  clientId = self.getClientIdSetting()
@@ -60,8 +60,9 @@ class CILogon(ProviderBase):
60
60
  response = requests.post(token_url, data=data)
61
61
 
62
62
  if response.status_code != 200:
63
- raise Exception('Error acquiring token: %s' %
64
- response.json().get('error_description', 'Unknown error'))
63
+ raise RestException('Error acquiring token: %s' %
64
+ response.json().get('error_description', 'Unknown error'),
65
+ code=502)
65
66
 
66
67
  return response.json()
67
68
 
@@ -60,7 +60,8 @@ class Microsoft(ProviderBase):
60
60
  )
61
61
  if 'access_token' not in result:
62
62
  strings = result['error'], result['error_description']
63
- raise Exception('Error "%s" acquiring access token for client: %s' % strings)
63
+ raise RestException(
64
+ 'Error "%s" acquiring access token for client: %s' % strings, code=502)
64
65
  return result
65
66
 
66
67
  def getUser(self, token):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: girder-oauth
3
- Version: 5.0.20.dev2
3
+ Version: 5.0.20.dev6
4
4
  Summary: Allow users to login via supported OAuth2 providers.
5
5
  Home-page: http://girder.readthedocs.io/en/latest/plugins.html#oauth-login
6
6
  Author: Kitware, Inc.
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "5.0.19",
3
+ "distance": 6,
4
+ "node": "gf73d49ac4fe62a16886ef8d19a3736475571f269",
5
+ "dirty": false,
6
+ "branch": "master",
7
+ "node_date": "2026-10-21"
8
+ }
@@ -1311,9 +1311,165 @@ class OauthTest(base.TestCase):
1311
1311
  ):
1312
1312
  self._testOauth(providerInfo)
1313
1313
 
1314
- def XtestMicrosoftOauth(self): # noqa
1315
- # This test is disabled for now:
1316
- # https://github.com/girder/girder/pull/3393#issuecomment-1113589758
1314
+ def testCilogonOauth(self): # noqa
1315
+ providerInfo = {
1316
+ 'id': 'cilogon',
1317
+ 'name': 'CILogon',
1318
+ 'client_id': {
1319
+ 'key': PluginSettings.CILOGON_CLIENT_ID,
1320
+ 'value': 'cilogon_test_client_id'
1321
+ },
1322
+ 'client_secret': {
1323
+ 'key': PluginSettings.CILOGON_CLIENT_SECRET,
1324
+ 'value': 'cilogon_test_client_secret'
1325
+ },
1326
+ 'allowed_callback_re':
1327
+ r'^http://127\.0\.0\.1(?::\d+)?/api/v1/oauth/cilogon/callback$',
1328
+ 'url_re': r'^https://cilogon\.org/authorize',
1329
+ 'accounts': {
1330
+ 'existing': {
1331
+ 'auth_code': 'cilogon_existing_auth_code',
1332
+ 'access_token': 'cilogon_existing_test_token',
1333
+ 'user': {
1334
+ 'login': self.adminUser['login'],
1335
+ 'email': self.adminUser['email'],
1336
+ 'firstName': self.adminUser['firstName'],
1337
+ 'lastName': self.adminUser['lastName'],
1338
+ 'oauth': {
1339
+ 'provider': 'cilogon',
1340
+ 'id': '2399'
1341
+ }
1342
+ }
1343
+ },
1344
+ 'new': {
1345
+ 'auth_code': 'cilogon_new_auth_code',
1346
+ 'access_token': 'cilogon_new_test_token',
1347
+ 'user': {
1348
+ # this login is not provided by CILogon, but will be
1349
+ # created internally by _deriveLogin
1350
+ 'login': 'apollo',
1351
+ 'email': 'apollo@creed.chi.il.us',
1352
+ 'firstName': 'Apollo',
1353
+ 'lastName': 'Creed',
1354
+ 'oauth': {
1355
+ 'provider': 'cilogon',
1356
+ 'id': 'the1best'
1357
+ }
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+
1363
+ @httmock.urlmatch(scheme='https', netloc='^cilogon.org$',
1364
+ path='^/authorize$', method='GET')
1365
+ def mockCilogonRedirect(url, request):
1366
+ redirectUri = None
1367
+ try:
1368
+ params = urllib.parse.parse_qs(url.query)
1369
+ # Check redirect_uri first, so other errors can still redirect
1370
+ redirectUri = params['redirect_uri'][0]
1371
+ self.assertEqual(params['client_id'], [providerInfo['client_id']['value']])
1372
+ except (KeyError, AssertionError) as e:
1373
+ return {
1374
+ 'status_code': 404,
1375
+ 'content': json.dumps({
1376
+ 'error': repr(e)
1377
+ })
1378
+ }
1379
+ try:
1380
+ self.assertRegex(redirectUri, providerInfo['allowed_callback_re'])
1381
+ self.assertEqual(params['response_type'], ['code'])
1382
+ self.assertEqual(params['scope'], ['openid email profile'])
1383
+ state = params['state'][0]
1384
+ # Nothing to test for state, since provider doesn't care
1385
+ except (KeyError, AssertionError) as e:
1386
+ returnQuery = urllib.parse.urlencode({
1387
+ 'error': repr(e),
1388
+ })
1389
+ else:
1390
+ returnQuery = urllib.parse.urlencode({
1391
+ 'state': state,
1392
+ 'code': providerInfo['accounts'][self.accountType]['auth_code']
1393
+ })
1394
+ return {
1395
+ 'status_code': 302,
1396
+ 'headers': {
1397
+ 'Location': '%s?%s' % (redirectUri, returnQuery)
1398
+ }
1399
+ }
1400
+
1401
+ @httmock.urlmatch(scheme='https', netloc='^cilogon.org$',
1402
+ path='^/oauth2/token$', method='POST')
1403
+ def mockCilogonToken(url, request):
1404
+ try:
1405
+ params = urllib.parse.parse_qs(request.body)
1406
+ self.assertEqual(params['grant_type'], ['authorization_code'])
1407
+ self.assertEqual(params['client_id'], [providerInfo['client_id']['value']])
1408
+ for account in providerInfo['accounts'].values():
1409
+ if account['auth_code'] == params['code'][0]:
1410
+ break
1411
+ else:
1412
+ self.fail()
1413
+ self.assertEqual(params['client_secret'], [providerInfo['client_secret']['value']])
1414
+ self.assertRegex(
1415
+ params['redirect_uri'][0], providerInfo['allowed_callback_re'])
1416
+ except (KeyError, AssertionError) as e:
1417
+ return {
1418
+ 'status_code': 400,
1419
+ 'headers': {
1420
+ 'Content-Type': 'application/json'
1421
+ },
1422
+ 'content': json.dumps({
1423
+ 'error': repr(e),
1424
+ 'error_description': repr(e)
1425
+ })
1426
+ }
1427
+ return {
1428
+ 'status_code': 200,
1429
+ 'headers': {
1430
+ 'Content-Type': 'application/json'
1431
+ },
1432
+ 'content': json.dumps({
1433
+ 'token_type': 'Bearer',
1434
+ 'access_token': account['access_token'],
1435
+ 'expires_in': 3600,
1436
+ 'id_token': 'cilogon_id_token'
1437
+ })
1438
+ }
1439
+
1440
+ @httmock.urlmatch(scheme='https', netloc='^cilogon.org$',
1441
+ path='^/oauth2/userinfo$', method='GET')
1442
+ def mockCilogonApiUser(url, request):
1443
+ try:
1444
+ for account in providerInfo['accounts'].values():
1445
+ if 'Bearer %s' % account['access_token'] == request.headers['Authorization']:
1446
+ break
1447
+ else:
1448
+ self.fail()
1449
+ except AssertionError as e:
1450
+ return {
1451
+ 'status_code': 401,
1452
+ 'content': json.dumps({
1453
+ 'message': repr(e)
1454
+ })
1455
+ }
1456
+ return json.dumps({
1457
+ 'sub': account['user']['oauth']['id'],
1458
+ 'email': account['user']['email'],
1459
+ 'given_name': account['user']['firstName'],
1460
+ 'family_name': account['user']['lastName']
1461
+ })
1462
+
1463
+ with httmock.HTTMock(
1464
+ mockCilogonRedirect,
1465
+ mockCilogonToken,
1466
+ mockCilogonApiUser,
1467
+ # Must keep 'mockOtherRequest' last
1468
+ self.mockOtherRequest
1469
+ ):
1470
+ self._testOauth(providerInfo)
1471
+
1472
+ def testMicrosoftOauth(self): # noqa
1317
1473
  providerInfo = {
1318
1474
  'id': 'microsoft',
1319
1475
  'name': 'Microsoft',
@@ -1348,11 +1504,10 @@ class OauthTest(base.TestCase):
1348
1504
  'auth_code': 'microsoft_new_auth_code',
1349
1505
  'access_token': 'microsoft_new_test_token',
1350
1506
  'user': {
1351
- # login may be provided externally by Microsoft; for
1352
- # simplicity here, do not use a username with whitespace
1353
- # or underscores
1507
+ # this login is not provided by Microsoft, but will be
1508
+ # derived from the email address by _deriveLogin
1354
1509
  'login': 'drago',
1355
- 'email': 'metaphor@labs.ussr.gov',
1510
+ 'email': 'drago@labs.ussr.gov',
1356
1511
  'firstName': 'Ivan',
1357
1512
  'lastName': 'Drago',
1358
1513
  'oauth': {
@@ -1364,14 +1519,27 @@ class OauthTest(base.TestCase):
1364
1519
  }
1365
1520
  }
1366
1521
 
1522
+ @httmock.urlmatch(scheme='https', netloc='^login.microsoftonline.com$',
1523
+ path=r'^/organizations/v2\.0/\.well-known/openid-configuration$',
1524
+ method='GET')
1525
+ def mockMicrosoftDiscovery(url, request):
1526
+ return json.dumps({
1527
+ 'authorization_endpoint':
1528
+ 'https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize',
1529
+ 'token_endpoint':
1530
+ 'https://login.microsoftonline.com/organizations/oauth2/v2.0/token',
1531
+ 'issuer': 'https://login.microsoftonline.com/organizations/v2.0',
1532
+ 'tenant_region_scope': 'EU',
1533
+ 'cloud_instance_name': 'microsoftonline.com',
1534
+ 'cloud_graph_host_name': 'graph.windows.net',
1535
+ 'msgraph_host': 'graph.microsoft.com',
1536
+ })
1537
+
1367
1538
  @httmock.urlmatch(scheme='https', netloc='^login.microsoftonline.com$',
1368
1539
  path='^/organizations/oauth2/v2.0/authorize$', method='GET')
1369
1540
  def mockMicrosoftRedirect(url, request):
1370
- redirectUri = None
1371
1541
  try:
1372
1542
  params = urllib.parse.parse_qs(url.query)
1373
- # Check redirect_uri first, so other errors can still redirect
1374
- redirectUri = params['redirect_uri'][0]
1375
1543
  self.assertEqual(params['client_id'], [providerInfo['client_id']['value']])
1376
1544
  except (KeyError, AssertionError) as e:
1377
1545
  return {
@@ -1381,10 +1549,13 @@ class OauthTest(base.TestCase):
1381
1549
  })
1382
1550
  }
1383
1551
  try:
1384
- self.assertRegex(redirectUri, providerInfo['allowed_callback_re'])
1385
1552
  state = params['state'][0]
1386
1553
  # Nothing to test for state, since provider doesn't care
1387
- self.assertEqual(params['scope'], ['account'])
1554
+ self.assertEqual(params['scope'], ['User.Read offline_access openid profile'])
1555
+ self.assertEqual(params['response_type'], ['code'])
1556
+ # msal's get_authorization_request_url is called without a
1557
+ # redirect_uri, so none should be present here
1558
+ self.assertNotIn('redirect_uri', params)
1388
1559
  except (KeyError, AssertionError) as e:
1389
1560
  returnQuery = urllib.parse.urlencode({
1390
1561
  'error': repr(e),
@@ -1398,15 +1569,79 @@ class OauthTest(base.TestCase):
1398
1569
  return {
1399
1570
  'status_code': 302,
1400
1571
  'headers': {
1401
- 'Location': '%s?%s' % (redirectUri, returnQuery)
1572
+ 'Location': 'http://127.0.0.1:30000/api/v1/oauth/microsoft/callback?%s'
1573
+ % returnQuery
1574
+ }
1575
+ }
1576
+
1577
+ @httmock.urlmatch(scheme='https', netloc='^login.microsoftonline.com$',
1578
+ path='^/organizations/oauth2/v2.0/token$', method='POST')
1579
+ def mockMicrosoftToken(url, request):
1580
+ try:
1581
+ params = urllib.parse.parse_qs(request.body)
1582
+ self.assertEqual(params['grant_type'], ['authorization_code'])
1583
+ self.assertEqual(params['client_id'], [providerInfo['client_id']['value']])
1584
+ for account in providerInfo['accounts'].values():
1585
+ if account['auth_code'] == params['code'][0]:
1586
+ break
1587
+ else:
1588
+ self.fail()
1589
+ self.assertEqual(params['client_secret'], [providerInfo['client_secret']['value']])
1590
+ except (KeyError, AssertionError) as e:
1591
+ return {
1592
+ 'status_code': 400,
1593
+ 'headers': {
1594
+ 'Content-Type': 'application/json'
1595
+ },
1596
+ 'content': json.dumps({
1597
+ 'error': repr(e),
1598
+ 'error_description': repr(e)
1599
+ })
1402
1600
  }
1601
+ return {
1602
+ 'status_code': 200,
1603
+ 'headers': {
1604
+ 'Content-Type': 'application/json'
1605
+ },
1606
+ 'content': json.dumps({
1607
+ 'token_type': 'Bearer',
1608
+ 'access_token': account['access_token'],
1609
+ 'expires_in': 3600
1610
+ })
1403
1611
  }
1404
1612
 
1613
+ @httmock.urlmatch(scheme='https', netloc='^graph.microsoft.com$',
1614
+ path='^/v1.0/me$', method='GET')
1615
+ def mockMicrosoftApiUser(url, request):
1616
+ try:
1617
+ for account in providerInfo['accounts'].values():
1618
+ if 'Bearer %s' % account['access_token'] == request.headers['Authorization']:
1619
+ break
1620
+ else:
1621
+ self.fail()
1622
+ except AssertionError as e:
1623
+ return {
1624
+ 'status_code': 401,
1625
+ 'content': json.dumps({
1626
+ 'message': repr(e)
1627
+ })
1628
+ }
1629
+ return json.dumps({
1630
+ 'id': account['user']['oauth']['id'],
1631
+ 'mail': account['user']['email'],
1632
+ 'givenName': account['user']['firstName'],
1633
+ 'surname': account['user']['lastName']
1634
+ })
1635
+
1636
+ # The mocked authority URLs above use the 'organizations' tenant,
1637
+ # whereas the default tenant is 'common'
1638
+ Setting().set(PluginSettings.MICROSOFT_TENANT_ID, 'organizations')
1639
+
1405
1640
  with httmock.HTTMock(
1641
+ mockMicrosoftDiscovery,
1406
1642
  mockMicrosoftRedirect,
1407
- # mockMicrosoftToken,
1408
- # mockMicrosoftApiUser,
1409
- # mockMicrosoftApiEmail,
1643
+ mockMicrosoftToken,
1644
+ mockMicrosoftApiUser,
1410
1645
  # Must keep 'mockOtherRequest' last
1411
1646
  self.mockOtherRequest,
1412
1647
  ):
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "5.0.19",
3
- "distance": 2,
4
- "node": "ge191b30e7b48e2656220391cec9322e14a51e4ea",
5
- "dirty": false,
6
- "branch": "master",
7
- "node_date": "2026-10-13"
8
- }