├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── oauth-server
├── README.md
├── src
│ └── main
│ │ ├── resources
│ │ ├── logback.xml
│ │ ├── application.properties
│ │ ├── db
│ │ │ └── migration
│ │ │ │ └── V0__initial_schema_and_data.sql
│ │ └── templates
│ │ │ ├── login.html
│ │ │ ├── index.html
│ │ │ └── clients
│ │ │ └── form.html
│ │ └── java
│ │ └── de
│ │ └── frontierpsychiatrist
│ │ └── example
│ │ └── oauth
│ │ ├── OauthServerMain.java
│ │ ├── editors
│ │ ├── SplitCollectionEditor.java
│ │ └── AuthorityPropertyEditor.java
│ │ ├── domain
│ │ ├── JdbcUserDetailsService.java
│ │ ├── IndexController.java
│ │ └── ClientController.java
│ │ ├── ConvertersConfiguration.java
│ │ ├── SecurityConfiguration.java
│ │ └── OAuthConfiguration.java
└── build.gradle
├── example-clients
├── terminal
│ └── curl-client.sh
└── html
│ └── read-only
│ └── index.html
├── resource-server
├── src
│ └── main
│ │ ├── resources
│ │ ├── logback.xml
│ │ ├── application.properties
│ │ └── db
│ │ │ └── migration
│ │ │ └── V0__initial_schema_and_data.sql
│ │ └── java
│ │ └── de
│ │ └── frontierpsychiatrist
│ │ └── example
│ │ └── oauth
│ │ ├── domain
│ │ ├── TodoRepository.java
│ │ ├── TodoController.java
│ │ └── Todo.java
│ │ ├── OAuthConfiguration.java
│ │ └── ResourceServerMain.java
└── build.gradle
├── oauth-common
├── src
│ └── main
│ │ └── java
│ │ ├── de
│ │ └── frontierpsychiatrist
│ │ │ └── example
│ │ │ └── oauth
│ │ │ └── domain
│ │ │ ├── CredentialsRepository.java
│ │ │ ├── Authority.java
│ │ │ └── Credentials.java
│ │ └── org
│ │ └── hibernate
│ │ └── dialect
│ │ └── SQLiteDialect.java
└── build.gradle
├── LICENSE.txt
├── gradlew.bat
├── README.md
└── gradlew
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'oauth-example'
2 | include 'oauth-server', 'resource-server', 'oauth-common'
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FrontierPsychiatrist/spring-oauth-example/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # IntelliJ
2 | .idea/
3 | *.iml
4 | atlassian-ide-plugin.xml
5 |
6 | # Build
7 | build/
8 | out/
9 | .gradle/
10 |
11 | # SQLlite databases
12 | /*.db
13 |
--------------------------------------------------------------------------------
/oauth-server/README.md:
--------------------------------------------------------------------------------
1 | OAuth Server
2 | ============
3 | This subproject is the OAuth authentication and authorization server.
4 |
5 | The OAuth server allows users to revoke access they granted to clients.
6 |
7 | OAuth admins can edit and add client applications as well.
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Apr 21 15:42:59 EEST 2020
2 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip
3 | distributionBase=GRADLE_USER_HOME
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/example-clients/terminal/curl-client.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Accquire token from the OAuth server with our client credentials
3 | TOKEN=`curl -s -u curl-client:client-secret -X POST localhost:8081/oauth/token\?grant_type=client_credentials | egrep -o '[a-f0-9-]{20,}'`
4 | echo "Got token $TOKEN"
5 | curl localhost:8080/todos -H "Authorization: Bearer $TOKEN"
--------------------------------------------------------------------------------
/oauth-server/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/resource-server/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/oauth-common/src/main/java/de/frontierpsychiatrist/example/oauth/domain/CredentialsRepository.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | /**
6 | * @author Moritz Schulze
7 | */
8 | public interface CredentialsRepository extends JpaRepository {
9 |
10 | Credentials findByName(String name);
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/resource-server/src/main/java/de/frontierpsychiatrist/example/oauth/domain/TodoRepository.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * @author Moritz Schulze
9 | */
10 | public interface TodoRepository extends JpaRepository {
11 |
12 | List findByMessageLike(String message);
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/oauth-common/build.gradle:
--------------------------------------------------------------------------------
1 | bootRepackage {
2 | enabled = false
3 | }
4 |
5 | dependencies {
6 | compile("org.springframework.data:spring-data-jpa") {
7 | exclude group: 'org.aspectj'
8 | exclude group: 'org.springframework'
9 | }
10 | compile("org.springframework.security:spring-security-core") {
11 | exclude group: 'org.springframework'
12 | }
13 | compile("org.hibernate:hibernate-entitymanager")
14 | compile("org.hibernate:hibernate-validator")
15 | }
16 |
--------------------------------------------------------------------------------
/resource-server/build.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | compile("org.springframework.boot:spring-boot-starter-web")
3 | compile("org.springframework.boot:spring-boot-starter-security")
4 | compile("org.springframework.boot:spring-boot-starter-data-jpa")
5 |
6 | //OAuth
7 | compile("org.springframework.security.oauth:spring-security-oauth2")
8 |
9 | //Database driver
10 | compile("org.xerial:sqlite-jdbc")
11 |
12 | compile("org.flywaydb:flyway-core")
13 |
14 | compile("jakarta.xml.bind:jakarta.xml.bind-api:2.3.2")
15 | compile("org.glassfish.jaxb:jaxb-runtime:2.3.2")
16 |
17 | compile project(":oauth-common")
18 | }
19 |
--------------------------------------------------------------------------------
/oauth-server/build.gradle:
--------------------------------------------------------------------------------
1 | dependencies {
2 | compile("org.springframework.boot:spring-boot-starter-web")
3 | compile("org.springframework.boot:spring-boot-starter-security")
4 | compile("org.springframework.boot:spring-boot-starter-data-jpa")
5 | compile("org.springframework.boot:spring-boot-starter-thymeleaf")
6 |
7 | //OAuth
8 | compile("org.springframework.security.oauth:spring-security-oauth2")
9 |
10 | //Database driver
11 | compile("org.xerial:sqlite-jdbc")
12 |
13 | compile("org.flywaydb:flyway-core")
14 |
15 | //GUI
16 | compile("org.webjars:bootstrap:3.2.0")
17 | compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4")
18 |
19 | compile("jakarta.xml.bind:jakarta.xml.bind-api:2.3.2")
20 | compile("org.glassfish.jaxb:jaxb-runtime:2.3.2")
21 |
22 | compile project(":oauth-common")
23 | }
24 |
--------------------------------------------------------------------------------
/resource-server/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.datasource.driverClassName=org.sqlite.JDBC
2 | spring.datasource.url=jdbc:sqlite:resource_db.db
3 | spring.datasource.username=
4 | spring.datasource.password=
5 | # Empty user and password need to be set, otherwise Flyway ignores the URL.
6 | flyway.url=jdbc:sqlite:resource_db.db
7 | flyway.user=
8 | flyway.password=
9 |
10 | # oauth database
11 | spring.datasource_oauth.driverClassName=org.sqlite.JDBC
12 | spring.datasource_oauth.url=jdbc:sqlite:oauth_db.db
13 | spring.datasource_oauth.username=
14 | spring.datasource_oauth.password=
15 |
16 | spring.jpa.database-platform=org.hibernate.dialect.SQLiteDialect
17 | # Create the database tables. Should be switched off later on.
18 | spring.jpa.ddl-create=false
19 | #spring.jpa.hibernate.ddl-auto=create
20 |
21 | # Needed so the cors filter comes before the security filter and responses rejected
22 | # by security still have the CORS headers.
23 | security.filter-order=5
--------------------------------------------------------------------------------
/oauth-common/src/main/java/de/frontierpsychiatrist/example/oauth/domain/Authority.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import org.springframework.security.core.GrantedAuthority;
4 |
5 | import javax.persistence.Entity;
6 | import javax.persistence.GeneratedValue;
7 | import javax.persistence.GenerationType;
8 | import javax.persistence.Id;
9 |
10 | /**
11 | * @author Moritz Schulze
12 | */
13 | @Entity
14 | public class Authority implements GrantedAuthority {
15 |
16 | @Id
17 | @GeneratedValue(strategy = GenerationType.AUTO)
18 | private Long id;
19 |
20 | private String authority;
21 |
22 | public Long getId() {
23 | return id;
24 | }
25 |
26 | public void setId(Long id) {
27 | this.id = id;
28 | }
29 |
30 | @Override
31 | public String getAuthority() {
32 | return authority;
33 | }
34 |
35 | public void setAuthority(String authority) {
36 | this.authority = authority;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/resource-server/src/main/java/de/frontierpsychiatrist/example/oauth/domain/TodoController.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import org.springframework.web.bind.annotation.*;
4 |
5 | import java.util.List;
6 |
7 | /**
8 | * @author Moritz Schulze
9 | */
10 | @RestController
11 | @RequestMapping(value = "/todos")
12 | public class TodoController {
13 |
14 | private final TodoRepository todoRepository;
15 |
16 | public TodoController(TodoRepository todoRepository) {
17 | this.todoRepository = todoRepository;
18 | }
19 |
20 | @GetMapping
21 | public List todos() {
22 | return todoRepository.findAll();
23 | }
24 |
25 | @GetMapping("/{id}")
26 | public Todo oneTodo(@PathVariable("id") Long id) {
27 | return todoRepository.findOne(id);
28 | }
29 |
30 | @GetMapping("/search/findByMessageLike")
31 | public List findByMessageLike(@RequestParam("message") String message) {
32 | return todoRepository.findByMessageLike("%" + message + "%");
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/oauth-server/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | # database with the credentials
2 | spring.datasource.driverClassName=org.sqlite.JDBC
3 | spring.datasource.url=jdbc:sqlite:resource_db.db
4 | spring.datasource.username=
5 | spring.datasource.password=
6 | spring.datasource.type=org.springframework.jdbc.datasource.SingleConnectionDataSource
7 | spring.jpa.database-platform=org.hibernate.dialect.SQLiteDialect
8 | # Create the database tables. Should be switched off later on.
9 | spring.jpa.ddl-create=false
10 |
11 |
12 | # oauth database
13 | spring.datasource_oauth.driverClassName=org.sqlite.JDBC
14 | spring.datasource_oauth.url=jdbc:sqlite:oauth_db.db
15 | spring.datasource_oauth.username=
16 | spring.datasource_oauth.password=
17 | spring.datasource_oauth.type=org.springframework.jdbc.datasource.SingleConnectionDataSource
18 | # Empty user and password need to be set, otherwise Flyway ignores the URL.
19 | flyway.url=jdbc:sqlite:oauth_db.db
20 | flyway.user=
21 | flyway.password=
22 |
23 | # Server
24 | server.port=8081
25 |
26 | # thymeleaf
27 | spring.thymeleaf.cache=false
28 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Moritz do Rio Schulze
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/OauthServerMain.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
6 | import org.springframework.boot.context.properties.ConfigurationProperties;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Primary;
9 |
10 | import javax.sql.DataSource;
11 |
12 | /**
13 | * @author Moritz Schulze
14 | */
15 | @SpringBootApplication
16 | public class OauthServerMain {
17 |
18 | /**
19 | * Main data source containing the credentials.
20 | * In this is example this is the DB from the resource server.
21 | */
22 | @Bean
23 | @Primary
24 | @ConfigurationProperties(prefix = "spring.datasource")
25 | public DataSource mainDataSource() {
26 | return DataSourceBuilder.create().build();
27 | }
28 |
29 | public static void main(String[] args) {
30 | SpringApplication.run(OauthServerMain.class, args);
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/editors/SplitCollectionEditor.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.editors;
2 |
3 | import org.springframework.beans.propertyeditors.CustomCollectionEditor;
4 |
5 | import java.util.Collection;
6 |
7 | /**
8 | * Creates collections from a string.
9 | * If the string is empty or null, return an empty collection. Otherwise split by the given splitRegex and use the array.
10 | *
11 | * @author Moritz Schulze
12 | */
13 | public class SplitCollectionEditor extends CustomCollectionEditor {
14 |
15 | private final Class extends Collection> collectionType;
16 | private final String splitRegex;
17 |
18 | public SplitCollectionEditor(Class extends Collection> collectionType, String splitRegex) {
19 | super(collectionType, true);
20 | this.collectionType = collectionType;
21 | this.splitRegex = splitRegex;
22 | }
23 |
24 | @Override
25 | public void setAsText(String text) throws IllegalArgumentException {
26 | if (text == null || text.isEmpty()) {
27 | super.setValue(super.createCollection(this.collectionType, 0));
28 | } else {
29 | super.setValue(text.split(splitRegex));
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/domain/JdbcUserDetailsService.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import org.springframework.security.core.userdetails.User;
4 | import org.springframework.security.core.userdetails.UserDetails;
5 | import org.springframework.security.core.userdetails.UserDetailsService;
6 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
7 |
8 | /**
9 | * @author Moritz Schulze
10 | */
11 | public class JdbcUserDetailsService implements UserDetailsService {
12 |
13 | private final CredentialsRepository credentialsRepository;
14 |
15 | public JdbcUserDetailsService(CredentialsRepository credentialsRepository) {
16 | this.credentialsRepository = credentialsRepository;
17 | }
18 |
19 | @Override
20 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
21 | Credentials credentials = credentialsRepository.findByName(username);
22 | if(credentials == null) {
23 | throw new UsernameNotFoundException("User " + username + " not found in database.");
24 | }
25 | return new User(credentials.getName(), credentials.getPassword(), credentials.isEnabled(), true, true, true, credentials.getAuthorities());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/resource-server/src/main/resources/db/migration/V0__initial_schema_and_data.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE authority (
2 | id integer,
3 | authority varchar(255),
4 | primary key (id)
5 | );
6 |
7 | CREATE TABLE credentials (
8 | id integer,
9 | enabled boolean not null,
10 | name varchar(255) not null,
11 | password varchar(255) not null,
12 | version integer,
13 | primary key (id)
14 | );
15 |
16 | CREATE TABLE credentials_authorities (
17 | credentials_id bigint not null,
18 | authorities_id bigint not null
19 | );
20 |
21 | CREATE TABLE todo (
22 | id integer,
23 | version integer,
24 | done boolean,
25 | done_time timestamp,
26 | message varchar(255) not null
27 | );
28 |
29 | INSERT INTO "authority" VALUES(0,'ROLE_OAUTH_ADMIN');
30 | INSERT INTO "authority" VALUES(1,'ROLE_ADMIN');
31 | INSERT INTO "authority" VALUES(2,'ROLE_USER');
32 | INSERT INTO "credentials" VALUES(0,1,'oauth_admin','admin',0);
33 | INSERT INTO "credentials" VALUES(1,1,'resource_admin','admin',0);
34 | INSERT INTO "credentials" VALUES(2,1,'user','user',0);
35 | INSERT INTO "credentials_authorities" VALUES(0,0);
36 | INSERT INTO "credentials_authorities" VALUES(1,1);
37 | INSERT INTO "credentials_authorities" VALUES(2,2);
38 | INSERT INTO "todo" (id, done, done_time, message, version) VALUES (1, 0, null, 'Write an oauth example application.', 0);
39 | INSERT INTO "todo" (id, done, done_time, message, version) VALUES (2, 1, '1403947411000', 'Do grocery shopping.', 0);
40 |
--------------------------------------------------------------------------------
/resource-server/src/main/java/de/frontierpsychiatrist/example/oauth/domain/Todo.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import org.hibernate.validator.constraints.NotEmpty;
4 |
5 | import javax.persistence.*;
6 | import java.util.Date;
7 |
8 | /**
9 | * @author Moritz Schulze
10 | */
11 | @Entity
12 | public class Todo {
13 |
14 | @Id
15 | @GeneratedValue(strategy = GenerationType.AUTO)
16 | private Long id;
17 |
18 | @Version
19 | private Integer version;
20 |
21 | @NotEmpty
22 | private String message;
23 |
24 | private boolean done;
25 |
26 | private Date doneTime;
27 |
28 | public Long getId() {
29 | return id;
30 | }
31 |
32 | public void setId(Long id) {
33 | this.id = id;
34 | }
35 |
36 | public Integer getVersion() {
37 | return version;
38 | }
39 |
40 | public void setVersion(Integer version) {
41 | this.version = version;
42 | }
43 |
44 | public String getMessage() {
45 | return message;
46 | }
47 |
48 | public void setMessage(String message) {
49 | this.message = message;
50 | }
51 |
52 | public boolean isDone() {
53 | return done;
54 | }
55 |
56 | public void setDone(boolean done) {
57 | this.done = done;
58 | }
59 |
60 | public Date getDoneTime() {
61 | return doneTime;
62 | }
63 |
64 | public void setDoneTime(Date doneTime) {
65 | this.doneTime = doneTime;
66 | }
67 | }
68 |
69 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/ConvertersConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.core.convert.converter.Converter;
6 |
7 | import java.text.ParseException;
8 | import java.text.SimpleDateFormat;
9 | import java.util.Date;
10 |
11 | /**
12 | * @author Moritz Schulze
13 | */
14 | @Configuration
15 | public class ConvertersConfiguration {
16 |
17 | /**
18 | * Converter for the format yyyy-MM-dd HH:mm:ss
19 | *
20 | * Currently needed for the approval revoke form that needs to bind the expiresAt and lastUpdatedAt
21 | * dates of an approval.
22 | */
23 | @Bean
24 | public Converter stringDateConverter() {
25 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
26 | // We can not use a lambda here since Spring can't detect the generic types that way.
27 | return new Converter() {
28 | @Override
29 | public Date convert(String source) {
30 | if (source == null) {
31 | throw new IllegalArgumentException("Date string may not be null");
32 | }
33 | try {
34 | return sdf.parse(source);
35 | } catch (ParseException e) {
36 | throw new IllegalArgumentException(e);
37 | }
38 | }
39 | };
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/oauth-common/src/main/java/de/frontierpsychiatrist/example/oauth/domain/Credentials.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import org.hibernate.validator.constraints.NotEmpty;
4 |
5 | import javax.persistence.*;
6 | import java.util.List;
7 |
8 | /**
9 | * @author Moritz Schulze
10 | */
11 | @Entity
12 | public class Credentials {
13 |
14 | @Id
15 | @GeneratedValue(strategy = GenerationType.AUTO)
16 | private Long id;
17 |
18 | @Version
19 | private Integer version;
20 |
21 | @NotEmpty
22 | private String name;
23 |
24 | @NotEmpty
25 | private String password;
26 |
27 | @ManyToMany(fetch = FetchType.EAGER)
28 | private List authorities;
29 |
30 | private boolean enabled;
31 |
32 | public Long getId() {
33 | return id;
34 | }
35 |
36 | public void setId(Long id) {
37 | this.id = id;
38 | }
39 |
40 | public Integer getVersion() {
41 | return version;
42 | }
43 |
44 | public void setVersion(Integer version) {
45 | this.version = version;
46 | }
47 |
48 | public String getName() {
49 | return name;
50 | }
51 |
52 | public void setName(String name) {
53 | this.name = name;
54 | }
55 |
56 | public String getPassword() {
57 | return password;
58 | }
59 |
60 | public void setPassword(String password) {
61 | this.password = password;
62 | }
63 |
64 | public List getAuthorities() {
65 | return authorities;
66 | }
67 |
68 | public void setAuthorities(List authorities) {
69 | this.authorities = authorities;
70 | }
71 |
72 | public boolean isEnabled() {
73 | return enabled;
74 | }
75 |
76 | public void setEnabled(boolean enabled) {
77 | this.enabled = enabled;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/SecurityConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth;
2 |
3 | import de.frontierpsychiatrist.example.oauth.domain.CredentialsRepository;
4 | import de.frontierpsychiatrist.example.oauth.domain.JdbcUserDetailsService;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8 | import org.springframework.security.config.annotation.web.builders.WebSecurity;
9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
11 | import org.springframework.security.core.userdetails.UserDetailsService;
12 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
13 |
14 | /**
15 | * @author Moritz Schulze
16 | */
17 | @Configuration
18 | @EnableWebSecurity
19 | public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
20 |
21 | @Bean
22 | public UserDetailsService userDetailsService(CredentialsRepository credentialsRepository) {
23 | return new JdbcUserDetailsService(credentialsRepository);
24 | }
25 |
26 | @Override
27 | public void configure(WebSecurity web) throws Exception {
28 | web.ignoring().antMatchers("/webjars/**");
29 | }
30 |
31 | @Override
32 | protected void configure(HttpSecurity http) throws Exception {
33 | http
34 | .authorizeRequests()
35 | .antMatchers("/login", "/logout.do").permitAll()
36 | .antMatchers("/**").authenticated()
37 | .and()
38 | .formLogin()
39 | .loginProcessingUrl("/login.do")
40 | .usernameParameter("name")
41 | .loginPage("/login")
42 | .and()
43 | .logout()
44 | //To match GET requests we have to use a request matcher.
45 | .logoutRequestMatcher(new AntPathRequestMatcher("/logout.do"));
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/oauth-server/src/main/resources/db/migration/V0__initial_schema_and_data.sql:
--------------------------------------------------------------------------------
1 | create table oauth_client_details (
2 | client_id VARCHAR(256) PRIMARY KEY,
3 | resource_ids VARCHAR(256),
4 | client_secret VARCHAR(256),
5 | scope VARCHAR(256),
6 | authorized_grant_types VARCHAR(256),
7 | web_server_redirect_uri VARCHAR(256),
8 | authorities VARCHAR(256),
9 | access_token_validity INTEGER,
10 | refresh_token_validity INTEGER,
11 | additional_information VARCHAR(4096),
12 | autoapprove VARCHAR(256)
13 | );
14 |
15 | create table oauth_client_token (
16 | token_id VARCHAR(256),
17 | token LONGVARBINARY,
18 | authentication_id VARCHAR(256),
19 | user_name VARCHAR(256),
20 | client_id VARCHAR(256)
21 | );
22 |
23 | create table oauth_access_token (
24 | token_id VARCHAR(256),
25 | token LONGVARBINARY,
26 | authentication_id VARCHAR(256),
27 | user_name VARCHAR(256),
28 | client_id VARCHAR(256),
29 | authentication LONGVARBINARY,
30 | refresh_token VARCHAR(256)
31 | );
32 |
33 | create table oauth_refresh_token (
34 | token_id VARCHAR(256),
35 | token LONGVARBINARY,
36 | authentication LONGVARBINARY
37 | );
38 |
39 | create table oauth_code (
40 | code VARCHAR(256), authentication LONGVARBINARY
41 | );
42 |
43 | create table oauth_approvals (
44 | userId VARCHAR(256),
45 | clientId VARCHAR(256),
46 | scope VARCHAR(256),
47 | status VARCHAR(10),
48 | expiresAt TIMESTAMP,
49 | lastModifiedAt TIMESTAMP
50 | );
51 |
52 | INSERT INTO oauth_client_details
53 | (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove)
54 | VALUES
55 | ('read-only-client', 'todo-services', null, 'read', 'implicit', 'http://localhost,http://localhost:9090', NULL, 7200, 0, NULL, 'false');
56 |
57 | INSERT INTO oauth_client_details
58 | (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove)
59 | VALUES
60 | ('curl-client', 'todo-services', 'client-secret', 'read,write', 'client_credentials', '', 'ROLE_ADMIN', 7200, 0, NULL, 'false');
61 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/editors/AuthorityPropertyEditor.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.editors;
2 |
3 | import org.springframework.security.core.GrantedAuthority;
4 | import org.springframework.security.core.authority.SimpleGrantedAuthority;
5 |
6 | import java.awt.*;
7 | import java.beans.PropertyChangeListener;
8 | import java.beans.PropertyEditor;
9 |
10 | /**
11 | * Used to bind Strings to a {@link org.springframework.security.core.GrantedAuthority} when adding/editing a client.
12 | *
13 | * Only implements {@link #getAsText()} and {@link #setAsText(String)}.
14 | *
15 | * @author Moritz Schulze
16 | */
17 | public class AuthorityPropertyEditor implements PropertyEditor {
18 | private GrantedAuthority grantedAuthority;
19 |
20 | @Override
21 | public void setValue(Object value) {
22 | this.grantedAuthority = (GrantedAuthority) value;
23 | }
24 |
25 | @Override
26 | public Object getValue() {
27 | return grantedAuthority;
28 | }
29 |
30 | @Override
31 | public boolean isPaintable() {
32 | return false;
33 | }
34 |
35 | @Override
36 | public void paintValue(Graphics gfx, Rectangle box) {
37 |
38 | }
39 |
40 | @Override
41 | public String getJavaInitializationString() {
42 | return null;
43 | }
44 |
45 | @Override
46 | public String getAsText() {
47 | return grantedAuthority.getAuthority();
48 | }
49 |
50 | @Override
51 | public void setAsText(String text) throws IllegalArgumentException {
52 | if (text != null && !text.isEmpty()) {
53 | this.grantedAuthority = new SimpleGrantedAuthority(text);
54 | }
55 | }
56 |
57 | @Override
58 | public String[] getTags() {
59 | return new String[0];
60 | }
61 |
62 | @Override
63 | public Component getCustomEditor() {
64 | return null;
65 | }
66 |
67 | @Override
68 | public boolean supportsCustomEditor() {
69 | return false;
70 | }
71 |
72 | @Override
73 | public void addPropertyChangeListener(PropertyChangeListener listener) {
74 |
75 | }
76 |
77 | @Override
78 | public void removePropertyChangeListener(PropertyChangeListener listener) {
79 |
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/resource-server/src/main/java/de/frontierpsychiatrist/example/oauth/OAuthConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth;
2 |
3 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
4 | import org.springframework.boot.context.properties.ConfigurationProperties;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.http.HttpMethod;
8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
9 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
10 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
11 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
12 | import org.springframework.security.oauth2.provider.token.TokenStore;
13 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
14 |
15 | import javax.sql.DataSource;
16 |
17 | /**
18 | * @author Moritz Schulze
19 | */
20 | @Configuration
21 | @EnableResourceServer
22 | public class OAuthConfiguration extends ResourceServerConfigurerAdapter {
23 |
24 | @Bean
25 | @ConfigurationProperties(prefix = "spring.datasource_oauth")
26 | public DataSource oauthDataSource() {
27 | return DataSourceBuilder.create().build();
28 | }
29 |
30 | @Override
31 | public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
32 | TokenStore tokenStore = new JdbcTokenStore(oauthDataSource());
33 | resources.resourceId("todo-services")
34 | .tokenStore(tokenStore);
35 | }
36 |
37 | @Override
38 | public void configure(HttpSecurity http) throws Exception {
39 | http
40 | .authorizeRequests()
41 | .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
42 | .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
43 | .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
44 | .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
45 | .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
46 | .antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/resource-server/src/main/java/de/frontierpsychiatrist/example/oauth/ResourceServerMain.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
6 | import org.springframework.boot.context.properties.ConfigurationProperties;
7 | import org.springframework.boot.web.servlet.FilterRegistrationBean;
8 | import org.springframework.context.annotation.Bean;
9 | import org.springframework.context.annotation.Primary;
10 | import org.springframework.web.cors.CorsConfiguration;
11 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
12 | import org.springframework.web.filter.CorsFilter;
13 |
14 | import javax.sql.DataSource;
15 | import java.util.Collections;
16 |
17 | /**
18 | * @author Moritz Schulze
19 | */
20 | @SpringBootApplication
21 | public class ResourceServerMain {
22 |
23 | /**
24 | * This special filter is needed so unauthorized request that are rejected by Spring security
25 | * still have CORS headers.
26 | * For some reason he {@code bean.setOrder} call is not enough, the configuration also needs
27 | * {@code security.filter-order=5} for the CORS filter to be in front of the Spring Security
28 | * filter.
29 | */
30 | @Bean
31 | public FilterRegistrationBean corsFilter() {
32 | //based on https://github.com/spring-projects/spring-boot/issues/5834
33 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
34 | CorsConfiguration config = new CorsConfiguration();
35 | config.setAllowCredentials(true);
36 | config.setAllowedOrigins(Collections.singletonList("*"));
37 | config.setAllowedMethods(Collections.singletonList("*"));
38 | config.setAllowedHeaders(Collections.singletonList("*"));
39 | source.registerCorsConfiguration("/**", config);
40 | FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
41 | bean.setOrder(0);
42 | return bean;
43 | }
44 |
45 | /**
46 | * Main data source containing the credentials.
47 | * In this is example this is the DB from the resource server.
48 | */
49 | @Bean
50 | @Primary
51 | @ConfigurationProperties(prefix = "spring.datasource")
52 | public DataSource mainDataSource() {
53 | return DataSourceBuilder.create().build();
54 | }
55 |
56 | public static void main(String[] args) {
57 | SpringApplication.run(ResourceServerMain.class, args);
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/domain/IndexController.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import org.springframework.security.oauth2.provider.approval.Approval;
4 | import org.springframework.security.oauth2.provider.approval.ApprovalStore;
5 | import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
6 | import org.springframework.security.oauth2.provider.token.TokenStore;
7 | import org.springframework.stereotype.Controller;
8 | import org.springframework.web.bind.annotation.GetMapping;
9 | import org.springframework.web.bind.annotation.ModelAttribute;
10 | import org.springframework.web.bind.annotation.PostMapping;
11 | import org.springframework.web.servlet.ModelAndView;
12 |
13 | import java.security.Principal;
14 | import java.util.Collection;
15 | import java.util.List;
16 | import java.util.Map;
17 | import java.util.stream.Collectors;
18 |
19 | import static java.util.Arrays.asList;
20 |
21 | /**
22 | * @author Moritz Schulze
23 | */
24 | @Controller
25 | public class IndexController {
26 |
27 | private final JdbcClientDetailsService clientDetailsService;
28 | private final ApprovalStore approvalStore;
29 | private final TokenStore tokenStore;
30 |
31 | public IndexController(JdbcClientDetailsService clientDetailsService, ApprovalStore approvalStore, TokenStore tokenStore) {
32 | this.clientDetailsService = clientDetailsService;
33 | this.approvalStore = approvalStore;
34 | this.tokenStore = tokenStore;
35 | }
36 |
37 | @GetMapping("/")
38 | public ModelAndView root(Map model, Principal principal) {
39 | List approvals = clientDetailsService.listClientDetails().stream()
40 | .map(clientDetail -> approvalStore.getApprovals(principal.getName(), clientDetail.getClientId()))
41 | .flatMap(Collection::stream)
42 | .collect(Collectors.toList());
43 | model.put("approvals", approvals);
44 | model.put("clientDetails", clientDetailsService.listClientDetails());
45 | return new ModelAndView("index", model);
46 | }
47 |
48 | @PostMapping(value = "/approval/revoke")
49 | public String revokeApproval(@ModelAttribute Approval approval) {
50 | approvalStore.revokeApprovals(asList(approval));
51 | tokenStore
52 | .findTokensByClientIdAndUserName(approval.getClientId(), approval.getUserId())
53 | .forEach(tokenStore::removeAccessToken);
54 | return "redirect:/";
55 | }
56 |
57 | @GetMapping("/login")
58 | public String loginPage() {
59 | return "login";
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/oauth-server/src/main/resources/templates/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | OAuth Server Login
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Login
14 |
15 |
16 |
Error logging in.
17 |
18 |
19 |
20 |
You have been logged out.
21 |
22 |
23 |
24 | Welcome to the OAuth control server. This server generates access tokens for clients of the resource server.
25 | For users it acts as a portal, think of it as the Google/Facebook/Twitter login.
26 |
27 | You can log in with the following users:
28 |
29 |
oauth_admin, password admin. An OAuth admin that can add new client applications.
30 |
resource_admin, password admin. A user with an admin role on our resource server.
31 |
user, password user. A standard user for the resource server.
32 |
33 |
34 |
35 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/OAuthConfiguration.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth;
2 |
3 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
4 | import org.springframework.boot.context.properties.ConfigurationProperties;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
8 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
9 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
10 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
11 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
12 | import org.springframework.security.oauth2.provider.approval.ApprovalStore;
13 | import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
14 | import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
15 | import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
16 | import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
17 | import org.springframework.security.oauth2.provider.token.TokenStore;
18 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
19 |
20 | import javax.sql.DataSource;
21 |
22 | /**
23 | * @author Moritz Schulze
24 | */
25 | @Configuration
26 | @EnableAuthorizationServer
27 | public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {
28 |
29 | @Bean
30 | @ConfigurationProperties(prefix = "spring.datasource_oauth")
31 | public DataSource oauthDataSource() {
32 | return DataSourceBuilder.create().build();
33 | }
34 |
35 | /**
36 | * We expose the JdbcClientDetailsService because it has extra methods that the Interface does not have. E.g.
37 | * {@link org.springframework.security.oauth2.provider.client.JdbcClientDetailsService#listClientDetails()} which we need for the
38 | * admin page.
39 | */
40 | @Bean
41 | public JdbcClientDetailsService clientDetailsService() {
42 | return new JdbcClientDetailsService(oauthDataSource());
43 | }
44 |
45 | @Bean
46 | public TokenStore tokenStore() {
47 | return new JdbcTokenStore(oauthDataSource());
48 | }
49 |
50 | @Bean
51 | public ApprovalStore approvalStore() {
52 | return new JdbcApprovalStore(oauthDataSource());
53 | }
54 |
55 | @Bean
56 | public AuthorizationCodeServices authorizationCodeServices() {
57 | return new JdbcAuthorizationCodeServices(oauthDataSource());
58 | }
59 |
60 | @Override
61 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
62 | clients.withClientDetails(clientDetailsService());
63 | }
64 |
65 | @Override
66 | public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
67 |
68 | }
69 |
70 | @Override
71 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
72 | endpoints
73 | .approvalStore(approvalStore())
74 | .authorizationCodeServices(authorizationCodeServices())
75 | .tokenStore(tokenStore());
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/oauth-server/src/main/java/de/frontierpsychiatrist/example/oauth/domain/ClientController.java:
--------------------------------------------------------------------------------
1 | package de.frontierpsychiatrist.example.oauth.domain;
2 |
3 | import de.frontierpsychiatrist.example.oauth.editors.AuthorityPropertyEditor;
4 | import de.frontierpsychiatrist.example.oauth.editors.SplitCollectionEditor;
5 | import org.springframework.security.access.prepost.PreAuthorize;
6 | import org.springframework.security.core.GrantedAuthority;
7 | import org.springframework.security.oauth2.provider.ClientDetails;
8 | import org.springframework.security.oauth2.provider.client.BaseClientDetails;
9 | import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
10 | import org.springframework.stereotype.Controller;
11 | import org.springframework.ui.Model;
12 | import org.springframework.web.bind.WebDataBinder;
13 | import org.springframework.web.bind.annotation.*;
14 |
15 | import java.util.Collection;
16 | import java.util.Set;
17 |
18 | /**
19 | * @author Moritz Schulze
20 | */
21 | @Controller
22 | @RequestMapping("/clients")
23 | public class ClientController {
24 |
25 | private final JdbcClientDetailsService clientDetailsService;
26 |
27 | public ClientController(JdbcClientDetailsService clientDetailsService) {
28 | this.clientDetailsService = clientDetailsService;
29 | }
30 |
31 | @InitBinder
32 | public void initBinder(WebDataBinder binder) {
33 | // This is mainly needed for the GrantedAuthority array. If we don't use this editor no authorities
34 | // will get bound to [null] instead of [].
35 | binder.registerCustomEditor(Collection.class, new SplitCollectionEditor(Set.class, ","));
36 | // To convert and display roles as strings we use this editor.
37 | binder.registerCustomEditor(GrantedAuthority.class, new AuthorityPropertyEditor());
38 | }
39 |
40 | /**
41 | * Display an edit/create form for a client.
42 | * @param clientId The id of the client to display. If null a create form will be displayed.
43 | * @param model The Spring MVC model.
44 | * @return clients/form view
45 | */
46 | @GetMapping(value = "/form")
47 | @PreAuthorize("hasRole('ROLE_OAUTH_ADMIN')")
48 | public String showEditOrAddForm(@RequestParam(value = "client", required = false) String clientId, Model model) {
49 | ClientDetails clientDetails;
50 | if(clientId != null) {
51 | clientDetails = clientDetailsService.loadClientByClientId(clientId);
52 | } else {
53 | clientDetails = new BaseClientDetails();
54 | }
55 | model.addAttribute("clientDetails", clientDetails);
56 | return "clients/form";
57 | }
58 |
59 | /**
60 | * Create/update a client from the form.
61 | * @param clientDetails The model to create/update.
62 | * @param newClient Indicates if this is a new client. If null it's an existing client.
63 | * @return redirects to the root.
64 | */
65 | @PostMapping(value = "/edit")
66 | @PreAuthorize("hasRole('ROLE_OAUTH_ADMIN')")
67 | public String editClient(
68 | @ModelAttribute BaseClientDetails clientDetails,
69 | @RequestParam(value = "newClient", required = false) String newClient
70 | ) {
71 | if (newClient == null) {
72 | //does not update the secret!
73 | // TODO: delete tokens and approvals
74 | clientDetailsService.updateClientDetails(clientDetails);
75 | } else {
76 | clientDetailsService.addClientDetails(clientDetails);
77 | }
78 |
79 | // If the user has entered a secret in the form update it.
80 | if (!clientDetails.getClientSecret().isEmpty()) {
81 | clientDetailsService.updateClientSecret(clientDetails.getClientId(), clientDetails.getClientSecret());
82 | }
83 | return "redirect:/";
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Spring Boot OAuth Authorization & Resource server
2 | =================================================
3 | I present to you an example on how to use Spring Boot together with Spring Security OAuth2 to implement an authorization server and
4 | a resource server.
5 |
6 | Also included are some example client applications for the resource server.
7 |
8 | It's a pretty modern application, using Spring Boot, gradle, thymeleaf and only JavaConfig. In my opinion it's also a good example of how Java applications
9 | aren't big bloated "enterprisy" things anymore. The current sloccount is 519. 178 of that are just the SQLite dialect for hibernate which
10 | I had to include because it's not in the official packages.
11 |
12 | Just tell me how to run it
13 | --------------------------
14 | * Clone the repository
15 | * If you have gradle installed, run
16 |
17 | gradle build
18 |
19 | in the main directory. Otherwise run
20 |
21 | ./gradlew build
22 |
23 | It will download a local gradle. On Windows use `gradlew.bat`.
24 | * Start the authorization server with
25 |
26 | java -jar oauth-server/build/libs/oauth-server.jar
27 |
28 | And the resource server with
29 |
30 | java -jar resource-server/build/libs/resource-server.jar
31 |
32 | The authorization server runs under [http://localhost:8081](http://localhost:8081) and the resource server under
33 | [http://localhost:8080](http://localhost:8080).
34 | * Additionally you can start a http server in example-clients/html, e.g. like this
35 |
36 | cd example-clients/html/read-only
37 | ruby -run -e httpd . -p 9090
38 |
39 | It will be reachable under [http://localhost:9090](http://localhost:9090).
40 |
41 | Starting from within a IDE
42 | --------------------------
43 | If you want to play around with the java code it's more practicable to start from within your IDE. Just run either `OAuthServerMain` or
44 | `ResourceServerMain`. The working directory to execute in should be the directory in which you cloned into because the database files are
45 | expected there.
46 |
47 | What to do when it is running?
48 | ------------------------------
49 | The OAuth server is fairly self explanatory. Just open [http://localhost:8081](http://localhost:8081) in a browser. You can login as
50 | * an OAuth admin to administrate clients
51 | * an resource admin or normal user to see what clients you have granted access.
52 | The login credentials should be displayed on the login page.
53 |
54 | The URL to get a new access token for a client is
55 |
56 | http://localhost:8081/oauth/authorize?client_id=$client_id&return_type=token&redirect_uri=some_uri
57 |
58 | If the call to this URL is valid and you are logged in it will redirect to `some_uri` with an access token attached to the location hash. If
59 | you want to call this with cURL you have to set the cookie header to include the session id.
60 |
61 | curl .../oauth/authorize?... -H "Cookie: JSESSIONID=..."
62 |
63 | which you can find in your browser development console.
64 |
65 | The resource server exposes a (very simple) REST API. You can use the example clients to access them or cURL after receiving an access token.
66 |
67 | curl -v localhost:8080/todos -H "Authorization: Bearer $token"
68 | curl -v localhost:8080/todos/1 -H "Authorization: Bearer $token"
69 | curl -v -X DELETE localhost:8080/todos/1 -H "Authorization: Bearer $token"
70 | curl -v -X POST localhost:8080/tokens localhost:8080/todos/1 -H "Authorization: Bearer $token" -d "{ \"message\": \"Do stuff\", \"done\": false }"
71 |
72 | Why?
73 | ----
74 | I wrote this because I had to get into OAuth with Spring and found it actually quite hard to find good examples and documentation. I hope
75 | others can learn from this.
76 |
77 | Caveats & Disclaimer
78 | --------------------
79 | I am not a security expert, far from it. I implemented this with my best knowledge on OAuth and Spring Security but I take no guarantee
80 | that it is usable in a productive application.
81 |
82 | I used sqlite for the database because most people will have sqlite on their system and can easily look into the database like this.
83 |
84 | It goes without saying that in any production environment all HTTP traffic must be HTTPS, otherwise your tokens and client secrets are sniffable.
85 |
86 | License
87 | -------
88 | See LICENSE.txt
89 |
--------------------------------------------------------------------------------
/oauth-server/src/main/resources/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | OAuth Server Index
6 |
7 |
8 |
9 |
10 |
OAuth Server
11 |
12 |
13 | Logged in as:
14 |
15 |
16 |
17 |
18 |
19 |
Approvals
20 |
21 |
22 | If you revoke the approval for one scope of a client all tokens for that client will be removed as well.
23 |