Kotlin spring boot security

Provide basic security for your Spring Boot application with Spring Security and Kotlin

When you’re creating your REST API, most of the time you don’t want it to be publically accessible. Moreover, sometimes you’d like to restrict certain paths for users with specific roles (for example administrators).

In this blog post, I’m going to provide very basic Spring Security integration for Spring Boot application written in Kotlin language.

Please note that I’m using the example REST API I provided in the previous article. So if you don’t have your Spring Boot REST API built yet, move through the steps I described there.

Adding dependencies

At first, let’s add Spring Boot Starter Security dependency pack into our project.
It contains all the stuff we need.

implementation("org.springframework.boot:spring-boot-starter-security:2.2.5.RELEASE")
 org.springframework.boot spring-boot-starter-security 2.2.5.RELEASE 

Providing configuration class

As I mentioned in the preface, let’s use the REST API that was created in the previous blogpost.

For simplification, we’d like to use out-of-the-box Spring Security users and roles and we’d use two roles here: ADMIN and USER.

We can secure our endpoints using @Secured annotation or via the config class. Let’s follow the latter approach.

Create a SecurityConfig class in the config package.

 @Configuration class SecurityConfig : WebSecurityConfigurerAdapter() < @Bean fun encoder(): PasswordEncoder < return BCryptPasswordEncoder() >override fun configure(auth: AuthenticationManagerBuilder) < auth.inMemoryAuthentication() .withUser("admin") .password(encoder().encode("pass")) .roles("USER", "ADMIN") .and() .withUser("user") .password(encoder().encode("pass")) .roles("USER") >@Throws(Exception::class) override fun configure(http: HttpSecurity) < http.httpBasic() .and() .authorizeRequests() .antMatchers(HttpMethod.GET, "/tasks").hasRole("ADMIN") .antMatchers(HttpMethod.POST, "/tasks/**").hasRole("ADMIN") .antMatchers(HttpMethod.PUT, "/tasks/**").hasRole("ADMIN") .antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN") .antMatchers(HttpMethod.GET, "/tasks/**").hasAnyRole("ADMIN", "USER") .and() .csrf().disable() .formLogin().disable() >>

The config provides two in-memory users: admin and user with encrypted passwords. If you don’t want to encrypt the password you can use a plain text password with noop> prefix.

The second part of the configuration is about paths, permitted HTTP methods and user roles permissions.

The admin user will be able to list all existing tasks, retrieve a single one, create a new one, update and delete existing ones.
The ordinary user will be able to retrieve a single task only.

csrf().disable is about disabling Spring Security built-in Cross Site Request Forgery protection.
formLogin().disable() is about disabling default login form. If you’d like to redirect user to the specific login page you can specify it here.

Okay, the basic configuration is done here. When you provide your own user details representation you’ll have to add more stuff here, but for our purposes is enough.

Updating the tests

Obviously, at this point, previously provided tests will fail.

Which is a very good sign, because it means that our security configuration works as expected.

Let’s update the tests and make sure they pass.
As TestRestTemplate has been used for testing the endpoints, the authentication in tests is really easy. In our case, withBasicAuth method will do the job.

In MockMvc-based tests you’d use @WithMockUser(“userName”) annotation.

First of all, let’s provide useful constants with credentials.
If you’re going to use it in other tests, it’s a good idea to delegate them to test utils class. But for our purposes, companion object does the job.

Example of a single updated test which uses withBasicAuth method:

 @Test fun `should return all tasks`()

Change the other tests like that and it will be all green again. As simple as this.

Читайте также:  Php string parsing functions

But let’s provide additional tests right now to check if the behavior is as we expected.

 fun `should not allow to return all tasks list for a non-authenticated user`() fun `should not allow to return all tasks list for a non-authorized user`() fun `should not allow to create new task as a non-authorized user`() fun `should not allow to update existing task as a non-authorized user`() fun `should not allow to delete existing task as a non-authorized user`()

The finally updated test class is as follows:

package net.mestwin.mongodbrestapidemo.controller . @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ExtendWith(SpringExtension::class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TaskControllerIntegrationTest @Autowired constructor( private val taskRepository: TaskRepository, private val restTemplate: TestRestTemplate ) < companion object < private const val DEFAULT_PASSWORD = "pass" private const val DEFAULT_USER = "user" private const val DEFAULT_ADMIN = "admin" >private val defaultTaskId = ObjectId.get() @LocalServerPort protected var port: Int = 0 @BeforeEach fun setUp() < taskRepository.deleteAll() >@Test fun `should return all tasks as an ADMIN user`() < saveOneTask() val response = restTemplate .withBasicAuth(DEFAULT_ADMIN, DEFAULT_PASSWORD) .getForEntity( getRootUrl(), List::class.java ) assertEquals(200, response.statusCode.value()) assertNotNull(response.body) assertEquals(1, response.body?.size) >@Test fun `should return single task by id as an ADMIN user`() < checkIfSingleTagIsReturned(DEFAULT_ADMIN) >@Test fun `should return single task by id as a USER user`() < checkIfSingleTagIsReturned(DEFAULT_USER) >@Test fun `should create new task as an ADMIN user`() < val taskRequest = prepareTaskRequest() val response = restTemplate .withBasicAuth(DEFAULT_ADMIN, DEFAULT_PASSWORD) .postForEntity( getRootUrl(), taskRequest, Task::class.java ) assertEquals(201, response.statusCode.value()) assertNotNull(response.body) assertNotNull(response.body?.id) assertEquals(taskRequest.description, response.body?.description) assertEquals(taskRequest.title, response.body?.title) >@Test fun `should update existing task as an ADMIN user`() < saveOneTask() val taskRequest = prepareTaskRequest() val updateResponse = restTemplate .withBasicAuth(DEFAULT_ADMIN, DEFAULT_PASSWORD) .exchange( getRootUrl() + "/$defaultTaskId", HttpMethod.PUT, HttpEntity(taskRequest, HttpHeaders()), Task::class.java ) val updatedTask = taskRepository.findOneById(defaultTaskId) assertEquals(200, updateResponse.statusCode.value()) assertEquals(defaultTaskId, updatedTask.id) assertEquals(taskRequest.description, updatedTask.description) assertEquals(taskRequest.title, updatedTask.title) >@Test fun `should delete existing task as an ADMIN user`() < saveOneTask() val delete = restTemplate .withBasicAuth(DEFAULT_ADMIN, DEFAULT_PASSWORD) .exchange( getRootUrl() + "/$defaultTaskId", HttpMethod.DELETE, HttpEntity(null, HttpHeaders()), ResponseEntity::class.java ) assertEquals(204, delete.statusCode.value()) assertThrows(EmptyResultDataAccessException::class.java) < taskRepository.findOneById(defaultTaskId) >> @Test fun `should not allow to return all tasks list for a non-authenticated user`() < saveOneTask() val response = restTemplate .getForEntity( getRootUrl(), Object::class.java ) assertEquals(401, response.statusCode.value()) >@Test fun `should not allow to return all tasks list for a non-authorized user`() < saveOneTask() val response = restTemplate .withBasicAuth(DEFAULT_USER, DEFAULT_PASSWORD) .getForEntity( getRootUrl(), Object::class.java ) assertEquals(403, response.statusCode.value()) >@Test fun `should not allow to create new task as a non-authorized user`() < val taskRequest = prepareTaskRequest() val response = restTemplate .withBasicAuth(DEFAULT_USER, DEFAULT_PASSWORD) .postForEntity( getRootUrl(), taskRequest, Object::class.java ) assertEquals(403, response.statusCode.value()) >@Test fun `should not allow to update existing task as a non-authorized user`() < saveOneTask() val taskRequest = prepareTaskRequest() val updateResponse = restTemplate .withBasicAuth(DEFAULT_USER, DEFAULT_PASSWORD) .exchange( getRootUrl() + "/$defaultTaskId", HttpMethod.PUT, HttpEntity(taskRequest, HttpHeaders()), Object::class.java ) assertEquals(403, updateResponse.statusCode.value()) >@Test fun `should not allow to delete existing task as a non-authorized user`() < saveOneTask() val delete = restTemplate .withBasicAuth(DEFAULT_USER, DEFAULT_PASSWORD) .exchange( getRootUrl() + "/$defaultTaskId", HttpMethod.DELETE, HttpEntity(null, HttpHeaders()), Object::class.java ) assertEquals(403, delete.statusCode.value()) >private fun getRootUrl(): String? = "http://localhost:$port/tasks" private fun saveOneTask() = taskRepository.save(Task(defaultTaskId, "Title", "Description")) private fun prepareTaskRequest() = TaskRequest("Default title", "Default description") private fun checkIfSingleTagIsReturned(userName: String) < saveOneTask() val response = restTemplate .withBasicAuth(userName, DEFAULT_PASSWORD) .getForEntity( getRootUrl() + "/$defaultTaskId", Task::class.java ) assertEquals(200, response.statusCode.value()) assertNotNull(response.body) assertEquals(defaultTaskId, response.body?.id) >>

And the result is satisfying:

Testing with curl

Of course, you can test it manually as well, using your terminal along with curl.

Take a look at example requests and responses:

$ curl localhost:8080/tasks < "timestamp": "2020-03-14T08:37:16.718+0000", "status": 401, "error": "Unauthorized", "message": "Unauthorized", "path": "/tasks" >$ curl localhost:8080/tasks -u user:pass < "timestamp": "2020-03-14T08:38:49.094+0000", "status": 403, "error": "Forbidden", "message": "Forbidden", "path": "/tasks" >$ curl localhost:8080/tasks -u admin:pass [ < "id": < "timestamp": 1584174661, "counter": 923070, "date": "2020-03-14T08:31:01.000+0000", "time": 1584174661000, "machineIdentifier": 14441788, "processIdentifier": 4120, "timeSecond": 1584174661 >, "title": "Title", "description": "Description", "createdDate": "2020-03-14T09:31:03.327", "modifiedDate": "2020-03-14T09:31:03.327" > ] $ curl -d '' -H "Content-Type: application/json" -X POST http://localhost:8080/tasks -u admin:pass < "id": < "timestamp": 1584175283, "counter": 4450354, "date": "2020-03-14T08:41:23.000+0000", "time": 1584175283000, "machineIdentifier": 6450927, "processIdentifier": 25501, "timeSecond": 1584175283 >, "title": "Test title", "description": "Test description", "createdDate": "2020-03-14T09:41:23.575904", "modifiedDate": "2020-03-14T09:41:23.575927" >

Conclusion

As you can see the basic Spring Security integration is very easy and works as expected out-of-the-box.

Читайте также:  Php install memcache extension

Please keep in mind that this tutorial provides basic security integration which is good but not entirely safe. Sending credentials with every request is not a secure approach. You should never use it publically without protecting your traffic with HTTPS.

The approach demonstrated in this blog post could be enough for basic purposes on the one hand, but on the other hand, you’ll need more sophisticated solutions like OAuth2.0.

I hope this post will help you to introduce basic protection mechanisms into your Spring Boot application.

Источник

Spring Tips: Kotlin and Spring Security

Hi, Spring fans! Welcome to another installment of Spring Tips. In this episode we’re going to look at the new Kotlin DSL for Spring Security. I love Kotlin. I introduced Kotlin in several other Spring Tips videos: The Kotlin Programming Language, Bootiful Kotlin Redux, and Spring’s Support for Coroutines. Some of those videos are very old! There are already a number of different projects in the Spring diaspora that are shipping Kotlin DSLs. They include, among others, Spring Framework, Spring Webflux, Spring Data, Spring Cloud Contract and Spring Cloud Gateway. And now, Spring Security!

Spring Security is an amazing project — it solves some of the hardest problems in the industry and helps people secure their applications. And, as if that weren’t enough, it’s displayed a steadfast determination to make security easy. If you ever used Spring Security in its earliest incarnations, you’d know that it required loads of XML — pages! — to get anything done. That improved to the point where in Spring Security 3 you could lock down an applicatino with common sense defaults with one or two stanzas of XML. Then, in Spring Security 4 they introduced a Java DSL that gave people the power of their compilers to help them validate things. Gradually, over time, Spring Security also introduced common sense defaults. Nowadays, you can register a UserDetailsService and Spring Security will lock down all HTTP endpoints in a Servlet-based HTTP application and require authentication. Couldn’t be easier! Or could it? In Spring Security 5.2, they introduced some much-appreciated refinements to Spring Security. Now, in addition to using the fluid Java config DSL of yore, there’s also a new approach where you can provide a lambda and be given a context object that you can then use. No longer do you need to indent your Spring Security APIs for their intent to be understood! And now, in this installment, we’re going to take things to the next level with a very quick look at the brand new Spring Security Kotlin DSL.

Remember, Spring Security addresses two orthagonal concerns: authentication and authorization. Authentication answers the question: «who is making the request?» Is it Josh, or Jane? Authorization answers the question «what permissions does the requester have once inside the system?» Authentication is all about plugging in identity providers. There are a million ways to do that (Active Directory, in-memory usernames and passwords, LDAP, SAML, etc.) It’s more about plugging in implementations. We’re just going to use an in-memory username and password authentication manager since we need it and that’s not where the DSL really shines.

Читайте также:  Php поля с обязательным заполнением

DSLs are most useful not when swapping out implementations of a given type, but when describing rules or customizing behavior. So, we’ll use the Spring Security DSL to customize the authorization behavior.

The following is a Spring Boot-based application that uses Spring Security. I generated a new project from the Spring Initializr that uses Kotlin, and uses Spring Security and Spring Boot 2.3.M2 or later. It uses the functional bean registration DSL to programatically register beans. We talked about programatic bean registration in a Spring Tips video from waaaaay back in 2017. Granted, that video demonstrated its use in Java, but the application is basically the same in Kotlin: you register a bean by wrapping it a call to the bean function.

The first bean is the InMemoryUserDetailsManager . The second bean is a functional HTTP endpoint, /greetings . When an HTTP request comes in, we extract the authenticated principal from the current request, extract the name and then build a ServerResponse whose body will be represented by a Map .

The interesting bit is the class KotlinSecurityConfiguration . It extends WebSecurityConfigurerAdapter . There are a few methods we might override there, but I chose to override the configure method to specify two things: that I wanted to opt-in to HTTP BASIC authentication, and to specify what routes require authentication and which are wide-open. The configuration below stipultes that all requests to /greetings/** (the /greetings/ endpoint, and anything below it, like /greetings/foo ) must be authenticated. The second rule says that everything else is wide-open. It’s very important that the more specific rule — /greetings/** — come before the more wide-open rule. The rules are evaluated in order, from top to bottom. If we’d put the second rule first, then it would match for every request and we’d never need to evaluate the rule for /greetings — it would be left effectively wide-open!

package com.example.kotlinsecurity import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Bean import org.springframework.context.support.beans import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.web.servlet.invoke import org.springframework.security.core.userdetails.User import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.web.servlet.function.ServerResponse import org.springframework.web.servlet.function.router @SpringBootApplication class KotlinSecurityApplication @EnableWebSecurity class KotlinSecurityConfiguration : WebSecurityConfigurerAdapter() < override fun configure(http: HttpSecurity?) < http < httpBasic <>authorizeRequests < authorize("/greetings/**", hasAuthority("ROLE_ADMIN")) authorize("/**", permitAll) >> > > fun main(args: Array) < runApplication(*args) < addInitializers(beans < bean < fun user(user: String, pw: String, vararg roles: String) = User.withDefaultPasswordEncoder().username(user).password(pw).roles(*roles).build() InMemoryUserDetailsManager(user("jlong", "pw", "USER"), user("rwinch", "pw1", "USER", "ADMIN")) >bean < router < GET("/greetings") < request ->request.principal().map < it.name >.map < ServerResponse.ok().body(mapOf("greeting" to "Hello, $it")) >.orElseGet < ServerResponse.badRequest().build() >> > > >) > > 

Conclusion

In this installment, we introduced Spring Security’s new Kotlin DSL. There’s more text than there is code because, and this is profound, Spring Security does a lot of stuff for you, so that the surface area of the API is literally the bare minimum in customizations you want to do beyond the already sensible defaults. I hope you learned something new, and will give the Spring Security DSL a shot.

Источник

Оцените статью