org.springframework.social
spring-social-config
@@ -1864,4 +1873,4 @@
integration-test
-
\ No newline at end of file
+
diff --git a/spring-boot-samples/pom.xml b/spring-boot-samples/pom.xml
index 277050ddfb..c81ca5b79c 100644
--- a/spring-boot-samples/pom.xml
+++ b/spring-boot-samples/pom.xml
@@ -58,6 +58,7 @@
spring-boot-sample-parent-context
spring-boot-sample-profile
spring-boot-sample-secure
+ spring-boot-sample-secure-oauth2
spring-boot-sample-servlet
spring-boot-sample-simple
spring-boot-sample-testng
diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/pom.xml b/spring-boot-samples/spring-boot-sample-secure-oauth2/pom.xml
new file mode 100644
index 0000000000..9aaf7f76d7
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/pom.xml
@@ -0,0 +1,56 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-samples
+ 1.3.0.BUILD-SNAPSHOT
+
+ spring-boot-sample-secure-oauth2
+ Spring Boot Security OAuth2 Sample
+ Spring Boot Security OAuth2 Sample
+ http://projects.spring.io/spring-boot/
+
+ Pivotal Software, Inc.
+ http://www.spring.io
+
+
+ ${basedir}/../..
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-data-rest
+
+
+ com.h2database
+ h2
+
+
+ org.springframework.security.oauth
+ spring-security-oauth2
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Application.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Application.java
new file mode 100644
index 0000000000..1a5fd5f639
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Application.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
+
+// @formatter:off
+/**
+ * After you launch the app, you can seek a bearer token like this:
+ *
+ *
+ *
+ * curl localhost:8080/oauth/token -d "grant_type=password&scope=read&username=greg&password=turnquist" -u foo:bar
+ *
+ *
+ *
+ *
+ * - grant_type=password (user credentials will be supplied)
+ * - scope=read (read only scope)
+ * - username=greg (username checked against user details service)
+ * - password=turnquist (password checked against user details service)
+ * - -u foo:bar (clientid:secret)
+ *
+ *
+ * Response should be similar to this:
+ * {"access_token":"533de99b-5a0f-4175-8afd-1a64feb952d5","token_type":"bearer","expires_in":43199,"scope":"read"}
+ *
+ * With the token value, you can now interrogate the RESTful interface like this:
+ *
+ *
+ * curl -H "Authorization: bearer [access_token]" localhost:8080/flights/1
+ *
+ *
+ * You should then see the pre-loaded data like this:
+ *
+ *
+ * {
+ * "origin" : "Nashville",
+ * "destination" : "Dallas",
+ * "airline" : "Spring Ways",
+ * "flightNumber" : "OAUTH2",
+ * "date" : null,
+ * "traveler" : "Greg Turnquist",
+ * "_links" : {
+ * "self" : {
+ * "href" : "http://localhost:8080/flights/1"
+ * }
+ * }
+ * }
+ *
+ *
+ * Test creating a new entry:
+ *
+ *
+ * curl -i -H "Authorization: bearer [access token]" -H "Content-Type:application/json" localhost:8080/flights -X POST -d @flight.json
+ *
+ *
+ * Insufficient scope? (read not write) Ask for a new token!
+ *
+ *
+ * curl localhost:8080/oauth/token -d "grant_type=password&scope=write&username=greg&password=turnquist" -u foo:bar
+ *
+ * {"access_token":"cfa69736-e2aa-4ae7-abbb-3085acda560e","token_type":"bearer","expires_in":43200,"scope":"write"}
+ *
+ *
+ * Retry with the new token. There should be a Location header.
+ *
+ *
+ * Location: http://localhost:8080/flights/2
+ *
+ * curl -H "Authorization: bearer [access token]" localhost:8080/flights/2
+ *
+ *
+ * @author Craig Walls
+ * @author Greg Turnquist
+ */
+// @formatter:on
+
+@Configuration
+@ComponentScan
+@EnableAutoConfiguration
+@EnableAuthorizationServer
+@EnableResourceServer
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Flight.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Flight.java
new file mode 100644
index 0000000000..0319074702
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/Flight.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import java.util.Date;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * Domain object for tracking flights
+ *
+ * @author Craig Walls
+ * @author Greg Turnquist
+ */
+@Entity
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class Flight {
+
+ @Id @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+
+ private String origin;
+ private String destination;
+ private String airline;
+ private String flightNumber;
+ private Date date;
+ private String traveler;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public String getOrigin() {
+ return origin;
+ }
+
+ public void setOrigin(String origin) {
+ this.origin = origin;
+ }
+
+ public String getDestination() {
+ return destination;
+ }
+
+ public void setDestination(String destination) {
+ this.destination = destination;
+ }
+
+ public String getAirline() {
+ return airline;
+ }
+
+ public void setAirline(String airline) {
+ this.airline = airline;
+ }
+
+ public String getFlightNumber() {
+ return flightNumber;
+ }
+
+ public void setFlightNumber(String flightNumber) {
+ this.flightNumber = flightNumber;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ public String getTraveler() {
+ return traveler;
+ }
+
+ public void setTraveler(String traveler) {
+ this.traveler = traveler;
+ }
+}
diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/FlightRepository.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/FlightRepository.java
new file mode 100644
index 0000000000..811b062af5
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/java/sample/FlightRepository.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package sample;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.security.access.prepost.PreAuthorize;
+
+/**
+ * Spring Data interface with secured methods
+ *
+ * @author Craig Walls
+ * @author Greg Turnquist
+ */
+public interface FlightRepository extends CrudRepository {
+
+ @PreAuthorize("#oauth2.hasScope('read')")
+ @Override
+ Iterable findAll();
+
+ @PreAuthorize("#oauth2.hasScope('read')")
+ @Override
+ Flight findOne(Long aLong);
+
+ @PreAuthorize("#oauth2.hasScope('write')")
+ @Override
+ S save(S entity);
+}
diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties
new file mode 100644
index 0000000000..32262012db
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/application.properties
@@ -0,0 +1,8 @@
+spring.datasource.platform=h2
+spring.oauth2.client.client-id=foo
+spring.oauth2.client.client-secret=bar
+
+security.user.name=greg
+security.user.password=turnquist
+
+logging.level.org.springframework.security=DEBUG
diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/data-h2.sql b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/data-h2.sql
new file mode 100644
index 0000000000..478a2ebf91
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/data-h2.sql
@@ -0,0 +1,4 @@
+insert into FLIGHT
+(id, origin, destination, airline, flight_number, traveler)
+values
+(1, 'Nashville', 'Dallas', 'Spring Ways', 'OAUTH2', 'Greg Turnquist');
diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/templates/.gitignore b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/main/resources/templates/.gitignore
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/spring-boot-samples/spring-boot-sample-secure-oauth2/src/test/java/sample/ApplicationTests.java b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/test/java/sample/ApplicationTests.java
new file mode 100644
index 0000000000..abe9dc7064
--- /dev/null
+++ b/spring-boot-samples/spring-boot-sample-secure-oauth2/src/test/java/sample/ApplicationTests.java
@@ -0,0 +1,145 @@
+package sample;
+
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.hateoas.MediaTypes;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.codec.Base64;
+import org.springframework.security.web.FilterChainProxy;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.web.context.WebApplicationContext;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+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.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
+
+/**
+ * Series of automated integration tests to verify proper behavior of auto-configured,
+ * OAuth2-secured system
+ *
+ * @author Greg Turnquist
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@WebAppConfiguration
+@SpringApplicationConfiguration(classes = Application.class)
+@IntegrationTest("server.port:0")
+public class ApplicationTests {
+
+ @Autowired
+ WebApplicationContext context;
+ @Autowired
+ FilterChainProxy filterChain;
+
+ private MockMvc mvc;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @Before
+ public void setUp() {
+
+ this.mvc = webAppContextSetup(this.context).addFilters(this.filterChain).build();
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ public void everythingIsSecuredByDefault() throws Exception {
+
+ this.mvc.perform(get("/").//
+ accept(MediaTypes.HAL_JSON)).// /
+ andExpect(status().isUnauthorized()).//
+ andDo(print());
+
+ this.mvc.perform(get("/flights").//
+ accept(MediaTypes.HAL_JSON)).// /
+ andExpect(status().isUnauthorized()).//
+ andDo(print());
+
+ this.mvc.perform(get("/flights/1").//
+ accept(MediaTypes.HAL_JSON)).// /
+ andExpect(status().isUnauthorized()).//
+ andDo(print());
+
+ this.mvc.perform(get("/alps").//
+ accept(MediaTypes.HAL_JSON)).// /
+ andExpect(status().isUnauthorized()).//
+ andDo(print());
+ }
+
+ @Test
+ @Ignore
+ // TODO: maybe show mixed basic + token auth on different resources?
+ public void accessingRootUriPossibleWithUserAccount() throws Exception {
+
+ this.mvc.perform(
+ get("/").//
+ accept(MediaTypes.HAL_JSON).//
+ header("Authorization",
+ "Basic "
+ + new String(Base64.encode("greg:turnquist"
+ .getBytes()))))
+ .//
+ andExpect(header().string("Content-Type", MediaTypes.HAL_JSON.toString()))
+ .//
+ andExpect(status().isOk()).//
+ andDo(print());
+ }
+
+ @Test
+ public void useAppSecretsPlusUserAccountToGetBearerToken() throws Exception {
+
+ MvcResult result = this.mvc
+ .perform(
+ get("/oauth/token").//
+ header("Authorization",
+ "Basic "
+ + new String(Base64.encode("foo:bar"
+ .getBytes()))).//
+ param("grant_type", "password").//
+ param("scope", "read").//
+ param("username", "greg").//
+ param("password", "turnquist")).//
+ andExpect(status().isOk()).//
+ andDo(print()).//
+ andReturn();
+
+ Object accessToken = this.objectMapper.readValue(
+ result.getResponse().getContentAsString(), Map.class).get("access_token");
+
+ MvcResult flightsAction = this.mvc
+ .perform(get("/flights/1").//
+ accept(MediaTypes.HAL_JSON).//
+ header("Authorization", "Bearer " + accessToken))
+ .//
+ andExpect(header().string("Content-Type", MediaTypes.HAL_JSON.toString()))
+ .//
+ andExpect(status().isOk()).//
+ andDo(print()).//
+ andReturn();
+
+ Flight flight = this.objectMapper.readValue(flightsAction.getResponse()
+ .getContentAsString(), Flight.class);
+
+ assertThat(flight.getOrigin(), is("Nashville"));
+ assertThat(flight.getDestination(), is("Dallas"));
+ assertThat(flight.getAirline(), is("Spring Ways"));
+ assertThat(flight.getFlightNumber(), is("OAUTH2"));
+ assertThat(flight.getTraveler(), is("Greg Turnquist"));
+ }
+
+}