Replies: 11 comments 14 replies
-
A couple ideas:
|
Beta Was this translation helpful? Give feedback.
-
Note these issues: |
Beta Was this translation helpful? Give feedback.
-
@hillairet just wanted to write down some of the issues and feature requests from the implementation of Together:
|
Beta Was this translation helpful? Give feedback.
-
Some notes from the meeting @lishanl, @ponty33, @qw-in and I had:
|
Beta Was this translation helpful? Give feedback.
-
After starring quite a bit at FastAPI's routing code, I think I have a pretty neat proposal for the FastAPI interface for the OAuth.
The following code works. from spylib.oauth import OAuthRouter
router = OAuthRouter(
public_domain: 'https://example.com',
private_key: 'PRIVATEKEY',
app_handle: 'supercoolapp',
api_key: 'APIKEY',
api_secret_key: 'APISECRETKEY',
app_scopes: ['write_orders'],
user_scopes: ['read_orders'],
)
@router.init('/shopify/auth'):
async def init():
return
@router.install_callback('/callback')
async def install_callback():
return
app = FastAPI()
app.include_router(router) I managed to get the above code to work and I can do whatever I want before and after |
Beta Was this translation helpful? Give feedback.
-
Ok, I spent some time today and I think I have come up with a pattern/design that would work for together. I'll try and explain as best I can below. Stage one - utility functionsThis will provide a flexible base upon which everything else is built. It will also always mean there is an escape hatch in case someone has slightly different requirements than what the library currently provides. This includes but is not limited to: # function names very much up for discussion
def create_authorization_url():
"""Returns a url that will initialise oauth"""
pass
def verify_oauth_hmac():
"""Verify an hmac, for example during oauth callback"""
pass
def verify_shop():
"""Verify a *.myshopify.com domain"""
pass
async def exchange_code_for_token():
"""Exchange temporary code for access token"""
pass
# Session tokens are very much part of oauth imo - they are directly tied to online tokens
def validate_session_token():
"""Ensure a session token is valid"""
pass Stage two - configuration/convenience classAdd an app = App(
api_key='api-key',
api_secret_key='api-secret-key',
...
)
app.verify_oauth_hmac() # <- no need to pass secret Stage three - larger flowsIdentify common flows that the app can automate. For example:
The following code would in theory be all that is needed to support ^ from a user. In this case for fastapi but you can see how it would be trivial to handle in any (async) framework from fastapi import FastAPI
from spylib.fastapi.app import App
f = FastAPI()
app = App(
api_key='api-key',
api_secret_key='api-secret-key',
offline_scopes=['read_themes', 'write_products'],
online_scopes=['read_products'],
callback_url="https://example.com/callback"
)
@app.save_installation
async def save_installation(install: Installation):
"""My custom code to persist an installation (includes offline token, shop, scopes, etc.)"""
pass
@app.load_installation
async def load_installation(shop: str) -> Union[Installation, None]:
"""My custom code to load an installation"""
pass
@app.save_login
async def save_login(login: Login):
"""My custom code to persist a login (includes online token, shop, scopes, user_id, user scopes, expiration, etc.)"""
pass
@app.load_login
async def load_login(shop: str, user_id: str) -> Union[Login, None]:
"""My custom code to load a login"""
pass
@f.get('/login')
async def login(shop: str):
# Redirect URL will be one the following based on calling `load_login` & `load_installation`
# - offline token request
# - online token request
# - direct redirect to embedded app
redirect_url = await app.initiate_auth(shop)
return RedirectResponse(url=redirect_url) # If forget the exact syntax
# Could also be written like this
@f.get('/login')
async def login(redirect_url: str = Depends(app.initiate_auth)):
return RedirectResponse(url=redirect_url) # If forget the exact syntax
@f.get('/callback')
async def callback(redirect_url: str = Depends(app.initiate_auth)):
# Will verify the whole callback
# Will exchange the token
# Will store the offline/online token
# Will redirect to get online token if needed
# Will finally redirect back to app (or let you do so)
return RedirectResponse(url=redirect_url) # If forget the exact syntax
@f.post('/api/resource')
async def create_resource(login: Login = Depends(app.authenticate(scope=['read_products'])):
# login now has valid online token
# optionally also verifies this user has the needed shopify scopes for this endpoint
# could return a token that can call shopify directly here instead - but that is out of the oauth scope
# could return a status/code to inform the frontend an oauth login is required
# This pattern continues, webhooks for example
app = App(
webhooks: ['uninstall', ...],
webhook_url: 'https://example.com/webhooks',
)
@app.on_uninstall
async def on_uninstall(webhook):
pass
@f.post('/webhook')
async def webhook(success = Depends(app.handle_webhook)):
return ... |
Beta Was this translation helpful? Give feedback.
-
Ok so taking into account your concerns for the router inheritance, although I don't think that would cause any issue, we could maybe find a compromise with decorators rather than dependencies. One advantage of the decorator over the dependency is that we can handle the inputs and output. I don't get why you need class Installation(OfflineTokenABC): And because of the inheritance of the abstract class, one can add fields if/when needed. Also with your design there are two places for flexibility when only one is needed! How about that much simpler approach: from fastapi import FastAPI
from spylib.oauth.fastapi import OAuth
f = FastAPI()
oauth = OAuth(
api_key='api-key',
api_secret_key='api-secret-key',
offline_scopes=['read_themes', 'write_products'],
online_scopes=['read_products'],
callback_url="https://example.com/callback"
)
@f.get('/callback')
@oauth.callback
async def callback(offlinetokenresponse: OfflineTokenResponse):
# In another file: Class OfflineToken(OfflineTokenABC)
offlinetoken = OfflineToken.create_from_response(offlinetokenresponse)
await offlinetoken.save() The decorator takes care of returning the right redirect but if the user returns something in the |
Beta Was this translation helpful? Give feedback.
-
We are putting this discussion on hold while completing #138 |
Beta Was this translation helpful? Give feedback.
-
Coming back to this. For authentication of endpoints, you are right that dependencies fit well in particular to be applied to a whole router. So let's use dependencies. |
Beta Was this translation helpful? Give feedback.
-
Now back to my previous comment about being confused: |
Beta Was this translation helpful? Give feedback.
-
We should implement this happy path (in blue) and then implement each check and each non-happy path outcome one by one. The session token valid/invalid is a basic check so happy path. graph TD
AA(Session token):::happypath --> A
classDef happypath fill:#00D,color:#fff
A(Is it valid?):::happypath -->|Valid token| B
A -->|Invalid token| C(Unauthorized):::happypath
B(Is associated shop installed?):::happypath -->|Yes| D
B -->|No| E(Go to install flow)
D(Are scopes up to date?):::happypath -->|Yes| F
D -->|No| E
F(Is there a valid online token<br>associated with this session?):::happypath -->|Yes| G
F -->|No| H(Go to login flow)
G(Return valid online token):::happypath
|
Beta Was this translation helpful? Give feedback.
-
One main point of friction of the current design is that it doesn't include the token classes out of the box, which is weird (according to @qw-in 😉 )
Beta Was this translation helpful? Give feedback.
All reactions