Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, AuthEvent>
}
Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
Expand All @@ -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()
}

Expand All @@ -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()
}
Expand Down Expand Up @@ -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()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ keycloak:
url: ${VAULT_URL}
app:
verify-redirect-url: ${VERIFY_REDIRECT_URL}
forgot-redirect-url: ${FORGOT_REDIRECT_URL}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down