Creating Simple Authentication With Rio

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"

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>

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

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'
});

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
}

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
}

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
}

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`);

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')
}

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
};

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 };
}

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"

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>

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

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()
}

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
}

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;
}

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 };
}

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

Total
1
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
generate-request-bodies-automatically-to-accelerate-api-debugging

Generate REQUEST bodies automatically to accelerate API debugging

Next Post
we-all-need-good-feedback

We All Need Good Feedback

Related Posts