Authorization
Learning objectives
- You know the term authorization.
- You know the implications of broken authorization.
In the part on authentication, once a user had authenticated, they had access to all the resources that were not available for non-authenticated users. This situation should not hold in all cases. As an example, if there are resources that should be available only to a specific user, then, everyone should not be able to access those resources.
As an example, consider an online bank. After authenticating into the online back, the user should only be able to access the bank accounts that belong to the user. The ability to access all bank accounts in the online bank would indicate broken authorization.
The term authorization refers to the process of verifying that the user has the rights to perform the actions that the user is trying to perform. In addition, it also refers to the process of defining access rights to specific resources in an application.
Two passwords and two resources
Let's start with an example of broken authorization. The following application has two different passwords with which the user can authenticate. One of the passwords redirects the user to "/secrets/1", while the other redirects the user to "/secrets/2".
For simplicity, the path "/secrets/:id" shows a string with the path variable id
. The path also uses session-based authentication to assess that the user has authenticated.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
import {
getSignedCookie,
setSignedCookie,
} from "https://deno.land/x/hono@v3.12.11/helper.ts";
const app = new Hono();
const secret = "secret";
const sessions = new Set();
app.get("/", (c) =>
c.html(`<form method="POST" action="/authenticate">
<input type="password" name="password" />
<input type="submit" />
</form>`));
app.post("/authenticate", async (c) => {
const body = await c.req.parseBody();
if (
body.password !== "very secret password" &&
body.password !== "super secret password"
) {
return c.text("Invalid password, you are not authenticated!", 401);
}
const sessionId = crypto.randomUUID();
await setSignedCookie(c, "sessionId", sessionId, secret, {
path: "/",
});
sessions.add(sessionId);
if (body.password === "very secret password") {
return c.redirect("/secrets/1");
} else if (body.password === "super secret password") {
return c.redirect("/secrets/2");
}
});
app.get("/secrets/:id", async (c) => {
const sessionId = await getSignedCookie(c, secret, "sessionId");
if (!sessionId || !sessions.has(sessionId)) {
return c.text("You have not authenticated!", 401);
}
return c.text(`Here's the secret ${c.req.param("id")}`);
});
Deno.serve(app.fetch);
On the surface level, from the point of a view of a user who does not adjust the addresses, it seems that the application allows us to see only the secrets that we have access to. However, if -- once authenticated -- we change the path in the browser address bar, we see that we are able to access the secret that relates to the other password as well.
The application does not verify that the user has the rights to access the resource.
Controlling access to resources
The above example demonstrates broken access control. Resources intended for a specific user (or a password) should be available only to that specific user. In the case of the above example, the issue lies in what information is stored about the user, as well as in that there is no functionality that verifies whether the user should have access to a specific resource.
One way to fix this would be to create a pre-defined map that would provide information on which resources a specific password could access, and then -- upon authentication -- map the specific resources to user sessions.
This would mean that the application would have to know all the resources that are available in the application, and the application would have to know which resources are available to which users. This approach is not scalable, and it is not feasible in practice. Nevertheless, it allows us to demonstrate access control.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
import {
getSignedCookie,
setSignedCookie,
} from "https://deno.land/x/hono@v3.12.11/helper.ts";
const app = new Hono();
const secret = "secret";
const sessionResourceAccess = new Map();
const passwordMap = new Map();
passwordMap.set("very secret password", "1");
passwordMap.set("super secret password", "2");
app.get("/", (c) =>
c.html(`<form method="POST" action="/authenticate">
<input type="password" name="password" />
<input type="submit" />
</form>`));
app.post("/authenticate", async (c) => {
const body = await c.req.parseBody();
if (!passwordMap.has(body.password)) {
return c.text("Invalid password, you are not authenticated!", 401);
}
const sessionId = crypto.randomUUID();
await setSignedCookie(c, "sessionId", sessionId, secret, {
path: "/",
});
const resourceId = passwordMap.get(body.password);
sessionResourceAccess.set(sessionId, resourceId);
return c.redirect(`/secrets/${resourceId}`);
});
app.get("/secrets/:id", async (c) => {
const sessionId = await getSignedCookie(c, secret, "sessionId");
if (!sessionId || !sessionResourceAccess.has(sessionId)) {
return c.text("You have not authenticated!", 401);
}
const askedForResource = c.req.param("id");
const accessToResource = sessionResourceAccess.get(sessionId);
if (askedForResource !== accessToResource) {
return c.text("You are not authorized!", 401);
}
return c.text(`Here's the secret ${c.req.param("id")}`);
});
Deno.serve(app.fetch);
Need a better way...
Here, we've looked into the concept of authentication and authorization. The passwords have been hard coded and the resources have been hard coded. This is not a scalable solution and we need a better way to do this.
This is where user management functionality and middlewares come to help. Let's look into what they are next.