Vaadin LoginForm Failure Handler Breaks login?error URL - spring-boot

I'm using the Vaadin LoginForm with Spring Security in a Spring Boot project. By default it redirects to /login?error if authentication fails for any reason. I want to redirect for certain failures, specifically if the user hasn't validated their email yet. I'm trying to use the failureHandler feature to do this, but as soon as I create one, the /login?error URL suddenly requires the user to be authenticated, while /login functions as normal.
My login view:
#PageTitle("Login")
#Route("login", layout = MainLayout::class)
class LoginView : VerticalLayout(), BeforeEnterObserver {
private val loginForm: LoginForm
init {
alignItems = FlexComponent.Alignment.CENTER
className = "login-view"
width = LumoUtility.Width.AUTO
loginForm = loginForm { action = "login" }
button("Register") {
onLeftClick { ui.ifPresent { it.navigate("register")} }
}
}
override fun beforeEnter(beforeEnterEvent: BeforeEnterEvent) {
if (beforeEnterEvent.location.queryParameters.parameters.containsKey("error")) {
loginForm.isError = true
}
}
}
My security configuration:
#Configuration
#EnableWebSecurity
class SecurityConfiguration : VaadinWebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.formLogin()
.failureHandler(object : SimpleUrlAuthenticationFailureHandler() {
override fun onAuthenticationFailure(
request: HttpServletRequest?,
response: HttpServletResponse?,
exception: AuthenticationException?
) {
super.onAuthenticationFailure(request, response, exception)
}
})
super.configure(http)
setLoginView(http, LoginView::class.java)
}
As you can see, all I did was add a failureHandler in my security configuration function that simply calls the default failureHandler, but that is enough to break the /login?error. If I remove that failureHandler, then it works as expected and shows the /login?error URL when login fails. How do I get this to work?

You setting a failureHandler overrides the default of simply redirecting to /login?error (and automatically configuring this URL to not require authentication). Looking at the code you can only have one AuthenticationFailureHandler.

Related

How to handle login failure with Spring API on server and Retrofit2 on Android?

On server side I have created simple Spring API with authentication. I have just added implementation("org.springframework.boot:spring-boot-starter-security") dependency and when I go to url with browser - it shows login page when I'm not logged in.
For now I'm using basic authentication, my username and password set in configuration file like this (resources/application.properties file):
spring.security.user.name=myusername
spring.security.user.password=mypassword
spring.security.user.roles=manager
I'm also using Spring Data REST, so Spring creates API automatically for JPA repositories that exist in my project. I had to set up my database, create JPA repositories for tables and add implementation("org.springframework.boot:spring-boot-starter-data-rest") to my dependencies to make it work.
On Android side I call my API with this Adapter and Client.
interface ApiClient {
#GET("inventoryItems/1")
suspend fun getFirstInventoryItem(): Response<InventoryItemDto>
}
object ApiAdapter {
private const val API_BASE_URL = "http://some.url/"
private const val API_USERNAME = "myusername"
private const val API_PASSWORD = "mypassword"
val apiClient: ApiClient = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.client(getHttpClient(API_USERNAME, API_PASSWORD))
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiClient::class.java)
private fun getHttpClient(user: String, pass: String): OkHttpClient =
OkHttpClient
.Builder()
.authenticator(getBasicAuth(user,pass))
.build()
private fun getBasicAuth(username: String?, password: String?): Authenticator? =
object : Authenticator {
override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
return response
.request()
.newBuilder()
.addHeader("Authorization", Credentials.basic(username, password))
.build()
}
}
}
And this is how I call my API on Android:
(I'm calling this from onViewCreated on my view Fragment)
lifecycleScope.launch {
val item: InventoryItemDto? = ApiAdapter.apiClient.getFirstInventoryItem().body()
binding?.tvTest?.text = item.toString()
}
When I provide correct password everything works.
But when I provide wrong password my Android app crashes because java.net.ProtocolException: Too many follow-up requests: 21 is thrown.
It looks like my Android client goes to requested url (inventoryItems/1) and then it is redirected to login page. Then my clients tries to authenticate on that page again, because I have .addHeader("Authorization", Credentials.basic(username, password)) added to every request (I assume). Login is failed again, so it is redirected again to login page where it sends wrong credentials again and again is redirected...
My question 1: how to deal with login failed properly on Android and/or Spring?
My question 2: how to handle other errors (like bad request) properly on Android and/or Spring?
What I have tried:
Disable followRedirects and followSslRedirects on Android side like this:
private fun getHttpClient(user: String, pass: String): OkHttpClient =
OkHttpClient
.Builder()
.followRedirects(false)
.followSslRedirects(talse)
.authenticator(getBasicAuth(user,pass))
.build()
Add .addHeader("X-Requested-With", "XMLHttpRequest") header, also on Android side:
private fun getBasicAuth(username: String?, password: String?):Authenticator? =
object : Authenticator {
override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
return response
.request()
.newBuilder()
.addHeader("Authorization", Credentials.basic(username, password))
.addHeader("X-Requested-With", "XMLHttpRequest")
.build()
}
}
OK, I got this (answering my own question). Solution is based on this: link
I will not accept my answer, maybe someone will propose some solution without deprecated class, and maybe with good explanation.
I have created my own AuthenticationFailureHandler, like this:
class CustomAuthenticationFailureHandler : AuthenticationFailureHandler {
private val objectMapper = ObjectMapper()
#Throws(IOException::class, ServletException::class)
override fun onAuthenticationFailure(
request: HttpServletRequest?,
response: HttpServletResponse,
exception: AuthenticationException
) {
response.status = HttpStatus.UNAUTHORIZED.value()
val data: MutableMap<String, Any> = HashMap()
data["timestamp"] = Calendar.getInstance().time
data["exception"] = exception.message.toString()
response.outputStream.println(objectMapper.writeValueAsString(data))
}
}
I had to configure security manually by creating this class:
#Configuration
#EnableWebSecurity
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
auth.inMemoryAuthentication()
.withUser(API_USERNAME)
.password(passwordEncoder().encode(API_PASSWORD))
.roles(API_ROLE)
}
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.anyRequest()
.authenticated()
}
#Bean
fun authenticationFailureHandler(): AuthenticationFailureHandler = CustomAuthenticationFailureHandler()
#Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
companion object {
private const val API_USERNAME = "user1"
private const val API_PASSWORD = "user1"
private const val API_ROLE = "USER"
}
}
Unfortunately WebSecurityConfigurerAdapter class is deprected. I will deal with that later.

Query a password protected WebService in Spring-Boot via HealthIndicator

I need to query a WebService and check its results to calculate a Health.status() in SpringBoot.
Health is then used for k8s in its liveness probe: /actuator/health.
However, the WebService that I need to query needs a username and password, whereas the endpoint itself does not, as it is consumed by k8s and Prometheus. Please see the following configuration:
http
.authorizeRequests()
.requestMatchers(EndpointRequest.to("info", "health")).permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.csrf().disable()
As the WebService in question can be queried when httpBasic() is used to authenticate, I need to somehow use basic authentication in HealthIndicator without passing these values in the request.
What I want to do is to have "health" permitted for unauthorised requests, but still transparently authenticate with a technical user to query a third party service. Please see comment in following snippet:
override fun health(): Health {
val status = Health.up()
val beforeServiceCall = System.nanoTime()
val partner =
try {
// COMMENT FOR StackOverflow
// this call here needs authentication resp. a token which is only generated when basicAuth is used
partnerService.getPartner(
process = metricsProperties.process,
partnerNumber = metricsProperties.partnernumber
)
} catch (e: Exception) {
null
}
val afterServiceCall = System.nanoTime()
return status
.withDetail(PARTNER_FOUND, partner.let {
if (it != null) {
1.0
} else {
status.unknown()
0.0
}
})
.withDetail(PARTNER_QUERY_TIME_MS, ((afterServiceCall - beforeServiceCall) / 1e6).toDouble())
.build()
tl;dr;
I need to somehow trigger the WebSecurityConfigurerAdapter's configure method from within my HealthIndicator to obtain a valid token to be able to query a third party service. How can this configure method be triggered explicitly?
#Configuration
#EnableWebSecurity
class SecurityConfig(omitted params) :
WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder) {
auth
.authenticationProvider(object : AuthenticationProvider {
override fun authenticate(authentication: Authentication): Authentication? {
val authorizationGrant = ResourceOwnerPasswordCredentialsGrant(authentication.name,
Secret(authentication.credentials as String))
val tokenRequest = TokenRequest(URI(keycloakUrl),
ClientID(clientID),
authorizationGrant)
val tokenResponse = TokenResponse
.parse(tokenRequest
.toHTTPRequest()
.send())
if (tokenResponse.indicatesSuccess()) {
return UsernamePasswordAuthenticationToken(
authentication.name,
null,
emptyList())
} else {
throw BadCredentialsException("Error logging in")
}
}
override fun supports(authentication: Class<*>): Boolean {
return authentication == UsernamePasswordAuthenticationToken::class.java
}
})
.eraseCredentials(false)
}

Vaadin 14 and Push: Accessing Spring SecurityContext and Authentication outside the request thread

I had the same problem this guy had:
https://vaadin.com/forum/thread/3383122/17008971
When receiving a status from the server I try to refresh content via push (using ui.access) on the client. That content needs the current principal's information.
final SecurityContext securityContext = SecurityContextHolder.getContext();
final Authentication authentication = securityContext.getAuthentication();
this
authentication
is returning null.
He solved this problem using Vaadin Shared Security, but I can't find any repo or library called Vaadin Shared Sec. Are there any other ways to solve the problem?
Why not just get the auth details in the view constructor and make them class-scoped, or have a bean to do that and set its values ? E.g.
#Route("some-view")
public class SomeView extends VerticalLayout {
Authentication authentication;
private void doHeavyStuff() {
try {
Thread.sleep(1500);
} catch (InterruptedException ex) {
// ignore
}
}
public SomeView() {
authentication = SecurityContextHolder.getContext().getAuthentication();
final Button button = new Button("Click me", e -> {
Notification.show("CLICKED");
getUI().ifPresent(ui -> {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
doHeavyStuff();
ui.access(() -> {
Notification.show("Calculation done");
});
});
});
});
add(button);
// simple link to the logout endpoint provided by Spring Security
Element logoutLink = ElementFactory.createAnchor("logout", "Logout");
getElement().appendChild(logoutLink);
}
}
I based this answer (tested it as well) in this tutorial, if you want to learn more about it:
https://vaadin.com/learn/tutorials/securing-your-app-with-spring-security/speciale

How to hook into Spring Security authentication process?

Currently I have this trivial configuration:
// Kotlin code
override fun configure(http: HttpSecurity) {
http
.formLogin()
.loginPage("/entry")
.loginProcessingUrl("/auth")
.usernameParameter("usr")
.passwordParameter("pwd")
.defaultSuccessUrl("/", true)
.failureHandler { request, response, exception ->
// Can't figure out what to enter here (see below).
}
}
If authentication fails, I have two requirements:
Flash error message into the session (avoiding 'error' param in query string). It seems I can't inject RedirectAttributes into this lambda; is there a workaround?
I want to send back the login (but not the password) that user entered before submitting login form, in order to repopulate the field. How do I do that?
I was able to figure it out.
#Configuration
#EnableWebSecurity
class SecurityConfig: WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http
.formLogin()
.loginPage("/entry")
.loginProcessingUrl("/auth")
.usernameParameter("usr")
.passwordParameter("pwd")
.defaultSuccessUrl("/", true)
.failureHandler { request, response, _ ->
request.session.setAttribute("loginError", "Login Error!")
request.session.setAttribute("failedUsername", request.getParameter("usr"))
response.sendRedirect("/entry")
}
}
}
Then, you have to set up login controller to customize serving of login form:
#Controller
#RequestMapping("/entry")
internal class LoginController {
#GetMapping
fun getLoginForm(session: HttpSession, model: Model): String {
if (session.getAttribute("loginError") != null) {
model.addAttribute("loginError", "Login Error!")
session.removeAttribute("loginError")
model.addAttribute("failedUsername", session.getAttribute("failedUsername"))
session.removeAttribute("failedUsername")
}
return "login"
}
}
Then, you can use loginError and failedUsername model attributes in your templates:
<div th:if="${loginError}">Incorrect login/password</div>
<!-- ... -->
<input type="text" name="usr" th:value="${failedUsername}">
Basically we are emulating "flashing" messages into session. We carry these messages in the session and remove them as soon as they are read and passed on into the model. It’s possible that redirect will go wrong and messages will remain in the session, but they are harmless on their own, plus they will be removed the next time user visits /entry page.
As a result, now there is no ?error in page URL, and the user is not required to retype username.

spring 4.1 javaConfg setting to get requestCache working

similar to this:
Spring 3.1: Redirect after login not working
when an authenticated user becomes inauthenticated while deep-linking into a single page web app.
Spring security redirects to logon but:
SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response);
is null
thus i cannot devine the url or params to send re-authenticated user to requested page
#Configuration
#EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers() //redacted .
.antMatchers("/**").permitAll()
.and()
.formLogin()
.loginPage("/x/y/logon")
.usernameParameter("userLogon") //redacted
.loginProcessingUrl("/x/y/logon") //redacted
.defaultSuccessUrl("/x/", true)
.failureUrl("/x/y/logon?error=true")
.and()
.logout()
.logoutUrl("/x/y/logout")
.logoutSuccessUrl("/x/")
.permitAll();
}
}
-- controller --
#RequestMapping(method=RequestMethod.GET, value="/y/logon")
public ModelAndView logonHandler(HttpServletRequest request, HttpServletResponse response) {
List<Client> clients = //manager call for list of clients to show at logon
SavedRequest savedRequest = new HttpSessionRequestCache().getRequest(request, response);
if (savedRequest != null) {
String reqUrl = savedRequest.getRedirectUrl();
String[] urlParams = reqUrl.split("&");
String prefix = "";
String urlParam = "";
String cid = "";
try {
urlParam = urlParams[1];
} catch(IndexOutOfBoundsException ioob) { }
if (reqUrl.contains("cid=")) { cid = reqUrl.substring(reqUrl.indexOf("cid=")+4, reqUrl.indexOf("&")); }
if (reqUrl.contains("?")) { reqUrl = reqUrl.substring(0, reqUrl.indexOf("?")); }
prefix = reqUrl.substring(reqUrl.indexOf("/x/")+6, reqUrl.indexOf("/x/")+8);
reqUrl = reqUrl.substring(reqUrl.indexOf(prefix)+2);
if (reqUrl.contains("/")) {
reqUrl = reqUrl.substring(0, reqUrl.indexOf("/"));
}
request.setAttribute("requestUrl", prefix+reqUrl);
request.setAttribute("urlParam", urlParam);
request.setAttribute("cid", cid);
}
request.setAttribute("IPAddress", request.getRemoteAddr());
return new ModelAndView("x/logon", "clients", clients);
}
problem is, SavedRequest is null
is this an issue with:
alwaysUseDefaultTargetUrl property?
if yes, how in javaConfig does one set this property?
----- on edit to address comments ------
i'll explain my understanding of ea. .formLogon() settings:
logonPage() will be read by spring and control redirect to logon page when you are not authorized (cookie expire/db record del, etc). There are many ways that a session can not be authorized and spring needs to know what page to send unauth requests to. My manual logon handler only handles requests to the logon url.
usernameParameter() is to change from the default form input name thus obfuscating that one is using spring security.
loginProcessingUrl() this seems to conflict with the custom logonHandler, but i think its req to handle the post and allow for spring to create a secure sesson.
defaultSucessUrl() tells spring where to go after successful logon (the post request).
failureUrl() defines the url for failed logon.
nowhere in my custom logon handler for the get request, are those settings in conflict... i think... but i've read the docs and the Spring Security 3 book and lots of online resources and i still do not feel as though i have a solid understanding of spring security... so i may be way off

Resources