Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
package com.example.autobank.controller


import com.example.autobank.data.authentication.AuthenticatedUserResponse
import com.example.autobank.service.AuthenticationService
import com.example.autobank.service.OnlineUserService
import jakarta.servlet.http.HttpServletRequest
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.http.ResponseCookie
import org.springframework.web.bind.annotation.*
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag

import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/auth")
@Tag(name = "Authentication Controller", description = "Endpoints for user authentication")
class AuthenticationController {

private val logger = LoggerFactory.getLogger(AuthenticationController::class.java)

@Autowired
lateinit var onlineUserService: OnlineUserService;
lateinit var onlineUserService: OnlineUserService

@Operation(summary = "Check authenticated user", description = "Returns information about the currently authenticated user")
@GetMapping("/getuser")
fun checkUser(): ResponseEntity<AuthenticatedUserResponse> {
return try {
ResponseEntity.ok().body(onlineUserService.checkUser())
} catch (e: Exception) {
print(e)
ResponseEntity.badRequest().build();
}
return try {
logger.info("=== /api/auth/getuser called ===")
val result = onlineUserService.checkUser()
logger.info("checkUser result: success=${result.success}, isadmin=${result.isadmin}")
ResponseEntity.ok().body(result)
} catch (e: Exception) {
logger.error("=== Exception in /api/auth/getuser ===", e)
logger.error("Exception type: ${e.javaClass.name}")
logger.error("Exception message: ${e.message}")
logger.error("Stack trace:", e)
ResponseEntity.badRequest().build()
}

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.autobank.data.authentication

import com.nimbusds.openid.connect.sdk.claims.Gender
import org.jetbrains.annotations.NotNull
import io.swagger.v3.oas.annotations.media.Schema

Expand Down
15 changes: 12 additions & 3 deletions src/main/kotlin/com/example/autobank/security/AudienceValidator.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
package com.example.autobank.security


import org.springframework.security.oauth2.core.OAuth2Error
import org.springframework.security.oauth2.core.OAuth2TokenValidator
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult
import org.springframework.security.oauth2.jwt.Jwt
import org.slf4j.LoggerFactory

class AudienceValidator(private val audience: String) : OAuth2TokenValidator<Jwt> {

private val logger = LoggerFactory.getLogger(AudienceValidator::class.java)

override fun validate(jwt: Jwt): OAuth2TokenValidatorResult {
logger.info("=== Validating JWT Audience ===")
logger.info("Expected audience: $audience")
logger.info("Token audiences: ${jwt.audience}")

val error = OAuth2Error("invalid_token", "The required audience is missing", null)
return if (jwt.audience.contains(audience)) {
logger.info("Audience validation: SUCCESS")
OAuth2TokenValidatorResult.success()
} else OAuth2TokenValidatorResult.failure(error)
} else {
logger.error("Audience validation: FAILED - Required audience '$audience' not found in ${jwt.audience}")
OAuth2TokenValidatorResult.failure(error)
}
}

}
146 changes: 82 additions & 64 deletions src/main/kotlin/com/example/autobank/service/AuthenticationService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.example.autobank.service

import com.example.autobank.data.authentication.Auth0User
import com.example.autobank.repository.user.OnlineUserRepository
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.core.ParameterizedTypeReference
Expand All @@ -14,6 +15,10 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import org.springframework.web.client.exchange
import org.springframework.web.util.UriComponentsBuilder
import java.net.URI
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.time.Duration
import java.time.Instant
import java.time.LocalDateTime
Expand All @@ -24,23 +29,16 @@ class AuthenticationService(
@Value("\${admincommittee}") private val adminCommittee: String,
@Value("\${auth0.domain}") private val domain: String,
@Value("\${environment}") private val environment: String,
@Value("\${api.base.domain}") private val apiBaseDomain: String,
) {

private val restTemplate = RestTemplate()

private val fetchProfileUrl = "https://old.online.ntnu.no/api/v1/profile/"

private val fetchUserCommitteesUrl = "https://old.online.ntnu.no/api/v1/group/online-groups/?members__user="

private val adminRecheckTime = 24 * 60 * 60 * 1000;

@Autowired
lateinit var onlineUserRepository: OnlineUserRepository

fun getAuth0User(token: String): Auth0User {
return Auth0User("sub", "email", "name")
}

fun getSecondsUntilExpiration(): Long {
val expiresAt = getExpiresAt()
return if (expiresAt != null) {
Expand Down Expand Up @@ -75,53 +73,57 @@ class AuthenticationService(
}

fun getUserDetails(): Auth0User {
val headers = HttpHeaders().apply {
set("Authorization", "Bearer ${getAccessToken()}")
}
val entity = HttpEntity<Void>(headers)
val response: ResponseEntity<Map<String, Any>> = restTemplate.exchange(
"${domain}/userinfo",
HttpMethod.GET,
entity,
)
val endpoint = UriComponentsBuilder
.fromHttpUrl("${apiBaseDomain}user.getMe")
.encode()
.toUriString()

return Auth0User(
sub = response.body?.get("sub").toString(),
email = response.body?.get("email").toString(),
name = response.body?.get("name").toString(),
)
}

private fun fetchOnlineuserId(): Int {
val headers = HttpHeaders().apply {
set("Authorization", "Bearer ${getAccessToken()}")
}
val entity = HttpEntity<Void>(headers)
val response: ResponseEntity<Map<String, Any>> = restTemplate.exchange(
fetchProfileUrl,

val response: ResponseEntity<UserResponse> = restTemplate.exchange(
endpoint,
HttpMethod.GET,
entity,
object : ParameterizedTypeReference<UserResponse>() {}
)

if (response.statusCode.isError || response.body == null) {
throw Exception("Error fetching user id")
throw Exception("Error fetching user details")
}

return response.body?.get("id").toString().toInt()
val user = response.body?.result?.data?.json
?: throw Exception("User not found")

return Auth0User(user.id, user.email, user.name)
}

fun fetchUserCommittees(): List<String> {

if (environment != "prod") {
return listOf("Applikasjonskomiteen", "Trivselskomiteen")
return listOf("Applikasjonskomiteen")
}

val userId = fetchOnlineuserId()
val userId = getUserDetails().sub

val headers = HttpHeaders()
// tRPC format: {"json":"<value>"}
// The value itself is the user ID string
val input = """{"json":"$userId"}"""
val encodedInput = URLEncoder.encode(input, StandardCharsets.UTF_8.toString())
val urlString = "${apiBaseDomain}group.allByMember?input=$encodedInput"

println("Final URL: $urlString") // Debug log

val uri = URI(urlString)

val headers = HttpHeaders().apply {
set("Authorization", "Bearer ${getAccessToken()}")
}
val entity = HttpEntity<Void>(headers)

val response: ResponseEntity<UserCommitteeResponse> = restTemplate.exchange(
fetchUserCommitteesUrl + userId,
uri,
HttpMethod.GET,
entity,
object : ParameterizedTypeReference<UserCommitteeResponse>() {}
Expand All @@ -131,41 +133,30 @@ class AuthenticationService(
throw Exception("Error fetching user committees")
}

return response.body?.results?.map { it.name_long } ?: listOf()
return response.body?.result?.data?.json?.map { it.slug } ?: emptyList()
}

fun checkAdmin(): Boolean {

val user = onlineUserRepository.findByOnlineId(getUserSub()) ?: throw Exception("User not found");
val currentTime = LocalDateTime.now()
if (Duration.between(user.lastUpdated, currentTime).toMillis() > adminRecheckTime) {
user.lastUpdated = currentTime

val isAdmin = checkBankomMembership()
user.isAdmin = isAdmin
onlineUserRepository.save(user)
// Time check for users last update isAdmin
if (Duration.between(user.lastUpdated, LocalDateTime.now()).toMillis() > adminRecheckTime) {
user.lastUpdated = LocalDateTime.now()

return isAdmin;
} else {
return user.isAdmin;
}
}
// Check if the user is admin and set bool accordingly
user.isAdmin = fetchUserCommittees().contains(adminCommittee)

private fun checkBankomMembership(): Boolean {
if (environment == "dev") {
return true
// I dont know what this is
onlineUserRepository.save(user)
}

val userCommittees = fetchUserCommittees()
return userCommittees.contains(adminCommittee)
}
if (user.isAdmin) {
return true;
}

/*
fun checkSuperAdmin(): Boolean {
return superadminEmails.split(",").contains(getUserDetails().email)
return false;
}
*/


fun getExpiresAt(): Instant? {
val authentication = SecurityContextHolder.getContext().authentication
Expand All @@ -177,14 +168,41 @@ class AuthenticationService(
}
}

data class Result(
val name_long: String = ""
)
data class UserCommitteeResponse(
val result: Result
)

data class Result(
val data: Data
)

data class UserCommitteeResponse(
val results: List<Result> = listOf()
)
data class Data(
val json: List<Committee>
)

}
data class Committee(
val slug: String,
val abbreviation: String,
val name: String,
val type: String,
val memberVisibility: String,
// Add other fields if needed
)
data class UserResponse(
val result: UserResult
)

data class UserResult(
val data: UserData
)

data class UserData(
val json: User // Single User object, not List<User>
)

data class User(
val id: String,
val email: String,
val name: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,6 @@ class AuthenticationServiceTest : FunSpec({
this.onlineUserRepository = onlineUserRepository
}

context("getAuth0User") {
test("should return Auth0User with provided token") {
// Given
val token = "test-token"

// When
val result = authenticationService.getAuth0User(token)

// Then
result shouldNotBe null
result.sub shouldBe "sub"
result.email shouldBe "email"
result.name shouldBe "name"
}
}

context("getUserSub") {
test("should return empty string when no authentication context") {
// When
Expand Down