Skip to content

Commit af0a105

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into stormpath-285
2 parents 2925684 + 8c0820f commit af0a105

File tree

4 files changed

+145
-0
lines changed

4 files changed

+145
-0
lines changed

‎CHANGES.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Version 2.5.2
66
- Adding support for Stormpath Client API configuration.
77
- Adding a pytest cleanup hook to ensure all Stormpath test resources are
88
destroyed even if the user performs a keyboard interrupt during testing.
9+
- Adding tests for token revocation.
10+
- Adding support for OAuth2 Client Credentials flow.
911

1012

1113
Version 2.5.1

‎stormpath/api_auth.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,46 @@ def authenticate(self, username, password, account_store=None, url=None):
693693
)
694694

695695

696+
class ClientCredentialsGrantAuthenticator(Authenticator):
697+
"""
698+
This class should authenticate using user's API ID and secret.
699+
It gets authentication tokens for valid credentials.
700+
"""
701+
def authenticate(self, client_id, client_secret, account_store=None, url=None):
702+
if not url:
703+
url = self.app.href + '/oauth/token'
704+
705+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
706+
707+
data = {
708+
'grant_type': 'client_credentials',
709+
'client_id': client_id,
710+
'client_secret': client_secret
711+
}
712+
713+
if account_store:
714+
if isinstance(account_store, string_types):
715+
data['accountStore'] = account_store
716+
elif hasattr(account_store, 'href'):
717+
data['accountStore'] = account_store.href
718+
else:
719+
raise TypeError('Unsupported type for account_store.')
720+
721+
try:
722+
res = self.app._store.executor.request('POST', url, headers=headers, data=data)
723+
except StormpathError:
724+
return None
725+
726+
refresh_token = res['refresh_token'] if 'refresh_token' in res else None
727+
728+
return PasswordAuthenticationResult(
729+
self.app,
730+
res['stormpath_access_token_href'],
731+
res['access_token'], res['expires_in'],
732+
res['token_type'], refresh_token
733+
)
734+
735+
696736
class JwtAuthenticator(Authenticator):
697737
"""This class should authenticate using access token. It can
698738
validate token using Stormpath or local validation.

‎tests/live/test_api_auth.py‎

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,78 @@ def test_authenticate_with_account_store_org_href_succeeds(self):
13161316
self.assertEqual(claims.get('org'), self.org.href)
13171317

13181318

1319+
class TestClientCredentialsGrantAuthenticator(ApiKeyBase):
1320+
1321+
def setUp(self):
1322+
super(TestClientCredentialsGrantAuthenticator, self).setUp()
1323+
1324+
self.username = self.get_random_name()
1325+
self.password = 'W00t123!' + self.username
1326+
_, self.acc = self.create_account(self.app.accounts,
1327+
username=self.username,
1328+
password=self.password)
1329+
1330+
self.user_api_key = self.acc.api_keys.create()
1331+
1332+
org_name = self.get_random_name()
1333+
org_name_key = org_name[:63]
1334+
1335+
self.org = self.client.tenant.organizations.create({
1336+
'name': org_name,
1337+
'name_key': org_name_key,
1338+
})
1339+
self.client.organization_account_store_mappings.create({
1340+
'account_store': self.dir,
1341+
'organization': self.org,
1342+
})
1343+
self.client.account_store_mappings.create({
1344+
'account_store': self.org,
1345+
'application': self.app,
1346+
})
1347+
1348+
def test_authenticate_succeeds(self):
1349+
authenticator = ClientCredentialsGrantAuthenticator(self.app)
1350+
result = authenticator.authenticate(self.user_api_key.id,
1351+
self.user_api_key.secret)
1352+
1353+
self.assertTrue(result.access_token)
1354+
self.assertFalse(result.refresh_token.token)
1355+
self.assertTrue(result.stormpath_access_token)
1356+
self.assertEqual(result.token_type, 'Bearer')
1357+
self.assertEqual(result.expires_in, 3600)
1358+
self.assertEqual(result.account.href, self.acc.href)
1359+
1360+
def test_authenticate_fails(self):
1361+
authenticator = ClientCredentialsGrantAuthenticator(self.app)
1362+
result = authenticator.authenticate('wrong id', 'wrong secret')
1363+
1364+
self.assertIsNone(result)
1365+
1366+
def test_authenticate_with_account_store_succeeds(self):
1367+
authenticator = ClientCredentialsGrantAuthenticator(self.app)
1368+
result = authenticator.authenticate(self.user_api_key.id,
1369+
self.user_api_key.secret,
1370+
account_store=self.dir)
1371+
1372+
self.assertTrue(result.access_token)
1373+
self.assertEqual(result.account.href, self.acc.href)
1374+
self.assertTrue('access_token' in result.access_token.to_json())
1375+
self.assertTrue(hasattr(result.stormpath_access_token, 'href'))
1376+
self.assertEqual(result.stormpath_access_token.account.href,
1377+
self.acc.href)
1378+
self.assertEqual(result.token_type, 'Bearer')
1379+
self.assertEqual(result.expires_in, 3600)
1380+
self.assertEqual(result.account.href, self.acc.href)
1381+
1382+
def test_authenticate_with_account_store_fails(self):
1383+
authenticator = ClientCredentialsGrantAuthenticator(self.app)
1384+
result = authenticator.authenticate('wrong id',
1385+
'wrong secret',
1386+
account_store=self.dir)
1387+
1388+
self.assertIsNone(result)
1389+
1390+
13191391
class TestJwtAuthenticator(ApiKeyBase):
13201392
def setUp(self):
13211393
super(TestJwtAuthenticator, self).setUp()
@@ -1602,3 +1674,29 @@ def test_authenticate_with_invalid_token_fails(self):
16021674
result = authenticator.authenticate('invalid_token')
16031675

16041676
self.assertIsNone(result)
1677+
1678+
1679+
class TestTokenRevocation(ApiKeyBase):
1680+
1681+
def setUp(self):
1682+
super(TestTokenRevocation, self).setUp()
1683+
1684+
self.username = self.get_random_name()
1685+
self.password = 'W00t123!' + self.username
1686+
_, self.acc = self.create_account(self.app.accounts,
1687+
username=self.username,
1688+
password=self.password)
1689+
1690+
def test_revoke_token_succeeds(self):
1691+
authenticator = PasswordGrantAuthenticator(self.app)
1692+
result = authenticator.authenticate(self.username, self.password)
1693+
1694+
self.assertTrue(result.access_token)
1695+
self.assertEqual(result.account.href, self.acc.href)
1696+
1697+
acc_tokens = self.acc.access_tokens
1698+
self.assertEqual(len(acc_tokens.items), 1)
1699+
1700+
acc_tokens.items[0].delete()
1701+
acc_tokens.refresh()
1702+
self.assertEqual(len(acc_tokens.items), 0)

‎tests/live/test_custom_data.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,11 +343,16 @@ def test_custom_data_modification(self):
343343
def test_custom_data_search(self):
344344
self.dir.groups.create({'name': self.get_random_name() + 'group1', 'custom_data': CUSTOM_DATA})
345345
self.dir.groups.create({'name': self.get_random_name() + 'group2', 'custom_data': {'omg': 'noway'}})
346+
self.dir.groups.create({'name': self.get_random_name() + 'group3', 'custom_data': {'omg': ['wow', 'cool']}})
346347

347348
for group in self.dir.groups.search('customData.omg=noway'):
348349
self.assertTrue('group2' in group.name)
349350
self.assertEqual(group.custom_data['omg'], 'noway')
350351

352+
for gorup in self.dir.groups.earch('customData.omg=wow'):
353+
self.assertTrue('group3' in group.name)
354+
self.assertTrue(group.custom_data['omg'], ['wow', 'cool'])
355+
351356

352357
class TestTenantCustomData(SingleApplicationBase):
353358

0 commit comments

Comments
 (0)