From 71238bea360f6c9d0f845b5f51196595774bb18e Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 9 Mar 2022 12:33:56 +0330 Subject: [PATCH 1/7] Add profile services --- .../gateway/data/ChangePasswordRequest.kt | 7 + .../opex/auth/gateway/data/KycRequest.kt | 7 + .../opex/auth/gateway/data/UploadResponse.kt | 3 + .../opex/auth/gateway/data/UserProfileInfo.kt | 20 +-- .../auth/gateway/data/UserSessionResponse.kt | 8 + .../extension/UserManagementResource.kt | 84 ++++----- .../gateway/extension/UserProfileResource.kt | 170 ++++++++++++++++++ .../extension/UserProfileResourceFactory.kt | 30 ++++ .../opex/utility/error/data/OpexError.kt | 1 + 9 files changed, 280 insertions(+), 50 deletions(-) create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KycRequest.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UploadResponse.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserSessionResponse.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResourceFactory.kt diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt new file mode 100644 index 000000000..d63ac2607 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.auth.gateway.data + +data class ChangePasswordRequest( + val password: String, + val newPassword: String, + val newPasswordConfirmation: String, +) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KycRequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KycRequest.kt new file mode 100644 index 000000000..4903c5d69 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/KycRequest.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.auth.gateway.data + +data class KycRequest( + val selfiePath: String, + val idCardPath: String, + val acceptFormPath: String +) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UploadResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UploadResponse.kt new file mode 100644 index 000000000..196ba586b --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UploadResponse.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.auth.gateway.data + +data class UploadResponse(val path: String) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserProfileInfo.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserProfileInfo.kt index 538cdfb46..94ab954a0 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserProfileInfo.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserProfileInfo.kt @@ -7,14 +7,14 @@ class UserProfileInfo { var firstNameFa: String? = null var lastNameFa: String? = null var birthday: String? = null - var birthdayJalali: String? = null + var birthdayAlt: String? = null var nationalID: String? = null var passport: String? = null var phoneNumber: String? = null - var homeNumber: String? = null - var email: String? = null + var telephone: String? = null var postalCode: String? = null - var address: String? = null + var residence: String? = null + var nationality: String? = null constructor() @@ -29,22 +29,22 @@ class UserProfileInfo { passport: String?, phoneNumber: String?, homeNumber: String?, - email: String?, postalCode: String?, - address: String? + address: String?, + nationality: String? ) { this.firstNameEn = firstNameEn this.lastNameEn = lastNameEn this.firstNameFa = firstNameFa this.lastNameFa = lastNameFa this.birthday = birthday - this.birthdayJalali = birthdayJalali + this.birthdayAlt = birthdayJalali this.nationalID = nationalID this.passport = passport this.phoneNumber = phoneNumber - this.homeNumber = homeNumber - this.email = email + this.telephone = homeNumber this.postalCode = postalCode - this.address = address + this.residence = address + this.nationality = nationality } } \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserSessionResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserSessionResponse.kt new file mode 100644 index 000000000..657ec1888 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserSessionResponse.kt @@ -0,0 +1,8 @@ +package co.nilin.opex.auth.gateway.data + +data class UserSessionResponse( + val ipAddress: String, + val started: Int, + val lastAccess: Int, + val state: String, +) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt index 107515f91..e21fa1a21 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt @@ -1,9 +1,7 @@ package co.nilin.opex.auth.gateway.extension import co.nilin.opex.auth.gateway.ApplicationContextHolder -import co.nilin.opex.auth.gateway.data.RegisterUserRequest -import co.nilin.opex.auth.gateway.data.RegisterUserResponse -import co.nilin.opex.auth.gateway.data.UserProfileInfo +import co.nilin.opex.auth.gateway.data.* import co.nilin.opex.auth.gateway.model.AuthEvent import co.nilin.opex.auth.gateway.model.UserCreatedEvent import co.nilin.opex.auth.gateway.utils.ErrorHandler @@ -15,6 +13,7 @@ import org.keycloak.common.util.Time import org.keycloak.email.EmailTemplateProvider import org.keycloak.models.Constants import org.keycloak.models.KeycloakSession +import org.keycloak.models.UserCredentialModel import org.keycloak.models.UserModel import org.keycloak.services.resource.RealmResourceProvider import org.keycloak.services.resources.LoginActionsService @@ -65,6 +64,42 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.ok(RegisterUserResponse(user.id)).build() } + @PUT + @Path("user/password") + @Consumes(MediaType.APPLICATION_JSON) + fun changePassword(body: ChangePasswordRequest): Response { + // AccountFormService + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserById(auth.getUserId(), opexRealm) + ?: return ErrorHandler.response( + Response.Status.NOT_FOUND, + OpexException(OpexError.NotFound, "User not found") + ) + + val cred = UserCredentialModel.password(body.password) + if (!session.userCredentialManager().isValid(opexRealm, user, cred)) { + return ErrorHandler.response( + Response.Status.FORBIDDEN, + OpexException(OpexError.Forbidden, "Incorrect password") + ) + } + + if (body.newPasswordConfirmation == body.newPassword) { + return ErrorHandler.response( + Response.Status.BAD_REQUEST, + OpexException(OpexError.BadRequest, "Invalid password confirmation") + ) + } + + session.userCredentialManager() + .updateCredential(opexRealm, user, UserCredentialModel.password(body.newPassword, false)) + + return Response.noContent().build() + } + @POST @Path("user/forgot") @Produces(MediaType.APPLICATION_JSON) @@ -99,10 +134,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour } @GET - @Path("user/profile") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - fun getAttributes(): Response { + @Path("user/sessions") + fun getActiveSessions(): Response { val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() @@ -113,40 +146,11 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour OpexException(OpexError.NotFound, "User not found") ) - return Response.ok(user.attributes).build() - } - - @POST - @Path("user/profile") - @Consumes(MediaType.APPLICATION_JSON) - fun updateAttributes(request: UserProfileInfo): Response { - val auth = ResourceAuthenticator.bearerAuth(session) - if (!auth.hasScopeAccess("trust")) - return ErrorHandler.forbidden() + val sessions = session.sessions().getUserSessionsStream(opexRealm, user) + .map { UserSessionResponse(it.ipAddress, it.started, it.lastSessionRefresh, it.state.name) } + .toList() - val user = session.users().getUserById(auth.getUserId(), opexRealm) - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.NotFound, "User not found") - ) - - with(request) { - user.setSingleAttribute("firstNameFa", firstNameFa) - user.setSingleAttribute("lastNameEn", lastNameEn) - user.setSingleAttribute("firstNameFa", firstNameFa) - user.setSingleAttribute("lastNameFa", lastNameFa) - user.setSingleAttribute("birthday", birthday) - user.setSingleAttribute("birthdayJalali", birthdayJalali) - user.setSingleAttribute("nationalID", nationalID) - user.setSingleAttribute("passport", passport) - user.setSingleAttribute("phoneNumber", phoneNumber) - user.setSingleAttribute("homeNumber", homeNumber) - user.setSingleAttribute("email", email) - user.setSingleAttribute("postalCode", postalCode) - user.setSingleAttribute("address", address) - } - - return Response.noContent().build() + return Response.ok(sessions).build() } private fun sendEmail(user: UserModel, actions: List) { diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt new file mode 100644 index 000000000..4243e2b9c --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt @@ -0,0 +1,170 @@ +package co.nilin.opex.auth.gateway.extension + +import co.nilin.opex.auth.gateway.data.KycRequest +import co.nilin.opex.auth.gateway.data.UserProfileInfo +import co.nilin.opex.auth.gateway.utils.ErrorHandler +import co.nilin.opex.auth.gateway.utils.ResourceAuthenticator +import co.nilin.opex.utility.error.data.OpexError +import co.nilin.opex.utility.error.data.OpexException +import org.jboss.resteasy.plugins.providers.multipart.InputPart +import org.keycloak.models.KeycloakSession +import org.keycloak.models.UserModel +import org.keycloak.services.resource.RealmResourceProvider +import org.slf4j.LoggerFactory +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.core.io.buffer.DataBufferUtils +import org.springframework.core.io.buffer.DefaultDataBufferFactory +import reactor.core.publisher.Flux +import java.io.File +import java.nio.file.Paths +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import kotlin.streams.toList + +class UserProfileResource(private val session: KeycloakSession) : RealmResourceProvider { + + private val logger = LoggerFactory.getLogger(UserProfileResource::class.java) + private val opexRealm = session.realms().getRealm("opex") + + @GET + @Path("profile") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + fun getAttributes(): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserById(auth.getUserId(), opexRealm) + ?: return ErrorHandler.response( + Response.Status.NOT_FOUND, + OpexException(OpexError.NotFound, "User not found") + ) + + return Response.ok(user.attributes).build() + } + + @POST + @Path("profile") + @Consumes(MediaType.APPLICATION_JSON) + fun updateAttributes(request: UserProfileInfo): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserById(auth.getUserId(), opexRealm) + ?: return ErrorHandler.response( + Response.Status.NOT_FOUND, + OpexException(OpexError.NotFound, "User not found") + ) + + with(request) { + firstNameEn?.let { user.setSingleAttribute("firstNameEn", it) } + lastNameEn?.let { user.setSingleAttribute("lastNameEn", it) } + firstNameFa?.let { user.setSingleAttribute("firstNameFa", it) } + lastNameFa?.let { user.setSingleAttribute("lastNameFa", it) } + birthday?.let { user.setSingleAttribute("birthday", it) } + birthdayAlt?.let { user.setSingleAttribute("birthdayAlt", it) } + nationalID?.let { user.setSingleAttribute("nationalID", it) } + passport?.let { user.setSingleAttribute("passport", it) } + phoneNumber?.let { user.setSingleAttribute("phoneNumber", it) } + telephone?.let { user.setSingleAttribute("telephone", it) } + postalCode?.let { user.setSingleAttribute("postalCode", it) } + residence?.let { user.setSingleAttribute("residence", it) } + nationality?.let { user.setSingleAttribute("nationality", it) } + } + + return Response.noContent().build() + } + + @POST + @Path("profile/kyc") + @Consumes(MediaType.MULTIPART_FORM_DATA) + fun kycFlow(request: KycRequest): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val userId = auth.getUserId()!! + val user = session.users().getUserById(userId, opexRealm) + ?: return ErrorHandler.response( + Response.Status.NOT_FOUND, + OpexException(OpexError.NotFound, "User not found") + ) + + if (isInKycGroups(user)) { + return ErrorHandler.response( + Response.Status.BAD_REQUEST, + OpexException(OpexError.BadRequest, "User is already in kyc groups") + ) + } + + /*val forms = input.formDataMap + + val selfiePart = createPartContent(forms["selfie"]?.get(0)!!) + val idPart = createPartContent(forms["idCard"]?.get(0)!!) + val formPart = createPartContent(forms["acceptForm"]?.get(0)!!) + + val selfiePath = proxy.upload(userId, selfiePart).path + val idCard = proxy.upload(userId, idPart).path + val acceptForm = proxy.upload(userId, formPart).path*/ + + + + val kycRequestGroup = session.groups() + .getGroupsStream(opexRealm) + .toList() + .find { it.name == "kyc-requested" } + ?: return ErrorHandler.response( + Response.Status.NOT_FOUND, + OpexException(OpexError.GroupNotFound) + ) + + user.joinGroup(kycRequestGroup) + + user.apply { + setSingleAttribute("kycSelfiePath", request.selfiePath) + setSingleAttribute("kycIdCardPath", request.idCardPath) + setSingleAttribute("kycAcceptFormPath", request.acceptFormPath) + } + + return Response.noContent().build() + } + + + + private fun isInKycGroups(user: UserModel): Boolean { + return user.groupsStream.map { it.name } + .filter { it == "kyc-accepted" || it == "kyc-rejected" || it == "kyc-requested" } + .toList() + .isNotEmpty() + } + + private fun createPartContent(input: InputPart): Flux { + val file = input.getBody(File::class.java, null) + val factory = DefaultDataBufferFactory() + return DataBufferUtils.read(Paths.get(file.absolutePath), factory, DEFAULT_BUFFER_SIZE) + +// val fileItem = DiskFileItem( +// "selfie", +// Files.probeContentType(file.toPath()), +// false, +// file.name, +// file.length().toInt(), +// file.parentFile +// ) +// +// FileInputStream(file).use { +// it.transferTo(fileItem.outputStream) +// } + } + + override fun close() { + + } + + override fun getResource(): Any { + return this + } +} \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResourceFactory.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResourceFactory.kt new file mode 100644 index 000000000..2839a1c71 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResourceFactory.kt @@ -0,0 +1,30 @@ +package co.nilin.opex.auth.gateway.extension + +import org.keycloak.Config +import org.keycloak.models.KeycloakSession +import org.keycloak.models.KeycloakSessionFactory +import org.keycloak.services.resource.RealmResourceProvider +import org.keycloak.services.resource.RealmResourceProviderFactory + +class UserProfileResourceFactory : RealmResourceProviderFactory { + + override fun create(session: KeycloakSession): RealmResourceProvider { + return UserProfileResource(session) + } + + override fun init(config: Config.Scope?) { + + } + + override fun postInit(factory: KeycloakSessionFactory?) { + + } + + override fun close() { + + } + + override fun getId(): String { + return "user-profile" + } +} \ No newline at end of file diff --git a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt index a7f76d4df..f19a34916 100644 --- a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt +++ b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt @@ -25,6 +25,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus // code 5000: user-management EmailAlreadyVerified(5001, "Email is already verified", HttpStatus.BAD_REQUEST), + GroupNotFound(5002, "Group not found", HttpStatus.NOT_FOUND), // code 6000: wallet WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND), From 85daa08c0aa014cbbadb05f5e8dc2c85444b9640 Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 9 Mar 2022 18:06:21 +0330 Subject: [PATCH 2/7] Add 2fa services, not working though --- .../auth/gateway/data/Check2FAResponse.kt | 5 ++ .../opex/auth/gateway/data/Get2FAResponse.kt | 3 + .../opex/auth/gateway/data/Setup2FARequest.kt | 15 +++++ .../extension/UserManagementResource.kt | 61 +++++++++++++++++++ .../gateway/extension/UserProfileResource.kt | 3 - .../nilin/opex/auth/gateway/utils/OTPUtils.kt | 36 +++++++++++ 6 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Check2FAResponse.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Setup2FARequest.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/OTPUtils.kt diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Check2FAResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Check2FAResponse.kt new file mode 100644 index 000000000..d4bbdb323 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Check2FAResponse.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.auth.gateway.data + +data class Check2FAResponse( + val isEnabled: Boolean +) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt new file mode 100644 index 000000000..0aae42f78 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.auth.gateway.data + +data class Get2FAResponse(val uri: String, val secret: String) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Setup2FARequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Setup2FARequest.kt new file mode 100644 index 000000000..a0f76442b --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Setup2FARequest.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.auth.gateway.data + +class Setup2FARequest { + + var secret: String? = null + var initialCode: String? = null + + constructor() + + constructor(secret: String?, initialCode: String?) { + this.secret = secret + this.initialCode = initialCode + } + +} \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt index e21fa1a21..d40d3668e 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt @@ -5,6 +5,7 @@ import co.nilin.opex.auth.gateway.data.* import co.nilin.opex.auth.gateway.model.AuthEvent import co.nilin.opex.auth.gateway.model.UserCreatedEvent import co.nilin.opex.auth.gateway.utils.ErrorHandler +import co.nilin.opex.auth.gateway.utils.OTPUtils import co.nilin.opex.auth.gateway.utils.ResourceAuthenticator import co.nilin.opex.utility.error.data.OpexError import co.nilin.opex.utility.error.data.OpexException @@ -15,8 +16,12 @@ import org.keycloak.models.Constants import org.keycloak.models.KeycloakSession import org.keycloak.models.UserCredentialModel import org.keycloak.models.UserModel +import org.keycloak.models.credential.OTPCredentialModel +import org.keycloak.models.utils.Base32 +import org.keycloak.models.utils.HmacOTP import org.keycloak.services.resource.RealmResourceProvider import org.keycloak.services.resources.LoginActionsService +import org.keycloak.utils.CredentialHelper import org.slf4j.LoggerFactory import org.springframework.kafka.core.KafkaTemplate import java.util.concurrent.TimeUnit @@ -133,6 +138,58 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.noContent().build() } + @GET + @Path("user/2fa") + @Produces(MediaType.APPLICATION_JSON) + fun get2FASecret(): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserById(auth.getUserId(), opexRealm) + ?: return ErrorHandler.response( + Response.Status.NOT_FOUND, + OpexException(OpexError.NotFound, "User not found") + ) + + val secret = HmacOTP.generateSecret(64) + val uri = OTPUtils.generateOTPKeyURI(opexRealm, secret, "Opex", user.email) + return Response.ok(Get2FAResponse(uri, Base32.encode(secret.toByteArray()))).build() + } + + @POST + @Path("user/2fa") + @Consumes(MediaType.APPLICATION_JSON) + fun setup2FA(body: Setup2FARequest): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserById(auth.getUserId(), opexRealm) + ?: return ErrorHandler.response( + Response.Status.NOT_FOUND, + OpexException(OpexError.NotFound, "User not found") + ) + + val otpCredential = OTPCredentialModel.createFromPolicy(opexRealm, body.secret) + CredentialHelper.createOTPCredential(session, opexRealm, user, body.initialCode, otpCredential) + return Response.noContent().build() + } + + @GET + @Path("user/2fa/check") + @Produces(MediaType.APPLICATION_JSON) + fun is2FAEnabled(@QueryParam("username") username: String?): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserByUsername(username, opexRealm) + ?: return Response.ok(Check2FAResponse(false)).build() + + return Response.ok(Check2FAResponse(is2FAEnabled(user))).build() + } + @GET @Path("user/sessions") fun getActiveSessions(): Response { @@ -187,6 +244,10 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour logger.info("$kafkaEvent produced in kafka topic") } + private fun is2FAEnabled(user: UserModel): Boolean { + return session.userCredentialManager().isConfiguredFor(opexRealm, user, OTPCredentialModel.TYPE) + } + override fun close() { } diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt index 4243e2b9c..25901d94e 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt @@ -111,7 +111,6 @@ class UserProfileResource(private val session: KeycloakSession) : RealmResourceP val acceptForm = proxy.upload(userId, formPart).path*/ - val kycRequestGroup = session.groups() .getGroupsStream(opexRealm) .toList() @@ -132,8 +131,6 @@ class UserProfileResource(private val session: KeycloakSession) : RealmResourceP return Response.noContent().build() } - - private fun isInKycGroups(user: UserModel): Boolean { return user.groupsStream.map { it.name } .filter { it == "kyc-accepted" || it == "kyc-rejected" || it == "kyc-requested" } diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/OTPUtils.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/OTPUtils.kt new file mode 100644 index 000000000..40c5c01c5 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/OTPUtils.kt @@ -0,0 +1,36 @@ +package co.nilin.opex.auth.gateway.utils + +import org.keycloak.models.RealmModel +import org.keycloak.models.credential.OTPCredentialModel +import org.keycloak.models.utils.Base32 +import java.net.URLEncoder + +object OTPUtils { + + fun generateOTPKeyURI( + realm: RealmModel, + secret: String, + displayName: String, + accountName: String + ): String { + val policy = realm.otpPolicy + val accountNameEncode = URLEncoder.encode(accountName, Charsets.UTF_8) + val issuer = URLEncoder.encode(displayName, Charsets.UTF_8) + val label = "$issuer:$accountNameEncode".replace("\\+", "%20") + + val params = StringBuilder() + .append("secret=${Base32.encode(secret.toByteArray())}") + .append("&digits=${policy.digits}") + .append("&algorithm=SHA1") + .append("&issuer=$issuer") + .append( + if (policy.type == OTPCredentialModel.HOTP) + "&counter=${policy.initialCounter}" + else + "&period=${policy.period}" + ) + + return "otpauth://${policy.type}/${label}?${params}" + } + +} \ No newline at end of file From 048a7886c3869f027454bd52f798e248b9310f0f Mon Sep 17 00:00:00 2001 From: Peyman Date: Sat, 12 Mar 2022 16:42:43 +0330 Subject: [PATCH 3/7] Fix otp services --- .../auth/gateway/data/Check2FAResponse.kt | 5 - .../opex/auth/gateway/data/Get2FAResponse.kt | 2 +- .../gateway/data/UserSecurityCheckResponse.kt | 5 + .../extension/UserManagementResource.kt | 100 +++++++++++------- .../opex/utility/error/data/OpexError.kt | 1 + 5 files changed, 66 insertions(+), 47 deletions(-) delete mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Check2FAResponse.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserSecurityCheckResponse.kt diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Check2FAResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Check2FAResponse.kt deleted file mode 100644 index d4bbdb323..000000000 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Check2FAResponse.kt +++ /dev/null @@ -1,5 +0,0 @@ -package co.nilin.opex.auth.gateway.data - -data class Check2FAResponse( - val isEnabled: Boolean -) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt index 0aae42f78..0028dfdfc 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt @@ -1,3 +1,3 @@ package co.nilin.opex.auth.gateway.data -data class Get2FAResponse(val uri: String, val secret: String) \ No newline at end of file +data class Get2FAResponse(val uri: String, val secret: String, val qr:String) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserSecurityCheckResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserSecurityCheckResponse.kt new file mode 100644 index 000000000..ebc00c837 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/UserSecurityCheckResponse.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.auth.gateway.data + +data class UserSecurityCheckResponse( + val otp: Boolean +) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt index d40d3668e..7e855f1bc 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt @@ -17,11 +17,11 @@ import org.keycloak.models.KeycloakSession import org.keycloak.models.UserCredentialModel import org.keycloak.models.UserModel import org.keycloak.models.credential.OTPCredentialModel -import org.keycloak.models.utils.Base32 import org.keycloak.models.utils.HmacOTP import org.keycloak.services.resource.RealmResourceProvider import org.keycloak.services.resources.LoginActionsService import org.keycloak.utils.CredentialHelper +import org.keycloak.utils.TotpUtils import org.slf4j.LoggerFactory import org.springframework.kafka.core.KafkaTemplate import java.util.concurrent.TimeUnit @@ -69,9 +69,43 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.ok(RegisterUserResponse(user.id)).build() } + @POST + @Path("user/forgot") + @Produces(MediaType.APPLICATION_JSON) + fun forgotPassword(@QueryParam("email") email: String?): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserByEmail(email, opexRealm) + if (user != null) { + sendEmail(user, listOf(UserModel.RequiredAction.UPDATE_PASSWORD.name)) + } + return Response.noContent().build() + } + + @POST + @Path("user/verify-email") + @Produces(MediaType.APPLICATION_JSON) + fun sendVerifyEmail(@QueryParam("email") email: String?): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserByEmail(email, opexRealm) + if (user != null) { + if (!auth.hasUserAccess(user.id)) + return ErrorHandler.forbidden() + + sendEmail(user, listOf(UserModel.RequiredAction.VERIFY_EMAIL.name)) + } + return Response.noContent().build() + } + @PUT - @Path("user/password") + @Path("user/security/password") @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) fun changePassword(body: ChangePasswordRequest): Response { // AccountFormService val auth = ResourceAuthenticator.bearerAuth(session) @@ -105,41 +139,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.noContent().build() } - @POST - @Path("user/forgot") - @Produces(MediaType.APPLICATION_JSON) - fun forgotPassword(@QueryParam("email") email: String?): Response { - val auth = ResourceAuthenticator.bearerAuth(session) - if (!auth.hasScopeAccess("trust")) - return ErrorHandler.forbidden() - - val user = session.users().getUserByEmail(email, opexRealm) - if (user != null) { - sendEmail(user, listOf(UserModel.RequiredAction.UPDATE_PASSWORD.name)) - } - return Response.noContent().build() - } - - @POST - @Path("user/verify-email") - @Produces(MediaType.APPLICATION_JSON) - fun sendVerifyEmail(@QueryParam("email") email: String?): Response { - val auth = ResourceAuthenticator.bearerAuth(session) - if (!auth.hasScopeAccess("trust")) - return ErrorHandler.forbidden() - - val user = session.users().getUserByEmail(email, opexRealm) - if (user != null) { - if (!auth.hasUserAccess(user.id)) - return ErrorHandler.forbidden() - - sendEmail(user, listOf(UserModel.RequiredAction.VERIFY_EMAIL.name)) - } - return Response.noContent().build() - } - @GET - @Path("user/2fa") + @Path("user/security/otp") @Produces(MediaType.APPLICATION_JSON) fun get2FASecret(): Response { val auth = ResourceAuthenticator.bearerAuth(session) @@ -152,14 +153,23 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour OpexException(OpexError.NotFound, "User not found") ) + if (is2FAEnabled(user)) { + return ErrorHandler.response( + Response.Status.BAD_REQUEST, + OpexException(OpexError.OTPAlreadyEnabled) + ) + } + val secret = HmacOTP.generateSecret(64) val uri = OTPUtils.generateOTPKeyURI(opexRealm, secret, "Opex", user.email) - return Response.ok(Get2FAResponse(uri, Base32.encode(secret.toByteArray()))).build() + val qr = TotpUtils.qrCode(secret, opexRealm, user) + return Response.ok(Get2FAResponse(uri, secret, qr)).build() } @POST - @Path("user/2fa") + @Path("user/security/otp") @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) fun setup2FA(body: Setup2FARequest): Response { val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) @@ -171,13 +181,20 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour OpexException(OpexError.NotFound, "User not found") ) + if (is2FAEnabled(user)) { + return ErrorHandler.response( + Response.Status.BAD_REQUEST, + OpexException(OpexError.OTPAlreadyEnabled) + ) + } + val otpCredential = OTPCredentialModel.createFromPolicy(opexRealm, body.secret) CredentialHelper.createOTPCredential(session, opexRealm, user, body.initialCode, otpCredential) return Response.noContent().build() } @GET - @Path("user/2fa/check") + @Path("user/security/check") @Produces(MediaType.APPLICATION_JSON) fun is2FAEnabled(@QueryParam("username") username: String?): Response { val auth = ResourceAuthenticator.bearerAuth(session) @@ -185,13 +202,14 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return ErrorHandler.forbidden() val user = session.users().getUserByUsername(username, opexRealm) - ?: return Response.ok(Check2FAResponse(false)).build() + ?: return Response.ok(UserSecurityCheckResponse(false)).build() - return Response.ok(Check2FAResponse(is2FAEnabled(user))).build() + return Response.ok(UserSecurityCheckResponse(is2FAEnabled(user))).build() } @GET @Path("user/sessions") + @Produces(MediaType.APPLICATION_JSON) fun getActiveSessions(): Response { val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) diff --git a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt index f19a34916..615f7b64f 100644 --- a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt +++ b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt @@ -26,6 +26,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus // code 5000: user-management EmailAlreadyVerified(5001, "Email is already verified", HttpStatus.BAD_REQUEST), GroupNotFound(5002, "Group not found", HttpStatus.NOT_FOUND), + OTPAlreadyEnabled(5003, "2FA/OTP already configured", HttpStatus.BAD_REQUEST), // code 6000: wallet WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND), From 9a1d9dfc79751dbf7e9d9bbf693a7f958812ec72 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 13 Mar 2022 15:56:04 +0330 Subject: [PATCH 4/7] Add disable 2fa service --- .../gateway/data/ChangePasswordRequest.kt | 2 +- .../extension/UserManagementResource.kt | 63 ++++++++++--------- .../opex/auth/gateway/utils/ErrorHandler.kt | 2 + .../opex/utility/error/data/OpexError.kt | 1 + 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt index d63ac2607..ac993d034 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ChangePasswordRequest.kt @@ -3,5 +3,5 @@ package co.nilin.opex.auth.gateway.data data class ChangePasswordRequest( val password: String, val newPassword: String, - val newPasswordConfirmation: String, + val confirmation: String, ) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt index 7e855f1bc..768f49a8a 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt @@ -87,18 +87,13 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour @POST @Path("user/verify-email") @Produces(MediaType.APPLICATION_JSON) - fun sendVerifyEmail(@QueryParam("email") email: String?): Response { + fun sendVerifyEmail(): Response { val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val user = session.users().getUserByEmail(email, opexRealm) - if (user != null) { - if (!auth.hasUserAccess(user.id)) - return ErrorHandler.forbidden() - - sendEmail(user, listOf(UserModel.RequiredAction.VERIFY_EMAIL.name)) - } + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() + sendEmail(user, listOf(UserModel.RequiredAction.VERIFY_EMAIL.name)) return Response.noContent().build() } @@ -112,11 +107,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val user = session.users().getUserById(auth.getUserId(), opexRealm) - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.NotFound, "User not found") - ) + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() val cred = UserCredentialModel.password(body.password) if (!session.userCredentialManager().isValid(opexRealm, user, cred)) { @@ -126,7 +117,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour ) } - if (body.newPasswordConfirmation == body.newPassword) { + if (body.confirmation == body.newPassword) { return ErrorHandler.response( Response.Status.BAD_REQUEST, OpexException(OpexError.BadRequest, "Invalid password confirmation") @@ -147,12 +138,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val user = session.users().getUserById(auth.getUserId(), opexRealm) - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.NotFound, "User not found") - ) - + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() if (is2FAEnabled(user)) { return ErrorHandler.response( Response.Status.BAD_REQUEST, @@ -175,11 +161,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val user = session.users().getUserById(auth.getUserId(), opexRealm) - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.NotFound, "User not found") - ) + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() if (is2FAEnabled(user)) { return ErrorHandler.response( @@ -193,6 +175,30 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.noContent().build() } + @DELETE + @Path("user/security/otp") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + fun disable2FA(): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() + + val response = Response.noContent().build() + if (!is2FAEnabled(user)) + return response + + session.userCredentialManager() + .getStoredCredentialsByTypeStream(opexRealm, user, OTPCredentialModel.TYPE) + .toList() + .find { it.type == OTPCredentialModel.TYPE } + ?.let { session.userCredentialManager().removeStoredCredential(opexRealm, user, it.id) } + + return response + } + @GET @Path("user/security/check") @Produces(MediaType.APPLICATION_JSON) @@ -215,12 +221,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val user = session.users().getUserById(auth.getUserId(), opexRealm) - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.NotFound, "User not found") - ) - + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() val sessions = session.sessions().getUserSessionsStream(opexRealm, user) .map { UserSessionResponse(it.ipAddress, it.started, it.lastSessionRefresh, it.state.name) } .toList() diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ErrorHandler.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ErrorHandler.kt index 2f00fcc5e..25bb33fee 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ErrorHandler.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ErrorHandler.kt @@ -15,4 +15,6 @@ object ErrorHandler { fun forbidden() = response(Response.Status.FORBIDDEN, OpexException(OpexError.Forbidden)) + fun userNotFound() = ErrorHandler.response(Response.Status.NOT_FOUND, OpexException(OpexError.UserNotFound)) + } \ No newline at end of file diff --git a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt index 615f7b64f..8da868e14 100644 --- a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt +++ b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt @@ -27,6 +27,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus EmailAlreadyVerified(5001, "Email is already verified", HttpStatus.BAD_REQUEST), GroupNotFound(5002, "Group not found", HttpStatus.NOT_FOUND), OTPAlreadyEnabled(5003, "2FA/OTP already configured", HttpStatus.BAD_REQUEST), + UserNotFound(5004, "User not found", HttpStatus.NOT_FOUND), // code 6000: wallet WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND), From d721bea0e91427a3495ce5a2caa1b9176e4d3bd4 Mon Sep 17 00:00:00 2001 From: Peyman Date: Mon, 14 Mar 2022 12:49:20 +0330 Subject: [PATCH 5/7] Fix authentication error Close #220 --- .../opex/auth/gateway/data/Get2FAResponse.kt | 2 +- .../extension/UserManagementResource.kt | 33 ++++++------------- .../gateway/extension/UserProfileResource.kt | 30 ++++------------- .../opex/auth/gateway/utils/ErrorHandler.kt | 8 +++-- .../gateway/utils/ResourceAuthenticator.kt | 10 +++--- 5 files changed, 30 insertions(+), 53 deletions(-) diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt index 0028dfdfc..a1fbd219a 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/Get2FAResponse.kt @@ -1,3 +1,3 @@ package co.nilin.opex.auth.gateway.data -data class Get2FAResponse(val uri: String, val secret: String, val qr:String) \ No newline at end of file +data class Get2FAResponse(val uri: String, val secret: String, val qr: String) \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt index 768f49a8a..74e40cf26 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserManagementResource.kt @@ -49,7 +49,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return ErrorHandler.forbidden() if (!request.isValid()) - return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexException(OpexError.BadRequest)) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.BadRequest) val user = session.users().addUser(opexRealm, request.username).apply { email = request.email @@ -110,19 +110,15 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() val cred = UserCredentialModel.password(body.password) - if (!session.userCredentialManager().isValid(opexRealm, user, cred)) { - return ErrorHandler.response( - Response.Status.FORBIDDEN, - OpexException(OpexError.Forbidden, "Incorrect password") - ) - } + if (!session.userCredentialManager().isValid(opexRealm, user, cred)) + return ErrorHandler.response(Response.Status.FORBIDDEN, OpexError.Forbidden, "Incorrect password") - if (body.confirmation == body.newPassword) { + if (body.confirmation == body.newPassword) return ErrorHandler.response( Response.Status.BAD_REQUEST, - OpexException(OpexError.BadRequest, "Invalid password confirmation") + OpexError.BadRequest, + "Invalid password confirmation" ) - } session.userCredentialManager() .updateCredential(opexRealm, user, UserCredentialModel.password(body.newPassword, false)) @@ -139,12 +135,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return ErrorHandler.forbidden() val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() - if (is2FAEnabled(user)) { - return ErrorHandler.response( - Response.Status.BAD_REQUEST, - OpexException(OpexError.OTPAlreadyEnabled) - ) - } + if (is2FAEnabled(user)) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.OTPAlreadyEnabled) val secret = HmacOTP.generateSecret(64) val uri = OTPUtils.generateOTPKeyURI(opexRealm, secret, "Opex", user.email) @@ -162,13 +154,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return ErrorHandler.forbidden() val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() - - if (is2FAEnabled(user)) { - return ErrorHandler.response( - Response.Status.BAD_REQUEST, - OpexException(OpexError.OTPAlreadyEnabled) - ) - } + if (is2FAEnabled(user)) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.OTPAlreadyEnabled) val otpCredential = OTPCredentialModel.createFromPolicy(opexRealm, body.secret) CredentialHelper.createOTPCredential(session, opexRealm, user, body.initialCode, otpCredential) diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt index 25901d94e..dab1eb138 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt @@ -36,12 +36,7 @@ class UserProfileResource(private val session: KeycloakSession) : RealmResourceP if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val user = session.users().getUserById(auth.getUserId(), opexRealm) - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.NotFound, "User not found") - ) - + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() return Response.ok(user.attributes).build() } @@ -53,11 +48,7 @@ class UserProfileResource(private val session: KeycloakSession) : RealmResourceP if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val user = session.users().getUserById(auth.getUserId(), opexRealm) - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.NotFound, "User not found") - ) + val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() with(request) { firstNameEn?.let { user.setSingleAttribute("firstNameEn", it) } @@ -87,18 +78,14 @@ class UserProfileResource(private val session: KeycloakSession) : RealmResourceP return ErrorHandler.forbidden() val userId = auth.getUserId()!! - val user = session.users().getUserById(userId, opexRealm) - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.NotFound, "User not found") - ) + val user = session.users().getUserById(userId, opexRealm) ?: return ErrorHandler.userNotFound() - if (isInKycGroups(user)) { + if (isInKycGroups(user)) return ErrorHandler.response( Response.Status.BAD_REQUEST, - OpexException(OpexError.BadRequest, "User is already in kyc groups") + OpexError.BadRequest, + "User is already in kyc groups" ) - } /*val forms = input.formDataMap @@ -115,10 +102,7 @@ class UserProfileResource(private val session: KeycloakSession) : RealmResourceP .getGroupsStream(opexRealm) .toList() .find { it.name == "kyc-requested" } - ?: return ErrorHandler.response( - Response.Status.NOT_FOUND, - OpexException(OpexError.GroupNotFound) - ) + ?: return ErrorHandler.response(Response.Status.NOT_FOUND, OpexError.GroupNotFound) user.joinGroup(kycRequestGroup) diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ErrorHandler.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ErrorHandler.kt index 25bb33fee..d1fca53df 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ErrorHandler.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ErrorHandler.kt @@ -13,8 +13,12 @@ object ErrorHandler { return Response.status(status).entity(translator.translate(ex)).build() } - fun forbidden() = response(Response.Status.FORBIDDEN, OpexException(OpexError.Forbidden)) + fun response(status: Response.Status, error: OpexError, message: String? = null): Response { + return Response.status(status).entity(translator.translate(OpexException(error, message))).build() + } + + fun forbidden(message: String? = null) = response(Response.Status.FORBIDDEN, OpexError.Forbidden, message) - fun userNotFound() = ErrorHandler.response(Response.Status.NOT_FOUND, OpexException(OpexError.UserNotFound)) + fun userNotFound(message: String? = null) = response(Response.Status.NOT_FOUND, OpexError.UserNotFound, message) } \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ResourceAuthenticator.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ResourceAuthenticator.kt index 01dfbf212..cd23cfdd2 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ResourceAuthenticator.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ResourceAuthenticator.kt @@ -6,10 +6,10 @@ import org.keycloak.representations.AccessToken import org.keycloak.services.managers.AppAuthManager import org.keycloak.services.managers.AuthenticationManager -class ResourceAuthenticator(private val result: AuthenticationManager.AuthResult) { +class ResourceAuthenticator(private val result: AuthenticationManager.AuthResult?) { - private val user: UserModel? = result.user - private val token: AccessToken = result.token + private val user: UserModel? = result?.user + private val token: AccessToken? = result?.token fun getUserId() = user?.id @@ -18,11 +18,13 @@ class ResourceAuthenticator(private val result: AuthenticationManager.AuthResult } fun hasScopeAccess(scope: String): Boolean { + if (token == null) return false return token.scope.split(" ").contains(scope) } fun hasUserAccess(userId: String? = null): Boolean { - return userId != null && user?.id == userId + if (user == null) return false + return userId != null && user.id == userId } companion object { From 5a6ec253f05864efd44229d7b933c6006fa5a162 Mon Sep 17 00:00:00 2001 From: Peyman Date: Mon, 14 Mar 2022 12:55:05 +0330 Subject: [PATCH 6/7] Update realm config --- .../src/main/resources/opex-realm.json | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/user-management/keycloak-gateway/src/main/resources/opex-realm.json b/user-management/keycloak-gateway/src/main/resources/opex-realm.json index 0d480eb6d..8756c8008 100644 --- a/user-management/keycloak-gateway/src/main/resources/opex-realm.json +++ b/user-management/keycloak-gateway/src/main/resources/opex-realm.json @@ -25,7 +25,7 @@ "enabled": true, "sslRequired": "none", "registrationAllowed": true, - "registrationEmailAsUsername": false, + "registrationEmailAsUsername": true, "rememberMe": false, "verifyEmail": false, "loginWithEmailAllowed": true, @@ -476,7 +476,7 @@ "otpPolicyAlgorithm": "HmacSHA1", "otpPolicyInitialCounter": 0, "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, + "otpPolicyLookAheadWindow": 5, "otpPolicyPeriod": 30, "otpSupportedApplications": [ "FreeOTP", @@ -932,6 +932,7 @@ "consentRequired": false, "config": { "multivalued": "true", + "userinfo.token.claim": "true", "user.attribute": "foo", "id.token.claim": "true", "access.token.claim": "true", @@ -2028,6 +2029,7 @@ "consentRequired": false, "config": { "user.session.note": "clientHost", + "userinfo.token.claim": "true", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientHost", @@ -2042,6 +2044,7 @@ "consentRequired": false, "config": { "user.session.note": "clientAddress", + "userinfo.token.claim": "true", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientAddress", @@ -2056,6 +2059,7 @@ "consentRequired": false, "config": { "user.session.note": "clientId", + "userinfo.token.claim": "true", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "clientId", @@ -2780,13 +2784,13 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-user-attribute-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper" ] } @@ -2807,13 +2811,13 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", - "oidc-address-mapper", - "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", + "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", - "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "oidc-address-mapper", "saml-user-attribute-mapper" ] } From 9497f9b72629a4620d69b4314786ed3ee3ee024f Mon Sep 17 00:00:00 2001 From: Peyman Date: Mon, 14 Mar 2022 13:12:32 +0330 Subject: [PATCH 7/7] Removed force not null --- .../nilin/opex/auth/gateway/extension/UserProfileResource.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt index dab1eb138..f090b7620 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/extension/UserProfileResource.kt @@ -77,7 +77,7 @@ class UserProfileResource(private val session: KeycloakSession) : RealmResourceP if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val userId = auth.getUserId()!! + val userId = auth.getUserId() val user = session.users().getUserById(userId, opexRealm) ?: return ErrorHandler.userNotFound() if (isInKycGroups(user)) @@ -104,9 +104,8 @@ class UserProfileResource(private val session: KeycloakSession) : RealmResourceP .find { it.name == "kyc-requested" } ?: return ErrorHandler.response(Response.Status.NOT_FOUND, OpexError.GroupNotFound) - user.joinGroup(kycRequestGroup) - user.apply { + joinGroup(kycRequestGroup) setSingleAttribute("kycSelfiePath", request.selfiePath) setSingleAttribute("kycIdCardPath", request.idCardPath) setSingleAttribute("kycAcceptFormPath", request.acceptFormPath)