Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e642f5d

Browse files
Add support for OAuth2 device flows.
* This PR adds support for OAuth2 flows for devices with limited input capabilities. * This enables the user to login using a secondary device, and have the limited device poll for authorization completion. Test: Added an example and tested various different workflows.
1 parent eae01bd commit e642f5d

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

‎uoauth2.device/example.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from uoauth2.device import DeviceAuth
2+
3+
# For more information on how to create clients
4+
# Look at: https://developers.google.com/identity/protocols/oauth2/limited-input-device
5+
6+
device_auth = DeviceAuth(
7+
client_id='648445354032-mv5p4b09hcj0116v57pnkmp42fn8m220.apps.googleusercontent.com',
8+
client_secret='9aeN3LGr0yq4TYjwGcfUVJKo',
9+
discovery_endpoint='https://accounts.google.com/.well-known/openid-configuration',
10+
scopes=list(['openid'])
11+
)
12+
13+
# Discover OpenID endpoints
14+
device_auth.discover()
15+
16+
# Start authorization process
17+
device_auth.authorize()
18+
19+
# Use the user-code and verification URL to show some UI to the user
20+
# To complete the authorization process.
21+
user_code = device_auth.user_code
22+
verification_url = device_auth.verification_url
23+
24+
print(user_code, verification_url)
25+
26+
# Check for completed authorization
27+
device_auth.check_authorization_complete()
28+
29+
# Fetch a valid access token
30+
print(device_auth.token())

‎uoauth2.device/metadata.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
srctype = micropython-lib
2+
type = module
3+
version = 0.1
4+
author = Rahul Ravikumar

‎uoauth2.device/setup.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import sys
2+
# Remove current dir from sys.path, otherwise setuptools will peek up our
3+
# module instead of system's.
4+
sys.path.pop(0)
5+
from setuptools import setup
6+
sys.path.append("..")
7+
import sdist_upip
8+
9+
setup(name='micropython-uoauth2.device',
10+
version='0.1',
11+
description='uoauth2.device module for MicroPython',
12+
long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.",
13+
url='https://github.com/micropython/micropython-lib',
14+
author='Rahul Ravikumar',
15+
author_email='micro-python@googlegroups.com',
16+
maintainer='micropython-lib Developers',
17+
maintainer_email='micro-python@googlegroups.com',
18+
license='MIT',
19+
cmdclass={'sdist': sdist_upip.sdist},
20+
py_modules=['uoauth2'])

‎uoauth2.device/uoauth2/device.py‎

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import json
2+
import time
3+
import urllib.parse as urlparse
4+
5+
import urequests as requests
6+
7+
8+
class DeviceAuth:
9+
'''
10+
Helps with authenticating devices with limited input capabilities
11+
per the OAuth2 device flow specification.
12+
'''
13+
14+
def __init__(self, client_id, client_secret, discovery_endpoint, scopes=list()):
15+
self.client_id = client_id
16+
self.client_secret = client_secret
17+
self.discovery_endpoint = discovery_endpoint
18+
self.scopes = scopes
19+
20+
self.user_code = None
21+
self.verification_url = None
22+
23+
self._discovered = False
24+
self._authorization_started = False
25+
self._authorization_completed = False
26+
27+
self._device_auth_endpoint = None
28+
self._token_endpoint = None
29+
self._device_code = None
30+
self._interval = None
31+
self._code_expires_in = None
32+
33+
self._access_token = None
34+
self._token_acquired_at = None
35+
self._token_expires_in = None
36+
self._token_scope = None
37+
self._token_type = None
38+
self._refresh_token = None
39+
40+
41+
def discover(self):
42+
'''
43+
Performs OAuth2 device endpoint discovery.
44+
'''
45+
46+
if not self._discovered:
47+
r = requests.request('GET', self.discovery_endpoint)
48+
j = r.json()
49+
self._device_auth_endpoint = j['device_authorization_endpoint']
50+
self._token_endpoint = j['token_endpoint']
51+
self._discovered = True
52+
r.close()
53+
54+
55+
def authorize(self):
56+
'''
57+
Makes an authorization request.
58+
'''
59+
60+
if not self._discovered:
61+
print('Need to discover authorization and token endpoints.')
62+
return
63+
64+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
65+
payload = {
66+
'client_id': self.client_id,
67+
'scope': ' '.join(self.scopes)
68+
}
69+
encoded = urlparse.urlencode(payload)
70+
r = requests.request('POST', self._device_auth_endpoint, data=encoded, headers=headers)
71+
j = r.json()
72+
r.close()
73+
74+
if 'error' in j:
75+
raise RuntimeError(j['error'])
76+
77+
self._device_code = j['device_code']
78+
self.user_code = j['user_code']
79+
self.verification_url = j['verification_url']
80+
self._interval = j['interval']
81+
self._code_expires_in = j['expires_in']
82+
self._authorization_started = True
83+
message = 'Use code %s at %s to authorize the device.' % (self.user_code, self.verification_url)
84+
print(message)
85+
86+
87+
def check_authorization_complete(self, sleep_duration_seconds=5, max_attempts=10):
88+
'''
89+
Polls until completion of an authorization request.
90+
'''
91+
92+
if not self._authorization_started:
93+
print('Start an authorization request.')
94+
return
95+
96+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
97+
payload = {
98+
'client_id': self.client_id,
99+
'client_secret': self.client_secret,
100+
'device_code': self._device_code,
101+
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code'
102+
}
103+
encoded = urlparse.urlencode(payload)
104+
105+
current_attempt = 0
106+
while not self._authorization_completed and current_attempt < max_attempts:
107+
current_attempt = current_attempt + 1
108+
r = requests.request('POST', self._token_endpoint, data=encoded, headers=headers)
109+
j = r.json()
110+
r.close()
111+
if 'error' in j:
112+
if j['error'] == 'authorization_pending':
113+
print('Pending authorization. ')
114+
time.sleep(sleep_duration_seconds)
115+
elif j['error'] == 'access_denied':
116+
print('Access denied')
117+
raise RuntimeError(j['error'])
118+
else:
119+
self._access_token = j['access_token']
120+
self._token_acquired_at = int(time.time())
121+
self._token_expires_in = j['expires_in']
122+
self._token_scope = j['scope']
123+
self._token_type = j['token_type']
124+
self._refresh_token = j['refresh_token']
125+
print('Completed authorization')
126+
self._authorization_completed = True
127+
128+
129+
def token(self, force_refresh=False):
130+
'''
131+
Fetches a valid access token.
132+
'''
133+
134+
if not self._authorization_completed:
135+
print('Complete an authorization request')
136+
return
137+
138+
buffer = 10 * 60 * -1 # 10 min in seconds
139+
now = int(time.time())
140+
is_valid = now < (self._token_acquired_at + self._token_expires_in + buffer)
141+
142+
if not is_valid or force_refresh:
143+
print('Token expired. Refreshing access tokens.')
144+
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
145+
payload = {
146+
'client_id': self.client_id,
147+
'client_secret': self.client_secret,
148+
'refresh_token': self._refresh_token,
149+
'grant_type': 'refresh_token'
150+
}
151+
encoded = urlparse.urlencode(payload)
152+
r = requests.request('POST', self._token_endpoint, data=encoded, headers=headers)
153+
status_code = r.status_code
154+
j = r.json()
155+
r.close()
156+
157+
if status_code == 400:
158+
print('Unable to refresh tokens.')
159+
raise(RuntimeError('Unable to refresh tokens.'))
160+
161+
print('Updated access tokens.')
162+
self._access_token = j['access_token']
163+
self._token_acquired_at = int(time.time())
164+
self._token_expires_in = j['expires_in']
165+
self._token_scope = j['scope']
166+
self._token_type = j['token_type']
167+
168+
return self._access_token

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /