diff --git a/docker-compose.yml b/docker-compose.yml index 6848c3688..34d765d3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -278,6 +278,7 @@ services: - ADMIN_URL=$KEYCLOAK_ADMIN_URL - FRONTEND_URL=$KEYCLOAK_FRONTEND_URL - VERIFY_REDIRECT_URL=$KEYCLOAK_VERIFY_REDIRECT_URL + - FORGOT_REDIRECT_URL=$KEYCLOAK_FORGOT_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/data/ForgotPasswordRequest.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ForgotPasswordRequest.kt new file mode 100644 index 000000000..50aebde38 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/data/ForgotPasswordRequest.kt @@ -0,0 +1,15 @@ +package co.nilin.opex.auth.gateway.data + +class ForgotPasswordRequest { + + var password: String? = null + var passwordConfirmation: String? = null + + constructor() + + constructor(password: String?, passwordConfirmation: String?) { + this.password = password + this.passwordConfirmation = passwordConfirmation + } + +} \ 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 7e783f404..1a8f8be7f 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 @@ -44,6 +44,9 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour private val verifyUrl by lazy { ApplicationContextHolder.getCurrentContext()!!.environment.resolvePlaceholders("\${verify-redirect-url}") } + private val forgotUrl by lazy { + ApplicationContextHolder.getCurrentContext()!!.environment.resolvePlaceholders("\${forgot-redirect-url}") + } private val kafkaTemplate by lazy { ApplicationContextHolder.getCurrentContext()!!.getBean("authKafkaTemplate") as KafkaTemplate } @@ -91,9 +94,9 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour 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 + val expiration = TimeUnit.SECONDS.toMinutes(opexRealm.actionTokenGeneratedByAdminLifespan.toLong()) logger.info(link) - sendEmail(this) { it.sendVerifyEmail(link, TimeUnit.SECONDS.toMinutes(expiration.toLong())) } + sendEmail(this) { it.sendVerifyEmail(link, expiration) } } session.userCredentialManager() @@ -106,17 +109,16 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour } @POST - @Path("user/forgot") + @Path("user/request-forgot") @Produces(MediaType.APPLICATION_JSON) fun forgotPassword( @QueryParam("email") email: String?, - @QueryParam("captcha-answer") captchaAnswer: String + @QueryParam("captcha") captcha: String ): Response { - val auth = ResourceAuthenticator.bearerAuth(session) - if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() + val uri = UriBuilder.fromUri(forgotUrl) runCatching { - validateCaptcha("$captchaAnswer-${session.context.connection.remoteAddr}") + validateCaptcha("$captcha-${session.context.connection.remoteAddr}") }.onFailure { return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidCaptcha) } @@ -130,10 +132,39 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour 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())) } + + val link = uri.queryParam("key", token).build().toString() + val expiration = TimeUnit.SECONDS.toMinutes(opexRealm.actionTokenGeneratedByAdminLifespan.toLong()) + logger.info(link) + logger.info(expiration.toString()) + sendEmail(user) { it.sendVerifyEmail(link, expiration) } } + + return Response.noContent().build() + } + + @PUT + @Path("user/forgot") + fun forgotPassword(@QueryParam("key") key: String, body: ForgotPasswordRequest): Response { + val actionToken = session.tokens().decode(key, ExecuteActionsActionToken::class.java) + + if (actionToken == null || !actionToken.isActive || actionToken.requiredActions.isEmpty()) + return ErrorHandler.badRequest() + + val user = session.users().getUserById(actionToken.subject, opexRealm) ?: return ErrorHandler.userNotFound() + if (body.password != body.passwordConfirmation) + return ErrorHandler.badRequest("Invalid password confirmation") + + val error = session.getProvider(PasswordPolicyManagerProvider::class.java) + .validate(user.email, body.password) + + if (error != null) { + logger.error(error.message) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidPassword) + } + session.userCredentialManager() + .updateCredential(opexRealm, user, UserCredentialModel.password(body.password, false)) + return Response.noContent().build() } @@ -158,22 +189,22 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour @POST @Path("user/request-verify") @Produces(MediaType.APPLICATION_JSON) - fun sendVerifyEmail(): Response { - val auth = ResourceAuthenticator.bearerAuth(session) - if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - - val user = session.users().getUserById(auth.getUserId(), opexRealm) ?: return ErrorHandler.userNotFound() + fun sendVerifyEmail(@QueryParam("email") email: String?): Response { + val user = session.users().getUserByEmail(email, opexRealm) + if (user != null) { + val token = ActionTokenHelper.generateRequiredActionsToken( + session, + opexRealm, + user, + listOf(UserModel.RequiredAction.VERIFY_EMAIL.name) + ) - val token = ActionTokenHelper.generateRequiredActionsToken( - session, - opexRealm, - user, - listOf(UserModel.RequiredAction.VERIFY_EMAIL.name) - ) - 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())) } + val url = "${session.context.getUri(UrlType.BACKEND).baseUri}/realms/opex/user-management/user/verify" + val link = ActionTokenHelper.attachTokenToLink(url, token) + val expiration = TimeUnit.SECONDS.toMinutes(opexRealm.actionTokenGeneratedByAdminLifespan.toLong()) + logger.info(link) + sendEmail(user) { it.sendVerifyEmail(link, expiration) } + } return Response.noContent().build() } @@ -306,8 +337,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour val auth = ResourceAuthenticator.bearerAuth(session) if (!auth.hasScopeAccess("trust")) return ErrorHandler.forbidden() - val userSession = - session.sessions().getUserSession(opexRealm, sessionId) ?: return ErrorHandler.notFound("Session not found") + val userSession = session.sessions().getUserSession(opexRealm, sessionId) + ?: return ErrorHandler.notFound("Session not found") if (userSession.user.id != auth.getUserId()) return ErrorHandler.forbidden() diff --git a/user-management/keycloak-gateway/src/main/resources/application.yml b/user-management/keycloak-gateway/src/main/resources/application.yml index e332f385c..c89d68fd4 100644 --- a/user-management/keycloak-gateway/src/main/resources/application.yml +++ b/user-management/keycloak-gateway/src/main/resources/application.yml @@ -72,3 +72,4 @@ keycloak: url: ${VAULT_URL} app: verify-redirect-url: ${VERIFY_REDIRECT_URL} + forgot-redirect-url: ${FORGOT_REDIRECT_URL} 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 96624729a..d5ec0a955 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 @@ -14,7 +14,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus ServiceUnavailable(1006, null, HttpStatus.SERVICE_UNAVAILABLE), InvalidRequestParam(1020, "Parameter '%s' is either missing or invalid", HttpStatus.BAD_REQUEST), InvalidRequestBody(1021, "Request body is invalid", HttpStatus.BAD_REQUEST), - NoRecordFound(1022,"No record found for this service",HttpStatus.NOT_FOUND), + NoRecordFound(1022, "No record found for this service", HttpStatus.NOT_FOUND), // code 2000: accountant InvalidPair(2001, "%s is not available", HttpStatus.BAD_REQUEST), @@ -71,6 +71,10 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus ; + fun exception(): OpexException { + return OpexException(this) + } + companion object { fun findByCode(code: Int?): OpexError? { code ?: return null