├── .gitignore
├── README.adoc
├── authserver
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── demo
│ │ │ └── AuthserverApplication.java
│ └── resources
│ │ ├── application.properties
│ │ └── keystore.jks
│ └── test
│ └── java
│ └── demo
│ └── ApplicationTests.java
├── docker-compose.yml
├── eureka
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── demo
│ │ └── EurekaApplication.java
│ └── resources
│ └── application.properties
├── pom.xml
├── resource
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── demo
│ │ │ └── ResourceApplication.java
│ └── resources
│ │ └── application.properties
│ └── test
│ └── groovy
│ └── demo
│ └── ApplicationTests.groovy
└── ui
├── pom.xml
└── src
├── main
├── java
│ └── demo
│ │ └── UiApplication.java
├── resources
│ ├── application.yml
│ └── static
│ │ ├── home.html
│ │ ├── index.html
│ │ └── js
│ │ └── hello.js
└── wro
│ ├── main.less
│ ├── wro.properties
│ └── wro.xml
└── test
└── java
└── demo
└── ApplicationTests.java
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | #*
3 | *#
4 | .#*
5 | .classpath
6 | .project
7 | .settings/
8 | .springBeans
9 | target/
10 | bin/
11 | _site/
12 | .idea
13 | *.iml
14 | .factorypath
15 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | [[_sso_with_oauth2_angular_js_and_spring_security_part_v]]
2 | = Single Sign On with OAuth2
3 |
4 | In this section we continue <<_the_api_gateway_pattern_angular_js_and_spring_security_part_iv,our discussion>> of how to use http://projects.spring.io/spring-security[Spring Security] with http://angularjs.org[Angular JS] in a "single page application". Here we show how to use http://projects.spring.io/spring-security-oauth/[Spring Security OAuth] together with http://projects.spring.io/spring-cloud/[Spring Cloud] to extend our API Gateway to do Single Sign On and OAuth2 token authentication to backend resources. This is the fifth in a series of sections, and you can catch up on the basic building blocks of the application or build it from scratch by reading the <<_spring_and_angular_js_a_secure_single_page_application,first section>>, or you can just go straight to the https://github.com/dsyer/spring-security-angular/tree/master/oauth2[source code in Github]. In the <<_the_api_gateway_pattern_angular_js_and_spring_security_part_iv,last section>> we built a small distributed application that used https://github.com/spring-projects/spring-session/[Spring Session] to authenticate the backend resources and http://projects.spring.io/spring-cloud/[Spring Cloud] to implement an embedded API Gateway in the UI server. In this section we extract the authentication responsibilities to a separate server to make our UI server the first of potentially many Single Sign On applications to the authorization server. This is a common pattern in many applications these days, both in the enterprise and in social startups. We will use an OAuth2 server as the authenticator, so that we can also use it to grant tokens for the backend resource server. Spring Cloud will automatically relay the access token to our backend, and enable us to further simplify the implementation of both the UI and resource servers.
5 |
6 | ____
7 | Reminder: if you are working through this section with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that for a single server is to open a new incognito window.
8 | ____
9 |
10 | == Creating an OAuth2 Authorization Server
11 |
12 | Our first step is to create a new server to handle authentication and token management. Following the steps in <<_spring_and_angular_js_a_secure_single_page_application,Part I>> we can begin with https://start.spring.io[Spring Boot Initializr]. E.g. using curl on a UN*X like system:
13 |
14 | [source]
15 | ----
16 | $ curl https://start.spring.io/starter.tgz -d style=web \
17 | -d style=security -d name=authserver | tar -xzvf -
18 | ----
19 |
20 | You can then import that project (it's a normal Maven Java project by default) into your favourite IDE, or just work with the files and "mvn" on the command line.
21 |
22 | === Adding the OAuth2 Dependencies
23 |
24 | We need to add the http://projects.spring.io/spring-security-oauth[Spring OAuth] dependencies, so in our https://github.com/dsyer/spring-security-angular/blob/master/oauth2/authserver/pom.xml[POM] we add:
25 |
26 | .pom.xml
27 | [source,xml]
28 | ----
29 |
30 | org.springframework.security.oauth
31 | spring-security-oauth2
32 | 2.0.5.RELEASE
33 |
34 | ----
35 |
36 | The authorization server is pretty easy to implement. A minimal version looks like this:
37 |
38 | .AuthserverApplication.java
39 | [source,java]
40 | ----
41 | @SpringBootApplication
42 | @EnableAuthorizationServer
43 | public class AuthserverApplication extends WebMvcConfigurerAdapter {
44 |
45 | public static void main(String[] args) {
46 | SpringApplication.run(AuthserverApplication.class, args);
47 | }
48 |
49 | }
50 | ----
51 |
52 | We only have to do 1 more thing (after adding `@EnableAuthorizationServer`):
53 |
54 | .application.properties
55 | [source,properties]
56 | ---
57 | ...
58 | security.oauth2.client.clientId: acme
59 | security.oauth2.client.clientSecret: acmesecret
60 | security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
61 | security.oauth2.client.scopes: openid
62 | ---
63 |
64 | This registers a client "acme" with a secret and some authorized grant types including "authorization_code".
65 |
66 | Now let's get it running on port 9999, with a predictable password for testing:
67 |
68 | .application.properties
69 | [source,properties]
70 | ----
71 | server.port=9999
72 | security.user.password=password
73 | server.contextPath=/uaa
74 | ...
75 | ----
76 |
77 | We also set the context path so that it doesn't use the default ("/") because otherwise you can get cookies for other servers on localhost being sent to the wrong server. So get the server running and we can make sure it is working:
78 |
79 | [source]
80 | ----
81 | $ mvn spring-boot:run
82 | ----
83 |
84 | or start the `main()` method in your IDE.
85 |
86 | === Testing the Authorization Server
87 |
88 | Our server is using the Spring Boot default security settings, so like the server in <<_spring_and_angular_js_a_secure_single_page_application,Part I>> it will be protected by HTTP Basic authentication. To initiate an https://tools.ietf.org/html/rfc6749#section-1.3.1[authorization code token grant] you visit the authorization endpoint, e.g. at http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.com[http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.com] once you have authenticated you will get a redirect to example.com with an authorization code attached, e.g. http://example.com/?code=jYWioI[http://example.com/?code=jYWioI].
89 |
90 | NOTE: for the purposes of this sample application we have created a client "acme" with no registered redirect, which is what enables us to get a redirect the example.com. In a production application you should always register a redirect (and use HTTPS).
91 |
92 | The code can be exchanged for an access token using the "acme" client credentials on the token endpoint:
93 |
94 | [source]
95 | ----
96 | $ curl acme:acmesecret@localhost:9999/uaa/oauth/token \
97 | -d grant_type=authorization_code -d client_id=acme \
98 | -d redirect_uri=http://example.com -d code=jYWioI
99 | {"access_token":"2219199c-966e-4466-8b7e-12bb9038c9bb","token_type":"bearer","refresh_token":"d193caf4-5643-4988-9a4a-1c03c9d657aa","expires_in":43199,"scope":"openid"}
100 | ----
101 |
102 | The access token is a UUID ("2219199c…"), backed by an in-memory token store in the server. We also got a refresh token that we can use to get a new access token when the current one expires.
103 |
104 | NOTE: since we allowed "password" grants for the "acme" client we can also get a token directly from the token endpoint using curl and user credentials instead of an authorization code. This is not suitable for a browser based client, but it's useful for testing.
105 |
106 | If you followed the link above you would have seen the whitelabel UI provided by Spring OAuth. To start with we will use this and we can come back later to beef it up like we did in <<_the_login_page_angular_js_and_spring_security_part_ii,Part II>> for the self-contained server.
107 |
108 | [[changing-the-resource-server]]
109 | == Changing the Resource Server
110 |
111 | If we follow on from <<_the_api_gateway_pattern_angular_js_and_spring_security_part_iv,Part IV>>, our resource server is using https://github.com/spring-projects/spring-session/[Spring Session] for authentication, so we can take that out and replace it with Spring OAuth. We also need to remove the Spring Session and Redis dependencies, so replace this:
112 |
113 | .pom.xml
114 | [source,xml]
115 | ----
116 |
117 | org.springframework.session
118 | spring-session
119 |
120 |
121 | org.springframework.boot
122 | spring-boot-starter-redis
123 |
124 | ----
125 |
126 | with this:
127 |
128 | .pom.xml
129 | [source,xml]
130 | ----
131 |
132 | org.springframework.security.oauth
133 | spring-security-oauth2
134 |
135 | ----
136 |
137 | and then remove the session `Filter` from the https://github.com/dsyer/spring-security-angular/blob/master/vanilla-oauth2/resource/src/main/groovy/demo/ResourceApplication.groovy[main application class], replacing it with the convenient `@EnableResourceServer` annotation (from Spring Security OAuth2):
138 |
139 | .ResourceApplication.groovy
140 | [source,java]
141 | ----
142 | @SpringBootApplication
143 | @RestController
144 | @EnableResourceServer
145 | class ResourceApplication {
146 |
147 | @RequestMapping('/')
148 | def home() {
149 | [id: UUID.randomUUID().toString(), content: 'Hello World']
150 | }
151 |
152 | static void main(String[] args) {
153 | SpringApplication.run ResourceApplication, args
154 | }
155 | }
156 |
157 | ----
158 |
159 | With that one change the app is ready to challenge for an access token instead of HTTP Basic, but we need a config change to actually finish the process. We are going to add a small amount of external configuration (in "application.properties") to allow the resource server to decode the tokens it is given and authenticate a user:
160 |
161 | .application.properties
162 | [source,properties]
163 | ----
164 | ...
165 | security.oauth2.resource.userInfoUri: http://localhost:9999/uaa/user
166 | ----
167 |
168 | This tells the server that it can use the token to access a "/user" endpoint and use that to derive authentication information (it's a bit like the https://developers.facebook.com/docs/graph-api/reference/v2.2/user/?locale=en_GB["/me" endpoint] in the Facebook API). Effectively it provides a way for the resource server to decode the token, as expressed by the `ResourceServerTokenServices` interface in Spring OAuth2.
169 |
170 | Run the application and hit the home page with a command line client:
171 |
172 | [source]
173 | ----
174 | $ curl -v localhost:9000
175 | > GET / HTTP/1.1
176 | > User-Agent: curl/7.35.0
177 | > Host: localhost:9000
178 | > Accept: */*
179 | >
180 | < HTTP/1.1 401 Unauthorized
181 | ...
182 | < WWW-Authenticate: Bearer realm="null", error="unauthorized", error_description="An Authentication object was not found in the SecurityContext"
183 | < Content-Type: application/json;charset=UTF-8
184 | {"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}
185 | ----
186 |
187 | and you will see a 401 with a "WWW-Authenticate" header indicating that it wants a bearer token.
188 |
189 | NOTE: the `userInfoUri` is by far not the only way of hooking a resource server up with a way to decode tokens. In fact it's sort of a lowest common denominator (and not part of the spec), but quite often available from OAuth2 providers (like Facebook, Cloud Foundry, Github), and other choices are available. For instance you can encode the user authentication in the token itself (e.g. with http://jwt.io/[JWT]), or use a shared backend store. There is also a `/token_info` endpoint in CloudFoundry, which provides more detailed information than the user info endpoint, but which requires more thorough authentication. Different options (naturally) provide different benefits and trade-offs, but a full discussion of those is outside the scope of this section.
190 |
191 | == Implementing the User Endpoint
192 |
193 | On the authorization server we can easily add that endpoint
194 |
195 | .AuthserverApplication.java
196 | [source,java]
197 | ----
198 | @SpringBootApplication
199 | @RestController
200 | @EnableAuthorizationServer
201 | @EnableResourceServer
202 | public class AuthserverApplication {
203 |
204 | @RequestMapping("/user")
205 | public Principal user(Principal user) {
206 | return user;
207 | }
208 |
209 | ...
210 |
211 | }
212 | ----
213 |
214 | We added a `@RequestMapping` the same as the UI server in <<_the_login_page_angular_js_and_spring_security_part_ii,Part II>>, and also the `@EnableResourceServer` annotation from Spring OAuth, which by default secures everything in an authorization server except the "/oauth/*" endpoints.
215 |
216 | With that endpoint in place we can test it and the greeting resource, since they both now accept bearer tokens that were created by the authorization server:
217 |
218 | [source]
219 | ----
220 | $ TOKEN=2219199c-966e-4466-8b7e-12bb9038c9bb
221 | $ curl -H "Authorization: Bearer $TOKEN" localhost:9000
222 | {"id":"03af8be3-2fc3-4d75-acf7-c484d9cf32b1","content":"Hello World"}
223 | $ curl -H "Authorization: Bearer $TOKEN" localhost:9999/uaa/user
224 | {"details":...,"principal":{"username":"user",...},"name":"user"}
225 | ----
226 |
227 | (substitute the value of the access token that you obtain from your own authorization server to get that working yourself).
228 |
229 | == The UI Server
230 |
231 | The final piece of this application we need to complete is the UI server, extracting the authentication part and delegating to the authorization server. So, as with link:#changing-the-resource-server[the resource server], we first need to remove the Spring Session and Redis dependencies and replace them with Spring OAuth2.
232 |
233 | Once that is done we can remove the session filter and the "/user" endpoint as well, and set up the application to redirect to the authorization server (using the `@EnableOAuth2Sso` annotation):
234 |
235 | .UiApplication.java
236 | [source,java]
237 | ----
238 | @SpringBootApplication
239 | @EnableZuulProxy
240 | @EnableOAuth2Sso
241 | public class UiApplication {
242 |
243 | public static void main(String[] args) {
244 | SpringApplication.run(UiApplication.class, args);
245 | }
246 |
247 | ...
248 |
249 | }
250 | ----
251 |
252 | Recall from <<_the_api_gateway_pattern_angular_js_and_spring_security_part_iv,Part IV>> that the UI server, by virtue of the `@EnableZuulProxy`, acts an API Gateway and we can declare the route mappings in YAML. So the "/user" endpoint can be proxied to the authorization server:
253 |
254 | .application.yml
255 | [source,yaml]
256 | ----
257 | zuul:
258 | routes:
259 | resource:
260 | path: /resource/**
261 | url: http://localhost:9000
262 | user:
263 | path: /user/**
264 | url: http://localhost:9999/uaa/user
265 | ----
266 |
267 | Lastly, we need to change the application to a `WebSecurityConfigurerAdapter` since now it is going to be used to modify the defaults in the SSO filter chain set up by `@EnableOAuth2Sso`:
268 |
269 | .SecurityConfiguration.java
270 | [source,java,indent=0]
271 | ----
272 | @SpringBootApplication
273 | @EnableZuulProxy
274 | @EnableOAuth2Sso
275 | public class UiApplication extends WebSecurityConfigurerAdapter {
276 | @Override
277 | public void configure(HttpSecurity http) throws Exception {
278 | http.authorizeRequests().antMatchers("/index.html", "/home.html", "/")
279 | .permitAll().anyRequest().authenticated().and().csrf()
280 | .csrfTokenRepository(csrfTokenRepository()).and()
281 | .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class);
282 | }
283 |
284 | ... // the csrf*() methods are the same as the old WebSecurityConfigurerAdapter
285 | }
286 | ----
287 |
288 | The main changes (apart from the base class name) are that the matchers
289 | go into their own method, and there is no need for `formLogin()` any more.
290 |
291 | There are also some mandatory external configuration properties for the
292 | `@EnableOAuth2Sso` annotation to be able to contact and authenticate with
293 | thr right authorization server. So we need this in `application.yml`:
294 |
295 | .application.yml
296 | [source,yaml]
297 | ----
298 | security:
299 | ...
300 | oauth2:
301 | client:
302 | accessTokenUri: http://localhost:9999/uaa/oauth/token
303 | userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
304 | clientId: acme
305 | clientSecret: acmesecret
306 | resource:
307 | userInfoUri: http://localhost:9999/uaa/user
308 | ----
309 |
310 | The bulk of that is about the OAuth2 client ("acme") and the
311 | authorization server locations. There is also a `userInfoUri` (just
312 | like in the resource server) so that the user can be authenticated in
313 | the UI app itself.
314 |
315 | === In the Client
316 |
317 | There are some minor tweaks to the UI application on the front end that we still need to make to trigger the redirect to the authorization server. The first is in the navigation bar in "index.html" where the "login" link changes from an Angular route:
318 |
319 | .index.html
320 | [source,html]
321 | ----
322 |