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.
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
.
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.
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.
npx teo serve
Send this JSON input to /User/create
to create a user.
{
"create": {
"email": "01@gmail.com",
"password": "Aa123456"
}
}
{
"data": {
"id": 1,
"email": "01@gmail.com"
}
}
Send this JSON input to /User/signIn.
{
"credentials": {
"email": "01@gmail.com",
"password": "Aa123456"
}
}
{
"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.
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
.
{
"credentials": {
"email": "01@gmail.com",
"password": "Aa123456",
"imageAuthToken": "anytoken"
}
}
{
"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.
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
.
{
"credentials": {
"email": "01@gmail.com",
"password": "Aa123456",
"imageAuthToken": "anytoken",
"expired": 2
}
}
{
"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#
{ }
{
"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.
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
.
{
"where": {
"id": 1
},
"update": {
"enabled": false
}
}
{
"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.
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
.
{
"create": {
"email": "02@gmail.com",
"password": "Aa123456",
"thirdPartyId": "myFacebookId",
"thirdPartyToken": "myThirdPartyToken"
}
}
{
"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
.
{
"credentials": {
"thirdPartyId": "myFacebookId",
"thirdPartyToken": "myFacebookToken",
"expired": 99999999999
}
}
{
"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.
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
.
npx teo generate entity
Now let’s create main program file. Create a file named app.ts
in the project directory.
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.
npx ts-node app.ts serve
Let’s recreate the previously created user. Send this to /User/create
.
{
"create": {
"email": "01@gmail.com",
"password": "Aa123456"
}
}
{
"data": {
"id": 1,
"email": "01@gmail.com"
}
}
Let’s send the auth code to this user. Send this JSON input to /AuthCode/create
.
{
"create": {
"email": "01@gmail.com"
}
}
{
"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.
{
"credentials": {
"email": "01@gmail.com",
"authCode": "8736",
"expired": 99999999999
}
}
{
"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.
Top comments (0)