Show a webpage as HTTP Endpoint call

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 and scope 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 in redirect_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… :sweat_smile:

Have fun
BR

2 Likes