Przekazanie dokładnych informajci o błędzie autentykacji w odpowiedzi HTTP

0

Mam klasę, która odpowiada za wyciągnięcie i walidacje JWT

internal class JwtTokenFilter(private val tokenUtils: JwtTokenUtils) : OncePerRequestFilter() {
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        request.getHeader("Authorization").let {
            val token = it?.replace("Bearer ", "")

            if (tokenUtils.validateToken(token)) {
                val id = tokenUtils.getId(token!!)
                SecurityContextHolder.getContext().authentication =
                    UsernamePasswordAuthenticationToken(id, null, Collections.emptyList())
            }
        }
        filterChain.doFilter(request, response)
    }
}

Która jest wpięta w SpringSecurity

 http.addFilterBefore(
            jwtTokenFilter, UsernamePasswordAuthenticationFilter::class.java
        )

Gdzie sama walidacja może się nie udać z N powodów

    fun validateToken(authToken: String?, tokenType: TokenType = TokenType.ACCESS_TOKEN): Boolean {
        return try {
            val claims = Jwts.parser().setSigningKey(tokenSecret).parseClaimsJws(authToken)
            return claims.body["type"] == tokenType.value
        } catch (ex: SignatureException) {
            false
        } catch (ex: MalformedJwtException) {
            false
        } catch (ex: ExpiredJwtException) {
            false
        } catch (ex: UnsupportedJwtException) {
            false
        } catch (ex: IllegalArgumentException) {
            false
        }
    }

Efektem jest to że w Security context ustawiam bądź nie ustawiam obiekt autentykacji. Gdy tego zabraknie, to wpadam w tę metodę, gdzie tworzę odpowiedź HTTP.

    private fun configureExceptionHandling(http: HttpSecurity) {
        http.exceptionHandling().authenticationEntryPoint { _, response, _ ->
            response.status = HttpServletResponse.SC_UNAUTHORIZED
            response.contentType = "application/json"
            response.writer.write(objectMapper.writeValueAsString(ExceptionResponse(UnauthorizedException())))
        }
    }

Problem w tym, że nie widzę sposobu by przekazać co dokładnie się stało, czyli odpowiedzieć użytkownikowi, że albo wygasł mu token, albo podał zupełnie zły token. Metoda powyżej dostaje wyjątek z bebechów Springa, który mówi w skrócie "Full authorization is required" i tyle.
W kodzie powyżej oczywiście nie ma to jak zadziałać, aczkolwiek próbowałem

  • w JwtTokenFilter ustawić zmienną w ThreadLocal i odczytać w configureExceptionHandling - nie działa, zmienna jest nullem
  • nie łapać żadnego wyjątku tylko go rzucić dalej - nie działa, łapie go SpringSecurity i dostaję ten sam wyjątek co w przypadku braku autentykacji

Jak więc mogę przekazać szczegóły błędu do użytkownika?

1

Hmmm skoro i tak ręcznie walidujesz JWT, to może niepotrzebne Ci te Spring Security? Skoro i tak tylko przeszkadza ;p

0

@Pinek:
Spring ma mechanizm do walidacji JWT? :O Jak inaczej mogę walidować JWT?

0

Mhm ale to użytkownik podaje jakiś token explicite? Po co chcesz to robić?

Btw. autentykacja -> uwierzytelnienie

0

@Charles_Ray:

Mam aplikację z backendem i frontendem.
Backend generuje JWT na podstawie danych z requesta (username, password) i zwraca go w odpowiedzi HTTP
Po stronie frontu dorzucam wspomniany token do kolejnych requestów
No i tutaj jest moja walidacja, która polega na sprawdzeniu czy ten JWT jest poprawny i czy mogę użytkownika wpuścić po zasób /api/protected-resource/123.
Na tym etapie nie potrzebuje żadnego sprawdzenia ról.

Problem w tym, że chce mieć możliwość zwrócenia odpowiedzi Token expired albo Token malformed, w zależności od tego co jest z tym tokenem nie tak, a przy obecnej implementacji nie ma jak przekazać powodu odrzucenia tokenu do wspomnianego AuthenticationEntryPoint, w którym mogę zwrócić co najwyżej { message: Unauthorized }

1

@Gazel:

Spring ma mechanizm do walidacji JWT? :O Jak inaczej mogę walidować JWT?

Tak Spring wspiera JWT, ale jak juz umiesz robić to po swojemu to odkryjesz, że w Spring Security jest mocno powalone, szczególnie z punktu widzenia np. aplikacji SPA.
Przeszkadza - mocno.
Jak robisz coś dla siebie to nie wiem czy warto w to brnąć. Depends.

0

@jarekr000000:
Czyli radzisz by zostać przy swoim rozwiązaniu? Trochę nie rozumiem zdzwienia @Pinek: na ten temat, że robię to ręcznie. Każdy jeden tutorial który znajdę robi to w ten sposób, zazwyczaj dorzucając jeszcze 10 klas Springowych w sumie nie wiadomo po co.

No a co z moim problemem? Jest jakiś sposób by przekazać do tego authenticationEntryPoint szczegóły błędu? Jak trzeba mogę wrzucić więcej kodu

1

@Gazel nie umiem doradzić nie znająć całości. Mi w ogóle Spring tak pomaga, że od jakiegoś czasu nie używam wcale.
Ale w projektach zespołowych czasem mam - ludzie narzucają i wtedy właśnie spring security zadziwia mnie najbardziej - ile się trzeba narypać kodu, żeby uzyskać dychawiczną i niepewną wersję tego co można zrobić przy pomocy kilku bibliotek. Ostatnio poległem właśnie na koncepcji zewnętrzny Oauth2 (azure) + własny custom JWT token. Nie dość, że absurdalne ilości kodu do napisania to jeszcze się okazało, że azure komponent do auth2 i tak na siłę używa sesji i cookies więc mogę sobie JWT tokena wsadzić (tak naprawdę tu wina była nie samego Springa, tylko tego co pisał te komponenty do azure).
A na koniec security, które działa na proxy, adnotacjach i threadlocalu 🤦 - czyli jak dla mnie nie działa (ale cóż - zespół tak chce).

0

https://docs.spring.io/spring-security/site/docs/5.4.6/api/org/springframework/security/authentication/AbstractAuthenticationToken.html

AuthenticationToken ma właściwość details. Może coś w tym kierunku?

Możesz też sprawdzić czy jak pozwolisz wyjątkom latać to ten pierwotny wyjątek nie przyjdzie jako cause w AuthenticationException.

0

przecież w metodzie doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain)) masz dostęp do response,
stwórz sobie klasę bądź interfejs jako AuthError
w validacji ustaw sobie jakiegoś mniej więcej takiego Eithera:

fun validateToken(authToken: String?, tokenType: TokenType = TokenType.ACCESS_TOKEN): Either<AuthError, Jws<Claims>> {
        return try {
            val claims = Jwts.parser().setSigningKey(tokenSecret).parseClaimsJws(authToken)
            return Either.right(claims);
        } catch (ex: SignatureException) {
            return Either.left(new AuthError("token have invalid signature"))
        } catch (ex: MalformedJwtException) {
            return Either.left(new AuthError("token is malformed"))
        } catch (ex: ExpiredJwtException) {
            return Either.left(new AuthError("token is expired"))
        } catch (ex: UnsupportedJwtException) {
            return Either.left(new AuthError("token not supported"))
        } catch (ex: IllegalArgumentException) {
            return Either.left(new AuthError("token is invalid"))
        }

następnie podczas validacji w przypadku błedu ustaw sobie response:

tokenUtils.validateToken(token)
   .peek( e -> {
      val id = tokenUtils.getId(token!!)
      SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken(id, null, Collections.emptyList())
}).peekLeft( authError - > {
            response.status = authError.getStatus()
            response.contentType = "application/json"
            response.writer.write(authError.getMessage())
})

kod jest tylko poglądowy, prawdopodobnie musiał byś zmienić coś jeszcze w configureExceptionHandling aby ci tego nie nadpisał

1 użytkowników online, w tym zalogowanych: 0, gości: 1