Hi @starwort here’s some code excerpts:
On the client I have a little form with a button to initiate authentication workflow:
def form_show(self, **event_args):
"""This method is called when the column panel is shown on the screen"""
(client_id, session_id) = anvil.server.call_s('get_client_id')
url = 'https://discordapp.com/api/oauth2/authorize?client_id={c_id}&' + \
'redirect_uri={api_root}%2Foauth_redirect%2F' + \
'&response_type=code&scope=identify&state={state}'
self.lnk_discord_check.url = url.format(c_id=client_id,
api_root=anvil.http.url_encode(anvil.server.get_api_origin()),
state=session_id)
On the server I have both the get_client_id
function and the oauth_redirect
endpoint:
@anvil.server.callable
def get_client_id():
client_id = anvil.secrets.get_secret('discord_client_id')
session_id = str(uuid.uuid4())
user = anvil.users.get_user()
# inserts a uuid temp row to be consumed by http endpoint
valid_until = datetime.datetime.now() + datetime.timedelta(minutes=5)
app_tables.temp_uuids.add_row(user=user,uuid=session_id,uuid_valid_until=valid_until)
return (client_id,session_id)
....
@anvil.server.http_endpoint("/oauth_redirect/")
def oauth_redirect(**qs):
contents = ''
# You'll get auth result from Discord as querystring parameters
if 'state' in qs.keys():
uuid = qs['state']
# 1st: get the user row and consume the uuid row
auth_user_row = app_tables.temp_uuids.get(uuid=uuid)
if auth_user_row != None:
auth_user = auth_user_row['user']
auth_user_valid = auth_user_row['uuid_valid_until']
auth_user_row.delete()
if auth_user_valid >= datetime.datetime.now(timezone.utc):
# 2nd: check if auth succeded
oauth_success = ('code' in qs.keys())
if oauth_success:
# record last successful login in order not to ask too soon the next check
auth_user.update(discord_code=qs['code'],discord_check_datetime=datetime.datetime.now())
# Do whatever you want on successful login, I retrieve the contents of a static webpage stored in the DB
contents = app_tables.files.get(name='auth_OK')['contents']
else:
# oAuth failed
# Your code for unsuccessful login
contents = app_tables.files.get(name='auth_KO')['contents']
else:
# state string no longer valid
# Your code for login token expired
contents = app_tables.files.get(name='auth_KO')['contents']
else:
# state string not found
# Your code for login token not present
contents = app_tables.files.get(name='auth_KO')['contents']
contents.content_type = 'text/html'
return contents
So, you can guess what happens.
When the “Check Discord Auth” user form is shown, I build an URL that will call Discord’s API passing:
- the
client_id
identifying my APP. You get this when you register your APP in Discord. I store it server-side in Anvil’s Secret Service. - the
redirect_uri
the user will be redirected to, upon authentication is complete. This is an HTTP API Endpoint of my App, that will process authentication flow outcome. - the
response_type
andscope
parameters, set to appropriate values according to this Discord API page and the needs of your flow. - the
state
parameter, which will be passed back unmodified by Discord to the endpoint specified inredirect_uri
above, and should help you identify the authentication flow that is coming back from Discord among the many that could have gone to Discord in the meantime.
In order to build that link, a server function is called that:
- retrieves the
client_id
from Anvil’s secret - creates a random UUID and stores it in the database along with its time validity data (5 mins)
When the user clicks that link, Discord’s authentication flow starts on aonther browser tab.
At the end, Discord calls the URL specified in redirect_uri
passing back:
- a JSON with the outcome of the authentication
- the UUID in the
state
parameter of the querystring
My HTTP Endpoint then:
- searches for the UUID
- retrieves from DB associated data and datetime-validity
- removes the “now consumed” UUID from the table
- checks time validity and auth flow outcome
- stores authentication OK datetime in DB
As a result of the process, my HTTP Endpoint renders a static webpage saying “OK you’re done! Return to the APP” or “KO you’re out!”. If you need to do some more elaborate stuff, like putting a link to a specific form of your APP, check this very good post of Stu about it.
Remember: here you’re on a browser’s tab different from the one you’re APP is. That’s why you need to store the authentication flow outcome in the DB, so your user can get back into the APP, which will check in the DB if the user has authenticated against Discord.
In the end, longer to say than to code…
Have fun
BR