├── .gitignore
├── README.md
├── pom.xml
└── src
└── main
├── java
└── ai
│ └── auth
│ └── jwt
│ ├── Application.java
│ ├── config
│ ├── AdditionalWebConfig.java
│ ├── AuthorizationServerConfig.java
│ ├── ResourceServerConfig.java
│ └── SecurityConfig.java
│ ├── controller
│ └── ResourceController.java
│ ├── domain
│ ├── RandomCity.java
│ ├── Role.java
│ └── User.java
│ ├── repository
│ ├── RandomCityRepository.java
│ ├── RoleRepository.java
│ └── UserRepository.java
│ └── service
│ ├── GenericService.java
│ └── impl
│ ├── AppUserDetailsService.java
│ └── GenericServiceImpl.java
└── resources
├── application.properties
├── data.sql
└── ldap-data.ldif
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | ### Maven template
3 | target/
4 | pom.xml.tag
5 | pom.xml.releaseBackup
6 | pom.xml.versionsBackup
7 | pom.xml.next
8 | release.properties
9 | dependency-reduced-pom.xml
10 | buildNumber.properties
11 | .mvn/timing.properties
12 |
13 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
14 | !/.mvn/wrapper/maven-wrapper.jar
15 |
16 | *.iml
17 | .idea/
18 | *.log
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spring Boot Oauth2 using JWT
2 | An Example of Spring Boot Application for Securing a REST API with Oauth2 using JSON Web Token (JWT). JdbcTokenStore is used to save the token issued to the users
3 | It supports multiple authentication providers LDAP and DB. First will check the DB and if the user doesn't exists in DB then it will check the LDAP which is configured.
4 | This application can be used as a seed to quick start your spring boot REST API project with a fully functional security module.
5 | ---------------------
6 | Main building blocks
7 | ---------------------
8 | Spring Boot 1.5.9.RELEASE go to [http://docs.spring.io/spring-boot/docs/1.5.3.RELEASE/reference/htmlsingle/](http://docs.spring.io/spring-boot/docs/1.5.3.RELEASE/reference/htmlsingle/) to learn more about spring boot
9 | JSON Web Token go to []() https://jwt.io/ to decode your generated token and learn more.
10 |
11 |
12 | To run the application
13 | Use one of the several ways of running a Spring Boot application. Below are just three options:
14 |
15 | 1. Build using maven goal: mvn clean package and execute the resulting artifact as follows java -jar springboot-jwt-1.0-SNAPSHOT.jar or
16 | 2. On Unix/Linux based systems: run mvn clean package then run the resulting jar as any other executable ./springboot-jwt-1.0-SNAPSHOT.jar
17 |
18 | To test the application
19 |
20 | First you will need the following basic pieces of information:
21 | - client: testjwtclientid
22 | - secret: XY7kmzoNzl100
23 | - Local DB
24 | - Non-admin username and password: john.doe and jwtpass
25 | - Admin user: admin.admin and jwtpass
26 | - LDAP
27 | - username and password: ben and benspassword
28 | - Example of resource accessible to all authenticated users: [http://localhost:8080/springjwt/cities](http://localhost:8080/springjwt/cities)
29 | - Example of resource accessible to only an admin user: [http://localhost:8080/springjwt/users](http://localhost:8080/springjwt/users)
30 |
31 | 1. Generate an access token
32 |
33 | Use the following generic command to generate an access token:
34 |
35 | ```
36 | $ curl client:secret@localhost:8080/oauth/token -d grant_type=password -d username=user -d password=pwd
37 | ```
38 |
39 | For this specific application, to generate an access token for the non-admin user john.doe, run:
40 | ```
41 | $ curl testjwtclientid:XY7kmzoNzl100@localhost:8080/oauth/token -d grant_type=password -d username=john.doe -d password=jwtpass
42 | ```
43 | You'll receive a response similar to below
44 |
45 | ```
46 | {
47 | "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDM2MDI2NjksInVzZXJfbmFtZSI6ImFkbWluLmFkbWluIiwiYXV0aG9yaXRpZXMiOlsiU1RBTkRBUkRfVVNFUiIsIkFETUlOX1VTRVIiXSwianRpIjoiYzU0OTUwOWMtOGE2Ni00MmM4LTk4ZDQtZTIxOGMwMmQxYmFiIiwiY2xpZW50X2lkIjoidGVzdGp3dGNsaWVudGlkIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl19.hltbUwoJN9IQRLEHs_RnJS_MBaVMMzp0CjRB6paVGpY",
48 | "token_type": "bearer",
49 | "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbi5hZG1pbiIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJjNTQ5NTA5Yy04YTY2LTQyYzgtOThkNC1lMjE4YzAyZDFiYWIiLCJleHAiOjE1NDYxOTM3NjksImF1dGhvcml0aWVzIjpbIlNUQU5EQVJEX1VTRVIiLCJBRE1JTl9VU0VSIl0sImp0aSI6IjU4MTkzMTNmLTg0YTAtNGM1Mi05ZGNiLThiZWM1ZTcwNWI1NiIsImNsaWVudF9pZCI6InRlc3Rqd3RjbGllbnRpZCJ9.yvZmi5SyVmXpVEFwyE2H2lZ6VuP4ZMYQ8udnPtiWIIs",
50 | "expires_in": 899,
51 | "scope": "read write",
52 | "jti": "c549509c-8a66-42c8-98d4-e218c02d1bab"
53 | }
54 |
55 | ```
56 |
57 | Use the following command to validate an access token:
58 |
59 | ```
60 | $ curl testjwtclientid:XY7kmzoNzl100@localhost:8080/oauth/check_token -d 'token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDM2MDI2NjksInVzZXJfbmFtZSI6ImFkbWluLmFkbWluIiwiYXV0aG9yaXRpZXMiOlsiU1RBTkRBUkRfVVNFUiIsIkFETUlOX1VTRVIiXSwianRpIjoiYzU0OTUwOWMtOGE2Ni00MmM4LTk4ZDQtZTIxOGMwMmQxYmFiIiwiY2xpZW50X2lkIjoidGVzdGp3dGNsaWVudGlkIiwic2NvcGUiOlsicmVhZCIsIndyaXRlIl19.hltbUwoJN9IQRLEHs_RnJS_MBaVMMzp0CjRB6paVGpY&undefined='
61 | ```
62 | Use the following command to refresh token:
63 |
64 | ```
65 | $ curl -X POST testjwtclientid:XY7kmzoNzl100@localhost:8080/oauth/check_token -d 'grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJqb2huLmRvZSIsInNjb3BlIjpbInJlYWQiLCJ3cml0ZSJdLCJhdGkiOiJkYmZiYjI5Mi00NjMyLTQ5ODEtYThjMi0xZjYxNGQ5MjgyY2QiLCJleHAiOjE1NDYxODk3NjcsImF1dGhvcml0aWVzIjpbIlNUQU5EQVJEX1VTRVIiXSwianRpIjoiZjdhZDNiYWMtOTViYy00ZTZhLTkzYTQtODg0NzQ0Y2M5ODc1IiwiY2xpZW50X2lkIjoidGVzdGp3dGNsaWVudGlkIn0.VD9AJeK6555CYk5DaKj7ik81c81C6gPypNhTlEs6bsY&undefined='
66 | ```
67 | 2. Use the token to access resources through your RESTful API
68 |
69 | Access content available to all authenticated users
70 | Use the generated token as the value of the Bearer in the Authorization header as follows:
71 | ```
72 | http://localhost:8080/springjwt/cities -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdGp3dHJlc291cmNlaWQiXSwidXNlcl9uYW1lIjoiYWRtaW4uYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNDk0NDU0MjgyLCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF9VU0VSIiwiQURNSU5fVVNFUiJdLCJqdGkiOiIwYmQ4ZTQ1MC03ZjVjLTQ5ZjMtOTFmMC01Nzc1YjdiY2MwMGYiLCJjbGllbnRfaWQiOiJ0ZXN0and0Y2xpZW50aWQifQ.rvEAa4dIz8hT8uxzfjkEJKG982Ree5PdUW17KtFyeec"
73 | ```
74 |
75 | Access content available only to an admin user
76 | As with the previous example first generate an access token for the admin user with the credentials provided above then run:
77 | ```
78 | curl http://localhost:8080/springjwt/users -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdGp3dHJlc291cmNlaWQiXSwidXNlcl9uYW1lIjoiYWRtaW4uYWRtaW4iLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNDk0NDU0OTIzLCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF9VU0VSIiwiQURNSU5fVVNFUiJdLCJqdGkiOiIyMTAzMjRmMS05MTE0LTQ1NGEtODRmMy1hZjUzZmUxNzdjNzIiLCJjbGllbnRfaWQiOiJ0ZXN0and0Y2xpZW50aWQifQ.OuprVlyNnKuLkoQmP8shP38G3Hje91GBhu4E0HD2Fes"
79 | ```
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | ai.auth.spring.jwt
8 | springboot-jwt
9 | 1.0-SNAPSHOT
10 |
11 | org.springframework.boot
12 | spring-boot-starter-parent
13 | 1.5.9.RELEASE
14 |
15 |
16 |
17 | UTF-8
18 | UTF-8
19 | 1.8
20 | 1.6.0.RELEASE
21 | Dalston.SR5
22 | 2.7.0
23 |
24 |
25 |
26 |
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-data-jpa
31 |
32 |
33 |
34 |
35 | org.springframework.boot
36 | spring-boot-starter-jdbc
37 |
38 |
39 |
40 |
41 | org.postgresql
42 | postgresql
43 | 42.2.1.jre7
44 |
45 |
46 |
47 |
48 | io.pivotal.spring.cloud
49 | spring-cloud-services-starter-config-client
50 |
51 |
52 |
53 | org.springframework.boot
54 | spring-boot-starter-actuator
55 |
56 |
57 |
58 |
59 | org.springframework.boot
60 | spring-boot-starter-aop
61 |
62 |
63 | org.springframework.cloud
64 | spring-cloud-starter-sleuth
65 |
66 |
67 |
68 | org.springframework.boot
69 | spring-boot-starter-web
70 |
71 |
72 |
73 |
74 | io.springfox
75 | springfox-swagger2
76 | ${swagger.version}
77 |
78 |
79 | io.springfox
80 | springfox-swagger-ui
81 | ${swagger.version}
82 | compile
83 |
84 |
85 |
86 |
87 | de.codecentric
88 | spring-boot-admin-starter-client
89 | 1.3.0
90 |
91 |
92 | org.springframework.boot
93 | spring-boot-starter-test
94 | test
95 |
96 |
97 |
98 |
99 | org.apache.commons
100 | commons-lang3
101 | 3.8
102 |
103 |
104 |
105 | org.springframework.boot
106 | spring-boot-starter-security
107 |
108 |
109 |
110 | org.springframework.boot
111 | spring-boot-devtools
112 | runtime
113 |
114 |
115 | org.springframework.security
116 | spring-security-jwt
117 |
118 |
119 | org.springframework.security.oauth
120 | spring-security-oauth2
121 |
122 |
123 | com.unboundid
124 | unboundid-ldapsdk
125 |
126 |
127 | org.springframework.security
128 | spring-security-ldap
129 |
130 |
131 | org.springframework.boot
132 | spring-boot-starter-data-ldap
133 |
134 |
135 |
136 |
137 |
138 |
139 | org.springframework.cloud
140 | spring-cloud-dependencies
141 | ${spring-cloud.version}
142 | pom
143 | import
144 |
145 |
146 | io.pivotal.spring.cloud
147 | spring-cloud-services-dependencies
148 | ${spring-cloud-services.version}
149 | pom
150 | import
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | org.springframework.boot
159 | spring-boot-maven-plugin
160 |
161 |
162 |
163 | build-info
164 | repackage
165 |
166 |
167 |
168 | ${maven.compiler.target}
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 | pl.project13.maven
177 | git-commit-id-plugin
178 |
179 | false
180 |
181 |
182 |
183 | maven-release-plugin
184 | 2.4.2
185 |
186 |
187 | org.apache.maven.scm
188 | maven-scm-provider-gitexe
189 | 1.8.1
190 |
191 |
192 |
193 | false
194 | true
195 |
196 |
197 |
198 |
199 |
200 | org.apache.maven.wagon
201 | wagon-webdav-jackrabbit
202 | 2.10
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 | org.apache.maven.plugins
211 | maven-project-info-reports-plugin
212 | 2.9
213 |
214 |
215 |
216 |
217 | dependencies
218 | dependency-convergence
219 | dependency-info
220 | dependency-management
221 | distribution-management
222 | index
223 | license
224 | project-team
225 | scm
226 | summary
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | 2018
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/Application.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.context.annotation.ComponentScan;
6 |
7 | /**
8 | * Created by suman.das on 11/28/18.
9 | */
10 | @SpringBootApplication
11 | @ComponentScan("ai.auth.jwt.*")
12 | public class Application {
13 | public static void main(String[] args) {
14 | SpringApplication.run(Application.class, args);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/config/AdditionalWebConfig.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.config;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.web.cors.CorsConfiguration;
6 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
7 | import org.springframework.web.filter.CorsFilter;
8 |
9 |
10 | /**
11 | * Created by suman.das on 11/28/18.
12 | */
13 | @Configuration
14 | public class AdditionalWebConfig {
15 | /**
16 | * Allowing all origins, headers and methods here is only intended to keep this example simple.
17 | * This is not a default recommended configuration. Make adjustments as
18 | * necessary to your use case.
19 | *
20 | */
21 | @Bean
22 | public CorsFilter corsFilter() {
23 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
24 | CorsConfiguration config = new CorsConfiguration();
25 | config.setAllowCredentials(true);
26 | config.addAllowedOrigin("*");
27 | config.addAllowedHeader("*");
28 | config.addAllowedMethod("*");
29 | source.registerCorsConfiguration("/**", config);
30 | return new CorsFilter(source);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/config/AuthorizationServerConfig.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.config;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.jdbc.core.JdbcTemplate;
6 | import org.springframework.security.authentication.AuthenticationManager;
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.token.TokenStore;
13 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
14 |
15 | /**
16 | * Created by suman.das on 11/28/18.
17 | */
18 | @Configuration
19 | @EnableAuthorizationServer
20 | public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
21 |
22 | @Autowired
23 | private TokenStore tokenStore;
24 |
25 | @Autowired
26 | private JwtAccessTokenConverter accessTokenConverter;
27 |
28 | @Autowired
29 | private AuthenticationManager authenticationManager;
30 |
31 | //@Autowired
32 | //private UserDetailsService userDetailsService;
33 |
34 | @Autowired
35 | private JdbcTemplate jdbcTemplate;
36 |
37 | @Override
38 | public void configure(ClientDetailsServiceConfigurer configurer) throws Exception {
39 | configurer
40 | .jdbc(jdbcTemplate.getDataSource());
41 | }
42 |
43 |
44 | @Override
45 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
46 | endpoints.tokenStore(tokenStore)
47 | .reuseRefreshTokens(false)
48 | .accessTokenConverter(accessTokenConverter)
49 | .authenticationManager(authenticationManager)
50 | ;
51 |
52 |
53 | }
54 |
55 |
56 | @Override
57 | public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
58 | oauthServer.tokenKeyAccess("hasAuthority('ROLE_TRUSTED_CLIENT')").checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
59 | }
60 |
61 |
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/config/ResourceServerConfig.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.config;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
6 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
7 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
8 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
9 | import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
10 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
11 |
12 | /**
13 | * Created by suman.das on 11/28/18.
14 | */
15 | @Configuration
16 | @EnableResourceServer
17 | public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
18 |
19 | @Autowired
20 | private ResourceServerTokenServices tokenServices;
21 |
22 | @Autowired
23 | private JwtAccessTokenConverter accessTokenConverter;
24 |
25 | @Override
26 | public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
27 | resources.tokenServices(tokenServices);
28 | }
29 |
30 | @Override
31 | public void configure(HttpSecurity http) throws Exception {
32 | http
33 | .requestMatchers()
34 | .and()
35 | .authorizeRequests()
36 | .antMatchers("/actuator/**", "/api-docs/**","/oauth/*").permitAll()
37 | .antMatchers("/jwttest/**" ).authenticated();
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/config/SecurityConfig.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.config;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.beans.factory.annotation.Value;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.context.annotation.Primary;
8 | import org.springframework.jdbc.core.JdbcTemplate;
9 | import org.springframework.security.authentication.AuthenticationManager;
10 | import org.springframework.security.authentication.encoding.LdapShaPasswordEncoder;
11 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
12 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
13 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
14 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
15 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
16 | import org.springframework.security.config.http.SessionCreationPolicy;
17 | import org.springframework.security.core.userdetails.UserDetailsService;
18 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
19 | import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
20 | import org.springframework.security.oauth2.provider.token.TokenStore;
21 | import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
22 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
23 | import org.springframework.web.cors.CorsConfiguration;
24 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
25 | import org.springframework.web.filter.CorsFilter;
26 |
27 | /**
28 | * Created by suman.das on 11/28/18.
29 | */
30 | @Configuration
31 | @EnableWebSecurity
32 | @EnableGlobalMethodSecurity(prePostEnabled = true)
33 | public class SecurityConfig extends WebSecurityConfigurerAdapter {
34 |
35 | @Value("${security.signing-key}")
36 | private String signingKey;
37 |
38 | @Value("${security.encoding-strength}")
39 | private Integer encodingStrength;
40 |
41 | @Value("${security.security-realm}")
42 | private String securityRealm;
43 |
44 | @Autowired
45 | private JdbcTemplate jdbcTemplate;
46 |
47 | @Autowired
48 | private UserDetailsService userDetailsService;
49 |
50 | @Bean
51 | @Override
52 | protected AuthenticationManager authenticationManager() throws Exception {
53 | return super.authenticationManager();
54 | }
55 |
56 | @Bean
57 | public BCryptPasswordEncoder passwordEncoder() {
58 | return new BCryptPasswordEncoder();
59 | }
60 |
61 | @Override
62 | protected void configure(HttpSecurity http) throws Exception {
63 | http
64 | .sessionManagement()
65 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
66 | .and()
67 | .httpBasic()
68 | .realmName(securityRealm)
69 | .and()
70 | .csrf()
71 | .disable();
72 |
73 | }
74 |
75 | @Bean
76 | public JwtAccessTokenConverter accessTokenConverter() {
77 | JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
78 | converter.setSigningKey(signingKey);
79 | return converter;
80 | }
81 |
82 | // Spring has UserDetailsService interface, which can be overriden to provide our implementation for fetching user from database (or any other source).
83 | // The UserDetailsService object is used by the auth manager to load the user from database.
84 | // In addition, we need to define the password encoder also. So, auth manager can compare and verify passwords.
85 | @Override
86 | protected void configure(AuthenticationManagerBuilder auth) throws Exception {
87 | auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
88 |
89 | auth.ldapAuthentication().userDnPatterns("uid={0},ou=people").groupSearchBase("ou=groups")
90 | .contextSource().url("ldap://localhost:8389/dc=springframework,dc=org")
91 | .and()
92 | .passwordCompare()
93 | .passwordEncoder(new LdapShaPasswordEncoder())
94 | .passwordAttribute("userPassword");
95 |
96 | }
97 |
98 |
99 | @Bean
100 | public TokenStore tokenStore() {
101 | return new JdbcTokenStore(jdbcTemplate.getDataSource());
102 | }
103 |
104 | @Bean
105 | @Primary
106 | //Making this primary to avoid any accidental duplication with another token service instance of the same name
107 | public DefaultTokenServices tokenServices() {
108 | DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
109 | defaultTokenServices.setTokenStore(tokenStore());
110 | defaultTokenServices.setSupportRefreshToken(true);
111 | return defaultTokenServices;
112 | }
113 |
114 | @Bean
115 | public CorsFilter corsFilter() {
116 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
117 | CorsConfiguration config = new CorsConfiguration();
118 | config.setAllowCredentials(true);
119 | config.addAllowedOrigin("*");
120 | config.addAllowedHeader("*");
121 | config.addAllowedMethod("*");
122 | source.registerCorsConfiguration("/**", config);
123 | return new CorsFilter(source);
124 | }
125 |
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/controller/ResourceController.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.controller;
2 |
3 | import ai.auth.jwt.domain.RandomCity;
4 | import ai.auth.jwt.domain.User;
5 | import ai.auth.jwt.service.GenericService;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.web.bind.annotation.RequestMapping;
8 | import org.springframework.web.bind.annotation.RequestMethod;
9 | import org.springframework.web.bind.annotation.RestController;
10 |
11 | import java.util.List;
12 |
13 | /**
14 | * Created by suman.das on 11/28/18.
15 | */
16 | @RestController
17 | @RequestMapping("/jwttest")
18 | public class ResourceController {
19 | @Autowired
20 | private GenericService userService;
21 |
22 | @RequestMapping(value ="/cities")
23 | //@PreAuthorize("hasAuthority('ADMIN_USER') or hasAuthority('STANDARD_USER')")
24 | public List getUser(){
25 | return userService.findAllRandomCities();
26 | }
27 |
28 | @RequestMapping(value ="/users", method = RequestMethod.GET)
29 | //@PreAuthorize("hasAuthority('ADMIN_USER')")
30 | public List getUsers(){
31 | return userService.findAllUsers();
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/domain/RandomCity.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.domain;
2 |
3 | import javax.persistence.*;
4 |
5 | /**
6 | * Created by suman.das on 11/28/18.
7 | */
8 | @Entity
9 | @Table(name = "random_city")
10 | public class RandomCity {
11 | @Id
12 | @GeneratedValue(strategy = GenerationType.IDENTITY)
13 | @Column(name = "id")
14 | private Long id;
15 |
16 | @Column(name = "name")
17 | private String name;
18 |
19 | public Long getId() {
20 | return id;
21 | }
22 |
23 | public void setId(Long id) {
24 | this.id = id;
25 | }
26 |
27 | public String getName() {
28 | return name;
29 | }
30 |
31 | public void setName(String name) {
32 | this.name = name;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/domain/Role.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.domain;
2 |
3 | import javax.persistence.*;
4 |
5 | /**
6 | * Created by suman.das on 11/28/18.
7 | */
8 | @Entity
9 | @Table(name="app_role")
10 | public class Role {
11 | private static final long serialVersionUID = 1L;
12 | @Id
13 | @GeneratedValue(strategy = GenerationType.IDENTITY)
14 | private Long id;
15 |
16 | @Column(name="role_name")
17 | private String roleName;
18 |
19 | @Column(name="description")
20 | private String description;
21 |
22 |
23 | public Long getId() {
24 | return id;
25 | }
26 |
27 | public void setId(Long id) {
28 | this.id = id;
29 | }
30 |
31 | public String getRoleName() {
32 | return roleName;
33 | }
34 |
35 | public void setRoleName(String roleName) {
36 | this.roleName = roleName;
37 | }
38 |
39 | public String getDescription() {
40 | return description;
41 | }
42 |
43 | public void setDescription(String description) {
44 | this.description = description;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/domain/User.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.domain;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 |
5 | import javax.persistence.*;
6 | import java.util.List;
7 |
8 | /**
9 | * Created by suman.das on 11/28/18.
10 | */
11 | @Entity
12 | @Table(name = "app_user")
13 | public class User {
14 | @Id
15 | @GeneratedValue(strategy = GenerationType.IDENTITY)
16 | @Column(name = "id")
17 | private Long id;
18 |
19 | @Column(name = "username")
20 | private String username;
21 |
22 | @Column(name = "password")
23 | @JsonIgnore
24 | private String password;
25 |
26 | @Column(name = "first_name")
27 | private String firstName;
28 |
29 | @Column(name = "last_name")
30 | private String lastName;
31 |
32 | /**
33 | * Roles are being eagerly loaded here because
34 | * they are a fairly small collection of items for this example.
35 | */
36 | @ManyToMany(fetch = FetchType.EAGER)
37 | @JoinTable(name = "user_role", joinColumns
38 | = @JoinColumn(name = "user_id",
39 | referencedColumnName = "id"),
40 | inverseJoinColumns = @JoinColumn(name = "role_id",
41 | referencedColumnName = "id"))
42 | private List roles;
43 |
44 | public Long getId() {
45 | return id;
46 | }
47 |
48 | public void setId(Long id) {
49 | this.id = id;
50 | }
51 |
52 | public String getUsername() {
53 | return username;
54 | }
55 |
56 | public void setUsername(String username) {
57 | this.username = username;
58 | }
59 |
60 | public String getPassword() {
61 | return password;
62 | }
63 |
64 | public void setPassword(String password) {
65 | this.password = password;
66 | }
67 |
68 | public String getFirstName() {
69 | return firstName;
70 | }
71 |
72 | public void setFirstName(String firstName) {
73 | this.firstName = firstName;
74 | }
75 |
76 | public String getLastName() {
77 | return lastName;
78 | }
79 |
80 | public void setLastName(String lastName) {
81 | this.lastName = lastName;
82 | }
83 |
84 | public List getRoles() {
85 | return roles;
86 | }
87 |
88 | public void setRoles(List roles) {
89 | this.roles = roles;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/repository/RandomCityRepository.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.repository;
2 |
3 | import ai.auth.jwt.domain.RandomCity;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | /**
7 | * Created by suman.das on 11/28/18.
8 | */
9 | public interface RandomCityRepository extends JpaRepository {
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/repository/RoleRepository.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.repository;
2 |
3 | import ai.auth.jwt.domain.Role;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | /**
7 | * Created by suman.das on 11/28/18.
8 | */
9 | public interface RoleRepository extends JpaRepository {
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.repository;
2 |
3 | import ai.auth.jwt.domain.User;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | /**
7 | * Created by suman.das on 11/28/18.
8 | */
9 | public interface UserRepository extends JpaRepository {
10 | User findByUsername(String username);
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/service/GenericService.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.service;
2 |
3 | import ai.auth.jwt.domain.RandomCity;
4 | import ai.auth.jwt.domain.User;
5 |
6 | import java.util.List;
7 |
8 | /**
9 | * Created by suman.das on 11/28/18.
10 | */
11 | public interface GenericService {
12 | User findByUsername(String username);
13 |
14 | List findAllUsers();
15 |
16 | List findAllRandomCities();
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/service/impl/AppUserDetailsService.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.service.impl;
2 |
3 | import ai.auth.jwt.domain.User;
4 | import ai.auth.jwt.repository.UserRepository;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.security.core.GrantedAuthority;
7 | import org.springframework.security.core.authority.SimpleGrantedAuthority;
8 | import org.springframework.security.core.userdetails.UserDetails;
9 | import org.springframework.security.core.userdetails.UserDetailsService;
10 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
11 | import org.springframework.stereotype.Component;
12 |
13 | import java.util.ArrayList;
14 | import java.util.List;
15 |
16 | /**
17 | * Created by suman.das on 11/28/18.
18 | */
19 | @Component
20 | public class AppUserDetailsService implements UserDetailsService{
21 | @Autowired
22 | private UserRepository userRepository;
23 |
24 | @Override
25 | public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
26 | User user = userRepository.findByUsername(s);
27 |
28 | if(user == null) {
29 | throw new UsernameNotFoundException(String.format("The username %s doesn't exist", s));
30 | }
31 |
32 | List authorities = new ArrayList<>();
33 | user.getRoles().forEach(role -> {
34 | authorities.add(new SimpleGrantedAuthority(role.getRoleName()));
35 | });
36 |
37 | UserDetails userDetails = new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
38 |
39 | return userDetails;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/ai/auth/jwt/service/impl/GenericServiceImpl.java:
--------------------------------------------------------------------------------
1 | package ai.auth.jwt.service.impl;
2 |
3 | import ai.auth.jwt.domain.RandomCity;
4 | import ai.auth.jwt.domain.User;
5 | import ai.auth.jwt.repository.RandomCityRepository;
6 | import ai.auth.jwt.repository.UserRepository;
7 | import ai.auth.jwt.service.GenericService;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.stereotype.Service;
10 |
11 | import java.util.List;
12 |
13 | /**
14 | * Created by suman.das on 11/28/18.
15 | */
16 | @Service
17 | public class GenericServiceImpl implements GenericService {
18 | @Autowired
19 | private UserRepository userRepository;
20 |
21 | @Autowired
22 | private RandomCityRepository randomCityRepository;
23 |
24 | @Override
25 | public User findByUsername(String username) {
26 | return userRepository.findByUsername(username);
27 | }
28 |
29 | @Override
30 | public List findAllUsers() {
31 | return userRepository.findAll();
32 | }
33 |
34 | @Override
35 | public List findAllRandomCities() {
36 | return randomCityRepository.findAll();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | #security.oauth2.resource.filter-order=3
2 |
3 | security.signing-key=MaYzkSjmkzPC57L
4 | security.encoding-strength=256
5 | security.security-realm=Spring Boot JWT Example Realm
6 |
7 | #security.jwt.client-id=testjwtclientid
8 | #security.jwt.client-secret=XY7kmzoNzl100
9 | #security.jwt.grant-type=password
10 | #security.jwt.scope-read=read
11 | #security.jwt.scope-write=write
12 | #security.jwt.resource-ids=testjwtresourceid
13 |
14 | spring.application.name=jwt-springBoot-oauth
15 | spring.datasource.url=jdbc:postgresql://localhost:5432/auth?ApplicationName=authentication
16 | spring.datasource.username=${DATABASE_USERNAME:postgres}
17 | spring.datasource.password=${DATABASE_PASSWORD:postgres}
18 | spring.datasource.driver-class-name=org.postgresql.Driver
19 |
20 | spring.ldap.embedded.port=8389
21 | spring.ldap.embedded.ldif=classpath:ldap-data.ldif
22 | spring.ldap.embedded.base-dn=dc=springframework,dc=org
--------------------------------------------------------------------------------
/src/main/resources/data.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE random_city (
2 | id bigint NOT NULL,
3 | name varchar(255) DEFAULT NULL,
4 | PRIMARY KEY (id)
5 | );
6 |
7 | CREATE TABLE app_role (
8 | id bigint NOT NULL ,
9 | description varchar(255) DEFAULT NULL,
10 | role_name varchar(255) DEFAULT NULL,
11 | PRIMARY KEY (id)
12 | );
13 |
14 |
15 | CREATE TABLE app_user (
16 | id bigint NOT NULL ,
17 | first_name varchar(255) NOT NULL,
18 | last_name varchar(255) NOT NULL,
19 | password varchar(255) NOT NULL,
20 | username varchar(255) NOT NULL,
21 | PRIMARY KEY (id)
22 | );
23 |
24 | CREATE TABLE user_role (
25 | user_id bigint NOT NULL,
26 | role_id bigint NOT NULL,
27 | CONSTRAINT FK859n2jvi8ivhui0rl0esws6o FOREIGN KEY (user_id) REFERENCES app_user (id),
28 | CONSTRAINT FKa68196081fvovjhkek5m97n3y FOREIGN KEY (role_id) REFERENCES app_role (id)
29 | );
30 |
31 | drop table if exists oauth_client_details;
32 | create table oauth_client_details (
33 | client_id VARCHAR(256) PRIMARY KEY,
34 | resource_ids VARCHAR(256),
35 | client_secret VARCHAR(256),
36 | scope VARCHAR(256),
37 | authorized_grant_types VARCHAR(256),
38 | web_server_redirect_uri VARCHAR(256),
39 | authorities VARCHAR(256),
40 | access_token_validity INTEGER,
41 | refresh_token_validity INTEGER,
42 | additional_information VARCHAR(4096),
43 | autoapprove VARCHAR(256)
44 | );
45 |
46 | drop table if exists oauth_client_token;
47 | create table oauth_client_token (
48 | token_id VARCHAR(255),
49 | token bytea ,
50 | authentication_id VARCHAR(255),
51 | user_name VARCHAR(255),
52 | client_id VARCHAR(255)
53 | );
54 |
55 | drop table if exists oauth_access_token;
56 | create table oauth_access_token (
57 | token_id VARCHAR(255),
58 | token bytea,
59 | authentication_id VARCHAR(255),
60 | user_name VARCHAR(255),
61 | client_id VARCHAR(255),
62 | authentication bytea,
63 | refresh_token VARCHAR(255)
64 | );
65 |
66 | create index oauth_access_token_id on oauth_access_token(token_id);
67 | create index oauth_refresh_token_id on oauth_access_token(token_id);
68 |
69 | drop table if exists oauth_refresh_token;
70 | create table oauth_refresh_token (
71 | token_id VARCHAR(255),
72 | token bytea,
73 | authentication bytea
74 | );
75 | drop table if exists oauth_code;
76 | create table oauth_code (
77 | code VARCHAR(255), authentication bytea
78 | );
79 |
80 | drop table if exists oauth_approvals;
81 | create table oauth_approvals (
82 | userId VARCHAR(255),
83 | clientId VARCHAR(255),
84 | scope VARCHAR(255),
85 | status VARCHAR(10),
86 | expiresAt TIMESTAMP,
87 | lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
88 | );
89 | drop table if exists ClientDetails;
90 | create table ClientDetails (
91 | appId VARCHAR(255) PRIMARY KEY,
92 | resourceIds VARCHAR(255),
93 | appSecret VARCHAR(255),
94 | scope VARCHAR(255),
95 | grantTypes VARCHAR(255),
96 | redirectUrl VARCHAR(255),
97 | authorities VARCHAR(255),
98 | access_token_validity INTEGER,
99 | refresh_token_validity INTEGER,
100 | additionalInformation VARCHAR(4096),
101 | autoApproveScopes VARCHAR(255)
102 | );
103 |
104 | INSERT INTO app_role (id, role_name, description) VALUES (1, 'STANDARD_USER', 'Standard User - Has no admin rights');
105 | INSERT INTO app_role (id, role_name, description) VALUES (2, 'ADMIN_USER', 'Admin User - Has permission to perform admin tasks');
106 |
107 | -- USER john.doe
108 | -- non-encrypted password: jwtpass
109 | INSERT INTO app_user (id, first_name, last_name, password, username) VALUES (1, 'John', 'Doe', '$2a$10$qtH0F1m488673KwgAfFXEOWxsoZSeHqqlB/8BTt3a6gsI5c2mdlfe', 'john.doe');
110 | INSERT INTO app_user (id, first_name, last_name, password, username) VALUES (2, 'Admin', 'Admin', '$2a$10$qtH0F1m488673KwgAfFXEOWxsoZSeHqqlB/8BTt3a6gsI5c2mdlfe', 'admin.admin');
111 |
112 |
113 | INSERT INTO user_role(user_id, role_id) VALUES(1,1);
114 | INSERT INTO user_role(user_id, role_id) VALUES(2,1);
115 | INSERT INTO user_role(user_id, role_id) VALUES(2,2);
116 |
117 | -- Populate random city table
118 |
119 | INSERT INTO random_city(id, name) VALUES (1, 'Bamako');
120 | INSERT INTO random_city(id, name) VALUES (2, 'Nonkon');
121 | INSERT INTO random_city(id, name) VALUES (3, 'Houston');
122 | INSERT INTO random_city(id, name) VALUES (4, 'Toronto');
123 | INSERT INTO random_city(id, name) VALUES (5, 'New York City');
124 | INSERT INTO random_city(id, name) VALUES (6, 'Mopti');
125 |
126 | -- insert client details
127 | INSERT INTO oauth_client_details
128 | (client_id, client_secret, scope, authorized_grant_types,
129 | authorities, access_token_validity, refresh_token_validity)
130 | VALUES
131 | ('testjwtclientid', 'XY7kmzoNzl100', 'read,write', 'password,refresh_token,client_credentials,authorization_code', 'ROLE_CLIENT,ROLE_TRUSTED_CLIENT', 900, 2592000);
132 | INSERT INTO oauth_client_details
133 | (client_id, client_secret, scope, authorized_grant_types,
134 | authorities, access_token_validity, refresh_token_validity)
135 | VALUES
136 | ('ANDROID_APP', 'VzagO+ufWljKCWHfY1a+0uR8Vek=', 'read,write', 'password,refresh_token,client_credentials,authorization_code', 'ROLE_CLIENT,ROLE_TRUSTED_CLIENT', 6048800, 259200000);
137 | ALTER TABLE oauth_access_token ADD CONSTRAINT user_client_const UNIQUE (user_name,client_id);
138 |
139 | ---LDAP username ben Password benspassword
--------------------------------------------------------------------------------
/src/main/resources/ldap-data.ldif:
--------------------------------------------------------------------------------
1 | dn: dc=springframework,dc=org
2 | objectclass: top
3 | objectclass: domain
4 | objectclass: extensibleObject
5 | dc: springframework
6 |
7 | dn: ou=groups,dc=springframework,dc=org
8 | objectclass: top
9 | objectclass: organizationalUnit
10 | ou: groups
11 |
12 | dn: ou=subgroups,ou=groups,dc=springframework,dc=org
13 | objectclass: top
14 | objectclass: organizationalUnit
15 | ou: subgroups
16 |
17 | dn: ou=people,dc=springframework,dc=org
18 | objectclass: top
19 | objectclass: organizationalUnit
20 | ou: people
21 |
22 | dn: ou=space cadets,dc=springframework,dc=org
23 | objectclass: top
24 | objectclass: organizationalUnit
25 | ou: space cadets
26 |
27 | dn: ou=\"quoted people\",dc=springframework,dc=org
28 | objectclass: top
29 | objectclass: organizationalUnit
30 | ou: "quoted people"
31 |
32 | dn: ou=otherpeople,dc=springframework,dc=org
33 | objectclass: top
34 | objectclass: organizationalUnit
35 | ou: otherpeople
36 |
37 | dn: uid=ben,ou=people,dc=springframework,dc=org
38 | objectclass: top
39 | objectclass: person
40 | objectclass: organizationalPerson
41 | objectclass: inetOrgPerson
42 | cn: Ben Alex
43 | sn: Alex
44 | uid: ben
45 | userPassword: {SHA}nFCebWjxfaLbHHG1Qk5UU4trbvQ=
46 |
47 | dn: uid=bob,ou=people,dc=springframework,dc=org
48 | objectclass: top
49 | objectclass: person
50 | objectclass: organizationalPerson
51 | objectclass: inetOrgPerson
52 | cn: Bob Hamilton
53 | sn: Hamilton
54 | uid: bob
55 | userPassword: bobspassword
56 |
57 | dn: uid=joe,ou=otherpeople,dc=springframework,dc=org
58 | objectclass: top
59 | objectclass: person
60 | objectclass: organizationalPerson
61 | objectclass: inetOrgPerson
62 | cn: Joe Smeth
63 | sn: Smeth
64 | uid: joe
65 | userPassword: joespassword
66 |
67 | dn: cn=mouse\, jerry,ou=people,dc=springframework,dc=org
68 | objectclass: top
69 | objectclass: person
70 | objectclass: organizationalPerson
71 | objectclass: inetOrgPerson
72 | cn: Mouse, Jerry
73 | sn: Mouse
74 | uid: jerry
75 | userPassword: jerryspassword
76 |
77 | dn: cn=slash/guy,ou=people,dc=springframework,dc=org
78 | objectclass: top
79 | objectclass: person
80 | objectclass: organizationalPerson
81 | objectclass: inetOrgPerson
82 | cn: slash/guy
83 | sn: Slash
84 | uid: slashguy
85 | userPassword: slashguyspassword
86 |
87 | dn: cn=quote\"guy,ou=\"quoted people\",dc=springframework,dc=org
88 | objectclass: top
89 | objectclass: person
90 | objectclass: organizationalPerson
91 | objectclass: inetOrgPerson
92 | cn: quote\"guy
93 | sn: Quote
94 | uid: quoteguy
95 | userPassword: quoteguyspassword
96 |
97 | dn: uid=space cadet,ou=space cadets,dc=springframework,dc=org
98 | objectclass: top
99 | objectclass: person
100 | objectclass: organizationalPerson
101 | objectclass: inetOrgPerson
102 | cn: Space Cadet
103 | sn: Cadet
104 | uid: space cadet
105 | userPassword: spacecadetspassword
106 |
107 |
108 |
109 | dn: cn=developers,ou=groups,dc=springframework,dc=org
110 | objectclass: top
111 | objectclass: groupOfUniqueNames
112 | cn: developers
113 | ou: developer
114 | uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
115 | uniqueMember: uid=bob,ou=people,dc=springframework,dc=org
116 |
117 | dn: cn=managers,ou=groups,dc=springframework,dc=org
118 | objectclass: top
119 | objectclass: groupOfUniqueNames
120 | cn: managers
121 | ou: manager
122 | uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
123 | uniqueMember: cn=mouse\, jerry,ou=people,dc=springframework,dc=org
124 |
125 | dn: cn=submanagers,ou=subgroups,ou=groups,dc=springframework,dc=org
126 | objectclass: top
127 | objectclass: groupOfUniqueNames
128 | cn: submanagers
129 | ou: submanager
130 | uniqueMember: uid=ben,ou=people,dc=springframework,dc=org
--------------------------------------------------------------------------------