├── .gitattributes ├── .gitignore ├── LICENSE.TXT ├── README.md ├── clientcreds ├── __init__.py ├── admin.py ├── certificates │ └── Get-KeyCredentials.ps1 ├── clientcredhelper.py ├── clientreg.py ├── migrations │ └── __init__.py ├── models.py ├── o365service.py ├── templates │ └── clientcreds │ │ └── mail.html ├── tests.py ├── urls.py └── views.py ├── manage.py ├── python_clientcred ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── requirements.txt └── setup_project.bat /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | # Django stuff 46 | debug.log 47 | *.sqlite3 48 | /clientcreds/__pycache__/* 49 | !/clientcreds/migrations 50 | /clientcreds/migrations/* 51 | !/clientcreds/migrations/__init__.py 52 | /python_clientcred/__pycache__/* 53 | 54 | # Certificates 55 | *.cer 56 | *.pfx 57 | *.pem 58 | !/clientcreds/certificates 59 | /clientcreds/certificates/* 60 | !/clientcreds/certificates/Get-KeyCredentials.ps1 61 | 62 | # Eclipse 63 | .project 64 | .pydevproject 65 | 66 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | python_clientcred https://github.com/jasonjoh/python_clientcred 2 | 3 | Copyright (c) Microsoft Corporation 4 | All rights reserved. 5 | 6 | MIT License: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | ""Software""), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Client Credentials Sample # 2 | 3 | This is a very rough sample illustrating how to implement the client credential OAuth2 flow in a Python/Django app. The app allows an administrator to logon and give consent, and then allows the user to view the first 10 emails in the inbox of any user in the organization. 4 | 5 | ## Required software ## 6 | 7 | - [Python 3.4.2](https://www.python.org/downloads/) 8 | - [Django 1.7.1](https://docs.djangoproject.com/en/1.7/intro/install/) 9 | - [Requests: HTTP for Humans](http://docs.python-requests.org/en/latest/) 10 | - [Python-RSA](http://stuvel.eu/rsa) 11 | 12 | ## Running the sample ## 13 | 14 | It's assumed that you have Python and Django installed before starting. Windows users should add the Python install directory and Scripts subdirectory to their PATH environment variable. 15 | 16 | 1. Download or fork the sample project. 17 | 1. Open your command prompt or shell to the directory where `manage.py` is located. 18 | 1. If you can run BAT files, run setup_project.bat. If not, run the three commands in the file manually. The last command prompts you to create a superuser, which you'll use later to logon. 19 | 1. Install the Requests: HTTP for Humans module from the command line: `pip install requests` 20 | 1. Install the Python-RSA module from the command line: `pip install rsa` 21 | 1. [Register the app in Azure Active Directory](https://github.com/jasonjoh/office365-azure-guides/blob/master/RegisterAnAppInAzure.md). The app should be registered as a web app with a Sign-on URL of "http://127.0.0.1:8000/", and should be given the permission to "Read mail in all mailboxes in the organization", which is available in the "Application Permissions" dropdown. 22 | 1. Configure an X509 certificate for your app following the directions [here](https://blogs.msdn.microsoft.com/exchangedev/2015/01/21/building-daemon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow/). 23 | > If you're using OpenSSL, you can try [these instructions](https://gist.github.com/carlopires/de085999dc69a13efe60). (Thanks to Carlo!) 24 | 1. Extract the private key in RSA format from your certificate and save it to a PEM file. (I used OpenSSL to do this). 25 | `openssl pkcs12 -in -nodes -nocerts -passin pass: | openssl rsa -out appcert.pem` 26 | 1. Edit the `.\clientcreds\clientreg.py` file. 27 | 1. Copy the client ID for your app obtained during app registration and paste it as the value for the `id` variable. 28 | 1. Enter the full path to the PEM file containing the RSA private key as the value for the `cert_file_path` variable. 29 | 1. Copy the thumbprint value of your certificate (same value used for the `customKeyIdentifier` value in the application manifest) and paste it as the value for the `cert_file_thumbprint` variable. 30 | 1. Save the file. 31 | 1. Start the development server: `python manage.py runserver` 32 | 1. You should see output like: 33 | `Performing system checks...` 34 | 35 | `System check identified no issues (0 silenced).` 36 | `December 18, 2014 - 12:36:32` 37 | `Django version 1.7.1, using settings 'pythoncontacts.settings'` 38 | `Starting development server at http://127.0.0.1:8000/` 39 | `Quit the server with CTRL-BREAK.` 40 | 1. Use your browser to go to http://127.0.0.1:8000/. 41 | 1. You should now be prompted to login with an adminstrative account. Click the link to do so and login with an Office 365 tenant administrator account. 42 | 2. You should be redirect to the mail page. Enter a valid email address for a user in the Office 365 tenant and click the "Set User" button. The most recent 10 emails for the user should load on the page. 43 | 44 | ## Copyright ## 45 | 46 | Copyright (c) Microsoft. All rights reserved. 47 | 48 | ---------- 49 | Connect with me on Twitter [@JasonJohMSFT](https://twitter.com/JasonJohMSFT) 50 | 51 | Follow the [Exchange Dev Blog](http://blogs.msdn.com/b/exchangedev/) 52 | -------------------------------------------------------------------------------- /clientcreds/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_clientcred/47ae1fc9f7762400e2a45ddf60b8005264293db8/clientcreds/__init__.py -------------------------------------------------------------------------------- /clientcreds/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /clientcreds/certificates/Get-KeyCredentials.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | # This script automates the steps outlined in 3 | # http://blogs.msdn.com/b/exchangedev/archive/2015/01/21/building-demon-or-service-apps-with-office-365-mail-calendar-and-contacts-apis-oauth2-client-credential-flow.aspx 4 | # For getting the base64 cert value and thumprint from an X509 .cer file. 5 | Param([string]$certFilePath) 6 | 7 | $outFile = ".\keyCredentials.txt" 8 | Clear-Content $outFile 9 | 10 | Write-Host "Loading certificate from" $certFilePath 11 | 12 | $cer = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 13 | $cer.Import($certFilePath) 14 | 15 | $data = $cer.GetRawCertData() 16 | $base64Value = [System.Convert]::ToBase64String($data) 17 | 18 | $hash = $cer.GetCertHash() 19 | $base64Thumbprint = [System.Convert]::ToBase64String($hash) 20 | 21 | $keyid = [System.Guid]::NewGuid().ToString() 22 | 23 | Add-Content $outFile '"keyCredentials": [' 24 | Add-Content $outFile ' {' 25 | $stringToAdd = ' "customKeyIdentifier": "' + $base64Thumbprint + '",' 26 | Add-Content $outFile $stringToAdd 27 | $stringToAdd = ' "keyId": "' + $keyid + '",' 28 | Add-Content $outFile $stringToAdd 29 | Add-Content $outFile ' "type": "AsymmetricX509Cert",' 30 | Add-Content $outFile ' "usage": "Verify",' 31 | $stringToAdd = ' "value": "' + $base64Value + '"' 32 | Add-Content $outFile $stringToAdd 33 | Add-Content $outFile ' }' 34 | Add-Content $outFile '],' 35 | 36 | Write-Host "Key Credential entry created in" $outFile 37 | 38 | # MIT License: 39 | 40 | # Permission is hereby granted, free of charge, to any person obtaining 41 | # a copy of this software and associated documentation files (the 42 | # ""Software""), to deal in the Software without restriction, including 43 | # without limitation the rights to use, copy, modify, merge, publish, 44 | # distribute, sublicense, and/or sell copies of the Software, and to 45 | # permit persons to whom the Software is furnished to do so, subject to 46 | # the following conditions: 47 | 48 | # The above copyright notice and this permission notice shall be 49 | # included in all copies or substantial portions of the Software. 50 | 51 | # THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 52 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 53 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 54 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 55 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 56 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 57 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /clientcreds/clientcredhelper.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | from urllib.parse import quote, urlencode 3 | import requests 4 | import json 5 | import base64 6 | import logging 7 | import uuid 8 | import datetime 9 | import time 10 | import rsa 11 | from clientcreds.clientreg import client_registration 12 | 13 | # Used for debug logging 14 | logger = logging.getLogger('clientcreds') 15 | 16 | # Constant strings for OAuth2 flow 17 | # The OAuth authority 18 | authority = 'https://login.microsoftonline.com' 19 | 20 | # The authorize URL that initiates the OAuth2 client credential flow for admin consent 21 | authorize_url = '{0}{1}'.format(authority, '/common/oauth2/authorize?{0}') 22 | 23 | # The token issuing endpoint 24 | token_url = '{0}{1}'.format(authority, '/{0}/oauth2/token') 25 | 26 | # Set to False to bypass SSL verification 27 | # Useful for capturing API calls in Fiddler 28 | verifySSL = True 29 | 30 | # Plugs in client ID and redirect URL to the authorize URL 31 | # App will call this to get a URL to redirect the user for sign in 32 | def get_client_cred_authorization_url(redirect_uri, resource): 33 | logger.debug('Entering get_client_cred_authorization_url.') 34 | logger.debug(' redirect_uri: {0}'.format(redirect_uri)) 35 | logger.debug(' resource: {0}'.format(resource)) 36 | 37 | # Create a GUID for the nonce value 38 | nonce = str(uuid.uuid4()) 39 | 40 | params = { 'client_id': client_registration.client_id(), 41 | 'redirect_uri': redirect_uri, 42 | 'response_type': 'code id_token', 43 | 'scope': 'openid', 44 | 'nonce': nonce, 45 | 'prompt': 'admin_consent', 46 | 'response_mode': 'form_post', 47 | 'resource': resource, 48 | } 49 | 50 | authorization_url = authorize_url.format(urlencode(params)) 51 | 52 | logger.debug('Authorization url: {0}'.format(authorization_url)) 53 | logger.debug('Leaving get_client_cred_authorization_url.') 54 | return authorization_url 55 | 56 | def get_access_token(id_token, redirect_uri, resource): 57 | # Get the tenant ID from the id token 58 | parsed_token = parse_token(id_token) 59 | tenantId = parsed_token['tid'] 60 | if (tenantId): 61 | logger.debug('Tenant ID: {0}'.format(tenantId)) 62 | get_token_url = token_url.format(tenantId) 63 | logger.debug('Token request url: '.format(get_token_url)) 64 | 65 | # Build the client assertion 66 | assertion = get_client_assertion(get_token_url) 67 | 68 | # Construct the required post data 69 | # See http://www.cloudidentity.com/blog/2015/02/06/requesting-an-aad-token-with-a-certificate-without-adal/ 70 | post_form = { 71 | 'resource': resource, 72 | 'client_id': client_registration.client_id(), 73 | 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 74 | 'client_assertion': assertion, 75 | 'grant_type': 'client_credentials', 76 | 'redirect_uri': redirect_uri, 77 | } 78 | 79 | r = requests.post(get_token_url, data = post_form, verify = verifySSL) 80 | logger.debug('Received response from token endpoint.') 81 | logger.debug(r.json()) 82 | return r.json() 83 | else: 84 | logger.debug('Could not parse token: {0}'.format(token)) 85 | return '' 86 | 87 | def get_client_assertion(get_token_url): 88 | # Create a GUID for the jti claim 89 | id = str(uuid.uuid4()) 90 | 91 | thumbprint = client_registration.cert_thumbprint() 92 | 93 | # Build the header 94 | client_assertion_header = { 95 | 'alg': 'RS256', 96 | 'x5t': thumbprint, 97 | } 98 | 99 | # Create a UNIX epoch time value for now - 5 minutes 100 | # Why -5 minutes? To allow for time skew between the local machine 101 | # and the server. 102 | now = int(time.time()) - 300 103 | # Create a UNIX epoch time value for now + 10 minutes 104 | ten_mins_from_now = now + 900 105 | 106 | # Build the payload per 107 | # http://www.cloudidentity.com/blog/2015/02/06/requesting-an-aad-token-with-a-certificate-without-adal/ 108 | client_assertion_payload = { 'sub': client_registration.client_id(), 109 | 'iss': client_registration.client_id(), 110 | 'jti': id, 111 | 'exp': ten_mins_from_now, 112 | 'nbf': now, 113 | 'aud': get_token_url, #.replace('/', '\\/'), 114 | } 115 | 116 | string_assertion = json.dumps(client_assertion_payload) 117 | logger.debug('Assertion: {0}'.format(string_assertion)) 118 | 119 | # Generate the stringified header blob 120 | assertion_blob = get_assertion_blob(client_assertion_header, client_assertion_payload) 121 | 122 | # Sign the data 123 | signature = get_signature(assertion_blob) 124 | 125 | # Concatenate the blob with the signature 126 | # Final product should look like: 127 | # .. 128 | client_assertion = '{0}.{1}'.format(assertion_blob, signature) 129 | logger.debug('CLIENT ASSERTION: {0}'.format(client_assertion)) 130 | 131 | return client_assertion 132 | 133 | def get_assertion_blob(header, payload): 134 | # Generate the blob, which looks like: 135 | # . 136 | header_string = json.dumps(header).encode('utf-8') 137 | encoded_header = base64.urlsafe_b64encode(header_string).decode('utf-8').strip('=') 138 | logger.debug('ENCODED HEADER: {0}'.format(encoded_header)) 139 | 140 | payload_string = json.dumps(payload).encode('utf-8') 141 | encoded_payload = base64.urlsafe_b64encode(payload_string).decode('utf-8').strip('=') 142 | logger.debug('ENCODED PAYLOAD: {0}'.format(encoded_payload)) 143 | 144 | assertion_blob = '{0}.{1}'.format(encoded_header, encoded_payload) 145 | return assertion_blob 146 | 147 | def get_signature(message): 148 | # Open the file containing the private key 149 | pemFile = open(client_registration.cert_path(), 'rb') 150 | keyData = pemFile.read() 151 | logger.debug('KEY FILE: {0}'.format(keyData)) 152 | 153 | privKey = rsa.PrivateKey.load_pkcs1(keyData) 154 | 155 | # Sign the data with the private key 156 | signature = rsa.sign(message.encode('utf-8'), privKey, 'SHA-256') 157 | 158 | logger.debug('SIGNATURE: {0}'.format(signature)) 159 | 160 | # Base64-encode the signature and remove any trailing '=' 161 | encoded_signature = base64.urlsafe_b64encode(signature) 162 | encoded_signature_string = encoded_signature.decode('utf-8').strip('=') 163 | 164 | logger.debug('ENCODED SIGNATURE: {0}'.format(encoded_signature_string)) 165 | return encoded_signature_string 166 | 167 | # This function takes the base64-encoded token value and breaks 168 | # it into header and payload, base64-decodes the payload, then 169 | # loads that into a JSON object. 170 | def parse_token(encoded_token): 171 | logger.debug('Entering parse_token.') 172 | logger.debug(' encoded_token: {0}'.format(encoded_token)) 173 | 174 | try: 175 | # First split the token into header and payload 176 | token_parts = encoded_token.split('.') 177 | 178 | # Header is token_parts[0] 179 | # Payload is token_parts[1] 180 | logger.debug('Token part to decode: {0}'.format(token_parts[1])) 181 | 182 | decoded_token = decode_token_part(token_parts[1]) 183 | logger.debug('Decoded token part: {0}'.format(decoded_token)) 184 | logger.debug('Leaving parse_token.') 185 | return json.loads(decoded_token) 186 | except: 187 | return 'Invalid token value: {0}'.format(encoded_token) 188 | 189 | def decode_token_part(base64data): 190 | logger.debug('Entering decode_token_part.') 191 | logger.debug(' base64data: {0}'.format(base64data)) 192 | 193 | # base64 strings should have a length divisible by 4 194 | # If this one doesn't, add the '=' padding to fix it 195 | leftovers = len(base64data) % 4 196 | logger.debug('String length % 4 = {0}'.format(leftovers)) 197 | if leftovers == 2: 198 | base64data += '==' 199 | elif leftovers == 3: 200 | base64data += '=' 201 | 202 | logger.debug('String with padding added: {0}'.format(base64data)) 203 | decoded = base64.b64decode(base64data) 204 | logger.debug('Decoded string: {0}'.format(decoded)) 205 | logger.debug('Leaving decode_token_part.') 206 | return decoded.decode('utf-8') 207 | 208 | # MIT License: 209 | 210 | # Permission is hereby granted, free of charge, to any person obtaining 211 | # a copy of this software and associated documentation files (the 212 | # ""Software""), to deal in the Software without restriction, including 213 | # without limitation the rights to use, copy, modify, merge, publish, 214 | # distribute, sublicense, and/or sell copies of the Software, and to 215 | # permit persons to whom the Software is furnished to do so, subject to 216 | # the following conditions: 217 | 218 | # The above copyright notice and this permission notice shall be 219 | # included in all copies or substantial portions of the Software. 220 | 221 | # THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 222 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 223 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 224 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 225 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 226 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 227 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /clientcreds/clientreg.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | 3 | # The client ID (register app in Azure AD to get this value) 4 | id = 'YOUR CLIENT ID' 5 | 6 | # The full path to your private key file. This file should be in RSA 7 | # private key format. 8 | cert_file_path = 'PATH TO YOUR PRIVATE KEY PEM FILE' 9 | 10 | # The thumbprint for the certificate that corresponds to your private key. 11 | cert_file_thumbprint = 'CERTIFICATE THUMBPRINT' 12 | 13 | class client_registration: 14 | @staticmethod 15 | def client_id(): 16 | return id 17 | 18 | @staticmethod 19 | def cert_path(): 20 | return cert_file_path 21 | 22 | @staticmethod 23 | def cert_thumbprint(): 24 | return cert_file_thumbprint 25 | 26 | # MIT License: 27 | 28 | # Permission is hereby granted, free of charge, to any person obtaining 29 | # a copy of this software and associated documentation files (the 30 | # ""Software""), to deal in the Software without restriction, including 31 | # without limitation the rights to use, copy, modify, merge, publish, 32 | # distribute, sublicense, and/or sell copies of the Software, and to 33 | # permit persons to whom the Software is furnished to do so, subject to 34 | # the following conditions: 35 | 36 | # The above copyright notice and this permission notice shall be 37 | # included in all copies or substantial portions of the Software. 38 | 39 | # THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 40 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 41 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 42 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 43 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 44 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 45 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /clientcreds/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_clientcred/47ae1fc9f7762400e2a45ddf60b8005264293db8/clientcreds/migrations/__init__.py -------------------------------------------------------------------------------- /clientcreds/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /clientcreds/o365service.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | from urllib.parse import quote 3 | import requests 4 | import json 5 | import base64 6 | import logging 7 | import uuid 8 | import datetime 9 | 10 | # Used for debug logging 11 | logger = logging.getLogger('clientcreds') 12 | 13 | # Set to False to bypass SSL verification 14 | # Useful for capturing API calls in Fiddler 15 | verifySSL = True 16 | 17 | # Generic API Sending 18 | def make_api_call(method, url, token, payload = None): 19 | # Send these headers with all API calls 20 | headers = { 'User-Agent' : 'python_clientcred/1.0', 21 | 'Authorization' : 'Bearer {0}'.format(token), 22 | 'Accept' : 'application/json' } 23 | 24 | # Use these headers to instrument calls. Makes it easier 25 | # to correlate requests and responses in case of problems 26 | # and is a recommended best practice. 27 | request_id = str(uuid.uuid4()) 28 | instrumentation = { 'client-request-id' : request_id, 29 | 'return-client-request-id' : 'true' } 30 | 31 | headers.update(instrumentation) 32 | 33 | response = None 34 | 35 | if (method.upper() == 'GET'): 36 | logger.debug('{0}: Sending request id: {1}'.format(datetime.datetime.now(), request_id)) 37 | response = requests.get(url, headers = headers, verify = verifySSL) 38 | elif (method.upper() == 'DELETE'): 39 | logger.debug('{0}: Sending request id: {1}'.format(datetime.datetime.now(), request_id)) 40 | response = requests.delete(url, headers = headers, verify = verifySSL) 41 | elif (method.upper() == 'PATCH'): 42 | headers.update({ 'Content-Type' : 'application/json' }) 43 | logger.debug('{0}: Sending request id: {1}'.format(datetime.datetime.now(), request_id)) 44 | response = requests.patch(url, headers = headers, data = payload, verify = verifySSL) 45 | elif (method.upper() == 'POST'): 46 | headers.update({ 'Content-Type' : 'application/json' }) 47 | logger.debug('{0}: Sending request id: {1}'.format(datetime.datetime.now(), request_id)) 48 | response = requests.post(url, headers = headers, data = payload, verify = verifySSL) 49 | 50 | if (not response is None): 51 | logger.debug('{0}: Request id {1} completed. Server id: {2}, Status: {3}'.format(datetime.datetime.now(), 52 | request_id, 53 | response.headers.get('request-id'), 54 | response.status_code)) 55 | 56 | return response 57 | 58 | 59 | # Contacts API # 60 | 61 | # Retrieves a set of contacts from the user's default contacts folder 62 | # parameters: 63 | # contact_endpoint: string. The URL to the Contacts API endpoint (https://outlook.office365.com/api/v1.0) 64 | # token: string. The access token 65 | # parameters: string. An optional string containing query parameters to filter, sort, etc. 66 | # http://msdn.microsoft.com/office/office365/APi/complex-types-for-mail-contacts-calendar#UseODataqueryparameters 67 | def get_contacts(contact_endpoint, token, parameters = None): 68 | logger.debug('Entering get_contacts.') 69 | logger.debug(' contact_endpoint: {0}'.format(contact_endpoint)) 70 | logger.debug(' token: {0}'.format(token)) 71 | if (not parameters is None): 72 | logger.debug(' parameters: {0}'.format(parameters)) 73 | 74 | get_contacts = '{0}/Me/Contacts'.format(contact_endpoint) 75 | 76 | if (not parameters is None): 77 | get_contacts = '{0}{1}'.format(get_contacts, parameters) 78 | 79 | r = make_api_call('GET', get_contacts, token) 80 | 81 | if (r.status_code == requests.codes.unauthorized): 82 | logger.debug('Leaving get_contacts.') 83 | return None 84 | 85 | logger.debug('Response: {0}'.format(r.json())) 86 | logger.debug('Leaving get_contacts.') 87 | return r.json() 88 | 89 | # Retrieves a single contact 90 | # parameters: 91 | # contact_endpoint: string. The URL to the Contacts API endpoint (https://outlook.office365.com/api/v1.0) 92 | # token: string. The access token 93 | # contact_id: string. The ID of the contact to retrieve. 94 | # parameters: string. An optional string containing query parameters to limit the properties returned. 95 | # http://msdn.microsoft.com/office/office365/APi/complex-types-for-mail-contacts-calendar#UseODataqueryparameters 96 | def get_contact_by_id(contact_endpoint, token, contact_id, parameters = None): 97 | logger.debug('Entering get_contact_by_id.') 98 | logger.debug(' contact_endpoint: {0}'.format(contact_endpoint)) 99 | logger.debug(' token: {0}'.format(token)) 100 | logger.debug(' contact_id: {0}'.format(contact_id)) 101 | if (not parameters is None): 102 | logger.debug(' parameters: {0}'.format(parameters)) 103 | 104 | get_contact = '{0}/Me/Contacts/{1}'.format(contact_endpoint, contact_id) 105 | 106 | if (not parameters is None and 107 | parameters != ''): 108 | get_contact = '{0}{1}'.format(get_contact, parameters) 109 | 110 | r = make_api_call('GET', get_contact, token) 111 | 112 | if (r.status_code == requests.codes.ok): 113 | logger.debug('Response: {0}'.format(r.json())) 114 | logger.debug('Leaving get_contact_by_id(.') 115 | return r.json() 116 | else: 117 | logger.debug('Leaving get_contact_by_id.') 118 | return None 119 | 120 | # Deletes a single contact 121 | # parameters: 122 | # contact_endpoint: string. The URL to the Contacts API endpoint (https://outlook.office365.com/api/v1.0) 123 | # token: string. The access token 124 | # contact_id: string. The ID of the contact to delete. 125 | def delete_contact(contact_endpoint, token, contact_id): 126 | logger.debug('Entering delete_contact.') 127 | logger.debug(' contact_endpoint: {0}'.format(contact_endpoint)) 128 | logger.debug(' token: {0}'.format(token)) 129 | logger.debug(' contact_id: {0}'.format(contact_id)) 130 | 131 | delete_contact = '{0}/Me/Contacts/{1}'.format(contact_endpoint, contact_id) 132 | 133 | r = make_api_call('DELETE', delete_contact, token) 134 | 135 | logger.debug('Leaving delete_contact.') 136 | 137 | return r.status_code 138 | 139 | # Updates a single contact 140 | # parameters: 141 | # contact_endpoint: string. The URL to the Contacts API endpoint (https://outlook.office365.com/api/v1.0) 142 | # token: string. The access token 143 | # contact_id: string. The ID of the contact to update. 144 | # update_payload: string. A JSON representation of the properties to update. 145 | def update_contact(contact_endpoint, token, contact_id, update_payload): 146 | logger.debug('Entering update_contact.') 147 | logger.debug(' contact_endpoint: {0}'.format(contact_endpoint)) 148 | logger.debug(' token: {0}'.format(token)) 149 | logger.debug(' contact_id: {0}'.format(contact_id)) 150 | logger.debug(' update_payload: {0}'.format(update_payload)) 151 | 152 | update_contact = '{0}/Me/Contacts/{1}'.format(contact_endpoint, contact_id) 153 | 154 | r = make_api_call('PATCH', update_contact, token, update_payload) 155 | 156 | logger.debug('Response: {0}'.format(r.json())) 157 | logger.debug('Leaving update_contact.') 158 | 159 | return r.status_code 160 | 161 | # Creates a contact 162 | # parameters: 163 | # contact_endpoint: string. The URL to the Contacts API endpoint (https://outlook.office365.com/api/v1.0) 164 | # token: string. The access token 165 | # contact_payload: string. A JSON representation of the new contact. 166 | def create_contact(contact_endpoint, token, contact_payload): 167 | logger.debug('Entering create_contact.') 168 | logger.debug(' contact_endpoint: {0}'.format(contact_endpoint)) 169 | logger.debug(' token: {0}'.format(token)) 170 | logger.debug(' contact_payload: {0}'.format(contact_payload)) 171 | 172 | create_contact = '{0}/Me/Contacts'.format(contact_endpoint) 173 | 174 | r = make_api_call('POST', create_contact, token, contact_payload) 175 | 176 | logger.debug('Response: {0}'.format(r.json())) 177 | logger.debug('Leaving create_contact.') 178 | 179 | return r.status_code 180 | 181 | # Mail API # 182 | 183 | # Retrieves a set of messages from the user's Inbox 184 | # parameters: 185 | # mail_endpoint: string. The URL to the Mail API endpoint (https://outlook.office365.com/api/v1.0) 186 | # token: string. The access token 187 | # parameters: string. An optional string containing query parameters to filter, sort, etc. 188 | # http://msdn.microsoft.com/office/office365/APi/complex-types-for-mail-contacts-calendar#UseODataqueryparameters 189 | def get_messages(mail_endpoint, token, parameters = None): 190 | logger.debug('Entering get_messages.') 191 | logger.debug(' mail_endpoint: {0}'.format(mail_endpoint)) 192 | logger.debug(' token: {0}'.format(token)) 193 | if (not parameters is None): 194 | logger.debug(' parameters: {0}'.format(parameters)) 195 | 196 | get_messages = '{0}/Me/Messages'.format(mail_endpoint) 197 | 198 | if (not parameters is None): 199 | get_messages = '{0}{1}'.format(get_messages, parameters) 200 | 201 | r = make_api_call('GET', get_messages, token) 202 | 203 | if (r.status_code == requests.codes.unauthorized): 204 | logger.debug('Leaving get_messages.') 205 | return None 206 | 207 | logger.debug('Response: {0}'.format(r.json())) 208 | logger.debug('Leaving get_messages.') 209 | return r.json() 210 | 211 | # Retrieves a single message 212 | # parameters: 213 | # mail_endpoint: string. The URL to the Mail API endpoint (https://outlook.office365.com/api/v1.0) 214 | # token: string. The access token 215 | # message_id: string. The ID of the message to retrieve. 216 | # parameters: string. An optional string containing query parameters to limit the properties returned. 217 | # http://msdn.microsoft.com/office/office365/APi/complex-types-for-mail-contacts-calendar#UseODataqueryparameters 218 | def get_message_by_id(mail_endpoint, token, message_id, parameters = None): 219 | logger.debug('Entering get_message_by_id.') 220 | logger.debug(' mail_endpoint: {0}'.format(mail_endpoint)) 221 | logger.debug(' token: {0}'.format(token)) 222 | logger.debug(' message_id: {0}'.format(message_id)) 223 | if (not parameters is None): 224 | logger.debug(' parameters: {0}'.format(parameters)) 225 | 226 | get_message = '{0}/Me/Messages/{1}'.format(mail_endpoint, message_id) 227 | 228 | if (not parameters is None and 229 | parameters != ''): 230 | get_message = '{0}{1}'.format(get_message, parameters) 231 | 232 | r = make_api_call('GET', get_message, token) 233 | 234 | if (r.status_code == requests.codes.ok): 235 | logger.debug('Response: {0}'.format(r.json())) 236 | logger.debug('Leaving get_message_by_id.') 237 | return r.json() 238 | else: 239 | logger.debug('Leaving get_message_by_id.') 240 | return None 241 | 242 | # Deletes a single message 243 | # parameters: 244 | # mail_endpoint: string. The URL to the Mail API endpoint (https://outlook.office365.com/api/v1.0) 245 | # token: string. The access token 246 | # message_id: string. The ID of the message to delete. 247 | def delete_message(mail_endpoint, token, message_id): 248 | logger.debug('Entering delete_message.') 249 | logger.debug(' mail_endpoint: {0}'.format(mail_endpoint)) 250 | logger.debug(' token: {0}'.format(token)) 251 | logger.debug(' message_id: {0}'.format(message_id)) 252 | 253 | delete_message = '{0}/Me/Messages/{1}'.format(mail_endpoint, message_id) 254 | 255 | r = make_api_call('DELETE', delete_message, token) 256 | 257 | logger.debug('Leaving delete_message.') 258 | 259 | return r.status_code 260 | 261 | # Updates a single message 262 | # parameters: 263 | # mail_endpoint: string. The URL to the Mail API endpoint (https://outlook.office365.com/api/v1.0) 264 | # token: string. The access token 265 | # message_id: string. The ID of the message to update. 266 | # update_payload: string. A JSON representation of the properties to update. 267 | def update_message(mail_endpoint, token, message_id, update_payload): 268 | logger.debug('Entering update_message.') 269 | logger.debug(' mail_endpoint: {0}'.format(mail_endpoint)) 270 | logger.debug(' token: {0}'.format(token)) 271 | logger.debug(' message_id: {0}'.format(message_id)) 272 | logger.debug(' update_payload: {0}'.format(update_payload)) 273 | 274 | update_message = '{0}/Me/Messages/{1}'.format(mail_endpoint, message_id) 275 | 276 | r = make_api_call('PATCH', update_message, token, update_payload) 277 | 278 | logger.debug('Response: {0}'.format(r.json())) 279 | logger.debug('Leaving update_message.') 280 | 281 | return r.status_code 282 | 283 | # Creates a message in the Drafts folder 284 | # parameters: 285 | # mail_endpoint: string. The URL to the Mail API endpoint (https://outlook.office365.com/api/v1.0) 286 | # token: string. The access token 287 | # message_payload: string. A JSON representation of the new message. 288 | def create_message(mail_endpoint, token, message_payload): 289 | logger.debug('Entering create_message.') 290 | logger.debug(' mail_endpoint: {0}'.format(mail_endpoint)) 291 | logger.debug(' token: {0}'.format(token)) 292 | logger.debug(' message_payload: {0}'.format(message_payload)) 293 | 294 | create_message = '{0}/Me/Messages'.format(mail_endpoint) 295 | 296 | r = make_api_call('POST', create_message, token, message_payload) 297 | 298 | logger.debug('Response: {0}'.format(r.json())) 299 | logger.debug('Leaving create_message.') 300 | 301 | return r.status_code 302 | 303 | # Sends an existing message in the Drafts folder 304 | # parameters: 305 | # mail_endpoint: string. The URL to the Mail API endpoint (https://outlook.office365.com/api/v1.0) 306 | # token: string. The access token 307 | # message_id: string. The ID of the message to send. 308 | def send_draft_message(mail_endpoint, token, message_id): 309 | logger.debug('Entering send_draft_message.') 310 | logger.debug(' mail_endpoint: {0}'.format(mail_endpoint)) 311 | logger.debug(' token: {0}'.format(token)) 312 | logger.debug(' message_id: {0}'.format(message_id)) 313 | 314 | send_message = '{0}/Me/Messages/{1}/Send'.format(mail_endpoint, message_id) 315 | 316 | r = make_api_call('POST', send_message, token) 317 | 318 | logger.debug('Leaving send_draft_message.') 319 | return r.status_code 320 | 321 | # Sends an new message in the Drafts folder 322 | # parameters: 323 | # mail_endpoint: string. The URL to the Mail API endpoint (https://outlook.office365.com/api/v1.0) 324 | # token: string. The access token 325 | # message_payload: string. The JSON representation of the message. 326 | # save_to_sentitems: boolean. True = save a copy in sent items, False = don't. 327 | def send_new_message(mail_endpoint, token, message_payload, save_to_sentitems = True): 328 | logger.debug('Entering send_new_message.') 329 | logger.debug(' mail_endpoint: {0}'.format(mail_endpoint)) 330 | logger.debug(' token: {0}'.format(token)) 331 | logger.debug(' message_payload: {0}'.format(message_payload)) 332 | logger.debug(' save_to_sentitems: {0}'.format(save_to_sentitems)) 333 | 334 | send_message = '{0}/Me/SendMail'.format(mail_endpoint) 335 | 336 | message_json = json.loads(message_payload) 337 | send_message_json = { 'Message' : message_json, 338 | 'SaveToSentItems' : str(save_to_sentitems).lower() } 339 | 340 | send_message_payload = json.dumps(send_message_json) 341 | 342 | logger.debug('Created payload for send: {0}'.format(send_message_payload)) 343 | 344 | r = make_api_call('POST', send_message, token, send_message_payload) 345 | 346 | logger.debug('Leaving send_new_message.') 347 | return r.status_code 348 | 349 | # Calendar API # 350 | 351 | # Retrieves a set of events from the user's Calendar 352 | # parameters: 353 | # calendar_endpoint: string. The URL to the Calendar API endpoint (https://outlook.office365.com/api/v1.0) 354 | # token: string. The access token 355 | # parameters: string. An optional string containing query parameters to filter, sort, etc. 356 | # http://msdn.microsoft.com/office/office365/APi/complex-types-for-mail-contacts-calendar#UseODataqueryparameters 357 | def get_events(calendar_endpoint, token, parameters = None): 358 | logger.debug('Entering get_events.') 359 | logger.debug(' calendar_endpoint: {0}'.format(calendar_endpoint)) 360 | logger.debug(' token: {0}'.format(token)) 361 | if (not parameters is None): 362 | logger.debug(' parameters: {0}'.format(parameters)) 363 | 364 | get_events = '{0}/Me/Events'.format(calendar_endpoint) 365 | 366 | if (not parameters is None): 367 | get_events = '{0}{1}'.format(get_events, parameters) 368 | 369 | r = make_api_call('GET', get_events, token) 370 | 371 | if (r.status_code == requests.codes.unauthorized): 372 | logger.debug('Leaving get_events.') 373 | return None 374 | 375 | logger.debug('Response: {0}'.format(r.json())) 376 | logger.debug('Leaving get_events.') 377 | return r.json() 378 | 379 | # Retrieves a single event 380 | # parameters: 381 | # calendar_endpoint: string. The URL to the Calendar API endpoint (https://outlook.office365.com/api/v1.0) 382 | # token: string. The access token 383 | # event_id: string. The ID of the event to retrieve. 384 | # parameters: string. An optional string containing query parameters to limit the properties returned. 385 | # http://msdn.microsoft.com/office/office365/APi/complex-types-for-mail-contacts-calendar#UseODataqueryparameters 386 | def get_event_by_id(calendar_endpoint, token, event_id, parameters = None): 387 | logger.debug('Entering get_event_by_id.') 388 | logger.debug(' calendar_endpoint: {0}'.format(calendar_endpoint)) 389 | logger.debug(' token: {0}'.format(token)) 390 | logger.debug(' event_id: {0}'.format(event_id)) 391 | if (not parameters is None): 392 | logger.debug(' parameters: {0}'.format(parameters)) 393 | 394 | get_event = '{0}/Me/Events/{1}'.format(calendar_endpoint, event_id) 395 | 396 | if (not parameters is None and 397 | parameters != ''): 398 | get_event = '{0}{1}'.format(get_event, parameters) 399 | 400 | r = make_api_call('GET', get_event, token) 401 | 402 | if (r.status_code == requests.codes.ok): 403 | logger.debug('Response: {0}'.format(r.json())) 404 | logger.debug('Leaving get_event_by_id.') 405 | return r.json() 406 | else: 407 | logger.debug('Leaving get_event_by_id.') 408 | return None 409 | 410 | # Deletes a single event 411 | # parameters: 412 | # calendar_endpoint: string. The URL to the Calendar API endpoint (https://outlook.office365.com/api/v1.0) 413 | # token: string. The access token 414 | # event_id: string. The ID of the event to delete. 415 | def delete_event(calendar_endpoint, token, event_id): 416 | logger.debug('Entering delete_event.') 417 | logger.debug(' calendar_endpoint: {0}'.format(calendar_endpoint)) 418 | logger.debug(' token: {0}'.format(token)) 419 | logger.debug(' event_id: {0}'.format(event_id)) 420 | 421 | delete_event = '{0}/Me/Events/{1}'.format(calendar_endpoint, event_id) 422 | 423 | r = make_api_call('DELETE', delete_event, token) 424 | 425 | logger.debug('Leaving delete_event.') 426 | 427 | return r.status_code 428 | 429 | # Updates a single event 430 | # parameters: 431 | # calendar_endpoint: string. The URL to the Calendar API endpoint (https://outlook.office365.com/api/v1.0) 432 | # token: string. The access token 433 | # event_id: string. The ID of the event to update. 434 | # update_payload: string. A JSON representation of the properties to update. 435 | def update_event(calendar_endpoint, token, event_id, update_payload): 436 | logger.debug('Entering update_event.') 437 | logger.debug(' calendar_endpoint: {0}'.format(calendar_endpoint)) 438 | logger.debug(' token: {0}'.format(token)) 439 | logger.debug(' event_id: {0}'.format(event_id)) 440 | logger.debug(' update_payload: {0}'.format(update_payload)) 441 | 442 | update_event = '{0}/Me/Events/{1}'.format(calendar_endpoint, event_id) 443 | 444 | r = make_api_call('PATCH', update_event, token, update_payload) 445 | 446 | logger.debug('Response: {0}'.format(r.json())) 447 | logger.debug('Leaving update_event.') 448 | 449 | return r.status_code 450 | 451 | # Creates an event in the Calendar 452 | # parameters: 453 | # calendar_endpoint: string. The URL to the Calendar API endpoint (https://outlook.office365.com/api/v1.0) 454 | # token: string. The access token 455 | # event_payload: string. A JSON representation of the new event. 456 | def create_event(calendar_endpoint, token, event_payload): 457 | logger.debug('Entering create_event.') 458 | logger.debug(' calendar_endpoint: {0}'.format(calendar_endpoint)) 459 | logger.debug(' token: {0}'.format(token)) 460 | logger.debug(' event_payload: {0}'.format(event_payload)) 461 | 462 | create_event = '{0}/Me/Events'.format(calendar_endpoint) 463 | 464 | r = make_api_call('POST', create_event, token, event_payload) 465 | 466 | logger.debug('Response: {0}'.format(r.json())) 467 | logger.debug('Leaving create_event.') 468 | 469 | return r.status_code 470 | 471 | # MIT License: 472 | 473 | # Permission is hereby granted, free of charge, to any person obtaining 474 | # a copy of this software and associated documentation files (the 475 | # ""Software""), to deal in the Software without restriction, including 476 | # without limitation the rights to use, copy, modify, merge, publish, 477 | # distribute, sublicense, and/or sell copies of the Software, and to 478 | # permit persons to whom the Software is furnished to do so, subject to 479 | # the following conditions: 480 | 481 | # The above copyright notice and this permission notice shall be 482 | # included in all copies or substantial portions of the Software. 483 | 484 | # THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 485 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 486 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 487 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 488 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 489 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 490 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /clientcreds/templates/clientcreds/mail.html: -------------------------------------------------------------------------------- 1 | 2 | {% if error_message %} 3 |
{{ error_message }}
4 | {% else %} 5 |
{% csrf_token %}
6 |
Messages(from {{user_email}})
7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for message in messages %} 14 | 15 | 16 | 17 | 18 | 19 | {% endfor %} 20 |
FromSubjectReceived
{{ message.From.EmailAddress.Name }}{{ message.Subject }}{{ message.DateTimeReceived }}
21 | {% endif %} 22 |
23 | Access Token: {{ request.session.access_token }}
24 | 
25 | 26 | -------------------------------------------------------------------------------- /clientcreds/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /clientcreds/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | from django.conf.urls import patterns, url 3 | from clientcreds import views 4 | 5 | urlpatterns = patterns('', 6 | # The home view ('/clientcreds/') 7 | url(r'^$', views.home, name='home'), 8 | # Explicit home ('/clientcreds/home/') 9 | url(r'^home/$', views.home, name='home'), 10 | # Used to receive OAuth2 consent response ('/clientcreds/get_consent/') 11 | url(r'^get_consent/$', views.consent, name='consent'), 12 | # Used to display email output ('/clientcreds/mail/') 13 | url(r'^mail/$', views.mail, name='mail'), 14 | ) 15 | 16 | # MIT License: 17 | 18 | # Permission is hereby granted, free of charge, to any person obtaining 19 | # a copy of this software and associated documentation files (the 20 | # ""Software""), to deal in the Software without restriction, including 21 | # without limitation the rights to use, copy, modify, merge, publish, 22 | # distribute, sublicense, and/or sell copies of the Software, and to 23 | # permit persons to whom the Software is furnished to do so, subject to 24 | # the following conditions: 25 | 26 | # The above copyright notice and this permission notice shall be 27 | # included in all copies or substantial portions of the Software. 28 | 29 | # THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 30 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 31 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 32 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 33 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 34 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 35 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /clientcreds/views.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | from django.shortcuts import render 3 | from django.http import HttpResponse, HttpResponseRedirect 4 | from django.core.urlresolvers import reverse 5 | from django.views.decorators.csrf import csrf_exempt 6 | import logging 7 | from clientcreds.clientcredhelper import get_client_cred_authorization_url, get_access_token 8 | from clientcreds.o365service import make_api_call 9 | 10 | # Used for debug logging 11 | logger = logging.getLogger('clientcreds') 12 | 13 | # Home page (just shows a link to admin consent) 14 | def home(request): 15 | redirect_url = request.build_absolute_uri(reverse('clientcreds:consent')) 16 | sign_in_url = get_client_cred_authorization_url(redirect_url, 'https://outlook.office365.com/') 17 | return HttpResponse('Click here to sign in with an admin account') 18 | 19 | # Consent action. Marked CSRF exempt because it is called from Azure 20 | @csrf_exempt 21 | def consent(request): 22 | # We're expecting a POST with form data 23 | if request.method == "POST": 24 | # The ID token we requested is included as the "id_token" form field 25 | id_token = request.POST["id_token"] 26 | redirect_url = request.build_absolute_uri(reverse('clientcreds:consent')) 27 | # Get an access token 28 | access_token = get_access_token(id_token, redirect_url, 'https://outlook.office365.com/') 29 | if (access_token['access_token']): 30 | # Save the token in the session 31 | request.session['access_token'] = access_token['access_token'] 32 | # Redirect to the mail page 33 | return HttpResponseRedirect(reverse('clientcreds:mail')) 34 | else: 35 | return HttpResponse("ERROR: " + access_token['error_description']) 36 | else: 37 | return HttpResponseRedirect(reverse('clientcreds:home')) 38 | 39 | def mail(request): 40 | # The mail page uses a form to set the user, so we only do work 41 | # if this is a POST from that form. 42 | if request.method == "POST": 43 | # Get the user (user@domain.com) 44 | user = request.POST["user_email"] 45 | logger.debug('POST to mail for user: {0}'.format(user)) 46 | access_token = request.session['access_token'] 47 | # Build the user-specific API endpoint to /User/Messages 48 | messages_url = "https://outlook.office365.com/api/v1.0/users('{0}')/Messages/".format(user) 49 | # Use query parameters to only request properties we use, to sort by time received, and to limit 50 | # the results to 10 items. 51 | query_params = '?$select=From,Subject,DateTimeReceived&$orderby=DateTimeReceived desc&$top=10' 52 | messages = make_api_call('GET', messages_url + query_params, access_token).json() 53 | context = { 54 | 'user_email': user, 55 | 'messages': messages['value'], 56 | } 57 | else: 58 | context = { 'user_email': 'NONE' } 59 | 60 | return render(request, 'clientcreds/mail.html', context) 61 | 62 | # MIT License: 63 | 64 | # Permission is hereby granted, free of charge, to any person obtaining 65 | # a copy of this software and associated documentation files (the 66 | # ""Software""), to deal in the Software without restriction, including 67 | # without limitation the rights to use, copy, modify, merge, publish, 68 | # distribute, sublicense, and/or sell copies of the Software, and to 69 | # permit persons to whom the Software is furnished to do so, subject to 70 | # the following conditions: 71 | 72 | # The above copyright notice and this permission notice shall be 73 | # included in all copies or substantial portions of the Software. 74 | 75 | # THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 76 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 77 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 78 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 79 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 80 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 81 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /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", "python_clientcred.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /python_clientcred/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjoh/python_clientcred/47ae1fc9f7762400e2a45ddf60b8005264293db8/python_clientcred/__init__.py -------------------------------------------------------------------------------- /python_clientcred/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for python_clientcred project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.7/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.7/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 14 | 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = '3+d#ee!d5d&132=*hw70jg%1+q84gxx^64f-ylga##%fs&qea&' 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | TEMPLATE_DEBUG = True 26 | 27 | ALLOWED_HOSTS = [] 28 | 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = ( 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'clientcreds', 40 | ) 41 | 42 | MIDDLEWARE_CLASSES = ( 43 | 'django.contrib.sessions.middleware.SessionMiddleware', 44 | 'django.middleware.common.CommonMiddleware', 45 | 'django.middleware.csrf.CsrfViewMiddleware', 46 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 47 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ) 51 | 52 | ROOT_URLCONF = 'python_clientcred.urls' 53 | 54 | WSGI_APPLICATION = 'python_clientcred.wsgi.application' 55 | 56 | 57 | # Database 58 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 59 | 60 | DATABASES = { 61 | 'default': { 62 | 'ENGINE': 'django.db.backends.sqlite3', 63 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 64 | } 65 | } 66 | 67 | # Internationalization 68 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 69 | 70 | LANGUAGE_CODE = 'en-us' 71 | 72 | TIME_ZONE = 'America/New_York' 73 | 74 | USE_I18N = True 75 | 76 | USE_L10N = True 77 | 78 | USE_TZ = True 79 | 80 | 81 | # Static files (CSS, JavaScript, Images) 82 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 83 | 84 | STATIC_URL = '/static/' 85 | 86 | TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')] 87 | 88 | from django.conf import global_settings 89 | TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + ( 90 | 'django.core.context_processors.request', 91 | ) 92 | 93 | LOGGING = { 94 | 'version': 1, 95 | 'disable_existing_loggers': False, 96 | 'handlers': { 97 | 'file': { 98 | 'level': 'DEBUG', 99 | 'class': 'logging.FileHandler', 100 | 'filename': './debug.log', 101 | }, 102 | }, 103 | 'loggers': { 104 | 'clientcreds': { 105 | 'handlers': ['file'], 106 | 'level': 'DEBUG', 107 | 'propagate': False, 108 | }, 109 | }, 110 | } 111 | -------------------------------------------------------------------------------- /python_clientcred/urls.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See full license at the bottom of this file. 2 | from django.conf.urls import patterns, include, url 3 | from django.contrib import admin 4 | 5 | urlpatterns = patterns('', 6 | url(r'^$', 'clientcreds.views.home', name='home'), 7 | url(r'^clientcreds/', include('clientcreds.urls', namespace='clientcreds')), 8 | url(r'^admin/', include(admin.site.urls)), 9 | ) 10 | 11 | # MIT License: 12 | 13 | # Permission is hereby granted, free of charge, to any person obtaining 14 | # a copy of this software and associated documentation files (the 15 | # ""Software""), to deal in the Software without restriction, including 16 | # without limitation the rights to use, copy, modify, merge, publish, 17 | # distribute, sublicense, and/or sell copies of the Software, and to 18 | # permit persons to whom the Software is furnished to do so, subject to 19 | # the following conditions: 20 | 21 | # The above copyright notice and this permission notice shall be 22 | # included in all copies or substantial portions of the Software. 23 | 24 | # THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, 25 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 27 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 28 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 29 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 30 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /python_clientcred/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for python_clientcred 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.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "python_clientcred.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.7.1 2 | requests>=2.7.0 3 | rsa>=3.2 4 | -------------------------------------------------------------------------------- /setup_project.bat: -------------------------------------------------------------------------------- 1 | python manage.py makemigrations clientcreds 2 | python manage.py migrate 3 | python manage.py createsuperuser --------------------------------------------------------------------------------