Implement user authentication with Node.js and Teo in 5 lines of code
Tags: node, authentication, teo, webdev
Published: 2024-03-17 14:11:00

---
title: Implement user authentication with Node.js and Teo in 5 lines of code
published: true
description: Authentication is vital to modern server apps. Teo simplifies the implementation of authentication. Authentication includes handling user signing in sessions, generate and validate user’s API tokens.
tags: Nodejs, Authentication, Teo, Webdevelopment
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u4nh1kfdk4xqhmc8s7rj.png
published_at: 2024-03-17 14:11 +0000
---

![Teo logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u4nh1kfdk4xqhmc8s7rj.png)

Teo is a schema-centered web framework for Node.js, Python and Rust. It’s very concise and declarative. In this article, we’re going to use the Node.js version.

Authentication is vital to modern server apps. Teo simplifies the implementation of authentication. Authentication includes handling user signing in sessions, generate and validate user’s API tokens.

## Setup the project

To setup a project, install the dependencies and do programming language specific setups. In the first article in the series, we explained what each step does in this process. In this tutorial, we just include the command for pasting.

```sh
mkdir hello-teo-authentication
cd hello-teo-authentication
npm init -y
npm install typescript ts-node -D
npx tsc --init
npm install @teocloud/teo
```

## Password

Let’s create a simple password authentication. Paste this into a new file named `schema.teo`.

```teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5052)
}
 
@identity.tokenIssuer($identity.jwt(expired: 3600 * 24 * 365))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker($get(.value).presents.bcrypt.verify($self.get(.password).presents))
  password: String
 
  include handler identity.signIn
  include handler identity.identity
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]
```

Create a dot env file `.env` in the same directory.

```sh
JWT_SECRET=my_top_secret
```

Let’s explain the newly introduced decorators and pipeline items one by one.

* The authentication functionalities reside in Teo’s std.identity namespace
* `@identity.id` specifies which field is used to fetch the user
* `@identity.checker` specifies which field is used to validate credentials and how to validate
* `@bcrypt.salt` performs a Bcrypt salting transform
* `@bcrypt.verify` verifies the input value against the stored one
* `@writeonly` hides the field value from the output
* `@identity.tokenIssuer` specifies which type of token it generates
* `$identity.jwt` performs the JWT token generation
* `identity.identityFromJwt` middleware decodes the JWT token and set the identity to the request
* `identity.signIn` is the template which defines the signIn handler
* `identity.identity` is the template which fetches the user information from the token in the header

Let’s start the server and perform a signIn request.

```sh
npx teo serve
```

Send this JSON input to `/User/create` to create a user.

```json
{
  "create": {
    "email": "01@gmail.com",
    "password": "Aa123456"
  }
}
```
```json
{
  "data": {
    "id": 1,
    "email": "01@gmail.com"
  }
}
```

Send this JSON input to /User/signIn.

```json
{
  "credentials": {
    "email": "01@gmail.com",
    "password": "Aa123456"
  }
}
```
```json
{
  "data": {
    "id": 1,
    "email": "01@gmail.com"
  },
  "meta": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6eyJpZCI6MX0sIm1vZGVsIjpbIlVzZXIiXSwiZXhwIjoxNzQyMTI0NDk2fQ.x2DSIpdnUeJtsUOGQsHlGksr29aF-CWog6X5LILxsOc"
  }
}
```

We’ve create a token from this user. If you intentionally type the password wrongly, an error response is returned.

## Companion validations

Practically only something like username and pasword are not enough. A lot of websites and apps integrates some third party image authentications to prevent non-human access. As a framework, Teo doesn’t integrate with any service providers. Instead, it’s easy to integrate any third-party service or identity with Teo.
Replace the content of `schema.teo` with this.

```teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5052)
}
 
@identity.tokenIssuer($identity.jwt(expired: 3600 * 24 * 365))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker(
    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))
    .do($get(.companions).presents.get(.imageAuthToken).presents))
  password: String
  @virtual @writeonly @identity.companion
  imageAuthToken: String?
 
  include handler identity.signIn
  include handler identity.identity
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]
```

We created a `@identity.companion` field which is `@virtual`. A virtual field isn't stored into the database. Companion values are present when checking and validating against the checker value. In this simple example, we just ensure the image auth token value exists.
Restart the server and send this JSON input to `/User/signIn`.

```json
{
  "credentials": {
    "email": "01@gmail.com",
    "password": "Aa123456",
    "imageAuthToken": "anytoken"
  }
}
```
```json
{
  "data": {
    "id": 1,
    "email": "01@gmail.com"
  },
  "meta": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6eyJpZCI6MX0sIm1vZGVsIjpbIlVzZXIiXSwiZXhwIjoxNzQyMTI2MDIwfQ.7e3gXp5zA_h-Yk4ClUhjIIZLL4sLXAIFpE3CDzL_gzs"
  }
}
```

Without the image auth token, this `signIn` request would fail.

## Custom expiration interval

The token expiration interval can be dynamic instead of static. Let’s try an example of frontend passed expiration interval. Replace the content of `schema.teo` with this.

```teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5052)
}
 
@identity.tokenIssuer($identity.jwt(expired: $get(.expired).presents))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker(
    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))
    .do($get(.companions).presents.get(.imageAuthToken).presents))
  password: String
  @virtual @writeonly @identity.companion
  imageAuthToken: String?
  @virtual @writeonly @identity.companion
  expired: Int64?
 
  include handler identity.signIn
  include handler identity.identity
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]
```

Starts the server again and send this to `/User/signIn`.

```json
{
  "credentials": {
    "email": "01@gmail.com",
    "password": "Aa123456",
    "imageAuthToken": "anytoken",
    "expired": 2
  }
}
```
```json
{
  "data": {
    "id": 1,
    "email": "01@gmail.com"
  },
  "meta": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6eyJpZCI6MX0sIm1vZGVsIjpbIlVzZXIiXSwiZXhwIjoxNzEwNTk4MjQ5fQ.Oz9O2rB-usonrdfzt8q75vr0biOf_C6Y3JIaf5O3MAE"
  }
}
```

Since this token is valid for 2 seconds, just paste the token to the header and send this empty JSON input to `/User/identity`.

**Headers**:

* Authorization: Bearer `#your token#`

```json
{ }
```
```json
{
  "error": {
    "type": "Unauthorized",
    "message": "token expired"
  }
}
```

## Blocked account

Practically accounts can be blocked from signing in. Implement account blocking is quite easy. Just tell us what does it mean by invalid account.

Replace the content of `schema.teo` with this.

```teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5052)
}
 
@identity.tokenIssuer($identity.jwt(expired: $get(.expired).presents))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
@identity.validateAccount($get(.enabled).presents.eq(true))
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker(
    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))
    .do($get(.companions).presents.get(.imageAuthToken).presents))
  password: String
  @virtual @writeonly @identity.companion
  imageAuthToken: String?
  @virtual @writeonly @identity.companion
  expired: Int64?
  @migration(default: true)
  enabled: Bool
 
  include handler identity.signIn
  include handler identity.identity
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]
```

Let’s disable the previously created account. Send this JSON input to `/User/update`.

```json
{
  "where": {
    "id": 1
  },
  "update": {
 "enabled": false
  }
}
```
```json
{
  "data": {
    "id": 1,
    "email": "01@gmail.com",
    "enabled": false
  }
}
```

Now the `signIn` handler doesn't work for him and token authentication always failed.

## Third-party integration

Teo provides an easy way for developers to integrate with third party identity services such as signing in with Google, Facebook. For China developers, this may be something like signing in with WeChat.

Update the the content of `schema.teo` with this.

```teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5052)
}
 
@identity.tokenIssuer($identity.jwt(expired: $get(.expired).presents))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
@identity.validateAccount(
  $message($get(.enabled).presents.eq(true), "this account is blocked"))
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker(
    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))
    .do($get(.companions).presents.get(.imageAuthToken).presents))
  password: String
  @virtual @writeonly @identity.companion
  imageAuthToken: String?
  @virtual @writeonly @identity.companion
  expired: Int64?
  @migration(default: true) @default(true)
  enabled: Bool
  @identity.id @unique
  thirdPartyId: String?
  @virtual @writeonly @identity.checker($get(.value).presents.valid)
  thirdPartyToken: String?
 
  include handler identity.signIn
  include handler identity.identity
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]
```

Restart the server and let’s create a new account with third party account binded by sending this input to `/User/create`.

```json
{
  "create": {
    "email": "02@gmail.com",
    "password": "Aa123456",
    "thirdPartyId": "myFacebookId",
    "thirdPartyToken": "myThirdPartyToken"
  }
}
```
```json
{
  "data": {
    "id": 2,
    "email": "02@gmail.com",
    "enabled": true,
    "thirdPartyId": "myFacebookId"
  }
}
```

Now try signing in with the third party id and token. Send this input to `/User/signIn`.

```json
{
  "credentials": {
    "thirdPartyId": "myFacebookId",
    "thirdPartyToken": "myFacebookToken",
    "expired": 99999999999
  }
}
```
```json
{
  "data": {
    "id": 2,
    "email": "02@gmail.com",
    "enabled": true,
    "thirdPartyId": "myFacebookId"
  },
  "meta": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6eyJpZCI6Mn0sIm1vZGVsIjpbIlVzZXIiXSwiZXhwIjoxMDE3MTA2MDMwOTJ9.LcOzp8DFToXDQEBtu8jMtnQp-BndAazsTdBT2i4Mi3U"
  }
}
```

For the sake of simplicity, let’s just use `$valid` to make every third party token valid. In the next section, we'll demonstrate how to create a custom pipeline item to validate user's credential input.

## Phone number and auth code

Let’s add some complexity by introducing phone number and auth code. Replace the content of `schema.teo` with this.

```teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5052)
}
 
entity {
  provider: .node,
  dest: "./entities"
}
 
declare pipeline item validateAuthCode<T>: T -> String
 
@identity.tokenIssuer($identity.jwt(expired: $get(.expired).presents))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
@identity.validateAccount(
  $message($get(.enabled).presents.eq(true), "this account is blocked"))
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  @presentWithout(.phoneNumber)
  email: String?
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker(
    $do($get(.value).presents.bcrypt.verify($self.get(.password).presents))
    .do($get(.companions).presents.get(.imageAuthToken).presents))
  password: String?
  @virtual @writeonly @identity.companion
  imageAuthToken: String?
  @virtual @writeonly @identity.companion
  expired: Int64?
  @migration(default: true) @default(true)
  enabled: Bool
  @identity.id @unique
  thirdPartyId: String?
  @virtual @writeonly @identity.checker($get(.value).presents.valid)
  thirdPartyToken: String?
  @onSet($if($presents, $regexMatch(/\\+?[0-9]+/))) @identity.id
  @presentWithout(.email) @unique
  phoneNumber: String?
  @virtual @writeonly @identity.checker($validateAuthCode)
  authCode: String?
 
  include handler identity.signIn
  include handler identity.identity
}
 
model AuthCode {
  @id @autoIncrement @readonly
  id: Int
  @presentWithout(.phoneNumber) @unique
  email: String?
  @presentWithout(.email) @unique
  phoneNumber: String?
  @onSave($randomDigits(4))
  code: String
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]
```

Generate entities for writing our custom pipeline item `$validateAuthCode`.

```sh
npx teo generate entity
```

Now let’s create main program file. Create a file named `app.ts` in the project directory.

```typescript
import { App } from '@teocloud/teo'
import { AuthCodeWhereUniqueInput, Teo, User } from './entities'
 
const app = new App()
 
app.mainNamespace().defineValidatorPipelineItem(
    "validateAuthCode", 
    async (checkArgs: any, _, user: User, teo: Teo) => {
        const finder: AuthCodeWhereUniqueInput = {}
        if (checkArgs.ids.email) {
            finder.email = user.email!
        }
        if (checkArgs.ids.phoneNumber) {
            finder.phoneNumber = user.phoneNumber!
        }
        const authCode = await teo.authCode.findUnique({
            where: finder
        })
        if (!authCode) {
            return "auth code not found"
        }
        if (authCode.code !== checkArgs.value) {
            return "auth code is wrong"
        }
})
 
app.run()
```

Notice that we changed the type of email field from `String` to `String?`. For databases other than SQLite is all ok. However, SQLite disallows altering table columns. Using SQLite is for our demo purpose as it doesn't require installation. Just delete the `database.sqlite` file and let's have a new database setup.

Start the server with the updated command.

```sh
npx ts-node app.ts serve
```

Let’s recreate the previously created user. Send this to `/User/create`.

```json
{
  "create": {
    "email": "01@gmail.com",
    "password": "Aa123456"
  }
}
```
```json
{
  "data": {
    "id": 1,
    "email": "01@gmail.com"
  }
}
```

Let’s send the auth code to this user. Send this JSON input to `/AuthCode/create`.

```json
{
  "create": {
    "email": "01@gmail.com"
  }
}
```
```json
{
  "data": {
    "id": 1,
    "email": "01@gmail.com",
    "code": "8736"
  }
}
```

In practice, mark the `code` field with `@writeonly` to hide it from the output, and set an expire time.

Now try to sign in with this auth code. Send this to `/User/signIn`. Remember to replace the auth code with the one that you got.

```json
{
  "credentials": {
    "email": "01@gmail.com",
    "authCode": "8736",
    "expired": 99999999999
  }
}
```
```json
{
  "data": {
    "id": 1,
    "email": "01@gmail.com",
    "enabled": true
  },
  "meta": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6eyJpZCI6MX0sIm1vZGVsIjpbIlVzZXIiXSwiZXhwIjoxMDE3MTA2MTE5NDV9.dfO7oJ4Ka11ZfzLqjjxU2XMv83lq6qc1Ijv5WUSz5Ac"
  }
}
```

Now we can sign in with email and password, email and auth code, phone number with password, phone number with auth code. Together with the third party account bindings.