From 9ba7fb99e5993a84e0d0cbadfe7cc1fb71f14c87 Mon Sep 17 00:00:00 2001 From: Peyman Date: Fri, 13 May 2022 14:42:59 +0430 Subject: [PATCH 1/9] Changing register flow --- .../auth/gateway/data/RegisterUserRequest.kt | 14 ++++++++++- .../extension/UserManagementResource.kt | 24 +++++++++++++++++-- .../opex/utility/error/data/OpexError.kt | 2 ++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/RegisterUserRequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/RegisterUserRequest.kt index b8981b2c0..a07914a5a 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/RegisterUserRequest.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/RegisterUserRequest.kt @@ -6,13 +6,25 @@ class RegisterUserRequest { var lastName: String? = null var email: String? = null var captchaAnswer: String? = null + var password: String? = null + var passwordConfirmation: String? = null constructor() - constructor(firstName: String?, lastName: String?, email: String?) { + constructor( + firstName: String?, + lastName: String?, + email: String?, + captchaAnswer: String?, + password: String?, + passwordConfirmation: String? + ) { this.firstName = firstName this.lastName = lastName this.email = email + this.captchaAnswer = captchaAnswer + this.password = password + this.passwordConfirmation = passwordConfirmation } fun isValid(): Boolean { 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 1315e8a31..57578f235 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 @@ -24,6 +24,7 @@ import org.keycloak.models.UserModel import org.keycloak.models.credential.OTPCredentialModel import org.keycloak.models.utils.CredentialValidation import org.keycloak.models.utils.HmacOTP +import org.keycloak.policy.PasswordPolicyManagerProvider import org.keycloak.services.managers.AuthenticationManager import org.keycloak.services.resource.RealmResourceProvider import org.keycloak.services.resources.LoginActionsService @@ -58,7 +59,22 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha) } - if (!request.isValid()) return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.BadRequest) + if (!request.isValid()) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.BadRequest) + + if (session.users().getUserByEmail(request.email, opexRealm) != null) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.UserAlreadyExists) + + if (request.password != request.passwordConfirmation) + return ErrorHandler.badRequest("Invalid password confirmation") + + val error = session.getProvider(PasswordPolicyManagerProvider::class.java) + .validate(request.email, request.password) + + if (error != null) { + logger.error(error.message) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidPassword) + } val user = session.users().addUser(opexRealm, request.email).apply { email = request.email @@ -72,6 +88,9 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour sendEmail(this, requiredActionsStream.toList()) } + session.userCredentialManager() + .updateCredential(opexRealm, user, UserCredentialModel.password(request.password, false)) + logger.info("User create response ${user.id}") sendUserEvent(user) @@ -129,7 +148,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour .isValid(opexRealm, user, cred) ) return ErrorHandler.forbidden("Incorrect password") - if (body.confirmation != body.newPassword) return ErrorHandler.badRequest("Invalid password confirmation") + if (body.confirmation != body.newPassword) + return ErrorHandler.badRequest("Invalid password confirmation") session.userCredentialManager() .updateCredential(opexRealm, user, UserCredentialModel.password(body.newPassword, false)) 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 634b517b9..9454dea9a 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 @@ -32,6 +32,8 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus OTPRequired(5006, "OTP Required", HttpStatus.BAD_REQUEST), AlreadyInKYC(5007, "KYC flow for this user has executed", HttpStatus.BAD_REQUEST), UserKYCBlocked(5008, "User is blocked from KYC", HttpStatus.BAD_REQUEST), + InvalidPassword(5009, "Password is not valid", HttpStatus.BAD_REQUEST), + UserAlreadyExists(5009, "User with email is already registered", HttpStatus.BAD_REQUEST), // code 6000: wallet WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND), From 64ddf5b75d7b72b788614a7fcaf70d40222af3d1 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 17 May 2022 12:19:29 +0430 Subject: [PATCH 2/9] Starting verify email --- .../gateway/extension/UserManagementResource.kt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 57578f235..d19304731 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 @@ -53,11 +53,12 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - runCatching { + + /*runCatching { validateCaptcha("${request.captchaAnswer}-${session.context.connection.remoteAddr}") }.onFailure { return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha) - } + }*/ if (!request.isValid()) return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.BadRequest) @@ -121,7 +122,14 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour } @POST - @Path("user/verify-email") + @Path("user/verify") + fun verifyEmail(): Response { + + return Response.noContent().build() + } + + @POST + @Path("user/request-verify") @Produces(MediaType.APPLICATION_JSON) fun sendVerifyEmail(): Response { val auth = ResourceAuthenticator.bearerAuth(session) @@ -311,6 +319,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour queryParam("key", token.serialize(session, opexRealm, session.context.uri)) } val link = builder.build(opexRealm.name).toString() + logger.info("************** link: $link") + provider.setRealm(opexRealm).setUser(user) .sendVerifyEmail(link, TimeUnit.SECONDS.toMinutes(lifespan.toLong())) } catch (e: Exception) { From 822286070d9f488bfb03434f0eb66c0402d24402 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 17 May 2022 15:21:20 +0430 Subject: [PATCH 3/9] Add password policy --- docker-compose.yml | 1 + .../extension/UserManagementResource.kt | 49 +++++++++++++------ .../src/main/resources/application.yml | 2 + .../src/main/resources/opex-realm.json | 1 + 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 17ad65393..c916b9229 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -346,6 +346,7 @@ services: - BACKEND_USER=$BACKEND_USER - ADMIN_URL=$KEYCLOAK_ADMIN_URL - FRONTEND_URL=$KEYCLOAK_FRONTEND_URL + - VERIFY_URL=$KEYCLOAK_VERIFY_URL - VAULT_URL=http://vault:8200 - VAULT_HOST=vault depends_on: 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 d19304731..f68e9b24d 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 @@ -39,10 +39,14 @@ import javax.ws.rs.core.Response import kotlin.streams.toList class UserManagementResource(private val session: KeycloakSession) : RealmResourceProvider { + private val logger = LoggerFactory.getLogger(UserManagementResource::class.java) private val opexRealm = session.realms().getRealm("opex") + private val verifyUrl by lazy { + ApplicationContextHolder.getCurrentContext()!!.environment.resolvePlaceholders("\${verify-url}") + } private val kafkaTemplate by lazy { - ApplicationContextHolder.getCurrentContext()!!.getBean("authKafkaTemplate") as KafkaTemplate + ApplicationContextHolder.getCurrentContext()!!.getBean("authKafkaTemplate",) as KafkaTemplate } @POST @@ -86,7 +90,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL) addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD) - sendEmail(this, requiredActionsStream.toList()) + sendEmail(this, requiredActionsStream.toList(), verifyUrl) } session.userCredentialManager() @@ -123,7 +127,18 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour @POST @Path("user/verify") - fun verifyEmail(): Response { + fun verifyEmail(@QueryParam("token") token: String): Response { + val actionToken = session.tokens().decode(token, ExecuteActionsActionToken::class.java) + if (actionToken == null || !actionToken.isActive || actionToken.requiredActions.isEmpty()) + return ErrorHandler.forbidden() + + if (!actionToken.requiredActions.contains(UserModel.RequiredAction.VERIFY_EMAIL.name)) + return Response.noContent().build() + + with(session.users().getUserById(actionToken.subject, opexRealm)) { + removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL) + isEmailVerified = true + } return Response.noContent().build() } @@ -302,27 +317,33 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.ok(sessions).build() } - private fun sendEmail(user: UserModel, actions: List) { + private fun sendEmail(user: UserModel, actions: List, link: String? = null) { if (!user.isEnabled) throw OpexException(OpexError.BadRequest, "User is disabled") val clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID val client = session.clients().getClientByClientId(opexRealm, clientId) if (client == null || !client.isEnabled) throw OpexException(OpexError.BadRequest, "Client error") - val lifespan = opexRealm.actionTokenGeneratedByAdminLifespan - val expiration = Time.currentTime() + lifespan - val token = ExecuteActionsActionToken(user.id, expiration, actions, null, clientId) - try { - val provider = session.getProvider(EmailTemplateProvider::class.java) - val builder = LoginActionsService.actionTokenProcessor(session.context.uri).apply { - queryParam("key", token.serialize(session, opexRealm, session.context.uri)) + val lifespan = opexRealm.actionTokenGeneratedByAdminLifespan + val expiration = Time.currentTime() + lifespan + + //TODO It might be better to use redirect uri + val token = ExecuteActionsActionToken(user.id, expiration, actions, null, clientId) + val serializedToken = token.serialize(session, opexRealm, session.context.uri) + val verifyLink = if (link.isNullOrEmpty()) { + LoginActionsService.actionTokenProcessor(session.context.uri) + .queryParam("key", serializedToken) + .build(opexRealm.name) + .toString() + } else { + "$link?key=$serializedToken" } - val link = builder.build(opexRealm.name).toString() - logger.info("************** link: $link") + val provider = session.getProvider(EmailTemplateProvider::class.java) + logger.info("verify link: $verifyLink") provider.setRealm(opexRealm).setUser(user) - .sendVerifyEmail(link, TimeUnit.SECONDS.toMinutes(lifespan.toLong())) + .sendVerifyEmail(verifyLink, TimeUnit.SECONDS.toMinutes(lifespan.toLong())) } catch (e: Exception) { logger.error("Unable to send verification email") e.printStackTrace() diff --git a/user-management/keycloak-gateway/src/main/resources/application.yml b/user-management/keycloak-gateway/src/main/resources/application.yml index 39159e8a6..e60d953b6 100644 --- a/user-management/keycloak-gateway/src/main/resources/application.yml +++ b/user-management/keycloak-gateway/src/main/resources/application.yml @@ -70,3 +70,5 @@ keycloak: token_exchange: enabled hashicorp: url: ${VAULT_URL} +app: + verify-url: ${VERIFY_URL} \ No newline at end of file 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 209292e34..9a048b797 100644 --- a/user-management/keycloak-gateway/src/main/resources/opex-realm.json +++ b/user-management/keycloak-gateway/src/main/resources/opex-realm.json @@ -482,6 +482,7 @@ "requiredCredentials": [ "password" ], + "passwordPolicy": "length(8)", "otpPolicyType": "totp", "otpPolicyAlgorithm": "HmacSHA1", "otpPolicyInitialCounter": 0, From 9266dddd5a4f90c218c4ec980ac84f31bf5fc563 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 29 May 2022 17:23:28 +0430 Subject: [PATCH 4/9] Fix issues --- .../auth/gateway/extension/UserManagementResource.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 f68e9b24d..e8661dd60 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 @@ -46,7 +46,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour ApplicationContextHolder.getCurrentContext()!!.environment.resolvePlaceholders("\${verify-url}") } private val kafkaTemplate by lazy { - ApplicationContextHolder.getCurrentContext()!!.getBean("authKafkaTemplate",) as KafkaTemplate + ApplicationContextHolder.getCurrentContext()!!.getBean("authKafkaTemplate") as KafkaTemplate } @POST @@ -89,7 +89,6 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour isEmailVerified = false addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL) - addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD) sendEmail(this, requiredActionsStream.toList(), verifyUrl) } @@ -127,6 +126,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour @POST @Path("user/verify") + @Produces(MediaType.APPLICATION_JSON) fun verifyEmail(@QueryParam("token") token: String): Response { val actionToken = session.tokens().decode(token, ExecuteActionsActionToken::class.java) if (actionToken == null || !actionToken.isActive || actionToken.requiredActions.isEmpty()) @@ -151,7 +151,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.userNotFound() - sendEmail(user, listOf(UserModel.RequiredAction.VERIFY_EMAIL.name)) + sendEmail(user, listOf(UserModel.RequiredAction.VERIFY_EMAIL.name), verifyUrl) return Response.noContent().build() } @@ -328,7 +328,6 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val lifespan = opexRealm.actionTokenGeneratedByAdminLifespan val expiration = Time.currentTime() + lifespan - //TODO It might be better to use redirect uri val token = ExecuteActionsActionToken(user.id, expiration, actions, null, clientId) val serializedToken = token.serialize(session, opexRealm, session.context.uri) val verifyLink = if (link.isNullOrEmpty()) { @@ -342,7 +341,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val provider = session.getProvider(EmailTemplateProvider::class.java) logger.info("verify link: $verifyLink") - provider.setRealm(opexRealm).setUser(user) + provider.setRealm(opexRealm) + .setUser(user) .sendVerifyEmail(verifyLink, TimeUnit.SECONDS.toMinutes(lifespan.toLong())) } catch (e: Exception) { logger.error("Unable to send verification email") From 8915cd84176d909c6bff3f0ae07716603b963923 Mon Sep 17 00:00:00 2001 From: Peyman Date: Mon, 30 May 2022 11:45:43 +0430 Subject: [PATCH 5/9] Uncomment captcha --- .../extension/UserManagementResource.kt | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) 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 e8661dd60..1ad507a9c 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 @@ -57,12 +57,11 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - - /*runCatching { + runCatching { validateCaptcha("${request.captchaAnswer}-${session.context.connection.remoteAddr}") }.onFailure { return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha) - }*/ + } if (!request.isValid()) return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.BadRequest) @@ -167,9 +166,8 @@ 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.forbidden("Incorrect password") + if (!session.userCredentialManager().isValid(opexRealm, user, cred)) + return ErrorHandler.forbidden("Incorrect password") if (body.confirmation != body.newPassword) return ErrorHandler.badRequest("Invalid password confirmation") @@ -360,14 +358,6 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return session.userCredentialManager().isConfiguredFor(opexRealm, user, OTPCredentialModel.TYPE) } - override fun close() { - - } - - override fun getResource(): Any { - return this - } - private fun validateCaptcha(proof: String) { val client: HttpClient = HttpClientBuilder.create().build() val post = HttpGet(URIBuilder("http://captcha:8080/verify").addParameter("proof", proof).build()) @@ -377,4 +367,12 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour require(response.statusLine.statusCode / 100 == 2) { "Invalid captcha" } } } + + override fun close() { + + } + + override fun getResource(): Any { + return this + } } From 9c321db9f25765b976225ae5d89a71a5e5a56c93 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 31 May 2022 12:28:08 +0430 Subject: [PATCH 6/9] Close #254: custom verify service --- docker-compose.yml | 2 +- .../extension/UserManagementResource.kt | 90 ++++++++++--------- .../auth/gateway/utils/ActionTokenHelper.kt | 43 +++++++++ .../src/main/resources/application.yml | 2 +- 4 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ActionTokenHelper.kt diff --git a/docker-compose.yml b/docker-compose.yml index c916b9229..12fd75099 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -346,7 +346,7 @@ services: - BACKEND_USER=$BACKEND_USER - ADMIN_URL=$KEYCLOAK_ADMIN_URL - FRONTEND_URL=$KEYCLOAK_FRONTEND_URL - - VERIFY_URL=$KEYCLOAK_VERIFY_URL + - VERIFY_REDIRECT_URL=$KEYCLOAK_VERIFY_REDIRECT_URL - VAULT_URL=http://vault:8200 - VAULT_HOST=vault depends_on: 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 1ad507a9c..461f6c44a 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 @@ -4,10 +4,7 @@ import co.nilin.opex.auth.gateway.ApplicationContextHolder 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.auth.gateway.utils.tryOrElse +import co.nilin.opex.auth.gateway.utils.* import co.nilin.opex.utility.error.data.OpexError import co.nilin.opex.utility.error.data.OpexException import org.apache.http.client.HttpClient @@ -27,7 +24,7 @@ import org.keycloak.models.utils.HmacOTP import org.keycloak.policy.PasswordPolicyManagerProvider import org.keycloak.services.managers.AuthenticationManager import org.keycloak.services.resource.RealmResourceProvider -import org.keycloak.services.resources.LoginActionsService +import org.keycloak.urls.UrlType import org.keycloak.utils.CredentialHelper import org.keycloak.utils.TotpUtils import org.slf4j.LoggerFactory @@ -36,6 +33,7 @@ import java.util.concurrent.TimeUnit import javax.ws.rs.* import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response +import javax.ws.rs.core.UriBuilder import kotlin.streams.toList class UserManagementResource(private val session: KeycloakSession) : RealmResourceProvider { @@ -43,7 +41,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour private val logger = LoggerFactory.getLogger(UserManagementResource::class.java) private val opexRealm = session.realms().getRealm("opex") private val verifyUrl by lazy { - ApplicationContextHolder.getCurrentContext()!!.environment.resolvePlaceholders("\${verify-url}") + ApplicationContextHolder.getCurrentContext()!!.environment.resolvePlaceholders("\${verify-redirect-url}") } private val kafkaTemplate by lazy { ApplicationContextHolder.getCurrentContext()!!.getBean("authKafkaTemplate") as KafkaTemplate @@ -57,11 +55,11 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - runCatching { + /*runCatching { validateCaptcha("${request.captchaAnswer}-${session.context.connection.remoteAddr}") }.onFailure { return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha) - } + }*/ if (!request.isValid()) return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.BadRequest) @@ -88,7 +86,13 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour isEmailVerified = false addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL) - sendEmail(this, requiredActionsStream.toList(), verifyUrl) + val actions = requiredActionsStream.toList() + val token = ActionTokenHelper.generateRequiredActionsToken(session, opexRealm, this, actions) + val url = "${session.context.getUri(UrlType.BACKEND).baseUri}/realms/opex/user-management/user/verify" + val link = ActionTokenHelper.attachTokenToLink(url, token) + val expiration = Time.currentTime() + opexRealm.actionTokenGeneratedByAdminLifespan + logger.info(link) + sendEmail(this) { it.sendVerifyEmail(link, TimeUnit.SECONDS.toMinutes(expiration.toLong())) } } session.userCredentialManager() @@ -118,28 +122,36 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val user = session.users().getUserByEmail(email, opexRealm) if (user != null) { - sendEmail(user, listOf(UserModel.RequiredAction.UPDATE_PASSWORD.name)) + val token = ActionTokenHelper.generateRequiredActionsToken( + session, + opexRealm, + user, + listOf(UserModel.RequiredAction.UPDATE_PASSWORD.name), + verifyUrl + ) + val link = ActionTokenHelper.createInternalEmailLink(session, opexRealm, token) + val expiration = Time.currentTime() + opexRealm.actionTokenGeneratedByAdminLifespan + sendEmail(user) { it.sendVerifyEmail(link, TimeUnit.SECONDS.toMinutes(expiration.toLong())) } } return Response.noContent().build() } - @POST + @GET @Path("user/verify") - @Produces(MediaType.APPLICATION_JSON) - fun verifyEmail(@QueryParam("token") token: String): Response { - val actionToken = session.tokens().decode(token, ExecuteActionsActionToken::class.java) - if (actionToken == null || !actionToken.isActive || actionToken.requiredActions.isEmpty()) - return ErrorHandler.forbidden() + fun verifyEmail(@QueryParam("key") key: String): Response { + val uri = UriBuilder.fromUri(verifyUrl) + val actionToken = session.tokens().decode(key, ExecuteActionsActionToken::class.java) - if (!actionToken.requiredActions.contains(UserModel.RequiredAction.VERIFY_EMAIL.name)) - return Response.noContent().build() + if (actionToken == null || !actionToken.isActive || actionToken.requiredActions.isEmpty()) + return Response.seeOther(uri.queryParam("result", "failed").build()).build() - with(session.users().getUserById(actionToken.subject, opexRealm)) { - removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL) - isEmailVerified = true + val user = session.users().getUserById(actionToken.subject, opexRealm) + if (actionToken.requiredActions.contains(UserModel.RequiredAction.VERIFY_EMAIL.name)) { + user.removeRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL) + user.isEmailVerified = true } - return Response.noContent().build() + return Response.seeOther(uri.queryParam("result", "success").build()).build() } @POST @@ -150,7 +162,18 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() - sendEmail(user, listOf(UserModel.RequiredAction.VERIFY_EMAIL.name), verifyUrl) + + val token = ActionTokenHelper.generateRequiredActionsToken( + session, + opexRealm, + user, + listOf(UserModel.RequiredAction.VERIFY_EMAIL.name), + verifyUrl + ) + val link = ActionTokenHelper.attachTokenToLink(verifyUrl, token) + val expiration = Time.currentTime() + opexRealm.actionTokenGeneratedByAdminLifespan + sendEmail(user) { it.sendVerifyEmail(link, TimeUnit.SECONDS.toMinutes(expiration.toLong())) } + return Response.noContent().build() } @@ -315,7 +338,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.ok(sessions).build() } - private fun sendEmail(user: UserModel, actions: List, link: String? = null) { + private fun sendEmail(user: UserModel, sendAction: (EmailTemplateProvider) -> Unit) { if (!user.isEnabled) throw OpexException(OpexError.BadRequest, "User is disabled") val clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID @@ -323,25 +346,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour if (client == null || !client.isEnabled) throw OpexException(OpexError.BadRequest, "Client error") try { - val lifespan = opexRealm.actionTokenGeneratedByAdminLifespan - val expiration = Time.currentTime() + lifespan - - val token = ExecuteActionsActionToken(user.id, expiration, actions, null, clientId) - val serializedToken = token.serialize(session, opexRealm, session.context.uri) - val verifyLink = if (link.isNullOrEmpty()) { - LoginActionsService.actionTokenProcessor(session.context.uri) - .queryParam("key", serializedToken) - .build(opexRealm.name) - .toString() - } else { - "$link?key=$serializedToken" - } - val provider = session.getProvider(EmailTemplateProvider::class.java) - logger.info("verify link: $verifyLink") - provider.setRealm(opexRealm) - .setUser(user) - .sendVerifyEmail(verifyLink, TimeUnit.SECONDS.toMinutes(lifespan.toLong())) + sendAction(provider.setRealm(opexRealm).setUser(user)) } catch (e: Exception) { logger.error("Unable to send verification email") e.printStackTrace() diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ActionTokenHelper.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ActionTokenHelper.kt new file mode 100644 index 000000000..16ddea40a --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/utils/ActionTokenHelper.kt @@ -0,0 +1,43 @@ +package co.nilin.opex.auth.gateway.utils + +import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken +import org.keycloak.common.util.Time +import org.keycloak.models.Constants +import org.keycloak.models.KeycloakSession +import org.keycloak.models.RealmModel +import org.keycloak.models.UserModel +import org.keycloak.services.resources.LoginActionsService +import javax.ws.rs.core.UriBuilder + +object ActionTokenHelper { + + fun createInternalEmailLink( + session: KeycloakSession, + realm: RealmModel, + token: String + ): String { + return LoginActionsService.actionTokenProcessor(session.context.uri) + .queryParam("key", token) + .build(realm.name) + .toString() + } + + fun attachTokenToLink(link: String, token: String, paramKey: String = "key"): String { + return UriBuilder.fromUri(link).queryParam(paramKey, token).build().toString() + } + + fun generateRequiredActionsToken( + session: KeycloakSession, + realm: RealmModel, + user: UserModel, + actions: List, + redirectUrl: String? = null + ): String { + val lifespan = realm.actionTokenGeneratedByAdminLifespan + val expiration = Time.currentTime() + lifespan + val clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID + val token = ExecuteActionsActionToken(user.id, expiration, actions, redirectUrl, clientId) + return token.serialize(session, realm, session.context.uri) + } + +} \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/resources/application.yml b/user-management/keycloak-gateway/src/main/resources/application.yml index e60d953b6..664a9faeb 100644 --- a/user-management/keycloak-gateway/src/main/resources/application.yml +++ b/user-management/keycloak-gateway/src/main/resources/application.yml @@ -71,4 +71,4 @@ keycloak: hashicorp: url: ${VAULT_URL} app: - verify-url: ${VERIFY_URL} \ No newline at end of file + verify-redirect-url: ${VERIFY_REDIRECT_URL} \ No newline at end of file From 1d10be0b4e1af0c6d6acaf1c43c1c115f75b815a Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 31 May 2022 12:28:42 +0430 Subject: [PATCH 7/9] Uncomment captcha --- .../opex/auth/gateway/extension/UserManagementResource.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 461f6c44a..e2846cc11 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 @@ -55,11 +55,11 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - /*runCatching { + runCatching { validateCaptcha("${request.captchaAnswer}-${session.context.connection.remoteAddr}") }.onFailure { return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha) - }*/ + } if (!request.isValid()) return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.BadRequest) From ffdfb8ecc7f785959f80000e3bce6883df4abda7 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 31 May 2022 12:49:20 +0430 Subject: [PATCH 8/9] Fix request verify issue --- .../opex/auth/gateway/extension/UserManagementResource.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 e2846cc11..16eb6286e 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 @@ -167,10 +167,10 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour session, opexRealm, user, - listOf(UserModel.RequiredAction.VERIFY_EMAIL.name), - verifyUrl + listOf(UserModel.RequiredAction.VERIFY_EMAIL.name) ) - val link = ActionTokenHelper.attachTokenToLink(verifyUrl, token) + val url = "${session.context.getUri(UrlType.BACKEND).baseUri}/realms/opex/user-management/user/verify" + val link = ActionTokenHelper.attachTokenToLink(url, token) val expiration = Time.currentTime() + opexRealm.actionTokenGeneratedByAdminLifespan sendEmail(user) { it.sendVerifyEmail(link, TimeUnit.SECONDS.toMinutes(expiration.toLong())) } From 0825b72a64fbfd4a40f3eb1c3dccda262a51112b Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 31 May 2022 12:57:36 +0430 Subject: [PATCH 9/9] Add ActionTokenResult --- .../opex/auth/gateway/extension/UserManagementResource.kt | 5 +++-- .../co/nilin/opex/auth/gateway/model/ActionTokenResult.kt | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/ActionTokenResult.kt 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 16eb6286e..7e783f404 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 @@ -2,6 +2,7 @@ package co.nilin.opex.auth.gateway.extension import co.nilin.opex.auth.gateway.ApplicationContextHolder import co.nilin.opex.auth.gateway.data.* +import co.nilin.opex.auth.gateway.model.ActionTokenResult import co.nilin.opex.auth.gateway.model.AuthEvent import co.nilin.opex.auth.gateway.model.UserCreatedEvent import co.nilin.opex.auth.gateway.utils.* @@ -143,7 +144,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val actionToken = session.tokens().decode(key, ExecuteActionsActionToken::class.java) if (actionToken == null || !actionToken.isActive || actionToken.requiredActions.isEmpty()) - return Response.seeOther(uri.queryParam("result", "failed").build()).build() + return Response.seeOther(uri.queryParam("result", ActionTokenResult.FAILED).build()).build() val user = session.users().getUserById(actionToken.subject, opexRealm) if (actionToken.requiredActions.contains(UserModel.RequiredAction.VERIFY_EMAIL.name)) { @@ -151,7 +152,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour user.isEmailVerified = true } - return Response.seeOther(uri.queryParam("result", "success").build()).build() + return Response.seeOther(uri.queryParam("result", ActionTokenResult.SUCCEED).build()).build() } @POST diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/ActionTokenResult.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/ActionTokenResult.kt new file mode 100644 index 000000000..9f3956f37 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/model/ActionTokenResult.kt @@ -0,0 +1,7 @@ +package co.nilin.opex.auth.gateway.model + +enum class ActionTokenResult { + + SUCCEED, FAILED + +} \ No newline at end of file