DEV Community

Bahadır Kurul for Retter

Posted on

Creating Simple Authentication With Rio

We will have 2 classes, Authenticator and User. Authenticator will be used for login and sending OTP mail. User will be used for keep user info for each user. Lets begin.
Image description

Authenticator Class

Authenticator will get an email on initial payload. On init we will save that email to state. And sendOTP will generate otp and send it to the email from state.

Dependencies

We will use mailjet to send otp mails and zod for models.

"node-mailjet": "5.1.1",
"zod": "3.19.1"
Enter fullscreen mode Exit fullscreen mode

Models

Turn this zod models into Rio models by following instructions on this link.

// classes/Authenticator/types.ts

import { z } from 'zod'

export const initInputModel = z.object({
    email: z.string().email()
})

export const loginModel = z.object({
    otp: z.number()
})

// Optional
export const privateState = z.object({
    email: z.string().email(),
    otp: z.number()
})

export type InitInputModel = z.infer<typeof initInputModel>
export type Login = z.infer<typeof loginModel>
export type PrivateState = z.infer<typeof privateState>
Enter fullscreen mode Exit fullscreen mode

Template.yml

You have to convert zod models above to rio models to use as an input model. As you can see we have models for init and login function.

# classes/Authenticator/template.yml

init: 
  handler: index.init
  inputModel: AuthInitInputModel
getInstanceId: index.getInstanceId
getState: index.getState
methods:
  - method: login
    type: WRITE
    inputModel: LoginModel
    handler: index.login

  - method: sendOTP
    type: WRITE
    handler: index.sendOTP
Enter fullscreen mode Exit fullscreen mode

Index.ts

We will use mailjet to send mails. So let's configure mailjet.

// classes/Authenticator/index.ts

import mailjet from 'node-mailjet'

const mailjetClient = new mailjet({
    apiKey: 'YOUR_API_KEY',
    apiSecret: 'YOUR_API_SECRET'
});
Enter fullscreen mode Exit fullscreen mode

Init Function

We are getting email here and saving it to state, later we will get email from state.

// classes/Authenticator/index.ts

export async function init(data: Data): Promise<Data> {
    data.state.private.email = data.request.body.email
    return data
}
Enter fullscreen mode Exit fullscreen mode

GetInstanceId Function

Making instanceId same as email, this way we will not create a billion instances.

// classes/Authenticator/index.ts

export async function getInstanceId(data: Data): Promise<string> {
    return data.request.body.email
}
Enter fullscreen mode Exit fullscreen mode

sendOTP Function

Then lets create a function that handles sending mails. This function gets email from state. And generating a six digit otp.

// classes/Authenticator/index.ts

export async function sendOTP(data: Data): Promise<Data> {
    try {
        const { email } = data.state.private
        const otp = Math.floor(100000 + Math.random() * 900000)
        await mailjetClient
            .post("send", { 'version': 'v3.1' })
            .request({
                "Messages": [
                    {
                        "From": {
                            "Email": "bahadir@rettermobile.com",
                            "Name": "Bahadır"
                        },
                        "To": [
                            {
                                "Email": email
                            }
                        ],
                        "Subject": "Greetings from Retter.",
                        "TextPart": "OTP Validation Email",
                        "HTMLPart": `OTP: ${otp}`,
                        "CustomID": "AppGettingStartedTest"
                    }
                ]
            })
        data.state.private.otp = otp
        data.response = {
            statusCode: 200,
            body: { emailSent: true },
        }
    } catch (error) {
        console.log(error)
        data.response = {
            statusCode: 400,
            body: { error: error.message },
        };
    }
    return data
}
Enter fullscreen mode Exit fullscreen mode

Login Function

First checking if received otp is a match. If not just throws an error.

// classes/Authenticator/index.ts

const { otp: recivedOtp } = data.request.body as Login
const { otp, email } = data.state.private as PrivateState

if (recivedOtp !== otp) throw new Error(`OTP is wrong`);
Enter fullscreen mode Exit fullscreen mode

After checking otp We are looking for an instance in User class with our email lookup key. If can't find it we are initializing a new instance. Checking if maybe initialization failed.

Giving email inside body because when user class initializing it will set email lookup key with this email and we will keep that email in state.

// classes/Authenticator/index.ts

// Get existing USER INSTANCE
let getUser = await rdk.getInstance({
    classId: "User",
    body: {
        email
    },
    lookupKey: {
        name: "email",
        value: email
    }
})

if (getUser.statusCode > 299) {
    // CREATE USER INSTANCE -> User class will connect email as lookup key itself
    getUser = await rdk.getInstance({
        classId: "User",
        body: {
        email
    }
    })

    if (getUser.statusCode > 299) throw new Error('Couldnt create user instance')
}
Enter fullscreen mode Exit fullscreen mode

After that, generating custom token and Using instanceId as userId and returning it inside body.

// classes/Authenticator/index.ts

const customToken = await rdk.generateCustomToken({
    userId: getUser.body.instanceId,
    identity: 'enduser'
})

data.state.private.relatedUser = getUser.body.instanceId
data.state.private.otp = undefined
data.response = {
    statusCode: 200,
    body: customToken
};
Enter fullscreen mode Exit fullscreen mode

Authorizer Function

Here we are allowing init and get. Allowing getting state if developer because state keeps ours otp. For login and sendOTP checking if we have instanceId, æction is "CALL".

// classes/Authenticator/index.ts

export async function authorizer(data: Data): Promise<Response> {
    const { methodName, identity, instanceId, action } = data.context

    switch (methodName) {  
        case 'INIT':
        case 'GET': {
            return { statusCode: 200 }
        }

        case 'STATE': {
            if (identity === 'developer') return { statusCode: 200 }
        }

        case 'login':
        case 'sendOTP': {
            if (instanceId && action === 'CALL') return { statusCode: 200 }
        }
    }

    return { statusCode: 403 };
}
Enter fullscreen mode Exit fullscreen mode

User Class

Dependencies

I used uuid library to generate userId's. Instance id's will be created with this.

"uuid": "9.0.0",
"zod": "3.19.1"
Enter fullscreen mode Exit fullscreen mode

Models

Turn this zod models into Rio models by following instructions on this link.

// classes/User/types.ts

import { z } from 'zod'

export const privateState = z.object({
    email: z.string().email(),
    userId: z.string()
})

export const userInitModel = z.object({
    email: z.string().email()
})

export type PrivateState = z.infer<typeof privateState>
export type UserInitModel = z.infer<typeof userInitModel>
Enter fullscreen mode Exit fullscreen mode

Template.yml

You have to convert zod models above to rio models to use as an input model. We have input model for init function here.

# classes/User/template.yml

init: 
  handler: index.init
  inputModel: UserInitInputModel
getState: index.getState
getInstanceId: index.getInstanceId
methods:
  - method: getProfile
    type: READ
    handler: index.getProfile
Enter fullscreen mode Exit fullscreen mode

Index.ts

GetInstanceId Function

Generating userId and making it instance id.

// classes/User/index.ts

import { v4 as uuidv4 } from 'uuid';

export async function getInstanceId(): Promise<string> {
    return uuidv4()
}
Enter fullscreen mode Exit fullscreen mode

Init Function

Saving email and userId to state. And setting lookup key so we can find this instance with email.

// classes/User/index.ts

export async function init(data: Data): Promise<Data> {
    const { email } = data.request.body as UserInitModel
    data.state.private = {
        email,
        userId: data.context.instanceId
    } as PrivateState
    await rdk.setLookUpKey({ key: { name: 'email', value: email } })
    return data
}
Enter fullscreen mode Exit fullscreen mode

GetProfile Function

This function just returns state because user data is in state.

// classes/User/index.ts

export async function getProfile(data: Data): Promise<Data> {
    data.response = {
        statusCode: 200,
        body: data.state.private,
    };
    return data;
}
Enter fullscreen mode Exit fullscreen mode

Authorizer Function

State keeps user data so we don't want everyone to access it. And allowing getProfile if it fulfills the requirements.

// classes/User/index.ts

export async function authorizer(data: Data): Promise<Response> {
    const { identity, methodName,instanceId, userId } = data.context
    if (identity === "developer" && methodName === "getState") {
        return { statusCode: 200 };
    }
    if (identity === "enduser" && methodName === "getProfile" && userId === instanceId) {
        return { statusCode: 200 };
    }
    return { statusCode: 403 };
}
Enter fullscreen mode Exit fullscreen mode

Here is the all code for the project .

Now you have an idea of how to create an authentication system. Im glad if I helped. Thanks!

References

Rio Docs
Zod Github
Mailjet Github
Uuid Github

Top comments (0)