Skip to content
This repository was archived by the owner on Nov 15, 2022. It is now read-only.

Commit fe822ae

Browse files
Merge pull request #1 from discogs/auth-token-api
Auth token api
2 parents ed03c67 + 220c02b commit fe822ae

3 files changed

Lines changed: 201 additions & 49 deletions

File tree

cas_client/cas_client.py

Lines changed: 118 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,36 @@ def delete_session(self, ticket):
7878
logging.debug('[CAS] Deleting session for ticket {}'.format(ticket))
7979
self.session_storage_adapter.delete(ticket)
8080

81+
def get_api_url(
82+
self,
83+
api_resource,
84+
auth_token_ticket,
85+
authenticator,
86+
private_key,
87+
service_url=None,
88+
**kwargs
89+
):
90+
'''
91+
Build an auth-token-protected CAS API url.
92+
'''
93+
auth_token, auth_token_signature = self._build_auth_token_data(
94+
auth_token_ticket,
95+
authenticator,
96+
private_key,
97+
**kwargs
98+
)
99+
params = {
100+
'at': auth_token,
101+
'ats': auth_token_signature,
102+
}
103+
if service_url is not None:
104+
params['service'] = service_url
105+
url = '{}?{}'.format(
106+
self._get_api_url(api_resource),
107+
urlencode(params),
108+
)
109+
return url
110+
81111
def get_auth_token_login_url(
82112
self,
83113
auth_token_ticket,
@@ -91,26 +121,13 @@ def get_auth_token_login_url(
91121
92122
See https://github.com/rbCAS/CASino/wiki/Auth-Token-Login for details.
93123
'''
94-
rsa_key = RSA.importKey(private_key)
95-
signer = PKCS1_v1_5.new(rsa_key)
96-
97-
auth_token = json.dumps({
98-
'authenticator': authenticator,
99-
'username': username,
100-
'ticket': auth_token_ticket,
101-
})
102-
if six.PY3:
103-
auth_token = auth_token.encode('utf-8')
104-
124+
auth_token, auth_token_signature = self._build_auth_token_data(
125+
auth_token_ticket,
126+
authenticator,
127+
private_key,
128+
username=username,
129+
)
105130
logging.debug('[CAS] AuthToken: {}'.format(auth_token))
106-
107-
digest = SHA256.new()
108-
digest.update(auth_token)
109-
auth_token = base64.b64encode(auth_token)
110-
111-
auth_token_signature = signer.sign(digest)
112-
auth_token_signature = base64.b64encode(auth_token_signature)
113-
114131
url = self._get_auth_token_login_url(
115132
auth_token=auth_token,
116133
auth_token_signature=auth_token_signature,
@@ -185,30 +202,6 @@ def get_logout_url(self, service_url=None):
185202
logging.debug('[CAS] Logout URL: {}'.format(url))
186203
return url
187204

188-
def perform_proxy(self, proxy_ticket):
189-
'''
190-
Fetch a response from the remote CAS `proxy` endpoint.
191-
'''
192-
url = self._get_proxy_url(ticket=proxy_ticket)
193-
logging.debug('[CAS] Proxy URL: {}'.format(url))
194-
return self._perform_cas_call(url, ticket=proxy_ticket)
195-
196-
def perform_proxy_validate(self, proxied_service_ticket):
197-
'''
198-
Fetch a response from the remote CAS `proxyValidate` endpoint.
199-
'''
200-
url = self._get_proxy_validate_url(ticket=proxied_service_ticket)
201-
logging.debug('[CAS] ProxyValidate URL: {}'.format(url))
202-
return self._perform_cas_call(url, ticket=proxied_service_ticket)
203-
204-
def perform_service_validate(self, ticket=None, service_url=None):
205-
'''
206-
Fetch a response from the remote CAS `serviceValidate` endpoint.
207-
'''
208-
url = self._get_service_validate_url(ticket, service_url=service_url)
209-
logging.debug('[CAS] ServiceValidate URL: {}'.format(url))
210-
return self._perform_cas_call(url, ticket=ticket)
211-
212205
def parse_logout_request(self, message_text):
213206
'''
214207
Parse the contents of a CAS `LogoutRequest` XML message.
@@ -256,6 +249,58 @@ def parse_logout_request(self, message_text):
256249
))
257250
return result
258251

252+
def perform_api_request(
253+
self,
254+
api_resource,
255+
auth_token_ticket,
256+
authenticator,
257+
private_key,
258+
method='POST',
259+
service_url=None,
260+
**kwargs
261+
):
262+
'''
263+
Perform an auth-token-protected request against a CAS API endpoint.
264+
'''
265+
assert method in ('GET', 'POST')
266+
url = self.get_api_url(
267+
api_resource,
268+
auth_token_ticket,
269+
authenticator,
270+
private_key,
271+
service_url=None,
272+
**kwargs
273+
)
274+
if method == 'GET':
275+
response = self._perform_get(url)
276+
elif method == 'POST':
277+
response = self._perform_post(url)
278+
return response
279+
280+
def perform_proxy(self, proxy_ticket):
281+
'''
282+
Fetch a response from the remote CAS `proxy` endpoint.
283+
'''
284+
url = self._get_proxy_url(ticket=proxy_ticket)
285+
logging.debug('[CAS] Proxy URL: {}'.format(url))
286+
return self._perform_cas_call(url, ticket=proxy_ticket)
287+
288+
def perform_proxy_validate(self, proxied_service_ticket):
289+
'''
290+
Fetch a response from the remote CAS `proxyValidate` endpoint.
291+
'''
292+
url = self._get_proxy_validate_url(ticket=proxied_service_ticket)
293+
logging.debug('[CAS] ProxyValidate URL: {}'.format(url))
294+
return self._perform_cas_call(url, ticket=proxied_service_ticket)
295+
296+
def perform_service_validate(self, ticket=None, service_url=None):
297+
'''
298+
Fetch a response from the remote CAS `serviceValidate` endpoint.
299+
'''
300+
url = self._get_service_validate_url(ticket, service_url=service_url)
301+
logging.debug('[CAS] ServiceValidate URL: {}'.format(url))
302+
return self._perform_cas_call(url, ticket=ticket)
303+
259304
def session_exists(self, ticket):
260305
'''
261306
Test if a session records exists for a service ticket.
@@ -267,6 +312,30 @@ def session_exists(self, ticket):
267312

268313
### PRIVATE METHODS ###
269314

315+
def _build_auth_token_data(
316+
self,
317+
auth_token_ticket,
318+
authenticator,
319+
private_key,
320+
**kwargs
321+
):
322+
auth_token = dict(
323+
authenticator=authenticator,
324+
ticket=auth_token_ticket,
325+
**kwargs
326+
)
327+
auth_token = json.dumps(auth_token, sort_keys=True)
328+
if six.PY3:
329+
auth_token = auth_token.encode('utf-8')
330+
digest = SHA256.new()
331+
digest.update(auth_token)
332+
auth_token = base64.b64encode(auth_token)
333+
rsa_key = RSA.importKey(private_key)
334+
signer = PKCS1_v1_5.new(rsa_key)
335+
auth_token_signature = signer.sign(digest)
336+
auth_token_signature = base64.b64encode(auth_token_signature)
337+
return auth_token, auth_token_signature
338+
270339
def _clean_up_response_text(self, response_text):
271340
lines = []
272341
for line in response_text.splitlines():
@@ -275,14 +344,18 @@ def _clean_up_response_text(self, response_text):
275344
lines.append(line)
276345
return '\n'.join(lines)
277346

278-
def _get_auth_token_tickets_url(self):
279-
template = '{server_url}{auth_prefix}/api/auth_token_tickets'
347+
def _get_api_url(self, api_resource):
348+
template = '{server_url}{auth_prefix}/api/{api_resource}'
280349
url = template.format(
350+
api_resource=api_resource,
281351
auth_prefix=self.auth_prefix,
282352
server_url=self.server_url,
283353
)
284354
return url
285355

356+
def _get_auth_token_tickets_url(self):
357+
return self._get_api_url('auth_token_tickets')
358+
286359
def _get_auth_token_login_url(self, auth_token, auth_token_signature, service_url):
287360
template = '{server_url}{auth_prefix}/authTokenLogin?{query_string}'
288361
query_string = urlencode({

test.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# -*- encoding: utf-8 -*-
2-
from cas_client import CASClient, CASResponse
32
import unittest
3+
from cas_client import CASClient, CASResponse
4+
try:
5+
from urlparse import parse_qs
6+
except ImportError:
7+
from urllib.parse import parse_qs
48

59

610
class TestCase(unittest.TestCase):
@@ -125,7 +129,56 @@ def test_parse_logout_request_2(self):
125129
'xmlns:samlp': 'urn:oasis:names:tc:SAML:2.0:protocol',
126130
})
127131

128-
def test_get_auth_token_logic_url(self):
132+
def test_get_api_url(self):
133+
cas_client = CASClient('https://dummy.url')
134+
api_resource = 'do_something_useful'
135+
auth_token_ticket = 'ATT-1234'
136+
authenticator = 'my_company_ldap'
137+
private_key_filepath = 'test_private_key.pem'
138+
with open(private_key_filepath, 'r') as file_pointer:
139+
private_key = file_pointer.read()
140+
service_url = 'https://example.com'
141+
kwargs = {
142+
'and': 'another_thing',
143+
'you': 'should_know',
144+
}
145+
url = cas_client.get_api_url(
146+
api_resource=api_resource,
147+
auth_token_ticket=auth_token_ticket,
148+
authenticator=authenticator,
149+
private_key=private_key,
150+
service_url=service_url,
151+
**kwargs
152+
)
153+
query_string = url.partition('?')[-1]
154+
query_parameters = {
155+
key: value[0]
156+
for key, value in parse_qs(query_string).items()
157+
}
158+
assert query_parameters == {
159+
'at': (
160+
'eyJhbmQiOiAiYW5vdGhlcl90aGluZyIsICJhdXRoZW50aWNhdG9yIjogIm15'
161+
'X2NvbXBhbnlfbGRhcCIsICJ0aWNrZXQiOiAiQVRULTEyMzQiLCAieW91Ijog'
162+
'InNob3VsZF9rbm93In0='
163+
),
164+
'ats': (
165+
'FISMx+fVfKKzI160MQRMauKdeqBRzzg+Ihwh0WqhqcnW4d+S0IyrTg6/oY1a'
166+
'wGvhBGrSMzOEBfYyihj5SxmLMr+xWm5Ndt+m0WcjuOR2GEwtEimIbbEQslCu'
167+
'f+//tG2u3UacStBRctt/cWnIGlW9cIPlUgU4iVVQtpbC7DdJc9+2rwzN10jV'
168+
'36JUwAWWT3iQseTiyMy+Bbuu1bzTcdtKvBdHTnCwcu1m9vkQraH/ZuVbYVMB'
169+
'jZC1s5lXECLN+fnC00laglYmgQ1w59EoQIXuaaHFqgq+zRvRxm4r0ASG5F0D'
170+
'bPT0fEDihQulSAbyOY5/6nhkFq6NYlJADKuGchFusk9D3Pcgs2KyEW3xvBb4'
171+
'ZArn2oaI8sxjOYUXutf1xe5MBGy8oTW+3QbHVv+hzXOrwJXsbSz6bx3gmDYb'
172+
'bDilhbRgPQeTH17IwqArrVgnjgcAMoDk6cTqU548S19KMc8B99pVZ7JMM5Ls'
173+
'uKx/ZWUF0naXFeuEaFJ5TdaO6HhhiRhUAEwlnwTQwwJuR1VtcYx4z3Lb5NhN'
174+
'CtH658M8acru4Dv4jV5NC3IPJcCijKGVjZQ0K6GrD863fr3usnH1gvnTzNgJ'
175+
'1jijF4FmyIr8E9kpNM5Mk7D0AqSGCC2nZcu/r4+2rcLiq9XxViv3jpe44alQ'
176+
'RjhkcqcbkcJvnhckfgjrU7w='
177+
),
178+
'service': 'https://example.com',
179+
}
180+
181+
def test_get_auth_token_login_url(self):
129182
cas_client = CASClient('https://dummy.url')
130183
auth_token_ticket = 'AT-1234'
131184
authenticator = 'my_company_ldap'
@@ -134,10 +187,36 @@ def test_get_auth_token_logic_url(self):
134187
private_key_filepath = 'test_private_key.pem'
135188
with open(private_key_filepath, 'r') as file_pointer:
136189
private_key = file_pointer.read()
137-
cas_client.get_auth_token_login_url(
190+
url = cas_client.get_auth_token_login_url(
138191
auth_token_ticket=auth_token_ticket,
139192
authenticator=authenticator,
140193
private_key=private_key,
141194
service_url=service_url,
142195
username=username,
143196
)
197+
query_string = url.partition('?')[-1]
198+
query_parameters = {
199+
key: value[0]
200+
for key, value in parse_qs(query_string).items()
201+
}
202+
assert query_parameters == {
203+
'at': (
204+
'eyJhdXRoZW50aWNhdG9yIjogIm15X2NvbXBhbnlfbGRhcCIsICJ0aWNrZXQi'
205+
'OiAiQVQtMTIzNCIsICJ1c2VybmFtZSI6ICJteV91c2VyIn0='
206+
),
207+
'ats': (
208+
'pZ3m58k8Xpd+TDlYb+VDV89TVGoPIAgsxDMNGtNLqzchg/EFy12NzVaUbVSz'
209+
'1PNZdQ/klMrfvxzehLlFp9QkyfFoUS5pgUo9XXjpowWe0E9eKX5hBJjpmvD+'
210+
'PhSMRXFOPUOLRohRX45aPqJ4mjh2MNP0mzKrRfoRoUT/6mmrvLRJu150rtnS'
211+
'A5E4n0V4BeJXWIFYqqu8B4CP3fbg18HMB5g36P61m6I67kDmBLfTlmtrwvM5'
212+
'Vh3r9q9HFGn1NGmdMTcqGwAqfrww2XuBBemTpcfvSLNhTf/nZ21042BDt0+J'
213+
'TLNsGBxNKS39NznyOcf2g5XtscdJXcDcKan/eJI7WHNtpmJPzhA4H5wTuAm7'
214+
'X0WgAN7hxmTYy3E0241j6Q1DNDuxvgkSMS7CJhD3p0Fp0kHsdCslLuqjMoou'
215+
'THSshfJU6lvE4dc1vh3fdzKiAcmvMQ2RT4ACNQVwVYiE9UWu23D16yz08sV2'
216+
'9kzlFTCTXT608tHMVCx1x7K959IxcRUFld314ooqJ5BgrK/2QqtZXS0w581f'
217+
'8P5qViQoOrQ5gRiPZ/bT6eF24RLuKN78VEkak2z0B1aZqpEcG3wQC4qHeUaM'
218+
'TgrihbVi6eIv7N5k6srSyGCAQ/9k7o53ZKG8MzkqMJq53AoEXNj8HNQxgO0D'
219+
'OtFwXLMrlrFpmqPS5OcO9NM='
220+
),
221+
'service': 'https://example.com',
222+
}

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ envlist = py27, py34
44
[testenv]
55
deps=pytest
66
commands=
7-
py.test -rf test.py
7+
py.test -rf -vv test.py
88
python -m doctest cas_client/cas_client.py

0 commit comments

Comments
 (0)