|
1 |
| -# Lab 3: Creating an OAuth 2.0/OIDC compliant Client |
| 1 | +# Lab 4: Creating a static OAuth 2.0/OIDC resource server |
2 | 2 |
|
| 3 | +In this fourth and final lab you will see how you can |
| 4 | +configure the resource server from [Lab 1](../lab1/README.md) with a custom static private/public key pair |
| 5 | +and create an application to generate your own JWT tokens using the corresponding signing private key. |
3 | 6 |
|
| 7 | +This is quite helpful in testing environments, e.g. doing load/performance testing and preventing |
| 8 | +from load testing the identity server as well. |
| 9 | + |
| 10 | +In this lab we will not really implement anything but try how to use such static resource server |
| 11 | +with custom generated JWt tokens. |
| 12 | + |
| 13 | +<u>Note:</u> |
| 14 | +The contents of this lab are build upon the preview of [Spring Security 5.2.0 Milestone 2](https://spring.io/blog/2019/04/16/spring-security-5-2-0-m2-released). |
| 15 | + |
| 16 | +## Lab Contents |
| 17 | + |
| 18 | +* [Lab 4 contents](#lab-contents) |
| 19 | +* [Lab 4 Tutorial](#lab-4-tutorial) |
| 20 | + * [Step 1: Implement a resource server with static public key](#step-1-resource-server-with-static-token-validation) |
| 21 | + * [Step 2: Generate custom JWT with the JWT generator app](#step-2-run-jwt-generator-web-application) |
| 22 | + * [Step 3: Run and test basic resource server](#step-3-run-and-test-static-resource-server) |
| 23 | + |
| 24 | +The [Keycloak](https://keycloak.org) identity provider is not required any more for this lab . |
| 25 | + |
| 26 | +## Lab 4 Tutorial |
| 27 | + |
| 28 | +Lab-4 is actually split into three steps: |
| 29 | + |
| 30 | +1. Look into a resource server with __static public key__ to verify JWT tokens |
| 31 | +2. Generate custom JWT tokens for different user identities to be used at the resource server of step 1 |
| 32 | +3. Make requests to the resource server of step 1 with generated JWT from step 2 |
| 33 | + |
| 34 | +### Contents of lab 4 folder |
| 35 | + |
| 36 | +In the lab 4 folder you find 3 applications: |
| 37 | + |
| 38 | +* __library-server-static-complete__: This application is the complete static resource server |
| 39 | +* __jwt-generator__: This application is the JWT generator to generate custom JWT tokens |
| 40 | + |
| 41 | +### Step 1: Resource server with static token validation |
| 42 | + |
| 43 | +Now, let's start with step 1 of this lab. Here we will have a look into the required changes we need |
| 44 | +compared to the resource server of [Lab 1](../lab1/README.md) to support static public keys for token signature validation. |
| 45 | + |
| 46 | +In [Lab 1](../lab1/README.md) we have seen how Spring security 5 uses the |
| 47 | +[OpenID Connect Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) specification |
| 48 | +to completely configure the resource server to use our keycloak instance. |
| 49 | + |
| 50 | +As we will now locally validate the incoming JWT access tokens using a static public key we do not |
| 51 | +need the discovery entries (especially the JWKS uri) any more. |
| 52 | + |
| 53 | +You can see the changes in _application.yml_, here no _issuer uri_ property is required any more. |
| 54 | +Instead we specify a location reference to a file containing a public key to verify JWT tokens. |
| 55 | + |
| 56 | +This looks like this: |
| 57 | + |
| 58 | +```yaml |
| 59 | +spring: |
| 60 | + jpa: |
| 61 | + open-in-view: false |
| 62 | + jackson: |
| 63 | + date-format: com.fasterxml.jackson.databind.util.StdDateFormat |
| 64 | + default-property-inclusion: non_null |
| 65 | + security: |
| 66 | + oauth2: |
| 67 | + resourceserver: |
| 68 | + jwt: |
| 69 | + publicKeyLocation: classpath:library_server.pub |
| 70 | +``` |
| 71 | +
|
| 72 | +Now we have to use this public key to configure the _JwtDecoder_ to use this for validating |
| 73 | +JWT tokens instead of contacting keycloak. |
| 74 | +
|
| 75 | +This requires a small change in the class _com.example.library.server.config.WebSecurityConfiguration_: |
| 76 | +
|
| 77 | +Open the class _com.example.library.server.config.WebSecurityConfiguration_ and look at the |
| 78 | +changes: |
| 79 | +
|
| 80 | +```java |
| 81 | +package com.example.library.server.config; |
| 82 | + |
| 83 | +import com.example.library.server.security.AudienceValidator; |
| 84 | +import com.example.library.server.security.LibraryUserDetailsService; |
| 85 | +import com.example.library.server.security.LibraryUserJwtAuthenticationConverter; |
| 86 | +import com.example.library.server.security.LibraryUserRolesJwtAuthenticationConverter; |
| 87 | +import org.springframework.beans.factory.annotation.Value; |
| 88 | +import org.springframework.context.annotation.Bean; |
| 89 | +import org.springframework.context.annotation.Configuration; |
| 90 | +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; |
| 91 | +import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
| 92 | +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; |
| 93 | +import org.springframework.security.config.http.SessionCreationPolicy; |
| 94 | +import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; |
| 95 | +import org.springframework.security.oauth2.core.OAuth2TokenValidator; |
| 96 | +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; |
| 97 | +import org.springframework.security.oauth2.jwt.Jwt; |
| 98 | +import org.springframework.security.oauth2.jwt.JwtDecoder; |
| 99 | +import org.springframework.security.oauth2.jwt.JwtValidators; |
| 100 | +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; |
| 101 | + |
| 102 | +import java.security.interfaces.RSAPublicKey; |
| 103 | + |
| 104 | +@Configuration |
| 105 | +@EnableGlobalMethodSecurity(prePostEnabled = true) |
| 106 | +public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { |
| 107 | + |
| 108 | + private final LibraryUserDetailsService libraryUserDetailsService; |
| 109 | + |
| 110 | + @Value("${spring.security.oauth2.resourceserver.jwt.publicKeyLocation}") |
| 111 | + private RSAPublicKey key; |
| 112 | + |
| 113 | + public WebSecurityConfiguration(LibraryUserDetailsService libraryUserDetailsService) { |
| 114 | + this.libraryUserDetailsService = libraryUserDetailsService; |
| 115 | + } |
| 116 | + |
| 117 | + @Override |
| 118 | + protected void configure(HttpSecurity http) throws Exception { |
| 119 | + http.sessionManagement() |
| 120 | + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) |
| 121 | + .and() |
| 122 | + .csrf() |
| 123 | + .disable() |
| 124 | + .authorizeRequests() |
| 125 | + .anyRequest() |
| 126 | + .fullyAuthenticated() |
| 127 | + .and() |
| 128 | + .oauth2ResourceServer() |
| 129 | + .jwt() |
| 130 | + .jwtAuthenticationConverter(libraryUserJwtAuthenticationConverter()); |
| 131 | + } |
| 132 | + |
| 133 | + @Bean |
| 134 | + JwtDecoder jwtDecoder() { |
| 135 | + NimbusJwtDecoder jwtDecoder = |
| 136 | + NimbusJwtDecoder.withPublicKey(this.key).build(); |
| 137 | + |
| 138 | + OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(); |
| 139 | + OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer("test_issuer"); |
| 140 | + OAuth2TokenValidator<Jwt> withAudience = |
| 141 | + new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); |
| 142 | + |
| 143 | + jwtDecoder.setJwtValidator(withAudience); |
| 144 | + |
| 145 | + return jwtDecoder; |
| 146 | + } |
| 147 | + |
| 148 | + @Bean |
| 149 | + LibraryUserJwtAuthenticationConverter libraryUserJwtAuthenticationConverter() { |
| 150 | + return new LibraryUserJwtAuthenticationConverter(libraryUserDetailsService); |
| 151 | + } |
| 152 | +} |
4 | 153 | ```
|
5 |
| -http --form http://localhost:8080/auth/realms/workshop/protocol/openid-connect/token grant_type=password \ |
6 |
| -username=bwayne password=wayne client_id=library-client client_secret=9584640c-3804-4dcd-997b-93593cfb9ea7 |
7 |
| -``` |
8 | 154 |
|
| 155 | +This configuration above looks like the one as in [Lab 1](../lab1/README.md) with one important change: |
9 | 156 |
|
10 | 157 | ```
|
11 |
| -http localhost:9091/library-service/books 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cC...' |
| 158 | +@Value("${spring.security.oauth2.resourceserver.jwt.publicKeyLocation}") |
| 159 | +private RSAPublicKey key; |
| 160 | +
|
| 161 | +... |
| 162 | +
|
| 163 | +NimbusJwtDecoder jwtDecoder = |
| 164 | + NimbusJwtDecoder.withPublicKey(this.key).build(); |
| 165 | +``` |
| 166 | + |
| 167 | +Here we use the public key (using RSA crypto algorithm) we read from the _publicKeyLocation_ |
| 168 | +and create a NimbusJwtDecoder using this public key instead of configuring a JwtDecoder |
| 169 | +from issuer uri. |
| 170 | + |
| 171 | +With this configuration in place we have already a working resource server |
| 172 | +that can handle JWt access tokens transmitted via http bearer token header. |
| 173 | +Spring Security also validates by default: |
| 174 | + |
| 175 | +* the JWT signature against the given static public key |
| 176 | +* the JWT _iss_ claim against the configured issuer uri |
| 177 | +* that the JWT is not expired, if the JWT contains such entry |
| 178 | + |
| 179 | +<hr> |
| 180 | + |
| 181 | +### Step 2: Run JWT generator web application |
| 182 | + |
| 183 | +Please navigate your Java IDE to the __lab4/jwt-generator__ project. |
| 184 | +Then start the application by running the class _com.example.jwt.generator.JwtGeneratorApplication_. |
| 185 | + |
| 186 | +After starting navigate your browser to [localhost:9093](http://localhost:9093). |
| 187 | + |
| 188 | +Then you should see a screen like the following one. |
| 189 | + |
| 190 | + |
| 191 | + |
| 192 | +To generate an JWT access token with the correct user identity and role information |
| 193 | +please fill the shown form with one of the following users and roles: |
| 194 | + |
| 195 | +| Username | Email | Role | |
| 196 | +| ---------| ------------------------ | --------------- | |
| 197 | +| bwayne | [email protected] | library_user | |
| 198 | +| bbanner | [email protected] | library_user | |
| 199 | +| pparker | [email protected] | library_curator | |
| 200 | +| ckent | [email protected] | library_admin | |
| 201 | + |
| 202 | +After filling the form click on the button _Generate JWT_ then you should get another web page |
| 203 | +with the generate access token. This should look like this one. |
| 204 | + |
| 205 | + |
| 206 | + |
| 207 | +To continue with this lab copy the contents of the JWT and use this JWT as access token to |
| 208 | +make a request to the resource server in the next step. |
| 209 | + |
| 210 | +### Step 3: Run and test static resource server |
| 211 | + |
| 212 | +Please navigate your Java IDE to the __lab4/library-server-static-complete__ project and at first explore this project a bit. |
| 213 | +Then start the application by running the class _com.example.library.server.CompleteStaticLibraryServerApplication_. |
| 214 | + |
| 215 | +Same as in [Lab 1](../lab1/README.md) we require bearer tokens in JWT format to authenticate at our resource server. |
| 216 | + |
| 217 | +To do this we will need to run the copied access token from the JWT generator web application in the previous step. |
| 218 | + |
| 219 | +To make a request for a list of users we have to |
| 220 | +specify the access token as part of a _Authorization_ header of type _Bearer_ like this: |
| 221 | + |
| 222 | +httpie: |
| 223 | + |
| 224 | +```bash |
| 225 | +http localhost:9091/library-server/users \ |
| 226 | +'Authorization: Bearer [access_token]' |
12 | 227 | ```
|
13 | 228 |
|
| 229 | +curl: |
| 230 | + |
| 231 | +```bash |
| 232 | +curl -H 'Authorization: Bearer [access_token]' \ |
| 233 | +-v http://localhost:9091/library-server/users | jq |
14 | 234 | ```
|
15 |
| -HTTP/1.1 200 OK |
16 |
| -Content-Type: application/json |
17 | 235 |
|
| 236 | +You have to replace _[access_token]_ with the one you have obtained from the |
| 237 | +JWt generator application. |
| 238 | + |
| 239 | +Navigate your web browser to [jwt.io](https://jwt.io) and paste your access token into the |
| 240 | +_Encoded_ text field. |
| 241 | + |
| 242 | + |
| 243 | + |
| 244 | +If you scroll down a bit on the right hand side then you will see the following block |
| 245 | +(depending on which user you have specified when generating a JWT): |
| 246 | + |
| 247 | +```json |
18 | 248 | {
|
19 |
| - "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgO...", |
20 |
| - "expires_in": 300, |
21 |
| - "not-before-policy": 1556650611, |
22 |
| - "refresh_expires_in": 1800, |
23 |
| - "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIg...", |
24 |
| - "scope": "profile email user", |
25 |
| - "session_state": "c92a82d1-8e6d-44d7-a2f3-02f621066968", |
26 |
| - "token_type": "bearer" |
| 249 | + "scope": "library_admin email profile", |
| 250 | + "email_verified": true, |
| 251 | + "name": "Clark Kent", |
| 252 | + "groups": [ |
| 253 | + "library_admin" |
| 254 | + ], |
| 255 | + "preferred_username": "ckent", |
| 256 | + "given_name": "Clark", |
| 257 | + "family_name": "Kent", |
| 258 | + |
27 | 259 | }
|
28 | 260 | ```
|
| 261 | +As you can see our user has the scopes _library_admin_, _email_ and _profile_. |
| 262 | +These scopes are now mapped to the Spring Security authorities |
| 263 | +_SCOPE_library_admin_, _SCOPE_email_ and _SCOPE_profile_. |
29 | 264 |
|
| 265 | + |
30 | 266 |
|
| 267 | +This request should succeed with an '200' OK status and return a list of users. |
31 | 268 |
|
| 269 | +<hr> |
32 | 270 |
|
33 |
| - |
| 271 | +This concludes the final Lab 4 and the whole hands-on workshop part. |
0 commit comments