Skip to content

Commit

Permalink
Merge pull request #2 from pavelfomin/feature/spring-security-jwt
Browse files Browse the repository at this point in the history
spring security jwt
  • Loading branch information
pavelfomin committed Feb 9, 2020
2 parents 745d9d4 + ce47b96 commit 740fa21
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 114 deletions.
26 changes: 19 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,42 @@

<groupId>com.droidablebee</groupId>
<artifactId>spring-boot-rest-example</artifactId>
<version>2.0.6-SNAPSHOT</version>
<version>2.2.4</version>
<name>Spring boot example with REST and spring data JPA</name>

<packaging>jar</packaging>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>

<properties>
<java.version>1.8</java.version>
<boot.version>2.2.4.RELEASE</boot.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${boot.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${boot.version}</version>
</dependency>

<!-- actuator with production-ready endpoints -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>${boot.version}</version>
</dependency>

<!-- spring security with oauth JWT -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

<!-- use jackson for xml instead of jaxb -->
Expand Down Expand Up @@ -64,7 +72,12 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${boot.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Expand All @@ -83,7 +96,6 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${boot.version}</version>
<executions>
<execution>
<goals>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.droidablebee.springboot.rest.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String issuerUri;

@Value("${app.security.ignore:/swagger/**, /swagger-resources/**, /swagger-ui.html, /webjars/**, /v2/api-docs, /actuator/info}")
private String[] ignorePatterns;

@Override
protected void configure(HttpSecurity http) throws Exception {

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

http.csrf().disable(); // do not require SCRF for POST and PUT

//make sure principal is created for the health endpoint to verify the role
http.authorizeRequests().antMatchers("/actuator/health").permitAll();

http
.authorizeRequests()
// .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}

@Override
public void configure(WebSecurity web) {

web.ignoring().antMatchers(ignorePatterns);
}

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.droidablebee.springboot.rest.endpoint;

import javax.validation.Valid;
import javax.validation.constraints.Size;

import com.droidablebee.springboot.rest.domain.Person;
import com.droidablebee.springboot.rest.service.PersonService;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
Expand All @@ -21,13 +25,8 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.droidablebee.springboot.rest.domain.Person;
import com.droidablebee.springboot.rest.service.PersonService;

import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import javax.validation.Valid;
import javax.validation.constraints.Size;

@RestController
@RequestMapping(produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
Expand All @@ -37,10 +36,14 @@ public class PersonEndpoint extends BaseEndpoint {
static final int DEFAULT_PAGE_SIZE = 10;
static final String HEADER_TOKEN = "token";
static final String HEADER_USER_ID = "userId";


static final String PERSON_READ_PERMISSION = "person-read";
static final String PERSON_WRITE_PERMISSION = "person-write";

@Autowired
PersonService personService;

private PersonService personService;

@PreAuthorize("hasAuthority('SCOPE_" + PERSON_READ_PERMISSION + "')")
@RequestMapping(path = "/v1/persons", method = RequestMethod.GET)
@ApiOperation(
value = "Get all persons",
Expand All @@ -62,7 +65,8 @@ public Page<Person> getAll(

return persons;
}


@PreAuthorize("hasAuthority('SCOPE_" + PERSON_READ_PERMISSION + "')")
@RequestMapping(path = "/v1/person/{id}", method = RequestMethod.GET)
@ApiOperation(
value = "Get person by id",
Expand All @@ -75,10 +79,11 @@ public ResponseEntity<Person> get(@ApiParam("Person id") @PathVariable("id") Lon
return (person == null ? ResponseEntity.status(HttpStatus.NOT_FOUND) : ResponseEntity.ok()).body(person);
}

@PreAuthorize("hasAuthority('SCOPE_" + PERSON_WRITE_PERMISSION + "')")
@RequestMapping(path = "/v1/person", method = RequestMethod.PUT, consumes = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
@ApiOperation(
value = "Create new or update existing person",
notes = "Creates new or updates exisitng person. Returns created/updated person with id.",
notes = "Creates new or updates existing person. Returns created/updated person with id.",
response = Person.class)
public ResponseEntity<Person> add(
@Valid @RequestBody Person person,
Expand Down
13 changes: 0 additions & 13 deletions src/main/resources/application.properties

This file was deleted.

29 changes: 29 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
management:
endpoints:
web:
exposure:
include: info,health,env
endpoint:
health:
show-details: when-authorized
roles: SCOPE_health-details
spring:
jpa:
properties:
hibernate:
format_sql: true
use_sql_comments: true
jackson:
# date-format: yyyy-MM-dd'T'HH:mm:ss.SSSZ:
serialization:
write_dates_as_timestamps: true
security:
oauth2:
resourceserver:
jwt:
# this is to wire the "org.springframework.security.oauth2.jwt.JwtDecoder" bean correctly
jwk-set-uri: http://localhost:9999/.well-known/jwks.json
datasource:
platform: h2
driverClassName: org.h2.Driver
url: jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL;INIT=CREATE SCHEMA IF NOT EXISTS ddb
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
package com.droidablebee.springboot.rest.endpoint;

import static org.hamcrest.Matchers.emptyArray;
import net.minidev.json.JSONArray;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.isA;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MvcResult;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ActuatorEndpointTest extends BaseEndpointTest {

@Before
public void setup() throws Exception {

super.setup();
}

@Test
public void getInfo() throws Exception {

Expand All @@ -50,13 +43,40 @@ public void getHealth() throws Exception {
.andExpect(status().isOk())
.andExpect(content().contentType(JSON_MEDIA_TYPE))
.andExpect(jsonPath("$.status", is("UP")))
// .andExpect(jsonPath("$.diskSpace.status", is("UP")))
// .andExpect(jsonPath("$.db.status", is("UP")))
.andExpect(jsonPath("$.components").doesNotExist())
;
}

@Test
public void getHealthAuthorized() throws Exception {

mockMvc.perform(get("/actuator/health").with(jwt()))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType(JSON_MEDIA_TYPE))
.andExpect(jsonPath("$.status", is("UP")))
.andExpect(jsonPath("$.components").doesNotExist())
;
}

@Test
public void getHealthAuthorizedWithConfiguredRole() throws Exception {

mockMvc.perform(get("/actuator/health").with(jwtWithScope("health-details")))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType(JSON_MEDIA_TYPE))
.andExpect(jsonPath("$.status", is("UP")))
.andExpect(jsonPath("$.components", isA(Object.class)))
.andExpect(jsonPath("$.components.diskSpace.status", is("UP")))
.andExpect(jsonPath("$.components.diskSpace.details", isA(Object.class)))
.andExpect(jsonPath("$.components.db.status", is("UP")))
.andExpect(jsonPath("$.components.db.details", isA(Object.class)))
.andExpect(jsonPath("$.components.ping.status", is("UP")))
;
}

@Test
@Ignore("enable security first")
public void getEnv() throws Exception {

mockMvc.perform(get("/actuator/env"))
Expand All @@ -65,4 +85,16 @@ public void getEnv() throws Exception {
;
}

@Test
public void getEnvAuthorized() throws Exception {

mockMvc.perform(get("/actuator/env").with(jwt()))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentType(JSON_MEDIA_TYPE))
.andExpect(jsonPath("$.activeProfiles", isA(JSONArray.class)))
.andExpect(jsonPath("$.propertySources", isA(JSONArray.class)))
;
}

}
Loading

0 comments on commit 740fa21

Please sign in to comment.