Authentication

For authentication, you'll need to use Server Session and Server Middlewares.

Login page

Let's build the login page with native form submission :

import {
    DataResponse,
    DynamicHandlerContext,
    EmptyResponse,
    RenderContext,
} from "https://deno.land/x/frugal@0.9.6/mod.ts";

export const route = "/login";

type Data = {
    error?: string;
    username: string;
    password: string;
};

export async function GET({ session }: DynamicHandlerContext<typeof route>) {
    const accessToken = session.get("accessToken");

    if (accessToken !== undefined) {
        // user is already authenticated, redirect to home page
        return new EmptyResponse({
            status: 303,
            headers: {
                "Location": "/",
            },
        });
    }

    const error = session.get("error");
    return new DataResponse({
        error,
        username: "",
        password: "",
    });
}

export async function POST({ session }: DynamicHandlerContext<typeof route>) {
    const formData = await request.formData();
    const username = formData.get("username") ?? "";
    const password = formData.get("password") ?? "";

    const accessToken = authenticate(username, password);
    if (accessToken === undefined) {
        session.set("error", "invalid password or username");
        // failed authentication, redirect to login page
        return new EmptyResponse({
            status: 303,
            headers: {
                "Location": request.url,
            },
        });
    } else {
        session.set("accessToken", accessToken);
        // successful authentication, redirect to home page
        return new EmptyResponse({
            status: 303,
            headers: {
                "Location": "/",
            },
        });
    }
}

export function render({ data }: RenderContext<typeof route, Data>) {
    return `<!DOCTYPE html>
<html>
    <body>
        <form encType='multipart/form-data' method='POST'>
            ${data.error ? `<p>${data.error}</p>` : ""}
            <input type="text" name="username" />
            <input type="password" name="password" />
            <button>Login</button>
        </form>
    </body>
</html>`;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
Warning

We store an accessToken in the session. If you use CookieSessionStorage, it will be stored unencrypted in the client browser.

The login page has multiple functionality :

  • If the user is already logged in, it redirects to the homepage
  • If the user is not logged in, it displays a login form
  • On form submission with a valid password and username, it redirects to the homepage
  • On form submission with an invalid password and username, it redirects to the login page with an error message

Restrict access to some pages

You could restrict access to some pages with a check to the accessToken in the GET method, but that would mean :

  • extra duplicate code in each restricted page
  • static pages won't be restricted, because they don't have a GET method

The best way to handle this situation is to use a Server Middleware :

import { Context, Next } from "https://deno.land/x/frugal@0.9.6/mod.ts";

const restrictedPages = ["/private"];

export function accessRestrictedPages(context: Context, next: Next<Context>) {
    const url = new URL(context.request.url);

    if (!restrictedPages.includes(url.pathname)) {
        return next(context);
    }

    const accessToken = context.session.get("accessToken");

    if (!isValidAccessToken(accessToken)) {
        return new Response("Forbidden", {
            status: 401,
        });
    }

    return next(context);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

Don't forget to register the middleware in your config

In this middleware :

  • if the URL is not restricted, we delegate to the next middleware
  • if the URL is protected, we check the access token
  • if the access token is invalid, we directly answer with a 401 (we could also redirect to a custom 401 page)
  • if the access token is valid, we delegate to the next middleware

That way, if the page is restricted, it can only be accessed with a valid access token that was obtained via the login page.