From 76e549e93e5990b347b7f75b2b341883aab24da4 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 5 Apr 2022 17:00:17 +0430 Subject: [PATCH 1/5] Add logout service --- .../auth/gateway/data/UserSessionResponse.kt | 5 +- .../extension/UserManagementResource.kt | 53 ++++++++++++++++--- .../opex/auth/gateway/utils/ErrorHandler.kt | 2 + .../gateway/utils/ResourceAuthenticator.kt | 4 +- 4 files changed, 52 insertions(+), 12 deletions(-) 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 index 657ec1888..6b26a8a4b 100644 --- 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 @@ -1,8 +1,9 @@ package co.nilin.opex.auth.gateway.data data class UserSessionResponse( + val id: String, val ipAddress: String, - val started: Int, - val lastAccess: Int, + val started: Long, + val lastAccess: Long, 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 651c9f0a6..443afa0be 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 @@ -22,6 +22,7 @@ import org.keycloak.models.UserCredentialModel import org.keycloak.models.UserModel import org.keycloak.models.credential.OTPCredentialModel import org.keycloak.models.utils.HmacOTP +import org.keycloak.services.managers.AuthenticationManager import org.keycloak.services.resource.RealmResourceProvider import org.keycloak.services.resources.LoginActionsService import org.keycloak.utils.CredentialHelper @@ -125,13 +126,11 @@ 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, OpexError.Forbidden, "Incorrect password") + if (!session.userCredentialManager().isValid(opexRealm, user, cred)) + return ErrorHandler.forbidden("Incorrect password") - if (body.confirmation == body.newPassword) return ErrorHandler.response( - Response.Status.BAD_REQUEST, OpexError.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)) @@ -147,7 +146,8 @@ 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() - if (is2FAEnabled(user)) return ErrorHandler.response(Response.Status.BAD_REQUEST, 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) @@ -205,6 +205,35 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.ok(UserSecurityCheckResponse(is2FAEnabled(user))).build() } + @GET + @Path("user/logout") + @Produces(MediaType.APPLICATION_JSON) + fun logout(): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val userSession = session.sessions().getUserSession(opexRealm, auth.token?.sessionState!!) + AuthenticationManager.backchannelLogout(session, userSession, true) + return Response.noContent().build() + } + + @GET + @Path("user/logout/{sessionId}") + @Produces(MediaType.APPLICATION_JSON) + fun logout(sessionId: String): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val userSession = session.sessions().getUserSession(opexRealm, sessionId) + if (userSession.user.id != auth.getUserId()) + return ErrorHandler.forbidden() + + AuthenticationManager.backchannelLogout(session, userSession, true) + return Response.noContent().build() + } + @GET @Path("user/sessions") @Produces(MediaType.APPLICATION_JSON) @@ -214,7 +243,15 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour 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() + .map { + UserSessionResponse( + it.id, + it.ipAddress, + it.started.toLong(), + it.lastSessionRefresh.toLong(), + it.state.name + ) + }.toList() return Response.ok(sessions).build() } 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 d1fca53df..905faecce 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 @@ -19,6 +19,8 @@ object ErrorHandler { fun forbidden(message: String? = null) = response(Response.Status.FORBIDDEN, OpexError.Forbidden, message) + fun badRequest(message: String? = null) = response(Response.Status.BAD_REQUEST, OpexError.BadRequest, message) + 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 cd23cfdd2..c5d2a9ac7 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 @@ -8,8 +8,8 @@ import org.keycloak.services.managers.AuthenticationManager class ResourceAuthenticator(private val result: AuthenticationManager.AuthResult?) { - private val user: UserModel? = result?.user - private val token: AccessToken? = result?.token + val user: UserModel? = result?.user + val token: AccessToken? = result?.token fun getUserId() = user?.id From 3049ca3663bc8078a2110e1afb0eb4a9289245ce Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 6 Apr 2022 11:05:50 +0430 Subject: [PATCH 2/5] Fixed OTP and change password --- .../gateway/data/ChangePasswordRequest.kt | 20 ++++++++++++++----- .../auth/gateway/data/UserSessionResponse.kt | 1 + .../extension/UserManagementResource.kt | 20 +++++++++++++------ .../opex/auth/gateway/utils/ErrorHandler.kt | 2 ++ .../opex/utility/error/data/OpexError.kt | 1 + 5 files changed, 33 insertions(+), 11 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 ac993d034..df866b20f 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 @@ -1,7 +1,17 @@ package co.nilin.opex.auth.gateway.data -data class ChangePasswordRequest( - val password: String, - val newPassword: String, - val confirmation: String, -) \ No newline at end of file +class ChangePasswordRequest{ + + var password: String?=null + var newPassword: String?=null + var confirmation: String?=null + + constructor() + + constructor(password: String?, newPassword: String?, confirmation: String?) { + this.password = password + this.newPassword = newPassword + this.confirmation = confirmation + } + +} \ 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 index 6b26a8a4b..838513feb 100644 --- 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 @@ -6,4 +6,5 @@ data class UserSessionResponse( val started: Long, val lastAccess: Long, val state: String, + val inUse: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 443afa0be..02b70a1a2 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 @@ -21,6 +21,7 @@ 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.CredentialValidation import org.keycloak.models.utils.HmacOTP import org.keycloak.services.managers.AuthenticationManager import org.keycloak.services.resource.RealmResourceProvider @@ -164,9 +165,13 @@ 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() - if (is2FAEnabled(user)) return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.OTPAlreadyEnabled) + if (is2FAEnabled(user)) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.OTPAlreadyEnabled) val otpCredential = OTPCredentialModel.createFromPolicy(opexRealm, body.secret) + if (!CredentialValidation.validOTP(body.initialCode, otpCredential, opexRealm.otpPolicy.lookAheadWindow)) + return ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.InvalidOTP) + CredentialHelper.createOTPCredential(session, opexRealm, user, body.initialCode, otpCredential) return Response.noContent().build() } @@ -205,7 +210,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.ok(UserSecurityCheckResponse(is2FAEnabled(user))).build() } - @GET + @POST @Path("user/logout") @Produces(MediaType.APPLICATION_JSON) fun logout(): Response { @@ -218,15 +223,17 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.noContent().build() } - @GET - @Path("user/logout/{sessionId}") + @POST + @Path("user/sessions/{sessionId}/logout") @Produces(MediaType.APPLICATION_JSON) - fun logout(sessionId: String): Response { + fun logout(@PathParam("sessionId") sessionId: String): Response { 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") + if (userSession.user.id != auth.getUserId()) return ErrorHandler.forbidden() @@ -249,7 +256,8 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour it.ipAddress, it.started.toLong(), it.lastSessionRefresh.toLong(), - it.state.name + it.state.name, + auth.token?.sessionState == it.id ) }.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 905faecce..57fc85532 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 @@ -21,6 +21,8 @@ object ErrorHandler { fun badRequest(message: String? = null) = response(Response.Status.BAD_REQUEST, OpexError.BadRequest, message) + fun notFound(message: String? = null) = response(Response.Status.NOT_FOUND, OpexError.NotFound, message) + fun userNotFound(message: String? = null) = response(Response.Status.NOT_FOUND, OpexError.UserNotFound, message) } \ 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 8da868e14..ad80b0bac 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 @@ -28,6 +28,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus 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), + InvalidOTP(5005, "Invalid OTP", HttpStatus.BAD_REQUEST), // code 6000: wallet WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND), From d74a5e86b074ee62dd3a5a1760675c7577f5e5b0 Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 13 Apr 2022 13:00:42 +0430 Subject: [PATCH 3/5] Add logout all and session data --- .../authenticator/CustomOTPAuthenticator.kt | 109 ++++++++++++++++++ .../authenticator/UserNotesAuthenticator.kt | 86 ++++++++++++++ .../auth/gateway/data/UserSessionResponse.kt | 3 +- .../extension/UserManagementResource.kt | 18 +++ .../opex/auth/gateway/utils/ErrorHandler.kt | 10 +- ...ycloak.authentication.AuthenticatorFactory | 2 + .../opex/utility/error/data/OpexError.kt | 3 +- 7 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt create mode 100644 user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/UserNotesAuthenticator.kt create mode 100644 user-management/keycloak-gateway/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt new file mode 100644 index 000000000..3f0cd2aea --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt @@ -0,0 +1,109 @@ +package co.nilin.opex.auth.gateway.authenticator + +import co.nilin.opex.auth.gateway.utils.ErrorHandler +import co.nilin.opex.utility.error.data.OpexError +import org.keycloak.authentication.AuthenticationFlowContext +import org.keycloak.authentication.AuthenticationFlowError +import org.keycloak.authentication.CredentialValidator +import org.keycloak.authentication.authenticators.directgrant.AbstractDirectGrantAuthenticator +import org.keycloak.credential.CredentialProvider +import org.keycloak.credential.OTPCredentialProvider +import org.keycloak.events.Errors +import org.keycloak.models.* +import org.keycloak.models.credential.OTPCredentialModel +import org.keycloak.provider.ProviderConfigProperty +import javax.ws.rs.core.Response + +class CustomOTPAuthenticator : AbstractDirectGrantAuthenticator(), CredentialValidator { + + override fun authenticate(context: AuthenticationFlowContext) { + val session = context.session + val realm = context.realm + val user = context.user + + if (!configuredFor(session, context.realm, user)) { + if (context.execution.isConditional) { + context.attempted() + } else if (context.execution.isRequired) { + context.event.error(Errors.INVALID_USER_CREDENTIALS) + val challengeResponse = ErrorHandler.response(Response.Status.FORBIDDEN, OpexError.OTPRequired) + context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse) + } + return + } + + val inputData = context.httpRequest.decodedFormParameters + val otp = inputData.getFirst("otp") ?: inputData.getFirst("totp") + + + if (otp == null) { + user?.let { context.event.user(it) } + val response = ErrorHandler.response(Response.Status.FORBIDDEN, OpexError.OTPRequired) + context.failure(AuthenticationFlowError.INVALID_USER, response) + return + } + + val credentialId = getCredentialProvider(session).getDefaultCredential(session, realm, user).id + val isValid = getCredentialProvider(session) + .isValid(realm, user, UserCredentialModel(credentialId, OTPCredentialModel.TYPE, otp)) + + if (!isValid) { + context.event.user(user) + context.event.error(Errors.INVALID_USER_CREDENTIALS) + val response = ErrorHandler.response(Response.Status.FORBIDDEN, OpexError.InvalidOTP) + context.failure(AuthenticationFlowError.INVALID_USER, response) + return + } + + context.success() + } + + override fun requiresUser(): Boolean { + return true + } + + override fun configuredFor(session: KeycloakSession, realm: RealmModel?, user: UserModel?): Boolean { + return getCredentialProvider(session).isConfiguredFor(realm, user) + } + + override fun setRequiredActions(session: KeycloakSession?, realm: RealmModel?, user: UserModel?) { + + } + + override fun getId(): String { + return "direct-grant-validate-otp-custom" + } + + override fun getHelpText(): String { + return "Custom OTP validator" + } + + override fun getDisplayType(): String { + return "Custom OTP" + } + + override fun getReferenceCategory(): String? { + return null + } + + override fun getConfigProperties(): MutableList { + return mutableListOf() + } + + override fun isConfigurable(): Boolean { + return false + } + + override fun getRequirementChoices(): Array { + return REQUIREMENT_CHOICES + } + + override fun isUserSetupAllowed(): Boolean { + return false + } + + override fun getCredentialProvider(session: KeycloakSession): OTPCredentialProvider { + return session.getProvider(CredentialProvider::class.java, "keycloak-otp") as OTPCredentialProvider + } + +} \ No newline at end of file diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/UserNotesAuthenticator.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/UserNotesAuthenticator.kt new file mode 100644 index 000000000..feef5b18d --- /dev/null +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/UserNotesAuthenticator.kt @@ -0,0 +1,86 @@ +package co.nilin.opex.auth.gateway.authenticator + +import co.nilin.opex.auth.gateway.utils.ErrorHandler +import co.nilin.opex.utility.error.data.OpexError +import org.keycloak.authentication.AuthenticationFlowContext +import org.keycloak.authentication.AuthenticationFlowError +import org.keycloak.authentication.authenticators.directgrant.AbstractDirectGrantAuthenticator +import org.keycloak.models.AuthenticationExecutionModel +import org.keycloak.models.KeycloakSession +import org.keycloak.models.RealmModel +import org.keycloak.models.UserModel +import org.keycloak.provider.ProviderConfigProperty +import javax.ws.rs.core.Response + +class UserNotesAuthenticator : AbstractDirectGrantAuthenticator() { + + override fun authenticate(context: AuthenticationFlowContext) { + if (context.execution.isDisabled) { + context.attempted() + return + } + + //TODO add configurable parameters with validation + val inputData = context.httpRequest.decodedFormParameters + val agent = inputData.getFirst("agent") + if (agent.isNullOrEmpty()) { + val response = ErrorHandler.response( + Response.Status.BAD_REQUEST, + OpexError.BadRequest, + "Parameter 'agent' required but not found" + ) + context.failure(AuthenticationFlowError.INTERNAL_ERROR, response) + return + } + + context.authenticationSession.setUserSessionNote("agent", agent) + context.success() + } + + override fun requiresUser(): Boolean { + return false + } + + override fun configuredFor(session: KeycloakSession, realm: RealmModel?, user: UserModel?): Boolean { + return true + } + + override fun setRequiredActions(session: KeycloakSession?, realm: RealmModel?, user: UserModel?) { + + } + + override fun getId(): String { + return "user-notes-validate" + } + + override fun getHelpText(): String { + return "User session notes validator" + } + + override fun getDisplayType(): String { + return "User session note validator" + } + + override fun getReferenceCategory(): String? { + return null + } + + override fun getConfigProperties(): MutableList { + return mutableListOf() + } + + override fun isConfigurable(): Boolean { + return false + } + + override fun getRequirementChoices(): Array { + return arrayOf( + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.DISABLED + ) + } + + override fun isUserSetupAllowed(): Boolean { + return false + } +} \ 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 index 838513feb..7153d8319 100644 --- 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 @@ -6,5 +6,6 @@ data class UserSessionResponse( val started: Long, val lastAccess: Long, val state: String, - val inUse:Boolean + val agent: String?, + val inUse: 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 02b70a1a2..a56a484be 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 @@ -223,6 +223,23 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.noContent().build() } + @POST + @Path("user/logout/all") + @Produces(MediaType.APPLICATION_JSON) + fun logoutAll(): Response { + val auth = ResourceAuthenticator.bearerAuth(session) + if (!auth.hasScopeAccess("trust")) + return ErrorHandler.forbidden() + + val currentSession = auth.token?.sessionState!! + session.sessions().getUserSessionsStream(opexRealm, auth.user) + .toList() + .filter { it.id != currentSession } + .forEach { AuthenticationManager.backchannelLogout(session, it, true) } + + return Response.noContent().build() + } + @POST @Path("user/sessions/{sessionId}/logout") @Produces(MediaType.APPLICATION_JSON) @@ -257,6 +274,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour it.started.toLong(), it.lastSessionRefresh.toLong(), it.state.name, + it.notes["agent"], auth.token?.sessionState == it.id ) }.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 57fc85532..fcec44010 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 @@ -3,18 +3,18 @@ package co.nilin.opex.auth.gateway.utils import co.nilin.opex.utility.error.DefaultErrorTranslator import co.nilin.opex.utility.error.data.OpexError import co.nilin.opex.utility.error.data.OpexException +import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response object ErrorHandler { private val translator = DefaultErrorTranslator() - fun response(status: Response.Status, ex: OpexException): Response { - return Response.status(status).entity(translator.translate(ex)).build() - } - fun response(status: Response.Status, error: OpexError, message: String? = null): Response { - return Response.status(status).entity(translator.translate(OpexException(error, message))).build() + return Response.status(status) + .entity(translator.translate(OpexException(error, message))) + .type(MediaType.APPLICATION_JSON_TYPE) + .build() } fun forbidden(message: String? = null) = response(Response.Status.FORBIDDEN, OpexError.Forbidden, message) diff --git a/user-management/keycloak-gateway/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/user-management/keycloak-gateway/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory new file mode 100644 index 000000000..9c74254b9 --- /dev/null +++ b/user-management/keycloak-gateway/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -0,0 +1,2 @@ +co.nilin.opex.auth.gateway.authenticator.CustomOTPAuthenticator +co.nilin.opex.auth.gateway.authenticator.UserNotesAuthenticator \ 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 ad80b0bac..c580b889c 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 @@ -28,7 +28,8 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus 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), - InvalidOTP(5005, "Invalid OTP", HttpStatus.BAD_REQUEST), + InvalidOTP(5005, "Invalid OTP", HttpStatus.FORBIDDEN), + OTPRequired(5006, "OTP Required", HttpStatus.FORBIDDEN), // code 6000: wallet WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND), From e7952fd517ee2bc8a064577a1de72c7f8998e67d Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 13 Apr 2022 13:15:42 +0430 Subject: [PATCH 4/5] Changed logout all path --- .../nilin/opex/auth/gateway/extension/UserManagementResource.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a56a484be..293b1ba33 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 @@ -224,7 +224,7 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour } @POST - @Path("user/logout/all") + @Path("user/sessions/logout") @Produces(MediaType.APPLICATION_JSON) fun logoutAll(): Response { val auth = ResourceAuthenticator.bearerAuth(session) From 165c2b62eeb5f156d5c4e09601bbcc0ce0128058 Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 13 Apr 2022 13:36:13 +0430 Subject: [PATCH 5/5] Changed OTP required error status --- .../opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt | 4 ++-- .../main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt index 3f0cd2aea..499f736b0 100644 --- a/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt +++ b/user-management/keycloak-gateway/src/main/kotlin/co/nilin/opex/auth/gateway/authenticator/CustomOTPAuthenticator.kt @@ -26,7 +26,7 @@ class CustomOTPAuthenticator : AbstractDirectGrantAuthenticator(), CredentialVal context.attempted() } else if (context.execution.isRequired) { context.event.error(Errors.INVALID_USER_CREDENTIALS) - val challengeResponse = ErrorHandler.response(Response.Status.FORBIDDEN, OpexError.OTPRequired) + val challengeResponse = ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.OTPRequired) context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse) } return @@ -38,7 +38,7 @@ class CustomOTPAuthenticator : AbstractDirectGrantAuthenticator(), CredentialVal if (otp == null) { user?.let { context.event.user(it) } - val response = ErrorHandler.response(Response.Status.FORBIDDEN, OpexError.OTPRequired) + val response = ErrorHandler.response(Response.Status.BAD_REQUEST, OpexError.OTPRequired) context.failure(AuthenticationFlowError.INVALID_USER, response) return } 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 c580b889c..fbe4543f6 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 @@ -29,7 +29,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus OTPAlreadyEnabled(5003, "2FA/OTP already configured", HttpStatus.BAD_REQUEST), UserNotFound(5004, "User not found", HttpStatus.NOT_FOUND), InvalidOTP(5005, "Invalid OTP", HttpStatus.FORBIDDEN), - OTPRequired(5006, "OTP Required", HttpStatus.FORBIDDEN), + OTPRequired(5006, "OTP Required", HttpStatus.BAD_REQUEST), // code 6000: wallet WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND),