Kotlin Multiplatform — Login|Authenticate|User Info (Server Side)

Pradyot Prakash
9 min read2 days ago

--

Photo by Ed Hardie on Unsplash

In our previous article we saw how to implement security and authentication into our Ktor server and also added the registration flow for new users.

Check the below link for the article

In this article, we will implement the Login, Authenticate and User Info routes for the user.

Since most the setup we did in the previous article have made our work easy so it will be fast to implement all these features.

Let’s start with Login.

Create an interface in server/features/authentication/controllers/login named LoginController

interface LoginController {
suspend fun loginUser(
call: ApplicationCall,
hashingService: HashingService,
tokenService: TokenService,
userDataSource: UserDataSource,
tokenConfig: TokenConfig,
)
}

Create a class in server/features/authentication/controllers/login named LoginControllerImplementation

class LoginControllerImplementation : LoginController {
override suspend fun loginUser(
call: ApplicationCall,
hashingService: HashingService,
tokenService: TokenService,
userDataSource: UserDataSource,
tokenConfig: TokenConfig,
) {
delay(API_RESPONSE_DELAY)

val loginRequest = call.receive<LoginRequest>()

if (loginRequest.username == null && loginRequest.emailAddress == null && loginRequest.phoneNumber == null) {
throw InvalidParameter(
message = Localization.USERNAME_OR_EMAIL_OR_PHONE_NUMBER_REQUIRED,
errorCode = USERNAME_OR_EMAIL_OR_PHONE_NUMBER_REQUIRED_ERROR_CODE
)
}

loginRequest.username?.let { UtilsMethod.Validation.isValidUserName(it) }
loginRequest.emailAddress?.let { UtilsMethod.Validation.isValidEmail(it) }
loginRequest.phoneNumber?.let { UtilsMethod.Validation.isValidPhoneNumber(it) }

UtilsMethod.Validation.isValidPassword(loginRequest.password)

val user =
loginRequest.phoneNumber?.let { userDataSource.getUserByPhoneNumber(it) }
?: loginRequest.emailAddress?.let { userDataSource.getUserByEmailAddress(it) }
?: loginRequest.username?.let { userDataSource.getUserByUsername(it) }
?: throw UserDetailsNotFound()

val isValidPassword = hashingService.verify(
value = loginRequest.password, saltedHash = SaltedHash(
hash = user.password, salt = user.salt
)
)
if (!isValidPassword) {
throw UserAuthDetailsError()
}

val token = tokenService.generate(
config = tokenConfig,
claims = arrayOf(
TokenClaim(
name = USER_ID,
value = user.id.toHexString()
)
)
)

call.respond(
status = HttpStatusCode.OK,
message = XFullStackResponse(
status = XFullStackResponseStatus.Success,
code = null,
message = Localization.TOKEN_GENERATED_SUCCESSFULLY,
data = AuthenticationResponse(
userId = user.id.toHexString(),
token = token
)
)
)
}
}

All the validation is reused here as well for the Login route.

So in X, the user can login from username, email address and phone number as well. So the request will contain all these information.

@Serializable
data class LoginRequest(
val username: String?,
val phoneNumber: String?,
val emailAddress: String?,
val password: String,
)

Based on what details the user has provided the login check will happen.

To know the salt which was used at the time of user registration we will using the UserDataSource to fetch the user details

interface UserDataSource {
/**
* Get a user by their username.
*/
suspend fun getUserByUsername(username: String): User?

suspend fun getUserByEmailAddress(email: String): User?

suspend fun getUserByPhoneNumber(phoneNumber: String): User?

...
}

And implement these into our MongoUserDataSource

class MongoUserDataSource(
db: MongoDatabase,
) : UserDataSource {
private val usersCollection = db.getCollection<User>(USERS)

override suspend fun getUserByUsername(username: String): User? {
return usersCollection.find(
eq(User::username.name, username)
).limit(1).firstOrNull()
}

override suspend fun getUserByEmailAddress(email: String): User? {
return usersCollection.find(
eq(EMAIL_ADDRESS, email)
).limit(1).firstOrNull()
}

override suspend fun getUserByPhoneNumber(phoneNumber: String): User? {
return usersCollection.find(
eq(PHONE_NUMBER, phoneNumber)
).limit(1).firstOrNull()
}

...
}

After this, if the user details were found in the database then we can proceed further otherwise will respond with error response.

Queries on MongoDB till now is straight forward, check the Kotlin driver link https://github.com/mongodb-developer/kotlin-driver-quick-start for more details.

After getting the user details from DB, we have the salt which was used during user registration for hashing the user password. We will use the same and verify if the entered password conversion using salt is same as the one which we have in our DB. If it matches then the user is authenticated, otherwise there is some mismatch with the details.

On successful verification, it’s time to generate a JWT token from the details which we have related to the user. We have our token service already created in our previous article so we are only needed to make the call

val token = tokenService.generate(
config = tokenConfig,
claims = arrayOf(
TokenClaim(
name = USER_ID,
value = user.id.toHexString()
)
)
)

The above service call will generate the JWT token for us, and the claims which I am using is only the UserId (a unique key assigned to each document in MongoDB). You can add other claims as well based on your requirement.

When the token is generated, we can now respond to the client with the details.

call.respond(                                               
status = HttpStatusCode.OK,
message = XFullStackResponse(
status = XFullStackResponseStatus.Success,
code = null,
message = Localization.TOKEN_GENERATED_SUCCESSFULLY,
data = AuthenticationResponse(
userId = user.id.toHexString(),
token = token
)
)
)
@Serializable
data class AuthenticationResponse(
val userId: String,
val token: String,
)

On login successful authentication, we are only sending the userId and the token as a response to the client. We can add the user details as well, but that is a separate API in the project. You can pass any details you want based on your project requirement.

So, our login API is ready to be used. Let’s test it out using Bruno

Login API testing using Bruno

In the above screenshots, you can see the success and error case for the Login API which we created. There can be other cases as well, and it totally depends on your project requirements, the response or request structure as well.

Now our user can register themselves in our X app and also Login themselves using the same credentials.

Let’s also provide a way to authenticate the token which was generated.

Why??? — Reason being, in our application we are going to save the token locally, which we received from the Login API. If the user closes the application and say open it again after a week, we need to know if the token which is being saved locally is still valid or not. It might have expired. So with this API we can check the token validity.

This will be a very easy to develop, since we have the authenticate plugin to know if the token is valid or not.

Create an interface in server/features/authentication/controllers/authenticate named AuthenticateController

interface AuthenticateController {
suspend fun authenticateUser(
call: ApplicationCall,
)
}

Create a class in server/features/authentication/controllers/authenticate named AuthenticateControllerImplementation

class AuthenticateControllerImplementation : AuthenticateController {
override suspend fun authenticateUser(
call: ApplicationCall,
) {
delay(API_RESPONSE_DELAY)

call.respond(
HttpStatusCode.OK,
XFullStackResponse(
status = XFullStackResponseStatus.Success,
code = null,
message = Localization.USER_AUTHENTICATED,
data = null
)
)
}
}

And voilà, you have the authenticate service ready to be used.

But how it will check the token?

Go to your authentication router, in that we need to add a route which will be used to call the service

fun Routing.authentication(
authenticationController: AuthenticationController,
...
) {
...

authenticate {
get<AuthenticationResource.Authenticate> {
authenticationController.authenticateUser(
call = this.context,
)
}

...
}
}

Notice, how the AuthenticationResource.Authenticate is wrapped inside the authenticate extension. This checks your headers for the key Authorization and get the token from there.

Let’s test the API

Authenticate API

For the logged in user, we got the authentication successful. But where we send the token? In the body? — No. In the Query? — No. In the Headers? — YES

Authenticate headers

Append the Bearer keyword in front of the token before sending it in the Headers with the key Authorization.

So, we are done with our Authenticate API as well. Now let’s say the user is authenticated. But how you will identify that which user has made the request? One option is to pass the user id in every API call, but that is not helpful.

If you remember, in token service we added claims and in login one of the claims which we used was the userId. So the token which was generated has the details of that. So from that we can get the userId rather than passing it in every requests.

Let’s see how we can do it.

An user info API will be helpful here, which will authenticate the token and then will get the userId from the token and find the details into our DB.

Create an interface in server/features/authentication/userInfo named UserInfoController

interface UserInfoController {
suspend fun getUserInfo(
call: ApplicationCall,
userDataSource: UserDataSource,
...
)
}

Create an class in server/features/authentication/userInfo named UserInfoControllerImplementation

class UserInfoControllerImplementation : UserInfoController {
override suspend fun getUserInfo(
call: ApplicationCall,
userDataSource: UserDataSource,
...
) {
delay(API_RESPONSE_DELAY)

val principal = call.principal<JWTPrincipal>()

val userId =
principal?.payload?.getClaim(USER_ID)?.asString() ?: throw UserDetailsNotFound()
val user = userDataSource.getUserByUserId(userId) ?: throw UserDetailsNotFound()

...

val response = user.parseToUserInfoResponse(
...
)

call.respond(
HttpStatusCode.OK,
XFullStackResponse(
status = XFullStackResponseStatus.Success,
code = null,
message = Localization.DETAILS_FOUND,
data = response
)
)
}
}

As you can see in the above code, we get the principal from the call. From that we retrieved the UserId using the same key which we used to add it to the token.

After that, we are making the query on the DB using the userId which we got from the token.

interface UserDataSource {
...

suspend fun getUserByUserId(userId: String): User?

...
}
class MongoUserDataSource(
db: MongoDatabase,
) : UserDataSource {
...

override suspend fun getUserByUserId(userId: String): User? {
return usersCollection.find(
eq(ID, ObjectId(userId))
).limit(1).firstOrNull()
}

...
}

After retrieving the user details from the DB, there can be a case where all the details would not be needed by your client. Or you have to refactor some details before you send it to the client.

So for that we are parsing our DB User instance to a UserInfoResponse instance with the required details.

fun User.parseToUserInfoResponse(
...
) = this.let { user ->
UserInfoResponse(
id = user.id.toHexString(),
name = user.name,
username = user.username,
bio = user.bio,
emailAddress = user.emailAddress,
phoneNumber = user.phoneNumber,
profilePicture = user.profilePicture,
dateOfBirth = user.dateOfBirth,
...
)
}
@Serializable
data class UserInfoResponse(
val id: String,
val name: String,
val username: String,
val bio: String?,
val emailAddress: String?,
val phoneNumber: String?,
val profilePicture: String?,
val dateOfBirth: Long,
val following: Int,
val followers: Int,
val isFollowedByCurrentUser: Boolean,
val isFollowingCurrentUser: Boolean,
val isSameUser: Boolean,
val chatId: String?,
)

Few of the details I have removed, since we will be touching those parts later in the series. This is only to reduce the length of the code. You can always go to the source code and check it.

After parsing the response, only work remaining is to return it back to the requester.

Let’s test out the User Info API which we created

Login API testing on Bruno

As you can see, in the headers we are not sending any userId or other details except the token and from that we get the user info.

In ideal scenario, we don’t make a separate a user info call for the current logged in user and a different one for the other users. But this is just to show you how the token will be helpful in getting the requester user details.

You can change the implementation as per your requirement. And feel free to share it us as well.

So we have covered everything which we wanted to in this article.

Let’s check in the next article where we will be implementing our authentication flow in the client side.

If you have any questions or suggestions please provide it in the comments, will try to answer or implement it.

Follow for getting the notification when new articles are published.

Follow me on Twitter and GitHub.

--

--