├── 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 collectionType; 16 | private final String splitRegex; 17 | 18 | public SplitCollectionEditor(Class 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 |
36 |
37 | 38 | 39 |
40 |
41 | 42 | 43 |
44 | 45 | 46 |
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 |

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 49 | 50 |
ClientScopeExpires atActions
36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 |
48 |
51 |
52 |
53 |
54 |
55 |

Clients

56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 79 | 80 | 81 | 86 | 87 |
Client IDResource IDsScopesGrant TypesRolesActions
72 | 73 | 74 | 75 | 76 | 77 | 78 |
82 | 83 | 84 | 85 |
88 |
89 |
90 |
91 | 92 | -------------------------------------------------------------------------------- /example-clients/html/read-only/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Read only client 6 | 7 | 8 | 9 | 76 | 77 | 78 |
79 |

Read only client

80 |
81 |
82 | 88 |
89 | 90 |
91 | 94 |
95 |
96 | 97 |
98 |
99 |
100 |
101 |
    102 |
  • 103 | {{todo.message}} 104 | 105 |
  • 106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /oauth-common/src/main/java/org/hibernate/dialect/SQLiteDialect.java: -------------------------------------------------------------------------------- 1 | package org.hibernate.dialect;/* 2 | * The author disclaims copyright to this source code. In place of 3 | * a legal notice, here is a blessing: 4 | * 5 | * May you do good and not evil. 6 | * May you find forgiveness for yourself and forgive others. 7 | * May you share freely, never taking more than you give. 8 | * 9 | */ 10 | 11 | import org.hibernate.dialect.function.*; 12 | import org.hibernate.type.StandardBasicTypes; 13 | 14 | import java.sql.Types; 15 | 16 | public class SQLiteDialect extends Dialect { 17 | public SQLiteDialect() { 18 | registerColumnType(Types.BIT, "boolean"); 19 | registerColumnType(Types.TINYINT, "tinyint"); 20 | registerColumnType(Types.SMALLINT, "smallint"); 21 | registerColumnType(Types.INTEGER, "integer"); 22 | registerColumnType(Types.BIGINT, "bigint"); 23 | registerColumnType(Types.FLOAT, "float"); 24 | registerColumnType(Types.REAL, "real"); 25 | registerColumnType(Types.DOUBLE, "double"); 26 | registerColumnType(Types.NUMERIC, "numeric($p, $s)"); 27 | registerColumnType(Types.DECIMAL, "decimal"); 28 | registerColumnType(Types.CHAR, "char"); 29 | registerColumnType(Types.VARCHAR, "varchar($l)"); 30 | registerColumnType(Types.LONGVARCHAR, "longvarchar"); 31 | registerColumnType(Types.DATE, "date"); 32 | registerColumnType(Types.TIME, "time"); 33 | registerColumnType(Types.TIMESTAMP, "datetime"); 34 | registerColumnType(Types.BINARY, "blob"); 35 | registerColumnType(Types.VARBINARY, "blob"); 36 | registerColumnType(Types.LONGVARBINARY, "blob"); 37 | registerColumnType(Types.BLOB, "blob"); 38 | registerColumnType(Types.CLOB, "clob"); 39 | registerColumnType(Types.BOOLEAN, "boolean"); 40 | 41 | registerFunction( "concat", new VarArgsSQLFunction(StandardBasicTypes.STRING, "", "||", "") ); 42 | registerFunction( "mod", new SQLFunctionTemplate(StandardBasicTypes.INTEGER, "?1 % ?2" ) ); 43 | registerFunction( "quote", new StandardSQLFunction("quote", StandardBasicTypes.STRING) ); 44 | registerFunction( "random", new NoArgSQLFunction("random", StandardBasicTypes.INTEGER) ); 45 | registerFunction( "round", new StandardSQLFunction("round") ); 46 | registerFunction( "substr", new StandardSQLFunction("substr", StandardBasicTypes.STRING) ); 47 | registerFunction( "trim", new AbstractAnsiTrimEmulationFunction() { 48 | protected SQLFunction resolveBothSpaceTrimFunction() { 49 | return new SQLFunctionTemplate(StandardBasicTypes.STRING, "trim(?1)"); 50 | } 51 | 52 | protected SQLFunction resolveBothSpaceTrimFromFunction() { 53 | return new SQLFunctionTemplate(StandardBasicTypes.STRING, "trim(?2)"); 54 | } 55 | 56 | protected SQLFunction resolveLeadingSpaceTrimFunction() { 57 | return new SQLFunctionTemplate(StandardBasicTypes.STRING, "ltrim(?1)"); 58 | } 59 | 60 | protected SQLFunction resolveTrailingSpaceTrimFunction() { 61 | return new SQLFunctionTemplate(StandardBasicTypes.STRING, "rtrim(?1)"); 62 | } 63 | 64 | protected SQLFunction resolveBothTrimFunction() { 65 | return new SQLFunctionTemplate(StandardBasicTypes.STRING, "trim(?1, ?2)"); 66 | } 67 | 68 | protected SQLFunction resolveLeadingTrimFunction() { 69 | return new SQLFunctionTemplate(StandardBasicTypes.STRING, "ltrim(?1, ?2)"); 70 | } 71 | 72 | protected SQLFunction resolveTrailingTrimFunction() { 73 | return new SQLFunctionTemplate(StandardBasicTypes.STRING, "rtrim(?1, ?2)"); 74 | } 75 | } ); 76 | } 77 | 78 | @Override 79 | public boolean supportsIdentityColumns() { 80 | return true; 81 | } 82 | 83 | @Override 84 | public boolean hasDataTypeInIdentityColumn() { 85 | return false; // As specified in NHibernate dialect 86 | } 87 | 88 | @Override 89 | public String getIdentityColumnString() { 90 | // return "integer primary key autoincrement"; 91 | return "integer"; 92 | } 93 | 94 | @Override 95 | public String getIdentitySelectString() { 96 | return "select last_insert_rowid()"; 97 | } 98 | 99 | @Override 100 | public boolean supportsLimit() { 101 | return true; 102 | } 103 | 104 | @Override 105 | public boolean bindLimitParametersInReverseOrder() { 106 | return true; 107 | } 108 | 109 | @Override 110 | protected String getLimitString(String query, boolean hasOffset) { 111 | return query + (hasOffset ? " limit ? offset ?" : " limit ?"); 112 | } 113 | 114 | @Override 115 | public boolean supportsCurrentTimestampSelection() { 116 | return true; 117 | } 118 | 119 | @Override 120 | public boolean isCurrentTimestampSelectStringCallable() { 121 | return false; 122 | } 123 | 124 | @Override 125 | public String getCurrentTimestampSelectString() { 126 | return "select current_timestamp"; 127 | } 128 | 129 | private static final int SQLITE_BUSY = 5; 130 | private static final int SQLITE_LOCKED = 6; 131 | private static final int SQLITE_IOERR = 10; 132 | private static final int SQLITE_CORRUPT = 11; 133 | private static final int SQLITE_NOTFOUND = 12; 134 | private static final int SQLITE_FULL = 13; 135 | private static final int SQLITE_CANTOPEN = 14; 136 | private static final int SQLITE_PROTOCOL = 15; 137 | private static final int SQLITE_TOOBIG = 18; 138 | private static final int SQLITE_CONSTRAINT = 19; 139 | private static final int SQLITE_MISMATCH = 20; 140 | private static final int SQLITE_NOTADB = 26; 141 | 142 | @Override 143 | public boolean supportsUnionAll() { 144 | return true; 145 | } 146 | 147 | @Override 148 | public boolean hasAlterTable() { 149 | return false; // As specified in NHibernate dialect 150 | } 151 | 152 | @Override 153 | public boolean dropConstraints() { 154 | return false; 155 | } 156 | 157 | @Override 158 | public String getForUpdateString() { 159 | return ""; 160 | } 161 | 162 | @Override 163 | public boolean supportsOuterJoinForUpdate() { 164 | return false; 165 | } 166 | 167 | @Override 168 | public String getDropForeignKeyString() { 169 | throw new UnsupportedOperationException("No drop foreign key syntax supported by SQLiteDialect"); 170 | } 171 | 172 | @Override 173 | public String getAddForeignKeyConstraintString(String constraintName, 174 | String[] foreignKey, String referencedTable, String[] primaryKey, 175 | boolean referencesPrimaryKey) { 176 | throw new UnsupportedOperationException("No add foreign key syntax supported by SQLiteDialect"); 177 | } 178 | 179 | @Override 180 | public String getAddPrimaryKeyConstraintString(String constraintName) { 181 | throw new UnsupportedOperationException("No add primary key syntax supported by SQLiteDialect"); 182 | } 183 | 184 | @Override 185 | public boolean supportsIfExistsBeforeTableName() { 186 | return true; 187 | } 188 | 189 | @Override 190 | public boolean supportsTupleDistinctCounts() { 191 | return false; 192 | } 193 | 194 | @Override 195 | public String getSelectGUIDString() { 196 | return "select hex(randomblob(16))"; 197 | } 198 | } -------------------------------------------------------------------------------- /oauth-server/src/main/resources/templates/clients/form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Client edit/add 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |

Add client

18 | 19 |

Edit client

20 | 21 |
22 |
23 |
Editing a client will delete all user approvals and active tokens.
24 |
25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 39 | 40 |
41 |
42 | 46 | 48 |
49 |
50 | 54 | 56 |
57 |
58 | 62 | 64 |
65 | 66 |
67 | 68 |
69 |
70 | 74 | 75 |
76 |
77 | 81 | 82 |
83 |
84 | 88 | 89 |
90 |
91 | 92 |
93 | 97 | 98 |
99 |
100 |

101 | 102 | Description 103 | 104 |

105 |
106 |
107 |
108 |
    109 |
  • client_credentials 110 | 111 |

    The client can get an access token without the user authorizing it from the /token endpoint using basic authentication with its id and 112 | secret. 113 | Only for trusted clients. See curl client example. The client secret must be set for this grant type. The admin can specify the roles 114 | the client will have on the resource 115 | servers.

    116 | 117 |

    You could imagine a client script with an admin role but only read scope that runs on a trusted server of you and pulls some data 118 | periodically.

    119 |
  • 120 |
  • implicit 121 | 122 |

    The client gets a token directly after the user authorizes it. Clients using this should have a rather limited scope.

    123 | 124 |

    The client uses the /oauth/authorize?client_id=$clientId&response_type=token&redirect_uri=$uri endpoint to get the code. After 125 | the user authorizes the 126 | client the oauth server will return a redirect to $uri with the token in the hash of the redirect location.

    127 |
  • 128 |
  • authorization_code 129 | 130 |

    The user authorizes the client the first time it wants to access resources. In exchange it gets a code to get tokens from the /token 131 | endpoint. Only for trusted clients. The client must have some storage to keep the token.

    132 | 133 |

    The client uses the /oauth/authorize?client_id=$clientId&response_type=code&redirect_uri=$uri endpoint to get the code. After the 134 | user authorizes the client the oauth server will return a redirect to $uri with the code in the hash of the redirect location.

    135 | 136 |

    The code can then be used at /oauth/token?code=$code&grant_type=authorization_code to obtain a token with it. The client has 137 | to authenticate itself with basic authentication at this point (with its name and the client secret, even when the client_secret grant 138 | is not used!)

    139 |
  • 140 |
  • refresh_token
  • 141 |
  • password 142 | 143 |

    The client asks the user for his credentials and forwards them. Only for clients that you wrote yourself like an official app for a 144 | phone.

    145 |
  • 146 |
147 |
148 |
149 |
150 |
151 | 155 |
156 |
157 | 161 |
162 |
163 | 167 |
168 |
169 | 173 |
174 |
175 | 179 |
180 |
181 |
182 |
183 |
184 |
185 | 186 | --------------------------------------------------------------------------------