Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: add docs about auth #134

Merged
merged 2 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions packages/docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,6 @@ export default defineConfig({
text: "Middleware",
link: "/building-your-application/routing/middleware",
},
{
text: "Authentication",
link: "/building-your-application/routing/authenticating",
},
{
text: "Internationalization",
link: "/building-your-application/routing/internationalization",
Expand Down Expand Up @@ -217,10 +213,19 @@ export default defineConfig({
{
text: "🔐 Authentication",
collapsed: true,
link: "/building-your-application/authentication/index",
items: [
{
text: "WIP",
link: "/wip",
text: "Authentication",
link: "/building-your-application/authentication/authentication",
},
{
text: "Authorization",
link: "/building-your-application/authentication/authorization",
},
{
text: "Session tracking",
link: "/building-your-application/authentication/session-tracking",
},
],
},
Expand Down Expand Up @@ -334,10 +339,6 @@ export default defineConfig({
link: "/api-reference/extended-html-attributes/ref",
text: "ref",
},
{
link: "/api-reference/extended-html-attributes/serverOnly",
text: "serverOnly",
},
{
link: "/api-reference/extended-html-attributes/skipSSR",
text: "skipSSR",
Expand Down Expand Up @@ -385,5 +386,9 @@ export default defineConfig({
pattern:
"https://github.com/brisa-build/brisa/tree/main/packages/docs/:path",
},
footer: {
message: "Released under the MIT License.",
copyright: "Copyright © 2023-present Aral Roca",
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ description: Use `debounce[Event]` attribute to debounce a server action

## Reference

### `debounceClick={400}`
### `debounce[Event]={400}`

Brisa extends all the HTML element events (`onInput`, `onMouseOver`, `onTouchStart`...) to allow to debounce the action call by replacing the `on` prefix to `debounce`.

```tsx
```tsx 4
<input
type="text"
onInput={(e) => console.log(e.target.value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The value is the generated `IndicatorSignal` by the `indicate` method:
- Read more docs about [`indicate`](/building-your-application/data-fetching/request-context#indicate) in Server Components.
- Read more docs about [`indicate`](/building-your-application/data-fetching/web-context#indicate) in Web Components.

```tsx
```tsx 6
const indicator = indicate('some-action-name')
// ...
<input
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Authentication

User verification confirms the user's identity, occurring during login via credentials like a username-password combination or through a service such as Google. It focuses on validating that users are indeed who they claim to be, safeguarding both user data and the application from unauthorized access or fraudulent activities.

## Authentication Strategies

Contemporary web applications commonly employ various authentication approaches:

1. **OAuth/OpenID Connect (OIDC)**: Facilitates third-party access without disclosing user credentials, ideal for social media logins and Single Sign-On (SSO) solutions. It introduces an identity layer through OpenID Connect.
2. **Credentials-based login (Email + Password)**: A standard choice where users log in using email and password, familiar and straightforward to implement, necessitating robust security measures against threats like phishing.
3. **Passwordless/Token-based authentication**: Use email magic links or SMS one-time codes for secure, password-free access. Popular for its convenience and heightened security, though reliant on user email or phone availability.
4. **Passkeys/WebAuthn**: Utilizes site-specific cryptographic credentials, offering strong protection against phishing. While secure, its newness might pose implementation challenges.

Choosing an authentication approach should align with your application's specific needs, user interface considerations, and security goals.

## Implementing Authentication

In this section, we'll explore the process of adding basic email-password authentication to a web application. While this method provides a fundamental level of security, it's worth considering more advanced options like OAuth or passwordless logins for enhanced protection against common security threats. The authentication flow we'll discuss is as follows:

1. The user submits their credentials through a login form.
2. The form calls a Server Action.
3. Upon successful verification, the process is completed, indicating the user's successful authentication.
4. If verification is unsuccessful, an error message is shown.

Consider a server component with login form where users can input their credentials:

```tsx 9-10
import { navigate, type RequestContext } from "brisa";
import { rerenderInAction } from "brisa/server";
import signIn from "@/utils/auth/sign-in";

export default function LoginPage({}, request: RequestContext) {
const errorMsg = request.store.get("auth-error");

async function authenticate(e) {
const email = e.formData.get("email");
const password = e.formData.get("password");
const success = await signIn(email, password);

if (success) navigate("/admin");

request.store.set("auth-error", "Invalid credentials");
rerenderInAction({ type: "page" });
}

return (
<form onSubmit={authenticate}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Login</button>
{errorMsg && <p>{errorMsg}</p>}
</form>
);
}
```

The form above has two input fields for capturing the user's `email` and `password`. On submission, it calls the `authenticate` Server Action. You can then call your Authentication Provider's API in the Server Action to handle authentication:

```tsx
const success = await signIn(email, password);
```

In this code, the `signIn` method checks the credentials against stored user data.
After the authentication provider processes the credentials, there are two possible outcomes:

- **Successful Authentication**: This outcome implies that the login was successful. Further actions, such as accessing protected routes and fetching user information, can then be initiated.
- **Failed Authentication**: In cases where the credentials are incorrect or an error is encountered, the function returns a corresponding error message to indicate the authentication failure.

Finally, you can `navigate` to another page or use the request `store` to save form errors, and use `rerenderInAction` to render these errors on the form:

```tsx
if (success) navigate("/admin");

request.store.set("auth-error", "Invalid credentials");
rerenderInAction({ type: "page" });
```

> [!IMPORTANT]
>
> Both `navigate` and `rerenderInAction` throw an exception returning a [Never](https://www.typescriptlang.org/docs/handbook/basic-types.html#never) type, therefore the code after is not executed so there is no need to put an `else` conditional. It is important to keep this in mind because if it is put in a `try-catch` they would stop working unless from the catch you throw again these actions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Authorization

Once a user is authenticated, you'll need to ensure the user is allowed to visit certain routes, and perform operations such as mutating data with Server Actions and calling Route Handlers.

## Protecting Routes with Middleware

[Middleware](/building-your-application/routing/middleware) in Brisa helps you control who can access different parts of your website. This is important for keeping areas like the user dashboard protected while having other pages like marketing pages be public. It's recommended to apply Middleware across all routes and specify exclusions for public access.

Here's how to implement Middleware for authentication in Brisa:

`src/middleware.ts`:

```tsx
import type { RequestContext } from "brisa";
import parseCookies from "@/utils/auth/parse-cookies";

export default async function middleware(req: RequestContext) {
// Early return for assets (no route) and api endpoints
if (!req.route || req.route.name.startsWith("/api/")) return;

const cookies = parseCookies(req.headers.get("cookie"));
const currentUser = cookies.get("currentUser");
const pathname = req.route.pathname ?? "";

if (currentUser && !pathname.startsWith("/dashboard")) {
return new Response("", {
status: 302,
headers: {
Location: new URL("/dashboard", req.url).toString(),
},
});
}

if (!currentUser && !pathname.startsWith("/login")) {
return new Response("", {
status: 302,
headers: {
Location: new URL("/login", req.url).toString(),
},
});
}
// ...
}
```

This example returns a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) for handling redirects early in the request pipeline, making it efficient and centralizing access control.

> [!NOTE]
>
> In Brisa the middleware works in a different way than in many frameworks. If you return nothing or `undefined`, it continues processing the route as if it had not entered the middleware, similar to the `next()` function of many frameworks. Another thing you can return, is a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), then the middleware cuts and finishes processing the route here. If what you want to do is to modify the response headers that route will have, you have to do it using the [`responseHeaders`](/building-your-application/routing/middleware#on-response) method.

After successful authentication, it's important to manage user navigation based on their roles. For example, an admin user might be redirected to an admin dashboard, while a regular user is sent to a different page. This is important for role-specific experiences and conditional navigation, such as prompting users to complete their profile if needed.

When setting up authorization, it's important to ensure that the main security checks happen where your app accesses or changes data. While Middleware can be useful for initial validation, it should not be the sole line of defense in protecting your data. The bulk of security checks should be performed in the Data Access Layer (DAL).

This approach advocates for consolidating all data access within a dedicated DAL. This strategy ensures consistent data access, minimizes authorization bugs, and simplifies maintenance. To ensure comprehensive security, consider the following key areas:

- **Server Actions**: Implement security checks in server-side processes, especially for sensitive operations.
- **Route Handlers**: Manage incoming requests with security measures to ensure access is limited to authorized users.
- **Data Access Layer (DAL)**: Directly interacts with the database and is crucial for validating and authorizing data transactions. It's vital to perform critical checks within the DAL to secure data at its most crucial interaction point—access or modification.

## Protecting Server Actions

It is important to treat [Server Actions](/building-your-application/data-fetching/server-actions) with the same security considerations as public-facing API endpoints. Verifying user authorization for each action is crucial. Implement checks within Server Actions to determine user permissions, such as restricting certain actions to admin users.

In the example below, we check the user's role before allowing the action to proceed:

```tsx 6-8,13-14,16
import type { RequestContext } from "brisa";
import parseCookies from "@/utils/auth/parse-cookies";
import db from "./lib/db";

async function getSession(req: RequestContext) {
const cookies = parseCookies(req.headers.get("cookie"));
const sessionId = cookies.get("sessionId")?.value;
return sessionId ? await db.findSession(sessionId) : null;
}

export default function AdminDashboard({}, request: RequestContext) {
async function someAction(e) {
const session = await getSession(request);
const userRole = session?.user?.role;

if (userRole !== "admin") {
throw new Error(
"Unauthorized access: User does not have admin privileges.",
);
}
// ...
}

return (
<div>
<button onClick={someAction}>Run an action</button>
</div>
);
}
```

> [!NOTE]
>
> The component `request` param is different when rendered during the SSR than when the action is called. When the action is called, it can be used as the request of the action.

## Protecting Route Handlers

Route Handlers in Brisa play a vital role in managing incoming requests. Just like Server Actions, they should be secured to ensure that only authorized users can access certain functionalities. This often involves verifying the user's authentication status and their permissions.

Here's an example of securing a Route Handler:

`src/api/route.ts`:

```ts 6-8,13
import type { RequestContext } from "brisa";
import parseCookies from "@/utils/auth/parse-cookies";
import db from "./lib/db";

async function getSession(req: RequestContext) {
const cookies = parseCookies(req.headers.get("cookie"));
const sessionId = cookies.get("sessionId")?.value;
return sessionId ? await db.findSession(sessionId) : null;
}

export async function GET(request: RequestContext) {
// User authentication and role verification
const session = await getSession(request);

// Check if the user is authenticated
if (!session) {
// User is not authenticated
return new Response(null, { status: 401 });
}

// Check if the user has the 'admin' role
if (session.user.role !== "admin") {
// User is authenticated but does not have the right permissions
return new Response(null, { status: 403 });
}

// Data fetching for authorized users
}
```

## Protecting Server Components

Like actions and API routes, you can manage authorization within [Server Components](/docs/app/building-your-application/rendering/server-components). Server Components in Brisa are designed for server-side execution and offer a secure environment for integrating complex logic like authorization. They enable direct access to back-end resources, optimizing performance for data-heavy tasks and enhancing security for sensitive operations.

In Server Components, a common practice is to conditionally render UI elements based on the user's role. This approach enhances user experience and security by ensuring users only access content they are authorized to view.

```tsx 11-13,17
import type { RequestContext } from "brisa";
import parseCookies from "@/utils/auth/parse-cookies";
import AdminDashboard from "@/components/admin-dashboard";
import UserDashboard from "@/components/user-dashboard";
import AccessDenied from "@/components/access-denied";
import db from "./lib/db";

type Props = {
/* .... */
};

async function getSession(req: RequestContext) {
const cookies = parseCookies(req.headers.get("cookie"));
const sessionId = cookies.get("sessionId")?.value;
return sessionId ? await db.findSession(sessionId) : null;
}

export default async function Dashboard(props: Props, request: RequestContext) {
const session = await getSession(request);
const userRole = session?.user?.role; // Assuming 'role' is part of the session object

if (userRole === "admin") {
return <AdminDashboard />; // Component for admin users
} else if (userRole === "user") {
return <UserDashboard />; // Component for regular users
} else {
return <AccessDenied />; // Component shown for unauthorized access
}
}
```
33 changes: 3 additions & 30 deletions packages/docs/building-your-application/authentication/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,8 @@ description: Learn how to implement authentication in Brisa covering best practi

To incorporate authentication in Brisa, acquaint yourself with three fundamental principles:

- **[Authentication](#authentication) (Identity Verification)** ensures the user's claimed identity. It mandates users to validate their identity with possessions like a username and password.
- **[Session Tracking](#session-tracking)** monitors the user's status (e.g., logged in) across various requests.
- **[Authorization](#authorization)** determines the application areas accessible to the user..
- **[Authentication](/building-your-application/authentication/authentication) (Identity Verification)** ensures the user's claimed identity. It mandates users to validate their identity with possessions like a username and password.
- **[Authorization](/building-your-application/authentication/authorization)** determines the application areas accessible to the user..
- **[Session Tracking](/building-your-application/authentication/session-tracking)** monitors the user's status (e.g., logged in) across various requests.

This page illustrates the utilization of Brisa features to implement prevalent patterns in authentication, authorization, and session management. This empowers you to select optimal solutions tailored to your application's requirements.

## Authentication

User verification confirms the user's identity, occurring during login via credentials like a username-password combination or through a service such as Google. It focuses on validating that users are indeed who they claim to be, safeguarding both user data and the application from unauthorized access or fraudulent activities.

### Authentication Strategies

Contemporary web applications commonly employ various authentication approaches:

1. **OAuth/OpenID Connect (OIDC)**: Facilitates third-party access without disclosing user credentials, ideal for social media logins and Single Sign-On (SSO) solutions. It introduces an identity layer through OpenID Connect.
2. **Credentials-based login (Email + Password)**: A standard choice where users log in using email and password, familiar and straightforward to implement, necessitating robust security measures against threats like phishing.
3. **Passwordless/Token-based authentication**: Use email magic links or SMS one-time codes for secure, password-free access. Popular for its convenience and heightened security, though reliant on user email or phone availability.
4. **Passkeys/WebAuthn**: Utilizes site-specific cryptographic credentials, offering strong protection against phishing. While secure, its newness might pose implementation challenges.

Choosing an authentication approach should align with your application's specific needs, user interface considerations, and security goals.

### Implementing Authentication

TODO

## Session Tracking

TODO

## Authorization

TODO
Loading
Loading