Skip to content

Commit 190098e

Browse files
committed
Added refresh token feature
1 parent f75acea commit 190098e

3 files changed

Lines changed: 171 additions & 8 deletions

File tree

README.md

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ Now the library is installed and ready to use. Create a new file in the `tutoria
138138

139139
```python
140140
from urllib.parse import quote, urlencode
141+
import base64
142+
import json
143+
import time
141144

142145
# Client ID and secret
143146
client_id = 'YOUR APP ID HERE'
@@ -375,7 +378,110 @@ def gettoken(request):
375378

376379
If you save your changes, restart the server, and go through the sign-in process again, you should now see a long string of seemingly nonsensical characters. If everything's gone according to plan, that should be an access token.
377380

378-
Now we're ready to call the Mail API.
381+
### Refreshing the access token
382+
383+
Access tokens returned from Azure are valid for an hour. If you use the token after it has expired, the API calls will return 401 errors. You could ask the user to sign in again, but the better option is to refresh the token silently.
384+
385+
In order to do that, the app must request the `offline_access` scope. Add this scope to the `scopes` array in `authhelper.py`:
386+
387+
```python
388+
# The scopes required by the app
389+
scopes = [ 'openid',
390+
'offline_access',
391+
'https://outlook.office.com/mail.read' ]
392+
```
393+
394+
This will cause the token response from Azure to include a refresh token. Let's update `gettoken` in `views.py` to save the refresh token and the expiration time in the session.
395+
396+
#### Updated `gettoken` function in `.\tutorial\views.py` ####
397+
398+
```python
399+
# Add import statement to include new function
400+
from tutorial.outlookservice import get_me
401+
402+
def gettoken(request):
403+
auth_code = request.GET['code']
404+
redirect_uri = request.build_absolute_uri(reverse('tutorial:gettoken'))
405+
token = get_token_from_code(auth_code, redirect_uri)
406+
access_token = token['access_token']
407+
user = get_me(access_token)
408+
refresh_token = token['refresh_token']
409+
expires_in = token['expires_in']
410+
411+
# expires_in is in seconds
412+
# Get current timestamp (seconds since Unix Epoch) and
413+
# add expires_in to get expiration time
414+
# Subtract 5 minutes to allow for clock differences
415+
expiration = int(time.time()) + expires_in - 300
416+
417+
# Save the token in the session
418+
request.session['access_token'] = access_token
419+
request.session['refresh_token'] = refresh_token
420+
request.session['token_expires'] = expiration
421+
request.session['user_email'] = user['EmailAddress']
422+
return HttpResponse('User Email: {0}, Access token: {1}'.format(user['EmailAddress'], access_token))
423+
```
424+
425+
Now let's create a function to refresh the access token. Add the following function to `authhelper.py`.
426+
427+
#### The `get_token_from_refresh_token` function in `./tutorial/authhelper.py` ####
428+
429+
```python
430+
def get_token_from_refresh_token(refresh_token, redirect_uri):
431+
# Build the post form for the token request
432+
post_data = { 'grant_type': 'refresh_token',
433+
'refresh_token': refresh_token,
434+
'redirect_uri': redirect_uri,
435+
'scope': ' '.join(str(i) for i in scopes),
436+
'client_id': client_id,
437+
'client_secret': client_secret
438+
}
439+
440+
r = requests.post(token_url, data = post_data)
441+
442+
try:
443+
return r.json()
444+
except:
445+
return 'Error retrieving token: {0} - {1}'.format(r.status_code, r.text)
446+
```
447+
448+
Finally let's create a helper function to retrieve the access token. The function will check the expiration time, and if the token is expired, will refresh it. Otherwise it will just return the access token from the session. Add the following function to `authhelper.py`.
449+
450+
#### The `get_access_token` function in `./tutorial/authhelper.py` ####
451+
452+
```python
453+
def get_access_token(request, redirect_uri):
454+
current_token = request.session['access_token']
455+
expiration = request.session['token_expires']
456+
now = int(time.time())
457+
if (current_token && now < expiration):
458+
// Token still valid
459+
return current_token
460+
else:
461+
// Token expired
462+
refresh_token = request.session['refresh_token']
463+
new_tokens = get_token_from_refresh_token(refresh_token, redirect_uri)
464+
465+
// Update session
466+
# expires_in is in seconds
467+
# Get current timestamp (seconds since Unix Epoch) and
468+
# add expires_in to get expiration time
469+
# Subtract 5 minutes to allow for clock differences
470+
expiration = int(time.time()) + new_tokens['expires_in'] - 300
471+
472+
# Save the token in the session
473+
request.session['access_token'] = new_tokens['access_token']
474+
request.session['refresh_token'] = new_tokens['refresh_token']
475+
request.session['token_expires'] = expiration
476+
477+
return new_tokens['access_token']
478+
```
479+
480+
Let's import `get_access_token` in `views.py` so we can make use of it.
481+
482+
```python
483+
from tutorial.authhelper import get_signin_url, get_token_from_code, get_access_token
484+
```
379485

380486
## Using the Mail API ##
381487

@@ -385,7 +491,7 @@ Now that we can get an access token, we're in a good position to do something wi
385491

386492
```python
387493
def mail(request):
388-
access_token = request.session['access_token']
494+
access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
389495
user_email = request.session['user_email']
390496
# If there is no token in the session, redirect to home
391497
if not access_token:
@@ -470,7 +576,7 @@ Then update the `mail` function to call the new function.
470576

471577
```python
472578
def mail(request):
473-
access_token = request.session['access_token']
579+
access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
474580
user_email = request.session['user_email']
475581
# If there is no token in the session, redirect to home
476582
if not access_token:
@@ -519,7 +625,7 @@ Update the `mail` function in `views.py` to use this new template.
519625

520626
```python
521627
def mail(request):
522-
access_token = request.session['access_token']
628+
access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
523629
user_email = request.session['user_email']
524630
# If there is no token in the session, redirect to home
525631
if not access_token:

tutorial/authhelper.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import requests
44
import base64
55
import json
6+
import time
67

78
# Client ID and secret
89
client_id = 'YOUR APP ID HERE'
@@ -20,6 +21,7 @@
2021

2122
# The scopes required by the app
2223
scopes = [ 'openid',
24+
'offline_access',
2325
'https://outlook.office.com/mail.read',
2426
'https://outlook.office.com/calendars.read',
2527
'https://outlook.office.com/contacts.read' ]
@@ -53,6 +55,49 @@ def get_token_from_code(auth_code, redirect_uri):
5355
except:
5456
return 'Error retrieving token: {0} - {1}'.format(r.status_code, r.text)
5557

58+
def get_token_from_refresh_token(refresh_token, redirect_uri):
59+
# Build the post form for the token request
60+
post_data = { 'grant_type': 'refresh_token',
61+
'refresh_token': refresh_token,
62+
'redirect_uri': redirect_uri,
63+
'scope': ' '.join(str(i) for i in scopes),
64+
'client_id': client_id,
65+
'client_secret': client_secret
66+
}
67+
68+
r = requests.post(token_url, data = post_data)
69+
70+
try:
71+
return r.json()
72+
except:
73+
return 'Error retrieving token: {0} - {1}'.format(r.status_code, r.text)
74+
75+
def get_access_token(request, redirect_uri):
76+
current_token = request.session['access_token']
77+
expiration = request.session['token_expires']
78+
now = int(time.time())
79+
if (current_token and now < expiration):
80+
# Token still valid
81+
return current_token
82+
else:
83+
# Token expired
84+
refresh_token = request.session['refresh_token']
85+
new_tokens = get_token_from_refresh_token(refresh_token, redirect_uri)
86+
87+
# Update session
88+
# expires_in is in seconds
89+
# Get current timestamp (seconds since Unix Epoch) and
90+
# add expires_in to get expiration time
91+
# Subtract 5 minutes to allow for clock differences
92+
expiration = int(time.time()) + new_tokens['expires_in'] - 300
93+
94+
# Save the token in the session
95+
request.session['access_token'] = new_tokens['access_token']
96+
request.session['refresh_token'] = new_tokens['refresh_token']
97+
request.session['token_expires'] = expiration
98+
99+
return new_tokens['access_token']
100+
56101
# MIT License:
57102

58103
# Permission is hereby granted, free of charge, to any person obtaining

tutorial/views.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
from django.shortcuts import render
33
from django.http import HttpResponse, HttpResponseRedirect
44
from django.core.urlresolvers import reverse
5-
from tutorial.authhelper import get_signin_url, get_token_from_code
5+
from tutorial.authhelper import get_signin_url, get_token_from_code, get_access_token
66
from tutorial.outlookservice import get_me, get_my_messages, get_my_events, get_my_contacts
7+
import time
78

89
# Create your views here.
910

@@ -18,14 +19,25 @@ def gettoken(request):
1819
token = get_token_from_code(auth_code, redirect_uri)
1920
access_token = token['access_token']
2021
user = get_me(access_token)
22+
refresh_token = token['refresh_token']
23+
expires_in = token['expires_in']
24+
25+
# expires_in is in seconds
26+
# Get current timestamp (seconds since Unix Epoch) and
27+
# add expires_in to get expiration time
28+
# Subtract 5 minutes to allow for clock differences
29+
expiration = int(time.time()) + expires_in - 300
2130

2231
# Save the token in the session
2332
request.session['access_token'] = access_token
33+
request.session['refresh_token'] = refresh_token
34+
request.session['token_expires'] = expiration
2435
request.session['user_email'] = user['EmailAddress']
36+
2537
return HttpResponseRedirect(reverse('tutorial:mail'))
2638

2739
def mail(request):
28-
access_token = request.session['access_token']
40+
access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
2941
user_email = request.session['user_email']
3042
# If there is no token in the session, redirect to home
3143
if not access_token:
@@ -36,7 +48,7 @@ def mail(request):
3648
return render(request, 'tutorial/mail.html', context)
3749

3850
def events(request):
39-
access_token = request.session['access_token']
51+
access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
4052
user_email = request.session['user_email']
4153
# If there is no token in the session, redirect to home
4254
if not access_token:
@@ -47,7 +59,7 @@ def events(request):
4759
return render(request, 'tutorial/events.html', context)
4860

4961
def contacts(request):
50-
access_token = request.session['access_token']
62+
access_token = get_access_token(request, request.build_absolute_uri(reverse('tutorial:gettoken')))
5163
user_email = request.session['user_email']
5264
# If there is no token in the session, redirect to home
5365
if not access_token:

0 commit comments

Comments
 (0)