Commit c5a0d6ee authored by Koen van der Veen's avatar Koen van der Veen
Browse files

Merge branch 'fix_oauth_url' into 'dev'

Fix oauth authentication url

See merge request memri/pymemri!89
parents 6c7cfd85 4e680832
Showing with 40 additions and 112 deletions
+40 -112
......@@ -154,21 +154,6 @@ nb_path: "nbs/plugin.authenticators.oauth.ipynb"
</div>
</div>
<div class="output_wrapper">
<div class="output">
<div class="output_area">
<div class="output_subarea output_stream output_stdout output_text">
<pre>creating Account (#None)
creating PluginRun (#aCBe7Ec18a827b1Ba731b5DebFBdE8D1)
</pre>
</div>
</div>
</div>
</div>
</div>
{% endraw %}
......@@ -203,26 +188,6 @@ creating PluginRun (#aCBe7Ec18a827b1Ba731b5DebFBdE8D1)
</div>
</div>
<div class="output_wrapper">
<div class="output">
<div class="output_area">
<div class="output_subarea output_stream output_stdout output_text">
<pre>running
updating Account (#7e280a7d735fe426892b24739e3b7ea7)
updating PluginRun (#aCBe7Ec18a827b1Ba731b5DebFBdE8D1)
updating Account (#7e280a7d735fe426892b24739e3b7ea7)
&lt;Response [500]&gt; b&#39;Failure: Database rusqlite error database is locked&#39;
logged in with dummy_access_token
Login completed!
</pre>
</div>
</div>
</div>
</div>
</div>
{% endraw %}
......
%% Cell type:code id:9731563d tags:
%% Cell type:code id: tags:
``` python
%load_ext autoreload
%autoreload 2
# default_exp plugin.authenticators.oauth
```
%% Cell type:code id:66755618 tags:
%% Cell type:code id: tags:
``` python
# export
# hide
import abc
from time import sleep
from pymemri.plugin.pluginbase import PluginBase
from pymemri.plugin.states import RUN_USER_ACTION_NEEDED, RUN_USER_ACTION_COMPLETED, RUN_FAILED
from pymemri.plugin.schema import PluginRun, Account
from pymemri.pod.client import PodClient
```
%% Cell type:markdown id:43151827 tags:
%% Cell type:markdown id: tags:
## OAuth Authenticator
For plugins that require OAuth, `OAuthAuthenticator` provides an easy interface to login to thirds party services. By implementing You can nest an inherited version class of `OAuthAuthenticator` in your `Plugin` item and call authenticator.authenticate().
`YourOauthAuthenticator` class should implement:
- get_oauth_url() -> str to setup OAuth endpoint
- get_tokens_from_code() -> Dict[str, str] to retrieve tokens from the service with returned OAuth code
- refresh_tokens() -> None to get new token pairs
%% Cell type:code id:e1ca39a6 tags:
%% Cell type:code id: tags:
``` python
# export
# hide
class OAuthAuthenticator(metaclass=abc.ABCMeta):
SLEEP_INTERVAL = 1.0
def __init__(self, client, pluginRun):
self.client = client
self.pluginRun = pluginRun
def authenticate(self):
account = self.pluginRun.account[0] if len(self.pluginRun.account) > 0 else None
if account.accessToken:
if self.verify_access_token(account.accessToken):
return
tokens = None
try:
if not self.pluginRun.account[0].refreshToken:
raise Exception("Refresh token is empty")
tokens = self.refresh_tokens(self.pluginRun.account[0].refreshToken)
except: # no account exists or expired refresh token
url = self.get_oauth_url()
self.present_url_to_user(url)
code = self.poll_for_code()
tokens = self.get_tokens_from_code(code)
# if not tokens: raise an exception or set pluginRun.status=FAILED and pluginRun.message=ERROR_MESSAGE
self.store_tokens(tokens)
def poll_for_code(self):
while True:
sleep(self.SLEEP_INTERVAL)
self.pluginRun = self.client.get(self.pluginRun.id)
if self.pluginRun.status == RUN_USER_ACTION_COMPLETED:
return self.pluginRun.account[0].code
if self.pluginRun.status == RUN_FAILED:
raise Exception(f"Error in plugin.authenticators.oauth {self.pluginRun.message}")
def present_url_to_user(self, url):
# request user to visit url
self.pluginRun.oAuthUrl = url
self.pluginRun.authUrl = url
self.pluginRun.status = RUN_USER_ACTION_NEEDED
self.pluginRun.update(self.client)
def store_tokens(self, tokens):
account = self.pluginRun.account[0]
account.accessToken = tokens['access_token']
account.refreshToken = tokens['refresh_token'] if 'refresh_token' in tokens else None
account.update(self.client)
def get_account(self):
return self.pluginRun.account[0]
@abc.abstractmethod
def get_oauth_url(self):
raise NotImplemented()
@abc.abstractmethod
def get_tokens_from_code(self, code):
""" Gets access and refresh tokens from 3rd party service
and returns them in form:
{
'access_token': '...',
'refresh_token': '...'
}
"""
raise NotImplemented()
@abc.abstractmethod
def refresh_tokens(self):
""" Gets new tokens by using an existing refresh token
and returns them in form:
{
'access_token': '...',
'refresh_token': '...'
}
"""
# use self.pluginRun.account[0].refreshToken
raise NotImplemented()
@abc.abstractmethod
def verify_access_token(self, token):
"""
Check if existing token is working. If this returns True, then user interaction will not be needed.
"""
raise NotImplemented()
```
%% Cell type:markdown id:e5452d83 tags:
%% Cell type:markdown id: tags:
## Example
%% Cell type:markdown id:cd1396bd tags:
%% Cell type:markdown id: tags:
Lets build an example oauth authenticator, to show its class structure
%% Cell type:code id:5aa094e1 tags:
%% Cell type:code id: tags:
``` python
class ExampleOAuthAuthenticator(OAuthAuthenticator):
def get_oauth_url(self):
return "https://example.com/oauth"
def get_tokens_from_code(self, code):
return {
'access_token': 'dummy_access_token',
'refresh_token': 'dummy_refresh_token'
}
def refresh_tokens(self, refreshToken):
return {
'access_token': 'refreshed_dummy_access_token',
'refresh_token': 'refreshed_dummy_refresh_token'
}
def verify_access_token(self, token):
if token:
return True
def present_url_to_user(self, *args):
# NORMALLY YOU WOULD NOT IMPLEMENT THIS
# mocking user interaction
self.pluginRun.status = RUN_USER_ACTION_COMPLETED
self.pluginRun.account[0].code = "dummy code"
self.pluginRun.account[0].update(self.client)
self.pluginRun.update(self.client)
```
%% Cell type:code id:d986232f tags:
%% Cell type:code id: tags:
``` python
class MyOAuthPlugin(PluginBase):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def run(self):
print("running")
self.login()
print("Login completed!")
def login(self):
auth = ExampleOAuthAuthenticator(self.client, self.pluginRun)
auth.authenticate()
print(f"logged in with {auth.get_account().accessToken}")
def add_to_schema(self):
self.client.add_to_schema(MyItem("my name", 10))
```
%% Cell type:code id:371d5b84 tags:
%% Cell type:code id: tags:
``` python
client = PodClient()
# Create a dummy account to use for authentication within the plugin
account = Account(service="my_plugin_service", identifier="username", secret="password")
# Create a run to enable plugin runtime
run = PluginRun("pymemri", "pymemri.plugin.pluginbase", "MyOAuthPlugin")
run.add_edge('account', account)
account.update(client)
run.update(client)
```
%% Output
creating Account (#None)
creating PluginRun (#aCBe7Ec18a827b1Ba731b5DebFBdE8D1)
%% Cell type:code id:0c945c49 tags:
%% Cell type:code id: tags:
``` python
plugin = MyOAuthPlugin(pluginRun=run, client=client)
```
%% Cell type:code id:974a10cd tags:
%% Cell type:code id: tags:
``` python
plugin.run()
```
%% Output
running
updating Account (#7e280a7d735fe426892b24739e3b7ea7)
updating PluginRun (#aCBe7Ec18a827b1Ba731b5DebFBdE8D1)
updating Account (#7e280a7d735fe426892b24739e3b7ea7)
<Response [500]> b'Failure: Database rusqlite error database is locked'
logged in with dummy_access_token
Login completed!
%% Cell type:markdown id:8a020ed7 tags:
%% Cell type:markdown id: tags:
### Test Plugin properties and attached items -
%% Cell type:code id:6b9cfee3 tags:
%% Cell type:code id: tags:
``` python
# # hide
# from pymemri.plugin.schema import Account
# from pymemri.plugin.pluginbase import PluginRun, run_plugin_from_run_id
# client = PodClient()
# # Create a dummy account to use for authentication within the plugin
# account = Account(service="my_plugin_service", identifier="username", secret="password")
# client.create(account)
# # Create a run to enable plugin runtime
# run = PluginRun("pymemri", "pymemri.plugin.pluginbase", "MyOAuthPlugin")
# run.add_edge('account', account)
# client.create(run)
# client.update(account)
# plugin = run_plugin_from_run_id(run.id, client)
# # check if authentication worked
# assert plugin.pluginRun.account[0].identifier == "username"
# assert plugin.pluginRun.account[0].accessToken == "dummy_access_token"
# # set a state
# plugin.pluginRun.status = "test state"
# plugin.pluginRun.update(client)
# plugin.pluginRun = client.get(plugin.pluginRun.id)
# assert plugin.pluginRun.status == "test state"
```
%% Cell type:code id:d1de6017 tags:
%% Cell type:code id: tags:
``` python
```
......
......@@ -53,7 +53,7 @@ class OAuthAuthenticator(metaclass=abc.ABCMeta):
def present_url_to_user(self, url):
# request user to visit url
self.pluginRun.oAuthUrl = url
self.pluginRun.authUrl = url
self.pluginRun.status = RUN_USER_ACTION_NEEDED
self.pluginRun.update(self.client)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment