├── .gitignore ├── LICENSE ├── README.md ├── authliboclc ├── __init__.py ├── accesstoken.py ├── authcode.py ├── refreshtoken.py ├── user.py └── wskey.py ├── examples ├── authentication_token │ ├── README.md │ ├── __init__.py │ ├── access_token_formatter.py │ ├── bibliographic_record.py │ ├── server.pem │ ├── server.py │ ├── session_handler.py │ └── tests │ │ ├── __init__.py │ │ ├── access_token_formatter_test.py │ │ ├── bibliographic_record_test.py │ │ ├── server_test.py │ │ └── session_handler_test.py ├── authentication_token_with_django │ ├── README.md │ ├── authTokenApp │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── models.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── djangoProject │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── manage.py ├── client_credentials_grant │ ├── README.md │ └── client_credentials_grant.py └── hmac_authentication │ ├── README.md │ └── hmac_request_example.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── accesstoken_test.py ├── authcode_test.py ├── refreshtoken_test.py ├── user_test.py └── wskey_test.py /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm IDE 2 | .idea 3 | 4 | # mac osx 5 | .DS_Store 6 | 7 | # python 8 | *.pyc 9 | *.pyo 10 | .installed.cfg 11 | bin 12 | build 13 | develop-eggs 14 | dist 15 | downloads 16 | eggs 17 | parts 18 | src/*.egg-info 19 | lib 20 | lib64 21 | 22 | # project 23 | main.py 24 | credentials.txt 25 | switcher.py 26 | MANIFEST 27 | *.p 28 | *.sh 29 | /.project 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OCLC Python Authentication Library 2 | ================================== 3 | 4 | > **WARNING**: This library is being sunset. Please use a standard OAuth 2.0 client. See https://github.com/OCLC-Developer-Network/gists/tree/master/authentication/python for example usage 5 | 6 | This library is a wrapper around the Web Service Authentication system used by OCLC web services, written for Python. It works with versions 2.7 and 3 (up to 3.6) 7 | 8 | Installation 9 | ------------ 10 | The easiest way to install is via pip: 11 | 12 | `pip install git+https://github.com/OCLC-Developer-Network/oclc-auth-python` 13 | 14 | Alternatively, clone the repository: 15 | 16 | `git clone https://github.com/OCLC-Developer-Network/oclc-auth-python.git` 17 | 18 | Install the library: 19 | 20 | `sudo python setup.py install` 21 | 22 | 23 | Running the Examples 24 | ==================== 25 | 26 | ### Server Side HMAC Authentication Example 27 | 28 | 1. Change directories to `examples/hmac_authentication` 29 | 30 | 1. Edit `hmac_request_example.py` to insert your: 31 | * key 32 | * secret 33 | * principal_id 34 | * principal_idns 35 | * authenticating_institution_id 36 |

37 | 1. Run from the command line: 38 | 39 | `python hmac_request_example.py` 40 | 41 | You should get back an XML result if your WSKey is configured properly. 42 | 43 |
 44 |    <?xml version="1.0" encoding="UTF-8"?>
 45 |        <entry xmlns="http://www.w3.org/2005/Atom">
 46 |        <content type="application/xml">
 47 |        <response xmlns="http://worldcat.org/rb" mimeType="application/vnd.oclc.marc21+xml">
 48 |        <record xmlns="http://www.loc.gov/MARC21/slim">
 49 |        <leader>00000cam a2200000Ia 4500
 50 |        ...
 51 |    
52 | 53 | ### Getting an Access Token with Client Credentials Grant Example 54 | 55 | 1. Change directories to `examples/client_credentials_grant` 56 | 57 | 1. Edit `client_credentials_grant.py` to insert your: 58 | * key 59 | * secret 60 | * authenticating_institution_id 61 | * context_institution_id 62 |

63 | 1. Run from the command line: 64 | 65 | `python client_credentials_grant.py` 66 | 67 | You should get back an access token if your WSKey is configured properly. 68 | 69 |
 70 |    access token:  tk_xxx5KWq9w1Cc0dc5MrvIhFvdEZteylgsR7VT
 71 |    expires_in:    1199
 72 |    expires_at:    2014-09-09 15:22:49Z
 73 |    type:          bearer
 74 |    
75 | 76 | Or an error message if the key is not configured properly 77 | 78 |
 79 |    error_code:    401
 80 |    error_message: HTTP Error 401: Unauthorized
 81 |    error_url:     https://authn.sd00.worldcat.org/oauth2/accessToken?
 82 |                   grant_type=client_credentials&
 83 |                   authenticatingInstitutionId=128807&
 84 |                   contextInstitutionId=128807&
 85 |                   scope=WorldCatDiscoveryAPI
 86 |    
87 | 88 | ### User Authentication and Access Token Example 89 | 90 | This example demonstrates how to retrieve an access token, and has the following features: 91 | * Provides a basic HTTPS server 92 | * Redirects a user to authenticate to retrieve an Access Code 93 | * Uses the Access Code to retrieve an Access Token 94 | * Stores the Access Token in a Session and manages a list of sessions using a simple flat file. 95 | * Uses the Access Token to request a Bibliographic Record from OCLC. 96 | 97 | To use the example: 98 | 99 | 1. Change directories to `examples/authentication_token` 100 | 101 | 1. Edit server.py to insert your WSKey parameters: 102 |
103 |    KEY = '{clientID}'
104 |    SECRET = '{secret}'
105 |    
106 | 107 | 1. From the command line: 108 | 109 | `python server.py` 110 | 111 | 1. Navigate your browser to: 112 | 113 | `https://localhost:8000/auth/` 114 | 115 | Do not be concerned about "security warnings" - click through them. That is expected with the supplied, unsigned 116 | CACERT in server.pem. In production, you will use your institution's signed CACERT when implementing SSL. 117 | 118 | ### User Authentication and Access Token Django Example 119 | 120 | For performing client side authentication using Access Tokens, we prepared an example using a popular framework, Django. 121 | We show how to set up a simple Django App and implement SSL on the localhost for testing. 122 | 123 | First, we need to install these dependencies: 124 | 125 | 1. Change directories to `examples/djangoProject`. 126 | 127 | 2. Install `pip` if you have not already - pip. 128 | 129 | 3. Install Django (see Django Installation Guide). 130 | 131 | `sudo pip install django` 132 | 133 | 4. To run SSL from localhost, install a django-sslserver. 134 | 135 | `sudo pip install django-sslserver`
136 | 137 | An alternate method popular with Django developers is to install Stunnel. 138 | 139 | Note: if running stunnel, you should edit `djangoProject/settings.py` and remove the reference to sslserver: 140 |
141 |        INSTALLED_APPS = (
142 |            'django.contrib.admin',
143 |            'django.contrib.auth',
144 |            'django.contrib.contenttypes',
145 |            'django.contrib.sessions',
146 |            'django.contrib.messages',
147 |            'django.contrib.staticfiles',
148 |            'exampleAuthTokenDjangoApp',
149 |            'sslserver', # remove if using Stunnel
150 |        )
151 |    
152 | 153 | 5. Edit `djangoProject/views.py` and insert your Key and Secret. 154 | Note that your WSKey must be configured with these parameters: 155 | * RedirectURI that matches the URI you are running the example from. For example, https://localhost:8000/auth/ 156 | * Scopes. ie, WorldCatMetadataAPI for the Django example provided with this library. 157 | 158 | 6. Use runsslserver to start Django's SSL server from the `examples/authentication_token_with_django` directory: 159 | 160 | `python manage.py runsslserver` 161 | 162 | 7. Direct your browser to `https://localhost:8000/auth/`. 163 | 164 | 8. If all goes well, you should see some authentication warnings (that's expected - because runsslserver uses a self-signed CACERT). Click through the warning messages and you should see an authentication screen. 165 | 166 | * Sign in with your userId and Password 167 | * When prompted to allow access, click yes 168 | 169 |
170 | You should see your access token details and a sample Bibliographic record, in XML format. 171 | 172 | Using the Library 173 | ================= 174 | 175 | HMAC Signature 176 | -------------- 177 | 178 | Authentication for server side requests uses HMAC Signatures. Because this pattern uses a secret 179 | and a key, it is never meant for client-side use. HMAC Signatures are discussed in detail at 180 | 181 | OCLC Developer Network - Authentication. 182 | 183 | To use the `authliboclc` library to create a HMAC Signature, include the following libraries in your Python script: 184 | 185 |
186 | from authliboclc import wskey
187 | from authliboclc import user
188 | import urllib2
189 | 
190 | 191 | You must supply authentication parameters. OCLC Web Service Keys can be requested and managed here. 192 | 193 |
194 | key = '{clientID}'
195 | secret = '{secret}'
196 | principal_id = '{principalID}'
197 | principal_idns = '{principalIDNS}'
198 | authenticating_institution_id = '{institutionID}'
199 | 
200 | 201 | Construct a request URL. See OCLC web services documentation. For example, to request a Bibliographic Record: 202 | 203 |
204 | request_url = 'https://worldcat.org/bib/data/823520553?classificationScheme=LibraryOfCongress&holdingLibraryCode=MAIN'
205 | 
206 | 207 | Construct the wskey and user objects. 208 | 209 |
210 | my_wskey = wskey.Wskey(
211 |     key=key,
212 |     secret=secret,
213 |     options=None
214 | )
215 | 
216 | my_user = user.User(
217 |     authenticating_institution_id=authenticating_institution_id,
218 |     principal_id=principal_id,
219 |     principal_idns=principal_idns
220 | )
221 | 
222 | 223 | Note that the options parameter is for access token use and you do not need to add them for this example. For details, see the wskey.py file in the authliboclc library folder. 224 | 225 | Calculate the Authorization header: 226 | 227 |
228 | authorization_header = my_wskey.get_hmac_signature(
229 |     method='GET',
230 |     request_url=request_url,
231 |     options={
232 |         'user': my_user,
233 |         'auth_params': None}
234 | )
235 | 
236 | 237 | With our request URL and Authorization header prepared, we are ready to use Python's urllib2 238 | library to make the GET request. 239 | 240 |
241 | my_request = urllib2.Request(
242 |     url=request_url,
243 |     data=None,
244 |     headers={'Authorization': authorization_header}
245 | )
246 | 
247 | try:
248 |     xmlresult = urllib2.urlopen(myRequest).read()
249 |     print(xmlresult)
250 | 
251 | except urllib2.HTTPError, e:
252 |     print ('** ' + str(e) + ' **')
253 | 
254 | 255 | You should get a string containing an xml object, or an error message if a parameter is wrong or the WSKey is not configured properly. 256 | 257 | User Authentication with Access Tokens 258 | -------------------------------------- 259 | 260 | The imports for working with the authentication library inside a Django view look like this: 261 | 262 |
263 | from django.http import HttpResponse
264 | from authliboclc import wskey
265 | import urllib2
266 | 
267 | 268 | The authentication pattern is 269 | described in detail on the OCLC Developer Network. More specifically, we implemented the 270 | 271 | Explicit Authorization Code pattern in the authliboclc library. 272 | 273 | #### Request an Authorization Code. 274 | 275 | An Authorization Code 276 | is a unique string which is returned in the url after a user has successfully authenticated. The Authorization Code will then be 277 | exchanged by the client to obtain Access Tokens: 278 | 279 | 1. You need to gather your authentication parameters: 280 | * key 281 | * secret 282 | * context_institution_id 283 | * authenticating_institution_id 284 | * services (api service name, ie `WorldCatMetadataAPI` for the Metadata API 285 | * redirect_uri (where your app runs on the web, i.e. `https://localhost:8000/auth/` 286 | 287 | 1. Create a wskey object: 288 |
289 |    myWskey = wskey.Wskey(
290 |        key=key,
291 |        secret=secret,
292 |        options={
293 |            'services': ['service1' {,'service2',...} ],
294 |            'redirect_uri': redirect_uri
295 |        }
296 |    )
297 |    
298 | 299 | 1. Generate a login URL and redirect to it: 300 |
301 |    login_url = myWskey.get_login_url(
302 |         authenticating_institution_id='{your institutionId}',
303 |         context_institution_id='{your institutionId}'
304 |     )
305 |     response['Location'] = login_url
306 |     response.status_code = '303'
307 |     
308 | 309 | 1. The user will be prompted to sign in with a UserId and Password. If they authenticate successfully, you will 310 | receive back a url with a code parameter embedded in it. Parse out the code parameter to be used to request an Access Token. 311 | 312 | #### Use the Authorization Code to request an Access Token. 313 | 314 | An Access Token is a unique string which the client will send to the web service in order to authenticate itself. Each 315 | Access Token represents a particular application’s right to access set of web services, on behalf of a given user in 316 | order to read or write data associated with a specific institution during a specific time period. 317 | 318 | This library function takes the code and makes the Access Token request, returning the Access Token object. 319 | 320 | access_token = myWskey.get_access_token_with_auth_code( 321 | code=code, 322 | authenticating_institution_id='128807', 323 | context_institution_id='128807' 324 | ) 325 | 326 | The access token object has these parameters: 327 | 328 | * accessTokenString 329 | * type 330 | * expiresAt (ISO 8601 time) 331 | * expiresIn (int, seconds) 332 | * user 333 | * principal_id 334 | * principal_idns 335 | * authenticating_institution_id 336 | * context_institution_id 337 | * errorCode 338 | 339 | If you include refresh_token as one of the services, you will also get back a refresh token: 340 | 341 | * refreshToken 342 | * refreshToken (the string value of the token) 343 | * expiresAt (ISO 8601 time) 344 | * expiresIn (int, seconds) 345 | 346 | 347 | #### Making requests with the Access Token 348 | 349 | Our access token has a user object which contains a principalID and principalIDNS. We can use those parameters to make 350 | a Bibliographic Record request. For example, let's retrieve the record for OCLC Number 823520553: 351 | 352 |
353 | request_url = (
354 |     'https://worldcat.org/bib/data/823520553?' +
355 |     'classificationScheme=LibraryOfCongress' +
356 |     '&holdingLibraryCode=MAIN'
357 | )
358 | 
359 | 360 | Now we construct an authorization header using our Access Token's user parameter: 361 | 362 |
363 | authorization_header = my_wskey.get_hmac_signature(
364 |     method='GET',
365 |     request_url=request_url,
366 |     options={
367 |         'user': access_token.user
368 |     }
369 | )
370 | 
371 | 372 | Finally, we make the request: 373 | 374 |
375 | myRequest = urllib2.Request(
376 |     url=request_url,
377 |     data=None,
378 |     headers={'Authorization': authorization_header}
379 | )
380 | 
381 | try:
382 |     xmlResult = urllib2.urlopen(myRequest).read()
383 | 
384 | except urllib2.HTTPError, e:
385 |     xmlResult = str(e)
386 | 
387 | 388 | Resources 389 | --------- 390 | 391 | * OCLC Developer Network 392 | * Authentication 393 | * Web Services 394 | * Manage your OCLC API Keys 395 | * OCLC's API Explorer 396 | -------------------------------------------------------------------------------- /authliboclc/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCLC-Developer-Network/oclc-auth-python/d4c56c604b9db280fc1c502e688a7fa9260ca9ec/authliboclc/__init__.py -------------------------------------------------------------------------------- /authliboclc/accesstoken.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # ############################################################################### 4 | # Copyright 2014 OCLC 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You may obtain a copy of 8 | # the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ############################################################################### 18 | 19 | """This class represents an access token object. 20 | 21 | Contains the parameters and methods to handle access tokens. An access token is received by having the user 22 | authenticate in a web context. Then an authorization code is returned, which is used to retrieve an access 23 | token from OCLC's servers. Calls against web services for a specific insitution and service are then permitted 24 | using the access token, or by using the principal_id and principal_idns returned with the user object portion 25 | of the access token. 26 | """ 27 | 28 | import json 29 | import string 30 | import time 31 | 32 | import six 33 | 34 | from .user import User 35 | from .refreshtoken import RefreshToken 36 | 37 | 38 | class InvalidGrantType(Exception): 39 | """Custom exception - an invalid grant type was passed""" 40 | 41 | def __init__(self, message): 42 | self.message = message 43 | 44 | 45 | class NoOptionsPassed(Exception): 46 | """Custom exception - no options were passed""" 47 | 48 | def __init__(self, message): 49 | self.message = message 50 | 51 | 52 | class RequiredOptionsMissing(Exception): 53 | """Custom exception - missing option""" 54 | 55 | def __init__(self, message): 56 | self.message = message 57 | 58 | 59 | class InvalidObject(Exception): 60 | """Custom exception - invalid parameter was passed""" 61 | 62 | def __init__(self, message): 63 | self.message = message 64 | 65 | 66 | class AccessToken(object): 67 | """ Create and manage an OCLC API access token 68 | 69 | Class Variables: 70 | 71 | access_token_string string the value of the access token, ie "at_..." 72 | access_token_url string the url for requesting an access token 73 | authenticating_institution_id string the institutionID the user authenticates against 74 | authorization_server string the url of the OCLC authorization server 75 | code string the authentication code string 76 | context_institution_id string the institutionID the user makes requests against 77 | error_code int the code, ie 401, if an access token fails. Normally None. 78 | error_message string the error message associated with the error code. Normally None. 79 | error_url string the request url that had the error 80 | expires_at string the ISO 8601 time that the refresh token expires at 81 | expires_in int the number of seconds until the token expires 82 | grant_type string 83 | options dict: Valid options are: 84 | - scope 85 | - authenticating_institution_id 86 | - context_institution_id 87 | - redirect_uri 88 | - code 89 | - refresh_token 90 | redirect_uri string string, the url that client authenticates from ie, https://localhost:8000/auth/ 91 | refresh_token string the refresh token object, see refreshtoken.py in the authliboclc folder. 92 | scope list web services associated with the WSKey, ie ['WorldCatMetadataAPI'] 93 | type str token type, for our use case it is always "bearer" 94 | user object user object, see user.py in the authliboclc folder. 95 | wskey object wskey object, see wskey.py in the authliboclc folder. 96 | """ 97 | 98 | access_token_string = None 99 | access_token_url = None 100 | authenticating_institution_id = None 101 | authorization_server = None 102 | code = None 103 | context_institution_id = None 104 | error_code = None 105 | error_message = None 106 | error_url = None 107 | expires_at = None 108 | expires_in = None 109 | grant_type = None 110 | options = None 111 | redirect_uri = None 112 | refresh_token = None 113 | scope = None 114 | type = None 115 | user = None 116 | wskey = None 117 | 118 | valid_options = [ 119 | 'scope', 120 | 'authenticating_institution_id', 121 | 'context_institution_id', 122 | 'redirect_uri', 123 | 'code', 124 | 'refresh_token' 125 | ] 126 | 127 | validGrantTypes = [ 128 | 'authorization_code', 129 | 'refresh_token', 130 | 'client_credentials' 131 | ] 132 | 133 | def __init__(self, authorization_server, grant_type=None, options=None): 134 | """Constructor. 135 | 136 | Args: 137 | authorization_server: string, url of the authorization server 138 | grant_type: string, the type of access token request to make: 139 | - authorization_code 140 | - client_credentials 141 | - refresh_token 142 | options: dict, options depend on the type of request being made, but may include: 143 | - scope 144 | - authenticating_institution_id 145 | - context_institution_id 146 | - redirect_uri 147 | - code 148 | - refresh_token 149 | """ 150 | self.authorization_server = authorization_server 151 | if grant_type is None or grant_type not in AccessToken.validGrantTypes: 152 | raise InvalidGrantType('You must pass a valid grant type to construct an Access Token.') 153 | self.grant_type = grant_type 154 | 155 | if not options: 156 | raise NoOptionsPassed('You must pass at least one option to construct an Access Token. Valid options ' 157 | 'are scope, authenticating_institution_id, context_institution_id, redirect_uri, ' 158 | 'code and refresh_token') 159 | 160 | if self.grant_type == 'authorization_code' and ( 161 | 'code' not in options or 162 | 'redirect_uri' not in options or 163 | 'authenticating_institution_id' not in options or 164 | 'context_institution_id' not in options): 165 | raise RequiredOptionsMissing('You must pass the options: code, redirect_uri, ' 166 | 'authenticating_institution_id and context_institution_id to construct an Access ' 167 | 'Token using the authorization_code grant type.') 168 | 169 | elif self.grant_type == 'client_credentials' and ( 170 | not 'scope' in options or 171 | not 'authenticating_institution_id' in options or 172 | not 'context_institution_id' in options or 173 | not 'scope' in options): 174 | raise RequiredOptionsMissing( 175 | 'You must pass the options: scope, authenticating_institution_id and context_institution_id ' + 176 | 'to construct an Access Token using the client_credential grant type.') 177 | 178 | elif self.grant_type == 'refresh_token' and 'refresh_token' not in options: 179 | raise RequiredOptionsMissing( 180 | 'You must pass the option refresh_token to construct an Access Token using the ' + 181 | 'refresh_token grant type.') 182 | 183 | if 'scope' in options and not isinstance(options['scope'], list): 184 | raise RequiredOptionsMissing("scope must be a list of one or more scopes, i.e. ['WMS_NCIP' {, ...}]") 185 | 186 | for key, value in six.iteritems(options): 187 | if key in AccessToken.valid_options: 188 | setattr(self, key, value) 189 | 190 | self.access_token_url = self.get_access_token_url() 191 | 192 | def is_expired(self): 193 | """Test if the token is expired. Returns true if it is.""" 194 | status = False 195 | if time.mktime(time.strptime(self.expires_at, "%Y-%m-%d %H:%M:%SZ")) < time.time(): 196 | status = True 197 | return status 198 | 199 | def create(self, wskey, user=None): 200 | """Create an access token.""" 201 | if not wskey.__class__.__name__ == 'Wskey': 202 | raise InvalidObject('A valid Wskey object is required.') 203 | elif user is not None and not user.__class__.__name__ == 'User': 204 | raise InvalidObject('A valid User object is required.') 205 | self.wskey = wskey 206 | if user is not None: 207 | self.user = user 208 | self.options = {'user': self.user} 209 | authorization = self.wskey.get_hmac_signature( 210 | method='POST', 211 | request_url=self.access_token_url, 212 | options=self.options) 213 | self.request_access_token(authorization, self.access_token_url) 214 | 215 | def refresh(self): 216 | """Refresh an access token.""" 217 | if self.wskey is None: 218 | raise InvalidObject('AccessToken must have an associated WSKey Property') 219 | 220 | self.grant_type = 'refresh_token' 221 | self.access_token_url = self.get_access_token_url() 222 | authorization = self.wskey.get_hmac_signature(method='POST', request_url=self.access_token_url) 223 | self.request_access_token(authorization, self.access_token_url) 224 | 225 | def request_access_token(self, authorization, url): 226 | """ Request an access token. """ 227 | request = six.moves.urllib.request.Request( 228 | url=url, 229 | headers={'Authorization': authorization, 230 | 'Accept': 'application/json'}, 231 | data={} 232 | ) 233 | 234 | opener = six.moves.urllib.request.build_opener() 235 | 236 | try: 237 | result = opener.open(request) 238 | self.parse_token_response(result.read()) 239 | except six.moves.urllib.error.HTTPError as e: 240 | self.parse_error_response(e) 241 | 242 | def get_access_token_url(self): 243 | """ get Access Token URL """ 244 | access_token_url = self.authorization_server + '/accessToken?grant_type=' + self.grant_type 245 | if self.grant_type == 'refresh_token': 246 | access_token_url += '&refresh_token=' + self.refresh_token.refresh_token 247 | elif self.grant_type == 'authorization_code': 248 | access_token_url += ( 249 | '&' + 'code=' + self.code + 250 | '&' + 'authenticatingInstitutionId=' + self.authenticating_institution_id + 251 | '&' + 'contextInstitutionId=' + self.context_institution_id + 252 | '&' + six.moves.urllib.parse.urlencode({'redirect_uri': self.redirect_uri})) 253 | elif self.grant_type == 'client_credentials': 254 | access_token_url += ( 255 | '&authenticatingInstitutionId=' + self.authenticating_institution_id + 256 | '&contextInstitutionId=' + self.context_institution_id + 257 | '&scope=' + '%20'.join(self.scope)) 258 | else: 259 | access_token_url = '' 260 | 261 | return access_token_url 262 | 263 | def parse_token_response(self, response_string): 264 | """ 265 | Parse the url string which consists of the redirect_uri followed by 266 | the access token parameters. 267 | """ 268 | try: 269 | response_json = json.loads(response_string) 270 | except ValueError: 271 | print("ValueError: Unable to decode this Access Token response string to JSON:") 272 | print(response_string) 273 | return 274 | 275 | self.access_token_string = response_json.get('access_token', None) 276 | self.type = response_json.get('token_type', None) 277 | self.expires_at = response_json.get('expires_at', None) 278 | self.expires_in = response_json.get('expires_in', None) 279 | self.context_institution_id = response_json.get('context_institution_id', None) 280 | self.error_code = response_json.get('error_code', None) 281 | 282 | principal_id = response_json.get('principalID', None) 283 | principal_idns = response_json.get('principalIDNS', None) 284 | 285 | if principal_id is not None and principal_idns is not None and not principal_id == '' and not principal_idns == '': 286 | self.user = User( 287 | authenticating_institution_id=self.authenticating_institution_id, 288 | principal_id=principal_id, 289 | principal_idns=principal_idns 290 | ) 291 | 292 | refresh_token = response_json.get('refresh_token', None) 293 | 294 | if refresh_token is not None: 295 | self.refresh_token = RefreshToken( 296 | tokenValue=refresh_token, 297 | expires_in=response_json.get('refresh_token_expires_in', None), 298 | expires_at=response_json.get('refresh_token_expires_at', None) 299 | ) 300 | 301 | def parse_error_response(self, http_error): 302 | try: 303 | error_json = json.loads(http_error.read()) 304 | self.error_code = http_error.getcode() 305 | self.error_message = error_json['message'] 306 | self.error_detail = error_json['details'] 307 | self.error_url = http_error.geturl() 308 | except ValueError: 309 | print("ValueError: Unable to decode this Access Token response string to JSON:") 310 | print(response_string) 311 | return 312 | 313 | def __str__(self): 314 | 315 | return string.Template(""" 316 | access_token_url: $access_token_url 317 | 318 | access_token_string $access_token_string 319 | authenticating_institution_id: $authenticating_institution_id 320 | authorization_server: $authorization_server 321 | code: $code 322 | context_institution_id: $context_institution_id 323 | error_code: $error_code 324 | error_message: $error_message 325 | error_url: $error_url 326 | expires_at: $expires_at 327 | expires_in: $expires_in 328 | grant_type: $grant_type 329 | options: $options 330 | redirect_uri: $redirect_uri 331 | refresh_token: 332 | $refresh_token 333 | scope: $scope 334 | type: $type 335 | user: 336 | $user 337 | wskey: 338 | $wskey""").substitute({ 339 | 'access_token_url': self.access_token_url. 340 | replace('?', '?\n' + ' ' * 18). 341 | replace('&', '\n' + ' ' * 18 + '&'), 342 | 'access_token_string':self.access_token_string, 343 | 'authenticating_institution_id': self.authenticating_institution_id, 344 | 'authorization_server': self.authorization_server, 345 | 'code': self.code, 346 | 'context_institution_id': self.context_institution_id, 347 | 'error_code': self.error_code, 348 | 'error_message': self.error_message, 349 | 'error_url': self.error_url, 350 | 'expires_at': self.expires_at, 351 | 'expires_in': self.expires_in, 352 | 'grant_type': self.grant_type, 353 | 'options': self.options, 354 | 'redirect_uri': self.redirect_uri, 355 | 'refresh_token': self.refresh_token, 356 | 'scope': self.scope, 357 | 'type': self.type, 358 | 'user': self.user, 359 | 'wskey': self.wskey 360 | }) 361 | -------------------------------------------------------------------------------- /authliboclc/authcode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ############################################################################### 4 | # Copyright 2014 OCLC 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You may obtain a copy of 8 | # the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ############################################################################### 18 | 19 | """ 20 | Class represents and authentication code object 21 | 22 | HMAC Requests, which are strictly server side, use an Authenication Code object to store their parameters 23 | and perform hashing. 24 | """ 25 | 26 | import string 27 | 28 | import six 29 | 30 | 31 | class InvalidParameter(Exception): 32 | """Custom exception - invalid parameter was passed to class""" 33 | 34 | def __init__(self, message): 35 | self.message = message 36 | 37 | 38 | """Class begins here""" 39 | 40 | 41 | class AuthCode(object): 42 | """Class represents an authentication code object. 43 | 44 | Organizes the parameters and produces a request url so that an authentication code can be obtained 45 | from OCLC's servers. 46 | 47 | Class Variables: 48 | authorization_server string the oclc server that conducts authentication 49 | client_id string the public portion of the Web Services Key (WSKey) 50 | authenticating_institution_id string the institutionID that is authenticated against 51 | context_institution_id string the institutionID that the request is made against 52 | redirect_uri string the redirect_uri for the request 53 | scopes list a list of one or more web services 54 | """ 55 | authorization_server = None 56 | client_id = None 57 | authenticating_institution_id = None 58 | context_institution_id = None 59 | redirect_uri = None 60 | scopes = None 61 | 62 | def __init__(self, 63 | authorization_server, 64 | client_id=None, 65 | authenticating_institution_id=None, 66 | context_institution_id=None, 67 | redirect_uri=None, 68 | scopes=None): 69 | """Constructor. 70 | 71 | Args: 72 | authorization_server: string, url of the authorization server 73 | client_id: string, the public portion of the Web Services Key (WSKey) 74 | authenticating_institution_id: string, the institutionID that is authenticated against 75 | context_institution_id: string, the institutionID that the request is made against 76 | redirect_uri: string, the redirect_uri for the request 77 | scopes: list, a list of one or more web services 78 | """ 79 | 80 | self.authorization_server = authorization_server 81 | self.client_id = client_id 82 | self.authenticating_institution_id = authenticating_institution_id 83 | self.context_institution_id = context_institution_id 84 | self.redirect_uri = redirect_uri 85 | self.scopes = scopes 86 | 87 | if self.client_id is None: 88 | raise InvalidParameter('Required option missing: client_id.') 89 | elif self.client_id == '': 90 | raise InvalidParameter('Cannot be empty string: client_id.') 91 | 92 | if self.authenticating_institution_id is None: 93 | raise InvalidParameter('Required option missing: authenticating_institution_id.') 94 | elif self.authenticating_institution_id == '': 95 | raise InvalidParameter('Cannot be empty string: authenticating_institution_id.') 96 | 97 | if self.context_institution_id is None: 98 | raise InvalidParameter('Required option missing: context_institution_id.') 99 | elif self.context_institution_id == '': 100 | raise InvalidParameter('Cannot be empty string: context_institution_id.') 101 | 102 | if self.redirect_uri is None: 103 | raise InvalidParameter('Required option missing: redirect_uri.') 104 | elif self.redirect_uri == '': 105 | raise InvalidParameter('Cannot be empty string: redirect_uri.') 106 | else: 107 | scheme = six.moves.urllib.parse.urlparse("".join(self.redirect_uri)).scheme 108 | if not scheme == 'http' and not scheme == 'https': 109 | raise InvalidParameter('Invalid redirect_uri. Must begin with http:// or https://') 110 | 111 | if self.scopes is None or self.scopes == '': 112 | raise InvalidParameter( 113 | 'Required option missing: scopes. Note scopes must be a list of one or more scopes.') 114 | elif not self.scopes or not self.scopes[0]: 115 | raise InvalidParameter('You must pass at least one valid scope') 116 | 117 | def get_login_url(self): 118 | """Returns a login url based on the auth code parameters.""" 119 | return ( 120 | self.authorization_server + '/authorizeCode' + 121 | '?' + 'authenticatingInstitutionId=' + self.authenticating_institution_id + 122 | '&' + 'client_id=' + self.client_id + 123 | '&' + 'contextInstitutionId=' + self.context_institution_id + 124 | '&' + six.moves.urllib.parse.urlencode({'redirect_uri': self.redirect_uri}) + 125 | '&' + 'response_type=code' + 126 | '&' + 'scope=' + " ".join(self.scopes) 127 | ) 128 | 129 | def __str__(self): 130 | 131 | return string.Template("""authorization_server: $authorization_server 132 | client_id: $client_id 133 | authenticating_institution_id: $authenticating_institution_id 134 | context_institution_id: $context_institution_id 135 | redirect_uri: $redirect_uri 136 | scopes: $scopes 137 | """).substitute({ 138 | 'authorization_server': self.authorization_server, 139 | 'client_id': self.client_id, 140 | 'authenticating_institution_id': self.authenticating_institution_id, 141 | 'context_institution_id': self.context_institution_id, 142 | 'redirect_uri': self.redirect_uri, 143 | 'scopes': self.scopes} 144 | ) 145 | 146 | -------------------------------------------------------------------------------- /authliboclc/refreshtoken.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ############################################################################### 4 | # Copyright 2014 OCLC 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You may obtain a copy of 8 | # the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ############################################################################### 18 | 19 | """This class represents a refresh token object. 20 | 21 | A refresh token can be returned with an Authentication Token and used to request another token if the authentication 22 | token is expiring. Refresh tokens are only returned with Authentication Tokens if the services list includes 23 | 'refresh_token'. 24 | 25 | """ 26 | 27 | import time 28 | import string 29 | 30 | 31 | class InvalidParameter(Exception): 32 | """Custom exception - invalid parameter was passed to class""" 33 | 34 | def __init__(self, message): 35 | self.message = message 36 | 37 | 38 | class RefreshToken(object): 39 | """Class represents a refresh token 40 | 41 | Class Variables: 42 | refresh_token string the refresh token string value 43 | expires_at string the ISO 8601 time that the refresh token expires at 44 | expires_in int the number of seconds until the token expires 45 | """ 46 | refresh_token = None 47 | expires_in = None 48 | expires_at = None 49 | 50 | def __init__(self, tokenValue=None, expires_in=None, expires_at=None): 51 | """Constructor. 52 | 53 | Args: 54 | tokenValue: string, the refresh token string value 55 | expires_at: string, the ISO 8601 time that the refresh token expires at 56 | expires_in: int, the number of seconds until the token expires 57 | """ 58 | if tokenValue is None or expires_in is None or expires_at is None: 59 | raise InvalidParameter('You must pass these parameters: tokenValue, expires_in and expires_at') 60 | 61 | if not isinstance(expires_in, int): 62 | raise InvalidParameter('expires_in must be an int') 63 | 64 | self.refresh_token = tokenValue 65 | self.expires_in = expires_in 66 | self.expires_at = expires_at 67 | 68 | def is_expired(self): 69 | """ Test if the refresh token is expired 70 | 71 | Returns: 72 | isExpired: boolean, true if refresh token is expired 73 | """ 74 | status = False 75 | if time.mktime(time.strptime(self.expires_at, "%Y-%m-%d %H:%M:%SZ")) < time.time(): 76 | status = True 77 | return status 78 | 79 | def __str__(self): 80 | 81 | return string.Template("""refresh_token: $refresh_token 82 | expires_in: $expires_in 83 | expires_at: $expires_at 84 | """).substitute({ 85 | 'refresh_token': self.refresh_token, 86 | 'expires_in': self.expires_in, 87 | 'expires_at': self.expires_at 88 | }) 89 | -------------------------------------------------------------------------------- /authliboclc/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ############################################################################### 4 | # Copyright 2014 OCLC 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You may obtain a copy of 8 | # the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ############################################################################### 18 | 19 | """Class the represents an OCLC User 20 | 21 | A user is authenticated against an institution and a particular set of services. Access is gained with a 22 | principal identifier and principal identifier namespace, along with the authenticating institution's ID. 23 | 24 | """ 25 | 26 | import string 27 | 28 | 29 | class InvalidParameter(Exception): 30 | """Custom exception - invalid parameter was passed to class""" 31 | 32 | def __init__(self, message): 33 | self.message = message 34 | 35 | 36 | class User(object): 37 | """Class represents a user. 38 | 39 | Class variables: 40 | 41 | principal_id string the principal identifier 42 | principal_idns string the principal identifier namespace 43 | authenticating_institution_id string the institutionID that the user is authenticating against 44 | """ 45 | principal_id = None 46 | principal_idns = None 47 | authenticating_institution_id = None 48 | 49 | def __init__(self, authenticating_institution_id=None, principal_id=None, principal_idns=None): 50 | """Constructor. 51 | 52 | Args: 53 | authenticating_institution_id: string, the institutionID that the user is authenticating against 54 | principal_id: string, the principal identifier 55 | principal_idns: string, the principal identifier namespace 56 | """ 57 | if not authenticating_institution_id: 58 | raise InvalidParameter('You must set a valid Authenticating Institution ID') 59 | if not principal_id: 60 | raise InvalidParameter('You must set a valid principal_id') 61 | if not principal_idns: 62 | raise InvalidParameter('You must set a valid principal_idns') 63 | 64 | self.authenticating_institution_id = authenticating_institution_id 65 | self.principal_id = principal_id 66 | self.principal_idns = principal_idns 67 | 68 | def __str__(self): 69 | 70 | return string.Template("""principal_id: $principal_id 71 | principal_idns: $principal_idns 72 | authenticating_institution_id: $authenticating_institution_id 73 | """).substitute({ 74 | 'principal_id': self.principal_id, 75 | 'principal_idns': self.principal_idns, 76 | 'authenticating_institution_id': self.authenticating_institution_id, 77 | }) 78 | -------------------------------------------------------------------------------- /authliboclc/wskey.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ############################################################################### 4 | # Copyright 2014 OCLC 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 7 | # use this file except in compliance with the License. You may obtain a copy of 8 | # the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | ############################################################################### 18 | 19 | """Class that represents a Web Services Key (WSKey) 20 | 21 | Stores the WSKey parameters and methods for HMAC Hashing and requesting access tokens. 22 | 23 | """ 24 | 25 | import copy 26 | import base64 27 | import hashlib 28 | import hmac 29 | import math 30 | import random 31 | import string 32 | import time 33 | 34 | from .authcode import AuthCode 35 | from .accesstoken import AccessToken 36 | 37 | import six 38 | 39 | AUTHORIZATION_SERVER = 'https://authn.sd00.worldcat.org/oauth2' 40 | SIGNATURE_URL = 'https://www.oclc.org/wskey' 41 | 42 | 43 | class InvalidObject(Exception): 44 | """Custom exception - invalid object was passed to class""" 45 | 46 | def __init__(self, message): 47 | self.message = message 48 | 49 | 50 | class InvalidParameter(Exception): 51 | """Custom exception - invalid parameter was passed to class""" 52 | 53 | def __init__(self, message): 54 | self.message = message 55 | 56 | 57 | class Wskey(object): 58 | """Web Services Key object 59 | 60 | Class variables 61 | authorization_server string url of the authorization server 62 | valid_options dict list of valid options that can be passed to this class 63 | key string the clientID (public) portion of the WSKey 64 | secret string the secret (private) portion of the WSKey 65 | redirect_uri string the url of the web app, for example https://localhost:8000/auth/ 66 | services list the web services associated with the WSKey, for example ['WorldCatMetadataAPI'] 67 | debug_time_stamp string if not None, then overrides the calculated timestamp. Used for unit tests. 68 | debug_nonce string if not None, then overrides the calculated nonce. Used for unit tests. 69 | body_hash string set to None - current implementation of OCLC's OAuth2 does not use body hashing 70 | auth_params dict custom list of authentication parameters - used for some specialized APIs 71 | user object a user object associated with the key. See user.py in the authliboclc library folder. 72 | """ 73 | 74 | authorization_server = AUTHORIZATION_SERVER 75 | valid_options = ['redirect_uri', 'services'] 76 | key = None 77 | secret = None 78 | redirect_uri = None 79 | services = None 80 | debug_time_stamp = None 81 | debug_nonce = None 82 | body_hash = None 83 | auth_params = None 84 | user = None 85 | 86 | def __init__(self, key, secret, options=None): 87 | """Constructor. 88 | 89 | Args: 90 | key: string, the clientID (public) portion of the WSKey 91 | secret: string, the secret (private) portion of the WSKey 92 | options: dict 93 | - redirect_uri: string, the url that the client authenticates from 94 | ie, https://localhost:8000/auth/ 95 | - services: list, the services associated with the key 96 | ie, ['WMS_ACQ','WorldCatMetadataAPI'] 97 | * note that including 'refresh_token' as a service causes access token requests 98 | to return a refresh token with the access token. 99 | """ 100 | if key is None or secret is None: 101 | raise InvalidObject('A valid key and secret are required to construct a WSKey.') 102 | elif options == '': 103 | raise InvalidObject('Options must be sent as a dictionary object.') 104 | 105 | self.key = key 106 | self.secret = secret 107 | 108 | """If options are included, they must include a redirect_uri and one or more services.""" 109 | if options: 110 | if 'redirect_uri' in options: 111 | if options['redirect_uri'] is None: 112 | raise InvalidParameter('redirect_uri must contain a value.') 113 | else: 114 | scheme = six.moves.urllib.parse.urlparse(options['redirect_uri']).scheme 115 | if not scheme == 'http' and not scheme == 'https': 116 | raise InvalidParameter('Invalid redirect_uri. Must begin with http:// or https://') 117 | 118 | if 'services' not in options: 119 | raise InvalidParameter('Missing service option.') 120 | elif not options['services']: 121 | raise InvalidParameter('A list containing at least one service is required.') 122 | 123 | for key, value in six.iteritems(options): 124 | if key in Wskey.valid_options: 125 | setattr(self, key, value) 126 | 127 | def get_login_url(self, authenticating_institution_id=None, context_institution_id=None): 128 | """Creates a login url. 129 | 130 | Args: 131 | authenticating_institution_id: string, the institution which the user authenticates against 132 | context_institution_id: string, the institution which the user will make requests against 133 | 134 | Returns: 135 | string, the login URL to be used to authenticate the user 136 | """ 137 | if authenticating_institution_id is None: 138 | raise InvalidParameter('You must pass an authenticating institution ID') 139 | if context_institution_id is None: 140 | raise InvalidParameter('You must pass a context institution ID') 141 | 142 | authCode = AuthCode( 143 | authorization_server=self.authorization_server, 144 | client_id=self.key, 145 | authenticating_institution_id=authenticating_institution_id, 146 | context_institution_id=context_institution_id, 147 | redirect_uri=self.redirect_uri, 148 | scopes=self.services 149 | ) 150 | 151 | return authCode.get_login_url() 152 | 153 | def get_access_token_with_auth_code(self, code=None, authenticating_institution_id=None, 154 | context_institution_id=None): 155 | """Retrieves an Access Token using an Authentication Code 156 | 157 | Args: 158 | code: string, the authentication code returned after the user authenticates 159 | authenticating_institution_id: string, the institution the user authenticates against 160 | context_institution_id: string, the institution that the requests will be made against 161 | 162 | Returns: 163 | object, an access token 164 | """ 165 | 166 | if not code: 167 | raise InvalidParameter('You must pass a code') 168 | if not authenticating_institution_id: 169 | raise InvalidParameter('You must pass an authenticating_institution_id') 170 | if not context_institution_id: 171 | raise InvalidParameter('You must pass a context_institution_id') 172 | 173 | accessToken = AccessToken( 174 | authorization_server=self.authorization_server, 175 | grant_type='authorization_code', 176 | options={ 177 | 'code': code, 178 | 'authenticating_institution_id': authenticating_institution_id, 179 | 'context_institution_id': context_institution_id, 180 | 'redirect_uri': self.redirect_uri 181 | } 182 | ) 183 | 184 | accessToken.create(wskey=self, user=None) 185 | 186 | return accessToken 187 | 188 | def get_access_token_with_client_credentials(self, authenticating_institution_id=None, context_institution_id=None, user=None): 189 | """Retrieves an Access Token using a Client Credentials Grant 190 | 191 | Args: 192 | authenticating_institution_id: string, the institution the user authenticates against 193 | context_institution_id: string, the institution that the requests will be made against 194 | user: object, a user object associated with the key. See user.py in the authliboclc library folder. 195 | 196 | Returns: 197 | object, an access token 198 | """ 199 | 200 | if not authenticating_institution_id: 201 | raise InvalidParameter('You must pass an authenticating_institution_id') 202 | if not context_institution_id: 203 | raise InvalidParameter('You must pass a context_institution_id') 204 | if not self.services or self.services == ['']: 205 | raise InvalidParameter('You must set at least one service on the Wskey') 206 | 207 | accessToken = AccessToken( 208 | authorization_server=self.authorization_server, 209 | grant_type='client_credentials', 210 | options={ 211 | 'authenticating_institution_id': authenticating_institution_id, 212 | 'context_institution_id': context_institution_id, 213 | 'scope': self.services 214 | } 215 | ) 216 | 217 | accessToken.create(wskey=self, user=user) 218 | 219 | return accessToken 220 | 221 | def get_hmac_signature(self, method=None, request_url=None, options=None): 222 | """Signs a url with an HMAC signature and builds an Authorization header 223 | 224 | Args: 225 | method: string, GET, POST, PUT, DELETE, etc. 226 | request_url: string, the url to be signed 227 | options: dict 228 | - user: object, a user object 229 | - auth_params: dict, various key value pairs to be added to the authorization header. For example, 230 | userid and password. Depends on the API and its specialized needs. 231 | 232 | Returns: 233 | authorization_header: string, the Authorization header to be added to the request. 234 | """ 235 | 236 | if not self.secret: 237 | raise InvalidParameter('You must construct a WSKey with a secret to build an HMAC Signature.') 238 | if not method: 239 | raise InvalidParameter('You must pass an HTTP Method to build an HMAC Signature.') 240 | if not request_url: 241 | raise InvalidParameter('You must pass a valid request URL to build an HMAC Signature.') 242 | 243 | if options is not None: 244 | for key, value in six.iteritems(options): 245 | setattr(self, key, value) 246 | 247 | timestamp = self.debug_time_stamp 248 | if not timestamp: 249 | timestamp = str(int(time.time())) 250 | 251 | nonce = self.debug_nonce 252 | if not nonce: 253 | nonce = str(hex(int(math.floor(random.random() * 4026531839 + 268435456)))) 254 | 255 | signature = self.sign_request( 256 | method=method, 257 | request_url=request_url, 258 | timestamp=timestamp, 259 | nonce=nonce 260 | ) 261 | 262 | q = '"' 263 | qc = '",' 264 | 265 | authorization_header = ("http://www.worldcat.org/wskey/v2/hmac/v1 " + 266 | "clientID=" + q + self.key + qc + 267 | "timestamp=" + q + timestamp + qc + 268 | "nonce=" + q + nonce + qc + 269 | "signature=" + q + signature) 270 | 271 | if self.user is not None or self.auth_params is not None: 272 | authorization_header += (qc + self.add_auth_params(self.user, self.auth_params)) 273 | else: 274 | authorization_header += q 275 | 276 | return authorization_header 277 | 278 | def sign_request(self, method, request_url, timestamp, nonce): 279 | """Requests a normalized request and hashes it 280 | 281 | Args: 282 | method: string, GET, POST, etc. 283 | request_url: string, the URL to be hashed 284 | timestamp: string, POSIX time 285 | nonce: string, a random 32 bit integer expressed in hexadecimal format 286 | 287 | Returns: 288 | A base 64 encoded SHA 256 HMAC hash 289 | """ 290 | normalized_request = self.normalize_request( 291 | method=method, 292 | request_url=request_url, 293 | timestamp=timestamp, 294 | nonce=nonce 295 | ) 296 | 297 | digest = hmac.new(self.secret.encode('utf-8'), 298 | msg=normalized_request.encode('utf-8'), 299 | digestmod=hashlib.sha256).digest() 300 | return str(base64.b64encode(digest).decode()) 301 | 302 | def normalize_request(self, method, request_url, timestamp, nonce): 303 | """Prepares a normalized request for hashing 304 | 305 | Args: 306 | method: string, GET, POST, etc. 307 | request_url: string, the URL to be hashed 308 | timestamp: string, POSIX time 309 | nonce: string, a random 32 bit integer expressed in hexadecimal format 310 | 311 | Returns: 312 | normalized_request: string, the normalized request to be hashed 313 | """ 314 | signature_url = SIGNATURE_URL 315 | parsed_signature_url = six.moves.urllib.parse.urlparse(six.moves.urllib.parse.unquote(signature_url)) 316 | parsed_request_url = six.moves.urllib.parse.urlparse(six.moves.urllib.parse.unquote(request_url)) 317 | 318 | host = str(parsed_signature_url.netloc) 319 | 320 | if parsed_signature_url.port is not None: 321 | port = str(parsed_signature_url.port) 322 | else: 323 | if str(parsed_signature_url.scheme) == 'http': 324 | port = '80' 325 | elif str(parsed_signature_url.scheme) == 'https': 326 | port = '443' 327 | 328 | path = str(parsed_signature_url.path) 329 | 330 | """ OCLC's OAuth implementation does not currently use body hashing, so this should always be ''.""" 331 | body_hash = '' 332 | if self.body_hash is not None: 333 | body_hash = self.body_hash 334 | 335 | """The base normalized request.""" 336 | normalized_request = (self.key + '\n' + 337 | timestamp + '\n' + 338 | nonce + '\n' + 339 | body_hash + '\n' + 340 | method + '\n' + 341 | host + '\n' + 342 | port + '\n' + 343 | path + '\n') 344 | 345 | """Add the request parameters to the normalized request.""" 346 | parameters = {} 347 | if parsed_request_url.query: 348 | for param in parsed_request_url.query.split('&'): 349 | key = (param.split('='))[0] 350 | value = (param.split('='))[1] 351 | parameters[key] = value 352 | 353 | """URL encode normalized request per OAuth 2 Official Specification.""" 354 | for key in sorted(parameters): 355 | nameAndValue = six.moves.urllib.parse.urlencode({key: parameters[key]}) 356 | nameAndValue = nameAndValue.replace('+', '%20') 357 | nameAndValue = nameAndValue.replace('*', '%2A') 358 | nameAndValue = nameAndValue.replace('%7E', '~') 359 | normalized_request += nameAndValue + '\n' 360 | 361 | return normalized_request 362 | 363 | def add_auth_params(self, user, auth_params): 364 | """Adds users custom authentication parameters, if any, to the Normalized request 365 | 366 | Args: 367 | user: object, a user object 368 | auth_params: dict, a list of parameters to add the custom parameters to 369 | 370 | Returns: 371 | authValuePairs: dict, the auth_params with any custom parameters added to them 372 | """ 373 | authValuePairs = '' 374 | combinedParams = copy.copy(auth_params) 375 | 376 | if not combinedParams: 377 | combinedParams = {} 378 | 379 | if user is not None: 380 | combinedParams['principalID'] = user.principal_id 381 | combinedParams['principalIDNS'] = user.principal_idns 382 | 383 | counter = 0 384 | 385 | for key in sorted(combinedParams): 386 | 387 | authValuePairs += key + '=' + '"' + combinedParams[key] 388 | counter += 1 389 | if counter == len(combinedParams): 390 | authValuePairs += '"' 391 | else: 392 | authValuePairs += '",' 393 | 394 | return authValuePairs 395 | 396 | def __str__(self): 397 | 398 | return string.Template("""key: $key 399 | secret: $secret 400 | redirect_uri: $redirect_uri 401 | services: $services 402 | debug_time_stamp: $debug_time_stamp 403 | debug_nonce: $debug_nonce 404 | body_hash: $body_hash 405 | auth_params: $auth_params 406 | user: 407 | $user""").substitute({ 408 | 'key': self.key, 409 | 'secret': self.secret, 410 | 'redirect_uri': self.redirect_uri, 411 | 'services': self.services, 412 | 'debug_time_stamp': self.debug_time_stamp, 413 | 'debug_nonce': self.debug_nonce, 414 | 'body_hash': self.body_hash, 415 | 'auth_params': self.auth_params, 416 | 'user': self.user 417 | }) -------------------------------------------------------------------------------- /examples/authentication_token/README.md: -------------------------------------------------------------------------------- 1 | ###User Authentication and Access Token Example 2 | 3 | This example demonstrates how to retrieve an access token, and has the following features: 4 | * Provides a basic HTTPS server 5 | * Redirects a user to authenticate to retrieve an Access Code 6 | * Uses the Access Code to retrieve an Access Token 7 | * Stores the Access Token in a Session and manages a list of sessions using a simple flat file. 8 | * Uses the Access Token to request a Bibliographic Record from OCLC. 9 | 10 | Get the repo and install the library: 11 | 12 | 1. Clone the repository: 13 | 14 | `git clone https://github.com/OCLC-Developer-Network/oclc-auth-python.git` 15 | 16 | 1. Install the library: 17 | 18 | `sudo python setup.py install` 19 | 20 | To use the example: 21 | 22 | 1. Change directories to `examples/authentication_token` 23 | 24 | 1. Edit server.py to insert your WSKey parameters: 25 |
26 |    KEY = '{clientID}'
27 |    SECRET = '{secret}'
28 |    
29 | 30 | 1. From the command line: 31 | 32 | `python server.py` 33 | 34 | 1. Navigate your browser to: 35 | 36 | `https://localhost:8000/auth/` 37 | 38 | Do not be concerned about "security warnings" - click through them. That is expected with the supplied, unsigned 39 | CACERT in server.pem. In production, you will use your institution's signed CACERT when implementing SSL. 40 | 41 | Note that this example writes sessions to a flat file, sessions.p. So it will need read/write access to the 42 | authentication_token directory. In practice, you would implement sessions using your own database. -------------------------------------------------------------------------------- /examples/authentication_token/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCLC-Developer-Network/oclc-auth-python/d4c56c604b9db280fc1c502e688a7fa9260ca9ec/examples/authentication_token/__init__.py -------------------------------------------------------------------------------- /examples/authentication_token/access_token_formatter.py: -------------------------------------------------------------------------------- 1 | from cStringIO import StringIO 2 | 3 | 4 | class AccessTokenFormatter(): 5 | _access_token = None 6 | 7 | def __init__(self, access_token): 8 | self._access_token = access_token 9 | 10 | def format(self): 11 | """Display all the parameters of the Access Token""" 12 | ret = StringIO() 13 | ret.write('

Access Token

') 14 | 15 | ret.write('') 16 | 17 | if self._access_token.error_code is not None: 18 | ret.write('') 21 | ret.write('') 24 | ret.write('') 27 | 28 | else: 29 | 30 | ret.write('') 33 | ret.write('') 36 | ret.write('') 39 | ret.write('') 42 | 43 | if self._access_token.user is not None: 44 | ret.write('') 47 | ret.write('') 50 | ret.write('') 53 | 54 | if self._access_token.refresh_token is not None: 55 | ret.write('') 58 | ret.write('') 61 | ret.write('') 64 | 65 | ret.write('
Error Code') 19 | ret.write(str(self._access_token.error_code)) 20 | ret.write('
Error Message') 22 | ret.write(str(self._access_token.error_message)) 23 | ret.write('
Error Url
')
25 |             ret.write(str(self._access_token.error_url).replace('?', '?\n').replace('&', '\n&'))
26 |             ret.write('
access_token') 31 | ret.write(str(self._access_token.access_token_string)) 32 | ret.write('
token_type') 34 | ret.write(str(self._access_token.type)) 35 | ret.write('
expires_at') 37 | ret.write(str(self._access_token.expires_at)) 38 | ret.write('
expires_in') 40 | ret.write(str(self._access_token.expires_in)) 41 | ret.write('
principalID') 45 | ret.write(str(self._access_token.user.principal_id)) 46 | ret.write('
principalIDNS') 48 | ret.write(str(self._access_token.user.principal_idns)) 49 | ret.write('
contextInstitutionId') 51 | ret.write(str(self._access_token.context_institution_id)) 52 | ret.write('
refresh_token') 56 | ret.write(str(self._access_token.refresh_token.refresh_token)) 57 | ret.write('
refresh_token_expires_at') 59 | ret.write(str(self._access_token.refresh_token.expires_at)) 60 | ret.write('
refresh_token_expires_in') 62 | ret.write(str(self._access_token.refresh_token.expires_in)) 63 | ret.write('
') 66 | return ret.getvalue() -------------------------------------------------------------------------------- /examples/authentication_token/bibliographic_record.py: -------------------------------------------------------------------------------- 1 | import urllib2 2 | 3 | 4 | class BibRecord(): 5 | _access_token = None 6 | _wskey = None 7 | 8 | def __init__(self, access_token, wskey): 9 | self._access_token = access_token 10 | self._wskey = wskey 11 | 12 | def read(self): 13 | """Use an Access Token's User Parameter to request a Bibliographic Record""" 14 | request_url = ( 15 | 'https://worldcat.org/bib/data/823520553?' + 16 | 'classificationScheme=LibraryOfCongress' + 17 | '&holdingLibraryCode=MAIN' 18 | ) 19 | 20 | authorization_header = self._wskey.get_hmac_signature( 21 | method='GET', 22 | request_url=request_url, 23 | options={'user': self._access_token.user} 24 | ) 25 | 26 | my_request = urllib2.Request( 27 | url=request_url, 28 | data=None, 29 | headers={'Authorization': authorization_header} 30 | ) 31 | 32 | try: 33 | xml_result = urllib2.urlopen(my_request).read() 34 | 35 | except urllib2.HTTPError, e: 36 | xml_result = str(e) 37 | 38 | return ''.join(['

Bibliographic Record

', xml_result.replace('<', '<'), '
']) -------------------------------------------------------------------------------- /examples/authentication_token/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC81BJpvfNui8ltK6ZRXQ0nEc890eiVQ2v27S4P2uA1C2kNBNRC 3 | XHviCowS2qi1PklzFDLUw/ZOQoyBfls4RuyDgttNxCttsp9IW8lINOys0OS7ipy2 4 | IWgEXu0jwc77Hxc+FM1NSx2OgmB4CoIYUeqfhok6HVH2CmEcq8lUeYIV0QIDAQAB 5 | AoGBALfLfDisiTu6mE7Iw9RCTEERFrVHkalnvLjWV5VbKAy5lID1iF0ng/Wa6oiX 6 | iMsRW5DFwkxSiXXXVMfeY4+9iQs0GSrT/j5FdLm9GJU1w9JObAsRpm46syz9tIlQ 7 | l4cp/raHLPTZUnLULnghxYbArG6tjK0PMXYdCya7WDlsE3BZAkEA6VEPgkBSYTjT 8 | kYg2YfIvh4GEjiVCZ5Oz08r2lt6WA2xN9kcUngwlQsyGOcl44nq6iNCbFR5XCVZV 9 | ynKb35wskwJBAM8vwjKhxuwYiCBPrzHzMXPvCyvxxBCi9kBvLVDR6XK+9incDAIk 10 | WDrgljnPtmPLTQ+Gs/cafEOZdDT8Qt6h1osCQFxXWr4AWxpjdUi6Elv9kFYfKqlf 11 | kcKQsLF4ONRJUDIWoVyBkWVkBTNE4zLnzFJGpKEVfuuC0Iu/gcDYT1zW4MUCQB3F 12 | RglSd6vrJnxGFu19fWikO5234q1lTS8bCo7nar0DNYn0RYF1SXxEUzHBZ/rU9qC5 13 | gViLZLmt7iXC7bTh2lsCQGJEPoPDVXV4TLB2JgDFGYd5vkv++thf/3Y1XUzyZFuC 14 | jLzrVf8kgiJug2+6bIii1xUYXkcfXSC31f7dP6ZSGak= 15 | -----END RSA PRIVATE KEY----- 16 | -----BEGIN CERTIFICATE----- 17 | MIIDkDCCAvmgAwIBAgIJAKP+tx+OninBMA0GCSqGSIb3DQEBBQUAMIGNMQswCQYD 18 | VQQGEwJVUzENMAsGA1UECBMET2hpbzEPMA0GA1UEBxMGRHVibGluMQ0wCwYDVQQK 19 | EwRPQ0xDMRMwEQYDVQQLEwpXb3JsZHNoYXJlMRgwFgYDVQQDEw9HZW9yZ2UgQ2Ft 20 | cGJlbGwxIDAeBgkqhkiG9w0BCQEWEWNhbXBiZWxnQG9jbGMub3JnMB4XDTE0MDQw 21 | MTE1MzIzM1oXDTE1MDQwMTE1MzIzM1owgY0xCzAJBgNVBAYTAlVTMQ0wCwYDVQQI 22 | EwRPaGlvMQ8wDQYDVQQHEwZEdWJsaW4xDTALBgNVBAoTBE9DTEMxEzARBgNVBAsT 23 | Cldvcmxkc2hhcmUxGDAWBgNVBAMTD0dlb3JnZSBDYW1wYmVsbDEgMB4GCSqGSIb3 24 | DQEJARYRY2FtcGJlbGdAb2NsYy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ 25 | AoGBALzUEmm9826LyW0rplFdDScRzz3R6JVDa/btLg/a4DULaQ0E1EJce+IKjBLa 26 | qLU+SXMUMtTD9k5CjIF+WzhG7IOC203EK22yn0hbyUg07KzQ5LuKnLYhaARe7SPB 27 | zvsfFz4UzU1LHY6CYHgKghhR6p+GiTodUfYKYRyryVR5ghXRAgMBAAGjgfUwgfIw 28 | HQYDVR0OBBYEFJUEc67p20CKHngpVLf/+kesQYLsMIHCBgNVHSMEgbowgbeAFJUE 29 | c67p20CKHngpVLf/+kesQYLsoYGTpIGQMIGNMQswCQYDVQQGEwJVUzENMAsGA1UE 30 | CBMET2hpbzEPMA0GA1UEBxMGRHVibGluMQ0wCwYDVQQKEwRPQ0xDMRMwEQYDVQQL 31 | EwpXb3JsZHNoYXJlMRgwFgYDVQQDEw9HZW9yZ2UgQ2FtcGJlbGwxIDAeBgkqhkiG 32 | 9w0BCQEWEWNhbXBiZWxnQG9jbGMub3JnggkAo/63H46eKcEwDAYDVR0TBAUwAwEB 33 | /zANBgkqhkiG9w0BAQUFAAOBgQCT1G4c56oYIBzLRgp9+ZFcBpt2PMCYwfKzfXvL 34 | 2TPFvHAK4awKiQnjp+XTZHEJtcjucJFm3BsizygU3A2hm+sZufhD78aIwjLjmBeP 35 | gDPvdImBmQBVNYImBMRe00Ul6+bo/c+C6fECvV+xSXOLaAuktTk0G4E81xEwbKTj 36 | pmhsew== 37 | -----END CERTIFICATE----- 38 | -------------------------------------------------------------------------------- /examples/authentication_token/server.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | import BaseHTTPServer 18 | import ssl 19 | from SimpleHTTPServer import SimpleHTTPRequestHandler 20 | from authliboclc import wskey 21 | from urlparse import urlparse, parse_qs 22 | from session_handler import SessionHandler 23 | from access_token_formatter import AccessTokenFormatter 24 | from bibliographic_record import BibRecord 25 | 26 | PORT = 8000 27 | 28 | """Authentication parameters.""" 29 | KEY = '{clientID}' 30 | SECRET = '{secret}' 31 | AUTHENTICATING_INSTITUTION_ID = '128807' # default value for Sandbox institution 32 | CONTEXT_INSTITUTION_ID = '128807' # default value for Sandbox institution 33 | SERVICES = ['WorldCatMetadataAPI', 'refresh_token'] 34 | REDIRECT_URI = 'https://localhost:8000/auth/' 35 | 36 | 37 | class Request(SimpleHTTPRequestHandler): 38 | """This is a general purpose request handler. We focus on /auth/ and managing access tokens.""" 39 | 40 | def do_GET(self): 41 | 42 | if KEY == '{clientID}': 43 | """The developer forgot to insert authentication parameters into the example.""" 44 | self.send_response(200) 45 | self.send_header('Content-Type', 'text/html') 46 | self.end_headers() 47 | self.wfile.write('

Please set the authentication parameters in ' + 48 | 'examples/authentication_token/server.py, ' + 49 | 'lines 29 & 30.

') 50 | return 51 | 52 | if (self.path[:6] != '/auth/'): 53 | """Handle other, non authentication requests here. For example, loading the favicon.ico""" 54 | return 55 | 56 | print "\n-- Handling a Request --" 57 | 58 | html = (""" 59 | 60 | 61 | 62 | 63 | 64 | 71 | 72 | 73 |

Authentication Token & Bibliographic Record

74 | """) 75 | 76 | """Populate the WSKey object's parameters""" 77 | my_wskey = wskey.Wskey(key=KEY, secret=SECRET, options={'services': SERVICES, 'redirect_uri': REDIRECT_URI}) 78 | 79 | session_handler = SessionHandler(headers=self.headers) 80 | access_token = session_handler.get_access_token() 81 | 82 | """If there is a code parameter on the current URL, load it.""" 83 | code = None 84 | if (self.path is not None): 85 | params = parse_qs(urlparse(self.path).query) 86 | if 'code' in params: 87 | code = params['code'][0] 88 | 89 | """If there are error parameters on the current URL, load them.""" 90 | error = self.headers.get('error', None) 91 | error_description = self.headers.get('error_description', None) 92 | 93 | if access_token is None and code is None: 94 | """There is no access token and no authentication code. Initiate user authentication.""" 95 | 96 | """Get the user authentication url""" 97 | login_url = my_wskey.get_login_url( 98 | authenticating_institution_id=AUTHENTICATING_INSTITUTION_ID, 99 | context_institution_id=CONTEXT_INSTITUTION_ID 100 | ) 101 | 102 | """Redirect the browser to the login_url""" 103 | self.send_response(303) 104 | self.send_header('Location', login_url) 105 | self.end_headers() 106 | 107 | print "Requiring user to authenticate." 108 | 109 | else: 110 | 111 | if error is not None: 112 | """If an error was returned, display it.""" 113 | html = ''.join([html, '

Error: ', error, '
', error_description, '

']) 114 | 115 | if access_token is None and code is not None: 116 | """Request an access token using the user authentication code returned after the user authenticated""" 117 | """Then request a bibliographic record""" 118 | 119 | print "I now have an authentication code. I will request an access token." 120 | access_token = my_wskey.get_access_token_with_auth_code( 121 | code=code, 122 | authenticating_institution_id=AUTHENTICATING_INSTITUTION_ID, 123 | context_institution_id=CONTEXT_INSTITUTION_ID 124 | ) 125 | 126 | if access_token.error_code is None: 127 | session_handler.save_access_token(access_token) 128 | html = ''.join([html, '

Access Token saved to session database.

']) 129 | 130 | if access_token is not None: 131 | """Display the token and request a Bibliographic Record""" 132 | print "Displaying access token parameters." 133 | if access_token.error_code is None and code is None: 134 | html = ''.join([html, '

Access Token retrieved from session database.

']) 135 | 136 | access_token_formatter = AccessTokenFormatter(access_token=access_token) 137 | html = ''.join([html, access_token_formatter.format()]) 138 | 139 | if access_token.error_code is None: 140 | print "Using Access Token to request a Bibliographic Record." 141 | bib_record = BibRecord(access_token=access_token, wskey=my_wskey) 142 | html = ''.join([html, bib_record.read()]) 143 | 144 | html = ''.join([html, '']) 145 | 146 | self.send_response(200) 147 | self.send_header('Content-Type', 'text/html') 148 | self.wfile.write(''.join([session_handler.cookie_header_output(), '\n'])) 149 | self.end_headers() 150 | self.wfile.write(html) 151 | 152 | return 153 | 154 | 155 | httpd = BaseHTTPServer.HTTPServer(('localhost', PORT), Request) 156 | httpd.socket = ssl.wrap_socket(httpd.socket, certfile='server.pem', server_side=True) 157 | print "\n\n\nStarting https server.\nNavigate your browser to " + \ 158 | REDIRECT_URI + \ 159 | "\nPress Ctrl-C to abort.\n---------------------------------------------------------" 160 | httpd.serve_forever() 161 | -------------------------------------------------------------------------------- /examples/authentication_token/session_handler.py: -------------------------------------------------------------------------------- 1 | import Cookie 2 | import uuid 3 | import pickle 4 | import os.path 5 | from time import time 6 | 7 | 8 | class SessionHandler(): 9 | """ This is a simple flat file session handler. In a production example, you might keep track of sessions 10 | in a mySQL database. 11 | 12 | The sessions are stored in a simple dict object 13 | { 14 | session id : pickled access token object, 15 | session id : pickled access token object, 16 | ... 17 | } 18 | """ 19 | _cookie = None 20 | _headers = None 21 | _session_id = None 22 | _sessions = None 23 | _session = None 24 | _access_token = None 25 | 26 | def __init__(self, headers): 27 | self._headers = headers 28 | self.get_cookie() 29 | self.get_session() 30 | 31 | def get_cookie(self): 32 | """ 33 | Determine if a cookie exists on the client. If so, load it. If not, create one by 34 | creating a new sessionid. 35 | """ 36 | self._cookie = Cookie.SimpleCookie() 37 | print "Looking for a cookie in the header:" 38 | if self._headers.has_key('cookie'): 39 | print " - found one. " 40 | cookie_string = self._headers.get('cookie') 41 | self._cookie.load(cookie_string) 42 | else: 43 | print " - did not find one. Creating a new cookie." 44 | self._cookie['sessionid'] = uuid.uuid4() 45 | 46 | def get_session(self): 47 | """ 48 | If a sessionid exists, retrieve the session information from the sessions.p file. 49 | Otherwise, write the current session to the sessions.p file. 50 | """ 51 | self._session_id = self._cookie['sessionid'].value 52 | print "The session id is " + self._session_id 53 | 54 | """Load the sessions file from disk. If it does not exist, create one""" 55 | self._sessions = pickle.load(open('sessions.p', 'rb')) if os.path.isfile('sessions.p') else dict() 56 | print "Looking to see if the session id is stored in the sessions file:" 57 | if self._session_id in self._sessions: 58 | print " - found it." 59 | self._session = pickle.loads(self._sessions[self._session_id]) 60 | else: 61 | print " - did not find it. Creating a new session" 62 | self._session = {'timestamp': time()} 63 | self._session.update({'id': self._session_id}) 64 | 65 | def get_access_token(self): 66 | print "Looking for an access token in the current session:" 67 | 68 | """If an access_token is stored in the current session load it.""" 69 | if self._session.get('access_token', None) is not None: 70 | print " - found one." 71 | self._access_token = pickle.loads(self._session.get('access_token', None)) 72 | 73 | """If the access_token we loaded is expired, we can't use it.""" 74 | elif self._access_token is not None and self._access_token.is_expired(): 75 | print " - found one, but it was expired." 76 | self._access_token = None 77 | 78 | else: 79 | print " - did not find an access token." 80 | 81 | return self._access_token 82 | 83 | def cookie_header_output(self): 84 | return self._cookie.output() 85 | 86 | def save_access_token(self, access_token): 87 | print "Saving new access token to the session." 88 | self._session['access_token'] = pickle.dumps(access_token) 89 | self._sessions.update({self._session_id: pickle.dumps(self._session)}) 90 | pickle.dump(self._sessions, open('sessions.p', 'wb')) -------------------------------------------------------------------------------- /examples/authentication_token/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCLC-Developer-Network/oclc-auth-python/d4c56c604b9db280fc1c502e688a7fa9260ca9ec/examples/authentication_token/tests/__init__.py -------------------------------------------------------------------------------- /examples/authentication_token/tests/access_token_formatter_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.access_token_formatter_test 18 | 19 | import unittest 20 | from authliboclc import accesstoken, refreshtoken, user 21 | from .. import access_token_formatter 22 | 23 | 24 | class AccessTokenFormatterTests(unittest.TestCase): 25 | def setUp(self): 26 | 27 | """Create a new access token which hasn't been authenticated yet.""" 28 | self._access_token = accesstoken.AccessToken( 29 | authorization_server='https://authn.sd00.worldcat.org/oauth2', 30 | grant_type='authorization_code', 31 | options={'scope': ['WMS_NCIP', 'WMS_ACQ'], 32 | 'authenticating_institution_id': '128807', 33 | 'context_institution_id': '128808', 34 | 'redirect_uri': 'https://localhost:8000/auth/', 35 | 'code': 'unknown' 36 | } 37 | ) 38 | 39 | """Assume authentication has occured and these parameters are now filled in.""" 40 | self._access_token.expires_at = '2014-04-08 13:38:29Z' 41 | self._access_token.expires_in = 1198 42 | self._access_token.access_token_string = 'tk_TBHrsDbSrWW1oS7d3gZr7NJb7PokyOFlf0pr' 43 | self._access_token.type = 'bearer' 44 | self._access_token.refresh_token = refreshtoken.RefreshToken( 45 | tokenValue='rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W', 46 | expires_in=1199, 47 | expires_at='2014-03-13 15:44:59Z' 48 | ) 49 | 50 | self._my_access_token_formatter = access_token_formatter.AccessTokenFormatter( 51 | access_token=self._access_token 52 | ) 53 | 54 | 55 | """Test the display of an access_token without the user properties set.""" 56 | def testAccessTokenFormatter(self): 57 | self.assertEqual(self._my_access_token_formatter.format(), 58 | '

Access Token

' + 59 | '' + 60 | '' + 61 | '' + 62 | '' + 63 | '' + 64 | '' + 65 | '' + 66 | '' + 67 | '
access_tokentk_TBHrsDbSrWW1oS7d3gZr7NJb7PokyOFlf0pr
token_typebearer
expires_at2014-04-08 13:38:29Z
expires_in1198
refresh_tokenrt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W
refresh_token_expires_at2014-03-13 15:44:59Z
refresh_token_expires_in1199
') 68 | 69 | """Test the display of an access_token with the user properties set.""" 70 | def testAccessTokenFormatterWithUser(self): 71 | self._access_token.user = user.User( 72 | authenticating_institution_id='128807', 73 | principal_id='2334ed24-b27e-63bd-8fea-7cw2deq70r8d', 74 | principal_idns='urn:oclc:platform:128807') 75 | 76 | self.assertEqual(self._my_access_token_formatter.format(), 77 | '

Access Token

' + 78 | '' + 79 | '' + 80 | '' + 81 | '' + 82 | '' + 83 | '' + 84 | '' + 85 | '' + 86 | '' + 87 | '' + 88 | '' + 89 | '
access_tokentk_TBHrsDbSrWW1oS7d3gZr7NJb7PokyOFlf0pr
token_typebearer
expires_at2014-04-08 13:38:29Z
expires_in1198
principalID2334ed24-b27e-63bd-8fea-7cw2deq70r8d
principalIDNSurn:oclc:platform:128807
contextInstitutionId128808
refresh_tokenrt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W
refresh_token_expires_at2014-03-13 15:44:59Z
refresh_token_expires_in1199
') 90 | 91 | """Test the display of an access_token without the error properties set.""" 92 | def testAccessTokenFormatterWithError(self): 93 | self._access_token.error_code = '500' 94 | self._access_token.error_message = 'No Reply at All' 95 | self._access_token.error_url = 'http://www.nobody-is-ho.me' 96 | 97 | self.assertEqual(self._my_access_token_formatter.format(), 98 | '

Access Token

' + 99 | '' + 100 | '' + 101 | '' + 102 | '' + 103 | '
Error Code500
Error MessageNo Reply at All
Error Url
http://www.nobody-is-ho.me
') 104 | 105 | def main(): 106 | unittest.main() 107 | 108 | 109 | if __name__ == '__main__': 110 | main() 111 | -------------------------------------------------------------------------------- /examples/authentication_token/tests/bibliographic_record_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.accesstoken_test 18 | 19 | import unittest 20 | 21 | 22 | class BibliographicRecordTests(unittest.TestCase): 23 | def setUp(self): 24 | pass 25 | 26 | 27 | def main(): 28 | unittest.main() 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /examples/authentication_token/tests/server_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.accesstoken_test 18 | 19 | import unittest 20 | 21 | 22 | class ServerTests(unittest.TestCase): 23 | def setUp(self): 24 | pass 25 | 26 | 27 | def main(): 28 | unittest.main() 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /examples/authentication_token/tests/session_handler_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.accesstoken_test 18 | 19 | import unittest 20 | 21 | 22 | class SessionHandlerTests(unittest.TestCase): 23 | def setUp(self): 24 | pass 25 | 26 | 27 | def main(): 28 | unittest.main() 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /examples/authentication_token_with_django/README.md: -------------------------------------------------------------------------------- 1 | ###User Authentication and Access Token Django Example 2 | 3 | For performing client side authentication using Access Tokens, we prepared an example using django. Note that 4 | Access Tokens require Secure Socket Layer be implemented on the host. 5 | 6 | Get the repo and install the library: 7 | 8 | 1. Clone the repository: 9 | 10 | `git clone https://github.com/OCLC-Developer-Network/oclc-auth-python.git` 11 | 12 | 1. Install the library: 13 | 14 | `sudo python setup.py install` 15 | 16 | First, we need to install these dependencies: 17 | 18 | 1. Change directories to `examples/djangoProject`. 19 | 20 | 1. Install `pip` if you have not already - pip. 21 | 22 | 1. Install Django (see Django Installation Guide). 23 | 24 | `sudo pip install django` 25 | 26 | 1. To run SSL from localhost, install a django-sslserver. 27 | 28 | `sudo pip install django-sslserver`
29 | 30 | An alternate method popular with Django developers is to install Stunnel. 31 | 32 | Note: if running stunnel, you should edit `djangoProject/settings.py` and remove the reference to sslserver: 33 |
34 |        INSTALLED_APPS = (
35 |            'django.contrib.admin',
36 |            'django.contrib.auth',
37 |            'django.contrib.contenttypes',
38 |            'django.contrib.sessions',
39 |            'django.contrib.messages',
40 |            'django.contrib.staticfiles',
41 |            'exampleAuthTokenDjangoApp',
42 |            'sslserver', # remove if using Stunnel
43 |        )
44 |    
45 | 46 | 1. Edit `djangoProject/views.py` and insert your Key and Secret. 47 | Note that your WSKey must be configured with these parameters: 48 | * RedirectURI that matches the URI you are running the example from. For example, https://localhost:8000/auth/ 49 | * Scopes. ie, WorldCatMetadataAPI for the Django example provided with this library. 50 | 51 | 1. Use runsslserver to start Django's SSL server from the `examples/authentication_token_with_django` directory: 52 | 53 | `python manage.py runsslserver` 54 | 55 | 1. Direct your browser to `https://localhost:8000/auth/`. 56 | 57 | 1. If all goes well, you should see some authentication warnings (that's expected - because runsslserver uses a self-signed CACERT). Click through the warning messages and you should see an authentication screen. 58 | 59 | * Sign in with your userId and Password 60 | * When prompted to allow access, click yes 61 | 62 |
63 | You should see your access token details and a sample Bibliographic record, in XML format. 64 | -------------------------------------------------------------------------------- /examples/authentication_token_with_django/authTokenApp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCLC-Developer-Network/oclc-auth-python/d4c56c604b9db280fc1c502e688a7fa9260ca9ec/examples/authentication_token_with_django/authTokenApp/__init__.py -------------------------------------------------------------------------------- /examples/authentication_token_with_django/authTokenApp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /examples/authentication_token_with_django/authTokenApp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /examples/authentication_token_with_django/authTokenApp/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examples/authentication_token_with_django/authTokenApp/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | from authTokenApp import views 3 | 4 | urlpatterns = patterns('', 5 | url(r'^$', views.index, name='index')) -------------------------------------------------------------------------------- /examples/authentication_token_with_django/authTokenApp/views.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | from django.shortcuts import render 18 | 19 | # Create your views here. 20 | 21 | from django.http import HttpResponse 22 | from authliboclc import wskey 23 | import urllib2 24 | import pickle 25 | 26 | """Django view generator""" 27 | 28 | 29 | def index(request): 30 | """You must fill in the clientID and secret with your WSKey parameters""" 31 | key = '{clientID}' 32 | secret = '{secret}' 33 | 34 | """Default values for the Sandbox Institution. You may want to change them to your institution's values""" 35 | authenticating_institution_id = '128807' 36 | context_institution_id = '128807' 37 | 38 | """We use the Worldcat Metadata API to test the Access Token""" 39 | services = ['WorldCatMetadataAPI', 'refresh_token'] 40 | 41 | """The response object is where we write data to display on the page.""" 42 | response = HttpResponse() 43 | 44 | response.write(""" 45 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 59 |

Authentication Token & Bibliographic Record - Django Example

60 | """) 61 | 62 | """The redirect URI is calculated here and must match the redirect URI assigned to your WSKey""" 63 | if request.is_secure: 64 | """You must use SSL to request an Access token""" 65 | redirect_uri = 'https://' + request.get_host() + request.path # https://localhost:8000/auth/ 66 | else: 67 | """This won't work.""" 68 | redirect_uri = 'http://' + request.get_host() + request.path # http://localhost:8000/auth/ 69 | 70 | """Populate the WSKey object's parameters""" 71 | my_wskey = wskey.Wskey( 72 | key=key, 73 | secret=secret, 74 | options={ 75 | 'services': services, 76 | 'redirect_uri': redirect_uri 77 | } 78 | ) 79 | 80 | access_token = None 81 | 82 | """If an access_token is stored in the current session load it.""" 83 | if request.session.get('access_token', None) != None: 84 | access_token = pickle.loads(request.session.get('access_token', None)) 85 | 86 | """If the access_token we loaded is expired, we can't use it.""" 87 | if access_token != None and access_token.is_expired(): 88 | access_token = None 89 | 90 | """If there is a code parameter on the current URL, load it.""" 91 | code = request.GET.get('code', None) 92 | 93 | """If there are error parameters on the current URL, load them.""" 94 | error = request.GET.get('error', None) 95 | errorDescription = request.GET.get('error_description', None) 96 | 97 | if error != None: 98 | """If an error was returned, display it.""" 99 | response.write('

Error: ' + error + '
' + errorDescription + '

') 100 | 101 | elif access_token == None and code == None: 102 | """Initiate user authentication by executing a redirect to the IDM sign in page.""" 103 | login_url = my_wskey.get_login_url( 104 | authenticating_institution_id=authenticating_institution_id, 105 | context_institution_id=context_institution_id 106 | ) 107 | response['Location'] = login_url 108 | response.status_code = '303' 109 | 110 | elif access_token == None and code != None: 111 | """Request an access token using the user authentication code returned after the user authenticated""" 112 | """Then request a bibliographic record""" 113 | access_token = my_wskey.get_access_token_with_auth_code( 114 | code=code, 115 | authenticating_institution_id=authenticating_institution_id, 116 | context_institution_id=context_institution_id 117 | ) 118 | 119 | if access_token.error_code == None: 120 | request.session['access_token'] = pickle.dumps(access_token) 121 | response.write('

Access Token NOT FOUND in this session, so I requested a new one.

') 122 | 123 | response.write(format_access_token(access_token=access_token)) 124 | 125 | if access_token.error_code == None: 126 | response.write(get_bib_record(access_token=access_token, wskey=my_wskey)) 127 | 128 | elif access_token != None: 129 | """We already have an Access Token, so display the token and request a Bibliographic Record""" 130 | if access_token.error_code == None: 131 | response.write('

Access Token found in this session, and it is still valid.

') 132 | 133 | response.write(format_access_token(access_token=access_token)) 134 | 135 | if access_token.error_code == None: 136 | response.write(get_bib_record(access_token=access_token, wskey=my_wskey)) 137 | 138 | return response 139 | 140 | 141 | def format_access_token(access_token): 142 | """Display all the parameters of the Access Token""" 143 | print(access_token) 144 | 145 | ret = '

Access Token

' 146 | 147 | ret += '' 148 | 149 | if access_token.error_code != None: 150 | ret += '' 151 | ret += '' 152 | ret += ('') 154 | 155 | else: 156 | ret += '' 157 | ret += '' 158 | ret += '' 159 | ret += '' 160 | 161 | if access_token.user != None: 162 | ret += '' 163 | ret += '' 164 | ret += '' 165 | 166 | if access_token.refresh_token != None: 167 | ret += '' 168 | ret += '' 170 | ret += '' 172 | 173 | ret += '
Error Code' + str(access_token.error_code) + '
Error Message' + str(access_token.error_message) + '
Error Url
' +
153 |                 str(access_token.error_url).replace('?', '?\n').replace('&', '\n&') + '
access_token' + str(access_token.access_token_string) + '
token_type' + str(access_token.type) + '
expires_at' + str(access_token.expires_at) + '
expires_in' + str(access_token.expires_in) + '
principalID' + str(access_token.user.principal_id) + '
principalIDNS' + str(access_token.user.principal_idns) + '
contextInstitutionId' + str(access_token.context_institution_id) + '
refresh_token' + str(access_token.refresh_token.refresh_token) + '
refresh_token_expires_at' + str( 169 | access_token.refresh_token.expires_at) + '
refresh_token_expires_in' + str( 171 | access_token.refresh_token.expires_in) + '
' 174 | return ret 175 | 176 | 177 | def get_bib_record(access_token, wskey): 178 | """Use an Access Token's User Parameter to request a Bibliographic Record""" 179 | request_url = ( 180 | 'https://worldcat.org/bib/data/823520553?' + 181 | 'classificationScheme=LibraryOfCongress' + 182 | '&holdingLibraryCode=MAIN' 183 | ) 184 | 185 | authorization_header = wskey.get_hmac_signature( 186 | method='GET', 187 | request_url=request_url, 188 | options={ 189 | 'user': access_token.user 190 | } 191 | ) 192 | 193 | my_request = urllib2.Request( 194 | url=request_url, 195 | data=None, 196 | headers={'Authorization': authorization_header} 197 | ) 198 | 199 | try: 200 | xml_result = urllib2.urlopen(my_request).read() 201 | 202 | except urllib2.HTTPError, e: 203 | xml_result = str(e) 204 | 205 | ret = '

Bibliographic Record

' 206 | ret += '
' + xml_result.replace('<', '<') + '
' 207 | 208 | return ret -------------------------------------------------------------------------------- /examples/authentication_token_with_django/djangoProject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCLC-Developer-Network/oclc-auth-python/d4c56c604b9db280fc1c502e688a7fa9260ca9ec/examples/authentication_token_with_django/djangoProject/__init__.py -------------------------------------------------------------------------------- /examples/authentication_token_with_django/djangoProject/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djangoProject project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.6/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.6/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | 14 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 15 | 16 | 17 | # Quick-start development settings - unsuitable for production 18 | # See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ 19 | 20 | # SECURITY WARNING: keep the secret key used in production secret! 21 | SECRET_KEY = '@r(u+6!o5qrw#rtb^p*f)m$mo!=1=zoxerhz7gmg47pft&=!+&' 22 | 23 | # SECURITY WARNING: don't run with debug turned on in production! 24 | DEBUG = True 25 | 26 | TEMPLATE_DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'authTokenApp', 41 | 'sslserver', # remove if using Stunnel 42 | ) 43 | 44 | MIDDLEWARE_CLASSES = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.messages.middleware.MessageMiddleware', 50 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 51 | ) 52 | 53 | ROOT_URLCONF = 'djangoProject.urls' 54 | 55 | WSGI_APPLICATION = 'djangoProject.wsgi.application' 56 | 57 | 58 | # Database 59 | # https://docs.djangoproject.com/en/1.6/ref/settings/#databases 60 | 61 | DATABASES = { 62 | 'default': { 63 | 'ENGINE': 'django.db.backends.sqlite3', 64 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 65 | } 66 | } 67 | 68 | # Cache for session storage - local memory sufficient for development testing only 69 | 70 | CACHES = { 71 | 'default': { 72 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 73 | } 74 | } 75 | 76 | # Configure the session engine to use local memory 77 | 78 | SESSION_ENGINE = 'django.contrib.sessions.backends.cache' 79 | 80 | # Internationalization 81 | # https://docs.djangoproject.com/en/1.6/topics/i18n/ 82 | 83 | LANGUAGE_CODE = 'en-us' 84 | 85 | TIME_ZONE = 'UTC' 86 | 87 | USE_I18N = True 88 | 89 | USE_L10N = True 90 | 91 | USE_TZ = True 92 | 93 | 94 | # Static files (CSS, JavaScript, Images) 95 | # https://docs.djangoproject.com/en/1.6/howto/static-files/ 96 | 97 | STATIC_URL = '/static/' 98 | -------------------------------------------------------------------------------- /examples/authentication_token_with_django/djangoProject/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | from django.contrib import admin 4 | 5 | admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | url(r'^admin/', include(admin.site.urls)), 9 | url(r'^auth/', include('authTokenApp.urls')), 10 | ) 11 | -------------------------------------------------------------------------------- /examples/authentication_token_with_django/djangoProject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djangoProject project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoProject.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /examples/authentication_token_with_django/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoProject.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /examples/client_credentials_grant/README.md: -------------------------------------------------------------------------------- 1 | ###Server Side Client Credentials Grant Example 2 | 3 | 1. Clone the repository: 4 | 5 | `git clone https://github.com/OCLC-Developer-Network/oclc-auth-python.git` 6 | 7 | 1. Install the library: 8 | 9 | `sudo python setup.py install` 10 | 11 | 1. Change directories to `examples/client_credentials grant` 12 | 13 | 1. Edit `client_credentials_grant.py` to insert your: 14 | * key 15 | * secret 16 | * authenticating_institution_id 17 | * context_institution_id 18 |

19 | 1. Run from the command line: 20 | 21 | `python client_credentials_grant.py` 22 | 23 | You should get back an access token if your WSKey is configured properly. 24 | 25 |
26 |    access token:  tk_xxx5KWq9w1Cc0dc5MrvIhFvdEZteylgsR7VT
27 |    expires_in:    1199
28 |    expires_at:    2014-09-09 15:22:49Z
29 |    type:          bearer
30 |    
31 | 32 | Or an error message if the key is not configured properly 33 | 34 |
35 |    error_code:    401
36 |    error_message: HTTP Error 401: Unauthorized
37 |    error_url:     https://authn.sd00.worldcat.org/oauth2/accessToken?
38 |                   grant_type=client_credentials&
39 |                   authenticatingInstitutionId=128807&
40 |                   contextInstitutionId=128807&
41 |                   scope=WorldCatDiscoveryAPI
42 |    
-------------------------------------------------------------------------------- /examples/client_credentials_grant/client_credentials_grant.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # ############################################################################### 5 | # Copyright 2014 OCLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 8 | # use this file except in compliance with the License. You may obtain a copy of 9 | # the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ############################################################################### 19 | 20 | # Example of retrieving a token with Client Credentials Grant 21 | 22 | from authliboclc import wskey 23 | import requests 24 | import xml.etree.ElementTree as ET 25 | 26 | # 27 | # Authentication Parameters 28 | # 29 | 30 | key = '{clientID}' 31 | secret = '{secret}' 32 | authenticating_institution_id = '{institutionId}' 33 | context_institution_id = '{institutionId}' 34 | 35 | # Configure the wskey library object 36 | my_wskey = wskey.Wskey( 37 | key=key, 38 | secret=secret, 39 | options={'services': ['WorldCatDiscoveryAPI']}) 40 | 41 | # Get an access token 42 | access_token = my_wskey.get_access_token_with_client_credentials( 43 | authenticating_institution_id=authenticating_institution_id, 44 | context_institution_id=context_institution_id 45 | ) 46 | 47 | # Describe the token received, or the error produced 48 | print("") 49 | if (access_token.access_token_string == None): 50 | if (key == '{clientID}'): 51 | print( 52 | "**** You must configure the key, secret, authenticating_institution_id and context_institution_id ****") 53 | print("") 54 | print("error_code: " + `access_token.error_code`) 55 | print("error_message: " + access_token.error_message) 56 | print("error_url: " + access_token.error_url) 57 | else: 58 | print("access token: " + access_token.access_token_string) 59 | print("expires_in: " + `access_token.expires_in`) 60 | print("expires_at: " + access_token.expires_at) 61 | print("type: " + access_token.type) 62 | if (access_token.refresh_token != None): 63 | print("refresh_token: " + access_token.refresh_token) 64 | 65 | # Make a Discovery API Search request with the following query: 66 | # businesses+utilities+and+transportation+AND+creator:Stoll 67 | # 68 | # Documentation for the Discovery API: 69 | # http://oclc.org/developer/develop/web-services/worldcat-discovery-api/bibliographic-resource.en.html 70 | # 71 | # Note that as of September 2014, some changes were made to the Discovery API: 72 | # 73 | # 1. An additional parameter is required on all searches to specify the data set: 74 | # dbIds=638 - WorldCat.org data set 75 | # dbIds=283 - WorldCat (traditional/proper, just the MARC cataloged stuff) 76 | # 77 | # 2. The search parameter "author" was mapped to "creator". In our example here we are searching on 78 | # "creator:Stoll" rather than "author:Stoll". 79 | # 80 | query = 'businesses+utilities+and+transportation+AND+creator:Stoll' 81 | dbIds = '638' 82 | request_url = 'https://beta.worldcat.org/discovery/bib/search?' + 'q=' + query + '&' + 'dbIds=' + dbIds 83 | authorization = 'Bearer ' + access_token.access_token_string 84 | 85 | headers={'Authorization': authorization_header, 'Accept': 'application/json'} 86 | try: 87 | r = requests.get(request_url, headers=headers) 88 | r.raise_for_status() 89 | response_body = r.json() 90 | print(response_body) 91 | except requests.exceptions.HTTPError as err: 92 | print("Read failed. " + str(err.response.status_code)) -------------------------------------------------------------------------------- /examples/hmac_authentication/README.md: -------------------------------------------------------------------------------- 1 | ###Server Side HMAC Authentication Example 2 | 3 | 1. Install the library: 4 | 5 | `pip install git+git:https://github.com/OCLC-Developer-Network/oclc-auth-python` 6 | 7 | 1. Change directories to `examples/hmac_authentication` 8 | 9 | 1. Edit `hmac_request_example.py` to insert your: 10 | * key 11 | * secret 12 | * principal_id 13 | * principal_idns 14 | * authenticating_institution_id 15 |

16 | 1. Run from the command line: 17 | 18 | `python hmac_request_example.py` 19 | 20 | You should get back an XML result if your WSKey is configured properly. 21 | 22 |
23 |    <?xml version="1.0" encoding="UTF-8"?>
24 |        <entry xmlns="http://www.w3.org/2005/Atom">
25 |        <content type="application/xml">
26 |        <response xmlns="http://worldcat.org/rb" mimeType="application/vnd.oclc.marc21+xml">
27 |        <record xmlns="http://www.loc.gov/MARC21/slim">
28 |        <leader>00000cam a2200000Ia 4500
29 |        ...
30 |    
-------------------------------------------------------------------------------- /examples/hmac_authentication/hmac_request_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # ############################################################################### 5 | # Copyright 2014 OCLC 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 8 | # use this file except in compliance with the License. You may obtain a copy of 9 | # the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | ############################################################################### 19 | 20 | # Sample HMAC Hashing for Bibliographic record retrieval 21 | 22 | from authliboclc import wskey, user 23 | import requests 24 | import xml.etree.ElementTree as ET 25 | 26 | # 27 | # You must supply these parameters to authenticate 28 | # Note - a WSKey consists of two parts, a public clientID and a private secret 29 | # 30 | 31 | key = '{clientID}' 32 | secret = '{secret}' 33 | principal_id = '{principalID}' 34 | principal_idns = '{principalIDNS}' 35 | authenticating_institution_id = '{institutionID}' 36 | 37 | request_url = 'https://worldcat.org/bib/data/823520553?classificationScheme=LibraryOfCongress' 38 | 39 | my_wskey = wskey.Wskey( 40 | key=key, 41 | secret=secret, 42 | options=None) 43 | 44 | my_user = user.User( 45 | authenticating_institution_id=authenticating_institution_id, 46 | principal_id=principal_id, 47 | principal_idns=principal_idns 48 | ) 49 | 50 | authorization_header = my_wskey.get_hmac_signature( 51 | method='GET', 52 | request_url=request_url, 53 | options={ 54 | 'user': my_user, 55 | 'auth_params': None} 56 | ) 57 | 58 | headers={'Authorization': authorization_header, 'Accept': 'application/atom+xml;content="application/vnd.oclc.marc21+xml"'} 59 | try: 60 | r = requests.get(request_url, headers=headers) 61 | r.raise_for_status() 62 | response_body = ET.fromstring(r.content) 63 | ns = {'atom': 'http://www.w3.org/2005/Atom', 64 | 'rb': 'http://worldcat.org/rb', 65 | 'marc': 'http://www.loc.gov/MARC21/slim'} 66 | 67 | record = response_body.find('.//atom:content/rb:response/marc:record', ns) 68 | print(ET.tostring(record, encoding='utf8').decode('utf8')) 69 | except requests.exceptions.HTTPError as err: 70 | print("Read failed. " + str(err.response.status_code)) -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [bdist_wheel] 5 | universal = 1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name = 'authliboclc', 7 | packages = ['authliboclc'], 8 | version = "0.0.2", 9 | license='Apache2', 10 | description = "OCLC API Authentication Library", 11 | author = 'OCLC Platform Team', 12 | author_email = "devnet@oclc.org", 13 | url = 'http://oclc.org/developer/home.en.html', 14 | download_url = 'git@github.com:OCLC-Developer-Network/oclc-auth-python.git', 15 | install_requires = ['six>=1'] 16 | ) 17 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OCLC-Developer-Network/oclc-auth-python/d4c56c604b9db280fc1c502e688a7fa9260ca9ec/tests/__init__.py -------------------------------------------------------------------------------- /tests/accesstoken_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.accesstoken_test 18 | 19 | import unittest 20 | from authliboclc import accesstoken, user, refreshtoken 21 | 22 | 23 | class AccessTokenTests(unittest.TestCase): 24 | """ Create a mock access token. """ 25 | 26 | def setUp(self): 27 | self._my_refresh_token = refreshtoken.RefreshToken( 28 | tokenValue='rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W', 29 | expires_in=1199, 30 | expires_at='2014-03-13 15:44:59Z' 31 | ) 32 | 33 | self._options = {'scope': ['WMS_NCIP', 'WMS_ACQ'], 34 | 'authenticating_institution_id': '128807', 35 | 'context_institution_id': '128808', 36 | 'redirect_uri': 'ncip://testapp', 37 | 'refresh_token': self._my_refresh_token, 38 | 'code': 'unknown'} 39 | 40 | self._authorization_server = 'https://authn.sd00.worldcat.org/oauth2' 41 | 42 | self._my_access_token = accesstoken.AccessToken(self._authorization_server, 43 | 'authorization_code', 44 | self._options) 45 | 46 | 47 | def testAuthorizationServer(self): 48 | self.assertEqual('https://authn.sd00.worldcat.org/oauth2', 49 | self._my_access_token.authorization_server) 50 | 51 | """ Make sure only the correct valid access token options are listed. """ 52 | 53 | def testValidOptions(self): 54 | options = accesstoken.AccessToken.valid_options 55 | valid_options = [ 56 | 'scope', 57 | 'authenticating_institution_id', 58 | 'context_institution_id', 59 | 'redirect_uri', 60 | 'code', 61 | 'refresh_token' 62 | ] 63 | self.assertEqual(options, valid_options, 64 | 'Options must be scope, authenticating_institution_id, context_institution_id, redirect_uri, ' 65 | 'code and refresh_token') 66 | 67 | """ Make sure the list of valid grant types is correct. """ 68 | 69 | def testValidGrantTypes(self): 70 | grant_types = accesstoken.AccessToken.validGrantTypes 71 | valid_grant_types = [ 72 | 'authorization_code', 73 | 'refresh_token', 74 | 'client_credentials' 75 | ] 76 | self.assertEqual(grant_types, valid_grant_types, 'Grant types must be authorization_code, refresh_token, ' 77 | 'client_credentials') 78 | 79 | """ Check that attempts to create Access Tokens work, and incorrect parameters raise exceptions. """ 80 | 81 | def testCreateAccessToken(self): 82 | self.assertEqual(self._my_access_token.scope, ['WMS_NCIP', 'WMS_ACQ']) 83 | self.assertEqual(self._my_access_token.authenticating_institution_id, '128807') 84 | self.assertEqual(self._my_access_token.context_institution_id, '128808') 85 | self.assertEqual(self._my_access_token.redirect_uri, 'ncip://testapp') 86 | self.assertEqual(self._my_access_token.refresh_token.refresh_token, 'rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W') 87 | self.assertEqual(self._my_access_token.code, 'unknown') 88 | 89 | with self.assertRaises(accesstoken.InvalidGrantType): 90 | accesstoken.AccessToken(authorization_server=self._authorization_server) 91 | 92 | # Tests to make sure there are no missing parameters for authorization_code 93 | with self.assertRaises(accesstoken.NoOptionsPassed): 94 | accesstoken.AccessToken(authorization_server=self._authorization_server, 95 | grant_type='authorization_code', 96 | options={}) 97 | with self.assertRaises(accesstoken.RequiredOptionsMissing): 98 | accesstoken.AccessToken(authorization_server=self._authorization_server, 99 | grant_type='authorization_code', 100 | options={'authenticating_institution_id': '', 'context_institution_id': ''}) 101 | with self.assertRaises(accesstoken.RequiredOptionsMissing): 102 | accesstoken.AccessToken(authorization_server=self._authorization_server, 103 | grant_type='authorization_code', 104 | options={'code': '', 'context_institution_id': ''}) 105 | with self.assertRaises(accesstoken.RequiredOptionsMissing): 106 | accesstoken.AccessToken(authorization_server=self._authorization_server, 107 | grant_type='authorization_code', 108 | options={'code': '', 'authenticating_institution_id': ''}) 109 | 110 | # Tests to make sure there are no missing parameters for client_credentials 111 | with self.assertRaises(accesstoken.RequiredOptionsMissing): 112 | accesstoken.AccessToken(authorization_server=self._authorization_server, 113 | grant_type='client_credentials', 114 | options={'refresh_token': '', 115 | 'context_institution_id': '', 116 | 'scope': ''}) 117 | with self.assertRaises(accesstoken.RequiredOptionsMissing): 118 | accesstoken.AccessToken(authorization_server=self._authorization_server, 119 | grant_type='refresh_token', 120 | options={'client_credentials': '', 121 | 'authenticating_institution_id': '', 122 | 'scope': ''}) 123 | with self.assertRaises(accesstoken.RequiredOptionsMissing): 124 | accesstoken.AccessToken(authorization_server=self._authorization_server, 125 | grant_type='client_credentials', 126 | options={'refresh_token': '', 127 | 'authenticating_institution_id': '', 128 | 'context_institution_id': ''}) 129 | 130 | # Tests to make sure there are no missing parameters for refresh_token 131 | with self.assertRaises(accesstoken.RequiredOptionsMissing): 132 | accesstoken.AccessToken(authorization_server=self._authorization_server, 133 | grant_type='refresh_token', 134 | options={'authenticating_institution_id': '', 135 | 'context_institution_id': '', 136 | 'scope': ''}) 137 | 138 | # Test that scope must be a list of scopes, not a string 139 | with self.assertRaises(accesstoken.RequiredOptionsMissing): 140 | accesstoken.AccessToken(authorization_server=self._authorization_server, 141 | grant_type='authorization_code', 142 | options={'code': '', 143 | 'redirect_uri': '', 144 | 'authenticating_institution_id': '', 145 | 'context_institution_id': '', 146 | 'scope': 'WMS_ACQ'}) 147 | 148 | """ Make sure an expired token is calculated properly. """ 149 | 150 | def testIsExpired(self): 151 | self._my_access_token.expires_at = '2014-01-01 12:00:00Z' 152 | self.assertTrue(self._my_access_token.is_expired()) 153 | 154 | self._my_access_token.expires_at = '2099-01-01 12:00:00Z' 155 | self.assertFalse(self._my_access_token.is_expired()) 156 | 157 | """ Test creation of an access token for authorization_code. """ 158 | 159 | def testGetAccessTokenURLforAuthorizationCode(self): 160 | sample_access_token = accesstoken.AccessToken(self._authorization_server, 161 | 'authorization_code', 162 | self._options) 163 | self.assertEqual(sample_access_token.get_access_token_url(), ( 164 | 'https://authn.sd00.worldcat.org/oauth2/accessToken?' + 165 | 'grant_type=authorization_code' + 166 | '&code=unknown' + 167 | '&authenticatingInstitutionId=128807' + 168 | '&contextInstitutionId=128808' + 169 | '&redirect_uri=ncip%3A%2F%2Ftestapp') 170 | ) 171 | 172 | """ Test creation of an access token for client_credentials. """ 173 | 174 | def testGetAccessTokenURLforClientCredentials(self): 175 | sample_access_token = accesstoken.AccessToken(self._authorization_server, 176 | 'client_credentials', 177 | self._options) 178 | self.assertEqual(sample_access_token.get_access_token_url(), ( 179 | 'https://authn.sd00.worldcat.org/oauth2/accessToken?' + 180 | 'grant_type=client_credentials&' + 181 | 'authenticatingInstitutionId=128807&' + 182 | 'contextInstitutionId=128808&' + 183 | 'scope=WMS_NCIP%20WMS_ACQ') 184 | ) 185 | 186 | """ Test creation of an access token for refresh_token. """ 187 | 188 | def testGetAccessTokenURLforRefreshToken(self): 189 | sample_access_token = accesstoken.AccessToken(self._authorization_server, 190 | 'refresh_token', 191 | self._options) 192 | self.assertEqual(sample_access_token.get_access_token_url(), ( 193 | 'https://authn.sd00.worldcat.org/oauth2/accessToken?' + 194 | 'grant_type=refresh_token' + 195 | '&refresh_token=rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W')) 196 | 197 | """ Create a mock token response and verify parsing is corrent. """ 198 | 199 | def testParseTokenResponse(self): 200 | sample_access_token = accesstoken.AccessToken(self._authorization_server, 201 | 'authorization_code', 202 | self._options) 203 | sample_access_token.parse_token_response( 204 | '{' + 205 | '"expires_at":"2014-03-13 15:44:59Z",' + 206 | '"principalIDNS":"urn:oclc:platform:128807",' + 207 | '"principalID":"2334dd24-b27e-49bd-8fea-7cc8de670f8d",' + 208 | '"error_code":"trouble",' + 209 | '"expires_in":1199,' + 210 | '"token_type":"bearer",' + 211 | '"context_institution_id":"128807",' + 212 | '"access_token":"tk_25fXauhJC09E5kwFxcf4TRXkTnaRYWHgJA0W",' + 213 | '"refresh_token":"rt_25fXauhJC09E5kwFxcf4TRXkTnaRYWHgJA0W",' + 214 | '"refresh_token_expires_in":1900,' + 215 | '"refresh_token_expires_at":"2014-03-13 15:44:59Z"' + 216 | '}' 217 | 218 | ) 219 | expected_user = user.User( 220 | authenticating_institution_id='128807', 221 | principal_id='2334dd24-b27e-49bd-8fea-7cc8de670f8d', 222 | principal_idns='urn:oclc:platform:128807' 223 | ) 224 | 225 | expected_refresh_token = refreshtoken.RefreshToken( 226 | tokenValue='rt_25fXauhJC09E5kwFxcf4TRXkTnaRYWHgJA0W', 227 | expires_in=1900, 228 | expires_at='2014-03-13 15:44:59Z' 229 | ) 230 | 231 | self.assertEqual(sample_access_token.access_token_string, 'tk_25fXauhJC09E5kwFxcf4TRXkTnaRYWHgJA0W') 232 | self.assertEqual(sample_access_token.type, 'bearer') 233 | self.assertEqual(sample_access_token.expires_at, '2014-03-13 15:44:59Z') 234 | self.assertEqual(sample_access_token.expires_in, 1199) 235 | self.assertEqual(sample_access_token.error_code, 'trouble') 236 | self.assertEqual(sample_access_token.context_institution_id, '128807') 237 | self.assertEqual(user.User, type(sample_access_token.user)) 238 | self.assertEqual(expected_user.authenticating_institution_id, 239 | sample_access_token.user.authenticating_institution_id) 240 | self.assertEqual(expected_user.principal_id, sample_access_token.user.principal_id) 241 | self.assertEqual(expected_user.principal_idns, sample_access_token.user.principal_idns) 242 | self.assertEqual(refreshtoken.RefreshToken, type(sample_access_token.refresh_token)) 243 | self.assertEqual(expected_refresh_token.refresh_token, sample_access_token.refresh_token.refresh_token) 244 | self.assertEqual(expected_refresh_token.expires_in, sample_access_token.refresh_token.expires_in) 245 | self.assertEqual(expected_refresh_token.expires_at, sample_access_token.refresh_token.expires_at) 246 | 247 | """Test that the string representation of the class is complete.""" 248 | 249 | def testStringRepresenationOfClass(self): 250 | """Create a new access token which hasn't been authenticated yet.""" 251 | sample_access_token = accesstoken.AccessToken( 252 | self._authorization_server, 253 | grant_type='authorization_code', 254 | options={'scope': ['WMS_NCIP', 'WMS_ACQ'], 255 | 'authenticating_institution_id': '128807', 256 | 'context_institution_id': '128808', 257 | 'redirect_uri': 'https://localhost:8000/auth/', 258 | 'code': 'unknown' 259 | } 260 | ) 261 | 262 | """Assume authentication has occured and these parameters are now filled in.""" 263 | sample_access_token.expires_at = '2014-04-08 13:38:29Z' 264 | sample_access_token.expires_in = 1198 265 | sample_access_token.access_token_string = 'tk_TBHrsDbSrWW1oS7d3gZr7NJb7PokyOFlf0pr' 266 | sample_access_token.type = 'bearer' 267 | sample_access_token.error_code = 404 268 | sample_access_token.error_message = 'No reply at all.' 269 | sample_access_token.error_url = 'http://www.noreply.oclc.org/auth/' 270 | 271 | self.assertEqual(str(sample_access_token), ( 272 | "\n" + 273 | "access_token_url: https://authn.sd00.worldcat.org/oauth2/accessToken?\n" + 274 | " grant_type=authorization_code\n" + 275 | " &code=unknown\n" + 276 | " &authenticatingInstitutionId=128807\n" + 277 | " &contextInstitutionId=128808\n" + 278 | " &redirect_uri=https%3A%2F%2Flocalhost%3A8000%2Fauth%2F\n" + 279 | "\n" + 280 | "access_token_string tk_TBHrsDbSrWW1oS7d3gZr7NJb7PokyOFlf0pr\n" + 281 | "authenticating_institution_id: 128807\n" + 282 | "authorization_server: https://authn.sd00.worldcat.org/oauth2\n" + 283 | "code: unknown\n" + 284 | "context_institution_id: 128808\n" + 285 | "error_code: 404\n" + 286 | "error_message: No reply at all.\n" + 287 | "error_url: http://www.noreply.oclc.org/auth/\n" + 288 | "expires_at: 2014-04-08 13:38:29Z\n" + 289 | "expires_in: 1198\n" + 290 | "grant_type: authorization_code\n" + 291 | "options: None\n" + 292 | "redirect_uri: https://localhost:8000/auth/\n" + 293 | "refresh_token:\n" + 294 | "None\n" + 295 | "scope: ['WMS_NCIP', 'WMS_ACQ']\n" + 296 | "type: bearer\n" + 297 | "user:\n" + 298 | "None\n" + 299 | "wskey:\n" + 300 | "None") 301 | ) 302 | 303 | 304 | def main(): 305 | unittest.main() 306 | 307 | 308 | if __name__ == '__main__': 309 | main() 310 | -------------------------------------------------------------------------------- /tests/authcode_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.authcode_test 18 | 19 | import unittest 20 | from authliboclc import authcode 21 | 22 | 23 | class AuthCodeTests(unittest.TestCase): 24 | def setUp(self): 25 | self._authorization_server = 'https://authn.sd00.worldcat.org/oauth2' 26 | 27 | self._authCode = authcode.AuthCode( 28 | authorization_server=self._authorization_server, 29 | client_id='1234ABCD', 30 | authenticating_institution_id='128807', 31 | context_institution_id='128808', 32 | redirect_uri='http://www.oclc.org/test', 33 | scopes=['WMS_NCIP', 'WMS_ACQ'] 34 | ) 35 | 36 | def testAuthorizationServer(self): 37 | self.assertEqual(self._authCode.authorization_server, 'https://authn.sd00.worldcat.org/oauth2') 38 | 39 | """ Test Create AuthCode - incorrect parameters should raise exceptions.""" 40 | 41 | def testCreateAuthCode(self): 42 | with self.assertRaises(authcode.InvalidParameter): 43 | authcode.AuthCode(authorization_server=self._authorization_server) 44 | with self.assertRaises(authcode.InvalidParameter): 45 | authcode.AuthCode( 46 | authorization_server=self._authorization_server, 47 | authenticating_institution_id='128807', 48 | context_institution_id='128808', 49 | redirect_uri='http://www.oclc.org/test', 50 | scopes=['WMS_NCIP', 'WMS_ACQ'] 51 | ) 52 | with self.assertRaises(authcode.InvalidParameter): 53 | authcode.AuthCode( 54 | authorization_server=self._authorization_server, 55 | client_id='1234ABCD', 56 | context_institution_id='128808', 57 | redirect_uri='http://www.oclc.org/test', 58 | scopes=['WMS_NCIP', 'WMS_ACQ'] 59 | ) 60 | with self.assertRaises(authcode.InvalidParameter): 61 | authcode.AuthCode( 62 | authorization_server=self._authorization_server, 63 | client_id='1234ABCD', 64 | authenticating_institution_id='128807', 65 | redirect_uri='http://www.oclc.org/test', 66 | scopes=['WMS_NCIP', 'WMS_ACQ'] 67 | ) 68 | with self.assertRaises(authcode.InvalidParameter): 69 | authcode.AuthCode( 70 | authorization_server=self._authorization_server, 71 | client_id='1234ABCD', 72 | authenticating_institution_id='128807', 73 | context_institution_id='128808', 74 | scopes=['WMS_NCIP', 'WMS_ACQ'] 75 | ) 76 | with self.assertRaises(authcode.InvalidParameter): 77 | authcode.AuthCode( 78 | authorization_server=self._authorization_server, 79 | client_id='1234ABCD', 80 | authenticating_institution_id='128807', 81 | context_institution_id='128808', 82 | redirect_uri='http://www.oclc.org/test' 83 | ) 84 | with self.assertRaises(authcode.InvalidParameter): 85 | authcode.AuthCode( 86 | authorization_server=self._authorization_server, 87 | client_id='1234ABCD', 88 | authenticating_institution_id='128807', 89 | context_institution_id='128808', 90 | redirect_uri='http://www.oclc.org/test', 91 | scopes='' 92 | ) 93 | with self.assertRaises(authcode.InvalidParameter): 94 | authcode.AuthCode( 95 | authorization_server=self._authorization_server, 96 | client_id='1234ABCD', 97 | authenticating_institution_id='128807', 98 | context_institution_id='128808', 99 | redirect_uri='http://www.oclc.org/test', 100 | scopes=[] 101 | ) 102 | with self.assertRaises(authcode.InvalidParameter): 103 | authcode.AuthCode( 104 | authorization_server=self._authorization_server, 105 | client_id='1234ABCD', 106 | authenticating_institution_id='128807', 107 | context_institution_id='128808', 108 | redirect_uri='http://www.oclc.org/test', 109 | scopes=[''] 110 | ) 111 | 112 | myAuthCode = authcode.AuthCode( 113 | authorization_server=self._authorization_server, 114 | client_id='1234ABCD', 115 | authenticating_institution_id='128807', 116 | context_institution_id='128808', 117 | redirect_uri='http://www.oclc.org/test', 118 | scopes=['WMS_NCIP', 'WMS_ACQ'] 119 | ) 120 | self.assertEqual(myAuthCode.client_id, '1234ABCD') 121 | self.assertEqual(myAuthCode.authenticating_institution_id, '128807') 122 | self.assertEqual(myAuthCode.context_institution_id, '128808') 123 | self.assertEqual(myAuthCode.redirect_uri, 'http://www.oclc.org/test') 124 | self.assertEqual(myAuthCode.scopes[0], 'WMS_NCIP') 125 | self.assertEqual(myAuthCode.scopes[1], 'WMS_ACQ') 126 | 127 | """ Verify that a proper login url to get the access token is generated.""" 128 | 129 | def testGetLoginUrl(self): 130 | expectedResult = ( 131 | 'https://authn.sd00.worldcat.org/oauth2/authorizeCode' + 132 | '?authenticatingInstitutionId=128807' + 133 | '&client_id=1234ABCD' + 134 | '&contextInstitutionId=128808' + 135 | '&redirect_uri=http%3A%2F%2Fwww.oclc.org%2Ftest' + 136 | '&response_type=code' + 137 | '&scope=WMS_NCIP WMS_ACQ' 138 | ) 139 | 140 | self.assertEqual(self._authCode.get_login_url(), expectedResult) 141 | 142 | """Test that the string representation of the class is complete.""" 143 | 144 | def testStringRepresenationOfClass(self): 145 | self.assertEqual(str(self._authCode), 146 | "authorization_server: https://authn.sd00.worldcat.org/oauth2\n" + 147 | "client_id: 1234ABCD\n" + 148 | "authenticating_institution_id: 128807\n" + 149 | "context_institution_id: 128808\n" + 150 | "redirect_uri: http://www.oclc.org/test\n" + 151 | "scopes: ['WMS_NCIP', 'WMS_ACQ']\n" 152 | ) 153 | 154 | 155 | def main(): 156 | unittest.main() 157 | 158 | 159 | if __name__ == '__main__': 160 | main() -------------------------------------------------------------------------------- /tests/refreshtoken_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.refreshtoken_test 18 | 19 | import unittest 20 | from authliboclc import refreshtoken 21 | 22 | 23 | class RefreshTokenTests(unittest.TestCase): 24 | _myRefreshToken = None 25 | 26 | """ Create a mock refresh token. """ 27 | 28 | def setUp(self): 29 | self._myRefreshToken = refreshtoken.RefreshToken(**{ 30 | 'tokenValue': 'rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W', 31 | 'expires_in': 1199, 32 | 'expires_at': '2014-03-13 15:44:59Z', 33 | }) 34 | 35 | 36 | """ Test that refresh token creation with invalid parameters raises exceptions.""" 37 | 38 | def testCreateRefreshTokenInvalidParameters(self): 39 | with self.assertRaises(refreshtoken.InvalidParameter): 40 | refreshtoken.RefreshToken() 41 | 42 | with self.assertRaises(refreshtoken.InvalidParameter): 43 | refreshtoken.RefreshToken(**{ 44 | 'tokenValue': 'rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W', 45 | 'expires_in': 1199, 46 | }) 47 | with self.assertRaises(refreshtoken.InvalidParameter): 48 | refreshtoken.RefreshToken(**{ 49 | 'tokenValue': 'rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W', 50 | 'expires_at': '2014-03-13 15:44:59Z', 51 | }) 52 | with self.assertRaises(refreshtoken.InvalidParameter): 53 | refreshtoken.RefreshToken(**{ 54 | 'expires_in': 1199, 55 | 'expires_at': '2014-03-13 15:44:59Z', 56 | }) 57 | with self.assertRaises(refreshtoken.InvalidParameter): 58 | refreshtoken.RefreshToken(**{ 59 | 'tokenValue': 'rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W', 60 | 'expires_in': '1199', 61 | 'expires_at': '2014-03-13 15:44:59Z', 62 | }) 63 | 64 | """ Make sure the parameters are saved properly when the token is created. """ 65 | 66 | def testCreateRefreshToken(self): 67 | self.assertEqual(self._myRefreshToken.refresh_token, 'rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W') 68 | self.assertEqual(self._myRefreshToken.expires_in, 1199) 69 | self.assertEqual(self._myRefreshToken.expires_at, '2014-03-13 15:44:59Z') 70 | 71 | """ Test the isExpired calculation.""" 72 | 73 | def testIsExpired(self): 74 | self._myRefreshToken.expires_at = '2014-01-01 12:00:00Z' 75 | self.assertTrue(self._myRefreshToken.is_expired()) 76 | 77 | self._myRefreshToken.expires_at = '2099-01-01 12:00:00Z' 78 | self.assertFalse(self._myRefreshToken.is_expired()) 79 | 80 | """Test that the string representation of the class is complete.""" 81 | 82 | def testStringRepresenationOfClass(self): 83 | self.assertEqual(str(self._myRefreshToken), 84 | 'refresh_token: rt_25fXauhJC09E4kwFxcf4TREkTnaRYWHgJA0W\n' + 85 | 'expires_in: 1199\n' + 86 | 'expires_at: 2014-03-13 15:44:59Z\n' 87 | ) 88 | 89 | 90 | def main(): 91 | unittest.main() 92 | 93 | 94 | if __name__ == '__main__': 95 | main() -------------------------------------------------------------------------------- /tests/user_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.user_test 18 | 19 | import unittest 20 | from authliboclc import user 21 | 22 | 23 | class UserTests(unittest.TestCase): 24 | def setUp(self): 25 | self._user = user.User( 26 | principal_id= '8eaa9f92-3951-431c-975a-e5dt26b7d232', 27 | principal_idns= 'urn:oclc:wms:da', 28 | authenticating_institution_id= '128807' 29 | ) 30 | 31 | """ Test that the creation of the user object incorrect parameters raise exceptions.""" 32 | 33 | def testCreateUserExceptions(self): 34 | with self.assertRaises(user.InvalidParameter): 35 | user.User() 36 | with self.assertRaises(user.InvalidParameter): 37 | user.User(**{ 38 | 'principal_idns': 'urn:oclc:wms:da', 39 | 'authenticating_institution_id': '128807' 40 | }) 41 | with self.assertRaises(user.InvalidParameter): 42 | user.User(**{ 43 | 'principal_id': '8eaa9f92-3951-431c-975a-e5dt26b7d232', 44 | 'authenticating_institution_id': '128807' 45 | }) 46 | with self.assertRaises(user.InvalidParameter): 47 | user.User(**{ 48 | 'principal_id': '8eaa9f92-3951-431c-975a-e5dt26b7d232', 49 | 'principal_idns': 'urn:oclc:wms:da' 50 | }) 51 | with self.assertRaises(user.InvalidParameter): 52 | user.User(**{ 53 | 'principal_id': '', 54 | 'principal_idns': 'urn:oclc:wms:da', 55 | 'authenticating_institution_id': '128807' 56 | }) 57 | with self.assertRaises(user.InvalidParameter): 58 | user.User(**{ 59 | 'principal_id': '8eaa9f92-3951-431c-975a-e5dt26b7d232', 60 | 'principal_idns': '', 61 | 'authenticating_institution_id': '128807' 62 | }) 63 | with self.assertRaises(user.InvalidParameter): 64 | user.User(**{ 65 | 'principal_id': '8eaa9f92-3951-431c-975a-e5dt26b7d232', 66 | 'principal_idns': 'urn:oclc:wms:da', 67 | 'authenticating_institution_id': '' 68 | }) 69 | 70 | """ Make sure that parameters are saved properly for a correctly created user.""" 71 | 72 | def testCreateUser(self): 73 | self.assertEqual(self._user.principal_id, '8eaa9f92-3951-431c-975a-e5dt26b7d232') 74 | self.assertEqual(self._user.principal_idns, 'urn:oclc:wms:da') 75 | self.assertEqual(self._user.authenticating_institution_id, '128807') 76 | 77 | """Test that the string representation of the class is complete.""" 78 | 79 | def testStringRepresenationOfClass(self): 80 | self.assertEqual(str(self._user), 81 | 'principal_id: 8eaa9f92-3951-431c-975a-e5dt26b7d232\n' + 82 | 'principal_idns: urn:oclc:wms:da\n' + 83 | 'authenticating_institution_id: 128807\n' 84 | ) 85 | 86 | 87 | def main(): 88 | unittest.main() 89 | 90 | 91 | if __name__ == '__main__': 92 | main() -------------------------------------------------------------------------------- /tests/wskey_test.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Copyright 2014 OCLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | # use this file except in compliance with the License. You may obtain a copy of 6 | # the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | ############################################################################### 16 | 17 | # to run this test from the command line: python -m tests.wskey_test 18 | 19 | import unittest 20 | from authliboclc import wskey, user 21 | 22 | 23 | class WskeyTests(unittest.TestCase): 24 | """ Make sure that valid options aren't changed by accident.""" 25 | 26 | """ Create a mock wskey object.""" 27 | 28 | def setUp(self): 29 | self._my_wskey = wskey.Wskey(**{ 30 | 'key': 'CancdeDMjFO9vnzkDrB6WJg1UnyTnkn8lLupLKygr0U1KJLiaAittuVjGRywCDdrsxahv2sbjgKq6hLM', 31 | 'secret': 'YeZfIJdGYUeatxQOjekRZw==', 32 | 'options': { 33 | 'redirect_uri': 'http://www.oclc.org/test', 34 | 'services': [ 35 | 'WMS_NCIP', 36 | 'WMS_ACQ' 37 | ] 38 | } 39 | }) 40 | 41 | """ Verify valid options list is correct.""" 42 | 43 | def testValidOptions(self): 44 | self.assertEqual(wskey.Wskey.valid_options, ['redirect_uri', 'services']) 45 | 46 | """ Make sure WSKey creation with invalid parameters raises exceptions. """ 47 | 48 | def testCreateWskeyExceptions(self): 49 | with self.assertRaises(wskey.InvalidObject): 50 | wskey.Wskey('123ABC', '987', '') 51 | with self.assertRaises(wskey.InvalidParameter): 52 | wskey.Wskey('123ABC', '987', {'redirect_uri': '', 'services': ['one', 'two']}) 53 | with self.assertRaises(wskey.InvalidParameter): 54 | wskey.Wskey('123ABC', '987', {'redirect_uri': 'www.mylibrary123.org/myapp', 'services': ['one', 'two']}) 55 | with self.assertRaises(wskey.InvalidParameter): 56 | wskey.Wskey('123ABC', '987', {'redirect_uri': 'http://www.mylibrary123.org/myapp', 'services': None}) 57 | with self.assertRaises(wskey.InvalidParameter): 58 | wskey.Wskey('123ABC', '987', {'redirect_uri': 'http://www.mylibrary123.org/myapp', 'services': ''}) 59 | with self.assertRaises(wskey.InvalidParameter): 60 | wskey.Wskey('123ABC', '987', {'redirect_uri': 'http://www.mylibrary123.org/myapp', 'services': []}) 61 | 62 | """ Check the parameters of the mock wskey to see if it was created properly. """ 63 | 64 | def testCreateWskey(self): 65 | self.assertEqual(self._my_wskey.key, 66 | 'CancdeDMjFO9vnzkDrB6WJg1UnyTnkn8lLupLKygr0U1KJLiaAittuVjGRywCDdrsxahv2sbjgKq6hLM') 67 | self.assertEqual(self._my_wskey.secret, 'YeZfIJdGYUeatxQOjekRZw==') 68 | self.assertEqual(self._my_wskey.redirect_uri, 'http://www.oclc.org/test') 69 | self.assertEqual(self._my_wskey.services, ['WMS_NCIP', 'WMS_ACQ']) 70 | 71 | """ Verify that the generation of a login URL from a WSKey is correct.""" 72 | 73 | def testGetLoginUrl(self): 74 | expectedResult = ( 75 | 'https://authn.sd00.worldcat.org/oauth2/authorizeCode?' + 76 | 'authenticatingInstitutionId=128807' + 77 | '&client_id=CancdeDMjFO9vnzkDrB6WJg1UnyTnkn8lLupLKygr0U1KJLiaAittuVjGRywCDdrsxahv2sbjgKq6hLM' + 78 | '&contextInstitutionId=128808' + 79 | '&redirect_uri=http%3A%2F%2Fwww.oclc.org%2Ftest' + 80 | '&response_type=code' + 81 | '&scope=WMS_NCIP WMS_ACQ') 82 | 83 | self.assertEqual(self._my_wskey.get_login_url( 84 | authenticating_institution_id='128807', 85 | context_institution_id='128808' 86 | ), expectedResult) 87 | 88 | 89 | """ Verify that attempts to get an Access Token with invalid parameters raises exceptions""" 90 | 91 | def testGetAccessTokenWithAuthCode(self): 92 | with self.assertRaises(wskey.InvalidParameter): 93 | self._my_wskey.get_access_token_with_auth_code( 94 | authenticating_institution_id=None, 95 | context_institution_id='128808', 96 | code='unknown', 97 | ) 98 | with self.assertRaises(wskey.InvalidParameter): 99 | self._my_wskey.get_access_token_with_auth_code( 100 | authenticating_institution_id='', 101 | context_institution_id='128808', 102 | code='unknown', 103 | ) 104 | with self.assertRaises(wskey.InvalidParameter): 105 | self._my_wskey.get_access_token_with_auth_code( 106 | authenticating_institution_id='128807', 107 | context_institution_id=None, 108 | code='unknown', 109 | ) 110 | with self.assertRaises(wskey.InvalidParameter): 111 | self._my_wskey.get_access_token_with_auth_code( 112 | authenticating_institution_id='128807', 113 | context_institution_id='', 114 | code='unknown', 115 | ) 116 | with self.assertRaises(wskey.InvalidParameter): 117 | self._my_wskey.get_access_token_with_auth_code( 118 | authenticating_institution_id='128807', 119 | context_institution_id='128808', 120 | code=None, 121 | ) 122 | 123 | """ Verify that attempts to get access token for client credentials grant raises exceptions.""" 124 | 125 | def testGetAccessTokenWithClientCredentials(self): 126 | with self.assertRaises(wskey.InvalidParameter): 127 | self._my_wskey.get_access_token_with_client_credentials( 128 | authenticating_institution_id=None, 129 | context_institution_id='12808' 130 | ) 131 | with self.assertRaises(wskey.InvalidParameter): 132 | self._my_wskey.get_access_token_with_client_credentials( 133 | authenticating_institution_id='', 134 | context_institution_id='12808' 135 | ) 136 | with self.assertRaises(wskey.InvalidParameter): 137 | self._my_wskey.get_access_token_with_client_credentials( 138 | authenticating_institution_id='128807', 139 | context_institution_id=None 140 | ) 141 | with self.assertRaises(wskey.InvalidParameter): 142 | self._my_wskey.get_access_token_with_client_credentials( 143 | authenticating_institution_id='128807', 144 | context_institution_id='' 145 | ) 146 | 147 | """ Verify that the calculation of an Authentication Header is correct. """ 148 | 149 | def testget_hmac_signature(self): 150 | self._my_wskey.debug_time_stamp = '1392239490' 151 | self._my_wskey.debug_nonce = '0x16577027' 152 | 153 | AuthenticationHeader = self._my_wskey.get_hmac_signature( 154 | method='GET', 155 | request_url=('https://worldcat.org/bib/data/1039085' + 156 | '?inst=128807' + 157 | '&classificationScheme=LibraryOfCongress' + 158 | '&holdingLibraryCode=MAIN'), 159 | options={ 160 | 'user': user.User( 161 | principal_id='8eaa9f92-3951-431c-975a-e5dt26b7d232', 162 | principal_idns='urn:oclc:wms:ad', 163 | authenticating_institution_id='128807'), 164 | 'auth_params': {'userid': 'tasty', 'password': 'buffet'} 165 | } 166 | ) 167 | 168 | expected = ('http://www.worldcat.org/wskey/v2/hmac/v1 ' + 169 | 'clientID="CancdeDMjFO9vnzkDrB6WJg1UnyTnkn8lLupLKygr0U1KJLiaAittuVjGRywCDdrsxahv2sbjgKq6hLM",' + 170 | 'timestamp="1392239490",' + 171 | 'nonce="0x16577027",' + 172 | 'signature="+RFPwih61799mpNBJqGhhSbQgd/JRfEinYv81z+CwRY=",' + 173 | 'password="buffet",' + 174 | 'principalID="8eaa9f92-3951-431c-975a-e5dt26b7d232",' + 175 | 'principalIDNS="urn:oclc:wms:ad",' + 176 | 'userid="tasty"' 177 | ) 178 | 179 | self.assertEqual(AuthenticationHeader, expected) 180 | 181 | """ Verify the correctness of the hashing algorithm. """ 182 | 183 | def testSignRequest(self): 184 | signature = self._my_wskey.sign_request( 185 | method='GET', 186 | request_url=('https://worldcat.org/bib/data/1039085' + 187 | '?inst=128807' + 188 | '&classificationScheme=LibraryOfCongress' + 189 | '&holdingLibraryCode=MAIN'), 190 | timestamp='1392239490', 191 | nonce='0x16577027' 192 | ) 193 | 194 | expected = '+RFPwih61799mpNBJqGhhSbQgd/JRfEinYv81z+CwRY=' 195 | 196 | self.assertEqual(signature, expected) 197 | 198 | """ Verify that a Normalized Request is generated properly. """ 199 | 200 | def testNormalizedRequest(self): 201 | normalized_request = self._my_wskey.normalize_request( 202 | method='GET', 203 | request_url=('https://worldcat.org/bib/data/1039085' + 204 | '?inst=128807' + 205 | '&classificationScheme=LibraryOfCongress' + 206 | '&holdingLibraryCode=MAIN'), 207 | timestamp='1392236038', 208 | nonce='0x66a29eea') 209 | 210 | expected = ('CancdeDMjFO9vnzkDrB6WJg1UnyTnkn8lLupLKygr0U1KJLiaAittuVjGRywCDdrsxahv2sbjgKq6hLM\n' + 211 | '1392236038\n' + 212 | '0x66a29eea\n' + 213 | '\n' + 214 | 'GET\n' + 215 | 'www.oclc.org\n' + 216 | '443\n' + 217 | '/wskey\n' + 218 | 'classificationScheme=LibraryOfCongress\n' + 219 | 'holdingLibraryCode=MAIN\n' + 220 | 'inst=128807\n') 221 | 222 | self.assertEqual(normalized_request, expected) 223 | 224 | """ Verify that a normalized request is produced correctly if there are no query parameters """ 225 | 226 | def testNormalizedRequestWithNoQueryParameters(self): 227 | normalized_request = self._my_wskey.normalize_request( 228 | method='GET', 229 | request_url='https://worldcat.org/bib/data/1039085', 230 | timestamp='1392236038', 231 | nonce='0x66a29eea') 232 | 233 | expected = ('CancdeDMjFO9vnzkDrB6WJg1UnyTnkn8lLupLKygr0U1KJLiaAittuVjGRywCDdrsxahv2sbjgKq6hLM\n' + 234 | '1392236038\n' + 235 | '0x66a29eea\n' + 236 | '\n' + 237 | 'GET\n' + 238 | 'www.oclc.org\n' + 239 | '443\n' + 240 | '/wskey\n') 241 | 242 | self.assertEqual(normalized_request, expected) 243 | 244 | """ If User and Auth parameters exist, make sure they are added to the Authentication Header. """ 245 | 246 | def testadd_auth_params(self): 247 | my_user = user.User( 248 | principal_id='8eaa9f92-3951-431c-975a-e5dt26b7d232', 249 | principal_idns='urn:oclc:wms:ad', 250 | authenticating_institution_id='128807' 251 | ) 252 | 253 | auth_params = {'userid': 'tasty', 'password': 'buffet'} 254 | 255 | """ Both User and Auth params exists """ 256 | self.assertEqual(self._my_wskey.add_auth_params(user=my_user, auth_params=auth_params), 257 | ('password="buffet",' + 258 | 'principalID="8eaa9f92-3951-431c-975a-e5dt26b7d232",' + 259 | 'principalIDNS="urn:oclc:wms:ad",' + 260 | 'userid="tasty"')) 261 | 262 | """ Just User params """ 263 | self.assertEqual(self._my_wskey.add_auth_params(user=my_user, auth_params=None), 264 | 'principalID="8eaa9f92-3951-431c-975a-e5dt26b7d232",principalIDNS="urn:oclc:wms:ad"') 265 | 266 | """ Just Auth params """ 267 | self.assertEqual(self._my_wskey.add_auth_params(user=None, auth_params=auth_params), 268 | 'password="buffet",userid="tasty"') 269 | 270 | """ Neither User nor Auth params exist.""" 271 | self.assertEqual(self._my_wskey.add_auth_params(user=None, auth_params=None), '') 272 | 273 | """Test that the string representation of the class is complete.""" 274 | 275 | def testStringRepresenationOfClass(self): 276 | self.assertEqual(str(self._my_wskey), 277 | "key: CancdeDMjFO9vnzkDrB6WJg1UnyTnkn8lLupLKygr0U1KJLiaAittuVjGRywCDdrsxahv2sbjgKq6hLM\n" + 278 | "secret: YeZfIJdGYUeatxQOjekRZw==\n" + 279 | "redirect_uri: http://www.oclc.org/test\n" + 280 | "services: ['WMS_NCIP', 'WMS_ACQ']\n" + 281 | "debug_time_stamp: None\n" + 282 | "debug_nonce: None\n" + 283 | "body_hash: None\n" + 284 | "auth_params: None\n" + 285 | "user:\n" + 286 | "None") 287 | 288 | 289 | def main(): 290 | unittest.main() 291 | 292 | 293 | if __name__ == '__main__': 294 | main() --------------------------------------------------------------------------------