How to extract access rights validation in Kotlin's Ktor - kotlin

I have Ktor based REST API application which uses the jwt token's as authentication. Then I have to restrict certain routes for the specific role. In order to do it I am creating principal, containing the relevant info:
data class UserPrincipal (
val id: Long,
val username: String,
val roleId: Long,
): Princpal {
override fun getName() = username
}
object AuthLogin {
fun Application.auth(jwt: JwtProvider) {
install(Authentication) {
jwt("jwt") {
realm = jwt.realm()
verifier(jwt.verifier())
validate {
val userId = it.payload.getClaim("id").asLong()
val username = it.payload.getClain("name")
val roleId = it.payload.getClaim("roleId").asLong()
UserPrincipal(userId, username, roleId)
}
}
}
}
}
The claims with userId and roleId are being provided when signing the correctly logged in user. Now I can restrict REST endpoints like that:
object RestModule {
fun Application.enititiesOne(userRepo: UserRepo) {
routing {
authenticate("jwt") {
route("/entities1") {
get {
val principal = call.principal<UserPrincipal>()
when(userRepo.hasAccessByRole(principal!!.roleId, "CAN_R_E1") {
false -> call.respond(HttpStatusCode.Forbidden)
true -> // some retrieval logic
}
post {
val principal = call.principal<UserPrincipal>()
when(userRepo.hasAccessByRole(principal!!.roleId, "CAN_W_E1") {
false -> call.respond(HttpStatusCode.Forbidden)
true -> // some update logic
}
}
}
}
}
}
As you can see even inside one routing function I have to duplicate code that checks the principal's role twice. I can move it out to function but what I want is a single place to define my security roles. Something like that:
authenticate {
val principal = call.principal<UserPrincipal()
val rights = userRepo.rightsByRole(principal.roleId)
when(routes) {
get("/entities1/**") ->
if(rights.contain("CAN_R_E1")) call.proceed
else call.respond(HttpStatusCode.Forbidden)
post("/entites1) -> rights.contain("CAN_W_E1") // similar
get("/entities2/**") -> rights.contain("CAN_R_E2") // similar
else -> call.respond(401)
}
}
And then plug it into the rest endpoints. Or is there some similar approach that I can use in Kotlin's Ktor? Seems like interceptors is what I need but I'm not sure how to use them in an intended way.

You can check the method and uri in the validate block.
install(Authentication) {
jwt {
validate {
val userId = it.payload.getClaim("id").asLong()
val username = it.payload.getClaim("name").asString()
val roleId = it.payload.getClaim("roleId").asLong()
UserPrincipal(userId, username, roleId)
val requiredRole = when (request.httpMethod) {
HttpMethod.Get -> // get role
HttpMethod.Post -> // get other role
}
// check if role exists in repo
}
}
}
install(Routing) {
get {
val principal = call.principal<UserPrincipal>()!!
call.respond(principal)
}
post {
val principal = call.principal<UserPrincipal>()!!
call.respond(principal)
}
}
By the way, there were several issues with the code you posted, so it wouldn't compile.

Related

Encoding a password with argon2 and save on PostgreSQL

I'm trying to encox user password to save in postgresql database. Using kotlin and argon2. I did the functions and in the tests, it works perfectly. But when I save it in the database, and I try to compare the passwords, it always gives an error. Could anyone help?
I created the following functions:
private val argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id, 32, 64)
fun String.encode(): String = argon2.hash(3, 64 * 1024, 1, this.toCharArray())
fun String.checkEncoding(hash: String): Boolean = argon2.verify(hash, this.toCharArray())
and in tests, everything works perfectly
class HashingTest : ShouldSpec({
context("Encoding a string") {
should("encode correctly") {
val dataOne = "S#!sc_%kah"
val encondeOne = dataOne.encode()
val dataTwo = "S#!sc_%kah"
dataTwo.checkEncoding(encondeOne) shouldBe true // works fine!
}
}
})
When I save to the database, and try to compare, I always get an error
//ENTITY
data class User(
override val id: UUID,
val username: String,
val password: String,
) : IEntity
//INSERT SCRITP -> EXPOSED FRAMEWORK
private fun newUser(schema: String, entity: User) {
val table = toUserTable(schema)
table
.insert {
it[id] = entity.id
it[username] = entity.username
it[password] = entity.password.encode()
}
}
//FIND USER
fun findUser(schema: String, model: SignInModel): User? {
val table = toUserTable(schema)
val user = table
.select { table.username eq model.username }
.firstNotNullOfOrNull { toUser(schema, it) }
val verifying = model.password.checkEncoding(user!!.password) // ERROR HERE, ALWAYS RETURNS FALSE
return when (verifying) {
true -> user
false -> null
}
}
I don't know if you used your findUser() and newUser() in a transaction but queries must be called in a transaction like below:
private fun newUser(schema: String, entity: User) {
val table = toUserTable(schema)
transaction {
table
.insert {
it[id] = entity.id
it[username] = entity.username
it[password] = entity.password.encode()
}
}
}
and:
fun findUser(schema: String, model: SignInModel): User? {
val table = toUserTable(schema)
val user = transaction {
table
.select { table.username eq model.username }
.firstNotNullOfOrNull { toUser(schema, it) }
}
val verifying = model.password.checkEncoding(user!!.password)
return when (verifying) {
true -> user
false -> null
}
}

If and how can I install a ktor plugin locally within a route for GET but not for POST?

For a ktor (2.0.3) application (kotlin 1.7.10) I want to have two endpoints on the same route (/feedback) but with different http methods, one GET and one POST.
So far no problem.
Now I would like to install an AuthorizationPlugin on only one of them.
I know how to install a plugin for a specific route only, but is it also possible to separately install it for different http methods on the same route?
So far I could not figure out a solution that does not require me to either introduce different routes (e.g. /feedback/read, /feedback/new) or handle the authorization check within the GET and POST callbacks directly.
The following is a reduced code containing two tests demonstrating the problem.
package some.example.package
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import org.apache.http.auth.AuthenticationException
import kotlin.test.Test
import kotlin.test.assertEquals
internal enum class AuthRole {
Admin, User
}
#kotlinx.serialization.Serializable
internal data class AuthUserSession(val username: String, val roles: Set<AuthRole> = setOf()) : Principal
const val authName = "form-auth"
const val usernameFormField = "username"
const val passwordFormField = "password"
/**
* Plugin Implementation
*/
internal val AuthorizationPlugin = createRouteScopedPlugin(
name = "AuthorizationPlugin",
createConfiguration = ::RoleBaseConfiguration
) {
pluginConfig.apply {
on(AuthenticationChecked) { call ->
val principal =
call.authentication.principal<AuthUserSession>() ?: throw Exception("Missing principal")
val userRoles = principal.roles
val denyReasons = mutableListOf<String>()
roles?.let {
if (roles!!.none { it in userRoles }) {
denyReasons += "Principal $principal has none of the sufficient role(s) ${
roles!!.joinToString(
" or "
)
}"
}
}
if (denyReasons.isNotEmpty()) {
val message = denyReasons.joinToString(". ")
throw Exception(message)
}
}
}
}
internal class RoleBaseConfiguration (
var roles: Set<AuthRole>? = null,
)
/**
* Server setup
*/
internal fun Application.setupConfig() {
install(Authentication) {
form(authName) {
userParamName = usernameFormField
passwordParamName = passwordFormField
challenge {
throw AuthenticationException()
}
validate { cred: UserPasswordCredential ->
if (cred.name == AuthRole.Admin.name) {
AuthUserSession(username = "admin", roles = setOf(AuthRole.Admin))
} else {
AuthUserSession(username = "user", roles = setOf(AuthRole.User))
}
}
}
}
routing {
route("feedback") {
authenticate(authName) {
post {
call.respond(HttpStatusCode.Created, "Submitting feedback")
}
install(AuthorizationPlugin) {
roles = setOf(AuthRole.Admin)
}
get {
call.respond(HttpStatusCode.OK, "Getting feedback")
}
}
}
}
}
/**
* Tests
*/
internal class PluginIssueTest {
/**
* For a valid solution this test should succeed.
*/
#Test
fun testGiveFeedback() = testApplication {
application {
setupConfig()
}
client.post("/feedback") {
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(
listOf(
usernameFormField to AuthRole.User.name,
passwordFormField to "mypassword"
).formUrlEncode()
)
}.apply {
assertEquals(HttpStatusCode.Created, status)
}
}
/**
* For this test the plugin is successfully called and required role is checked.
*/
#Test
fun testReadFeedback() = testApplication {
application {
setupConfig()
}
client.get("/feedback") {
header(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody(
listOf(
usernameFormField to AuthRole.Admin.name,
passwordFormField to "mypassword"
).formUrlEncode()
)
}.apply {
assertEquals(HttpStatusCode.OK, status)
}
}
}
I made most of the things internal so they would not interfere with the implementations for my actual application. It should not have any influence on the tests.
Suggestions are highly appreciated.
If I forgot some important information please let me know.
I took a closer look at the implementation of authenticate and found a solution there.
It uses the createChild of the Route class with a custom RouteSelector that always evaluates to a "transparent" quality, to define a specific route for the authentication.
Since I currently only need a single RouteSelector instance I simplified it to be an object instead of a class.
By adding the following implementation to my code...
fun Route.authorize(
roles: Set<AuthRole>,
build: Route.() -> Unit
): Route {
val authenticatedRoute = createChild(AuthorizationRouteSelector)
authenticatedRoute.install(AuthorizationPlugin) {
this.roles = roles
}
authenticatedRoute.build()
return authenticatedRoute
}
object AuthorizationRouteSelector : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation {
return RouteSelectorEvaluation.Transparent
}
override fun toString(): String = "(authorize \"default\" )"
}
...I was able to use my authorization plugin as follows:
routing {
route("feedback") {
authenticate(authName) {
post {
call.respond(HttpStatusCode.Created, "Submitting feedback")
}
authorize(setOf(AuthRole.Admin)) {
get {
call.respond(HttpStatusCode.OK, "Getting feedback")
}
}
}
}
}
making both tests succeed.

purchasesList from BillingClient is null at first

I have the BillingHandler class that you can see below which I'm using to handle in-app billing related logic using google's billing library v.3. I'm using Koin to create a singleton instance using single { BillingHandler(androidContext()) } in my app's module.
Now my issue occurs when I call the class' doesUserOwnPremium() method from my SettingsFragment which uses settings preferences for displaying a preference to be used as a purchase button. Firstly, I use get() to access the billingHandler instance and then call the method to check whether or not the user owns the premium product. I've already purchased it while testing but when I first navigate to the fragment, the purchasesList in the BillingHandler class is null so this returns false. After clicking the preference and attempting to launch a billing flow, the handler's if(!ownsProduct()) {..} logic in loadSKUs() is called and evaluates to false thus notifying me that I do own it.
Both the loadSKUs() method and the doesUserOwnPremium() method call ownsProduct() at different times and return the above results each time. Why is that? Does it have something to do with initialization?
SettingsFragment.kt:
class SettingsFragment : SharedPreferences.OnSharedPreferenceChangeListener
, PreferenceFragmentCompat() {
private val TAG = SettingsFragment::class.java.simpleName
// Billing library setup
private lateinit var billingHandler:BillingHandler
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
purchasePremiumPreference = findPreference(resources.getString(R.string.purchase_premium))!!
purchasePremiumPreference.isEnabled = false // disable until the client is ready
billingHandler = get()
val ownsPremium = billingHandler.doesUserOwnPremium()
Toast.makeText(requireContext(),"owns product = $ownsPremium",Toast.LENGTH_LONG).show()
if(!ownsPremium) {
purchasePremiumPreference.isEnabled = true
purchasePremiumPreference.setOnPreferenceClickListener {
billingHandler.startConnection()
true
}
}
}
}
BillingHandler.kt:
/**
* Singleton class that acts as an abstraction layer on top of the Billing library V.3 by google.
*/
class BillingHandler(private val context: Context) : PurchasesUpdatedListener {
// Billing library setup
private var billingClient: BillingClient
private var skuList:ArrayList<String> = ArrayList()
private val sku = "remove_ads" // the sku to sell
private lateinit var skuDetails: SkuDetails // the details of the sku to sell
private var ownsPremium = false
fun doesUserOwnPremium() : Boolean = ownsPremium
// analytics
private lateinit var firebaseAnalytics: FirebaseAnalytics
init {
skuList.add(sku) // add SKUs to the sku list (only one in this case)
billingClient = BillingClient.newBuilder(context)
.enablePendingPurchases()
.setListener(this)
.build()
ownsPremium = ownsProduct()
}
/**
* Attempts to establish a connection to the billing client. If successful,
* it will attempt to load the SKUs for sale and begin a billing flow if needed.
* If the connection fails, it will prompt the user to either retry the connection
* or cancel it.
*/
fun startConnection() {
// start the connection
billingClient.startConnection(object:BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
loadSKUs()
} else {
Toast.makeText(context,"Something went wrong, please try again!",
Toast.LENGTH_SHORT).show()
}
}
override fun onBillingServiceDisconnected() {
Toast.makeText(context,"Billing service disconnected!", Toast.LENGTH_SHORT).show()
TODO("implement retry policy. Maybe using a dialog w/ retry and cancel buttons")
}
})
}
/**
* Loads the skus from the skuList and starts the billing flow
* for the selected sku(s) if needed.
*/
private fun loadSKUs() {
if(billingClient.isReady) { // load the products that the user can purchase
val skuDetailsParams = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.INAPP)
.build()
billingClient.querySkuDetailsAsync(skuDetailsParams
) { billingResult, skuDetailsList ->
if(billingResult.responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList != null && skuDetailsList.isNotEmpty()) {
// for each sku details object
for(skuDetailsObj in skuDetailsList) {
// make sure the sku we want to sell is in the list and do something for it
if(skuDetailsObj.sku == sku) {
if(!ownsProduct()) { // product not owned
skuDetails = skuDetailsObj // store the details of that sku
startBillingFlow(skuDetailsObj)
} else { // give premium benefits
Toast.makeText(context,"You already own Premium!",Toast.LENGTH_SHORT).show()
}
}
}
}
}
} else {
Toast.makeText(context,"Billing client is not ready. Please try again!",Toast.LENGTH_SHORT).show()
}
}
/**
* Checks whether or not the user owns the desired sku
* #return True if they own the product, false otherwise
*/
private fun ownsProduct(): Boolean {
var ownsProduct = false
// check if the user already owns this product
// query the user's purchases (reads them from google play's cache)
val purchasesResult: Purchase.PurchasesResult =
billingClient.queryPurchases(BillingClient.SkuType.INAPP)
val purchasesList = purchasesResult.purchasesList // get the actual list of purchases
if (purchasesList != null) {
for (purchase in purchasesList) {
if (purchase.sku == sku) {
ownsProduct = true
break
}
}
} else {
Toast.makeText(context,"Purchases list was null",Toast.LENGTH_SHORT).show()
}
return ownsProduct
}
/**
* Starts the billing flow for the purchase of the desired
* product.
* #param skuDetailsObj The SkuDetails object of the selected sku
*/
private fun startBillingFlow(skuDetailsObj:SkuDetails) {
val billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetailsObj)
.build()
billingClient.launchBillingFlow(
context as Activity,
billingFlowParams
)
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchasesList: MutableList<Purchase>?) {
if(billingResult.responseCode == BillingClient.BillingResponseCode.OK &&
purchasesList != null) {
for(purchase in purchasesList) {
handlePurchase(purchase)
}
}
}
/**
* Handles the given purchase by acknowledging it if needed .
* #param purchase The purchase to handle
*/
private fun handlePurchase(purchase: Purchase) {
// if the user purchased the desired sku
if(purchase.sku == sku && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if(!purchase.isAcknowledged) { // acknowledge the purchase so that it doesn't get refunded
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(acknowledgePurchaseParams
) { billingResult ->
if(billingResult.responseCode == BillingClient.BillingResponseCode.OK) { // log the event using firebase
// log event to firebase
val eventBundle = Bundle()
eventBundle.putString(FirebaseAnalytics.Param.ITEM_ID,"purchase_ack")
eventBundle.putString(FirebaseAnalytics.Param.ITEM_NAME,"Purchase acknowledged")
eventBundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, "IN_APP_PURCHASES")
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.PURCHASE,eventBundle)
}
}
}
showPurchaseSuccessDialog()
}
}
/**
* Shows a dialog to inform the user of the successful purchase
*/
private fun showPurchaseSuccessDialog() {
MaterialDialog(context).show {
title(R.string.premium_success_dialog_title)
message(R.string.premium_success_dialog_msg)
icon(R.drawable.ic_premium)
}
}
}
After doing some more digging and reading the docs again, I realized that when I'm first calling the ownsProduct() method, the billing client's startConnection() method hasn't been called yet thus why the query returned null, the client wasn't ready yet.
I decided to bypass that by simply using the following method to begin a dummy connection in order to set up the client from within my Application class. This way, by the time the user gets anywhere in the app, the client is ready and I can get his/hers actual purchase list.
fun dummyConnection() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(p0: BillingResult) {
}
override fun onBillingServiceDisconnected() {
}
})
}
I'm guessing that this could have bad side effects so I'd love to get some feedback on whether this is the right way to go about it or not. By the way, I need the client ready as soon as possible because I want to be able to verify that they own premium throughout the app (to disable ads, etc.).

How to inject scopeId into Koin to get the dependency?

In https://github.com/InsertKoinIO/koin/blob/master/koin-projects/docs/reference/koin-android/scope.md#sharing-instances-between-components-with-scopes it is shown the below example
module {
// Shared user session data
scope(named("session")) {
scoped { UserSession() }
}
// Inject UserSession instance from "session" Scope
factory { (scopeId : ScopeID) -> Presenter(getScope(scopeId).get())}
}
But I don't even know how to get presenter?
I try
val nameScope = getKoin().createScope("SomeName", named("session"))
val presenter = get<Presenter>(nameScope.id)
but it's not the correct. How to get my presenter?
After tracing the code, the way to do it is to use parameter to pass over the scopeId
For the above example, it will be
val nameScope = getKoin().createScope("SomeName", named("session"))
val presenter = get<Presenter>(parameters = { parametersOf(nameScope.id) )
If there's qualifier, we just need to send through them as well
One Example as below where we need a parameter of the lambda to send through scopeId and name of the qualifier. (the argument is self definable through the parameters of any type).
module {
scope(named("AScopeName")) {
scoped(qualifier = named("scopedName")) { Dependency() }
factory(qualifier = named("factoryName")) { Dependency() }
}
factory { (scopeId: ScopeID, name: String) ->
Environment(getScope(scopeId).get(qualifier = named(name)))
}
}
Then the calling is as simple as below
val nameScope = getKoin().createScope("SomeName", named("AScopeName"))
val environment = get<Environment>(parameters = { parametersOf(nameScope.id, "scopedName") })
Or we could also
val nameScope = getKoin().createScope("SomeName", named("AScopeName"))
val environment = get<Environment>(parameters = { parametersOf("SomeName", "scopedName") })

Getting spring oauth2 user info data

I have an oauth2 server and client. In client I configured ClientRegistrationRepository:
#Bean
#Conditional(SsoCondition::class)
open fun clientRegistrationRepository(): ClientRegistrationRepository {
val test = ClientRegistration.withRegistrationId(registrationId)
.clientId(clientId)
.clientSecret(clientSecret)
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
.authorizationUri(authorizeUri)
.tokenUri(tokenUri)
.userInfoUri(userInfoUri)
.scope("read", "write")
.userNameAttributeName(userNameAttribute)
.build()
return InMemoryClientRegistrationRepository(test)
}
This works fine and authorization is performed.
The problem is in userInfoUri. This uri is invoked and needed server method is performed. I see the user data and that method return this data.
The method for authorizeUri is:
#GetMapping("/api/user/me")
fun getUserInfo(response: HttpServletResponse, request: HttpServletRequest, principal: Principal): HashMap<String, Any?> {
val authentication = SecurityContextHolder.getContext().authentication
val userData = HashMap<String, Any?>()
userData[OUTER_ID] = principal.name
val ssoUser = authentication.userAuthentication.principal.attributes
// getting data from ssoUser to userData
...
return userData
}
And so the question is: where or how can I get this data in the client application?
I don't know how right this solution, but I got the user data like this:
Creating custom implementation of OAuth2AuthorizedClientService interface:
class CustomOAuth2AuthorizedClientService(private val clientRegistrationRepository: ClientRegistrationRepository) : OAuth2AuthorizedClientService {
private val principalData = ConcurrentHashMap<String, Authentication>()
...
override fun saveAuthorizedClient(authorizedClient: OAuth2AuthorizedClient, principal: Authentication) {
...
val key = ... // create some key
principalData[key] = principal
}
...
fun getPrincipal(key: String): Authentication? {
return authorizedClientsPrincipal[key]
}
}
Creating bean for CustomOAuth2AuthorizedClientService:
#Bean
open fun authorizedClientService(): OAuth2AuthorizedClientService {
return CustomOAuth2AuthorizedClientService(clientRegistrationRepository())
}
where clientRegistrationRepository() is a ClientRegistrationRepository bean.
In the code get user data from CustomOAuth2AuthorizedClientService bean:
#Autowired
private var oAuth2AuthorizedClientService: OAuth2AuthorizedClientService
...
fun test() {
val userData = (oAuth2AuthorizedClientService as CustomOAuth2AuthorizedClientService).getPrincipal(key)
}