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..499f736b0 --- /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.BAD_REQUEST, 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.BAD_REQUEST, 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..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 @@ -223,6 +223,23 @@ class UserManagementResource(private val session: KeycloakSession) : RealmResour return Response.noContent().build() } + @POST + @Path("user/sessions/logout") + @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..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 @@ -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.BAD_REQUEST), // code 6000: wallet WalletOwnerNotFound(6001, null, HttpStatus.NOT_FOUND),