Authentication with Svelte

Figuring out how to authenticate with Svelte can be tricky business. The official docs for Sapper, the Server-Side Rendering platform designed for Svelte, recognize that session management should be handled by some other service such as express-session, but you are not limited to using any backend with Svelte. Moreover, Sapper does not have native support for persistent sessions (as of April 2020).

Your best options might be to offload session management from Svelte to some other web server that is configured to use HTTPS. In many cases, your clients will want to authenticate using a modern web browser, and most modern web browsers implement strong security regulations over how data gets transferred between a server and a client. During development, you might see this error: “Reason: CORS header ‘Access-Control-Allow-Origin’ missing”, and if you do then it may be worth a few minutes to read up on “Dealing with CORS Errors in Svelte”.

Method 1: JSON Fetch using a POST method (same-origin cors headers)

While Svelte may not necessary require an asynchronous authentication method, your application’s performance could benefit from trying to use one. It is generally accepted that POST methods are the way to go, since they do not append sensitive data after the request URI. In this example, we incorporate writable stores (for saving the auth server’s response), reactive statements for building the data body of the POST request, and specialized Svelte tags {#await <promise>}, {:then <awaited response>}, {:catch <some error>} to render a different HTML tag at each stage of the authentication request.

It is important to note that this example includes preventDefault to prevent the runtime from making an HTTP request at the instant when the form element gets created: <form on:submit|preventDefault={submitHandler}>.

<script>
  import { session } from './session.js';
  /* session.js:
   * ===========
   * import { writable } from 'svelte/store';
   * export const session = writable({ data: "" })
   */
  // Alternatively, you could write:
  //    export const AUTH_SERVER_URL;
  // instead of:
  //    const AUTH_SERVER_URL = "...";
  // to set the destination using the props spread

  const AUTH_SERVER_URL = "https://authserverurl.com/api/login";
  let email = "";
  let password = "";
  let combined;
  let data;
  $: combined = {email: email, password: password};
  $: if(data) {
      $session.data = data;
     }
  async function authenticate() {
    // Gathered from: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
    let response = await fetch(AUTH_SERVER_URL, {
      method: 'post',
      mode: 'cors', // no-cors, *cors, same-origin,
      cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/json'
        // 'Content-Type': 'application/x-www-form-urlencoded',
      },
      redirect: 'follow', // manual, *follow, error
      referrerPolicy: 'no-referrer', // no-referrer, *client
      body: JSON.stringify(combined) // body data type must match "Content-Type" header
    });

    // One additional option is to use:
    // ... = await response.json();
    // But since we're printing out the response in an HTML element,
    // it is convenient to await the `.text()` promise.
    let text = await response.text();

    // This next line is verbose, but it's meant to demonstrate
    // what happens when we want to use a reactive value change
    // to bind our new information using `$: if(data) {...}`
    let data = text;
    return text;
  }
  function submitHandler() {
    /* This promise needs to be awaited somewhere --
     * either in the HTML body via `{#await}` tags,
     * in a `<script>` tag, or in an imported `.js` module.
     */
    result = authenticate();
    // Clear out the data fields
    email = "";
    password = "";
  }
</script>
<div>
  <form on:submit|preventDefault={submitHandler}>
    <input type="text" bind:value={email}>
    <input type="password" bind:value={password}>
    <button>Submit</button>
  </form>
</div>
<div>
  {#if result===undefined}
    <p />
  {:else}
    {#await result}
      <div><span>Logging in...</span></div>
    {:then value}
      <div><span>{value}</span></div>
    {:catch error}
      <div><span>{error.message}</span></div>
    {/await}
  {/if}
</div>