├── .gitignore ├── .sdkmanrc ├── LICENSE ├── README.adoc ├── README.html ├── api.adoc ├── api ├── pom.xml ├── run.sh └── src │ ├── main │ ├── java │ │ └── bootiful │ │ │ └── api │ │ │ ├── ApiApplication.java │ │ │ ├── ClientCredentialsClient.java │ │ │ ├── Customer.java │ │ │ ├── CustomerHttpController.java │ │ │ ├── CustomerRepository.java │ │ │ ├── EmailController.java │ │ │ ├── EmailRequestsIntegrationFlowConfiguration.java │ │ │ └── MeHttpController.java │ └── resources │ │ ├── application.properties │ │ ├── data.sql │ │ └── schema.sql │ └── test │ └── java │ └── bootiful │ └── api │ └── ApiApplicationTests.java ├── authorization-server.adoc ├── authorization-server ├── native.sh ├── pom.xml ├── run.sh ├── snippets │ ├── ClientsConfiguration.java │ ├── DumbestUserDetailsService.java │ ├── DumbestUserDetailsServiceConfiguration.java │ ├── KSMain.java │ ├── SimpleKeyConfiguration.java │ ├── UserDetailsConfiguration.java │ ├── app.key │ ├── app.pub │ ├── default-user.properties │ └── registered-clients.yaml └── src │ ├── main │ ├── java │ │ └── bootiful │ │ │ └── authorizationserver │ │ │ ├── AotConfiguration.java │ │ │ ├── AuthorizationConfiguration.java │ │ │ ├── AuthorizationServerApplication.java │ │ │ ├── ClientsConfiguration.java │ │ │ ├── SecurityConfiguration.java │ │ │ ├── UsersConfiguration.java │ │ │ └── keys │ │ │ ├── Converters.java │ │ │ ├── JdbcRsaKeyPairRepository.java │ │ │ ├── KeyConfiguration.java │ │ │ ├── Keys.java │ │ │ ├── LifecycleConfiguration.java │ │ │ ├── RsaKeyPair.java │ │ │ ├── RsaKeyPairGenerationRequestEvent.java │ │ │ ├── RsaKeyPairRepository.java │ │ │ ├── RsaKeyPairRepositoryJWKSource.java │ │ │ ├── RsaKeyPairRowMapper.java │ │ │ ├── RsaPrivateKeyConverter.java │ │ │ └── RsaPublicKeyConverter.java │ └── resources │ │ ├── application.properties │ │ └── sql │ │ └── schema │ │ ├── oauth2-authorization-consent-schema.sql │ │ ├── oauth2-authorization-schema.sql │ │ ├── oauth2-registered-client.sql │ │ ├── rsa_key_pairs.sql │ │ ├── spring-session-jdbc.sql │ │ └── users.sql │ └── test │ └── java │ └── bootiful │ └── authorizationserver │ └── AuthorizationServerApplicationTests.java ├── docker-compose.yml ├── federated-oauth.adoc ├── frontmatter.adoc ├── gateway.adoc ├── gateway ├── pom.xml ├── run.sh └── src │ ├── main │ ├── java │ │ └── bootiful │ │ │ └── gateway │ │ │ ├── GatewayApplication.java │ │ │ ├── GatewayConfiguration.java │ │ │ └── SecurityConfiguration.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── bootiful │ └── gateway │ └── GatewayApplicationTests.java ├── idea.sh ├── images ├── .DS_Store ├── google-consent-screen.png ├── google-is-it-you.png ├── google-mfa.png ├── google-signin.png ├── yelp-logged-in-with-google-part-2.png ├── yelp-logged-in-with-google.png └── yelp-signup.png ├── java.adoc ├── java ├── .github │ └── workflows │ │ └── deploy.yml ├── LICENSE ├── pom.xml ├── run.sh ├── snippets │ └── traditional-io └── src │ ├── main │ ├── java │ │ └── bootiful │ │ │ └── javareloaded │ │ │ └── Main.java │ └── resources │ │ └── application.properties │ └── test │ ├── java │ └── bootiful │ │ └── javareloaded │ │ ├── LambdasTest.java │ │ ├── MultilineStringsTest.java │ │ ├── SealedTypesTest.java │ │ ├── SmartCastsTest.java │ │ ├── closeable │ │ ├── TraditionalResourceHandlingTest.java │ │ ├── TryWithResourcesTest.java │ │ └── Utils.java │ │ ├── loom │ │ └── LoomTest.java │ │ ├── patternmatching │ │ └── PatternMatchingTest.java │ │ ├── records │ │ ├── RecordConstructorsTest.java │ │ └── SimpleRecordsTest.java │ │ ├── switches │ │ ├── EnhancedSwitchExpressionTest.java │ │ └── TraditionalSwitchExpressionTest.java │ │ └── typeinference │ │ ├── LambdasAndTypeInferenceTest.java │ │ └── TypeInferenceTest.java │ └── resources │ └── data ├── oauth.adoc ├── persistence.adoc ├── preview.sh ├── processor.adoc ├── processor ├── pom.xml ├── run.sh └── src │ ├── main │ ├── java │ │ └── bootiful │ │ │ └── processor │ │ │ ├── AmqpConfiguration.java │ │ │ ├── Constants.java │ │ │ ├── IntegrationConfiguration.java │ │ │ ├── JwtAuthenticationInterceptor.java │ │ │ ├── ProcessorApplication.java │ │ │ └── SecurityConfiguration.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── bootiful │ └── processor │ └── ProcessorApplicationTests.java └── static ├── app.css ├── app.js ├── index.html └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | .DS_Store 39 | target 40 | -------------------------------------------------------------------------------- /.sdkmanrc: -------------------------------------------------------------------------------- 1 | # Enable auto-env through the sdkman_auto_env config 2 | # Add key=value pairs of SDKs to use below 3 | java=23.0.1-graalce -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | :tilde: ~ 2 | :author: Josh Long 3 | :email: josh@joshlong.com 4 | :revnumber: 1.0 5 | :revdate: {docdate} 6 | :revyear: 2023 7 | :keywords: the Spring Authorization Serer 8 | :doctype: book 9 | :media: screen 10 | ifeval::["{media}" != "prepress"] 11 | :front-cover-image: image:cover.png[Front Cover,1050,1600] 12 | endif::[] 13 | :toc: 14 | :toc-placement: macro 15 | :icons: font 16 | :lang: en 17 | :language: javadocript 18 | :experimental: 19 | :pdf-fontsdir: ./styles/pdf/fonts 20 | :pdf-stylesdir: ./styles/pdf 21 | :pdf-style: screen 22 | :leveloffset: 1 23 | // ifndef::ebook-format[:leveloffset: 1] 24 | toc::[] 25 | :sectnums!: 26 | [frontmatter] 27 | = Frontmatter 28 | 29 | include::frontmatter.adoc[] 30 | 31 | = The Spring Authorization Server 32 | include::oauth.adoc[] 33 | include::authorization-server.adoc[] 34 | include::persistence.adoc[] 35 | include::federated-oauth.adoc[] 36 | 37 | = OAuth all the Things 38 | 39 | include::api.adoc[] 40 | include::gateway.adoc[] 41 | include::processor.adoc[] 42 | 43 | [appendix] 44 | = Appendix 45 | 46 | include::java.adoc[] -------------------------------------------------------------------------------- /api.adoc: -------------------------------------------------------------------------------- 1 | = Protecting a Simple HTTP API 2 | 3 | Let's build an HTTP API. 4 | Honestly, this is just a pretense to have something to secure with the Spring Security OAuth support, so we'll make this quick. 5 | First, go to the https://start.spring.io[Spring Initializr], and then add the following dependencies: ` 6 | 7 | * `Spring for RabbitMQ` 8 | * `Spring Integration` 9 | * `Spring Web` 10 | * `OAuth2 Resource Server` 11 | * `Spring Data JDBC` 12 | * `PostgreSQL Driver` 13 | 14 | I gave the newly minted project a group of `bootiful` and named it `api`. 15 | 16 | Click `Generate`, unzip the newly minted `.zip` file, and then open the project in your IDE. 17 | 18 | The domain's a trivial one: customer data, like a CRM (customer relationship manager). Each `Customer` entity has an `id` field, a `name` field, and an `email`. 19 | 20 | [source,java] 21 | ----- 22 | include::api/src/main/java/bootiful/api/Customer.java[] 23 | ----- 24 | 25 | And of course there's a Spring Data JDBC repository to make working with that data easier... 26 | 27 | [source,java] 28 | ----- 29 | include::api/src/main/java/bootiful/api/CustomerRepository.java[] 30 | ----- 31 | 32 | The Spring Data JDBC repository talks to a SQL table, called `customer`, whose definition we must specify. 33 | 34 | [source,sql] 35 | ----- 36 | include::api/src/main/resources/schema.sql[] 37 | ----- 38 | 39 | Let's insert some `customer` records, just for good measure, so that we have something to look at, and so that the system is in a well-known state. 40 | 41 | [source,sql] 42 | ----- 43 | include::api/src/main/resources/data.sql[] 44 | ----- 45 | 46 | And if we write data in a database but have no HTTP controller by which to examine the data, did we actually write any data at all? Philosophers disagree, but just to be safe let's build an HTTP controller with good ol' Spring MVC. 47 | 48 | [source,java] 49 | ----- 50 | include::api/src/main/java/bootiful/api/CustomerHttpController.java[] 51 | ----- 52 | 53 | Remember we're going to be securing this with Spring Security's OAuth support, and just to be sure everything's worked, we'll have a simple HTTP endpoint that injects and then spits out the current authenticated user's username, so you can see who the system thinks is authenticated at the moment. 54 | 55 | This is particularly useful in the client, which is goign to want to at least acknowldge that the user has successfully authenticated with a message like, `Welcome `, where `` might be `jlong`. You might also leak other inforamation to the client, like the first name and last name. 56 | 57 | [source,java] 58 | ----- 59 | include::api/src/main/java/bootiful/api/MeHttpController.java[] 60 | ----- 61 | 62 | And, finally, we've got a little integration that sends a message using the AMQP protocol via RabbitMQ to another service called `processor`. 63 | We'll introduce that thing later. 64 | The idea is that you'll be able to click a button to get some sort of email sent to each user. We're not going to actually send an email. 65 | I haven't really thought out what sort of email we would send if we were! 66 | Just use your imagination. 67 | 68 | The significant bit here is that sending email is one of those things you don't necessarily want to do in the hot path of handling HTTP requests. It should be farmed out to another node in the system which can be dedicated to scaling up and down as is required to handle the email-sending load, and to scale independently of the front-office HTTP traffic load. 69 | Sending email may take a while, be error-prone, etc. It's the kind of dirty integration stuff that Spring Integration is purpose built for. So we'll use Spring Integration. 70 | 71 | 72 | TIP: This is what we used to call a backoffice process. 73 | I'm calling it `processor`, because I'm amazing with names, like everyone on the Spring team is. 74 | We named our MVC framework Spring MVC, our data framework Spring Data, our batch framework Spring Batch... Well, you get the idea. 75 | (Don't ask about Spring Boot, tho!) 76 | 77 | Requests originate here, in this controller. 78 | Each request contains a `Customer` payload, and header, `jwt`, which contains the JWT associated with the current authenticated user. 79 | We'll use that JWT later to validate the request is from a trusted, authenticated, user. 80 | 81 | [source,java] 82 | ----- 83 | include::api/src/main/java/bootiful/api/EmailController.java[] 84 | ----- 85 | 86 | And there's a bit of Spring Integration plumbing to route those requests to our RabbitMQ broker running in a separate process. 87 | This `IntegrationFlow` looks at requests (which come in the shape of a `Message` object, which has headers and a payload) coming in from the injected `MessageChannel`, transforms them into JSON data, and then sends them, along with a JWT token associated to the current authenticated user, on to the broker, where it'll eventually get delivered to the consumer, `processor`. 88 | 89 | [source,java] 90 | ----- 91 | include::api/src/main/java/bootiful/api/EmailRequestsIntegrationFlowConfiguration.java[] 92 | ----- 93 | 94 | <1> inbound adapters translate events from the real world into Spring Framework `Message` objects. 95 | Outbound adapters translate `Message` objects into events in the real world. 96 | This adapter lets us interface with RabbitMQ via the AMQP protocol. 97 | <2> In this case, messages pass through the `MessageChannel`... 98 | <3> ...and into the next stage in the flow, a transformer, which will translate the `Message` into a `Message`, with a JSON payload 99 | <4> and from there, it gets routed to the outbound AMQP adapter, which will translate the Spring Framework `Message` into a request sent over AMQP to the RabbitMQ broker 100 | 101 | We're using Spring Security's Resource Server support to protect requests to the API, rejecting requests that don't have a valid OAuth 2 token. 102 | It does this by connecting to the OAuth 2 IDP (our amazing Spring Authorization Server instance) and validating the JWT. 103 | 104 | The Spring Security Resource Server support, the Spring Data support, the Spring Integration AMQP support, all of it requires configuration, which brings us to our `application.properties`. 105 | 106 | [source,properties] 107 | ---- 108 | include::api/src/main/resources/application.properties[] 109 | ---- 110 | <1> the issuer URI is the address of the Spring Authorization Server against which Spring Security can validate a JWT token 111 | <2> we need to connect to the RabbitMQ instance.. 112 | <3> and the PostgresSQL database... 113 | <4> the Spring Authorization Server is already running on port `8080`, so we'll need to run this Java application on port `8081`. 114 | (Remember that!) 115 | 116 | WARNING: I've hardcoded database connectivity credentials and RabbitMQ credentials and so on. It's worth restating: don't do this in a production application. Use environment variables or the Spring Cloud Config Server or Spring Cloud Vault or any of the infinitely more secure approaches than hardcoding credentials in plaintext on a public Git repository! 117 | 118 | With all that in place, you should be able to start the application. 119 | It's got a REST endpoint. 120 | You can try it out by hitting the new `/customers` endpoint that we created. 121 | 122 | [source,shell] 123 | ---- 124 | curl http://localhost:8081/customers 125 | ---- 126 | 127 | But it fails! 128 | Which is good. 129 | It fails because it's an authenticated request. 130 | We need a valid JWT token. 131 | We can get one because, when we registered the client with the Spring Authorization Server, we listed `client_credentials` as an authorized grant type. 132 | This in turn allows us to make a request without a user context. 133 | And if there's no user, then there's no need to verify the user, and thus no need for a browser or a web page or anything. 134 | We only need the client ID and client secret: `crm`/`crm`. 135 | 136 | [source,shell] 137 | ---- 138 | curl -X POST \ 139 | -H "Authorization: Basic $(echo -n 'crm:crm' | base64)" \ 140 | -H "Content-Type: application/x-www-form-urlencoded" \ 141 | -d "grant_type=client_credentials&scope=user.read" \ 142 | http://localhost:8080/oauth2/token 143 | ---- 144 | 145 | That should dump a token in a JSON document. 146 | On my machine I got this very long JSON document that I've abbreviated for you here. 147 | 148 | [source,json] 149 | ---- 150 | {"access_token":"eyJraWQiOiI4YzQyNGU...Qy1Fg","scope":"user.read","token_type":"Bearer","expires_in":299} 151 | ---- 152 | 153 | The important bit is the `access_token` attribute. 154 | If you have the `jq` command line utility installed, you can extract out the `access_token` like this 155 | 156 | [source,shell] 157 | ---- 158 | TOKEN=$( curl -X POST \ 159 | -H "Authorization: Basic $(echo -n 'crm:crm' | base64)" \ 160 | -H "Content-Type: application/x-www-form-urlencoded" \ 161 | -d "grant_type=client_credentials&scope=user.read" \ 162 | http://localhost:8080/oauth2/token | jq -r .access_token ) 163 | ---- 164 | 165 | By the way, you could also issue the same request using Spring's `RestTemplate` or `WebClient` or `RestClient`. 166 | Here's the equivalent using ye ole `RestTemplate`: 167 | 168 | [source,java] 169 | ---- 170 | include::api/src/main/java/bootiful/api/ClientCredentialsClient.java[] 171 | ---- 172 | 173 | You can use this token to then issue a request to the HTTP server: 174 | 175 | [source,shell] 176 | ----- 177 | curl -H "Authorization: Bearer $TOKEN" http://localhost:8081/customers 178 | ----- 179 | 180 | And there's the data! 181 | At long last, reunited with the data. 182 | Feels good doesn't it? 183 | We've got an HTTP API that is secure and all we had to do was specify the `issuer-uri` int he property file. 184 | Our Spring Authorization Server is paying dividends already! 185 | We've got honest-to-goodness identity management, security, and more, all for the cost of one lousy little property. 186 | And if we want to protect any other microservices, its the same story. 187 | One little `issuer-uri` property, and our services will automatically be protected and automatically be able to work with the identities in the centralized Spring Authorization Server. 188 | 189 | I love this for us. 190 | We did something amazing here. 191 | Do you feel the possibilities? 192 | We stood up one little Spring Authorization Server and suddenly every microservice in our system can be protected. 193 | No need to redundantly duplicate the requests. 194 | 195 | We sort of cheated here, though. 196 | We got a token using the `client_credentials` authorization grant type. 197 | No user context, remember? 198 | We _want_ users. 199 | That's sort of the point of this whole exercise. 200 | Somewhere, somehow, we'll need to get a user involved. 201 | Once they're involved, they'll have a token that we can use to make requests to this resource server. 202 | The thing that originates the token, that forces the redirect to the OAuth IDP where the user will be asked to consent? 203 | That's called an OAuth 2 client. 204 | An OAuth 2 client is both an OAuth 2 resource server, in that it'll reject invalid requests, _and_ it has this extra ability to initiate and handle the OAuth flow - the "OAuth dance" - we looked at earlier when we discussed Yelp.com's authentication flow. 205 | Let's build one. -------------------------------------------------------------------------------- /api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.2.0-M3 9 | 10 | 11 | bootiful 12 | api 13 | 0.0.1-SNAPSHOT 14 | api 15 | Demo project for Spring Boot 16 | 17 | 21 18 | 19 | 20 | 21 | org.postgresql 22 | postgresql 23 | runtime 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-amqp 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-data-jdbc 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-integration 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-oauth2-resource-server 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-web 45 | 46 | 47 | org.springframework.integration 48 | spring-integration-amqp 49 | 50 | 51 | org.springframework.integration 52 | spring-integration-http 53 | 54 | 55 | org.springframework.integration 56 | spring-integration-jdbc 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-test 62 | test 63 | 64 | 65 | org.springframework.amqp 66 | spring-rabbit-test 67 | test 68 | 69 | 70 | org.springframework.integration 71 | spring-integration-test 72 | test 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-maven-plugin 81 | 82 | 83 | 84 | 85 | 86 | spring-milestones 87 | Spring Milestones 88 | https://repo.spring.io/milestone 89 | 90 | false 91 | 92 | 93 | 94 | 95 | 96 | spring-milestones 97 | Spring Milestones 98 | https://repo.spring.io/milestone 99 | 100 | false 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /api/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mvn clean spring-boot:run 3 | -------------------------------------------------------------------------------- /api/src/main/java/bootiful/api/ApiApplication.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ApiApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ApiApplication.class, args); 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /api/src/main/java/bootiful/api/ClientCredentialsClient.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import org.springframework.http.HttpEntity; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.util.Assert; 8 | import org.springframework.util.LinkedMultiValueMap; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | class ClientCredentialsClient { 12 | 13 | String getJwtToken(RestTemplate restTemplate, String clientId, String clientSecret) { 14 | var headers = new HttpHeaders(); 15 | headers.setBasicAuth(clientId, clientSecret); 16 | headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); 17 | 18 | var body = new LinkedMultiValueMap(); 19 | body.add("grant_type", "client_credentials"); 20 | body.add("scope", "user.read"); 21 | 22 | var entity = new HttpEntity<>(body, headers); 23 | var url = "http://localhost:8080/oauth2/token"; 24 | var response = restTemplate.postForEntity(url, entity, JsonNode.class); 25 | Assert.state(response.getStatusCode().is2xxSuccessful(), "the response needs to be 200x"); 26 | return response.getBody().get("access_token").asText(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /api/src/main/java/bootiful/api/Customer.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import org.springframework.data.annotation.Id; 4 | 5 | record Customer(@Id Integer id, String name, String email) { 6 | } 7 | -------------------------------------------------------------------------------- /api/src/main/java/bootiful/api/CustomerHttpController.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | import java.util.Collection; 7 | 8 | @RestController 9 | class CustomerHttpController { 10 | 11 | private final CustomerRepository repository; 12 | 13 | CustomerHttpController(CustomerRepository repository) { 14 | this.repository = repository; 15 | } 16 | 17 | @GetMapping("/customers") 18 | Collection customers() { 19 | return this.repository.findAll(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /api/src/main/java/bootiful/api/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import org.springframework.data.repository.ListCrudRepository; 4 | 5 | interface CustomerRepository extends ListCrudRepository { 6 | Customer findCustomerById(Integer id); 7 | } 8 | -------------------------------------------------------------------------------- /api/src/main/java/bootiful/api/EmailController.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import org.springframework.messaging.MessageChannel; 4 | import org.springframework.messaging.support.MessageBuilder; 5 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 6 | import org.springframework.security.oauth2.jwt.Jwt; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | 12 | import java.util.Map; 13 | 14 | @Controller 15 | @ResponseBody 16 | class EmailController { 17 | 18 | private final MessageChannel requests; 19 | 20 | private final CustomerRepository repository; 21 | 22 | EmailController(CustomerRepository repository, MessageChannel requests) { 23 | this.requests = requests; 24 | this.repository = repository; 25 | } 26 | 27 | @PostMapping("/email") 28 | Map email( 29 | @AuthenticationPrincipal Jwt jwt, 30 | @RequestParam Integer customerId) { 31 | var token = jwt.getTokenValue(); 32 | var message = MessageBuilder 33 | .withPayload(repository.findCustomerById(customerId)) 34 | .setHeader("jwt", token) 35 | .build(); 36 | var sent = this.requests.send(message); 37 | return Map.of("sent", sent, "customerId", customerId); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /api/src/main/java/bootiful/api/EmailRequestsIntegrationFlowConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import org.springframework.amqp.core.AmqpTemplate; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.integration.amqp.dsl.Amqp; 7 | import org.springframework.integration.dsl.DirectChannelSpec; 8 | import org.springframework.integration.dsl.IntegrationFlow; 9 | import org.springframework.integration.dsl.MessageChannels; 10 | import org.springframework.integration.json.ObjectToJsonTransformer; 11 | import org.springframework.messaging.MessageChannel; 12 | 13 | @Configuration 14 | class EmailRequestsIntegrationFlowConfiguration { 15 | 16 | private final String destinationName = "emails"; 17 | 18 | @Bean 19 | IntegrationFlow emailRequestsIntegrationFlow(MessageChannel requests, AmqpTemplate template) { 20 | // <1> 21 | var outboundAmqpAdapter = Amqp 22 | .outboundAdapter(template) 23 | .routingKey(this.destinationName); 24 | 25 | return IntegrationFlow 26 | .from(requests)// <2> 27 | .transform(new ObjectToJsonTransformer()) // <3> 28 | .handle(outboundAmqpAdapter) // <4> 29 | .get(); 30 | } 31 | 32 | @Bean 33 | DirectChannelSpec requests() { 34 | return MessageChannels.direct(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /api/src/main/java/bootiful/api/MeHttpController.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | 7 | import java.security.Principal; 8 | import java.util.Map; 9 | 10 | @Controller 11 | @ResponseBody 12 | class MeHttpController { 13 | 14 | @GetMapping("/me") 15 | Map principal(Principal principal) { 16 | return Map.of("name", principal.getName()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # <1> 2 | spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080 3 | 4 | # <2> 5 | spring.rabbitmq.username=user 6 | spring.rabbitmq.password=password 7 | spring.sql.init.mode=always 8 | 9 | # <3> 10 | spring.datasource.username=postgres 11 | spring.datasource.password=postgres 12 | spring.datasource.url=jdbc:postgresql://localhost/postgres 13 | 14 | # <4> 15 | server.port=8081 16 | 17 | -------------------------------------------------------------------------------- /api/src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | delete from customer; 2 | insert into customer (name,email) values ('Violetta Giorgieva', 'vg@email.com'); 3 | insert into customer (name,email) values ('Madhura Bhave' , 'mb@email.com'); 4 | insert into customer (name,email) values ('David Syer' ,'ds@email.com'); 5 | insert into customer (name,email) values ('Josh Long' , 'jl@email.com'); 6 | insert into customer (name,email) values ('Stéphane Nicoll' , 'sn@email.com'); 7 | insert into customer (name,email) values ('Jürgen Hoeller' , 'jh@email.com'); 8 | insert into customer (name,email) values ('Audrey Neveu' , 'an@email.com'); 9 | insert into customer (name,email) values ('Yuxin Bae' , 'yb@email.com'); -------------------------------------------------------------------------------- /api/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists customer 2 | ( 3 | id serial primary key, 4 | name varchar(255) not null, 5 | email varchar(255) not null 6 | ); -------------------------------------------------------------------------------- /api/src/test/java/bootiful/api/ApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package bootiful.api; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ApiApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /authorization-server/native.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rm -rf build 3 | ./gradlew nativeCompile && ./build/native/nativeCompile/authorization-server 4 | -------------------------------------------------------------------------------- /authorization-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.3.4 9 | 10 | 11 | bootiful 12 | authorization-server 13 | 0.0.1-SNAPSHOT 14 | authorization-server 15 | Demo project for Spring Boot 16 | 17 | 21 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-jdbc 23 | 24 | 25 | org.reflections 26 | reflections 27 | 0.10.2 28 | 29 | 30 | org.postgresql 31 | postgresql 32 | runtime 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-oauth2-authorization-server 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-security 41 | 42 | 43 | org.springframework.session 44 | spring-session-jdbc 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-test 50 | test 51 | 52 | 53 | org.springframework.security 54 | spring-security-test 55 | test 56 | 57 | 58 | 59 | 60 | 61 | 62 | org.graalvm.buildtools 63 | native-maven-plugin 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-maven-plugin 68 | 69 | 70 | 71 | 72 | 73 | spring-milestones 74 | Spring Milestones 75 | https://repo.spring.io/milestone 76 | 77 | false 78 | 79 | 80 | 81 | 82 | 83 | spring-milestones 84 | Spring Milestones 85 | https://repo.spring.io/milestone 86 | 87 | false 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /authorization-server/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mvn clean spring-boot:run 3 | -------------------------------------------------------------------------------- /authorization-server/snippets/ClientsConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 6 | import org.springframework.security.oauth2.core.ClientAuthenticationMethod; 7 | import org.springframework.security.oauth2.core.oidc.OidcScopes; 8 | import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; 9 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; 10 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; 11 | 12 | import java.util.Set; 13 | import java.util.UUID; 14 | 15 | @Configuration 16 | class ClientsConfiguration { 17 | 18 | @Bean 19 | RegisteredClientRepository registeredClientRepository() { 20 | // <1> 21 | var registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) 22 | .clientId("crm") 23 | .clientSecret("{bcrypt}$2a$10$m7dGi0viwVH63EjwZc6UdeUQxPuiVEEdFbZFI9nMxHAASTOIDlaVO") 24 | .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) 25 | .authorizationGrantTypes(grantTypes -> grantTypes.addAll(Set.of( 26 | AuthorizationGrantType.CLIENT_CREDENTIALS, 27 | AuthorizationGrantType.AUTHORIZATION_CODE, 28 | AuthorizationGrantType.REFRESH_TOKEN))) 29 | .redirectUri("http://127.0.0.1:8082/login/oauth2/code/spring") 30 | .scopes(scopes -> scopes.addAll(Set.of("user.read", "user.write", OidcScopes.OPENID))) 31 | .build(); 32 | return new InMemoryRegisteredClientRepository(registeredClient); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /authorization-server/snippets/DumbestUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 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 | import org.springframework.util.Assert; 8 | 9 | import java.util.Map; 10 | import java.util.Set; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | import java.util.stream.Collectors; 13 | 14 | class DumbestUserDetailsService implements UserDetailsService { 15 | 16 | private final Map users; 17 | 18 | DumbestUserDetailsService( Set userDetails) { 19 | var map = userDetails 20 | .stream() 21 | .collect(Collectors.toMap(UserDetails::getUsername, ud -> ud)); 22 | this.users = new ConcurrentHashMap<>(map); 23 | } 24 | 25 | @Override 26 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 27 | Assert.hasText(username, "the username must not be null"); 28 | var result = this.users.getOrDefault(username, null); 29 | if (null == result) 30 | throw new UsernameNotFoundException("the user %s could not be found".formatted(username)); 31 | return result; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /authorization-server/snippets/DumbestUserDetailsServiceConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | 5 | @Configuration 6 | class DumbestUserDetailsConfiguration { 7 | 8 | @Bean 9 | UserDetailsService userDetailsService() { 10 | var builder = withDefaultPasswordEncoder(); 11 | return new DumbestUserDetailsService(Set.of( 12 | builder.roles("USER").username("jlong").password("password").build(), 13 | builder.roles("USER", "ADMIN").username("rwinch").password("p@ssw0rd").build()) 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /authorization-server/snippets/KSMain.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import javax.crypto.SecretKey; 4 | import java.io.FileInputStream; 5 | import java.security.KeyStore; 6 | import java.security.PrivateKey; 7 | import java.security.PublicKey; 8 | 9 | public class KSMain { 10 | 11 | static void keystore() throws Exception { 12 | 13 | // this program assumes you've imported a key into a Java KeyStore file with the following use of keytool 14 | //keytool -importkeystore -deststorepass -destkeystore .jks -srckeystore -srcstoretype PKCS8 -srcstorepass 15 | 16 | var keystorePasswordCharArray = "password".toCharArray(); // replace with your keystore password 17 | var keyPasswordCharArray = "keypassword".toCharArray(); // replace with your key's password 18 | 19 | var ks = KeyStore.getInstance("JKS"); 20 | 21 | // Load keystore from file 22 | try (var fis = new FileInputStream("keystore.jks")) { 23 | ks.load(fis, keystorePasswordCharArray); 24 | } 25 | 26 | // Get key from keystore 27 | var key = ks.getKey("mykeyalias", keyPasswordCharArray); // replace "mykeyalias" with your key's alias 28 | if (key instanceof SecretKey secretKey) { 29 | // Handle secret key 30 | 31 | }// 32 | else if (key instanceof PublicKey publicKey) { 33 | // Handle private (or public) key 34 | }// 35 | else if (key instanceof PrivateKey privateKey) { 36 | 37 | } 38 | 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /authorization-server/snippets/SimpleKeyConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import com.nimbusds.jose.jwk.source.JWKSource; 4 | import com.nimbusds.jose.proc.SecurityContext; 5 | import java.security.interfaces.RSAPrivateKey; 6 | import java.security.interfaces.RSAPublicKey; 7 | import com.nimbusds.jose.jwk.RSAKey; 8 | import com.nimbusds.jose.jwk.JWKSet; 9 | import com.nimbusds.jose.jwk.source.ImmutableJWKSet; 10 | 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.context.annotation.Bean; 13 | 14 | @Configuration 15 | class SimpleKeyConfiguration { 16 | 17 | @Bean 18 | JWKSource jwkSource( 19 | @Value("${jwt.key.id}") String id, 20 | @Value("${jwt.key.private}") RSAPrivateKey privateKey, 21 | @Value("${jwt.key.public}") RSAPublicKey publicKey) { 22 | var rsa = new RSAKey.Builder(publicKey) 23 | .privateKey(privateKey) 24 | .keyID(id) 25 | .build(); 26 | var jwk = new JWKSet(rsa); 27 | return new ImmutableJWKSet<>(jwk); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /authorization-server/snippets/UserDetailsConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.core.userdetails.User; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 8 | 9 | @Configuration 10 | class UserDetailsConfiguration { 11 | 12 | @Bean 13 | UserDetailsService inMemoryUserDetailsManager() { 14 | var userBuilder = User.withDefaultPasswordEncoder(); 15 | return new InMemoryUserDetailsManager( 16 | userBuilder.roles("USER").username("jlong").password("password").build(), 17 | userBuilder.roles("USER", "ADMIN").username("rwinch").password("p@ssw0rd").build() 18 | ); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /authorization-server/snippets/app.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC2twqRdARZMYWb 3 | MfGDte2DLU/lkrDXEdrk6rt9e69PtRRkQJ/1IfP1KBqB1Jbt9B066TflRVgnnKQ6 4 | NxHCrMrihov+n7dsIMqAvDa8Z2xGB/O5PyTyDyQAZBIZU4zhJZKrrW+zZk6UEBPT 5 | EOgrFZfN52KIULQ3umpjwu7iz8fZdwXyrCPEzTagaYruA2hOzY9haAKEXeeq8KPT 6 | hWeIj1YPhUffbxxW9GVhKA/bjdJIzzW16M70glnvDiLBfMIGpRevf77h88/JvWko 7 | kAZFS3J+O4g6vrqdlVFXZCUc24K4QflmwFR7HKDLG6KMWTkMIcaUmOHCaDmp8vmE 8 | UyxuXmGDAgMBAAECggEAebBu2XDrdHwG/9XDhHUmOrdy/vMz1AmQP+YV+PznRa7U 9 | ZfCkmB6E3EJZZR6xZsmurg2lrI0CqV8qAZuruHxco4H4uxykjN9J/3NbAR/gfMPP 10 | DxF/CbgLwjbj1vpOWaUsiip4uoLo0rPigDBCcG9xKzFv7lnRrUv6j3bEo0q/T5VU 11 | 4GdJyf8GM0sa8gsKpUdgLcBRjnRgKyJ5zuWOIwSb/zKEQTkMXbwW/nZAQaiYzyPQ 12 | 5yOD4FRa6+jPadX6TWGCReds/3OvJZ8swoJdIkY71O7InXQKcP1ZDzrb7B2D+Hjv 13 | QJSQUQttVKSGhdcyk4kntOypwu9weoGr7om3MB0gAQKBgQDqORquJ2426JgYTPYh 14 | mbBuQK7Hvd479cPuFX0WM7Fy9GDdY0BbYn7hHA/584AWAlUdj737DWcmJyp90GzZ 15 | Hq4ZDjoRj32UmDJQBGWnb61STHPctfUdt++lyflGMvzU2t+kl9Jjs9fpc0NIFn1p 16 | +YMurQ6iI2Pj4iyp4UhTUv2yowKBgQDHs/XPaeTo5qMxtRlOrqB5xzIaCCcvpuUs 17 | caYtPvxML7PJJj1QNhk+G7ngRsgjLJivu6GREKE8aaZ8qwZQTMZ7BUAflLXFLhS/ 18 | +1l7Jb0ax604lbLbXbgBD+2e32XnitJVOMKsndC8yRniGDfRcx0erBzWPUYZGa+Q 19 | SuRW5BRjoQKBgFtuZTLco5J9o3nA+UfOhefUCiZgwNLpMk3LR6QWE8wLB5EEgIfr 20 | 4Bmh6b6pxjNRP8alaQUKi1yCZ3zrksICzvVq71IRkHUkIGfJ/6Cn4KHCxGvA/+lU 21 | 9xDh0hQMLVQuCKVourE+8CbqXrZSSSzIQOREm/TBGepITSUXkzMrr2s/AoGAPrt2 22 | TRh7svm4bAXylDfg60A6qdjDzoFD3mk5BV+Sy6/0bwyXGBpWZZ86DYzOk9YPhKyR 23 | PUuXCq45gVIMIq9rbfuhQApr6yvlksU3P6sEM2RkMrE7xRM0mQjS4SRWE50VI3Y4 24 | GRdeGd+mRNLMvfvGOvtf96C542qhLteH0Q/Go+ECgYBMdmP60zEoBVp1nw7ZZIAm 25 | i2S8z/K3SCdw2s3dqChYD0VQRi/c/UEqXlRv8svPJC6G5SOpaS1izBUz4wYo2sIP 26 | Ze6RU+GClzdkqcyunSr0WZZhfCRFoW3iDo76QDPWP6FeG247QLuTuED/EqS75fGS 27 | 1x1j2aHQjCYm6qXj7WwDBg== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /authorization-server/snippets/app.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrcKkXQEWTGFmzHxg7Xt 3 | gy1P5ZKw1xHa5Oq7fXuvT7UUZECf9SHz9SgagdSW7fQdOuk35UVYJ5ykOjcRwqzK 4 | 4oaL/p+3bCDKgLw2vGdsRgfzuT8k8g8kAGQSGVOM4SWSq61vs2ZOlBAT0xDoKxWX 5 | zediiFC0N7pqY8Lu4s/H2XcF8qwjxM02oGmK7gNoTs2PYWgChF3nqvCj04VniI9W 6 | D4VH328cVvRlYSgP243SSM81tejO9IJZ7w4iwXzCBqUXr3++4fPPyb1pKJAGRUty 7 | fjuIOr66nZVRV2QlHNuCuEH5ZsBUexygyxuijFk5DCHGlJjhwmg5qfL5hFMsbl5h 8 | gwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /authorization-server/snippets/default-user.properties: -------------------------------------------------------------------------------- 1 | spring.security.user.name=jlong 2 | spring.security.user.password=password 3 | spring.security.user.roles=admin,users.write,users.read,openid -------------------------------------------------------------------------------- /authorization-server/snippets/registered-clients.yaml: -------------------------------------------------------------------------------- 1 | 2 | spring: 3 | security: 4 | oauth2: 5 | authorizationserver: 6 | client: 7 | crm-client: 8 | require-authorization-consent: true 9 | registration: 10 | client-id: crm 11 | # <1> 12 | client-secret: "{bcrypt}$2a$10$m7dGi0viwVH63EjwZc6UdeUQxPuiVEEdFbZFI9nMxHAASTOIDlaVO" 13 | # <2> 14 | authorization-grant-types: client_credentials, authorization_code, refresh_token 15 | # <3> 16 | redirect-uris: http://127.0.0.1:8082/login/oauth2/code/spring 17 | # <4> 18 | scopes: user.read,user.write,openid 19 | # <5> 20 | client-authentication-methods: client_secret_basic 21 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/AotConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAutoDetect; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | import com.fasterxml.jackson.core.*; 6 | import com.fasterxml.jackson.databind.Module; 7 | import com.fasterxml.jackson.databind.*; 8 | import com.fasterxml.jackson.databind.cfg.MutableConfigOverride; 9 | import com.fasterxml.jackson.databind.deser.*; 10 | import com.fasterxml.jackson.databind.introspect.ClassIntrospector; 11 | import com.fasterxml.jackson.databind.jsontype.NamedType; 12 | import com.fasterxml.jackson.databind.node.MissingNode; 13 | import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; 14 | import com.fasterxml.jackson.databind.ser.Serializers; 15 | import com.fasterxml.jackson.databind.type.TypeFactory; 16 | import com.fasterxml.jackson.databind.type.TypeModifier; 17 | import jakarta.servlet.http.Cookie; 18 | import org.reflections.Reflections; 19 | import org.springframework.aot.hint.MemberCategory; 20 | import org.springframework.aot.hint.RuntimeHints; 21 | import org.springframework.aot.hint.RuntimeHintsRegistrar; 22 | import org.springframework.aot.hint.TypeReference; 23 | import org.springframework.boot.ApplicationRunner; 24 | import org.springframework.context.annotation.Bean; 25 | import org.springframework.context.annotation.Configuration; 26 | import org.springframework.context.annotation.ImportRuntimeHints; 27 | import org.springframework.core.io.ClassPathResource; 28 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 29 | import org.springframework.security.core.GrantedAuthority; 30 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 31 | import org.springframework.security.core.context.SecurityContextImpl; 32 | import org.springframework.security.core.userdetails.User; 33 | import org.springframework.security.jackson2.SecurityJackson2Modules; 34 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 35 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; 36 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType; 37 | import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; 38 | import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; 39 | import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; 40 | import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; 41 | import org.springframework.security.web.authentication.WebAuthenticationDetails; 42 | import org.springframework.security.web.savedrequest.DefaultSavedRequest; 43 | import org.springframework.security.web.savedrequest.SavedCookie; 44 | 45 | import java.io.Serializable; 46 | import java.net.URL; 47 | import java.security.Principal; 48 | import java.time.Duration; 49 | import java.time.Instant; 50 | import java.util.*; 51 | import java.util.stream.Collectors; 52 | import java.util.stream.Stream; 53 | 54 | @Configuration 55 | @ImportRuntimeHints(AotConfiguration.Hints.class) 56 | class AotConfiguration { 57 | 58 | static class Hints implements RuntimeHintsRegistrar { 59 | 60 | private Set> subs(Reflections reflections, Class... classesToFind) { 61 | var all = new HashSet>(); 62 | for (var individualClass : classesToFind) { 63 | var subTypesOf = reflections.getSubTypesOf(individualClass); 64 | all.addAll(subTypesOf); 65 | } 66 | return all; 67 | } 68 | 69 | private Set> resolveJacksonTypes() { 70 | var all = new HashSet>(); 71 | for (var pkg : Set.of("com.fasterxml", "org.springframework")) { 72 | var reflections = new Reflections(pkg); 73 | all.addAll(subs(reflections, JsonDeserializer.class, JsonSerializer.class, Module.class)); 74 | all.addAll(reflections.getTypesAnnotatedWith(JsonTypeInfo.class)); 75 | all.addAll(reflections.getTypesAnnotatedWith(JsonAutoDetect.class)); 76 | } 77 | all.addAll(registerJacksonModuleDeps(all.stream().filter(Module.class::isAssignableFrom).collect(Collectors.toSet()))); 78 | return all; 79 | } 80 | 81 | private static Collection> registerJacksonModuleDeps(Set> moduleClasses) { 82 | var set = new HashSet>(); 83 | var classLoader = AotConfiguration.class.getClassLoader(); 84 | var securityModules = new ArrayList(); 85 | securityModules.addAll(SecurityJackson2Modules.getModules(classLoader)); 86 | securityModules.addAll(moduleClasses 87 | .stream() 88 | .map(cn -> { 89 | try { 90 | for (var ctor : cn.getConstructors()) 91 | if (ctor.getParameterCount() == 0) 92 | return (Module) ctor.newInstance(); 93 | } // 94 | catch (Throwable t) { 95 | System.out.println("couldn't construct and inspect module " + cn.getName()); 96 | } 97 | return null; 98 | }) 99 | .collect(Collectors.toSet()) 100 | ); 101 | var om = new ObjectMapper(); 102 | var sc = new AccumulatingSetupContext(om, set); 103 | for (var module : securityModules) { 104 | set.add(module.getClass()); 105 | module.setupModule(sc); 106 | module.getDependencies().forEach(m -> set.add(m.getClass())); 107 | } 108 | 109 | return set; 110 | 111 | } 112 | 113 | @Override 114 | public void registerHints(RuntimeHints hints, ClassLoader classLoader) { 115 | 116 | var javaClasses = Set.of(ArrayList.class, Date.class, Duration.class, Instant.class, URL.class, TreeMap.class, HashMap.class, LinkedHashMap.class, List.class); 117 | 118 | var savedRequestClasses = Set.of(DefaultSavedRequest.class, SavedCookie.class); 119 | 120 | var oauth2CoreClasses = Set.of(SignatureAlgorithm.class, OAuth2AuthorizationResponseType.class, OAuth2AuthorizationRequest.class, AuthorizationGrantType.class, OAuth2TokenFormat.class, OAuth2Authorization.class, SecurityContextImpl.class); 121 | 122 | var securityClasses = Set.of(User.class, WebAuthenticationDetails.class, GrantedAuthority.class, Principal.class, SimpleGrantedAuthority.class, UsernamePasswordAuthenticationToken.class); 123 | 124 | var servletClasses = Set.of(Cookie.class); 125 | 126 | var jacksonTypes = new HashSet<>(resolveJacksonTypes()); 127 | jacksonTypes.add(SecurityJackson2Modules.class); 128 | 129 | var classes = new ArrayList>(); 130 | classes.addAll(jacksonTypes); 131 | classes.addAll(servletClasses); 132 | classes.addAll(oauth2CoreClasses); 133 | classes.addAll(savedRequestClasses); 134 | classes.addAll(javaClasses); 135 | classes.addAll(securityClasses); 136 | 137 | var stringClasses = Map.of( 138 | "java.util.", Set.of("Arrays$ArrayList"), 139 | "java.util.Collections$", Set.of("UnmodifiableRandomAccessList", "EmptyList", "UnmodifiableMap", "EmptyMap", "SingletonList", "UnmodifiableSet") 140 | );// 141 | 142 | var all = classes.stream().map(Class::getName).collect(Collectors.toCollection(HashSet::new)); 143 | stringClasses.forEach((root, setOfClasses) -> setOfClasses.forEach(cn -> all.add(root + cn))); 144 | 145 | var memberCategories = MemberCategory.values(); 146 | 147 | all.forEach(type -> { 148 | var typeReference = TypeReference.of(type); 149 | hints.reflection().registerType(typeReference, memberCategories); 150 | try { 151 | var clzz = Class.forName(typeReference.getName()); 152 | if (Serializable.class.isAssignableFrom(clzz)) { 153 | hints.serialization().registerType(typeReference); 154 | } 155 | } // 156 | catch (Throwable t) { 157 | System.out.println("couldn't register serialization hint for " + typeReference.getName() + ":" + t.getMessage()); 158 | } 159 | }); 160 | 161 | Set.of("data", "schema").forEach(folder -> hints.resources().registerPattern("sql/" + folder + "/*sql")); 162 | 163 | Set.of("key", "pub").forEach(suffix -> hints.resources().registerResource(new ClassPathResource("app." + suffix))); 164 | 165 | } 166 | 167 | } 168 | 169 | static class AccumulatingSetupContext implements Module.SetupContext { 170 | 171 | private final Collection> classesToRegister; 172 | 173 | private final ObjectMapper objectMapper; 174 | 175 | AccumulatingSetupContext(ObjectMapper objectMapper, Collection> classes) { 176 | this.objectMapper = objectMapper; 177 | this.classesToRegister = classes; 178 | } 179 | 180 | @Override 181 | public Version getMapperVersion() { 182 | return null; 183 | } 184 | 185 | @Override 186 | public C getOwner() { 187 | return (C) this.objectMapper; 188 | } 189 | 190 | @Override 191 | public TypeFactory getTypeFactory() { 192 | return null; 193 | } 194 | 195 | @Override 196 | public boolean isEnabled(MapperFeature f) { 197 | return false; 198 | } 199 | 200 | @Override 201 | public boolean isEnabled(DeserializationFeature f) { 202 | return false; 203 | } 204 | 205 | @Override 206 | public boolean isEnabled(SerializationFeature f) { 207 | return false; 208 | } 209 | 210 | @Override 211 | public boolean isEnabled(JsonFactory.Feature f) { 212 | return false; 213 | } 214 | 215 | @Override 216 | public boolean isEnabled(JsonParser.Feature f) { 217 | return false; 218 | } 219 | 220 | @Override 221 | public boolean isEnabled(JsonGenerator.Feature f) { 222 | return false; 223 | } 224 | 225 | @Override 226 | public MutableConfigOverride configOverride(Class type) { 227 | this.classesToRegister.add(type); 228 | return null; 229 | } 230 | 231 | @Override 232 | public void addDeserializers(Deserializers d) { 233 | 234 | } 235 | 236 | @Override 237 | public void addKeyDeserializers(KeyDeserializers s) { 238 | 239 | } 240 | 241 | @Override 242 | public void addSerializers(Serializers s) { 243 | 244 | } 245 | 246 | @Override 247 | public void addKeySerializers(Serializers s) { 248 | 249 | } 250 | 251 | @Override 252 | public void addBeanDeserializerModifier(BeanDeserializerModifier mod) { 253 | 254 | } 255 | 256 | @Override 257 | public void addBeanSerializerModifier(BeanSerializerModifier mod) { 258 | 259 | } 260 | 261 | @Override 262 | public void addAbstractTypeResolver(AbstractTypeResolver resolver) { 263 | 264 | } 265 | 266 | @Override 267 | public void addTypeModifier(TypeModifier modifier) { 268 | 269 | } 270 | 271 | @Override 272 | public void addValueInstantiators(ValueInstantiators instantiators) { 273 | 274 | } 275 | 276 | @Override 277 | public void setClassIntrospector(ClassIntrospector ci) { 278 | 279 | } 280 | 281 | @Override 282 | public void insertAnnotationIntrospector(AnnotationIntrospector ai) { 283 | 284 | } 285 | 286 | @Override 287 | public void appendAnnotationIntrospector(AnnotationIntrospector ai) { 288 | 289 | } 290 | 291 | @Override 292 | public void registerSubtypes(Class... subtypes) { 293 | this.classesToRegister.addAll(Stream.of(subtypes).collect(Collectors.toSet())); 294 | } 295 | 296 | @Override 297 | public void registerSubtypes(NamedType... subtypes) { 298 | this.classesToRegister.addAll(Stream.of(subtypes).map(NamedType::getType).collect(Collectors.toSet())); 299 | } 300 | 301 | @Override 302 | public void registerSubtypes(Collection> subtypes) { 303 | this.classesToRegister.addAll(subtypes); 304 | } 305 | 306 | @Override 307 | public void setMixInAnnotations(Class target, Class mixinSource) { 308 | this.classesToRegister.add(target); 309 | this.classesToRegister.add(mixinSource); 310 | } 311 | 312 | @Override 313 | public void addDeserializationProblemHandler(DeserializationProblemHandler handler) { 314 | 315 | } 316 | 317 | @Override 318 | public void setNamingStrategy(PropertyNamingStrategy naming) { 319 | 320 | } 321 | } 322 | 323 | 324 | } 325 | 326 | //@Configuration 327 | class JsonConfiguration { 328 | 329 | @Bean 330 | ApplicationRunner parse() { 331 | return a -> { 332 | var gaList = new com.fasterxml.jackson.core.type.TypeReference>() { 333 | }; 334 | var objectMapper = new ObjectMapper(); 335 | SecurityJackson2Modules.getModules(ClassLoader.getSystemClassLoader()).forEach(objectMapper::registerModule); 336 | objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); 337 | var json = """ 338 | {"@class":"java.util.Collections$UnmodifiableMap","org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest":{"@class":"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest","authorizationUri":"http://localhost:8080/oauth2/authorize","authorizationGrantType":{"value":"authorization_code"},"responseType":{"value":"code"},"clientId":"crm","redirectUri":"http://127.0.0.1:8082/login/oauth2/code/spring","scopes":["java.util.Collections$UnmodifiableSet",["user.read","openid"]],"state":"QjdbcbnM2uoxnwksbT1IooOOWxNbkdMVV0LDsptQuH4=","additionalParameters":{"@class":"java.util.Collections$UnmodifiableMap","nonce":"ryv3qPgr5IwFA6LYmLf1QkQY4fRtaZmg_ePB2rSJrqQ","continue":""},"authorizationRequestUri":"http://localhost:8080/oauth2/authorize?response_type=code&client_id=crm&scope=user.read%20openid&state=QjdbcbnM2uoxnwksbT1IooOOWxNbkdMVV0LDsptQuH4%3D&redirect_uri=http://127.0.0.1:8082/login/oauth2/code/spring&nonce=ryv3qPgr5IwFA6LYmLf1QkQY4fRtaZmg_ePB2rSJrqQ&continue=","attributes":{"@class":"java.util.Collections$UnmodifiableMap"}},"java.security.Principal":{"@class":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","authorities":["java.util.Collections$UnmodifiableRandomAccessList",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"details":{"@class":"org.springframework.security.web.authentication.WebAuthenticationDetails","remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"745F400BA9E8317369ECFD9B9E826695"},"authenticated":true,"principal":{"@class":"org.springframework.security.core.userdetails.User","password":null,"username":"jlong","authorities":["java.util.Collections$UnmodifiableSet",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true},"credentials":null}} 339 | """; 340 | var jsonNode = objectMapper.readTree(json); 341 | var authoritiesJsonNode = readJsonNode(jsonNode, "authorities").traverse(objectMapper); 342 | objectMapper.readValue(authoritiesJsonNode, gaList).forEach(System.out::println); 343 | 344 | }; 345 | } 346 | 347 | private static JsonNode readJsonNode(JsonNode jsonNode, String field) { 348 | return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/AuthorizationConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.jdbc.core.JdbcOperations; 6 | import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; 7 | import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; 8 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; 9 | 10 | @Configuration 11 | class AuthorizationConfiguration { 12 | 13 | @Bean 14 | JdbcOAuth2AuthorizationConsentService jdbcOAuth2AuthorizationConsentService( 15 | JdbcOperations jdbcOperations, RegisteredClientRepository repository) { 16 | return new JdbcOAuth2AuthorizationConsentService(jdbcOperations, repository); 17 | } 18 | 19 | @Bean 20 | JdbcOAuth2AuthorizationService jdbcOAuth2AuthorizationService( 21 | JdbcOperations jdbcOperations, RegisteredClientRepository rcr) { 22 | return new JdbcOAuth2AuthorizationService(jdbcOperations, rcr); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/AuthorizationServerApplication.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class AuthorizationServerApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(AuthorizationServerApplication.class, args); 11 | } 12 | 13 | } 14 | 15 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/ClientsConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import org.springframework.boot.ApplicationRunner; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.jdbc.core.JdbcTemplate; 7 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 8 | import org.springframework.security.oauth2.core.ClientAuthenticationMethod; 9 | import org.springframework.security.oauth2.core.oidc.OidcScopes; 10 | import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; 11 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; 12 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; 13 | 14 | import java.util.Set; 15 | import java.util.UUID; 16 | 17 | @Configuration 18 | class ClientsConfiguration { 19 | 20 | // <1> 21 | @Bean 22 | RegisteredClientRepository registeredClientRepository(JdbcTemplate template) { 23 | return new JdbcRegisteredClientRepository(template); 24 | } 25 | 26 | //<2> 27 | @Bean 28 | ApplicationRunner clientsRunner(RegisteredClientRepository repository) { 29 | return args -> { 30 | var clientId = "crm"; 31 | if (repository.findByClientId(clientId) == null) { 32 | repository.save( 33 | RegisteredClient 34 | .withId(UUID.randomUUID().toString()) 35 | .clientId(clientId) 36 | .clientSecret("{bcrypt}$2a$10$m7dGi0viwVH63EjwZc6UdeUQxPuiVEEdFbZFI9nMxHAASTOIDlaVO") 37 | .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) 38 | .authorizationGrantTypes(grantTypes -> grantTypes.addAll(Set.of( 39 | AuthorizationGrantType.CLIENT_CREDENTIALS, 40 | AuthorizationGrantType.AUTHORIZATION_CODE, 41 | AuthorizationGrantType.REFRESH_TOKEN))) 42 | .redirectUri("http://127.0.0.1:8082/login/oauth2/code/spring") 43 | .scopes(scopes -> scopes.addAll(Set.of("user.read", "user.write", OidcScopes.OPENID))) 44 | .build() 45 | ); 46 | } 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import com.nimbusds.jose.jwk.JWKSet; 4 | import com.nimbusds.jose.jwk.RSAKey; 5 | import com.nimbusds.jose.jwk.source.ImmutableJWKSet; 6 | import com.nimbusds.jose.jwk.source.JWKSource; 7 | import com.nimbusds.jose.proc.SecurityContext; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.security.crypto.factory.PasswordEncoderFactories; 12 | import org.springframework.security.crypto.password.PasswordEncoder; 13 | 14 | import java.security.interfaces.RSAPrivateKey; 15 | import java.security.interfaces.RSAPublicKey; 16 | 17 | @Configuration 18 | class SecurityConfiguration { 19 | 20 | // <1> 21 | @Bean 22 | PasswordEncoder passwordEncoder() { 23 | return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/UsersConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import org.springframework.boot.ApplicationRunner; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.core.userdetails.User; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 8 | import org.springframework.security.provisioning.JdbcUserDetailsManager; 9 | import org.springframework.security.provisioning.UserDetailsManager; 10 | 11 | import javax.sql.DataSource; 12 | import java.util.Map; 13 | 14 | @Configuration 15 | class UsersConfiguration { 16 | 17 | @Bean 18 | JdbcUserDetailsManager jdbcUserDetailsManager(DataSource dataSource) { 19 | return new JdbcUserDetailsManager(dataSource); 20 | } 21 | 22 | @Bean 23 | ApplicationRunner usersRunner(PasswordEncoder passwordEncoder, UserDetailsManager userDetailsManager) { 24 | return args -> { 25 | // <1> 26 | var builder = User.builder().roles("USER").passwordEncoder(passwordEncoder::encode); 27 | // <2> 28 | var users = Map.of("jlong", "password", "rwinch", "p@ssw0rd"); 29 | users.forEach((username, password) -> { 30 | if (!userDetailsManager.userExists(username)) { 31 | var user = builder 32 | .username(username) 33 | .password(password) 34 | .build(); 35 | userDetailsManager.createUser(user); 36 | } 37 | }); 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/Converters.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.crypto.encrypt.Encryptors; 6 | import org.springframework.security.crypto.encrypt.TextEncryptor; 7 | 8 | @Configuration 9 | class Converters { 10 | 11 | @Bean 12 | RsaPublicKeyConverter rsaPublicKeyConverter(TextEncryptor textEncryptor) { 13 | return new RsaPublicKeyConverter(textEncryptor); 14 | } 15 | 16 | @Bean 17 | RsaPrivateKeyConverter rsaPrivateKeyConverter(TextEncryptor textEncryptor) { 18 | return new RsaPrivateKeyConverter(textEncryptor); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/JdbcRsaKeyPairRepository.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import org.springframework.jdbc.core.JdbcTemplate; 4 | import org.springframework.jdbc.core.RowMapper; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.util.Assert; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | import java.util.Date; 11 | import java.util.List; 12 | 13 | @Component 14 | class JdbcRsaKeyPairRepository implements RsaKeyPairRepository { 15 | 16 | private final JdbcTemplate jdbc; 17 | 18 | private final RsaPublicKeyConverter rsaPublicKeyConverter; 19 | 20 | private final RsaPrivateKeyConverter rsaPrivateKeyConverter; 21 | 22 | private final RowMapper keyPairRowMapper; 23 | 24 | JdbcRsaKeyPairRepository( 25 | RowMapper keyPairRowMapper, 26 | RsaPublicKeyConverter publicConverter, 27 | RsaPrivateKeyConverter privateConverter, 28 | JdbcTemplate jdbc) { 29 | this.jdbc = jdbc; 30 | this.keyPairRowMapper = keyPairRowMapper; 31 | this.rsaPublicKeyConverter = publicConverter; 32 | this.rsaPrivateKeyConverter = privateConverter; 33 | } 34 | 35 | // <.> 36 | @Override 37 | public List findKeyPairs() { 38 | return this.jdbc.query( 39 | "select * from rsa_key_pairs order by created desc", 40 | this.keyPairRowMapper); 41 | } 42 | 43 | // <.> 44 | @Override 45 | public void save(RsaKeyPair keyPair) { 46 | var sql = """ 47 | insert into rsa_key_pairs (id, private_key, public_key, created) 48 | values (?, ?, ?, ?) 49 | on conflict on constraint rsa_key_pairs_id_created_key 50 | do nothing 51 | """; 52 | try (var privateBaos = new ByteArrayOutputStream(); 53 | var publicBaos = new ByteArrayOutputStream()) { 54 | this.rsaPrivateKeyConverter.serialize(keyPair.privateKey(), privateBaos); 55 | this.rsaPublicKeyConverter.serialize(keyPair.publicKey(), publicBaos); 56 | var updated = this.jdbc.update(sql, 57 | keyPair.id(), 58 | privateBaos.toString(), 59 | publicBaos.toString(), 60 | new Date(keyPair.created().toEpochMilli()) 61 | ); 62 | Assert.state(updated == 0 || updated == 1, 63 | "no more than one record should have been updated"); 64 | }// 65 | catch (IOException e) { 66 | throw new IllegalArgumentException("there's been an exception", e); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/KeyConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import com.nimbusds.jose.jwk.source.JWKSource; 4 | import com.nimbusds.jose.proc.SecurityContext; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.security.crypto.encrypt.Encryptors; 9 | import org.springframework.security.crypto.encrypt.TextEncryptor; 10 | import org.springframework.security.oauth2.core.OAuth2Token; 11 | import org.springframework.security.oauth2.jwt.JwtEncoder; 12 | import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; 13 | import org.springframework.security.oauth2.server.authorization.token.*; 14 | 15 | 16 | @Configuration 17 | class KeyConfiguration { 18 | 19 | // <.> 20 | @Bean 21 | TextEncryptor textEncryptor( 22 | @Value("${jwk.persistence.password}") String pw, 23 | @Value("${jwk.persistence.salt}") String salt) { 24 | return Encryptors.text(pw, salt); 25 | } 26 | 27 | // <.> 28 | @Bean 29 | OAuth2TokenGenerator delegatingOAuth2TokenGenerator( 30 | JwtEncoder encoder, 31 | OAuth2TokenCustomizer customizer) { 32 | var generator = new JwtGenerator(encoder); 33 | generator.setJwtCustomizer(customizer); 34 | return new DelegatingOAuth2TokenGenerator(generator, 35 | new OAuth2AccessTokenGenerator(), new OAuth2RefreshTokenGenerator()); 36 | } 37 | 38 | // <.> 39 | @Bean 40 | NimbusJwtEncoder jwtEncoder(JWKSource jwkSource) { 41 | return new NimbusJwtEncoder(jwkSource); 42 | } 43 | } 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/Keys.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.security.KeyPair; 6 | import java.security.KeyPairGenerator; 7 | import java.security.interfaces.RSAPrivateKey; 8 | import java.security.interfaces.RSAPublicKey; 9 | import java.time.Instant; 10 | 11 | @Component 12 | class Keys { 13 | 14 | RsaKeyPair generateKeyPair(String keyId, Instant created) { 15 | var keyPair = generateRsaKey(); 16 | var publicKey = (RSAPublicKey) keyPair.getPublic(); 17 | var privateKey = (RSAPrivateKey) keyPair.getPrivate(); 18 | return new RsaKeyPair(keyId, created, publicKey, privateKey); 19 | } 20 | 21 | private KeyPair generateRsaKey() { 22 | try { 23 | var keyPairGenerator = KeyPairGenerator.getInstance("RSA"); 24 | keyPairGenerator.initialize(2048); 25 | return keyPairGenerator.generateKeyPair(); 26 | }// 27 | catch (Exception ex) { 28 | throw new IllegalStateException(ex); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/LifecycleConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.context.event.ApplicationReadyEvent; 5 | import org.springframework.context.ApplicationEventPublisher; 6 | import org.springframework.context.ApplicationListener; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import java.time.Instant; 11 | 12 | @Configuration 13 | class LifecycleConfiguration { 14 | 15 | // <.> 16 | @Bean 17 | ApplicationListener keyPairGenerationRequestListener( 18 | Keys keys, RsaKeyPairRepository repository, @Value("${jwk.key.id}") String keyId) { 19 | return event -> repository.save(keys.generateKeyPair(keyId, event.getSource())); 20 | } 21 | 22 | // <.> 23 | @Bean 24 | ApplicationListener applicationReadyListener( 25 | ApplicationEventPublisher publisher, RsaKeyPairRepository repository) { 26 | return event -> { 27 | if (repository.findKeyPairs().isEmpty()) 28 | publisher.publishEvent(new RsaKeyPairGenerationRequestEvent(Instant.now())); 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/RsaKeyPair.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import java.security.interfaces.RSAPrivateKey; 4 | import java.security.interfaces.RSAPublicKey; 5 | import java.time.Instant; 6 | 7 | 8 | // <1> 9 | record RsaKeyPair(String id, Instant created, RSAPublicKey publicKey, RSAPrivateKey privateKey) { 10 | } 11 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/RsaKeyPairGenerationRequestEvent.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import org.springframework.context.ApplicationEvent; 4 | 5 | import java.time.Instant; 6 | 7 | class RsaKeyPairGenerationRequestEvent extends ApplicationEvent { 8 | 9 | RsaKeyPairGenerationRequestEvent(Instant instant) { 10 | super(instant); 11 | } 12 | 13 | @Override 14 | public Instant getSource() { 15 | return (Instant) super.getSource(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/RsaKeyPairRepository.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import java.util.List; 4 | 5 | interface RsaKeyPairRepository { 6 | 7 | // <1> 8 | List findKeyPairs(); 9 | 10 | // <2> 11 | void save(RsaKeyPair rsaKeyPair); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/RsaKeyPairRepositoryJWKSource.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import com.nimbusds.jose.KeySourceException; 4 | import com.nimbusds.jose.jwk.JWK; 5 | import com.nimbusds.jose.jwk.JWKSelector; 6 | import com.nimbusds.jose.jwk.RSAKey; 7 | import com.nimbusds.jose.jwk.source.JWKSource; 8 | import com.nimbusds.jose.proc.SecurityContext; 9 | import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; 10 | import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | @Component 17 | class RsaKeyPairRepositoryJWKSource implements JWKSource, 18 | OAuth2TokenCustomizer { 19 | 20 | private final RsaKeyPairRepository keyPairRepository; 21 | 22 | RsaKeyPairRepositoryJWKSource(RsaKeyPairRepository keyPairRepository) { 23 | this.keyPairRepository = keyPairRepository; 24 | } 25 | 26 | // <.> 27 | @Override 28 | public List get(JWKSelector jwkSelector, SecurityContext context) throws KeySourceException { 29 | var keyPairs = this.keyPairRepository.findKeyPairs(); 30 | var result = new ArrayList(keyPairs.size()); 31 | for (var keyPair : keyPairs) { 32 | var rsaKey = new RSAKey.Builder( 33 | keyPair.publicKey()).privateKey(keyPair.privateKey()) 34 | .keyID(keyPair.id()).build(); 35 | if (jwkSelector.getMatcher().matches(rsaKey)) { 36 | result.add(rsaKey); 37 | } 38 | } 39 | return result; 40 | } 41 | 42 | // <.> 43 | @Override 44 | public void customize(JwtEncodingContext context) { 45 | var keyPairs = this.keyPairRepository.findKeyPairs(); 46 | var kid = keyPairs.get(0).id(); 47 | context.getJwsHeader().keyId(kid); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/RsaKeyPairRowMapper.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import org.springframework.core.serializer.Deserializer; 4 | import org.springframework.jdbc.core.RowMapper; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.io.IOException; 8 | import java.sql.ResultSet; 9 | import java.sql.SQLException; 10 | import java.util.Date; 11 | 12 | @Component 13 | class RsaKeyPairRowMapper implements RowMapper { 14 | 15 | private final RsaPrivateKeyConverter rsaPrivateKeyConverter; 16 | 17 | private final RsaPublicKeyConverter rsaPublicKeyConverter; 18 | 19 | RsaKeyPairRowMapper(RsaPrivateKeyConverter rsaPrivateKeyConverter, 20 | RsaPublicKeyConverter rsaPublicKeyConverter) { 21 | this.rsaPrivateKeyConverter = rsaPrivateKeyConverter; 22 | this.rsaPublicKeyConverter = rsaPublicKeyConverter; 23 | } 24 | 25 | @Override 26 | public RsaKeyPair mapRow(ResultSet rs, int rowNum) throws SQLException { 27 | try { 28 | 29 | // <.> 30 | var privateKey = loadKey(rs, "private_key", this.rsaPrivateKeyConverter); 31 | var publicKey = loadKey(rs, "public_key", this.rsaPublicKeyConverter); 32 | 33 | // <.> 34 | var created = new Date(rs.getDate("created").getTime()).toInstant(); 35 | var id = rs.getString("id"); 36 | 37 | // <.> 38 | return new RsaKeyPair(id, created, publicKey, privateKey); 39 | } catch (IOException e) { 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | 44 | private static T loadKey(ResultSet rs, String fn, Deserializer f) 45 | throws SQLException, IOException { 46 | var privateKeyBytes = rs.getString(fn).getBytes(); 47 | return f.deserializeFromByteArray(privateKeyBytes); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/RsaPrivateKeyConverter.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import org.springframework.core.serializer.Deserializer; 4 | import org.springframework.core.serializer.Serializer; 5 | import org.springframework.security.crypto.encrypt.TextEncryptor; 6 | import org.springframework.util.FileCopyUtils; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.io.OutputStream; 12 | import java.security.KeyFactory; 13 | import java.security.interfaces.RSAPrivateKey; 14 | import java.security.spec.PKCS8EncodedKeySpec; 15 | import java.util.Base64; 16 | 17 | 18 | class RsaPrivateKeyConverter implements Serializer, 19 | Deserializer { 20 | 21 | private final TextEncryptor textEncryptor; 22 | 23 | RsaPrivateKeyConverter(TextEncryptor textEncryptor) { 24 | this.textEncryptor = textEncryptor; 25 | } 26 | 27 | // <1> 28 | @Override 29 | public void serialize(RSAPrivateKey object, OutputStream outputStream) throws IOException { 30 | var pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(object.getEncoded()); 31 | var string = "-----BEGIN PRIVATE KEY-----\n" + Base64.getMimeEncoder().encodeToString(pkcs8EncodedKeySpec.getEncoded()) 32 | + "\n-----END PRIVATE KEY-----"; 33 | outputStream.write(this.textEncryptor.encrypt(string).getBytes()); 34 | } 35 | 36 | // <2> 37 | @Override 38 | public RSAPrivateKey deserialize(InputStream inputStream) { 39 | try { 40 | var pem = this.textEncryptor.decrypt( 41 | FileCopyUtils.copyToString(new InputStreamReader(inputStream))); 42 | var privateKeyPEM = pem 43 | .replace("-----BEGIN PRIVATE KEY-----", "") 44 | .replace("-----END PRIVATE KEY-----", ""); 45 | var encoded = Base64.getMimeDecoder().decode(privateKeyPEM); 46 | var keyFactory = KeyFactory.getInstance("RSA"); 47 | var keySpec = new PKCS8EncodedKeySpec(encoded); 48 | return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); 49 | }// 50 | catch (Throwable throwable) { 51 | throw new IllegalArgumentException("there's been an exception", throwable); 52 | } 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /authorization-server/src/main/java/bootiful/authorizationserver/keys/RsaPublicKeyConverter.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver.keys; 2 | 3 | import org.springframework.core.serializer.Deserializer; 4 | import org.springframework.core.serializer.Serializer; 5 | import org.springframework.security.crypto.encrypt.TextEncryptor; 6 | import org.springframework.util.FileCopyUtils; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.InputStreamReader; 11 | import java.io.OutputStream; 12 | import java.security.KeyFactory; 13 | import java.security.interfaces.RSAPublicKey; 14 | import java.security.spec.X509EncodedKeySpec; 15 | import java.util.Base64; 16 | 17 | class RsaPublicKeyConverter implements Serializer, 18 | Deserializer { 19 | 20 | private final TextEncryptor textEncryptor; 21 | 22 | RsaPublicKeyConverter(TextEncryptor textEncryptor) { 23 | this.textEncryptor = textEncryptor; 24 | } 25 | 26 | @Override 27 | public RSAPublicKey deserialize(InputStream inputStream) throws IOException { 28 | try { 29 | var pem = textEncryptor.decrypt( 30 | FileCopyUtils.copyToString(new InputStreamReader(inputStream))); 31 | var publicKeyPEM = pem 32 | .replace("-----BEGIN PUBLIC KEY-----", "") 33 | .replace("-----END PUBLIC KEY-----", ""); 34 | var encoded = Base64.getMimeDecoder().decode(publicKeyPEM); 35 | var keyFactory = KeyFactory.getInstance("RSA"); 36 | var keySpec = new X509EncodedKeySpec(encoded); 37 | return (RSAPublicKey) keyFactory.generatePublic(keySpec); 38 | }// 39 | catch (Throwable throwable) { 40 | throw new IllegalArgumentException("there's been an exception", throwable); 41 | } 42 | 43 | } 44 | 45 | @Override 46 | public void serialize(RSAPublicKey object, OutputStream outputStream) throws IOException { 47 | var x509EncodedKeySpec = new X509EncodedKeySpec(object.getEncoded()); 48 | var pem = "-----BEGIN PUBLIC KEY-----\n" + 49 | Base64.getMimeEncoder().encodeToString(x509EncodedKeySpec.getEncoded()) + 50 | "\n-----END PUBLIC KEY-----"; 51 | outputStream.write(this.textEncryptor.encrypt(pem).getBytes()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://localhost/postgres 2 | spring.datasource.username=postgres 3 | spring.datasource.password=postgres 4 | spring.sql.init.mode=always 5 | spring.sql.init.schema-locations=classpath:sql/schema/*sql 6 | # spring.sql.init.data-locations=classpath:sql/data/*sql 7 | jwk.key.id=bootiful-key 8 | jwk.key.private=classpath:app.key 9 | jwk.key.public=classpath:app.pub 10 | jwk.persistence.password=b00t1ful 11 | jwk.persistence.salt=24e23407390934 12 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/sql/schema/oauth2-authorization-consent-schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists oauth2_authorization_consent 2 | ( 3 | registered_client_id varchar(100) NOT NULL, 4 | principal_name varchar(200) NOT NULL, 5 | authorities varchar(1000) NOT NULL, 6 | PRIMARY KEY (registered_client_id, principal_name) 7 | ); 8 | 9 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/sql/schema/oauth2-authorization-schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists oauth2_authorization 2 | ( 3 | id varchar(100) NOT NULL, 4 | registered_client_id varchar(100) NOT NULL, 5 | principal_name varchar(200) NOT NULL, 6 | authorization_grant_type varchar(100) NOT NULL, 7 | authorized_scopes varchar(1000) DEFAULT NULL, 8 | attributes text DEFAULT NULL, 9 | state varchar(500) DEFAULT NULL, 10 | authorization_code_value text DEFAULT NULL, 11 | authorization_code_issued_at timestamp DEFAULT NULL, 12 | authorization_code_expires_at timestamp DEFAULT NULL, 13 | authorization_code_metadata text DEFAULT NULL, 14 | access_token_value text DEFAULT NULL, 15 | access_token_issued_at timestamp DEFAULT NULL, 16 | access_token_expires_at timestamp DEFAULT NULL, 17 | access_token_metadata text DEFAULT NULL, 18 | access_token_type varchar(100) DEFAULT NULL, 19 | access_token_scopes varchar(1000) DEFAULT NULL, 20 | oidc_id_token_value text DEFAULT NULL, 21 | oidc_id_token_issued_at timestamp DEFAULT NULL, 22 | oidc_id_token_expires_at timestamp DEFAULT NULL, 23 | oidc_id_token_metadata text DEFAULT NULL, 24 | refresh_token_value text DEFAULT NULL, 25 | refresh_token_issued_at timestamp DEFAULT NULL, 26 | refresh_token_expires_at timestamp DEFAULT NULL, 27 | refresh_token_metadata text DEFAULT NULL, 28 | user_code_value text DEFAULT NULL, 29 | user_code_issued_at timestamp DEFAULT NULL, 30 | user_code_expires_at timestamp DEFAULT NULL, 31 | user_code_metadata text DEFAULT NULL, 32 | device_code_value text DEFAULT NULL, 33 | device_code_issued_at timestamp DEFAULT NULL, 34 | device_code_expires_at timestamp DEFAULT NULL, 35 | device_code_metadata text DEFAULT NULL, 36 | PRIMARY KEY (id) 37 | ); 38 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/sql/schema/oauth2-registered-client.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE if not exists oauth2_registered_client 2 | ( 3 | id varchar(100) NOT NULL, 4 | client_id varchar(100) NOT NULL, 5 | client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, 6 | client_secret varchar(200) DEFAULT NULL, 7 | client_secret_expires_at timestamp DEFAULT NULL, 8 | client_name varchar(200) NOT NULL, 9 | client_authentication_methods varchar(1000) NOT NULL, 10 | authorization_grant_types varchar(1000) NOT NULL, 11 | redirect_uris varchar(1000) DEFAULT NULL, 12 | post_logout_redirect_uris varchar(1000) DEFAULT NULL, 13 | scopes varchar(1000) NOT NULL, 14 | client_settings varchar(2000) NOT NULL, 15 | token_settings varchar(2000) NOT NULL, 16 | PRIMARY KEY (id) 17 | ); 18 | 19 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/sql/schema/rsa_key_pairs.sql: -------------------------------------------------------------------------------- 1 | create table if not exists rsa_key_pairs 2 | ( 3 | id varchar(1000) not null primary key, 4 | private_key text not null, 5 | public_key text not null, 6 | created date not null, 7 | unique (id, created ) 8 | ); -------------------------------------------------------------------------------- /authorization-server/src/main/resources/sql/schema/spring-session-jdbc.sql: -------------------------------------------------------------------------------- 1 | -- sessions 2 | create table if not exists spring_session 3 | ( 4 | primary_id character(36) primary key not null, 5 | session_id character(36) not null, 6 | creation_time bigint not null, 7 | last_access_time bigint not null, 8 | max_inactive_interval integer not null, 9 | expiry_time bigint not null, 10 | principal_name character varying(100) 11 | ); 12 | create unique index if not exists spring_session_ix1 on spring_session using btree (session_id); 13 | create index if not exists spring_session_ix2 on spring_session using btree (expiry_time); 14 | create index if not exists spring_session_ix3 on spring_session using btree (principal_name); 15 | 16 | -- session attributes 17 | create table if not exists spring_session_attributes 18 | ( 19 | session_primary_id character(36) not null, 20 | attribute_name character varying(200) not null, 21 | attribute_bytes bytea not null, 22 | primary key (session_primary_id, attribute_name), 23 | foreign key (session_primary_id) references spring_session (primary_id) 24 | match simple on update no action on delete cascade 25 | ); 26 | 27 | -------------------------------------------------------------------------------- /authorization-server/src/main/resources/sql/schema/users.sql: -------------------------------------------------------------------------------- 1 | create table if not exists users 2 | ( 3 | username varchar(200) not null primary key, 4 | password varchar(500) not null, 5 | enabled boolean not null 6 | ); 7 | create table if not exists authorities 8 | ( 9 | username varchar(200) not null, 10 | authority varchar(50) not null, 11 | constraint fk_authorities_users foreign key (username) references users (username), 12 | constraint username_authority UNIQUE (username, authority) 13 | ); 14 | 15 | -------------------------------------------------------------------------------- /authorization-server/src/test/java/bootiful/authorizationserver/AuthorizationServerApplicationTests.java: -------------------------------------------------------------------------------- 1 | package bootiful.authorizationserver; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class AuthorizationServerApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:15.2 4 | environment: 5 | - POSTGRES_USER=postgres 6 | - PGUSER=postgres 7 | - POSTGRES_NAME=postgres 8 | - POSTGRES_PASSWORD=postgres 9 | ports: 10 | - "5432:5432" 11 | rabbitmq: 12 | image: rabbitmq:3-management 13 | environment: 14 | - RABBITMQ_DEFAULT_USER=user 15 | - RABBITMQ_DEFAULT_PASS=password 16 | ports: 17 | - "5672:5672" 18 | - "15672:15672" -------------------------------------------------------------------------------- /federated-oauth.adoc: -------------------------------------------------------------------------------- 1 | = Federated OAuth with the Spring Authorization Server 2 | 3 | // there's an example in the Spring Authorization Server samples that shows how you can get a Google and Github token with one SAS JWT token 4 | -------------------------------------------------------------------------------- /frontmatter.adoc: -------------------------------------------------------------------------------- 1 | [[frontmatter]] 2 | = Frontmatter 3 | 4 | _Secure all the things with the Spring Authorization Server_ 5 | by Josh Long 6 | 7 | (C) {revyear} {author}. 8 | All rights reserved. 9 | 10 | ISBN: {isbn} 11 | Version {revnumber}. 12 | 13 | While the author has used reasonable faith efforts to ensure that the information and instructions in this work are accurate, the author disclaims all responsibility for errors or omissions, including without limitation, responsibility for damages resulting from the use of or reliance on this work. 14 | The use of the information and instructions contained in this work is at your own risk. 15 | If any code samples or other technology this work contains or describes is subject to open source licenses or the intellectual property rights of others, it is your responsibility to ensure that your use thereof complies with such licenses and/or rights. -------------------------------------------------------------------------------- /gateway.adoc: -------------------------------------------------------------------------------- 1 | == The Gateway Client 2 | 3 | In our super secure OAuth onion, this next microservice, the `gateway`, is the outermost layer. 4 | It is the first port-of-call for all requests destined for the microservices in our system. 5 | When visitors to our site punch in the domain name for our system into a browser, it'll resolve to a loadbalancer serving this gateway service, and it is here where we'll originate an OAuth token. 6 | This service is also a gateway, powered by Spring Cloud Gateway. 7 | The gateway acts as a proxy, allowing us to forward requests to different hosts and ports, concealing from the user that the responses they're seeing are from different services. 8 | 9 | The `gateway` service's job is entirely to handle the OAuth dance - if it detects that the request is not authenticated - and to otherwise proxy requests to two other HTTP endpoints: the `api` we just built, and have running on port `8081`, and the static HTML process running on port `8020`. 10 | 11 | Here's the routing table 12 | 13 | |=== 14 | |*HTTP origin* | *HTTP destination* 15 | |http://gateway:8082/api/customers | http://api:8080/customers 16 | |http://gateway:8082/api/me | http://api:8080/me 17 | |http://gateway:8082/api/email | http://api:8080/email 18 | |http://gateway:8082/index.html | http://static:8020/index.html 19 | |=== 20 | 21 | The `gateway` also sends along the JWT token to the downstream HTTP endpoints, acting as a token relay. 22 | The `static` site won't care (it's just serving up static `.html` and `.css` files, after all), but the `api` is a resource server, and it will care. 23 | The `api` will refuse to send back data unless that token is specified. 24 | 25 | Let's look at the Java code first. 26 | 27 | [source,java] 28 | ----- 29 | include::gateway/src/main/java/bootiful/gateway/SecurityConfiguration.java[] 30 | ----- 31 | 32 | <1> you could probably get away with not defining this bean at all _if_ you were willing to deal with default behavior of CSRF tokens, which I am not. 33 | In the interest of simplicity, I'm disabling them, but in so doing I am also disabling all the other things Spring Security assumed I wanted. 34 | So we will also go through and re-enable those things. 35 | <2> We want all HTTP requests to be authenticated... 36 | <3> ...and to disable the CSRF support... 37 | <4> ...and to re-enable OAuth 2 OIDC login support and the OAuth 2 client support. 38 | The OIDC login functionality is what triggers the OAuth dance we've talked about. 39 | The OAuth 2 client support is what tells Spring Security, running in this process, as which OAuth 2 client requests for OIDC login should be done. 40 | We'll need to specify the particulars in the property configuration later. 41 | 42 | The security configuration is pretty straightforward. 43 | We're an OAuth client. 44 | We want to prompt users to login with a particular client. 45 | Once a user is authenticated, well, there's not much for them to see! 46 | We need Spring Cloud Gateway to connect our other HTTP services to the user visiting this `gateway` service. 47 | 48 | We'll configure two Spring Cloud Gateway _routes_. 49 | Each request has a predicate, optional filter(s), and a destination URI. 50 | The predicate defines how requests to the Spring Cloud Gateway service are matched, e.g.: does this request have a particular header or cookie, or a particlar path, or a particular virtual host? 51 | You may specify one or more filters that act on the incoming requests, changing it. 52 | Finally, the request, after it has passed through any and all filters, is sent to a final destination, which we specify with a URI. 53 | 54 | [source,java] 55 | ----- 56 | include::gateway/src/main/java/bootiful/gateway/GatewayConfiguration.java[] 57 | ----- 58 | 59 | <1> the first route matches all requests to `/api/**`, notes and forwards any OAuth JWTs to the backend service, and changes the path of the request from `gateway:8082/api/foo` to `api:8081/foo`, dropping the `/api/` bit. 60 | <2> the second route takes every other request and sends it unchecked on to the HTTP endpoint service up the static HTMl 5 and JavaScript assets. 61 | 62 | That's just about all the Java code for this service, but its role and importance in the architecture can not be overstated. 63 | Let's look at the property file that ties it all together. 64 | 65 | [source,properties] 66 | ----- 67 | include::gateway/src/main/resources/application.properties[] 68 | ----- 69 | 70 | <1> this process will run on port `8082`. 71 | <2> it will use the issuer URI to validate JWTs if it detects them 72 | <3> otherwise it will use this configuration to, acting as a client, inintiate an OIDC login to allow you to come up with a valid token. 73 | 74 | The last several lines are where we tell the OAuth client what it should ask for from our OAuth IDP (the Spring Authorization Server). 75 | You'd write similar configuration for any OAuth IDP, not just the Spring Authorization Server. 76 | In this example, the client is configured to identify itself as the `crm` client, with `crm` as the client secret. 77 | It's being configured to ask for the `authorization_code` authorization grant. 78 | It's being configured to instruct the OAuth IDP to rediect _back_ to the gateway, with a special pseudo URL syntax: `{baseUrl}/login/oauth2/code/{registrationId}`. 79 | Spring Security will set that endpoint up for us on the gateway, so there's nothing we need to do for that to work. 80 | Finally, the OAuth client asks for certain permissions: `user.read` and `openid`. 81 | 82 | == A User Interface for a Browser User 83 | 84 | The OAuth client is where all outside visitors will begin their journey. 85 | But obviously, they're not going to be issuing HTTP requests with `curl` , they're going to be expecting some sort of user interface. 86 | We looked at how Spring Cloud Gateway requests will route proxied resources: `/api/**` to our backend `api` and everything else to the static HTML5 and JavaScript code. 87 | In this case, it's just running on our local machines, but one images this stuff would be deployed to a CDN and made available locally in a real production application. 88 | 89 | In order to keep things as simple as possible, I've written a snigle page `.html` application with a smattering - a pinch, a dash, even! - of JavaScript. 90 | The concepts will be the same whether you eventually use Vue.js, React, Angular, jQuery, or whatever other thing you decide to use. 91 | 92 | Here's the `.html` file. 93 | It has a panel to greet the signed in user and another to show a grid of `customer` records. 94 | 95 | [source,html] 96 | ---- 97 | include::static/index.html[] 98 | ---- 99 | 100 | Both the user name and the `customer` records we'll load by talking to the API from JavaScript. 101 | 102 | [source,html] 103 | ---- 104 | include::static/app.js[] 105 | ---- 106 | 107 | <.> the first two functions of the file do the work of interacting with our API using the non-blocking, but oh-so-verbose, `async`/`await` syntax. 108 | (If only JavaScript had Java 21's Project Loom and virtual threads!). 109 | NB: we're making these requests relative to the `/api` path. 110 | We've written the JavaScript code in such a way that it knows that it's being accessed via an HTTP proxy, our Spring Cloud Gateway gateway. 111 | <.> this function is a little more interesting in that we're also interacting with the backend API but we're sending `POST` calls, not `GET` calls. 112 | And of course we've got a little animation goin' on. 113 | <.> there are at least two separate places that I need the `id` that'll have been assigned to a `div` for records for a particular `customer`. 114 | So I've extracted it into a sepearate function here. 115 | <.> this is the heart of the code: when the page loads, we read the data for the customers and the user's name and then draw the page with some DOM manipulation. 116 | This code also wires up an event so that when you click on one of the rendered buttons, it triggers a `POST` which initiates a message sent to RabbitMQ. 117 | 118 | I love this code, not because of the JavaScript (bleargh!), but because it's so plain to understand. 119 | Remember, this code will run in a browser that has cookies associated with an HTTP session, and that HTTP session in turn is connected with a valid OAuth token. 120 | So each time the JavaScript code calls `/api/customers`, it's implicitly sending the associated cookie to the gateway code, which in turn then allows the gateway to find the OAuth session associated with this user and to then forward the token to the backend `api`. 121 | 122 | At no point does the JavaScript code ever see your username and password. 123 | And, in this case, it doesn't even see your JWT! 124 | The code is written in such a way that it doesn't even know OAuth is involved. 125 | Now you can develop the application without OAuth, get it working, then add the OAuth resource server and OAuth client support to the backend code and call it done. 126 | 127 | Remember, the user visiting the HTML page won't even see any of this markup unless they've authenticated. 128 | They'll be redirected away before they ever get a chance. 129 | This page is both completely and blissfully unaware of any security, and it doesn't need to be aware of security. 130 | 131 | -------------------------------------------------------------------------------- /gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.2.0-M3 9 | 10 | 11 | bootiful 12 | gateway 13 | 0.0.1-SNAPSHOT 14 | gateway 15 | Demo project for Spring Boot 16 | 17 | 21 18 | 2023.0.0-M2 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-webflux 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-security 28 | 29 | 30 | org.springframework.cloud 31 | spring-cloud-starter-gateway 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-oauth2-client 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | io.projectreactor 44 | reactor-test 45 | test 46 | 47 | 48 | 49 | 50 | 51 | org.springframework.cloud 52 | spring-cloud-dependencies 53 | ${spring-cloud.version} 54 | pom 55 | import 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.graalvm.buildtools 64 | native-maven-plugin 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-maven-plugin 69 | 70 | 71 | 72 | 73 | 74 | spring-milestones 75 | Spring Milestones 76 | https://repo.spring.io/milestone 77 | 78 | false 79 | 80 | 81 | 82 | 83 | 84 | spring-milestones 85 | Spring Milestones 86 | https://repo.spring.io/milestone 87 | 88 | false 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /gateway/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mvn clean spring-boot:run 3 | -------------------------------------------------------------------------------- /gateway/src/main/java/bootiful/gateway/GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package bootiful.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | import java.util.concurrent.Executor; 7 | import java.util.concurrent.Executors; 8 | 9 | @SpringBootApplication 10 | public class GatewayApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(GatewayApplication.class, args); 14 | } 15 | 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /gateway/src/main/java/bootiful/gateway/GatewayConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.gateway; 2 | 3 | import org.springframework.cloud.gateway.route.RouteLocator; 4 | import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | class GatewayConfiguration { 10 | 11 | @Bean 12 | RouteLocator gateway(RouteLocatorBuilder rlb) { 13 | var apiPrefix = "/api/"; 14 | return rlb 15 | .routes() 16 | // <1> 17 | .route(rs -> rs 18 | .path(apiPrefix + "**") 19 | .filters(f -> f 20 | .tokenRelay() 21 | .rewritePath(apiPrefix + "(?.*)", "/$\\{segment}") 22 | ) 23 | .uri("http://localhost:8081")) 24 | // <2> 25 | .route(rs -> rs 26 | .path("/**") 27 | .uri("http://localhost:8020") 28 | ) 29 | .build(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /gateway/src/main/java/bootiful/gateway/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.gateway; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.Customizer; 6 | import org.springframework.security.config.web.server.ServerHttpSecurity; 7 | import org.springframework.security.web.server.SecurityWebFilterChain; 8 | 9 | @Configuration 10 | class SecurityConfiguration { 11 | 12 | // <1> 13 | @Bean 14 | SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { 15 | http 16 | .authorizeExchange((authorize) -> authorize.anyExchange().authenticated())//<2> 17 | .csrf(ServerHttpSecurity.CsrfSpec::disable)// <3> 18 | .oauth2Login(Customizer.withDefaults())//<4> 19 | .oauth2Client(Customizer.withDefaults()); 20 | return http.build(); 21 | } 22 | 23 | 24 | } 25 | -------------------------------------------------------------------------------- /gateway/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # <1> 2 | server.port=8082 3 | # <2> 4 | spring.security.oauth2.client.provider.spring.issuer-uri=http://localhost:8080 5 | # <3> 6 | spring.security.oauth2.client.registration.spring.provider=spring 7 | spring.security.oauth2.client.registration.spring.client-id=crm 8 | spring.security.oauth2.client.registration.spring.client-secret=crm 9 | spring.security.oauth2.client.registration.spring.authorization-grant-type=authorization_code 10 | spring.security.oauth2.client.registration.spring.client-authentication-method=client_secret_basic 11 | spring.security.oauth2.client.registration.spring.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} 12 | spring.security.oauth2.client.registration.spring.scope=user.read,openid 13 | -------------------------------------------------------------------------------- /gateway/src/test/java/bootiful/gateway/GatewayApplicationTests.java: -------------------------------------------------------------------------------- 1 | package bootiful.gateway; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class GatewayApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /idea.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | idea authorization-server/build.gradle & 4 | idea gateway/build.gradle & 5 | idea api/build.gradle & 6 | webstorm static/ & 7 | cd static && run.sh -------------------------------------------------------------------------------- /images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/images/.DS_Store -------------------------------------------------------------------------------- /images/google-consent-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/images/google-consent-screen.png -------------------------------------------------------------------------------- /images/google-is-it-you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/images/google-is-it-you.png -------------------------------------------------------------------------------- /images/google-mfa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/images/google-mfa.png -------------------------------------------------------------------------------- /images/google-signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/images/google-signin.png -------------------------------------------------------------------------------- /images/yelp-logged-in-with-google-part-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/images/yelp-logged-in-with-google-part-2.png -------------------------------------------------------------------------------- /images/yelp-logged-in-with-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/images/yelp-logged-in-with-google.png -------------------------------------------------------------------------------- /images/yelp-signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/images/yelp-signup.png -------------------------------------------------------------------------------- /java/.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | 9 | jobs: 10 | compliance: 11 | uses: reactive-spring-book/actions/.github/workflows/default-flow.yml@main 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /java/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /java/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.2.0-M3 9 | 10 | 11 | bootiful 12 | java 13 | 0.0.1-SNAPSHOT 14 | java 15 | Demo project for Spring Boot 16 | 17 | 21 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-test 27 | test 28 | 29 | 30 | org.springframework.integration 31 | spring-integration-test 32 | test 33 | 34 | 35 | 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-maven-plugin 41 | 42 | 43 | 44 | 45 | 46 | spring-milestones 47 | Spring Milestones 48 | https://repo.spring.io/milestone 49 | 50 | false 51 | 52 | 53 | 54 | 55 | 56 | spring-milestones 57 | Spring Milestones 58 | https://repo.spring.io/milestone 59 | 60 | false 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /java/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mvn clean package -------------------------------------------------------------------------------- /java/snippets/traditional-io: -------------------------------------------------------------------------------- 1 | Executor executor = Executors.newCachedThreadPool(); 2 | executor.submit(() -> { 3 | InputStream in = ... 4 | System.out.println("before"); 5 | int next = in.read(); 6 | System.out.println("after"); 7 | }); -------------------------------------------------------------------------------- /java/src/main/java/bootiful/javareloaded/Main.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded; 2 | 3 | public class Main { 4 | 5 | public static void main(String[] args) { 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /java/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-tips/spring-authorization-server-book/8a6d1cf16b97b2d471da1bbebc5994b7a991ca08/java/src/main/resources/application.properties -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/LambdasTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.function.Function; 7 | 8 | class LambdasTest { 9 | 10 | @Test 11 | void lambdas() { 12 | Function stringIntegerFunction = str -> 2;// <1> 13 | interface MyHandler { 14 | 15 | String handle(String one, Integer two); 16 | 17 | } 18 | MyHandler withExplicit = (one, two) -> one + ':' + two;// <2> 19 | Assertions.assertEquals(stringIntegerFunction.apply(""), 2); 20 | Assertions.assertEquals(withExplicit.handle("one", 2), "one:2"); 21 | var withVar = (MyHandler) (one, two) -> one + ':' + two;// <3> 22 | Assertions.assertEquals(withVar.handle("one", 2), "one:2"); 23 | MyHandler delegate = this::doHandle; // <4> 24 | Assertions.assertEquals(delegate.handle("one", 2), "one:2"); 25 | 26 | } 27 | 28 | private String doHandle(String one, Integer two) { 29 | return one + ':' + two; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/MultilineStringsTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.Instant; 7 | 8 | class MultilineStringsTest { 9 | 10 | private final String instant = Instant.now().toString(); 11 | 12 | // <1> 13 | private final String multilines = String.format(""" 14 | 15 | 16 |

Hello, world, @ %s!

17 | 18 | """, instant).trim(); 19 | 20 | // <2> 21 | private final String concatenated = "\n\n" + "

Hello, world, @ " + instant + "!

\n" 22 | + ""; 23 | 24 | @Test 25 | void stringTheory() { 26 | Assertions.assertEquals(this.multilines, this.concatenated); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/SealedTypesTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.util.Assert; 6 | 7 | class SealedTypesTest { 8 | 9 | // <1> 10 | sealed interface Shape permits Oval, Polygon { 11 | 12 | } 13 | 14 | // <2> 15 | static sealed class Oval implements Shape permits Circle { 16 | 17 | } 18 | 19 | static final class Circle extends Oval { 20 | 21 | } 22 | 23 | static final class Polygon implements Shape { 24 | 25 | } 26 | 27 | // <3> 28 | // static final class Rhombus implements Shape {} 29 | 30 | // <4> 31 | private String describeShape(Shape shape) { 32 | Assert.notNull(shape, () -> "the shape should never be null!"); 33 | if (shape instanceof Oval) 34 | return "round"; 35 | if (shape instanceof Polygon) 36 | return "straight"; 37 | throw new RuntimeException("we should never get to this point!"); 38 | } 39 | 40 | @Test 41 | void disjointedUnionTypes() { 42 | Assertions.assertEquals(describeShape(new Oval()), "round"); 43 | Assertions.assertEquals(describeShape(new Polygon()), "straight"); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/SmartCastsTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.HashSet; 7 | 8 | public class SmartCastsTest { 9 | 10 | @Test 11 | void casts() { 12 | interface Animal { 13 | 14 | String speak(); 15 | 16 | } 17 | 18 | class Cat implements Animal { 19 | 20 | @Override 21 | public String speak() { 22 | return "meow!"; 23 | } 24 | 25 | } 26 | 27 | class Dog implements Animal { 28 | 29 | @Override 30 | public String speak() { 31 | return "woof!"; 32 | } 33 | 34 | } 35 | 36 | var newPet = Math.random() < .5 ? new Cat() : new Dog(); 37 | var messages = new HashSet(); 38 | 39 | // <1> 40 | if (newPet instanceof Cat) { 41 | var cat = (Cat) newPet; 42 | messages.add("the cat says " + cat.speak()); 43 | } 44 | // <2> 45 | if (newPet instanceof Cat cat) { 46 | messages.add("the cat says " + cat.speak()); 47 | } 48 | 49 | if (newPet instanceof Dog dog) { 50 | messages.add("the dog says " + dog.speak()); 51 | } 52 | 53 | Assertions.assertEquals(messages.size(), 1); 54 | Assertions.assertTrue(messages.contains("the dog says woof!") || messages.contains("the cat says meow!")); 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/closeable/TraditionalResourceHandlingTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.closeable; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.*; 7 | 8 | import static bootiful.javareloaded.closeable.Utils.error; 9 | 10 | class TraditionalResourceHandlingTest { 11 | 12 | private final File file = Utils.setup();// <1> 13 | 14 | @Test 15 | void read() { 16 | var bufferedReader = (BufferedReader) null; // <2> 17 | try { 18 | bufferedReader = new BufferedReader(new FileReader(this.file)); 19 | var stringBuilder = new StringBuilder(); 20 | var line = (String) null; 21 | while ((line = bufferedReader.readLine()) != null) { 22 | stringBuilder.append(line); 23 | stringBuilder.append(System.lineSeparator()); 24 | } 25 | var contents = stringBuilder.toString().trim(); 26 | Assertions.assertEquals(contents, Utils.CONTENTS); 27 | } // 28 | catch (IOException e) { // <3> 29 | error(e); 30 | } // 31 | finally { // <4> 32 | close(bufferedReader); 33 | } 34 | } 35 | 36 | private static void close(Reader reader) { // <3> 37 | if (reader != null) { 38 | try { 39 | reader.close(); 40 | } // 41 | catch (IOException e) { 42 | error(e); 43 | } 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/closeable/TryWithResourcesTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.closeable; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.File; 8 | import java.io.FileReader; 9 | import java.io.IOException; 10 | 11 | class TryWithResourcesTest { 12 | 13 | private final File file = Utils.setup(); 14 | 15 | @Test 16 | void tryWithResources() { 17 | try (var fileReader = new FileReader(this.file);// 18 | var bufferedReader = new BufferedReader(fileReader)) { 19 | var stringBuilder = new StringBuilder(); 20 | var line = (String) null; 21 | while ((line = bufferedReader.readLine()) != null) { 22 | stringBuilder.append(line); 23 | stringBuilder.append(System.lineSeparator()); 24 | } 25 | var contents = stringBuilder.toString().trim(); 26 | Assertions.assertEquals(contents, Utils.CONTENTS); 27 | } // 28 | catch (IOException e) { 29 | Utils.error(e); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/closeable/Utils.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.closeable; 2 | 3 | 4 | import java.io.File; 5 | import java.nio.file.Files; 6 | import java.time.Instant; 7 | 8 | 9 | public class Utils { 10 | 11 | // <1> 12 | static String CONTENTS = String.format(""" 13 | 14 |

Hello, world, @ %s !

15 | 16 | """, Instant.now().toString()).trim(); 17 | 18 | // <2> 19 | static File setup() { 20 | try { 21 | var path = Files.createTempFile("bootiful", ".txt"); 22 | var file = path.toFile(); 23 | file.deleteOnExit(); 24 | Files.writeString(path, CONTENTS); 25 | return file; 26 | }// 27 | catch (Throwable t) { 28 | error(t); 29 | throw new RuntimeException("could not produce a new file"); 30 | } 31 | 32 | } 33 | 34 | // <3> 35 | static void error(Throwable throwable) {// <2> 36 | System.err.println( 37 | "there's been an exception processing the read! " + 38 | throwable.getMessage()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/loom/LoomTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.loom; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | import java.util.concurrent.ConcurrentSkipListSet; 10 | import java.util.stream.IntStream; 11 | 12 | class LoomTest { 13 | 14 | @Test 15 | void threads() throws Exception { 16 | var switches = 5; 17 | var observed = new ConcurrentSkipListSet(); 18 | var threads = IntStream 19 | .range(0, 1000)// <.> 20 | .mapToObj(index -> Thread 21 | .ofVirtual()// <.> 22 | .unstarted(() -> { 23 | for (var i = 0; i < switches; i++) // <.> 24 | observed.addAll(observe(index)); 25 | })) 26 | .toList(); 27 | for (var t : threads) t.start(); 28 | for (var t : threads) t.join(); 29 | Assertions.assertTrue(observed.size() > 1); // <.> 30 | } 31 | 32 | private static Set observe(int index) { 33 | var before = Thread.currentThread().toString(); 34 | try { 35 | Thread.sleep(100); 36 | }// 37 | catch (InterruptedException e) { 38 | throw new RuntimeException(e); 39 | } 40 | var after = Thread.currentThread().toString(); 41 | return index == 0 ? new HashSet<>(Arrays.asList(before, after)) : Set.of(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/patternmatching/PatternMatchingTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.patternmatching; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | class PatternMatchingTest { 8 | 9 | // <.> 10 | sealed interface BrowserClient permits UnknownUser, Customer { 11 | } 12 | 13 | record UnknownUser() implements BrowserClient { 14 | } 15 | 16 | record Customer(Integer id, String name) implements BrowserClient { 17 | } 18 | 19 | // <.> 20 | final String authenticateMessage = """ 21 | please authenticate, so that we can 22 | get to know you a little better. 23 | """ 24 | .stripIndent() 25 | .stripLeading() 26 | .stripTrailing(); 27 | 28 | final String customerMessage = "the customer's name is %s"; 29 | 30 | // <.> 31 | String showMessageWithIf(BrowserClient browserClient) { 32 | var message = ""; 33 | if (browserClient instanceof Customer(var id, var name)) { 34 | message = this.customerMessage.formatted(name); 35 | } else if (browserClient instanceof UnknownUser) { 36 | message = this.authenticateMessage; 37 | } 38 | return message; 39 | } 40 | 41 | // <.> 42 | String showMessageWithSwitch(BrowserClient browserClient) { 43 | return switch (browserClient) { 44 | case Customer(var id, var name) -> this.customerMessage.formatted(name); 45 | case UnknownUser() -> this.authenticateMessage; 46 | }; 47 | } 48 | 49 | @Test 50 | void patternMatching() throws Exception { 51 | assertTrue(showMessageWithIf(new Customer(1, "Josh")).contains(this.customerMessage.formatted("Josh"))); 52 | assertTrue(showMessageWithSwitch(new UnknownUser()).contains(this.authenticateMessage)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/records/RecordConstructorsTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.records; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.util.Assert; 6 | import org.springframework.util.StringUtils; 7 | 8 | class RecordConstructorsTest { 9 | 10 | record Customer(Integer id, String email) { // <1> 11 | 12 | Customer { // <2> 13 | Assert.notNull(id, () -> "the id must never be null!"); 14 | Assert.isTrue(StringUtils.hasText(email), () -> "the email is invalid"); 15 | } 16 | 17 | Customer(String email) { 18 | this(-1, email); 19 | } 20 | } 21 | 22 | @Test 23 | void multipleConstructors() { 24 | var customer1 = new Customer("test@email.com"); 25 | var customer2 = new Customer(2, "test2@gmail.com"); 26 | Assertions.assertEquals(customer1.id(), -1); 27 | Assertions.assertEquals(customer2.id(), 2); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/records/SimpleRecordsTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.records; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.List; 7 | 8 | 9 | class SimpleRecordsTest { 10 | 11 | // <1> 12 | record Customer(Integer id, String name) { 13 | } 14 | 15 | record Order(Integer id, double total) { 16 | } 17 | 18 | record CustomerOrders(Customer customer, List orders) { 19 | } 20 | 21 | @Test 22 | void records() { 23 | var customer = new Customer(253, "Tammie"); 24 | var order1 = new Order(2232, 74.023); 25 | var order2 = new Order(9593, 23.44); 26 | var cos = new CustomerOrders(customer, List.of(order1, order2)); 27 | Assertions.assertEquals(order1.id(), 2232); 28 | Assertions.assertEquals(order1.total(), 74.023); 29 | Assertions.assertEquals(customer.name(), "Tammie"); 30 | Assertions.assertEquals(cos.orders().size(), 2); 31 | System.out.println("order components " + order1.id() + ':' + order1.total()); // <2> 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/switches/EnhancedSwitchExpressionTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.switches; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class EnhancedSwitchExpressionTest { 7 | 8 | @Test 9 | void switchExpression() { 10 | Assertions.assertEquals(respondToEmotionalState(Emotion.HAPPY), "that's wonderful."); 11 | Assertions.assertEquals(respondToEmotionalState(Emotion.SAD), "I'm so sorry to hear that."); 12 | } 13 | 14 | private String respondToEmotionalState(Emotion emotion) { // <1> 15 | return switch (emotion) { 16 | case HAPPY -> "that's wonderful."; 17 | case SAD -> "I'm so sorry to hear that."; 18 | }; 19 | } 20 | 21 | enum Emotion { 22 | 23 | HAPPY, SAD; 24 | 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/switches/TraditionalSwitchExpressionTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.switches; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | class TraditionalSwitchExpressionTest { 7 | 8 | enum Emotion { 9 | 10 | // <1> 11 | HAPPY, SAD 12 | 13 | } 14 | 15 | @Test 16 | void switchExpression() { 17 | Assertions.assertEquals(respondToEmotionalState(Emotion.HAPPY), "that's wonderful."); 18 | Assertions.assertEquals(respondToEmotionalState(Emotion.SAD), "I'm so sorry to hear that."); 19 | } 20 | 21 | public String respondToEmotionalState(Emotion emotion) { 22 | var response = ""; // <2> 23 | switch (emotion) { 24 | case HAPPY: 25 | response = "that's wonderful."; 26 | break; // <3> 27 | case SAD: 28 | response = "I'm so sorry to hear that."; 29 | break; 30 | } 31 | 32 | return response; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/typeinference/LambdasAndTypeInferenceTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.typeinference; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.HashSet; 7 | import java.util.List; 8 | 9 | class LambdasAndTypeInferenceTest { 10 | 11 | @FunctionalInterface 12 | interface MyHandler { 13 | 14 | String handle(String one, int two); 15 | 16 | } 17 | 18 | private String delegate(String s, Integer two) { 19 | return "Hello " + s + ":" + two; 20 | } 21 | 22 | @Test 23 | void lambdas() { 24 | MyHandler defaultHandler = this::delegate; // <1> 25 | var withVar = new MyHandler() { // <2> 26 | 27 | @Override 28 | public String handle(String one, int two) { 29 | return delegate(one, two); 30 | } 31 | }; 32 | var withCast = (MyHandler) this::delegate; // <3> 33 | var string = "hello"; 34 | var integer = 2; 35 | var set = new HashSet<>( // 36 | List.of(withCast.handle(string, integer), // 37 | withVar.handle(string, integer), // 38 | defaultHandler.handle(string, integer))); 39 | Assertions.assertEquals(set.size(), 1, "the 3 entries should all be the same, and thus deduplicated out"); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /java/src/test/java/bootiful/javareloaded/typeinference/TypeInferenceTest.java: -------------------------------------------------------------------------------- 1 | package bootiful.javareloaded.typeinference; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.Map; 7 | 8 | class TypeInferenceTest { 9 | 10 | @Test 11 | void infer() throws Exception { 12 | var map1 = Map.of("key", "value"); // <1> 13 | Map map2 = Map.of("key", "value"); 14 | Assertions.assertEquals(map2, map1); // <1> 15 | var anonymousSubclass = new Object() { 16 | final String name = "Peanut the Poodle"; 17 | 18 | int age = 7; 19 | 20 | }; 21 | // <2> 22 | Assertions.assertEquals(anonymousSubclass.age, 7); 23 | Assertions.assertEquals(anonymousSubclass.name, "Peanut the Poodle"); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /java/src/test/resources/data: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License.] -------------------------------------------------------------------------------- /oauth.adoc: -------------------------------------------------------------------------------- 1 | = Introducing OAuth 2 | 3 | OAuth is an open standard for access delegation that is commonly used to grant websites or applications access to information on other websites but without giving them the passwords. 4 | This allows users to permit third-party applications to access their data without sharing their credentials. 5 | 6 | == What is OAuth? 7 | 8 | OAuth (Open Authorization) is a protocol that lets applications securely act on a user's behalf. 9 | Think of it like a valet key for the internet. 10 | It provides a way for users to grant third-party apps limited access to their resources, either on a temporary or permanent basis, without revealing their credentials, in the same way that a car's valet key allows the valet to drive the car, but does not permit access to the secured, locked compartments of the vehicle like the glovebox or the trunk (or boot). 11 | 12 | You have probably used OAuth before! 13 | If you've ever clicked on a "Log in with Facebook" or "Log in with Google" or "Log in with Apple" button on a website, you were probably using OAuth. 14 | Instead of creating a new username and password for that website, you’re granting it permission to access a certain piece of information from platforms like Facebook, Google, or Apple, respectively. 15 | That information is usually just enough identity information to be able to put together a profile so that the service you're logging in to knows what to call you and how to correlate and associate requests with you. 16 | 17 | == Why is OAuth used? 18 | 19 | Before OAuth, user data was accessed by third-party apps in an cumbersome and insecure way: the user would have had to provide their own username and password to every application, meaning the application had full access to the account. 20 | Ideally, every service into which you authenticated was doing the right thing and fully encrypting your credentials, but if even one of them wasn't then your password is vulnerable should that service ever be hacked. 21 | 22 | This arrangement poses a security risk. 23 | It's also annoying: you'd have to get in the business of coming up with and maintaining passwords for every site you visit. 24 | Sites don't want your password, they want to know who they're dealing with! There's a difference. Passwords just create more work for everyone. As a person using the internet, there are slightly better tools available today than ye 'ole Google Doc with all your passwords, tools like LastPass, 1Password, etc., but they're bandaids on the real problem: you've got too many passwords to maintain. 25 | 26 | Most sites would be very happy to outsource the work of validating that you are who you say you are, and focusing on selling you stuff, presenting you stuff, or whatever else it is those sites did. 27 | OAuth lets them get out of that business. 28 | 29 | OAuth introduced a way to provide tokens instead of credentials, granting limited access to the user's data. 30 | If an attacker steals the token, they would only have access for a limited time and only to the data to which their token was entitled. 31 | This entitlement is called a _scope_. 32 | 33 | There are many scenarios where it makes sense to use OAuth. 34 | 35 | * Third-party applications and services: applications that want to access services like Google Drive, Twitter feeds, or Facebook posts. 36 | * Mobile Applications: smartphone applications that access web services but don’t want to store passwords. 37 | * Content Aggregation: an app that pulls data from multiple sources (e.g., various email accounts). 38 | * Federation: if you want need to maintain secure integrations with a number of OAuth platforms, it's possible to use an OAuth IDP like Spring Authorization Server to act as a facade. 39 | 40 | == OAuth is a big Improvement 41 | 42 | Before OAuth came onto the scene, the most common method of granting third-party applications access was by sharing usernames and passwords. 43 | 44 | This posed a series of challenges: 45 | 46 | * Security: sharing your actual password with third parties is inherently risky. 47 | * Full Access: third-party apps would have full access to a user's account. 48 | * Revocation: if a user wanted to stop an app from accessing their data, the only way was to change their password. 49 | 50 | OAuth changed this by not requiring users to share their passwords, letting users specify what data the application can access (scope), and providing a way to revoke access without changing user passwords. 51 | 52 | == Evolution of OAuth: 53 | 54 | OAuth has evolved over the years to address shortcomings and to better meet the requirements of developers and organizations. 55 | There are two main versions of OAuth: 56 | 57 | **OAuth 1.0**: The initial version, published in 2010. It was complex and required cryptographic signatures. 58 | There is also OAuth 1.0a, which is slightly better, but we're not stopping the tour to take a look at it. 59 | Not when our destination is so much more magnificent... 60 | 61 | **OAuth 2.0**: Introduced in 2012 as a successor, it simplifies the process and separates the roles of obtaining credentials and API access. 62 | It's more flexible, and while it doesn’t force encryption, when combined with HTTPS, it’s considered secure. 63 | 64 | == OAuth vs. SAML 65 | 66 | OAuth and SAML (Security Assertion Markup Language) are both protocols for identity and authentication, but they serve different purposes and have been designed with different use cases in mind: 67 | 68 | OAuth is primarily for authorization and delegated access to resources without sharing the original credentials. 69 | SAML, on the other hand, is focused on authentication and single sign-on (SSO) solutions, allowing users to log in once and gain access to multiple applications. 70 | OAuth is often used for token-based authentication, while SAML is used for enterprise-level SSO. 71 | SAML tokens are XML-based while OAuth tokens are JSON Web Tokens (JWTs) and are smaller and more efficient. 72 | SAML has a steeper learning curve and is more complex, while OAuth is simpler, especially in its 2.0 version. 73 | 74 | == Towards a passwordless future 75 | 76 | one of the key improvemtns of OAuth is that it centralizes authentication. Authentication is a tough problem, and there are a million things to get right for it to even begin to be considered secure. Okta, Google and Meta and Github and the Spring team have a vested effort in committing people and time to this ongoing effort so that you don't have to. Done correctly, a good OAuth integration can reduce the number of passwords you need to maintain to live on the internet by a considerable amount. 77 | 78 | in this book, we're going to be focusing on the Spring Authorization Server, which - once at the heart of your SSO solution - can greatly simplify the identity for your organization, reducing it down to one credential. 79 | 80 | But what about no credentials? Or, better, what about more secure, alternative credentials? That's the ideal, and the trend seems to be moving that way, with large providers like Apple embracing _passwordless_ logins tied to your biometrics. Behind the scenes, a lot of these solutions are integrating WebAuthn and Passkey. Conceptually, this sort of thing could be added to a Spring Authorization Server instance. 81 | 82 | 83 | -------------------------------------------------------------------------------- /preview.sh: -------------------------------------------------------------------------------- 1 | asciidoctor README.adoc && open README.html -------------------------------------------------------------------------------- /processor.adoc: -------------------------------------------------------------------------------- 1 | = The Processor 2 | 3 | // in this section we'll look at how to use OAuth to secure a headless backoffice process 4 | // the approach we'll take could be easily applied to other contexts, like Spring's WebSocket support using the `BeforeSocketHandshakeInterceptor` 5 | 6 | This chapter is my favorite one. 7 | Not because it's going to be the most useful, but because it's the one that took the longest for me to figure out. 8 | You see, I love backoffice code. 9 | I love messaging, and integration. 10 | I love analytics. 11 | I love workflow. 12 | I love stream and batch processing. 13 | I love workflow engines and business process management. 14 | I love grid computing. 15 | I love short-lived tasks like CRON jobs. 16 | I love all the sorts of stuff that has no business being anywhere near an HTTP request or taking up space on an HTTP webservice. 17 | We used to call these sorts of things "backoffice" jobs. 18 | And you've probably built some of these in your career, too. 19 | 20 | And I didn't know how OAuth fit in this world of backoffice code. 21 | I mean, just look at the Spring portfolio! 22 | We've got Spring Integration, Spring AMQP, Spring for Apache Kafka, Spring Cloud Data Flow, Spring Cloud Task, Spring Batch, and even Spring Shell. 23 | You can build all sorts of cool stuff with those libraries and never see an HTTP header! 24 | So how does OAuth fit here? 25 | And, more precisely, how does Spring Security's OAuth support plugin? 26 | We've already met the usual suspects: `spring-boot-starter-oauth-client`, `spring-boot-starter-resource-server`, etc. 27 | But those all assume the presence of an HTTP server and the Spring Security web filter chain. 28 | 29 | In this chapter we're going to expand our sample application a bit with some Spring Integration code that will receive a message that the API (the resource server) will send. 30 | Each message will contain the JWT token associated with the authenticated user and it'll contain a payload tat the processor will.., you know, process. 31 | In this example, we'll imagine that this processor is busy doing the work of sending emails. 32 | We're not going to actually write that bit of the code. 33 | But it is a good example. 34 | After all, sending email can sometimes take a long time and we don't want to keep the API busy doing this when it should be fielding HTTP requests. 35 | 36 | When the message arrives at this `processor` module, we'll validate the JWT token by talking to the Spring Authorization Server (our OAuth IDP) through its issuer URI. 37 | Does this sort of sound familiar? 38 | It should! 39 | We're going to basically do the same trick as the resource server did, albeit a bit more granuarly. 40 | Well get to see how the resource server support does some of its work. 41 | Its good to know this because, outside of an HTTP environment, there's no one-sized fits all approach. 42 | It's convenient then that we can easily plug this stuff in ourselves. 43 | 44 | == (Re-) Introducing Spring Integration 45 | 46 | Let's talk again about Spring Integration. we looked at it ever so briefly when we introduced the API, but let's review some basics. 47 | 48 | Remember this code from the API we built earlier? 49 | 50 | [source,java] 51 | ---- 52 | include::api/src/main/java/bootiful/api/EmailRequestsIntegrationFlowConfiguration.java[] 53 | ---- 54 | 55 | It's a bean of type `IntegrationFlow`, from the Spring Integration project. The bean describes how we handle messages intended for a RabbitMQ broker: messages come, then we turn it into JSON, then we send it over AMQP. Simple. 56 | 57 | Spring Integration is an old project, from 2007. The core conceit fo Spring Integration is that as we move forward in time the body of systems and services with which we need to integrate - to maximally retain value - grows, and the protocols and paradigms required for integration also grow. 58 | Spring Integration is an enterprise application technnology. 59 | It's designed to help glue systems together, particularly those things that wouldn't otherwise know about and work with each other. 60 | 61 | In 2004, Gregor Hohpe and Bobby Woolf wrote the book https://enterpriseintegrationpatterns.com[_Enterprise Integration Patterns_], which gave us the names for the patterns typical of integration solutions. 62 | Broadly, the book said, there are four different kinds of integration styles. 63 | 64 | * **RPC**: in this style, network services are made to work like a local object. 65 | In such a style, a client could invoke a method on a local Java object and have that translated into remote procedure calls on another object on another object, presumably running on another host. 66 | This style feels simple but it hides the reality of the network - that it will fail. 67 | It makes assumptions that it shouldn't, namely that the service will always be available. 68 | If the service - the consumer - is not available then has to retry or abandon the integration. 69 | Because of these design tradeoffs, RPC is a poor choice for service integration. 70 | 71 | * **Shared Database**: in this style, a client connects the same database (e.g.: Oracle, PostgreSQL, MongoDB, etc), writes data there that another client then reads. 72 | This is fragile because a schema change by one client might break another client. 73 | This approach completely violates the principles of encapsulation, exposing the peculiarities data storage to consumers, and is therefore a poor choice for service integration. 74 | There's a reason nobody ever says "put your best liver forward!" It's nobody's business what your liver looks like! 75 | They should be shaking your hand, instead. 76 | The same is true in integration: don't share too much. 77 | 78 | * **File synchronization**: in this style, a client deposits a file on a filesystem (NFS, FTP, FTPS, SFTP, SMB, etc.) that a client then consumes. 79 | This approach works alright but care must be taken so that the consumer of a file doesn't start processing it before the producer has finished writing it. 80 | Additionally, this approach lacks any sort of sophistication around message delivery (once and only once, transactions, message rollback) or routing. 81 | 82 | * **Messaging**: in this style, integration is done in terms of a messaging system like Apache Kafka or RabbitMQ. 83 | This is usually the best approach if you can get access to it. 84 | Messaging systems support transactions, they can be made to ensure a message is delivered, that it is delivered at most once, or at least once, etc. 85 | It can hanndle routing, allowing you to send the message to different consumers as required. 86 | And of course it does not couple producer or consumer; a consumer can send a message to the messaging system, and it'll be recorded there. 87 | When the consumer is available and able, it can read and process the message. 88 | We can say that this style of integration is the most decoupled: producers and consumers don't need to both be available at the same time; producers and consumers only see and agree upon message payloads, and not the internals of their state management schemes. 89 | 90 | Generally, speaking, messaging is the most flexible approach to building and integrating systems. 91 | So it is that Spring Integration models everything as `Message` objects that pass through `MessageChannel` objects. 92 | A `MessageChannel` is the connective tissue between components that act on `Message` objects, in a sort of pipeline. 93 | These components are written in terms of the `Message`s they accept and the `Message`s they produce. 94 | They're otherwise usually stateless. 95 | In a way, Spring Integration encourages a lot of the same discipline and conventions as any functional programming language might. 96 | You're encouraged to write your business logic in terms of granular, composable, and reusable functions. 97 | 98 | Where do these messages come from, and where do they go? Well, the real world of course! They have to come from somewhere. It is Spring Integration's job to connect our code to events in the real world and translate them into `Message` objects: a microwave turned on (MQTT), a new file appeared in an FTP service, a new row appeared in a SQL database, a new email was sent to an inbox, a message arrived on a JMS destination, etc. It does this work with adapters which _adapt_ events into `Message` objects. Each `Message` typically contains a payload (the inbound file adapter might contain a `java.io.File` payload, for example) and headers telling us about the payload (the folder in which the file was found, or the timestamp of when the message was produced). 99 | 100 | Once we have a `Message` ,we can do all sorts of things to it. We could split it into smaller messages, route it to other handlers, add information to it, filter it, etc. 101 | 102 | And then when we're finally done with it, we use an outbound adapter - which does the reverse of an inbound adapter -to send the message onward to some place in the real world. 103 | 104 | So, to review the review: events come via inbound adapters, they're turned into `Message` objects with headers and payloads. In this form they're processed, and ultimately sent out via outbound adapters. 105 | 106 | Now back to our regularly scheduled programming. 107 | 108 | == Defining RabbitMQ Infrastructure 109 | 110 | First thing's first: we're using RabbitMQ. In RabbitMQ, you send messages to an exchange which then can route it anyway it wants. In our case, it's going to be sent to a single queue, which is where consumers will know to look for it. So, we have an exchange and a queue, and they're bound together through something called a binding. We'll use the Spring AMQP project to make short work of defining these things. If we define them as beans, Spring AMQP will automaticall create the real structures on the RabbitMQ broker. 111 | 112 | [source,java] 113 | ---- 114 | include::processor/src/main/java/bootiful/processor/AmqpConfiguration.java[] 115 | ---- 116 | 117 | You'll notice that this code uses a constant variable I've defined. There are a few others... 118 | 119 | == A Few Well Known Constants 120 | 121 | There are a few things that I've defined as constants: a header name, a `MessageChannel` bean ID, and the RabbitMQ destination. 122 | 123 | [source,java] 124 | ---- 125 | include::processor/src/main/java/bootiful/processor/Constants.java[] 126 | ---- 127 | 128 | === The Integration 129 | 130 | Now we're into the meat of the example, the actual Spring Integration code. 131 | 132 | In Spring Integration, an `IntegrationFlow` is the definition of a processing pipeline. You may have more than one `IntegrationFlow` in your application. `IntegrationFlow` objects chain together different components that act on the `Message` objects within the flow. You can route messages from one `IntegrationFlow` to another using `MessageChannel` instances. 133 | 134 | Conceptually, all we want to do is take a message from the inbound AMQP (that's the protocol that RabbitMQ speaks) in and then print out the message. One might send an email or do something useful here, but we'll leave that as an exercise for another day... And yet, I've written it a little more obtusely: we have one `IntegrationFlow` that takes the message rom AMQP and then stuffs the message into a `MessageChannel`. It pops out the other side and _then_ we print out the message's payload and headers. Why the indirection? Why add the `MessageChannel` into the mix, I hear you query. `MessageChannel` objects can have interceptors that can, in effect, _veto_ messages. So we'll configure some interceptors to act on the message, validate the attached JWT token, and if it doesn't match, to reject the message _before_ it gets to whatever important business logic we've got downstream of thee `MessageChannel`. 135 | 136 | Let's see it all in action. 137 | 138 | 139 | 140 | [source,java] 141 | ---- 142 | include::processor/src/main/java/bootiful/processor/IntegrationConfiguration.java[] 143 | ---- 144 | <.> the first `IntegrationFlow` defines an AMQP Inbound adapter which listens for new `Messages` on our auto configured RabbitMQ connection. As soon as a message comes in, it gets sent to the next step in the `INtegrationFlow`. IN this case, that next step is to travel through an injected `MessageChannel` to another `IntegrationFlow`. 145 | <.> the second `IntegrationFlow` takes whatevers been stuffed into the `MessageChannel` and passews it to the next step, which is a simple handler that inspects the message payload and headers. 146 | <.> both `IntegrationFlow` objects are connected by the `MessageChannel` whoe bean ID is `Constants.REQUESTS_MESSAGE_CHANNEL`, and whose definition we see here. It's a bit complicated but this is where we do the work of validating the JWT token. 147 | <.> most of the important woerk is done in these interceptors. Each interceptr can inspect, transform, or reject the `Message` objects flowign through the `MessageChannel` on which they're configured. I wrote this first interceptor. We'll look at it shortyl, but suffice it to say that it in turn is using the injected `JwtAuthenticationProvider` that is provided by Spring Security and whose configuration we'll examine momentarily to do the work of validating the JWT token against the Spring Authorization Server. If the token is valid, the interceptor creates a new message with a valid Spring Security `Authentication` in the header. If the token is not valid, then the interceptor will create a new message with a null value for the header. 148 | <.> this next interceptor hoists the `Authentication` from the message header and puts it into the Spring Security `SecurityContextHolder`, which is a well-known `ThreadLocal`-like holder of the current thread's authenticated user. Now, all the other machinery in Spring Security that consults this thread local will work correctly. 149 | <.> the final interceptor does the actual check, rejecting the message if the current thread doesn't have a valid, authenticated `Authentication`. 150 | 151 | The `JwtAuthenticationInterceptor` leaves nothing to the imagination: 152 | 153 | [source,java] 154 | ---- 155 | include::processor/src/main/java/bootiful/processor/JwtAuthenticationInterceptor.java[] 156 | ---- 157 | <.> We've defined this bean elsewhere and injected it here 158 | <.> in which header shall we place the authentication once we've validated the token? 159 | <.> extract the token from the header 160 | <.> use the `AuthenticationProvider` to talk to the Spring Authorizatoin Server. 161 | <.> if the JWT is in fact valid then we'll create a new `Message`, cloning the headers and payload from the incoming message, but specufying one new header, whose value is an object of type `Authentication`. 162 | <.> if the JWT is not valid, then create a new `Message` with a null header value. 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /processor/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.2.0-M3 9 | 10 | 11 | bootiful 12 | processor 13 | 0.0.1-SNAPSHOT 14 | api 15 | Demo project for Spring Boot 16 | 17 | 21 18 | 19 | 20 | 21 | org.springframework.integration 22 | spring-integration-security 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-amqp 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-integration 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-oauth2-resource-server 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-json 39 | 40 | 41 | org.springframework.integration 42 | spring-integration-amqp 43 | 44 | 45 | org.springframework.integration 46 | spring-integration-http 47 | 48 | 49 | org.springframework.integration 50 | spring-integration-jdbc 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-test 56 | test 57 | 58 | 59 | org.springframework.amqp 60 | spring-rabbit-test 61 | test 62 | 63 | 64 | org.springframework.integration 65 | spring-integration-test 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-maven-plugin 75 | 76 | 77 | 78 | 79 | 80 | spring-milestones 81 | Spring Milestones 82 | https://repo.spring.io/milestone 83 | 84 | false 85 | 86 | 87 | 88 | 89 | 90 | spring-milestones 91 | Spring Milestones 92 | https://repo.spring.io/milestone 93 | 94 | false 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /processor/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | mvn clean spring-boot:run 3 | -------------------------------------------------------------------------------- /processor/src/main/java/bootiful/processor/AmqpConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.processor; 2 | 3 | import org.springframework.amqp.core.*; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import static bootiful.processor.Constants.RABBITMQ_DESTINATION_NAME; 8 | 9 | @Configuration 10 | class AmqpConfiguration { 11 | 12 | @Bean 13 | Queue queue() { 14 | return QueueBuilder.durable(RABBITMQ_DESTINATION_NAME).build(); 15 | } 16 | 17 | @Bean 18 | Exchange exchange() { 19 | return ExchangeBuilder.directExchange(RABBITMQ_DESTINATION_NAME).build(); 20 | } 21 | 22 | @Bean 23 | Binding binding() { 24 | return BindingBuilder.bind(queue()).to(exchange()).with(RABBITMQ_DESTINATION_NAME).noargs(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /processor/src/main/java/bootiful/processor/Constants.java: -------------------------------------------------------------------------------- 1 | package bootiful.processor; 2 | 3 | public class Constants { 4 | 5 | public final static String REQUESTS_MESSAGE_CHANNEL = "requests"; 6 | 7 | public static final String RABBITMQ_DESTINATION_NAME = "emails"; 8 | 9 | public static final String AUTHORIZATION_HEADER_NAME = "jwt"; 10 | 11 | } 12 | -------------------------------------------------------------------------------- /processor/src/main/java/bootiful/processor/IntegrationConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.processor; 2 | 3 | import org.slf4j.LoggerFactory; 4 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.integration.amqp.dsl.Amqp; 9 | import org.springframework.integration.dsl.DirectChannelSpec; 10 | import org.springframework.integration.dsl.IntegrationFlow; 11 | import org.springframework.integration.dsl.MessageChannels; 12 | import org.springframework.messaging.MessageChannel; 13 | import org.springframework.security.authorization.AuthenticatedAuthorizationManager; 14 | import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; 15 | import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; 16 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; 17 | 18 | import static bootiful.processor.Constants.AUTHORIZATION_HEADER_NAME; 19 | import static bootiful.processor.Constants.RABBITMQ_DESTINATION_NAME; 20 | 21 | @Configuration 22 | class IntegrationConfiguration { 23 | 24 | 25 | // <.> 26 | @Bean 27 | IntegrationFlow inboundAmqpRequestsIntegrationFlow( 28 | @Qualifier(Constants.REQUESTS_MESSAGE_CHANNEL) MessageChannel requests, 29 | ConnectionFactory connectionFactory) { 30 | var inboundAmqpAdapter = Amqp 31 | .inboundAdapter(connectionFactory, RABBITMQ_DESTINATION_NAME); 32 | return IntegrationFlow 33 | .from(inboundAmqpAdapter) 34 | .channel(requests) 35 | .get(); 36 | } 37 | 38 | // <.> 39 | @Bean 40 | IntegrationFlow requestsIntegrationFlow( 41 | @Qualifier(Constants.REQUESTS_MESSAGE_CHANNEL) MessageChannel requests) { 42 | 43 | var log = LoggerFactory.getLogger(getClass()); 44 | 45 | return IntegrationFlow 46 | .from(requests)// 47 | .handle((payload, headers) -> { 48 | log.info("----"); 49 | headers.forEach((key, value) -> log.info("{}={}", key, value)); 50 | return null; 51 | })// 52 | .get(); 53 | } 54 | 55 | // <.> 56 | @Bean(Constants.REQUESTS_MESSAGE_CHANNEL) 57 | DirectChannelSpec requests(JwtAuthenticationProvider jwtAuthenticationProvider) { 58 | // <.> 59 | var jwtAuthInterceptor = new JwtAuthenticationInterceptor( 60 | AUTHORIZATION_HEADER_NAME, jwtAuthenticationProvider); 61 | // <.> 62 | var securityContextChannelInterceptor = new SecurityContextChannelInterceptor( 63 | AUTHORIZATION_HEADER_NAME); 64 | // <.> 65 | var authorizationChannelInterceptor = new AuthorizationChannelInterceptor( 66 | AuthenticatedAuthorizationManager.authenticated()); 67 | return MessageChannels 68 | .direct() 69 | .interceptor( 70 | jwtAuthInterceptor, 71 | securityContextChannelInterceptor, 72 | authorizationChannelInterceptor 73 | ); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /processor/src/main/java/bootiful/processor/JwtAuthenticationInterceptor.java: -------------------------------------------------------------------------------- 1 | package bootiful.processor; 2 | 3 | import org.springframework.messaging.Message; 4 | import org.springframework.messaging.MessageChannel; 5 | import org.springframework.messaging.support.ChannelInterceptor; 6 | import org.springframework.messaging.support.MessageBuilder; 7 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 | import org.springframework.security.core.authority.AuthorityUtils; 9 | import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthenticationToken; 10 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; 11 | import org.springframework.util.Assert; 12 | 13 | class JwtAuthenticationInterceptor implements ChannelInterceptor { 14 | 15 | // <.> 16 | private final JwtAuthenticationProvider authenticationProvider; 17 | 18 | // <.> 19 | private final String headerName; 20 | 21 | JwtAuthenticationInterceptor(String headerName, JwtAuthenticationProvider ap) { 22 | this.headerName = headerName; 23 | this.authenticationProvider = ap; 24 | } 25 | 26 | @Override 27 | public Message preSend(Message message, MessageChannel channel) { 28 | // <.> 29 | var token = (String) message.getHeaders().get(headerName); 30 | Assert.hasText(token, "the token must be non-empty!"); 31 | 32 | // <.> 33 | var authentication = this.authenticationProvider 34 | .authenticate(new BearerTokenAuthenticationToken(token)); 35 | 36 | // <.> 37 | if (authentication != null && authentication.isAuthenticated()) { 38 | var upt = 39 | UsernamePasswordAuthenticationToken.authenticated(authentication.getName(), 40 | null, AuthorityUtils.NO_AUTHORITIES); 41 | return MessageBuilder 42 | .fromMessage(message) 43 | .setHeader(headerName, upt) 44 | .build(); 45 | } 46 | 47 | // <.> 48 | return MessageBuilder 49 | .fromMessage(message) 50 | .setHeader(headerName, null) 51 | .build(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /processor/src/main/java/bootiful/processor/ProcessorApplication.java: -------------------------------------------------------------------------------- 1 | package bootiful.processor; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | import java.util.concurrent.Executors; 7 | 8 | @SpringBootApplication 9 | public class ProcessorApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ProcessorApplication.class, args); 13 | } 14 | 15 | 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /processor/src/main/java/bootiful/processor/SecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package bootiful.processor; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.oauth2.jwt.JwtDecoder; 7 | import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; 8 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; 9 | import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; 10 | 11 | @Configuration 12 | class SecurityConfiguration { 13 | 14 | @Bean 15 | JwtAuthenticationProvider jwtAuthenticationProvider(JwtDecoder decoder) { 16 | return new JwtAuthenticationProvider(decoder); 17 | } 18 | 19 | @Bean 20 | JwtDecoder jwtDecoder(@Value("${spring.security.oauth2.authorizationserver.issuer}") String issuerUri) { 21 | return NimbusJwtDecoder.withIssuerLocation(issuerUri).build(); 22 | } 23 | 24 | @Bean 25 | JwtAuthenticationConverter jwtAuthenticationConverter() { 26 | return new JwtAuthenticationConverter(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /processor/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.security.oauth2.authorizationserver.issuer=http://localhost:8080 2 | spring.rabbitmq.username=user 3 | spring.rabbitmq.password=password 4 | -------------------------------------------------------------------------------- /processor/src/test/java/bootiful/processor/ProcessorApplicationTests.java: -------------------------------------------------------------------------------- 1 | package bootiful.processor; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ProcessorApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /static/app.css: -------------------------------------------------------------------------------- 1 | .id { 2 | font-weight: bold; 3 | } 4 | 5 | .fade { 6 | background-color: green; 7 | animation: fade 2s ease forwards; 8 | } 9 | 10 | @keyframes fade { 11 | to { 12 | background-color: #fff; 13 | } 14 | } 15 | 16 | .id { 17 | display: inline-block; width: 2em; text-align: center; 18 | } 19 | 20 | html { 21 | line-height: 1.5em; 22 | } -------------------------------------------------------------------------------- /static/app.js: -------------------------------------------------------------------------------- 1 | // <.> 2 | const root = '/api' 3 | 4 | async function customers() { 5 | const response = await fetch(root + '/customers') 6 | return await response.json() 7 | } 8 | 9 | async function me() { 10 | const response = await fetch(root + '/me') 11 | return await response.json() 12 | } 13 | 14 | 15 | // <.> 16 | async function email(customerId) { 17 | const response = await fetch(root + '/email?customerId=' + customerId, {method: 'POST'}) 18 | const data = await response.json() 19 | const cssQuerySelector = '#' + divIdFor({id: customerId}) + ' .id' 20 | document.querySelector(cssQuerySelector).classList.add('fade') 21 | return data 22 | } 23 | 24 | // <.> 25 | function divIdFor(customer) { 26 | return 'customerDiv' + customer.id 27 | } 28 | 29 | // <.> 30 | window.addEventListener('load', async (event) => { 31 | document.getElementById('me').innerHTML = (await me()).name; 32 | const customersDiv = document.getElementById('customers') 33 | const customersResults = await customers(); 34 | customersResults.forEach(customer => { 35 | const div = document.createElement('div') 36 | div.innerHTML = ` 37 | 38 | ${customer.id} 39 | ${customer.name} 40 | ` 41 | div.id = divIdFor(customer) 42 | customersDiv.appendChild(div) 43 | document 44 | .querySelector('#' + divIdFor(customer)) 45 | .addEventListener('click', () => email(customer.id)) 46 | }); 47 | }) -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Spring CRM 8 | 9 | 10 | 11 | 12 |
welcome,
13 |
14 | 15 | -------------------------------------------------------------------------------- /static/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | python3 -m http.server 8020 3 | 4 | --------------------------------------------------------------------------------