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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ PANEL_PASS=admin
BACKEND_USER=admin
KEYCLOAK_ADMIN_USERNAME=opex
KEYCLOAK_ADMIN_PASSWORD=hiopex
SMTP_PASS=x
SMTP_PASSWORD=x
API_KEY_CLIENT_SECRET=x
KEYCLOAK_FRONTEND_URL=http://localhost:8083/auth
KEYCLOAK_ADMIN_URL=http://localhost:8083/auth
Expand All @@ -76,7 +76,7 @@ TAG=debug
| BACKEND_USER | Username used by services to access vault data. Also used as the username for Vault admin panel |
| KEYCLOAK_ADMIN_USERNAME | Username for Keycloak admin panel |
| KEYCLOAK_ADMIN_PASSWORD | Password for Keycloak admin panel |
| SMTP_PASS | SMTP password used by keycloak to send emails for various operations (e.g. user verification, reset password) |
| SMTP_PASSWORD | SMTP password used by keycloak to send emails for various operations (e.g. user verification, reset password) |
| API_KEY_CLIENT_SECRET | In order to access the api key feature, please follow the steps below:</br>1. Go to Keycloak admin panel located at http://localhost:8083/auth/admin/master/console/#/realms/opex/clients <br/>2. Login with the username and password you provided in the `.env` file (KEYCLOAK_ADMIN_USERNAME and KEYCLOAK_ADMIN_PASSWORD)<br/>3. Go to `clients` section on the left menu </br>4. Click on `opex-api-key` client </br>5. In the credentials section, click on `Regenerate Secret` button </br>6. Copy the generated secret and paste it into this section |
| KEYCLOAK_FRONTEND_URL<br/>KEYCLOAK_ADMIN_URL<br/>KEYCLOAK_VERIFY_REDIRECT_URL<br/>KEYCLOAK_FORGOT_REDIRECT_URL | Replace `localhost` with your server's IP if you're not running on local machine. Do not change the rest. |
| WHITELIST_REGISTER_ENABLED | Allows registration only for whitelisted emails |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package co.nilin.opex.auth.controller

import co.nilin.opex.auth.data.ActiveSession
import co.nilin.opex.auth.service.UserService
import co.nilin.opex.auth.utils.jwtAuthentication
import co.nilin.opex.common.OpexError
import org.springframework.security.core.annotation.CurrentSecurityContext
import org.springframework.security.core.context.SecurityContext
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/v1/user")
Expand All @@ -17,5 +17,33 @@ class UserController(private val userService: UserService) {
userService.logout(securityContext.jwtAuthentication().name)
}

@GetMapping("/session")
suspend fun getSessions(@CurrentSecurityContext securityContext: SecurityContext): List<ActiveSession> {
val uuid = securityContext.authentication.name
val sid = securityContext.jwtAuthentication().tokenAttributes["sid"] as String?
?: throw OpexError.InvalidToken.exception()
return userService.fetchActiveSessions(uuid, sid)
}

@DeleteMapping("/session/{sessionId}")
suspend fun logout(@CurrentSecurityContext securityContext: SecurityContext, @PathVariable sessionId: String) {
val uuid = securityContext.authentication.name
userService.logoutSession(uuid, sessionId)
}

@PostMapping("/session/delete-others")
suspend fun logoutOthers(@CurrentSecurityContext securityContext: SecurityContext) {
val uuid = securityContext.authentication.name
val sid = securityContext.jwtAuthentication().tokenAttributes["sid"] as String?
?: throw OpexError.InvalidToken.exception()
userService.logoutOthers(uuid, sid)
}

@PostMapping("/session/delete-all")
suspend fun logoutAll(@CurrentSecurityContext securityContext: SecurityContext) {
val uuid = securityContext.authentication.name
userService.logoutAll(uuid)
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package co.nilin.opex.auth.data

data class ActiveSession(
val id: String,
val username: String,
val userId: String,
val ip: String,
val loginDate: Long,
val lastAccessDate: Long,
val client: String?,
val current: Boolean = false
)
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package co.nilin.opex.auth.proxy

import co.nilin.opex.auth.config.KeycloakConfig
import co.nilin.opex.auth.data.ActiveSession
import co.nilin.opex.auth.model.*
import co.nilin.opex.common.OpexError
import co.nilin.opex.common.utils.LoggerDelegate
import kotlinx.coroutines.reactive.awaitFirstOrElse
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import org.keycloak.admin.client.resource.RealmResource
import org.keycloak.admin.client.resource.UserResource
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
Expand Down Expand Up @@ -138,6 +140,10 @@ class KeycloakProxy(
.awaitFirstOrElse { emptyList() }
}

private suspend fun findUser(uuid: String): UserResource? {
return opexRealm.users().get(uuid)
}

suspend fun createUser(
username: Username,
firstName: String?,
Expand Down Expand Up @@ -295,12 +301,59 @@ class KeycloakProxy(
.awaitSingleOrNull()
}

suspend fun WebClient.RequestHeadersSpec<*>.withAdminToken(token: String? = null): WebClient.RequestHeadersSpec<*> {
suspend fun fetchActiveSessions(uuid: String, currentSessionId: String): List<ActiveSession> {
val user = findUser(uuid) ?: throw OpexError.BadRequest.exception()
val sessions = user.userSessions
return sessions.map {
ActiveSession(
it.id,
it.username,
it.userId,
it.ipAddress,
it.start,
it.lastAccess,
it.clients.values.firstOrNull(),
it.id == currentSessionId
)
}
}

suspend fun logoutSession(uuid: String, sessionId: String) {
val user = findUser(uuid) ?: throw OpexError.BadRequest.exception()
user.userSessions.find { it.id == sessionId } ?: OpexError.BadRequest.exception()
callLogout(sessionId)
}

suspend fun logoutOthers(uuid: String, currentSessionId: String) {
val user = findUser(uuid) ?: throw OpexError.BadRequest.exception()
user.userSessions.forEach {
if (currentSessionId != it.id)
callLogout(it.id)
}
}

suspend fun logoutAll(uuid: String) {
val user = findUser(uuid) ?: throw OpexError.BadRequest.exception()
//user.userSessions.forEach { opexRealm.deleteSession(it.id, true) }
user.logout()
}

private suspend fun callLogout(sessionId: String) {
val url = "${keycloakConfig.url}/admin/realms/${keycloakConfig.realm}/sessions/$sessionId"
keycloakClient.delete()
.uri(url)
.withAdminToken()
.retrieve()
.toBodilessEntity()
.awaitSingleOrNull()
}

private suspend fun WebClient.RequestHeadersSpec<*>.withAdminToken(token: String? = null): WebClient.RequestHeadersSpec<*> {
header(HttpHeaders.AUTHORIZATION, "Bearer ${token ?: getAdminAccessToken()}")
return this
}

suspend fun WebClient.RequestBodySpec.withAdminToken(token: String? = null): WebClient.RequestBodySpec {
private suspend fun WebClient.RequestBodySpec.withAdminToken(token: String? = null): WebClient.RequestBodySpec {
header(HttpHeaders.AUTHORIZATION, "Bearer ${token ?: getAdminAccessToken()}")
return this
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package co.nilin.opex.auth.service

import co.nilin.opex.auth.data.ActiveSession
import co.nilin.opex.auth.data.UserCreatedEvent
import co.nilin.opex.auth.kafka.AuthEventProducer
import co.nilin.opex.auth.model.*
Expand Down Expand Up @@ -139,6 +140,22 @@ class UserService(
keycloakProxy.resetPassword(user.id, request.newPassword)
}

suspend fun fetchActiveSessions(uuid: String, currentSessionId: String): List<ActiveSession> {
return keycloakProxy.fetchActiveSessions(uuid, currentSessionId)
}

suspend fun logoutSession(uuid: String, sessionId: String) {
keycloakProxy.logoutSession(uuid, sessionId)
}

suspend fun logoutOthers(uuid: String, currentSessionId: String) {
keycloakProxy.logoutOthers(uuid, currentSessionId)
}

suspend fun logoutAll(uuid: String) {
keycloakProxy.logoutAll(uuid)
}

private suspend fun isUserDuplicate(username: Username): Boolean {
val user = keycloakProxy.findUserByUsername(username)
return if (user == null)
Expand Down
1 change: 1 addition & 0 deletions common/src/main/kotlin/co/nilin/opex/common/OpexError.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus
InvalidUserCredentials(5015, "Invalid user credentials", HttpStatus.BAD_REQUEST),
InvalidRegisterToken(5016, "Invalid register token", HttpStatus.BAD_REQUEST),
ExpiredOTP(5017, "OTP is expired", HttpStatus.BAD_REQUEST),
InvalidToken(5018, "Invalid token", HttpStatus.BAD_REQUEST),


// code 6000: wallet
Expand Down
Loading