diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 62da4936..00000000 --- a/LICENSE.md +++ /dev/null @@ -1 +0,0 @@ -Apache 2.0 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..779dbb71 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright 2020-2021 the original author or authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this artifact or file except in compliance with the License. +You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md index 51d73cd1..347b80f6 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,80 @@ # Spring Lemon -When developing **real-world** Spring REST services for JavaScript web applications, mobile clients or any consumer, you face many challenges, such as +> Note: for latest patterns and best practices, follow the successor https://github.com/naturalprogrammer/np-spring-mvc-demo -1. How to make the API truly _stateless_, using token authentication, session sliding etc. +When developing **real-world** Spring REST APIs and microservices, you face many challenges like + +1. How to follow a stateless and efficient security model – using JWT authentication, session sliding etc. 1. How to configure Spring Security to suit API development, e.g. returning _200_ or _401_ responses on login, configuring _CORS_, _JSON vulnerability protection_, etc. -1. How to handle _validations_ and _exceptions_ in a cross functional manner and send precise errors to the client. +1. How to elegantly do _validations_ and _exceptions_ and send precise errors to the client. +1. How to easily mix manual and bean validations in a single validation cycle. 1. How exactly to support multiple _social sign up/in_, using _OpenID Connect_ or _OAuth2_ providers such as _Google_ and _Facebook_. 1. How to code a robust user module (with features like _sign up_, _sign in_, _verify email_, _social sign up/in_, _update profile_, _forgot password_, _change password_, _change email_, _token authentication_ etc.) and share it across all your applications. -1. _What would be good ways to test your API_. +1. How to correctly and effeciently _secure microservices_, using long-lived and short-lived JWTs. +1. What would be good ways to _test your API_. 1. How to do _Captcha validation_. 1. How to properly organize _application properties_. 1. How to use _PATCH_ and _JsonPatch_ to handle partial updates correctly. +1. How to do all the above reactively, using WebFlux and WebFlux security. -Coding all the above effectively needs in-depth knowledge of Spring. It also takes a lot of development time and effort, and needs to be properly maintained as new versions of Spring modules come out. +Coding all this rightly needs in-depth knowledge of Spring. It also takes a lot of development time and effort, and needs to be properly maintained as new versions of Spring comes out. -To relieve you of this non-trivial job, we thought to bring out **Spring Lemon**, a tiny open source library holding all these common configuration and components, and a production grade user module with all the abovementioned features. +**Spring Lemon** relieves you of all this burden. It's a set of configurable and extensible libraries, providing all above features. Use these to develop quality reactive or non-reactive monolith or microservices applications quickly and easily. -Even if you don't plan to use Spring Lemon, it's a good example application to learn from, because it showcases the essential best practices for developing elegant web services using Spring. +Even if you don't plan to use Spring Lemon, it's a good example to learn from, because it showcases the essential best practices for developing elegant web services and microservices using Spring. Most Spring Boot applications can use Spring Lemon straight away, with some simple configurations. But, if you don't find it suitable for your application, feel free to fork it, or just roll out your own library by learning its patterns and practices. Better yet, be a contributor! -Watch [this video tutorial](https://www.naturalprogrammer.com/p/spring-lemon-restful-web-services-development) or read [this quick starter guide](https://github.com/naturalprogrammer/spring-lemon/wiki/Getting-Started-With-Spring-Lemon) for getting started. +Read [this quick starter guide](https://github.com/naturalprogrammer/spring-lemon/wiki/Getting-Started-With-Spring-Lemon) or watch [this video tutorial](https://www.naturalprogrammer.com/p/spring-lemon-restful-web-services-development) for getting started. + +## Libraries hierarchy + +* [spring-lemon-exceptions](https://github.com/naturalprogrammer/spring-lemon/wiki/Spring-Lemon-Exceptions-Guide): Useful for elegant exception handling and validation in any Spring project + * [spring-lemon-commons](https://github.com/naturalprogrammer/spring-lemon/wiki/Spring-Lemon-Commons-Guide): Common for all things below + * [spring-lemon-commons-web](https://github.com/naturalprogrammer/spring-lemon/wiki/Spring-Lemon-Commons-Web-Guide): For developing Spring Web (non-reactive) microservices + * [spring-lemon-commons-jpa](https://github.com/naturalprogrammer/spring-lemon/wiki/Spring-Lemon-Commons-JPA-Guide): For developing Spring Web (non-reactive) JPA microservices + * [spring-lemon-jpa](https://github.com/naturalprogrammer/spring-lemon/wiki/Spring-Lemon-JPA-Guide): For developing Spring Web (non-reactive) JPA monolith or auth-microservice + * [spring-lemon-commons-reactive](https://github.com/naturalprogrammer/spring-lemon/wiki/Spring-Lemon-Commons-Reactive-Guide): For developing Spring WebFlux (reactive) microservices + * [spring-lemon-commons-mongo](https://github.com/naturalprogrammer/spring-lemon/wiki/Spring-Lemon-Commons-MongoDB-Guide): For developing Spring WebFlux (reactive) MongoDB microservices + * [spring-lemon-reactive](https://github.com/naturalprogrammer/spring-lemon/wiki/Spring-Lemon-Reactive-Guide): For developing Spring WebFlux (reactive) MongoDB monolith or auth-microservice + +For example usages, see + +* [Demo non-reactive monolith](https://github.com/naturalprogrammer/spring-lemon/tree/master/lemon-demo-jpa) +* [Demo reactive monolith](https://github.com/naturalprogrammer/spring-lemon/tree/master/lemon-demo-reactive) +* [Demo non-reactive microservices](https://github.com/naturalprogrammer/np-microservices-sample-01) and its [configuration repository](https://github.com/naturalprogrammer/np-microservices-sample-01-config) +* [Demo reactive microservices](https://github.com/naturalprogrammer/np-microservices-sample-02) and its [configuration repository](https://github.com/naturalprogrammer/np-microservices-sample-02-config) ## Documentation and Resources -> _Part I of our [Mastering Spring RESTful Web Services Development](https://www.naturalprogrammer.com/p/spring-restful-web-services-tutorial-i) — the ultimate course to master REST API development using Spring whether you use Spring Lemon or not — is now available for FREE._ +> _Our [Spring Framework Recipes For Real World Application Development](https://www.naturalprogrammer.com/p/spring-framework-book-of-best-practices) — a live book discussing key real-world topics on developing Spring applications and APIs — is now available for FREE. [Click here](https://www.naturalprogrammer.com/p/spring-framework-book-of-best-practices) to get it._ 1. Feature demo: https://youtu.be/6mNg-Feq8CY 1. _Getting started guide_ 1. [Book](https://github.com/naturalprogrammer/spring-lemon/wiki/Getting-Started-With-Spring-Lemon) 1. [Video Tutorial](https://www.naturalprogrammer.com/p/spring-lemon-restful-web-services-development) -1. _[Example application](https://github.com/naturalprogrammer/lemon-demo)_ — A sample application using Spring Lemon. Quite similar to the one developed in the above [getting started guide](https://github.com/naturalprogrammer/spring-lemon/wiki/Getting-Started-With-Spring-Lemon), but additionally has automated tests. -1. _[API documentation](https://documenter.getpostman.com/view/305915/RVu2mqEH)_ of the above application. -1. Spring Lemon [JavaDoc](https://naturalprogrammer.github.io/javadoc/spring-lemon/1.0.0.m2/) -1. _[Example Angular 1.x front-end application](https://github.com/naturalprogrammer/lemon-demo-angular1)_ — A sample AngularJS 1.x front-end. It'll work both for the application developed in the above [getting started guide](https://documenter.getpostman.com/view/305915/lemondemo/RVnPL46k) as well as the [Lemon Demo application](https://github.com/naturalprogrammer/lemon-demo). -1. _Mastering Spring RESTful Web Services Development_ — Ultimate course to master REST API development using Spring. Also covers Spring Lemon in depth. A must guide if you want to become an expert Spring developer, whether you use Spring Lemon or not. [Click here](https://www.naturalprogrammer.com/p/spring-restful-web-services-tutorial-i) to get part I now for FREE! +1. _[Official Documentation](https://github.com/naturalprogrammer/spring-lemon/wiki)_ +1. _Example applications_ + * [Demo non-reactive monolith](https://github.com/naturalprogrammer/spring-lemon/tree/master/lemon-demo-jpa) + * [Demo reactive monolith](https://github.com/naturalprogrammer/spring-lemon/tree/master/lemon-demo-reactive) + * [Demo non-reactive microservices](https://github.com/naturalprogrammer/np-microservices-sample-01) and its [configuration repository](https://github.com/naturalprogrammer/np-microservices-sample-01-config) + * [Demo reactive microservices](https://github.com/naturalprogrammer/np-microservices-sample-02) and its [configuration repository](https://github.com/naturalprogrammer/np-microservices-sample-02-config) +1. _[API documentation](https://documenter.getpostman.com/view/305915/RVu2mqEH)_ of the above applications. +1. _[Example AngularJS front-end application](https://github.com/naturalprogrammer/spring-lemon/tree/master/lemon-demo-angularjs)_ — A sample AngularJS 1.x front-end. It'll work for the application developed in the above [getting started guide](https://github.com/naturalprogrammer/spring-lemon/wiki/Getting-Started-With-Spring-Lemon) as well all the above example applications. See the [Getting Started Guide](https://github.com/naturalprogrammer/spring-lemon/wiki/Getting-Started-With-Spring-Lemon) on how to use it. +1. _[Spring Framework Recipes For Real World Application Development](https://www.naturalprogrammer.com/p/spring-framework-book-of-best-practices)_ — a live book discussing key real-world topics on developing Spring applications, APIs and microservoces. Includes many Spring Lemon topics. [Click here](https://www.naturalprogrammer.com/p/spring-framework-book-of-best-practices) to get it now for FREE! +1. [Using Spring Lemon Effectively](https://github.com/naturalprogrammer/spring-lemon/wiki/Using-Spring-Lemon-Effectively) +1. [DZone Articles](https://dzone.com/users/1211183/skpatel20.html) +1. Video tutorials coming soon: + 1. Spring Framework 5 REST API Development — A Complete Blueprint For Real-World Developers + 1. Spring WebFlux Reactive REST API Development — A Complete Blueprint For Real-World Developers + 1. Microservices Using Spring Cloud — A Rapid Course For Real World Developers + 1. Join [here](https://www.naturalprogrammer.com/p/spring-framework-book-of-best-practices) to get notified and avail heavy discounts when the above courses get released ## Help and Support 1. Community help is available at [stackoverflow.com](http://stackoverflow.com/questions/tagged/spring-lemon), under the `spring-lemon` tag. Do not miss to tag the questions with `spring-lemon`! 1. [Submit an issue](https://github.com/naturalprogrammer/spring-lemon/issues) for any bug or enhancement. Please check first that the issue isn't already reported earlier. -1. Mentoring, training and professional help is provided by [naturalprogrammer.com](http://www.naturalprogrammer.com/consulting/). - -## Donate -Like Spring Lemon? We have been putting continuous efforts to develop and maintain it. If it's being useful to you, why not donate a little amount — it’ll help us give more time to the project! - -[Click here](http://www.naturalprogrammer.com/support-spring-lemon/) to donate. +1. Mentoring, training and professional help is provided by [naturalprogrammer.com](https://www.naturalprogrammer.com). -## Releases +## Releases and Breaking Changes 1. See [here](https://github.com/naturalprogrammer/spring-lemon/releases). diff --git a/lemon-demo-angularjs/app/scripts/services/formservice.js b/lemon-demo-angularjs/app/scripts/services/formservice.js index 83dccd02..c4127020 100644 --- a/lemon-demo-angularjs/app/scripts/services/formservice.js +++ b/lemon-demo-angularjs/app/scripts/services/formservice.js @@ -10,7 +10,16 @@ angular.module('appBoot') .factory('formService', function($http, $location, alerts) { - /** + var serialize = function(obj) { + var str = []; + for (var p in obj) + if (obj.hasOwnProperty(p)) { + str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p])); + } + return str.join("&"); + } + + /** * Creates the array if not already present * * @param obj @@ -30,6 +39,8 @@ angular.module('appBoot') switch (errorData.exception) { case 'ConstraintViolationException': // server side JSR-303 errors + case 'ExplicitConstraintViolationException': // server side JSR-303 errors + case 'WebExchangeBindException': // server side JSR-303 errors case 'MultiErrorException': // server side form errors manually thrown angular.forEach(errorData.errors, function(error) { // for each fields @@ -72,7 +83,7 @@ angular.module('appBoot') if (method === 'post' || method === 'put' || method === 'patch') { if (options.asParam) // data should be sent as params - return $http[method](serverUrl + url, null, {params: options.data}); + return $http[method](serverUrl + url, serialize(options.data), {headers: {'Content-Type': 'application/x-www-form-urlencoded'}}); return $http[method](serverUrl + url, options.data); } diff --git a/lemon-demo-angularjs/app/scripts/services/user-service.js b/lemon-demo-angularjs/app/scripts/services/user-service.js index 65112e6a..7f06c10e 100644 --- a/lemon-demo-angularjs/app/scripts/services/user-service.js +++ b/lemon-demo-angularjs/app/scripts/services/user-service.js @@ -24,8 +24,8 @@ angular.module('angularSampleApp') this.goodUser = !(this.unverified || this.blocked); this.goodAdmin = this.admin && this.goodUser; - this.editable = authService.isAuthenticated() && (this.id === authService.user.id || authService.isGoodAdmin()); - this.rolesEditable = authService.isGoodAdmin() && this.id !== authService.user.id; + this.editable = authService.isAuthenticated() && (this.id == authService.user.id || authService.isGoodAdmin()); + this.rolesEditable = authService.isGoodAdmin() && this.id != authService.user.id; }; User.prototype.hasRole = function(role) { diff --git a/lemon-demo-angularjs/package.json b/lemon-demo-angularjs/package.json index 973999cb..6faefb36 100644 --- a/lemon-demo-angularjs/package.json +++ b/lemon-demo-angularjs/package.json @@ -2,6 +2,7 @@ "name": "angularsample", "version": "0.0.0", "dependencies": { + "natives": "^1.1.6", "wiredep": "^2.2.2" }, "repository": {}, diff --git a/lemon-demo-jpa/pom.xml b/lemon-demo-jpa/pom.xml index f49a7e52..66068e70 100644 --- a/lemon-demo-jpa/pom.xml +++ b/lemon-demo-jpa/pom.xml @@ -14,7 +14,7 @@ com.naturalprogrammer spring-lemon - 1.0.0.M4 + 1.0.2 @@ -30,10 +30,10 @@ runtime + - mysql - mysql-connector-java - runtime + org.postgresql + postgresql @@ -50,6 +50,27 @@ org.springframework.boot spring-boot-maven-plugin + + + org.jacoco + jacoco-maven-plugin + ${jacacoVersion} + + + + prepare-agent + + + + report + test + + report + + + + + diff --git a/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/MySecurityConfig.java b/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/MySecurityConfig.java index effea0aa..e7627186 100644 --- a/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/MySecurityConfig.java +++ b/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/MySecurityConfig.java @@ -1,14 +1,13 @@ package com.naturalprogrammer.spring.lemondemo; +import com.naturalprogrammer.spring.lemon.security.LemonJpaSecurityConfig; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.stereotype.Component; -import com.naturalprogrammer.spring.lemon.security.LemonSecurityConfig; - @Component -public class MySecurityConfig extends LemonSecurityConfig { +public class MySecurityConfig extends LemonJpaSecurityConfig { private static final Log log = LogFactory.getLog(MySecurityConfig.class); diff --git a/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/controllers/MyController.java b/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/controllers/MyController.java index c5d5e8e9..4f312eaf 100644 --- a/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/controllers/MyController.java +++ b/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/controllers/MyController.java @@ -1,13 +1,14 @@ package com.naturalprogrammer.spring.lemondemo.controllers; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import com.naturalprogrammer.spring.lemon.LemonController; import com.naturalprogrammer.spring.lemondemo.entities.User; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/core") +@RequestMapping(MyController.BASE_URI) public class MyController extends LemonController { + + public static final String BASE_URI = "/api/core"; } \ No newline at end of file diff --git a/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/entities/User.java b/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/entities/User.java index dcbdc23d..66fa23cf 100644 --- a/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/entities/User.java +++ b/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/entities/User.java @@ -1,43 +1,37 @@ package com.naturalprogrammer.spring.lemondemo.entities; -import java.io.Serializable; +import com.fasterxml.jackson.annotation.JsonView; +import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemon.domain.AbstractUser; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; - -import com.fasterxml.jackson.annotation.JsonView; -import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; -import com.naturalprogrammer.spring.lemon.domain.AbstractUser; +import java.io.Serializable; @Entity @Table(name="usr") -public class User extends AbstractUser { +@Getter @Setter @NoArgsConstructor +public class User extends AbstractUser { private static final long serialVersionUID = 2716710947175132319L; public static final int NAME_MIN = 1; public static final int NAME_MAX = 50; + @Getter @Setter @ToString public static class Tag implements Serializable { private static final long serialVersionUID = -2129078111926834670L; - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } } - public User() {} - public User(String email, String password, String name) { this.email = email; this.password = password; @@ -47,7 +41,7 @@ public User(String email, String password, String name) { @JsonView(UserUtils.SignupInput.class) @NotBlank(message = "{blank.name}", groups = {UserUtils.SignUpValidation.class, UserUtils.UpdateValidation.class}) @Size(min=NAME_MIN, max=NAME_MAX, groups = {UserUtils.SignUpValidation.class, UserUtils.UpdateValidation.class}) - @Column(nullable = false, length = NAME_MAX) + @Column(nullable = false, length = NAME_MAX) // Note: don't use JPA annotations on getter: https://github.com/naturalprogrammer/spring-lemon/issues/9 private String name; @Override @@ -57,12 +51,4 @@ public Tag toTag() { tag.setName(name); return tag; } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } } \ No newline at end of file diff --git a/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/services/MyService.java b/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/services/MyService.java index be2754b0..206cd1c7 100644 --- a/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/services/MyService.java +++ b/lemon-demo-jpa/src/main/java/com/naturalprogrammer/spring/lemondemo/services/MyService.java @@ -1,14 +1,13 @@ package com.naturalprogrammer.spring.lemondemo.services; -import java.util.Map; - -import org.springframework.security.oauth2.core.oidc.StandardClaimNames; -import org.springframework.stereotype.Service; - import com.naturalprogrammer.spring.lemon.LemonService; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; +import com.naturalprogrammer.spring.lemon.commonsjpa.LecjUtils; import com.naturalprogrammer.spring.lemondemo.entities.User; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.stereotype.Service; + +import java.util.Map; @Service public class MyService extends LemonService { @@ -21,14 +20,14 @@ public User newUser() { } @Override - protected void updateUserFields(User user, User updatedUser, UserDto currentUser) { + protected void updateUserFields(User user, User updatedUser, UserDto currentUser) { super.updateUserFields(user, updatedUser, currentUser); user.setName(updatedUser.getName()); - LemonUtils.afterCommit(() -> { - if (currentUser.getId().equals(user.getId())) + LecjUtils.afterCommit(() -> { + if (currentUser.getId().equals(user.getId().toString())) currentUser.setTag(user.toTag()); }); } @@ -39,8 +38,7 @@ protected User createAdminUser() { User user = super.createAdminUser(); user.setName(ADMIN_NAME); return user; - } - + } @Override public void fillAdditionalFields(String registrationId, User user, Map attributes) { @@ -63,4 +61,10 @@ public void fillAdditionalFields(String registrationId, User user, Map form(Long jwtExpirationMillis) { -// -// NonceForm nonceForm = new NonceForm<>(); -// nonceForm.setNonce(NONCE); -// nonceForm.setUserId(UNVERIFIED_USER_ID); -// nonceForm.setExpirationMillis(jwtExpirationMillis); -// -// return nonceForm; -// } -// -// @Before -// public void setUp() { -// -// User user = userRepository.findById(UNVERIFIED_USER_ID).get(); -// user.setNonce(NONCE); -// userRepository.save(user); -// } -// -// @Test -// public void testLoginWithNonce() throws Exception { -// -// String token = loginWithNonce(null); -// -// User user = userRepository.findById(UNVERIFIED_USER_ID).get(); -// Assert.assertNull(user.getNonce()); -// -// ensureTokenWorks(token); -// -// // Retry should fail -// mvc.perform(post("/api/core/login-with-nonce") -// .contentType(MediaType.APPLICATION_JSON) -// .content(LemonUtils.toJson(form(null)))) -// .andExpect(status().is(401)); -// } -// -// @Test -// public void testLoginWithNonceExpiryOk() throws Exception { -// -// String token = loginWithNonce(1000L); -// ensureTokenWorks(token); -// } -// -// @Test -// public void testLoginWithNonceExpiryShouldFail() throws Exception { -// -// String token = loginWithNonce(100L); -// Thread.sleep(101L); -// -// mvc.perform(get("/api/core/context") -// .header(LemonSecurityConfig.TOKEN_REQUEST_HEADER_NAME, token)) -// .andExpect(status().is(401)); -// } -// -// @Test -// public void testLoginWithNonceInvalidData() throws Exception { -// -// mvc.perform(post("/api/core/login-with-nonce") -// .contentType(MediaType.APPLICATION_JSON) -// .content(LemonUtils.toJson(new NonceForm()))) -// .andExpect(status().is(422)) -// .andExpect(jsonPath("$.errors[*].field").value(hasSize(2))) -// .andExpect(jsonPath("$.errors[*].field").value(hasItems( -// "nonce.userId", -// "nonce.nonce"))); -// } -// -// @Test -// public void testLoginWithNonceUnknownUser() throws Exception { -// -// NonceForm form = form(null); -// form.setUserId(99L); -// -// mvc.perform(post("/api/core/login-with-nonce") -// .contentType(MediaType.APPLICATION_JSON) -// .content(LemonUtils.toJson(form))) -// .andExpect(status().is(404)); -// } -// -// @Test -// public void testLoginWithWrongNonce() throws Exception { -// -// NonceForm form = form(null); -// form.setNonce("wrong-nonce"); -// -// mvc.perform(post("/api/core/login-with-nonce") -// .contentType(MediaType.APPLICATION_JSON) -// .content(LemonUtils.toJson(form))) -// .andExpect(status().is(401)); -// } -// -// private String loginWithNonce(Long jwtExpirationMillis) throws JsonProcessingException, Exception { -// -// MvcResult result = mvc.perform(post("/api/core/login-with-nonce") -// .contentType(MediaType.APPLICATION_JSON) -// .content(LemonUtils.toJson(form(jwtExpirationMillis)))) -// .andExpect(status().is(200)) -// .andExpect(header().string(LemonSecurityConfig.TOKEN_RESPONSE_HEADER_NAME, containsString("."))) -// .andExpect(jsonPath("$.id").value(UNVERIFIED_USER_ID)) -// .andReturn(); -// -// return result.getResponse().getHeader(LemonSecurityConfig.TOKEN_RESPONSE_HEADER_NAME); -// } -//} diff --git a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/RequestEmailChangeMvcTests.java b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/RequestEmailChangeMvcTests.java index a403654f..38f7fa75 100644 --- a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/RequestEmailChangeMvcTests.java +++ b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/RequestEmailChangeMvcTests.java @@ -1,7 +1,16 @@ package com.naturalprogrammer.spring.lemondemo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.entities.User; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -9,16 +18,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.http.MediaType; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; -import com.naturalprogrammer.spring.lemondemo.entities.User; - -public class RequestEmailChangeMvcTests extends AbstractMvcTests { +class RequestEmailChangeMvcTests extends AbstractMvcTests { private static final String NEW_EMAIL = "new.email@example.com"; @@ -32,47 +32,47 @@ private User form() { } @Test - public void testRequestEmailChange() throws Exception { + void testRequestEmailChange() throws Exception { mvc.perform(post("/api/core/users/{id}/email-change-request", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) - .content(LemonUtils.toJson(form()))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) + .content(LecUtils.toJson(form()))) .andExpect(status().is(204)); verify(mailSender).send(any()); User updatedUser = userRepository.findById(UNVERIFIED_USER_ID).get(); - Assert.assertEquals(NEW_EMAIL, updatedUser.getNewEmail()); - Assert.assertEquals(UNVERIFIED_USER_EMAIL, updatedUser.getEmail()); + assertEquals(NEW_EMAIL, updatedUser.getNewEmail()); + assertEquals(UNVERIFIED_USER_EMAIL, updatedUser.getEmail()); } /** * A good admin should be able to request changing email of another user. */ @Test - public void testGoodAdminRequestEmailChange() throws Exception { + void testGoodAdminRequestEmailChange() throws Exception { mvc.perform(post("/api/core/users/{id}/email-change-request", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(ADMIN_ID)) - .content(LemonUtils.toJson(form()))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(ADMIN_ID)) + .content(LecUtils.toJson(form()))) .andExpect(status().is(204)); User updatedUser = userRepository.findById(UNVERIFIED_USER_ID).get(); - Assert.assertEquals(NEW_EMAIL, updatedUser.getNewEmail()); + assertEquals(NEW_EMAIL, updatedUser.getNewEmail()); } /** * A request changing email of unknown user. */ @Test - public void testRequestEmailChangeUnknownUser() throws Exception { + void testRequestEmailChangeUnknownUser() throws Exception { mvc.perform(post("/api/core/users/99/email-change-request") .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(ADMIN_ID)) - .content(LemonUtils.toJson(form()))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(ADMIN_ID)) + .content(LecUtils.toJson(form()))) .andExpect(status().is(404)); verify(mailSender, never()).send(any()); @@ -83,35 +83,31 @@ public void testRequestEmailChangeUnknownUser() throws Exception { * the email id of another user */ @Test - public void testNonAdminRequestEmailChangeAnotherUser() throws Exception { + void testNonAdminRequestEmailChangeAnotherUser() throws Exception { mvc.perform(post("/api/core/users/{id}/email-change-request", ADMIN_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(USER_ID)) - .content(LemonUtils.toJson(form()))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(USER_ID)) + .content(LecUtils.toJson(form()))) .andExpect(status().is(403)); verify(mailSender, never()).send(any()); User updatedUser = userRepository.findById(UNVERIFIED_USER_ID).get(); - Assert.assertNull(updatedUser.getNewEmail()); + assertNull(updatedUser.getNewEmail()); } /** * A bad admin trying to change the email id * of another user */ - /** - * A non-admin should not be able to request changing - * the email id of another user - */ @Test - public void testBadAdminRequestEmailChangeAnotherUser() throws Exception { + void testBadAdminRequestEmailChangeAnotherUser() throws Exception { mvc.perform(post("/api/core/users/{id}/email-change-request", ADMIN_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_ADMIN_ID)) - .content(LemonUtils.toJson(form()))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_ADMIN_ID)) + .content(LecUtils.toJson(form()))) .andExpect(status().is(403)); verify(mailSender, never()).send(any()); @@ -119,17 +115,15 @@ public void testBadAdminRequestEmailChangeAnotherUser() throws Exception { /** * Trying with invalid data. - * @throws Exception - * @throws JsonProcessingException */ @Test - public void tryingWithInvalidData() throws JsonProcessingException, Exception { + void tryingWithInvalidData() throws JsonProcessingException, Exception { // try with null newEmail and password mvc.perform(post("/api/core/users/{id}/email-change-request", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) - .content(LemonUtils.toJson(new User()))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) + .content(LecUtils.toJson(new User()))) .andExpect(status().is(422)) .andExpect(jsonPath("$.errors[*].field").value(hasSize(2))) .andExpect(jsonPath("$.errors[*].field").value(hasItems( @@ -140,11 +134,11 @@ public void tryingWithInvalidData() throws JsonProcessingException, Exception { updatedUser.setPassword(""); updatedUser.setNewEmail(""); - // try with null newEmail and password + // try with blank newEmail and password mvc.perform(post("/api/core/users/{id}/email-change-request", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) - .content(LemonUtils.toJson(updatedUser))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) + .content(LecUtils.toJson(updatedUser))) .andExpect(status().is(422)) .andExpect(jsonPath("$.errors[*].field").value(hasSize(4))) .andExpect(jsonPath("$.errors[*].field").value(hasItems( @@ -156,8 +150,8 @@ public void tryingWithInvalidData() throws JsonProcessingException, Exception { updatedUser.setNewEmail("an-invalid-email"); mvc.perform(post("/api/core/users/{id}/email-change-request", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) - .content(LemonUtils.toJson(updatedUser))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) + .content(LecUtils.toJson(updatedUser))) .andExpect(status().is(422)) .andExpect(jsonPath("$.errors[*].field").value(hasSize(1))) .andExpect(jsonPath("$.errors[*].field").value(hasItems("updatedUser.newEmail"))); @@ -167,8 +161,8 @@ public void tryingWithInvalidData() throws JsonProcessingException, Exception { updatedUser.setPassword("wrong-password"); mvc.perform(post("/api/core/users/{id}/email-change-request", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) - .content(LemonUtils.toJson(updatedUser))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) + .content(LecUtils.toJson(updatedUser))) .andExpect(status().is(422)) .andExpect(jsonPath("$.errors[*].field").value(hasSize(1))) .andExpect(jsonPath("$.errors[*].field").value(hasItems("updatedUser.password"))); @@ -178,8 +172,8 @@ public void tryingWithInvalidData() throws JsonProcessingException, Exception { updatedUser.setPassword(null); mvc.perform(post("/api/core/users/{id}/email-change-request", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) - .content(LemonUtils.toJson(updatedUser))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) + .content(LecUtils.toJson(updatedUser))) .andExpect(status().is(422)) .andExpect(jsonPath("$.errors[*].field").value(hasSize(1))) .andExpect(jsonPath("$.errors[*].field").value(hasItems("updatedUser.password"))); @@ -189,8 +183,8 @@ public void tryingWithInvalidData() throws JsonProcessingException, Exception { updatedUser.setNewEmail(ADMIN_EMAIL);; mvc.perform(post("/api/core/users/{id}/email-change-request", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) - .content(LemonUtils.toJson(updatedUser))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) + .content(LecUtils.toJson(updatedUser))) .andExpect(status().is(422)) .andExpect(jsonPath("$.errors[*].field").value(hasSize(1))) .andExpect(jsonPath("$.errors[*].field").value(hasItems("updatedUser.newEmail"))); diff --git a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/ResendVerificationMailMvcTests.java b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/ResendVerificationMailMvcTests.java index e8b824fd..a20a32f5 100644 --- a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/ResendVerificationMailMvcTests.java +++ b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/ResendVerificationMailMvcTests.java @@ -1,51 +1,50 @@ package com.naturalprogrammer.spring.lemondemo; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.junit.Test; - -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; - -public class ResendVerificationMailMvcTests extends AbstractMvcTests { +class ResendVerificationMailMvcTests extends AbstractMvcTests { @Test - public void testResendVerificationMail() throws Exception { + void testResendVerificationMail() throws Exception { mvc.perform(post("/api/core/users/{id}/resend-verification-mail", UNVERIFIED_USER_ID) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID))) .andExpect(status().is(204)); verify(mailSender).send(any()); } @Test - public void testAdminResendVerificationMailOtherUser() throws Exception { + void testAdminResendVerificationMailOtherUser() throws Exception { mvc.perform(post("/api/core/users/{id}/resend-verification-mail", UNVERIFIED_USER_ID) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(ADMIN_ID))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(ADMIN_ID))) .andExpect(status().is(204)); } @Test - public void testBadAdminResendVerificationMailOtherUser() throws Exception { + void testBadAdminResendVerificationMailOtherUser() throws Exception { mvc.perform(post("/api/core/users/{id}/resend-verification-mail", UNVERIFIED_USER_ID) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_ADMIN_ID))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_ADMIN_ID))) .andExpect(status().is(403)); mvc.perform(post("/api/core/users/{id}/resend-verification-mail", UNVERIFIED_USER_ID) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(BLOCKED_ADMIN_ID))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(BLOCKED_ADMIN_ID))) .andExpect(status().is(403)); verify(mailSender, never()).send(any()); } @Test - public void testResendVerificationMailUnauthenticated() throws Exception { + void testResendVerificationMailUnauthenticated() throws Exception { mvc.perform(post("/api/core/users/{id}/resend-verification-mail", UNVERIFIED_USER_ID)) .andExpect(status().is(403)); @@ -54,30 +53,30 @@ public void testResendVerificationMailUnauthenticated() throws Exception { } @Test - public void testResendVerificationMailAlreadyVerified() throws Exception { + void testResendVerificationMailAlreadyVerified() throws Exception { mvc.perform(post("/api/core/users/{id}/resend-verification-mail", USER_ID) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(USER_ID))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(USER_ID))) .andExpect(status().is(422)); verify(mailSender, never()).send(any()); } @Test - public void testResendVerificationMailOtherUser() throws Exception { + void testResendVerificationMailOtherUser() throws Exception { mvc.perform(post("/api/core/users/{id}/resend-verification-mail", UNVERIFIED_USER_ID) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(USER_ID))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(USER_ID))) .andExpect(status().is(403)); verify(mailSender, never()).send(any()); } @Test - public void testResendVerificationMailNonExistingUser() throws Exception { + void testResendVerificationMailNonExistingUser() throws Exception { mvc.perform(post("/api/core/users/99/resend-verification-mail") - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(ADMIN_ID))) + .header(HttpHeaders.AUTHORIZATION, tokens.get(ADMIN_ID))) .andExpect(status().is(404)); verify(mailSender, never()).send(any()); diff --git a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/ResetPasswordMvcTests.java b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/ResetPasswordMvcTests.java index 801add24..efee8510 100644 --- a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/ResetPasswordMvcTests.java +++ b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/ResetPasswordMvcTests.java @@ -1,39 +1,35 @@ package com.naturalprogrammer.spring.lemondemo; -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; - import com.fasterxml.jackson.core.JsonProcessingException; import com.naturalprogrammer.spring.lemon.commons.domain.ResetPasswordForm; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; +import com.naturalprogrammer.spring.lemon.commons.security.GreenTokenService; import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; + +import static org.hamcrest.Matchers.containsString; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -public class ResetPasswordMvcTests extends AbstractMvcTests { +class ResetPasswordMvcTests extends AbstractMvcTests { private String forgotPasswordCode; @Autowired - private JwtService jwtService; + private GreenTokenService greenTokenService; - @Before + @BeforeEach public void setUp() { - forgotPasswordCode = jwtService.createToken( - JwtService.FORGOT_PASSWORD_AUDIENCE, + forgotPasswordCode = greenTokenService.createToken( + GreenTokenService.FORGOT_PASSWORD_AUDIENCE, ADMIN_EMAIL, 60000L); } @Test - public void testResetPassword() throws Exception { + void testResetPassword() throws Exception { final String NEW_PASSWORD = "newPassword!"; @@ -57,7 +53,7 @@ public void testResetPassword() throws Exception { } @Test - public void testResetPasswordInvalidData() throws Exception { + void testResetPasswordInvalidData() throws Exception { // Wrong code mvc.perform(post("/api/core/reset-password") @@ -84,6 +80,6 @@ private String form(String code, String newPassword) throws JsonProcessingExcept form.setCode(code); form.setNewPassword(newPassword); - return LemonUtils.toJson(form); + return LecUtils.toJson(form); } } diff --git a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/SignupMvcTests.java b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/SignupMvcTests.java index f6ef38d8..7d124129 100644 --- a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/SignupMvcTests.java +++ b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/SignupMvcTests.java @@ -1,52 +1,64 @@ package com.naturalprogrammer.spring.lemondemo; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasItems; -import static org.hamcrest.Matchers.hasSize; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.entities.User; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; + +import javax.validation.ConstraintViolationException; + +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.Assert; -import org.junit.Test; -import org.springframework.http.MediaType; -import org.springframework.test.context.jdbc.Sql; - -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; -import com.naturalprogrammer.spring.lemondemo.entities.User; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @Sql({"/test-data/initialize.sql", "/test-data/finalize.sql"}) -public class SignupMvcTests extends AbstractMvcTests { +class SignupMvcTests extends AbstractMvcTests { @Test - public void testSignupWithInvalidData() throws Exception { + void testSignupWithInvalidData() throws Exception { User invalidUser = new User("abc", "user1", null); mvc.perform(post("/api/core/users") .contentType(MediaType.APPLICATION_JSON) - .content(LemonUtils.toJson(invalidUser))) + .content(LecUtils.toJson(invalidUser))) .andExpect(status().is(422)) - .andExpect(jsonPath("$.errors[*].field").value(hasSize(4))) - .andExpect(jsonPath("$.errors[*].field").value(hasItems( - "user.email", "user.password", "user.name"))); + .andExpect(jsonPath("id").isString()) + .andExpect(jsonPath("exceptionId").value(ConstraintViolationException.class.getSimpleName())) + .andExpect(jsonPath("error").value("Unprocessable Entity")) + .andExpect(jsonPath("message").value("Validation Error")) + .andExpect(jsonPath("status").value(HttpStatus.UNPROCESSABLE_ENTITY.value())) + .andExpect(jsonPath("errors[*].field").value(hasSize(4))) + .andExpect(jsonPath("errors[*].field").value(hasItems( + "user.email", "user.password", "user.name"))) + .andExpect(jsonPath("errors[*].code").value(hasItems( + "{com.naturalprogrammer.spring.invalid.email}", + "{blank.name}", + "{com.naturalprogrammer.spring.invalid.email.size}", + "{com.naturalprogrammer.spring.invalid.password.size}"))) + .andExpect(jsonPath("errors[*].message").value(hasItems( + "Not a well formed email address", + "Name required", + "Email must be between 4 and 250 characters", + "Password must be between 6 and 50 characters"))); verify(mailSender, never()).send(any()); } @Test - public void testSignup() throws Exception { + void testSignup() throws Exception { User user = new User("user.foo@example.com", "user123", "User Foo"); mvc.perform(post("/api/core/users") .contentType(MediaType.APPLICATION_JSON) - .content(LemonUtils.toJson(user))) + .content(LecUtils.toJson(user))) .andExpect(status().is(201)) .andExpect(header().string(LecUtils.TOKEN_RESPONSE_HEADER_NAME, containsString("."))) .andExpect(jsonPath("$.id").exists()) @@ -64,31 +76,17 @@ public void testSignup() throws Exception { verify(mailSender).send(any()); // Ensure that password got encrypted - Assert.assertNotEquals("user123", userRepository.findByEmail("user.foo@example.com").get().getPassword()); + assertNotEquals("user123", userRepository.findByEmail("user.foo@example.com").get().getPassword()); } -// @Test -// public void testSignupLoggedIn() throws Exception { -// -// String adminToken = login("admin@example.com", "admin!"); -// -// User user = new User("user1@example.com", "user123", "User 1"); -// -// mvc.perform(post("/api/core/users") -// .header(LemonSecurityConfig.TOKEN_REQUEST_HEADER_NAME, adminToken) -// .contentType(MediaType.APPLICATION_JSON) -// .content(LemonUtils.toJson(user))) -// .andExpect(status().is(403)); -// } -// @Test - public void testSignupDuplicateEmail() throws Exception { + void testSignupDuplicateEmail() throws Exception { User user = new User("user@example.com", "user123", "User"); mvc.perform(post("/api/core/users") .contentType(MediaType.APPLICATION_JSON) - .content(LemonUtils.toJson(user))) + .content(LecUtils.toJson(user))) .andExpect(status().is(422)); verify(mailSender, never()).send(any()); diff --git a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/UpdateUserMvcTests.java b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/UpdateUserMvcTests.java index 618e66b5..69919bdf 100644 --- a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/UpdateUserMvcTests.java +++ b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/UpdateUserMvcTests.java @@ -1,28 +1,26 @@ package com.naturalprogrammer.spring.lemondemo; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasSize; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import java.io.IOException; - -import org.junit.Assert; -import org.junit.Test; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemondemo.entities.User; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.test.context.jdbc.Sql; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; -import com.naturalprogrammer.spring.lemondemo.entities.User; +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @Sql({"/test-data/initialize.sql", "/test-data/finalize.sql"}) -public class UpdateUserMvcTests extends AbstractMvcTests { +class UpdateUserMvcTests extends AbstractMvcTests { private static final String UPDATED_NAME = "Edited name"; @@ -33,22 +31,22 @@ public class UpdateUserMvcTests extends AbstractMvcTests { @Value("classpath:/update-user/patch-update-user.json") public void setUserPatch(Resource patch) throws IOException { - this.userPatch = LemonUtils.toString(patch); + this.userPatch = LecUtils.toStr(patch); } @Value("classpath:/update-user/patch-admin-role.json") public void setUserPatchAdminRole(Resource patch) throws IOException { - this.userPatchAdminRole = LemonUtils.toString(patch);; + this.userPatchAdminRole = LecUtils.toStr(patch);; } @Value("classpath:/update-user/patch-null-name.json") public void setUserPatchNullName(Resource patch) throws IOException { - this.userPatchNullName = LemonUtils.toString(patch);; + this.userPatchNullName = LecUtils.toStr(patch);; } @Value("classpath:/update-user/patch-long-name.json") public void setUserPatchLongName(Resource patch) throws IOException { - this.userPatchLongName = LemonUtils.toString(patch);; + this.userPatchLongName = LecUtils.toStr(patch);; } /** @@ -56,14 +54,13 @@ public void setUserPatchLongName(Resource patch) throws IOException { * but changes in roles should be skipped. * The name of security principal object should also * change in the process. - * @throws Exception */ @Test - public void testUpdateSelf() throws Exception { + void testUpdateSelf() throws Exception { mvc.perform(patch("/api/core/users/{id}", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) .content(userPatch)) .andExpect(status().is(200)) .andExpect(header().string(LecUtils.TOKEN_RESPONSE_HEADER_NAME, containsString("."))) @@ -75,15 +72,15 @@ public void testUpdateSelf() throws Exception { User user = userRepository.findById(UNVERIFIED_USER_ID).get(); // Ensure that data changed properly - Assert.assertEquals(UNVERIFIED_USER_EMAIL, user.getEmail()); - Assert.assertEquals(1, user.getRoles().size()); - Assert.assertTrue(user.getRoles().contains(UserUtils.Role.UNVERIFIED)); - Assert.assertEquals(2L, user.getVersion().longValue()); + assertEquals(UNVERIFIED_USER_EMAIL, user.getEmail()); + assertEquals(1, user.getRoles().size()); + assertTrue(user.getRoles().contains(UserUtils.Role.UNVERIFIED)); + assertEquals(2L, user.getVersion().longValue()); // Version mismatch mvc.perform(patch("/api/core/users/{id}", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) .content(userPatch)) .andExpect(status().is(409)); } @@ -93,14 +90,13 @@ public void testUpdateSelf() throws Exception { * The name of security principal object should NOT change in the process, * and the verification code should get set/unset on addition/deletion of * the UNVERIFIED role. - * @throws Exception */ @Test - public void testGoodAdminCanUpdateOther() throws Exception { + void testGoodAdminCanUpdateOther() throws Exception { mvc.perform(patch("/api/core/users/{id}", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(ADMIN_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(ADMIN_ID)) .content(userPatch)) .andExpect(status().is(200)) .andExpect(header().string(LecUtils.TOKEN_RESPONSE_HEADER_NAME, containsString("."))) @@ -113,68 +109,65 @@ public void testGoodAdminCanUpdateOther() throws Exception { User user = userRepository.findById(UNVERIFIED_USER_ID).get(); // Ensure that data changed properly - Assert.assertEquals(UNVERIFIED_USER_EMAIL, user.getEmail()); - Assert.assertEquals(1, user.getRoles().size()); - Assert.assertTrue(user.getRoles().contains(UserUtils.Role.ADMIN)); + assertEquals(UNVERIFIED_USER_EMAIL, user.getEmail()); + assertEquals(1, user.getRoles().size()); + assertTrue(user.getRoles().contains(UserUtils.Role.ADMIN)); } /** * Providing an unknown id should return 404. */ @Test - public void testUpdateUnknownId() throws Exception { + void testUpdateUnknownId() throws Exception { mvc.perform(patch("/api/core/users/{id}", 99) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(ADMIN_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(ADMIN_ID)) .content(userPatch)) .andExpect(status().is(404)); } /** * A non-admin trying to update the name and roles of another user should throw exception - * @throws Exception */ @Test - public void testUpdateAnotherUser() throws Exception { + void testUpdateAnotherUser() throws Exception { mvc.perform(patch("/api/core/users/{id}", ADMIN_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) .content(userPatch)) .andExpect(status().is(403)); } /** * A bad ADMIN trying to update the name and roles of another user should throw exception - * @throws Exception */ @Test - public void testBadAdminUpdateAnotherUser() throws Exception { + void testBadAdminUpdateAnotherUser() throws Exception { mvc.perform(patch("/api/core/users/{id}", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_ADMIN_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_ADMIN_ID)) .content(userPatch)) .andExpect(status().is(403)); mvc.perform(patch("/api/core/users/{id}", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(BLOCKED_ADMIN_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(BLOCKED_ADMIN_ID)) .content(userPatch)) .andExpect(status().is(403)); } /** * A good ADMIN should not be able to change his own roles - * @throws Exception */ @Test - public void goodAdminCanNotUpdateSelfRoles() throws Exception { + void goodAdminCanNotUpdateSelfRoles() throws Exception { mvc.perform(patch("/api/core/users/{id}", ADMIN_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(ADMIN_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(ADMIN_ID)) .content(userPatchAdminRole)) .andExpect(status().is(200)) .andExpect(jsonPath("$.tag.name").value(UPDATED_NAME)) @@ -184,22 +177,21 @@ public void goodAdminCanNotUpdateSelfRoles() throws Exception { /** * Invalid name - * @throws Exception */ @Test - public void testUpdateUserInvalidNewName() throws Exception { + void testUpdateUserInvalidNewName() throws Exception { // Null name mvc.perform(patch("/api/core/users/{id}", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) .content(userPatchNullName)) .andExpect(status().is(422)); // Too long name mvc.perform(patch("/api/core/users/{id}", UNVERIFIED_USER_ID) .contentType(MediaType.APPLICATION_JSON) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID)) + .header(HttpHeaders.AUTHORIZATION, tokens.get(UNVERIFIED_USER_ID)) .content(userPatchLongName)) .andExpect(status().is(422)); } diff --git a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/VerificationMvcTests.java b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/VerificationMvcTests.java index f360e8f4..00224391 100644 --- a/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/VerificationMvcTests.java +++ b/lemon-demo-jpa/src/test/java/com/naturalprogrammer/spring/lemondemo/VerificationMvcTests.java @@ -1,38 +1,37 @@ package com.naturalprogrammer.spring.lemondemo; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.hasSize; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.Before; -import org.junit.Test; +import com.naturalprogrammer.spring.lemon.commons.security.GreenTokenService; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import com.naturalprogrammer.spring.lemondemo.entities.User; +import org.aspectj.lang.annotation.Before; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.naturalprogrammer.spring.lemondemo.entities.User; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -public class VerificationMvcTests extends AbstractMvcTests { +class VerificationMvcTests extends AbstractMvcTests { private String verificationCode; @Autowired - private JwtService jwtService; + private GreenTokenService greenTokenService; - @Before + @BeforeEach public void setUp() { - verificationCode = jwtService.createToken(JwtService.VERIFY_AUDIENCE, + verificationCode = greenTokenService.createToken(GreenTokenService.VERIFY_AUDIENCE, Long.toString(UNVERIFIED_USER_ID), 60000L, LecUtils.mapOf("email", UNVERIFIED_USER_EMAIL)); } @Test - public void testEmailVerification() throws Exception { + void testEmailVerification() throws Exception { mvc.perform(post("/api/core/users/{userId}/verification", UNVERIFIED_USER_ID) .param("code", verificationCode) @@ -52,17 +51,16 @@ public void testEmailVerification() throws Exception { } @Test - public void testEmailVerificationNonExistingUser() throws Exception { + void testEmailVerificationNonExistingUser() throws Exception { mvc.perform(post("/api/core/users/99/verification") .param("code", verificationCode) - .header("contentType", MediaType.APPLICATION_FORM_URLENCODED) - .header(LecUtils.TOKEN_REQUEST_HEADER_NAME, tokens.get(UNVERIFIED_USER_ID))) + .header("contentType", MediaType.APPLICATION_FORM_URLENCODED)) .andExpect(status().is(404)); } @Test - public void testEmailVerificationWrongToken() throws Exception { + void testEmailVerificationWrongToken() throws Exception { // null token mvc.perform(post("/api/core/users/{userId}/verification", UNVERIFIED_USER_ID) @@ -76,16 +74,16 @@ public void testEmailVerificationWrongToken() throws Exception { .andExpect(status().is(401)); // Wrong audience - String token = jwtService.createToken("wrong-audience", + String token = greenTokenService.createToken("wrong-audience", Long.toString(UNVERIFIED_USER_ID), 60000L, LecUtils.mapOf("email", UNVERIFIED_USER_EMAIL)); mvc.perform(post("/api/core/users/{userId}/verification", UNVERIFIED_USER_ID) .param("code", token) .header("contentType", MediaType.APPLICATION_FORM_URLENCODED)) - .andExpect(status().is(403)); + .andExpect(status().is(401)); // Wrong email - token = jwtService.createToken(JwtService.VERIFY_AUDIENCE, + token = greenTokenService.createToken(GreenTokenService.VERIFY_AUDIENCE, Long.toString(UNVERIFIED_USER_ID), 60000L, LecUtils.mapOf("email", "wrong.email@example.com")); mvc.perform(post("/api/core/users/{userId}/verification", UNVERIFIED_USER_ID) @@ -94,28 +92,27 @@ public void testEmailVerificationWrongToken() throws Exception { .andExpect(status().is(403)); // expired token - token = jwtService.createToken(JwtService.VERIFY_AUDIENCE, + token = greenTokenService.createToken(GreenTokenService.VERIFY_AUDIENCE, Long.toString(UNVERIFIED_USER_ID), 1L, LecUtils.mapOf("email", UNVERIFIED_USER_EMAIL)); // Thread.sleep(1001L); mvc.perform(post("/api/core/users/{userId}/verification", UNVERIFIED_USER_ID) .param("code", token) .header("contentType", MediaType.APPLICATION_FORM_URLENCODED)) - .andExpect(status().is(403)); + .andExpect(status().is(401)); } @Test - public void testEmailVerificationAfterCredentialsUpdate() throws Exception { + void testEmailVerificationAfterCredentialsUpdate() throws Exception { // Credentials updated after the verification token is issued - Thread.sleep(1L); - User user = userRepository.findById(UNVERIFIED_USER_ID).get(); - user.setCredentialsUpdatedMillis(System.currentTimeMillis()); + User user = userRepository.findById(UNVERIFIED_USER_ID).orElseThrow(LexUtils.notFoundSupplier()); + user.setCredentialsUpdatedMillis(System.currentTimeMillis() + 1); userRepository.save(user); mvc.perform(post("/api/core/users/{userId}/verification", UNVERIFIED_USER_ID) .param("code", verificationCode) .header("contentType", MediaType.APPLICATION_FORM_URLENCODED)) - .andExpect(status().is(403)); + .andExpect(status().is(401)); } } diff --git a/lemon-demo-jpa/src/test/resources/postman/Spring Lemon 1.0.postman_collection.json b/lemon-demo-jpa/src/test/resources/postman/Spring Lemon 1.0.postman_collection.json index 1b6ef2e9..ec088f47 100644 --- a/lemon-demo-jpa/src/test/resources/postman/Spring Lemon 1.0.postman_collection.json +++ b/lemon-demo-jpa/src/test/resources/postman/Spring Lemon 1.0.postman_collection.json @@ -46,7 +46,7 @@ }, "response": [ { - "id": "dc13c098-4ab6-4708-9a22-4c6647aa1698", + "id": "94e97d70-c5b8-46d1-9a0a-fdd64c888f71", "name": "Ping", "originalRequest": { "method": "GET", @@ -205,11 +205,13 @@ ] }, "url": { - "raw": "{{lemonDemoUrl}}/login", + "raw": "{{lemonDemoUrl}}/api/core/login", "host": [ "{{lemonDemoUrl}}" ], "path": [ + "api", + "core", "login" ] }, @@ -217,7 +219,7 @@ }, "response": [ { - "id": "e6eefb5e-b4a3-4b42-b9ad-2d2e6dbfb48d", + "id": "fd19ca94-a61e-4af6-8831-97d2ed9a07b7", "name": "Login", "originalRequest": { "method": "POST", @@ -252,11 +254,13 @@ ] }, "url": { - "raw": "{{lemonDemoUrl}}/login", + "raw": "{{lemonDemoUrl}}/api/core/login", "host": [ "{{lemonDemoUrl}}" ], "path": [ + "api", + "core", "login" ] } @@ -330,7 +334,7 @@ "body": "{\"id\":9,\"username\":\"admin@example.com\",\"roles\":[\"ADMIN\"],\"tag\":{\"name\":\"Administrator\"},\"unverified\":false,\"blocked\":false,\"admin\":true,\"goodUser\":true,\"goodAdmin\":true}" }, { - "id": "e3a0b42d-4ad2-46bd-aa6e-ef47d47c60c2", + "id": "5badab55-018e-4fd7-baf4-31c9f6541643", "name": "Login with wrong credentials", "originalRequest": { "method": "POST", @@ -365,11 +369,13 @@ ] }, "url": { - "raw": "{{lemonDemoUrl}}/login", + "raw": "{{lemonDemoUrl}}/api/core/login", "host": [ "{{lemonDemoUrl}}" ], "path": [ + "api", + "core", "login" ] } @@ -486,7 +492,7 @@ }, "response": [ { - "id": "123567f0-1a53-4fe1-b75d-3775da8559e1", + "id": "be379d95-60bb-4066-af3c-1303bc1f879f", "name": "Get context with Authorization header", "originalRequest": { "method": "GET", @@ -618,7 +624,7 @@ "body": "{\"context\":{\"reCaptchaSiteKey\":\"6LdwxRcUAAAAABkhOGWQXhl9FsR27D5YUJRuGzx0\",\"shared\":{\"fooBar\":\"123...\"}},\"user\":{\"id\":2,\"username\":\"skpatel20@gmail.com\",\"roles\":[\"UNVERIFIED\"],\"tag\":{\"name\":\"Sanjay Patel\"},\"unverified\":true,\"blocked\":false,\"admin\":false,\"goodUser\":false,\"goodAdmin\":false}}" }, { - "id": "6f023dfc-2374-4155-86b7-236b150c427e", + "id": "4b46a9e7-8fc7-4c28-aa3d-f8da31ac72af", "name": "Get context using a wrong Authorization token", "originalRequest": { "method": "GET", @@ -744,7 +750,7 @@ "body": "{\"timestamp\":\"2018-03-03T06:21:34.748+0000\",\"status\":401,\"error\":\"Unauthorized\",\"message\":\"Authentication Failed: Invalid JWT serialization: Missing dot delimiter(s)\",\"path\":\"/api/core/context\"}" }, { - "id": "761ee1eb-aaa5-48ae-ac7b-d91a666f7ae1", + "id": "90c0d043-46b7-4b23-9dca-2d12f0278ab4", "name": "Get context without Authorization header", "originalRequest": { "method": "GET", @@ -975,7 +981,7 @@ }, "response": [ { - "id": "e7d318ce-ae30-4120-a3b2-4207ffcf6661", + "id": "c76cf940-d63b-4692-8fa5-bf1eb6a808d2", "name": "Sign up with invalid data", "originalRequest": { "method": "POST", @@ -1100,7 +1106,7 @@ "body": "{\"exception\":\"ConstraintViolationException\",\"error\":\"Unprocessable Entity\",\"message\":\"Validation Error\",\"status\":422,\"errors\":[{\"field\":\"user.email\",\"code\":\"{com.naturalprogrammer.spring.invalid.email}\",\"message\":\"Not a well formed email address\"},{\"field\":\"user.password\",\"code\":\"{com.naturalprogrammer.spring.invalid.password.size}\",\"message\":\"Password must be between 6 and 50 characters\"},{\"field\":\"user.name\",\"code\":\"{blank.name}\",\"message\":\"Name required\"},{\"field\":\"user.email\",\"code\":\"{com.naturalprogrammer.spring.invalid.email.size}\",\"message\":\"Email must be between 4 and 250 characters\"}]}" }, { - "id": "ee65ea2e-743a-4135-bbab-fad97ce3c06f", + "id": "23eba689-6667-49f7-b1ef-501148a40922", "name": "Sign up", "originalRequest": { "method": "POST", @@ -1275,7 +1281,7 @@ }, "response": [ { - "id": "7b70613c-b89e-45e4-b641-4ccfbde839d8", + "id": "bf12db2b-2585-4ca2-a709-65604a2398d8", "name": "Resend verification mail", "originalRequest": { "method": "POST", @@ -1434,7 +1440,7 @@ }, "response": [ { - "id": "79277fc4-5434-4863-99b7-cf4529a4cbcf", + "id": "faaee1e5-9286-4611-889e-260834a7205b", "name": "Verify User", "originalRequest": { "method": "POST", @@ -1599,7 +1605,7 @@ }, "response": [ { - "id": "b7913835-3286-4c8f-b2c8-56f8432bf971", + "id": "dfa67f50-9db1-4801-94dd-b7bba429f4c9", "name": "Forgot password", "originalRequest": { "method": "POST", @@ -1719,25 +1725,12 @@ "header": [ { "key": "Content-Type", - "value": "application/x-www-form-urlencoded" + "value": "application/json" } ], "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "newPassword", - "value": "a-new-password", - "description": "", - "type": "text" - }, - { - "key": "code", - "value": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..HoDu5pdYAx8fS7p9PKCl5g.jRNA1Vu0ZydW9G4bDd6f7GirpNahXtytXke9i7M5xmrsTqUvCitotGwOm3NwhmyIUcBeRTS53lx1SCxDa8cuv9McD2v2exRfKhnNu1i7cG1E10Cmb4m9nuDuAP2LviXpUOLJyYUFRGrxQOrCwwkMNQ.iN6xYjU1Xe-w9O4VDKDEOA", - "description": "", - "type": "text" - } - ] + "mode": "raw", + "raw": "{\n\t\"newPassword\": \"a-new-password\",\n\t\"code\": \"eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..HoDu5pdYAx8fS7p9PKCl5g.jRNA1Vu0ZydW9G4bDd6f7GirpNahXtytXke9i7M5xmrsTqUvCitotGwOm3NwhmyIUcBeRTS53lx1SCxDa8cuv9McD2v2exRfKhnNu1i7cG1E10Cmb4m9nuDuAP2LviXpUOLJyYUFRGrxQOrCwwkMNQ.iN6xYjU1Xe-w9O4VDKDEOA\"\n}" }, "url": { "raw": "{{lemonDemoUrl}}/api/core/reset-password", @@ -1754,35 +1747,19 @@ }, "response": [ { - "id": "bbc954f4-d67a-421c-9045-58cf125ecd31", + "id": "e34d5413-6cf8-40d5-8d17-1dbf7bde7af5", "name": "Reset password", "originalRequest": { "method": "POST", "header": [ { "key": "Content-Type", - "type": "text", - "name": "Content-Type", - "value": "application/x-www-form-urlencoded", - "disabled": false + "value": "application/json" } ], "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "newPassword", - "value": "a-new-password", - "description": "", - "type": "text" - }, - { - "key": "code", - "value": "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..HoDu5pdYAx8fS7p9PKCl5g.jRNA1Vu0ZydW9G4bDd6f7GirpNahXtytXke9i7M5xmrsTqUvCitotGwOm3NwhmyIUcBeRTS53lx1SCxDa8cuv9McD2v2exRfKhnNu1i7cG1E10Cmb4m9nuDuAP2LviXpUOLJyYUFRGrxQOrCwwkMNQ.iN6xYjU1Xe-w9O4VDKDEOA", - "description": "", - "type": "text" - } - ] + "mode": "raw", + "raw": "{\n\t\"newPassword\": \"a-new-password\",\n\t\"code\": \"eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..HoDu5pdYAx8fS7p9PKCl5g.jRNA1Vu0ZydW9G4bDd6f7GirpNahXtytXke9i7M5xmrsTqUvCitotGwOm3NwhmyIUcBeRTS53lx1SCxDa8cuv9McD2v2exRfKhnNu1i7cG1E10Cmb4m9nuDuAP2LviXpUOLJyYUFRGrxQOrCwwkMNQ.iN6xYjU1Xe-w9O4VDKDEOA\"\n}" }, "url": { "raw": "{{lemonDemoUrl}}/api/core/reset-password", @@ -1929,7 +1906,7 @@ }, "response": [ { - "id": "d1ccdda8-bf5d-446a-a468-e1fbc607e26c", + "id": "a5278a35-86da-4ead-b2d3-9860c1861687", "name": "Fetch user by email - Others", "originalRequest": { "method": "POST", @@ -2042,7 +2019,7 @@ "body": "{\"id\":11,\"version\":2,\"roles\":[],\"name\":\"Sample User\",\"new\":false}" }, { - "id": "5595f812-2a4f-4360-bf32-f1813a22a6f3", + "id": "c45a4fbc-9a0b-4397-8a0a-ce8d363efe99", "name": "Fetch user by email - Self or Admin", "originalRequest": { "method": "POST", @@ -2205,7 +2182,7 @@ }, "response": [ { - "id": "2448a1cc-627d-4d23-a10f-fa4f6d32efc7", + "id": "2dce2342-b2e3-4269-a343-7727addd2e67", "name": "Fetch user by ID - Self or Admin", "originalRequest": { "method": "GET", @@ -2331,7 +2308,7 @@ "body": "{\"id\":2,\"version\":2,\"email\":\"skpatel20+lemon1842637@example.com\",\"roles\":[],\"name\":\"Sample User\",\"new\":false}" }, { - "id": "7288f472-5f1e-437a-be18-bd3e0e11d789", + "id": "faf6cfba-2759-4a2d-b983-2e6a7a442260", "name": "Fetch user by ID - Others", "originalRequest": { "method": "GET", @@ -2502,7 +2479,7 @@ }, "response": [ { - "id": "9c1fc11b-c7e2-4406-9db2-07325bdd6da2", + "id": "380c4cbe-efd5-4bdc-9c2d-54af1ff78f54", "name": "Update user", "originalRequest": { "method": "PATCH", @@ -2689,7 +2666,7 @@ }, "response": [ { - "id": "81590aa2-a281-4755-bf43-568fc79f82b1", + "id": "2f63c1e3-be33-47ca-8275-7efd188c19d3", "name": "Change password", "originalRequest": { "method": "POST", @@ -2871,7 +2848,7 @@ }, "response": [ { - "id": "f7320de8-3bd4-45ac-851a-9b4db1870fe5", + "id": "290c8a5c-a3fd-4a5b-a6ae-4b76c894494c", "name": "Requesting for changing email", "originalRequest": { "method": "POST", @@ -3052,7 +3029,7 @@ }, "response": [ { - "id": "09ead591-4e70-4726-b240-f29b5e0f16af", + "id": "e26ba30b-8978-4c97-8709-1972ca5a56a7", "name": "Change email", "originalRequest": { "method": "POST", @@ -3236,7 +3213,7 @@ }, "response": [ { - "id": "af5ab736-94b8-4ef8-9f43-c240d1331df1", + "id": "8ff37fe9-e010-4add-9f79-b7298de2d943", "name": "Fetch new token", "originalRequest": { "method": "POST", diff --git a/lemon-demo-reactive/pom.xml b/lemon-demo-reactive/pom.xml index a1b52aab..a310142b 100644 --- a/lemon-demo-reactive/pom.xml +++ b/lemon-demo-reactive/pom.xml @@ -13,7 +13,7 @@ com.naturalprogrammer spring-lemon - 1.0.0.M4 + 1.0.2 @@ -24,6 +24,12 @@ ${project.version} + + io.projectreactor + reactor-test + test + + diff --git a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/LemonDemoReactiveApplication.java b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/LemonDemoReactiveApplication.java index 25d67a53..044543a2 100644 --- a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/LemonDemoReactiveApplication.java +++ b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/LemonDemoReactiveApplication.java @@ -2,26 +2,11 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Bean; - -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.naturalprogrammer.spring.lemondemo.domain.User; -import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; @SpringBootApplication public class LemonDemoReactiveApplication { public static void main(String[] args) { SpringApplication.run(LemonDemoReactiveApplication.class, args); - } - - @Bean - public SimpleModule objectIdModule() { - - SimpleModule module = new SimpleModule(); - module.setMixInAnnotation(User.class, AbstractMongoUser.class); - //module.addSerializer(ObjectId.class, new ToStringSerializer()); - - return module; - } + } } diff --git a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/controllers/MyController.java b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/controllers/MyController.java index 3aae7ac0..a9914e42 100644 --- a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/controllers/MyController.java +++ b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/controllers/MyController.java @@ -1,27 +1,27 @@ package com.naturalprogrammer.spring.lemondemo.controllers; -import org.bson.types.ObjectId; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import com.fasterxml.jackson.annotation.JsonView; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; import com.naturalprogrammer.spring.lemon.commons.util.UserUtils.SignUpValidation; import com.naturalprogrammer.spring.lemondemo.domain.User; import com.naturalprogrammer.spring.lemonreactive.LemonReactiveController; - +import org.bson.types.ObjectId; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @RestController -@RequestMapping("/api/core") +@RequestMapping(MyController.BASE_URI) public class MyController extends LemonReactiveController { + public static final String BASE_URI = "/api/core"; + @Override - public Mono> signup( + public Mono signup( @RequestBody @JsonView(UserUtils.SignupInput.class) @Validated(SignUpValidation.class) Mono user, ServerHttpResponse response) { diff --git a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/domain/User.java b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/domain/User.java index a613e63a..b12dfd8f 100644 --- a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/domain/User.java +++ b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/domain/User.java @@ -1,23 +1,20 @@ package com.naturalprogrammer.spring.lemondemo.domain; -import java.io.Serializable; - -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.Size; - -import org.bson.types.ObjectId; -import org.springframework.data.annotation.TypeAlias; -import org.springframework.data.mongodb.core.mapping.Document; - import com.fasterxml.jackson.annotation.JsonView; import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; - import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.ToString; +import org.bson.types.ObjectId; +import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.mongodb.core.mapping.Document; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.io.Serializable; -@Getter @Setter @NoArgsConstructor +@Getter @Setter @TypeAlias("User") @Document(collection = "usr") public class User extends AbstractMongoUser { @@ -25,25 +22,11 @@ public class User extends AbstractMongoUser { public static final int NAME_MIN = 1; public static final int NAME_MAX = 50; + @Getter @Setter @ToString public static class Tag implements Serializable { private static final long serialVersionUID = -2129078111926834670L; - private String name; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public User(String email, String password, String name) { - this.email = email; - this.password = password; - this.name = name; } @JsonView(UserUtils.SignupInput.class) @@ -58,12 +41,4 @@ public Tag toTag() { tag.setName(name); return tag; } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } } diff --git a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/repositories/UserRepository.java b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/repositories/UserRepository.java index ff841fc9..fcea2d33 100644 --- a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/repositories/UserRepository.java +++ b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/repositories/UserRepository.java @@ -1,8 +1,8 @@ package com.naturalprogrammer.spring.lemondemo.repositories; -import org.bson.types.ObjectId; import com.naturalprogrammer.spring.lemondemo.domain.User; import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUserRepository; +import org.bson.types.ObjectId; public interface UserRepository extends AbstractMongoUserRepository { diff --git a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/services/MyService.java b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/services/MyService.java index faf2d3f8..d61c2a3b 100644 --- a/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/services/MyService.java +++ b/lemon-demo-reactive/src/main/java/com/naturalprogrammer/spring/lemondemo/services/MyService.java @@ -1,11 +1,13 @@ package com.naturalprogrammer.spring.lemondemo.services; -import org.bson.types.ObjectId; -import org.springframework.stereotype.Service; - import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemondemo.domain.User; import com.naturalprogrammer.spring.lemonreactive.LemonReactiveService; +import org.bson.types.ObjectId; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.stereotype.Service; + +import java.util.Map; @Service public class MyService extends LemonReactiveService { @@ -26,9 +28,36 @@ protected User createAdminUser() { } @Override - protected void updateUserFields(User user, User updatedUser, UserDto currentUser) { + protected void updateUserFields(User user, User updatedUser, UserDto currentUser) { super.updateUserFields(user, updatedUser, currentUser); user.setName(updatedUser.getName()); } + + @Override + public void fillAdditionalFields(String registrationId, User user, Map attributes) { + + String nameKey; + + switch (registrationId) { + + case "facebook": + nameKey = StandardClaimNames.NAME; + break; + + case "google": + nameKey = StandardClaimNames.NAME; + break; + + default: + throw new UnsupportedOperationException("Fetching name from " + registrationId + " login not supprrted"); + } + + user.setName((String) attributes.get(nameKey)); + } + + @Override + protected ObjectId toId(String id) { + return new ObjectId(id); + } } diff --git a/lemon-demo-reactive/src/main/resources/ValidationMessages.properties b/lemon-demo-reactive/src/main/resources/ValidationMessages.properties index 9145571f..533b725f 100644 --- a/lemon-demo-reactive/src/main/resources/ValidationMessages.properties +++ b/lemon-demo-reactive/src/main/resources/ValidationMessages.properties @@ -1,3 +1,7 @@ +# +## Spring Lemon Bean Validation messages +# + com.naturalprogrammer.spring.blank.email: Email needed com.naturalprogrammer.spring.invalid.email: Not a well formed email address com.naturalprogrammer.spring.invalid.email.size: Email must be between {min} and {max} characters @@ -9,4 +13,8 @@ com.naturalprogrammer.spring.invalid.password.size: Password must be between {mi com.naturalprogrammer.spring.different.passwords: Passwords do not match com.naturalprogrammer.spring.blank.password: Password needed +# +## My Bean Validation Messages +# + blank.name: Name required diff --git a/lemon-demo-reactive/src/main/resources/config/application-dev.yml b/lemon-demo-reactive/src/main/resources/config/application-dev.yml index 2cd5ec47..78ff07d2 100644 --- a/lemon-demo-reactive/src/main/resources/config/application-dev.yml +++ b/lemon-demo-reactive/src/main/resources/config/application-dev.yml @@ -4,19 +4,20 @@ spring: data: mongodb: database: lemon + security: oauth2: client: provider: facebook: - user-info-uri: https://graph.facebook.com/me?fields=email,name,verified + user-info-uri: https://graph.facebook.com/me?fields=email,name registration: google: client-id: 1011974249454-6gq0hr01gqh3cndoqnss5r69tkk2nd84.apps.googleusercontent.com client-secret: saDA6Cj60wipncFM-hzBD-C6 facebook: - client-id: 1234020186718741 - client-secret: 0c0abaf685a83e879e8e48b1167c96ab + client-id: 548349525905412 + client-secret: 15a20c560c4c780dabdc0e637c02087a logging: level: diff --git a/lemon-demo-reactive/src/main/resources/config/application.yml b/lemon-demo-reactive/src/main/resources/config/application.yml index d20bb70f..0737450c 100644 --- a/lemon-demo-reactive/src/main/resources/config/application.yml +++ b/lemon-demo-reactive/src/main/resources/config/application.yml @@ -13,6 +13,9 @@ spring: deserialization: accept-single-value-as-array: true + # https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.1-Release-Notes#bean-overriding + main.allow-bean-definition-overriding: true + # Spring Lemon related properties lemon: diff --git a/lemon-demo-reactive/src/main/resources/messages.properties b/lemon-demo-reactive/src/main/resources/messages.properties index b05037fd..4be0d170 100644 --- a/lemon-demo-reactive/src/main/resources/messages.properties +++ b/lemon-demo-reactive/src/main/resources/messages.properties @@ -1,3 +1,7 @@ +# +## Spring Lemon Normal Messages +# + com.naturalprogrammer.spring.validationError: Validation Error com.naturalprogrammer.spring.verifySubject: Please verify your email id @@ -34,3 +38,10 @@ com.naturalprogrammer.spring.expiredToken: Expired token com.naturalprogrammer.spring.notGoodAdminOrSameUser: Only a good Admin or same user is permitted for this operation com.naturalprogrammer.spring.blank: Please provide {0} + +com.naturalprogrammer.spring.userClaimAbsent: User claim absent in the authorization token +com.naturalprogrammer.spring.fullTokenNotAllowed: Full authorization tokens aren't allowed here + +# +## My Normal Messages +# diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/AbstractTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/AbstractTests.java new file mode 100644 index 00000000..f6d245d2 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/AbstractTests.java @@ -0,0 +1,51 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.mail.MailSender; +import com.naturalprogrammer.spring.lemondemo.dto.TestErrorResponse; +import com.naturalprogrammer.spring.lemondemo.dto.TestLemonFieldError; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.test.web.reactive.server.EntityExchangeResult; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest({ + "logging.level.com.naturalprogrammer=ERROR", // logging.level.root=ERROR does not work: https://stackoverflow.com/questions/49048298/springboottest-not-overriding-logging-level + "logging.level.org.springframework=ERROR", + "lemon.recaptcha.sitekey=", + "spring.data.mongodb.database=lemontest" +}) +public abstract class AbstractTests { + + @Autowired + protected ReactiveMongoTemplate mongoTemplate; + + @Autowired + protected MyTestUtils testUtils; + + @MockBean + protected MailSender mailSender; + + @BeforeEach + public void initialize() { + + testUtils.initDatabase(); + } + + protected void assertErrors(EntityExchangeResult errorResponseResult, String... fields) { + + TestErrorResponse response = errorResponseResult.getResponseBody(); + assertEquals(fields.length, response.getErrors().size()); + + assertTrue(response.getErrors().stream() + .map(TestLemonFieldError::getField).collect(Collectors.toSet()) + .containsAll(Arrays.asList(fields))); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/BasicTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/BasicTests.java new file mode 100644 index 00000000..fc934cd0 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/BasicTests.java @@ -0,0 +1,46 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import org.junit.jupiter.api.Test; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; + +class BasicTests extends AbstractTests { + + @Test + void testPing() throws Exception { + + CLIENT.get() + .uri(BASE_URI + "/ping") + .exchange() + .expectStatus() + .isNoContent(); + } + + @Test + void testGetContextLoggedIn() throws Exception { + + testUtils.contextResponse(TOKENS.get(ADMIN_ID)) + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody() + .jsonPath("$.context.reCaptchaSiteKey").exists() + .jsonPath("$.user.id").isEqualTo(ADMIN_ID.toString()) + .jsonPath("$.user.roles[0]").isEqualTo("ADMIN") + .jsonPath("$.user.password").doesNotExist(); + } + + @Test + void testGetContextWithoutLoggedIn() throws Exception { + + CLIENT.get() + .uri(BASE_URI + "/context") + .exchange() + .expectStatus().isOk() + .expectHeader().doesNotExist(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody() + .jsonPath("$.context.reCaptchaSiteKey").exists() + .jsonPath("$.user").doesNotExist(); + } + +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ChangeEmailTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ChangeEmailTests.java new file mode 100644 index 00000000..07dae7eb --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ChangeEmailTests.java @@ -0,0 +1,156 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.security.GreenTokenService; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.domain.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; + +public class ChangeEmailTests extends AbstractTests { + + private static final String NEW_EMAIL = "new.email@example.com"; + + private String changeEmailCode; + + @Autowired + private GreenTokenService greenTokenService; + + @BeforeEach + public void setUp() { + + User user = mongoTemplate.findById(UNVERIFIED_USER_ID, User.class).block(); + assert user != null; + user.setNewEmail(NEW_EMAIL); + mongoTemplate.save(user).block(); + + changeEmailCode = greenTokenService.createToken( + GreenTokenService.CHANGE_EMAIL_AUDIENCE, + UNVERIFIED_USER_ID.toString(), 60000L, + LecUtils.mapOf("newEmail", NEW_EMAIL)); + } + + @Test + void testChangeEmail() throws Exception { + + changeEmailResponse(changeEmailCode) + .expectStatus().isOk() + .expectHeader().valueMatches(LecUtils.TOKEN_RESPONSE_HEADER_NAME, ".*\\..*") + .expectBody().jsonPath("$.id").isEqualTo(UNVERIFIED_USER_ID.toString()); + + User updatedUser = mongoTemplate.findById(UNVERIFIED_USER_ID, User.class).block(); + assertNull(updatedUser.getNewEmail()); + assertEquals(NEW_EMAIL, updatedUser.getEmail()); + + changeEmailResponse(changeEmailCode) + .expectStatus().isUnauthorized(); + } + + /** + * Providing a wrong changeEmailCode shouldn't work. + */ + @Test + void testChangeEmailWrongCode() throws Exception { + + // Blank token + changeEmailResponse("") + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // Wrong audience + String code = greenTokenService.createToken( + "", // blank audience + UNVERIFIED_USER_ID.toString(), 60000L, + LecUtils.mapOf("newEmail", NEW_EMAIL)); + + changeEmailResponse(code) + .expectStatus().isUnauthorized(); + + // Wrong userId subject + code = greenTokenService.createToken( + GreenTokenService.CHANGE_EMAIL_AUDIENCE, + ADMIN_ID.toString(), 60000L, + LecUtils.mapOf("newEmail", NEW_EMAIL)); + + changeEmailResponse(code) + .expectStatus().isForbidden(); + + // Wrong new email + code = greenTokenService.createToken( + GreenTokenService.CHANGE_EMAIL_AUDIENCE, + UNVERIFIED_USER_ID.toString(), 60000L, + LecUtils.mapOf("newEmail", "wrong.new.email@example.com")); + + changeEmailResponse(code) + .expectStatus().isForbidden(); + } + + /** + * Providing a wrong changeEmailCode shouldn't work. + */ + @Test + public void testChangeEmailObsoleteCode() throws Exception { + + // credentials updated after the request for email change was made + User user = mongoTemplate.findById(UNVERIFIED_USER_ID, User.class).block(); + user.setCredentialsUpdatedMillis(System.currentTimeMillis() + 1); + mongoTemplate.save(user).block(); + + // A new auth token is needed, because old one would be obsolete! + String authToken = testUtils.login(UNVERIFIED_USER_EMAIL, USER_PASSWORD); + + CLIENT.post().uri(BASE_URI + "/users/{id}/email", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, authToken) + .body(fromFormData("code", changeEmailCode)) + .exchange() + .expectStatus().isUnauthorized(); + } + + /** + * Trying without having requested first. + * @throws Exception + */ + @Test + void testChangeEmailWithoutAnyRequest() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/email", USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(USER_ID)) + .body(fromFormData("code", changeEmailCode)) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + + /** + * Trying after some user registers the newEmail, leaving it non unique. + * @throws Exception + */ + @Test + public void testChangeEmailNonUniqueEmail() throws Exception { + + // Some other user changed to the same email + User user = mongoTemplate.findById(ADMIN_ID, User.class).block(); + user.setEmail(NEW_EMAIL); + mongoTemplate.save(user).block(); + + // Blank token + changeEmailResponse(changeEmailCode) + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + + private ResponseSpec changeEmailResponse(String code) { + + return CLIENT.post().uri(BASE_URI + "/users/{id}/email", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_USER_ID)) + .body(fromFormData("code", code)) + .exchange(); + } + +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ChangePasswordTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ChangePasswordTests.java new file mode 100644 index 00000000..a7c0d04c --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ChangePasswordTests.java @@ -0,0 +1,192 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.domain.ChangePasswordForm; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.dto.TestErrorResponse; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import reactor.core.publisher.Mono; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; + +class ChangePasswordTests extends AbstractTests { + + private static final String NEW_PASSWORD = "a-new-password"; + + private ChangePasswordForm changePasswordForm(String oldPassword) { + + ChangePasswordForm form = new ChangePasswordForm(); + form.setOldPassword(oldPassword); + form.setPassword(NEW_PASSWORD); + form.setRetypePassword(NEW_PASSWORD); + + return form; + } + + /** + * A non-admin user should be able to change his password. + */ + @Test + void testChangePassword() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/password", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_USER_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(changePasswordForm(USER_PASSWORD)), ChangePasswordForm.class) + .exchange() + .expectStatus().isNoContent() + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME); + + // Ensure able to login with new password + testUtils.login(UNVERIFIED_USER_EMAIL, NEW_PASSWORD); + } + + + /** + * An good admin user should be able to change the password of another user. + */ + @Test + void testAdminChangePasswordAnotherUser() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/password", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(changePasswordForm(ADMIN_PASSWORD)), ChangePasswordForm.class) + .exchange() + .expectStatus().isNoContent() + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME); + + // Ensure able to login with new password + testUtils.login(UNVERIFIED_USER_EMAIL, NEW_PASSWORD); + } + + /** + * Providing an unknown id should return 404. + */ + @Test + void testChangePasswordUnknownId() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/password", ObjectId.get()) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(changePasswordForm(ADMIN_PASSWORD)), ChangePasswordForm.class) + .exchange() + .expectStatus().isNotFound() + .expectHeader().doesNotExist(LecUtils.TOKEN_RESPONSE_HEADER_NAME); + } + + /** + * A non-admin user should not be able to change others' password. + */ + @Test + void testChangePasswordAnotherUser() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/password", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(USER_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(changePasswordForm(USER_PASSWORD)), ChangePasswordForm.class) + .exchange() + .expectStatus().isForbidden() + .expectHeader().doesNotExist(LecUtils.TOKEN_RESPONSE_HEADER_NAME); + + // Ensure password didn't change + testUtils.login(UNVERIFIED_USER_EMAIL, USER_PASSWORD); + } + + + /** + * A bad admin user should not be able to change others' password. + */ + @Test + void testBadAdminChangePasswordAnotherUser() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/password", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(changePasswordForm(ADMIN_PASSWORD)), ChangePasswordForm.class) + .exchange() + .expectStatus().isForbidden() + .expectHeader().doesNotExist(LecUtils.TOKEN_RESPONSE_HEADER_NAME); + + // Ensure password didn't change + testUtils.login(UNVERIFIED_USER_EMAIL, USER_PASSWORD); + } + + + @Test + void testChangePasswordInvalidData() throws Exception { + + //@formatter:off + CLIENT.post().uri(BASE_URI + "/users/{id}/password", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(new ChangePasswordForm()), ChangePasswordForm.class) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY) + .expectHeader().doesNotExist(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody(TestErrorResponse.class) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "changePasswordFormMono.oldPassword", + "changePasswordFormMono.retypePassword", + "changePasswordFormMono.password"); + }); + //@formatter:on + + // Ensure password didn't change + testUtils.login(UNVERIFIED_USER_EMAIL, USER_PASSWORD); + + // All fields too short + ChangePasswordForm form = new ChangePasswordForm(); + form.setOldPassword("short"); + form.setPassword("short"); + form.setRetypePassword("short"); + + //@formatter:off + CLIENT.post().uri(BASE_URI + "/users/{id}/password", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form), ChangePasswordForm.class) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY) + .expectHeader().doesNotExist(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody(TestErrorResponse.class) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "changePasswordFormMono.oldPassword", + "changePasswordFormMono.retypePassword", + "changePasswordFormMono.password"); + }); + //@formatter:on + + // Ensure password didn't change + testUtils.login(UNVERIFIED_USER_EMAIL, USER_PASSWORD); + + // different retype-password + form = changePasswordForm(USER_PASSWORD); + form.setRetypePassword("different-retype-password"); + + //@formatter:off + CLIENT.post().uri(BASE_URI + "/users/{id}/password", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form), ChangePasswordForm.class) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY) + .expectHeader().doesNotExist(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody(TestErrorResponse.class) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "changePasswordFormMono.retypePassword", + "changePasswordFormMono.password"); + }); + //@formatter:on + + // Ensure password didn't change + testUtils.login(UNVERIFIED_USER_EMAIL, USER_PASSWORD); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/FetchNewTokenTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/FetchNewTokenTests.java new file mode 100644 index 00000000..5caa9c93 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/FetchNewTokenTests.java @@ -0,0 +1,94 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemondemo.dto.TestToken; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.EntityExchangeResult; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; + +class FetchNewTokenTests extends AbstractTests { + + @Test + void testFetchNewToken() throws Exception { + + CLIENT.post().uri(BASE_URI + "/fetch-new-auth-token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_USER_ID)) + .exchange() + .expectStatus().isOk() + .expectBody(TestToken.class) + .consumeWith(this::ensureTokenWorks); + } + + + @Test + void testFetchNewTokenExpiration() throws Exception { + + CLIENT.post().uri(BASE_URI + "/fetch-new-auth-token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_USER_ID)) + .body(fromFormData("expirationMillis", "1000")) + .exchange() + .expectStatus().isOk() + .expectBody(TestToken.class) + .consumeWith(result -> { + + ensureTokenWorks(result); + TestToken token = result.getResponseBody(); + + try { + Thread.sleep(1001L); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + CLIENT.get() + .uri(BASE_URI + "/context") + .header(HttpHeaders.AUTHORIZATION, token.getToken()) + .exchange() + .expectStatus().isUnauthorized(); + }); + } + + @Test + void testFetchNewTokenByAdminForAnotherUser() throws Exception { + + CLIENT.post().uri(BASE_URI + "/fetch-new-auth-token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .body(fromFormData("username", UNVERIFIED_USER_EMAIL)) + .exchange() + .expectStatus().isOk() + .expectBody(TestToken.class) + .consumeWith(this::ensureTokenWorks); + } + + + @Test + void testFetchNewTokenByNonAdminForAnotherUser() throws Exception { + + CLIENT.post().uri(BASE_URI + "/fetch-new-auth-token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_USER_ID)) + .body(fromFormData("username", ADMIN_EMAIL)) + .exchange() + .expectStatus().isForbidden() + .expectBody().jsonPath("$.token").doesNotExist(); + } + + + private void ensureTokenWorks(EntityExchangeResult result) { + + TestToken token = result.getResponseBody(); + assertNotNull(token.getToken()); + + testUtils.contextResponse(token.getToken()) + .expectBody() + .jsonPath("$.user.id").isEqualTo(UNVERIFIED_USER_ID.toString()); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/FetchUserTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/FetchUserTests.java new file mode 100644 index 00000000..5826c4f4 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/FetchUserTests.java @@ -0,0 +1,110 @@ +package com.naturalprogrammer.spring.lemondemo; + +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; + +class FetchUserTests extends AbstractTests { + + @Test + void testFetchUserById() throws Exception { + + CLIENT.get().uri(BASE_URI + "/users/{id}", ADMIN_ID) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.id").isEqualTo(ADMIN_ID.toString()) + .jsonPath("$.email").doesNotExist() + .jsonPath("$.password").doesNotExist() + .jsonPath("$.credentialsUpdatedAt").doesNotExist() + .jsonPath("$.name").isEqualTo("Admin 1"); + } + + @Test + void testFetchUserByIdLoggedIn() throws Exception { + + // Same user logged in + CLIENT.get().uri(BASE_URI + "/users/{id}", ADMIN_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.id").isEqualTo(ADMIN_ID.toString()) + .jsonPath("$.email").isEqualTo(ADMIN_EMAIL) + .jsonPath("$.password").doesNotExist() + .jsonPath("$.credentialsUpdatedAt").doesNotExist() + .jsonPath("$.name").isEqualTo("Admin 1"); + + // Another user logged in + CLIENT.get().uri(BASE_URI + "/users/{id}", ADMIN_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_USER_ID)) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.id").isEqualTo(ADMIN_ID.toString()) + .jsonPath("$.email").doesNotExist(); + + // Admin user logged in - fetching another user + CLIENT.get().uri(BASE_URI + "/users/{id}", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.id").isEqualTo(UNVERIFIED_USER_ID.toString()) + .jsonPath("$.email").isEqualTo(UNVERIFIED_USER_EMAIL); + } + + @Test + void testFetchNonExistingUserById() throws Exception { + + CLIENT.get().uri(BASE_URI + "/users/{id}", ObjectId.get()) + .exchange() + .expectStatus().isNotFound(); + } + + @Test + void testFetchUserByEmail() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/fetch-by-email") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("email", ADMIN_EMAIL)) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.id").isEqualTo(ADMIN_ID.toString()) + .jsonPath("$.password").doesNotExist() + .jsonPath("$.credentialsUpdatedAt").doesNotExist() + .jsonPath("$.name").isEqualTo("Admin 1"); + } + + @Test + void testFetchUserByInvalidEmail() throws Exception { + + // email does not exist + CLIENT.post().uri(BASE_URI + "/users/fetch-by-email") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("email", "foo@example.com")) + .exchange() + .expectStatus().isNotFound(); + + // Blank email + CLIENT.post().uri(BASE_URI + "/users/fetch-by-email") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("email", "")) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // Invalid email + CLIENT.post().uri(BASE_URI + "/users/fetch-by-email") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("email", "invalid-email")) + .exchange() + .expectStatus().isNotFound(); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ForgotPasswordTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ForgotPasswordTests.java new file mode 100644 index 00000000..b3682d80 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ForgotPasswordTests.java @@ -0,0 +1,61 @@ +package com.naturalprogrammer.spring.lemondemo; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.ADMIN_EMAIL; +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.CLIENT; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; + +class ForgotPasswordTests extends AbstractTests { + + @Test + void testForgotPassword() { + + CLIENT.post().uri(BASE_URI + "/forgot-password") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("email", ADMIN_EMAIL)) + .exchange() + .expectStatus().isNoContent(); + + verify(mailSender).send(any()); + } + + @Test + void testForgotPasswordInvalidEmail() throws Exception { + + // Unknown email + CLIENT.post().uri(BASE_URI + "/forgot-password") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("email", "unknown@example.com")) + .exchange() + .expectStatus().isNotFound(); + + // Null email + CLIENT.post().uri(BASE_URI + "/forgot-password") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // Blank email + CLIENT.post().uri(BASE_URI + "/forgot-password") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("email", "")) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // Wrong email format + CLIENT.post().uri(BASE_URI + "/forgot-password") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(fromFormData("email", "wrong-email-format")) + .exchange() + .expectStatus().isNotFound(); + + verify(mailSender, never()).send(any()); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/LemonDemoReactiveApplicationTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/LemonDemoReactiveApplicationTests.java deleted file mode 100644 index 16d1772b..00000000 --- a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/LemonDemoReactiveApplicationTests.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.naturalprogrammer.spring.lemondemo; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit4.SpringRunner; - -@RunWith(SpringRunner.class) -@SpringBootTest -public class LemonDemoReactiveApplicationTests { - - @Test - public void contextLoads() { - } - -} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/LoginTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/LoginTests.java new file mode 100644 index 00000000..a6de6ab3 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/LoginTests.java @@ -0,0 +1,129 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.domain.User; +import com.naturalprogrammer.spring.lemondemo.dto.TestUserDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; + +class LoginTests extends AbstractTests { + + @Test + void testLogin() { + + testUtils.loginResponse(ADMIN_EMAIL, ADMIN_PASSWORD) + .expectStatus().isOk() + .expectBody(TestUserDto.class) + .consumeWith(result -> { + + TestUserDto user = result.getResponseBody(); + + assertEquals(ADMIN_ID, user.getId()); + assertNull(user.getPassword()); + assertEquals("admin@example.com", user.getUsername()); + assertEquals(1, user.getRoles().size()); + assertTrue(user.getRoles().contains("ADMIN")); + assertEquals("Admin 1", user.getTag().getName()); + assertFalse(user.isUnverified()); + assertFalse(user.isBlocked()); + assertTrue(user.isAdmin()); + assertTrue(user.isGoodUser()); + assertTrue(user.isGoodAdmin()); + }); + } + + + @Test + void testLoginTokenExpiry() throws Exception { + + String token = login(ADMIN_EMAIL, ADMIN_PASSWORD, 500L); + Thread.sleep(501L); + + CLIENT.get() + .uri(BASE_URI + "/ping") + .header(HttpHeaders.AUTHORIZATION, token) + .exchange() + .expectStatus().isUnauthorized(); + } + + + @Test + void testObsoleteToken() throws Exception { + + User user = mongoTemplate.findById(ADMIN_ID, User.class).block(); + user.setCredentialsUpdatedMillis(System.currentTimeMillis()); + mongoTemplate.save(user).block(); + + CLIENT.get() + .uri(BASE_URI + "/ping") + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .exchange() + .expectStatus().isUnauthorized(); + } + + + @Test + void testLoginWrongPassword() throws Exception { + + testUtils.loginResponse(ADMIN_EMAIL, "wrong-password") + .expectStatus().isUnauthorized(); + } + + + @Test + void testLoginBlankPassword() throws Exception { + + testUtils.loginResponse(ADMIN_EMAIL, "") + .expectStatus().isUnauthorized(); + } + + + @Test + void testTokenLogin() { + + CLIENT.get().uri(BASE_URI + "/context") + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$.user.id").isEqualTo(ADMIN_ID.toString()); + } + + + @Test + void testTokenLoginWrongToken() { + + CLIENT.get().uri(BASE_URI + "/context") + .header(HttpHeaders.AUTHORIZATION, "Bearer a-wrong-token") + .exchange() + .expectStatus().isUnauthorized(); + } + + + @Test + void testLogout() throws Exception { + + CLIENT.post().uri("/logout") + .exchange() + .expectStatus().isNotFound(); + } + + + private String login(String username, String password, long expirationMillis) { + + return CLIENT.post() + .uri(BASE_URI + "/login") + .body(fromFormData("username", username) + .with("password", password) + .with("expirationMillis", Long.toString(expirationMillis))) + .exchange() + .returnResult(TestUserDto.class) + .getResponseHeaders() + .getFirst(LecUtils.TOKEN_RESPONSE_HEADER_NAME); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/MyTestUtils.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/MyTestUtils.java new file mode 100644 index 00000000..26696ead --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/MyTestUtils.java @@ -0,0 +1,119 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.domain.User; +import com.naturalprogrammer.spring.lemondemo.dto.TestUserDto; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.types.ObjectId; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; + +@Component +@Slf4j +public class MyTestUtils { + + public static final ObjectId ADMIN_ID = ObjectId.get(); + public static final ObjectId UNVERIFIED_ADMIN_ID = ObjectId.get(); + public static final ObjectId BLOCKED_ADMIN_ID = ObjectId.get(); + + public static final ObjectId USER_ID = ObjectId.get(); + public static final ObjectId UNVERIFIED_USER_ID = ObjectId.get(); + public static final ObjectId BLOCKED_USER_ID = ObjectId.get(); + + public static final String ADMIN_EMAIL = "admin@example.com"; + public static final String ADMIN_PASSWORD = "admin!"; + + public static final String USER_PASSWORD = "Sanjay99!"; + public static final String UNVERIFIED_USER_EMAIL = "unverifieduser@example.com"; + + public static final Map TOKENS = new HashMap<>(6); + + public static WebTestClient CLIENT; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private ReactiveMongoTemplate mongoTemplate; + + public MyTestUtils(ApplicationContext context) { + CLIENT = WebTestClient + .bindToApplicationContext(context) + .configureClient() + .responseTimeout(Duration.ofHours(1L)) + .build(); + } + + public String login(String userName, String password) { + + return loginResponse(userName, password) + .expectStatus().isOk() + .returnResult(TestUserDto.class) + .getResponseHeaders() + .getFirst(LecUtils.TOKEN_RESPONSE_HEADER_NAME); + } + + public ResponseSpec loginResponse(String userName, String password) { + + return CLIENT.post() + .uri(BASE_URI + "/login") + .body(fromFormData("username", userName) + .with("password", password)) + .exchange(); + } + + public ResponseSpec contextResponse(String token) { + + return CLIENT.get() + .uri(BASE_URI + "/context") + .header(HttpHeaders.AUTHORIZATION, token) + .exchange() + .expectStatus().isOk(); + } + + public void initDatabase() { + + mongoTemplate.dropCollection("usr").block(); + log.debug("Creating users ... "); + + createUser(ADMIN_ID, ADMIN_EMAIL, ADMIN_PASSWORD, "Admin 1", "ADMIN"); + createUser(UNVERIFIED_ADMIN_ID, "unverifiedadmin@example.com", ADMIN_PASSWORD, "Unverified Admin", "ADMIN", "UNVERIFIED"); + createUser(BLOCKED_ADMIN_ID, "blockedadmin@example.com", ADMIN_PASSWORD, "Blocked Admin", "ADMIN", "BLOCKED"); + createUser(USER_ID, "user@example.com", USER_PASSWORD, "User"); + createUser(UNVERIFIED_USER_ID, UNVERIFIED_USER_EMAIL, USER_PASSWORD, "Unverified User", "UNVERIFIED"); + createUser(BLOCKED_USER_ID, "blockeduser@example.com", USER_PASSWORD, "Blocked User", "BLOCKED"); + + log.debug("Created users."); + } + + private void createUser(ObjectId id, String email, String password, String name, String... roles) { + + User user = new User(); + user.setId(id); + user.setEmail(email); + user.setPassword(passwordEncoder.encode(password)); + user.setName(name); + user.setCredentialsUpdatedMillis(0L); + user.setRoles(new HashSet(Arrays.asList(roles))); + + mongoTemplate.insert(user).block(); + TOKENS.put(user.getId(), login(user.getEmail(), password)); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/RequestEmailChangeTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/RequestEmailChangeTests.java new file mode 100644 index 00000000..153e355b --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/RequestEmailChangeTests.java @@ -0,0 +1,218 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.naturalprogrammer.spring.lemondemo.domain.User; +import com.naturalprogrammer.spring.lemondemo.dto.TestEmailForm; +import com.naturalprogrammer.spring.lemondemo.dto.TestErrorResponse; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient.BodySpec; +import reactor.core.publisher.Mono; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class RequestEmailChangeTests extends AbstractTests { + + private static final String NEW_EMAIL = "new.email@example.com"; + + private TestEmailForm form() { + + TestEmailForm emailForm = new TestEmailForm(); + emailForm.setPassword(USER_PASSWORD); + emailForm.setNewEmail(NEW_EMAIL); + + return emailForm; + } + + + @Test + void testRequestEmailChange() { + + CLIENT.post().uri(BASE_URI + "/users/{id}/email-change-request", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_USER_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form()), TestEmailForm.class) + .exchange() + .expectStatus().isNoContent(); + + verify(mailSender).send(any()); + + User updatedUser = mongoTemplate.findById(UNVERIFIED_USER_ID, User.class).block(); + assertEquals(NEW_EMAIL, updatedUser.getNewEmail()); + assertEquals(UNVERIFIED_USER_EMAIL, updatedUser.getEmail()); + } + + + /** + * A good admin should be able to request changing email of another user. + */ + @Test + void testGoodAdminRequestEmailChange() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/email-change-request", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form()), TestEmailForm.class) + .exchange() + .expectStatus().isNoContent(); + + User updatedUser = mongoTemplate.findById(UNVERIFIED_USER_ID, User.class).block(); + assertEquals(NEW_EMAIL, updatedUser.getNewEmail()); + } + + + /** + * A request changing email of unknown user. + */ + @Test + void testRequestEmailChangeUnknownUser() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/email-change-request", ObjectId.get()) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form()), TestEmailForm.class) + .exchange() + .expectStatus().isNotFound(); + + verify(mailSender, never()).send(any()); + } + + + /** + * A non-admin should not be able to request changing + * the email id of another user + */ + @Test + void testNonAdminRequestEmailChangeAnotherUser() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/email-change-request", ADMIN_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(USER_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form()), TestEmailForm.class) + .exchange() + .expectStatus().isForbidden(); + + assertNotChanged(); + verify(mailSender, never()).send(any()); + } + + + /** + * A bad admin trying to change the email id + * of another user + */ + @Test + void testBadAdminRequestEmailChangeAnotherUser() throws Exception { + + CLIENT.post().uri(BASE_URI + "/users/{id}/email-change-request", ADMIN_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_ADMIN_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form()), TestEmailForm.class) + .exchange() + .expectStatus().isForbidden(); + + assertNotChanged(); + verify(mailSender, never()).send(any()); + } + + + /** + * Trying with invalid data. + */ + @Test + void testRequestEmailChangeWithInvalidData() { + + // try with null newEmail and password + tryRequestingEmailChangeBodySpec(new TestEmailForm()) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "emailFormMono.newEmail", + "emailFormMono.password"); + }); + assertNotChanged(); + + // try with blank newEmail and password + TestEmailForm emailForm = new TestEmailForm(); + emailForm.setPassword(""); + emailForm.setNewEmail(""); + tryRequestingEmailChangeBodySpec(emailForm) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "emailFormMono.newEmail", + "emailFormMono.newEmail", + "emailFormMono.password", + "emailFormMono.password"); + }); + + // try with invalid newEmail + emailForm = form(); + emailForm.setNewEmail("an-invalid-email"); + tryRequestingEmailChangeBodySpec(emailForm) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "emailFormMono.newEmail"); + }); + assertNotChanged(); + + // try with wrong password + emailForm = form(); + emailForm.setPassword("wrong-password"); + tryRequestingEmailChangeBodySpec(emailForm) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "emailFormMono.password"); + }); + assertNotChanged(); + + // try with null password + emailForm = form(); + emailForm.setPassword(null); + tryRequestingEmailChangeBodySpec(emailForm) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "emailFormMono.password"); + }); + assertNotChanged(); + + // try with an existing email + emailForm = form(); + emailForm.setNewEmail(ADMIN_EMAIL);; + tryRequestingEmailChangeBodySpec(emailForm) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "emailFormMono.newEmail"); + }); + assertNotChanged(); + + verify(mailSender, never()).send(any()); + } + + + private BodySpec tryRequestingEmailChangeBodySpec(TestEmailForm form) { + + //@formatter:off + return CLIENT.post().uri(BASE_URI + "/users/{id}/email-change-request", UNVERIFIED_USER_ID) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(UNVERIFIED_USER_ID)) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form), TestEmailForm.class) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY) + .expectBody(TestErrorResponse.class); + //@formatter:on + } + + + private void assertNotChanged() { + User updatedUser = mongoTemplate.findById(UNVERIFIED_USER_ID, User.class).block(); + assertNull(updatedUser.getNewEmail()); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ResendVerificationMailTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ResendVerificationMailTests.java new file mode 100644 index 00000000..6c1039e9 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ResendVerificationMailTests.java @@ -0,0 +1,96 @@ +package com.naturalprogrammer.spring.lemondemo; + +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class ResendVerificationMailTests extends AbstractTests { + + @Test + void testResendVerificationMail() { + + resendVerificationMail(UNVERIFIED_USER_ID, UNVERIFIED_USER_ID) + .expectStatus().isNoContent(); + + verify(mailSender).send(any()); + } + + + @Test + void testAdminResendVerificationMailOtherUser() { + + resendVerificationMail(UNVERIFIED_USER_ID, ADMIN_ID) + .expectStatus().isNoContent(); + } + + + @Test + void testBadAdminResendVerificationMailOtherUser() { + + resendVerificationMail(UNVERIFIED_USER_ID, UNVERIFIED_ADMIN_ID) + .expectStatus().isForbidden(); + + resendVerificationMail(UNVERIFIED_USER_ID, BLOCKED_ADMIN_ID) + .expectStatus().isForbidden(); + + verify(mailSender, never()).send(any()); + } + + + @Test + void testResendVerificationMailUnauthenticated() { + + CLIENT.post().uri(BASE_URI + "/users/{id}/resend-verification-mail", UNVERIFIED_USER_ID) + .exchange() + .expectStatus().isForbidden(); + + verify(mailSender, never()).send(any()); + } + + + @Test + void testResendVerificationMailAlreadyVerified() { + + resendVerificationMail(USER_ID, USER_ID) + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + verify(mailSender, never()).send(any()); + } + + + @Test + void testResendVerificationMailOtherUser() { + + resendVerificationMail(UNVERIFIED_USER_ID, USER_ID) + .expectStatus().isForbidden(); + + verify(mailSender, never()).send(any()); + } + + + @Test + void testResendVerificationMailNonExistingUser() { + + resendVerificationMail(ObjectId.get(), ADMIN_ID) + .expectStatus().isNotFound(); + + verify(mailSender, never()).send(any()); + } + + + private ResponseSpec resendVerificationMail(ObjectId userId, ObjectId loggedInId) { + + return CLIENT.post().uri(BASE_URI + "/users/{id}/resend-verification-mail", userId) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(loggedInId)) + .exchange(); + + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ResetPasswordTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ResetPasswordTests.java new file mode 100644 index 00000000..9d8276ec --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/ResetPasswordTests.java @@ -0,0 +1,82 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.security.GreenTokenService; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.dto.TestResetPasswordForm; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +import reactor.core.publisher.Mono; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; + +class ResetPasswordTests extends AbstractTests { + + private String forgotPasswordCode; + + @Autowired + private GreenTokenService greenTokenService; + + @BeforeEach + public void setUp() { + + forgotPasswordCode = greenTokenService.createToken( + GreenTokenService.FORGOT_PASSWORD_AUDIENCE, + ADMIN_EMAIL, 60000L); + } + + @Test + void testResetPassword() throws Exception { + + final String NEW_PASSWORD = "newPassword!"; + + resetPassword(forgotPasswordCode, NEW_PASSWORD) + .expectStatus().isOk() + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody().jsonPath("$.id").isEqualTo(ADMIN_ID.toString()); + + // Ensure able to login with new password + testUtils.login(ADMIN_EMAIL, NEW_PASSWORD); + + // Repeating shouldn't work + resetPassword(forgotPasswordCode, NEW_PASSWORD) + .expectStatus().isUnauthorized(); + } + + @Test + void testResetPasswordInvalidData() throws Exception { + + // Wrong code + resetPassword("wrong-code", "abc99!") + .expectStatus().isUnauthorized(); + + // Blank password + resetPassword(forgotPasswordCode, "") + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // Invalid password + resetPassword(forgotPasswordCode, "abc") + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + + private ResponseSpec resetPassword(String forgotPasswordCode, String newPassword) { + + return CLIENT.post().uri(BASE_URI + "/reset-password") + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(form(forgotPasswordCode, newPassword)), TestResetPasswordForm.class) + .exchange(); + } + + private TestResetPasswordForm form(String code, String newPassword) { + + TestResetPasswordForm form = new TestResetPasswordForm(); + form.setCode(code); + form.setNewPassword(newPassword); + + return form; + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/SignupTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/SignupTests.java new file mode 100644 index 00000000..48b319af --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/SignupTests.java @@ -0,0 +1,112 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.domain.User; +import com.naturalprogrammer.spring.lemondemo.dto.TestErrorResponse; +import com.naturalprogrammer.spring.lemondemo.dto.TestLemonFieldError; +import com.naturalprogrammer.spring.lemondemo.dto.TestUser; +import com.naturalprogrammer.spring.lemondemo.dto.TestUserDto; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; + +class SignupTests extends AbstractTests { + + @Test + void testSignup() throws Exception { + + signup("user.foo@example.com", "user123", "User Foo") + .expectStatus().isCreated() + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody(TestUserDto.class) + .consumeWith(result -> { + TestUserDto userDto = result.getResponseBody(); + assertNotNull(userDto.getId()); + assertNull(userDto.getPassword()); + assertEquals("user.foo@example.com", userDto.getUsername()); + assertEquals(1, userDto.getRoles().size()); + assertTrue(userDto.getRoles().contains("UNVERIFIED")); + assertEquals("User Foo", userDto.getTag().getName()); + assertTrue(userDto.isUnverified()); + assertFalse(userDto.isBlocked()); + assertFalse(userDto.isAdmin()); + assertFalse(userDto.isGoodUser()); + assertFalse(userDto.isGoodAdmin()); + }); + + verify(mailSender).send(any()); + + // Ensure that password got encrypted + assertNotEquals("user123", + mongoTemplate.findOne(query(where("email").is("user.foo@example.com")), + User.class).block().getPassword()); + } + + @Test + void testSignupWithInvalidData() throws Exception { + + signup("abc", "user1", null) + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY) + .expectBody(TestErrorResponse.class) + .consumeWith(errorResponseResult -> { + assertErrors(errorResponseResult, + "userMono.email", + "userMono.email", + "userMono.password", + "userMono.name"); + + Collection errors = errorResponseResult.getResponseBody().getErrors(); + assertTrue(errors.stream() + .map(TestLemonFieldError::getCode).collect(Collectors.toSet()) + .containsAll(Arrays.asList( + "NotBlank", + "Size", + "Email"))); + + assertTrue(errors.stream() + .map(TestLemonFieldError::getMessage).collect(Collectors.toSet()) + .containsAll(Arrays.asList( + "Not a well formed email address", + "Name required", + "Email must be between 4 and 250 characters", + "Password must be between 6 and 50 characters"))); + }); + + verify(mailSender, never()).send(any()); + } + + @Test + void testSignupDuplicateEmail() throws Exception { + + signup(ADMIN_EMAIL, "user123", "User Foo") + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + verify(mailSender, never()).send(any()); + } + + private ResponseSpec signup(String email, String password, String name) { + + TestUser user = new TestUser(email, password, name); + + return CLIENT.post().uri(BASE_URI + "/users", UNVERIFIED_USER_ID) + .contentType(MediaType.APPLICATION_JSON) + .body(Mono.just(user), TestUser.class) + .exchange(); + } + +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/TestConfiguration.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/TestConfiguration.java new file mode 100644 index 00000000..a163c84d --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/TestConfiguration.java @@ -0,0 +1,14 @@ +package com.naturalprogrammer.spring.lemondemo; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.core.MongoTemplate; + +@Configuration +public class TestConfiguration { + + //@Bean + public MongoTemplate mongoTemplate(MongoDatabaseFactory mongoDbFactory) { + return new MongoTemplate(mongoDbFactory); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/UpdateUserTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/UpdateUserTests.java new file mode 100644 index 00000000..9d498563 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/UpdateUserTests.java @@ -0,0 +1,188 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemondemo.domain.User; +import com.naturalprogrammer.spring.lemondemo.dto.TestUserDto; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +import org.springframework.web.reactive.function.BodyInserters; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class UpdateUserTests extends AbstractTests { + + private static final String UPDATED_NAME = "Edited name"; + + @Value("classpath:/update-user/patch-update-user.json") + private Resource userPatch; + + @Value("classpath:/update-user/patch-admin-role.json") + private Resource userPatchAdminRole; + + @Value("classpath:/update-user/patch-null-name.json") + private Resource userPatchNullName; + + @Value("classpath:/update-user/patch-long-name.json") + private Resource userPatchLongName; + + /** + * A non-admin user should be able to update his own name, + * but changes in roles should be skipped. + * The name of security principal object should also + * change in the process. + */ + @Test + void testUpdateSelf() { + + updateUser(UNVERIFIED_USER_ID, UNVERIFIED_USER_ID, userPatch) + .expectStatus().isOk() + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody(TestUserDto.class) + .consumeWith(result -> { + + TestUserDto userDto = result.getResponseBody(); + + assertEquals(UNVERIFIED_USER_ID, userDto.getId()); + assertEquals(UNVERIFIED_USER_EMAIL, userDto.getUsername()); + assertEquals(UPDATED_NAME, userDto.getTag().getName()); + assertEquals(1, userDto.getRoles().size()); + assertTrue(userDto.getRoles().contains(UserUtils.Role.UNVERIFIED)); + }); + + User user = mongoTemplate.findById(UNVERIFIED_USER_ID, User.class).block(); + + // Ensure that data changed properly + assertEquals(UNVERIFIED_USER_EMAIL, user.getEmail()); + assertEquals(1, user.getRoles().size()); + assertTrue(user.getRoles().contains(UserUtils.Role.UNVERIFIED)); + assertEquals(1L, user.getVersion().longValue()); + + // Version mismatch + updateUser(UNVERIFIED_USER_ID, UNVERIFIED_USER_ID, userPatch) + .expectStatus().isEqualTo(HttpStatus.CONFLICT); + } + + + /** + * A good ADMIN should be able to update another user's name and roles. + * The name of security principal object should NOT change in the process, + * and the verification code should get set/unset on addition/deletion of + * the UNVERIFIED role. + */ + @Test + void testGoodAdminCanUpdateOther() { + + updateUser(UNVERIFIED_USER_ID, ADMIN_ID, userPatch) + .expectStatus().isOk() + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody(TestUserDto.class) + .consumeWith(result -> { + + TestUserDto userDto = result.getResponseBody(); + + assertEquals(UNVERIFIED_USER_ID, userDto.getId()); + assertEquals(UNVERIFIED_USER_EMAIL, userDto.getUsername()); + assertEquals(UPDATED_NAME, userDto.getTag().getName()); + assertEquals(1, userDto.getRoles().size()); + assertTrue(userDto.getRoles().contains(UserUtils.Role.ADMIN)); + }); + + User user = mongoTemplate.findById(UNVERIFIED_USER_ID, User.class).block(); + + // Ensure that data changed properly + assertEquals(UNVERIFIED_USER_EMAIL, user.getEmail()); + assertEquals(1, user.getRoles().size()); + assertTrue(user.getRoles().contains(UserUtils.Role.ADMIN)); + } + + /** + * Providing an unknown id should return 404. + */ + @Test + void testUpdateUnknownId() { + + updateUser(ObjectId.get(), ADMIN_ID, userPatch) + .expectStatus().isNotFound(); + } + + /** + * A non-admin trying to update the name and roles of another user should throw exception + */ + @Test + void testUpdateAnotherUser() { + + updateUser(ADMIN_ID, UNVERIFIED_USER_ID, userPatch) + .expectStatus().isForbidden(); + } + + /** + * A bad ADMIN trying to update the name and roles of another user should throw exception + */ + @Test + void testBadAdminUpdateAnotherUser() { + + updateUser(UNVERIFIED_USER_ID, UNVERIFIED_ADMIN_ID, userPatch) + .expectStatus().isForbidden(); + + updateUser(UNVERIFIED_USER_ID, BLOCKED_ADMIN_ID, userPatch) + .expectStatus().isForbidden(); + } + + /** + * A good ADMIN should not be able to change his own roles + */ + @Test + void goodAdminCanNotUpdateSelfRoles() { + + updateUser(ADMIN_ID, ADMIN_ID, userPatchAdminRole) + .expectStatus().isOk() + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody(TestUserDto.class) + .consumeWith(result -> { + + TestUserDto userDto = result.getResponseBody(); + + assertEquals(UPDATED_NAME, userDto.getTag().getName()); + assertEquals(1, userDto.getRoles().size()); + assertTrue(userDto.getRoles().contains(UserUtils.Role.ADMIN)); + }); + + User user = mongoTemplate.findById(ADMIN_ID, User.class).block(); + + assertEquals(1, user.getRoles().size()); + } + + /** + * Invalid name + */ + @Test + void testUpdateUserInvalidNewName() { + + // Null name + updateUser(UNVERIFIED_USER_ID, ADMIN_ID, userPatchNullName) + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // Too long name + updateUser(UNVERIFIED_USER_ID, UNVERIFIED_USER_ID, userPatchLongName) + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + + private ResponseSpec updateUser(ObjectId userId, ObjectId loggedInId, Resource patch) { + + return CLIENT.patch().uri(BASE_URI + "/users/{id}", userId) + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, TOKENS.get(loggedInId)) + .body(BodyInserters.fromResource(patch)) + .exchange(); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/VerificationTests.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/VerificationTests.java new file mode 100644 index 00000000..51290c0f --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/VerificationTests.java @@ -0,0 +1,103 @@ +package com.naturalprogrammer.spring.lemondemo; + +import com.naturalprogrammer.spring.lemon.commons.security.GreenTokenService; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemondemo.dto.TestUserDto; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +import static com.naturalprogrammer.spring.lemondemo.MyTestUtils.*; +import static com.naturalprogrammer.spring.lemondemo.controllers.MyController.BASE_URI; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.web.reactive.function.BodyInserters.fromFormData; + +class VerificationTests extends AbstractTests { + + private String verificationCode; + + @Autowired + private GreenTokenService greenTokenService; + + @BeforeEach + public void setUp() { + + verificationCode = greenTokenService.createToken(GreenTokenService.VERIFY_AUDIENCE, + UNVERIFIED_USER_ID.toString(), 60000L, + LecUtils.mapOf("email", UNVERIFIED_USER_EMAIL)); + } + + @Test + void testEmailVerification() { + + emailVerification(UNVERIFIED_USER_ID, verificationCode) + .expectStatus().isOk() + .expectHeader().exists(LecUtils.TOKEN_RESPONSE_HEADER_NAME) + .expectBody(TestUserDto.class) + .consumeWith(result -> { + + TestUserDto userDto = result.getResponseBody(); + assert userDto != null; + + assertEquals(UNVERIFIED_USER_ID, userDto.getId()); + assertEquals(0, userDto.getRoles().size()); + assertTrue(userDto.isGoodUser()); + assertFalse(userDto.isUnverified()); + }); + + // Already verified + emailVerification(UNVERIFIED_USER_ID, verificationCode) + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + } + + @Test + void testEmailVerificationNonExistingUser() throws Exception { + + emailVerification(ObjectId.get(), verificationCode) + .expectStatus().isNotFound(); + } + + @Test + void testEmailVerificationWrongToken() throws Exception { + + // null token + CLIENT.post().uri(BASE_URI + "/users/{id}/verification", UNVERIFIED_USER_ID) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // blank token + emailVerification(UNVERIFIED_USER_ID, "") + .expectStatus().isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY); + + // Wrong audience + String token = greenTokenService.createToken("wrong-audience", + UNVERIFIED_USER_ID.toString(), 60000L, + LecUtils.mapOf("email", UNVERIFIED_USER_EMAIL)); + emailVerification(UNVERIFIED_USER_ID, token) + .expectStatus().isUnauthorized(); + + // Wrong email + token = greenTokenService.createToken(GreenTokenService.VERIFY_AUDIENCE, + UNVERIFIED_USER_ID.toString(), 60000L, + LecUtils.mapOf("email", "wrong.email@example.com")); + emailVerification(UNVERIFIED_USER_ID, token) + .expectStatus().isForbidden(); + + // expired token + token = greenTokenService.createToken(GreenTokenService.VERIFY_AUDIENCE, + UNVERIFIED_USER_ID.toString(), 1L, + LecUtils.mapOf("email", UNVERIFIED_USER_EMAIL)); + emailVerification(UNVERIFIED_USER_ID, token) + .expectStatus().isUnauthorized(); + } + + private ResponseSpec emailVerification(ObjectId userId, String code) { + + return CLIENT.post().uri(BASE_URI + "/users/{id}/verification", userId) + .body(fromFormData("code", code)) + .exchange(); + } +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestEmailForm.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestEmailForm.java new file mode 100644 index 00000000..3cdd8297 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestEmailForm.java @@ -0,0 +1,11 @@ +package com.naturalprogrammer.spring.lemondemo.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class TestEmailForm { + + private String newEmail; + private String password; +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestErrorResponse.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestErrorResponse.java new file mode 100644 index 00000000..4fb59bbb --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestErrorResponse.java @@ -0,0 +1,20 @@ +package com.naturalprogrammer.spring.lemondemo.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Collection; + +/** + * Error DTO, to be sent as response body + * in case of errors + */ +@Getter @Setter +public class TestErrorResponse { + + private String exception; + private String error; + private String message; + private Integer status; // We'd need it as integer in JSON serialization + private Collection errors; +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestLemonFieldError.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestLemonFieldError.java new file mode 100644 index 00000000..2aa8da3d --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestLemonFieldError.java @@ -0,0 +1,13 @@ +package com.naturalprogrammer.spring.lemondemo.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter @Setter @NoArgsConstructor +public class TestLemonFieldError { + + private String field; + private String code; + private String message; +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestResetPasswordForm.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestResetPasswordForm.java new file mode 100644 index 00000000..9f8191c9 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestResetPasswordForm.java @@ -0,0 +1,11 @@ +package com.naturalprogrammer.spring.lemondemo.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class TestResetPasswordForm { + + private String code; + private String newPassword; +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestToken.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestToken.java new file mode 100644 index 00000000..c537f60b --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestToken.java @@ -0,0 +1,10 @@ +package com.naturalprogrammer.spring.lemondemo.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class TestToken { + + private String token; +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestUser.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestUser.java new file mode 100644 index 00000000..448b3711 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestUser.java @@ -0,0 +1,11 @@ +package com.naturalprogrammer.spring.lemondemo.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter @AllArgsConstructor +public class TestUser { + + private String email, password, name; +} diff --git a/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestUserDto.java b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestUserDto.java new file mode 100644 index 00000000..d7bafc59 --- /dev/null +++ b/lemon-demo-reactive/src/test/java/com/naturalprogrammer/spring/lemondemo/dto/TestUserDto.java @@ -0,0 +1,25 @@ +package com.naturalprogrammer.spring.lemondemo.dto; + +import com.naturalprogrammer.spring.lemondemo.domain.User.Tag; +import lombok.Getter; +import lombok.Setter; +import org.bson.types.ObjectId; + +import java.util.HashSet; +import java.util.Set; + +@Getter @Setter +public class TestUserDto { + + private ObjectId id; + private String username; + private String password; + private Set roles = new HashSet(); + private Tag tag; + + private boolean unverified = false; + private boolean blocked = false; + private boolean admin = false; + private boolean goodUser = false; + private boolean goodAdmin = false; +} diff --git a/lemon-demo-reactive/src/test/resources/update-user/patch-admin-role.json b/lemon-demo-reactive/src/test/resources/update-user/patch-admin-role.json new file mode 100644 index 00000000..541a71f9 --- /dev/null +++ b/lemon-demo-reactive/src/test/resources/update-user/patch-admin-role.json @@ -0,0 +1,6 @@ +[ + {"op": "replace", "path": "/name", "value": "Edited name"}, + {"op": "replace", "path": "/email", "value": "should.not@get.replaced"}, + {"op": "replace", "path": "/roles", "value": []}, + {"op": "replace", "path": "/version", "value": 0} +] diff --git a/lemon-demo-reactive/src/test/resources/update-user/patch-long-name.json b/lemon-demo-reactive/src/test/resources/update-user/patch-long-name.json new file mode 100644 index 00000000..62e0d730 --- /dev/null +++ b/lemon-demo-reactive/src/test/resources/update-user/patch-long-name.json @@ -0,0 +1,4 @@ +[ + {"op": "replace", "path": "/name", "value": "A123456789A123456789A123456789A123456789A123456789A123456789A123456789"}, + {"op": "replace", "path": "/version", "value": 0} +] diff --git a/lemon-demo-reactive/src/test/resources/update-user/patch-null-name.json b/lemon-demo-reactive/src/test/resources/update-user/patch-null-name.json new file mode 100644 index 00000000..9fc08cb5 --- /dev/null +++ b/lemon-demo-reactive/src/test/resources/update-user/patch-null-name.json @@ -0,0 +1,4 @@ +[ + {"op": "replace", "path": "/name", "value": null}, + {"op": "replace", "path": "/version", "value": 0} +] diff --git a/lemon-demo-reactive/src/test/resources/update-user/patch-update-user.json b/lemon-demo-reactive/src/test/resources/update-user/patch-update-user.json new file mode 100644 index 00000000..228cc7c5 --- /dev/null +++ b/lemon-demo-reactive/src/test/resources/update-user/patch-update-user.json @@ -0,0 +1,6 @@ +[ + {"op": "replace", "path": "/name", "value": "Edited name"}, + {"op": "replace", "path": "/email", "value": "should.not@get.replaced"}, + {"op": "replace", "path": "/roles", "value": ["ADMIN"]}, + {"op": "replace", "path": "/version", "value": 0} +] diff --git a/pom.xml b/pom.xml index 293011e7..676875c5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,95 +1,192 @@ - - - 4.0.0 - - com.naturalprogrammer - spring-lemon - 1.0.0.M4 - pom - - spring-lemon - Helper library for Spring Boot Web Applications - - - org.springframework.boot - spring-boot-starter-parent - 2.0.3.RELEASE - - - - - spring-lemon-exceptions - spring-lemon-commons - spring-lemon-jpa - spring-lemon-reactive - lemon-demo-jpa - lemon-demo-reactive - - - - - - org.projectlombok - lombok - true - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - - - org.apache.maven.plugins - maven-source-plugin - - - attach-sources - - jar - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - false - - - - attach-javadocs - - jar - - - - - - - - - - - jitpack.io - https://jitpack.io - - - - - UTF-8 - UTF-8 - 1.8 - - - + + + 4.0.0 + + com.naturalprogrammer + spring-lemon + 1.0.2 + pom + + ${project.groupId}:${project.artifactId} + Helper library for Spring Boot Web Applications + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + org.springframework.boot + spring-boot-starter-parent + 2.7.1 + + + + + spring-lemon-exceptions + spring-lemon-commons + spring-lemon-commons-web + spring-lemon-commons-reactive + spring-lemon-commons-jpa + spring-lemon-commons-mongo + spring-lemon-jpa + spring-lemon-reactive + lemon-demo-jpa + lemon-demo-reactive + + + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.8 + true + + ossrh + https://s01.oss.sonatype.org/ + true + + + + + + + + UTF-8 + UTF-8 + 1.8 + 0.8.7 + + + + + + + spring-snapshots + https://repo.spring.io/snapshot + true + + + spring-milestones + https://repo.spring.io/milestone + + + + + spring-snapshots + https://repo.spring.io/snapshot + + + spring-milestones + https://repo.spring.io/milestone + + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots/ + + + + + + release + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + + + + + diff --git a/spring-lemon-commons-jpa/pom.xml b/spring-lemon-commons-jpa/pom.xml new file mode 100644 index 00000000..a205789e --- /dev/null +++ b/spring-lemon-commons-jpa/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + com.naturalprogrammer.spring-lemon + spring-lemon-commons-jpa + jar + + ${project.groupId}:${project.artifactId} + Spring Lemon Commons JPA + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + com.naturalprogrammer + spring-lemon + 1.0.2 + + + + + + com.naturalprogrammer.spring-lemon + spring-lemon-commons-web + ${project.version} + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + javax.xml.bind + jaxb-api + + + + + diff --git a/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LecjUtils.java b/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LecjUtils.java new file mode 100644 index 00000000..93625c0a --- /dev/null +++ b/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LecjUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsjpa; + +import com.naturalprogrammer.spring.lemon.exceptions.VersionException; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.io.Serializable; +import java.util.Objects; + +public class LecjUtils { + + /** + * A convenient method for running code + * after successful database commit. + * + * @param runnable + */ + public static void afterCommit(Runnable runnable) { + + TransactionSynchronizationManager.registerSynchronization( + new TransactionSynchronizationAdapter() { + @Override + public void afterCommit() { + + runnable.run(); + } + }); + } + + + /** + * Throws a VersionException if the versions of the + * given entities aren't same. + * + * @param original + * @param updated + */ + public static + void ensureCorrectVersion(LemonEntity original, LemonEntity updated) { + + if (!Objects.equals(original.getVersion(), updated.getVersion())) + throw new VersionException(original.getClass().getSimpleName(), original.getId().toString()); + } +} diff --git a/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LemonCommonsJpaAutoConfiguration.java b/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LemonCommonsJpaAutoConfiguration.java new file mode 100644 index 00000000..a055fc66 --- /dev/null +++ b/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LemonCommonsJpaAutoConfiguration.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsjpa; + +import com.naturalprogrammer.spring.lemon.commonsweb.LemonCommonsWebAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EnableTransactionManagement +@EnableJpaAuditing +@AutoConfigureBefore({LemonCommonsWebAutoConfiguration.class}) +public class LemonCommonsJpaAutoConfiguration { + +} diff --git a/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LemonEntity.java b/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LemonEntity.java new file mode 100644 index 00000000..41e638c3 --- /dev/null +++ b/spring-lemon-commons-jpa/src/main/java/com/naturalprogrammer/spring/lemon/commonsjpa/LemonEntity.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsjpa; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.naturalprogrammer.spring.lemon.commons.security.PermissionEvaluatorEntity; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.AbstractPersistable; + +import javax.persistence.MappedSuperclass; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.Version; +import java.io.Serializable; +import java.util.Date; + +/** + * Base class for all entities. + * + * @author Sanjay Patel + */ +@MappedSuperclass +@Getter @Setter +@JsonIgnoreProperties({ "createdById", "lastModifiedById", "createdDate", "lastModifiedDate", "new" }) +public class LemonEntity extends AbstractPersistable implements PermissionEvaluatorEntity { + + private static final long serialVersionUID = -8151190931948396443L; + + @CreatedBy + private ID createdById; + + @CreatedDate + @Temporal(TemporalType.TIMESTAMP) + private Date createdDate; + + @LastModifiedBy + private ID lastModifiedById; + + @LastModifiedDate + @Temporal(TemporalType.TIMESTAMP) + private Date lastModifiedDate; + + @Version + private Long version; + + /** + * Whether the given user has the given permission for + * this entity. Override this method where you need. + */ + @Override + public boolean hasPermission(UserDto user, String permission) { + return false; + } + +} diff --git a/spring-lemon-jpa/src/main/resources/META-INF/orm.xml b/spring-lemon-commons-jpa/src/main/resources/META-INF/orm.xml similarity index 100% rename from spring-lemon-jpa/src/main/resources/META-INF/orm.xml rename to spring-lemon-commons-jpa/src/main/resources/META-INF/orm.xml diff --git a/spring-lemon-commons-jpa/src/main/resources/META-INF/spring.factories b/spring-lemon-commons-jpa/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..c4284f64 --- /dev/null +++ b/spring-lemon-commons-jpa/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.naturalprogrammer.spring.lemon.commonsjpa.LemonCommonsJpaAutoConfiguration \ No newline at end of file diff --git a/spring-lemon-commons-mongo/pom.xml b/spring-lemon-commons-mongo/pom.xml new file mode 100644 index 00000000..6c5f2b9f --- /dev/null +++ b/spring-lemon-commons-mongo/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + com.naturalprogrammer.spring-lemon + spring-lemon-commons-mongo + jar + + ${project.groupId}:${project.artifactId} + Spring Lemon Commons MongoDB + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + com.naturalprogrammer + spring-lemon + 1.0.2 + + + + + + com.naturalprogrammer.spring-lemon + spring-lemon-commons-reactive + ${project.version} + + + + org.springframework.boot + spring-boot-starter-data-mongodb-reactive + + + + io.projectreactor + reactor-test + test + + + + + diff --git a/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/AbstractDocument.java b/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/AbstractDocument.java new file mode 100644 index 00000000..4b989291 --- /dev/null +++ b/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/AbstractDocument.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsmongo; + +import com.naturalprogrammer.spring.lemon.commons.security.PermissionEvaluatorEntity; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.*; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.io.Serializable; +import java.util.Date; + +@Document +@Getter @Setter +public abstract class AbstractDocument implements PermissionEvaluatorEntity { + + @Id + protected ID id; + + @CreatedBy + private ID createdBy; + + @CreatedDate + private Date createdDate; + + @LastModifiedBy + private ID lastModifiedBy; + + @LastModifiedDate + private Date lastModifiedDate; + + @Version + private Long version; + + /** + * Whether the given user has the given permission for + * this entity. Override this method where you need. + */ + @Override + public boolean hasPermission(UserDto currentUser, String permission) { + + return false; + } + +} diff --git a/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/LecmUtils.java b/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/LecmUtils.java new file mode 100644 index 00000000..c0843fd2 --- /dev/null +++ b/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/LecmUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsmongo; + +import com.naturalprogrammer.spring.lemon.exceptions.VersionException; + +import java.io.Serializable; +import java.util.Objects; + +public class LecmUtils { + + /** + * Throws a VersionException if the versions of the + * given entities aren't same. + * + * @param original + * @param updated + */ + public static + void ensureCorrectVersion(AbstractDocument original, AbstractDocument updated) { + + if (!Objects.equals(original.getVersion(), updated.getVersion())) + throw new VersionException(original.getClass().getSimpleName(), original.getId().toString()); + } + +} diff --git a/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/LemonCommonsMongoAutoConfiguration.java b/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/LemonCommonsMongoAutoConfiguration.java new file mode 100644 index 00000000..2c1e730c --- /dev/null +++ b/spring-lemon-commons-mongo/src/main/java/com/naturalprogrammer/spring/lemon/commonsmongo/LemonCommonsMongoAutoConfiguration.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsmongo; + +import com.naturalprogrammer.spring.lemon.commonsreactive.LemonCommonsReactiveAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.EnableMongoAuditing; + +@Configuration +@EnableMongoAuditing +@AutoConfigureBefore({ + MongoReactiveAutoConfiguration.class, + LemonCommonsReactiveAutoConfiguration.class}) +public class LemonCommonsMongoAutoConfiguration { + +} diff --git a/spring-lemon-commons-mongo/src/main/resources/META-INF/spring.factories b/spring-lemon-commons-mongo/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..a567becb --- /dev/null +++ b/spring-lemon-commons-mongo/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.naturalprogrammer.spring.lemon.commonsmongo.LemonCommonsMongoAutoConfiguration \ No newline at end of file diff --git a/spring-lemon-commons-reactive/pom.xml b/spring-lemon-commons-reactive/pom.xml new file mode 100644 index 00000000..f0623243 --- /dev/null +++ b/spring-lemon-commons-reactive/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + com.naturalprogrammer.spring-lemon + spring-lemon-commons-reactive + jar + + ${project.groupId}:${project.artifactId} + >Helper reactive commons library for Spring Boot REST APIs + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + com.naturalprogrammer + spring-lemon + 1.0.2 + + + + + + com.naturalprogrammer.spring-lemon + spring-lemon-commons + ${project.version} + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.mongodb + bson + + + + org.springframework.boot + spring-boot-starter-test + test + + + + io.projectreactor + reactor-test + test + + + + + diff --git a/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/LemonCommonsReactiveAutoConfiguration.java b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/LemonCommonsReactiveAutoConfiguration.java new file mode 100644 index 00000000..ae230245 --- /dev/null +++ b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/LemonCommonsReactiveAutoConfiguration.java @@ -0,0 +1,155 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsreactive; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import com.naturalprogrammer.spring.lemon.commons.LemonCommonsAutoConfiguration; +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commonsreactive.exceptions.LemonReactiveErrorAttributes; +import com.naturalprogrammer.spring.lemon.commonsreactive.exceptions.handlers.VersionExceptionHandler; +import com.naturalprogrammer.spring.lemon.commonsreactive.security.LemonCommonsReactiveSecurityConfig; +import com.naturalprogrammer.spring.lemon.commonsreactive.security.LemonCorsConfigurationSource; +import com.naturalprogrammer.spring.lemon.commonsreactive.security.LemonReactiveAuditorAware; +import com.naturalprogrammer.spring.lemon.commonsreactive.util.LecrUtils; +import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponseComposer; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.types.ObjectId; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration; +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.access.expression.AbstractSecurityExpressionHandler; +import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.web.cors.reactive.CorsConfigurationSource; + +import java.io.Serializable; + +@Configuration +@EnableReactiveMethodSecurity +@AutoConfigureBefore({ + WebFluxAutoConfiguration.class, + ErrorWebFluxAutoConfiguration.class, + ReactiveSecurityAutoConfiguration.class, + LemonCommonsAutoConfiguration.class}) +@ComponentScan(basePackageClasses=VersionExceptionHandler.class) +public class LemonCommonsReactiveAutoConfiguration { + + private static final Log log = LogFactory.getLog(LemonCommonsReactiveAutoConfiguration.class); + + public LemonCommonsReactiveAutoConfiguration() { + log.info("Created"); + } + + + /** + * Configures an Error Attributes if missing + */ + @Bean + @ConditionalOnMissingBean(ErrorAttributes.class) + public + ErrorAttributes errorAttributes(ErrorResponseComposer errorResponseComposer) { + + log.info("Configuring LemonErrorAttributes"); + return new LemonReactiveErrorAttributes(errorResponseComposer); + } + + + @Bean + @ConditionalOnMissingBean(LemonCommonsReactiveSecurityConfig.class) + public LemonCommonsReactiveSecurityConfig lemonReactiveSecurityConfig(BlueTokenService blueTokenService) { + + log.info("Configuring LemonCommonsReactiveSecurityConfig ..."); + return new LemonCommonsReactiveSecurityConfig(blueTokenService); + } + + + /** + * Configures SecurityWebFilterChain if missing + */ + @Bean + public SecurityWebFilterChain springSecurityFilterChain( + ServerHttpSecurity http, + LemonCommonsReactiveSecurityConfig securityConfig, + AbstractSecurityExpressionHandler expressionHandler, + PermissionEvaluator permissionEvaluator) { + + log.info("Configuring SecurityWebFilterChain ..."); + expressionHandler.setPermissionEvaluator(permissionEvaluator); + return securityConfig.springSecurityFilterChain(http); + } + + + /** + * Configures LemonCorsConfig if missing and lemon.cors.allowed-origins is provided + */ + @Bean + @ConditionalOnProperty(name="lemon.cors.allowed-origins") + @ConditionalOnMissingBean(CorsConfigurationSource.class) + public LemonCorsConfigurationSource corsConfigurationSource(LemonProperties properties) { + + log.info("Configuring LemonCorsConfigurationSource"); + return new LemonCorsConfigurationSource(properties); + } + + + @Bean + public SimpleModule objectIdModule() { + + SimpleModule module = new SimpleModule(); + module.addSerializer(ObjectId.class, new ToStringSerializer()); + + return module; + } + + + /** + * Configures an Auditor Aware if missing + */ + @Bean + @ConditionalOnMissingBean(AuditorAware.class) + public + AuditorAware auditorAware() { + + log.info("Configuring LemonAuditorAware"); + return new LemonReactiveAuditorAware(); + } + + + /** + * Configures LeeUtils + */ + @Bean + public LecrUtils lecrUtils(LexUtils lexUtils) { + + log.info("Configuring LecrUtils"); + return new LecrUtils(); + } +} diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/exceptions/LemonReactiveErrorAttributes.java b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/LemonReactiveErrorAttributes.java similarity index 60% rename from spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/exceptions/LemonReactiveErrorAttributes.java rename to spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/LemonReactiveErrorAttributes.java index 57793866..ac23cf62 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/exceptions/LemonReactiveErrorAttributes.java +++ b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/LemonReactiveErrorAttributes.java @@ -1,13 +1,30 @@ -package com.naturalprogrammer.spring.lemonreactive.exceptions; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.util.Map; +package com.naturalprogrammer.spring.lemon.commonsreactive.exceptions; +import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponseComposer; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; import org.springframework.web.reactive.function.server.ServerRequest; -import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponseComposer; +import java.util.Map; public class LemonReactiveErrorAttributes extends DefaultErrorAttributes { @@ -26,9 +43,9 @@ public LemonReactiveErrorAttributes(ErrorResponseComposer errorResponseCompos @Override public Map getErrorAttributes(ServerRequest request, - boolean includeStackTrace) { + ErrorAttributeOptions options) { - Map errorAttributes = super.getErrorAttributes(request, includeStackTrace); + Map errorAttributes = super.getErrorAttributes(request, options); addLemonErrorDetails(errorAttributes, request); return errorAttributes; } @@ -42,12 +59,13 @@ protected void addLemonErrorDetails( Throwable ex = getError(request); - errorAttributes.put("exception", ex.getClass().getSimpleName()); - errorResponseComposer.compose((T)ex).ifPresent(errorResponse -> { // check for nulls - errorResponse may have left something for the DefaultErrorAttributes + if (errorResponse.getExceptionId() != null) + errorAttributes.put("exceptionId", errorResponse.getExceptionId()); + if (errorResponse.getMessage() != null) errorAttributes.put("message", errorResponse.getMessage()); @@ -61,5 +79,8 @@ protected void addLemonErrorDetails( if (errorResponse.getErrors() != null) errorAttributes.put("errors", errorResponse.getErrors()); }); + + if (errorAttributes.get("exceptionId") == null) + errorAttributes.put("exceptionId", LexUtils.getExceptionId(ex)); } } diff --git a/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/handlers/InsufficientAuthenticationExceptionHandler.java b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/handlers/InsufficientAuthenticationExceptionHandler.java new file mode 100644 index 00000000..4e98c085 --- /dev/null +++ b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/handlers/InsufficientAuthenticationExceptionHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsreactive.exceptions.handlers; + +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.stereotype.Component; + +@Component +@Order(Ordered.LOWEST_PRECEDENCE) +public class InsufficientAuthenticationExceptionHandler extends AbstractExceptionHandler { + + public InsufficientAuthenticationExceptionHandler() { + super(InsufficientAuthenticationException.class); + log.info("Created"); + } + + @Override + public HttpStatus getStatus(InsufficientAuthenticationException ex) { + return HttpStatus.FORBIDDEN; + } +} diff --git a/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/handlers/VersionExceptionHandler.java b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/handlers/VersionExceptionHandler.java new file mode 100644 index 00000000..b596faf2 --- /dev/null +++ b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/exceptions/handlers/VersionExceptionHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsreactive.exceptions.handlers; + +import com.naturalprogrammer.spring.lemon.exceptions.VersionException; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +@Component +@Order(Ordered.LOWEST_PRECEDENCE) +public class VersionExceptionHandler extends AbstractExceptionHandler { + + public VersionExceptionHandler() { + + super(VersionException.class); + log.info("Created"); + } + + @Override + public HttpStatus getStatus(VersionException ex) { + return HttpStatus.CONFLICT; + } +} \ No newline at end of file diff --git a/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonCommonsReactiveSecurityConfig.java b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonCommonsReactiveSecurityConfig.java new file mode 100644 index 00000000..93783938 --- /dev/null +++ b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonCommonsReactiveSecurityConfig.java @@ -0,0 +1,147 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsreactive.security; + +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import com.nimbusds.jwt.JWTClaimsSet; +import lombok.AllArgsConstructor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; +import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +public class LemonCommonsReactiveSecurityConfig { + + private static final Log log = LogFactory.getLog(LemonCommonsReactiveSecurityConfig.class); + + protected BlueTokenService blueTokenService; + + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + + log.info("Configuring SecurityWebFilterChain ..."); + + formLogin(http); // Configure form login + authorizeExchange(http); // configure authorization + oauth2Login(http); // configure OAuth2 login + + return http + .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) + .exceptionHandling() + .accessDeniedHandler((exchange, exception) -> Mono.error(exception)) + .authenticationEntryPoint((exchange, exception) -> Mono.error(exception)) + .and() + .cors() + .and() + .csrf().disable() + .addFilterAt(tokenAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION) + .logout().disable() + .build(); + } + + /** + * Override this to configure oauth2 Login + */ + protected void oauth2Login(ServerHttpSecurity http) { + + // Bypass here. OAuth2 login is needed only in the auth service + } + + /** + * Override this to configure authorization + */ + protected void authorizeExchange(ServerHttpSecurity http) { + + http.authorizeExchange() + .anyExchange().permitAll(); + } + + + /** + * Configures form login + */ + protected void formLogin(ServerHttpSecurity http) { + + // Bypass here. Form login is needed only in the auth service + } + + + protected AuthenticationWebFilter tokenAuthenticationFilter() { + + AuthenticationWebFilter filter = new AuthenticationWebFilter(tokenAuthenticationManager()); + filter.setServerAuthenticationConverter(tokenAuthenticationConverter()); + filter.setAuthenticationFailureHandler((exchange, exception) -> Mono.error(exception)); + + return filter; + } + + protected ReactiveAuthenticationManager tokenAuthenticationManager() { + + return authentication -> { + + log.debug("Authenticating with token ..."); + + String token = (String) authentication.getCredentials(); + + JWTClaimsSet claims = blueTokenService.parseToken(token, BlueTokenService.AUTH_AUDIENCE); + + UserDto userDto = LecUtils.getUserDto(claims); + + Mono userDtoMono = userDto == null ? + fetchUserDto(claims) : Mono.just(userDto); + + return userDtoMono.map(LemonPrincipal::new) + .doOnNext(LemonPrincipal::eraseCredentials) + .map(principal -> new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities())); + }; + } + + /** + * Default behaviour is to throw error. To be overridden in auth service. + */ + protected Mono fetchUserDto(JWTClaimsSet claims) { + return Mono.error(new AuthenticationCredentialsNotFoundException( + LexUtils.getMessage("com.naturalprogrammer.spring.userClaimAbsent"))); + } + + protected ServerAuthenticationConverter tokenAuthenticationConverter() { + + return serverWebExchange -> { + + String authorization = serverWebExchange.getRequest() + .getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + + if(authorization == null || !authorization.startsWith(LecUtils.TOKEN_PREFIX)) + return Mono.empty(); + + return Mono.just(new UsernamePasswordAuthenticationToken(null, authorization.substring(LecUtils.TOKEN_PREFIX_LENGTH))); + }; + } +} diff --git a/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonCorsConfigurationSource.java b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonCorsConfigurationSource.java new file mode 100644 index 00000000..1cb7fad7 --- /dev/null +++ b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonCorsConfigurationSource.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsreactive.security; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.LemonProperties.Cors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.reactive.CorsConfigurationSource; +import org.springframework.web.server.ServerWebExchange; + +import java.util.Arrays; + +/** + * CORS Configuration + */ +public class LemonCorsConfigurationSource implements CorsConfigurationSource { + + private static final Log log = LogFactory.getLog(LemonCorsConfigurationSource.class); + + private Cors cors; + + public LemonCorsConfigurationSource(LemonProperties properties) { + + this.cors = properties.getCors(); + log.info("Created"); + } + + @Override + public CorsConfiguration getCorsConfiguration(ServerWebExchange exchange) { + + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowCredentials(true); + config.setAllowedHeaders(Arrays.asList(cors.getAllowedHeaders())); + config.setAllowedMethods(Arrays.asList(cors.getAllowedMethods())); + config.setAllowedOrigins(Arrays.asList(cors.getAllowedOrigins())); + config.setExposedHeaders(Arrays.asList(cors.getExposedHeaders())); + config.setMaxAge(cors.getMaxAge()); + + return config; + } + +} diff --git a/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonReactiveAuditorAware.java b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonReactiveAuditorAware.java new file mode 100644 index 00000000..2999eee6 --- /dev/null +++ b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/security/LemonReactiveAuditorAware.java @@ -0,0 +1,53 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsreactive.security; + +import com.naturalprogrammer.spring.lemon.commons.domain.AbstractAuditorAware; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.Serializable; + +/** + * Needed for auto-filling of the + * AbstractAuditable columns of AbstractUser + * + * @author Sanjay Patel + */ +public class LemonReactiveAuditorAware +extends AbstractAuditorAware { + + private static final Log log = LogFactory.getLog(LemonReactiveAuditorAware.class); + + public LemonReactiveAuditorAware() { + log.info("Created"); + } + + @Override + protected UserDto currentUser() { + + // TODO: Can't return a mono, as below + // See this: https://jira.spring.io/browse/DATACMNS-1231 + // So, sorry, no reactive auditing until Spring Data supports it + // But, if using MongoDB, you could implement a ReactiveBeforeConvertCallback: + // https://juliuskrah.com/blog/2018/02/15/auditing-with-spring-data-jpa/#comment-4848839807 + + // return LecrUtils.currentUser(); + return null; + } +} diff --git a/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/util/LecrUtils.java b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/util/LecrUtils.java new file mode 100644 index 00000000..79ee6353 --- /dev/null +++ b/spring-lemon-commons-reactive/src/main/java/com/naturalprogrammer/spring/lemon/commonsreactive/util/LecrUtils.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsreactive.util; + +import com.github.fge.jsonpatch.JsonPatchException; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import org.springframework.http.HttpCookie; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.io.Serializable; +import java.util.Optional; + +/** + * Useful helper methods + * + * @author Sanjay Patel + */ +public class LecrUtils { + + private static Mono NOT_FOUND_MONO; + + @PostConstruct + public void postConstruct() { + NOT_FOUND_MONO = Mono.error(LexUtils.NOT_FOUND_EXCEPTION); + } + + public static Optional fetchCookie(ServerWebExchange exchange, String cookieName) { + return Optional.ofNullable(exchange.getRequest().getCookies().getFirst(cookieName)); + } + + /** + * Gets the current-user + */ + public static Mono> currentUser() { + + return ReactiveSecurityContextHolder.getContext() + .map(LecUtils::currentUser) + .map(Optional::of) + .defaultIfEmpty(Optional.empty()); + } + + public static Mono notFoundMono() { + return (Mono) NOT_FOUND_MONO; + } + + public static T applyPatch(T originalObj, String patchString) { + + try { + return LecUtils.applyPatch(originalObj, patchString); + } catch (IOException | JsonPatchException e) { + throw new RuntimeException(e); + } + } +} diff --git a/spring-lemon-commons-reactive/src/main/resources/META-INF/spring.factories b/spring-lemon-commons-reactive/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..bf5f0b82 --- /dev/null +++ b/spring-lemon-commons-reactive/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.naturalprogrammer.spring.lemon.commonsreactive.LemonCommonsReactiveAutoConfiguration \ No newline at end of file diff --git a/spring-lemon-commons-web/pom.xml b/spring-lemon-commons-web/pom.xml new file mode 100644 index 00000000..53b0ebc0 --- /dev/null +++ b/spring-lemon-commons-web/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + com.naturalprogrammer.spring-lemon + spring-lemon-commons-web + jar + + ${project.groupId}:${project.artifactId} + >Helper commons web library for Spring Boot REST APIs + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + com.naturalprogrammer + spring-lemon + 1.0.2 + + + + + + com.naturalprogrammer.spring-lemon + spring-lemon-commons + ${project.version} + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + diff --git a/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/LemonCommonsWebAutoConfiguration.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/LemonCommonsWebAutoConfiguration.java new file mode 100644 index 00000000..e7639569 --- /dev/null +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/LemonCommonsWebAutoConfiguration.java @@ -0,0 +1,175 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsweb; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.naturalprogrammer.spring.lemon.commons.LemonCommonsAutoConfiguration; +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commonsweb.exceptions.DefaultExceptionHandlerControllerAdvice; +import com.naturalprogrammer.spring.lemon.commonsweb.exceptions.LemonErrorAttributes; +import com.naturalprogrammer.spring.lemon.commonsweb.exceptions.LemonErrorController; +import com.naturalprogrammer.spring.lemon.commonsweb.exceptions.handlers.MissingPathVariableExceptionHandler; +import com.naturalprogrammer.spring.lemon.commonsweb.security.LemonCorsConfigurationSource; +import com.naturalprogrammer.spring.lemon.commonsweb.security.LemonWebAuditorAware; +import com.naturalprogrammer.spring.lemon.commonsweb.security.LemonWebSecurityConfig; +import com.naturalprogrammer.spring.lemon.commonsweb.util.LecwUtils; +import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponseComposer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.data.web.config.EnableSpringDataWebSupport; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfigurationSource; + +import java.io.Serializable; +import java.util.List; + +@Configuration +@EnableSpringDataWebSupport +@EnableGlobalMethodSecurity(prePostEnabled = true) +@ComponentScan(basePackageClasses= MissingPathVariableExceptionHandler.class) +@AutoConfigureBefore({ + WebMvcAutoConfiguration.class, + ErrorMvcAutoConfiguration.class, + SecurityAutoConfiguration.class, + SecurityFilterAutoConfiguration.class, + LemonCommonsAutoConfiguration.class}) +public class LemonCommonsWebAutoConfiguration { + + /** + * For handling JSON vulnerability, + * JSON response bodies would be prefixed with + * this string. + */ + public static final String JSON_PREFIX = ")]}',\n"; + + private static final Log log = LogFactory.getLog(LemonCommonsWebAutoConfiguration.class); + + public LemonCommonsWebAutoConfiguration() { + log.info("Created"); + } + + /** + * Prefixes JSON responses for JSON vulnerability. Disabled by default. + * To enable, add this to your application properties: + * lemon.enabled.json-prefix: true + * + * @param objectMapper Object Mapper (configured by Spring Boot) + * @return Message converted + */ + @Bean + @ConditionalOnProperty(name="lemon.enabled.json-prefix") + public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( + ObjectMapper objectMapper) { + + log.info("Configuring JSON vulnerability prefix"); + + MappingJackson2HttpMessageConverter converter = + new MappingJackson2HttpMessageConverter(objectMapper); + converter.setJsonPrefix(JSON_PREFIX); + + return converter; + } + + /** + * Configures DefaultExceptionHandlerControllerAdvice if missing + * @param exception + * @param errorResponseComposer the configured errorResponseComposer + * @return configured defaultExceptionHandlerControllerAdvice + */ + @Bean + @ConditionalOnMissingBean(DefaultExceptionHandlerControllerAdvice.class) + public + DefaultExceptionHandlerControllerAdvice defaultExceptionHandlerControllerAdvice( + ErrorResponseComposer errorResponseComposer) { + + log.info("Configuring DefaultExceptionHandlerControllerAdvice"); + return new DefaultExceptionHandlerControllerAdvice<>(errorResponseComposer); + } + + @Bean + @ConditionalOnMissingBean(ErrorAttributes.class) + public + ErrorAttributes errorAttributes(ErrorResponseComposer errorResponseComposer) { + + log.info("Configuring LemonErrorAttributes"); + return new LemonErrorAttributes<>(errorResponseComposer); + } + + @Bean + @ConditionalOnMissingBean(ErrorController.class) + public ErrorController errorController(ErrorAttributes errorAttributes, + ServerProperties serverProperties, + List errorViewResolvers) { + + log.info("Configuring LemonErrorController"); + return new LemonErrorController(errorAttributes, serverProperties, errorViewResolvers); + } + + @Bean + @ConditionalOnProperty(name="lemon.cors.allowed-origins") + @ConditionalOnMissingBean(CorsConfigurationSource.class) + public LemonCorsConfigurationSource corsConfigurationSource(LemonProperties properties) { + + log.info("Configuring LemonCorsConfigurationSource"); + return new LemonCorsConfigurationSource(properties); + } + + @Bean + @ConditionalOnBean(LemonWebSecurityConfig.class) + public SecurityFilterChain lemonSecurityFilterChain(HttpSecurity http, LemonWebSecurityConfig securityConfig) throws Exception { + + log.info("Configuring lemonSecurityFilterChain"); + return securityConfig.configure(http).build(); + } + + @Bean + @ConditionalOnMissingBean(AuditorAware.class) + public + AuditorAware auditorAware() { + + log.info("Configuring LemonAuditorAware"); + return new LemonWebAuditorAware<>(); + } + + @Bean + public LecwUtils lecwUtils(ApplicationContext applicationContext, + ObjectMapper objectMapper) { + + log.info("Configuring LecwUtils"); + return new LecwUtils(); + } +} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/DefaultExceptionHandlerControllerAdvice.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/DefaultExceptionHandlerControllerAdvice.java similarity index 62% rename from spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/DefaultExceptionHandlerControllerAdvice.java rename to spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/DefaultExceptionHandlerControllerAdvice.java index 96f64f29..aad68145 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/DefaultExceptionHandlerControllerAdvice.java +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/DefaultExceptionHandlerControllerAdvice.java @@ -1,5 +1,23 @@ -package com.naturalprogrammer.spring.lemon.exceptions; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsweb.exceptions; +import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponse; +import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponseComposer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpStatus; @@ -28,9 +46,6 @@ public DefaultExceptionHandlerControllerAdvice(ErrorResponseComposer errorRes } - /** - * Handles exceptions - */ @RequestMapping(produces = "application/json") @ExceptionHandler(Throwable.class) public ResponseEntity handleException(T ex) throws T { @@ -42,10 +57,6 @@ public ResponseEntity handleException(T ex) throws T { throw ex; log.warn("Handling exception", ex); - - // We didn't do this inside compose because LemonErrorAttributes would do it differently - errorResponse.setException(ex.getClass().getSimpleName()); - return new ResponseEntity(errorResponse, HttpStatus.valueOf(errorResponse.getStatus())); } } diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonErrorAttributes.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/LemonErrorAttributes.java similarity index 55% rename from spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonErrorAttributes.java rename to spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/LemonErrorAttributes.java index 8e19bf59..14808f61 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonErrorAttributes.java +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/LemonErrorAttributes.java @@ -1,24 +1,40 @@ -package com.naturalprogrammer.spring.lemon.exceptions; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.util.Map; +package com.naturalprogrammer.spring.lemon.commonsweb.exceptions; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponseComposer; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.web.error.ErrorAttributeOptions; import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; import org.springframework.web.context.request.WebRequest; +import java.util.Map; + /** * Used for handling exceptions that can't be handled by * DefaultExceptionHandlerControllerAdvice, * e.g. exceptions thrown in filters. */ +@Slf4j public class LemonErrorAttributes extends DefaultErrorAttributes { - private static final Log log = LogFactory.getLog(LemonErrorAttributes.class); - static final String HTTP_STATUS_KEY = "httpStatus"; - - private ErrorResponseComposer errorResponseComposer; + private final ErrorResponseComposer errorResponseComposer; public LemonErrorAttributes(ErrorResponseComposer errorResponseComposer) { @@ -31,34 +47,29 @@ public LemonErrorAttributes(ErrorResponseComposer errorResponseComposer) { */ @Override public Map getErrorAttributes(WebRequest request, - boolean includeStackTrace) { + ErrorAttributeOptions options) { Map errorAttributes = - super.getErrorAttributes(request, includeStackTrace); + super.getErrorAttributes(request, options); addLemonErrorDetails(errorAttributes, request); return errorAttributes; } - /** - * Handles exceptions - */ @SuppressWarnings("unchecked") protected void addLemonErrorDetails( Map errorAttributes, WebRequest request) { Throwable ex = getError(request); - if (ex == null) // sometimes getError may return null, - return; // in which case, we can't add any more details - - errorAttributes.put("exception", ex.getClass().getSimpleName()); - errorResponseComposer.compose((T)ex).ifPresent(errorResponse -> { // check for null - errorResponse may have left something for the DefaultErrorAttributes + if (errorResponse.getExceptionId() != null) + errorAttributes.put("exceptionId", errorResponse.getExceptionId()); + if (errorResponse.getMessage() != null) errorAttributes.put("message", errorResponse.getMessage()); @@ -73,5 +84,8 @@ protected void addLemonErrorDetails( if (errorResponse.getErrors() != null) errorAttributes.put("errors", errorResponse.getErrors()); }); + + if (errorAttributes.get("exceptionId") == null) + errorAttributes.put("exceptionId", LexUtils.getExceptionId(ex)); } } diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonErrorController.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/LemonErrorController.java similarity index 67% rename from spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonErrorController.java rename to spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/LemonErrorController.java index 8e1e91b7..72115c81 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonErrorController.java +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/LemonErrorController.java @@ -1,9 +1,20 @@ -package com.naturalprogrammer.spring.lemon.exceptions; - -import java.util.List; -import java.util.Map; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.servlet.http.HttpServletRequest; +package com.naturalprogrammer.spring.lemon.commonsweb.exceptions; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -16,6 +27,10 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + /** * Used for handling exceptions that can't be handled by * DefaultExceptionHandlerControllerAdvice, @@ -40,7 +55,7 @@ public LemonErrorController(ErrorAttributes errorAttributes, public ResponseEntity> error(HttpServletRequest request) { Map body = getErrorAttributes(request, - isIncludeStackTrace(request, MediaType.ALL)); + getErrorAttributeOptions(request, MediaType.ALL)); // if a status was put in LemonErrorAttributes, fetch that Object statusObj = body.get(LemonErrorAttributes.HTTP_STATUS_KEY); @@ -54,8 +69,8 @@ public ResponseEntity> error(HttpServletRequest request) { body.remove(LemonErrorAttributes.HTTP_STATUS_KEY); // clean the status from the map } HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON_UTF8); + headers.setContentType(MediaType.APPLICATION_JSON); - return new ResponseEntity>(body, headers, status); + return new ResponseEntity<>(body, headers, status); } } diff --git a/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/handlers/MissingPathVariableExceptionHandler.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/handlers/MissingPathVariableExceptionHandler.java new file mode 100644 index 00000000..db8c38f7 --- /dev/null +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/exceptions/handlers/MissingPathVariableExceptionHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsweb.exceptions.handlers; + +import com.naturalprogrammer.spring.lemon.exceptions.LemonFieldError; +import com.naturalprogrammer.spring.lemon.exceptions.MultiErrorException; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.MissingPathVariableException; + +import java.util.Collection; + +@Component +@Order(Ordered.LOWEST_PRECEDENCE) +public class MissingPathVariableExceptionHandler extends AbstractExceptionHandler { + + public MissingPathVariableExceptionHandler() { + + super(MissingPathVariableException.class); + log.info("Created"); + } + + @Override + public HttpStatus getStatus(MissingPathVariableException ex) { + return HttpStatus.NOT_FOUND; + } +} diff --git a/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonCommonsWebTokenAuthenticationFilter.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonCommonsWebTokenAuthenticationFilter.java new file mode 100644 index 00000000..eb0d3a9d --- /dev/null +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonCommonsWebTokenAuthenticationFilter.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsweb.security; + +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import com.nimbusds.jwt.JWTClaimsSet; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Filter for token authentication + */ +@AllArgsConstructor +@Slf4j +public class LemonCommonsWebTokenAuthenticationFilter extends OncePerRequestFilter { + + private final BlueTokenService blueTokenService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + log.debug("Inside LemonTokenAuthenticationFilter ..."); + + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (header != null && header.startsWith(LecUtils.TOKEN_PREFIX)) { // token present + + log.debug("Found a token"); + String token = header.substring(7); + + try { + + Authentication auth = createAuthToken(token); + SecurityContextHolder.getContext().setAuthentication(auth); + + log.debug("Token authentication successful"); + + } catch (Exception e) { + + log.debug("Token authentication failed - {}", e.getMessage()); + + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, + "Authentication Failed: " + e.getMessage()); + + return; + } + + } else + + log.debug("Token authentication skipped"); + + filterChain.doFilter(request, response); + } + + protected Authentication createAuthToken(String token) { + + JWTClaimsSet claims = blueTokenService.parseToken(token, BlueTokenService.AUTH_AUDIENCE); + UserDto userDto = LecUtils.getUserDto(claims); + if (userDto == null) + userDto = fetchUserDto(claims); + + LemonPrincipal principal = new LemonPrincipal(userDto); + + return new UsernamePasswordAuthenticationToken(principal, token, principal.getAuthorities()); + } + + /* + * Default behaviour is to throw error. To be overridden in auth service. + */ + protected UserDto fetchUserDto(JWTClaimsSet claims) { + throw new AuthenticationCredentialsNotFoundException( + LexUtils.getMessage("com.naturalprogrammer.spring.userClaimAbsent")); + } +} diff --git a/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonCorsConfigurationSource.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonCorsConfigurationSource.java new file mode 100644 index 00000000..a8c45ccf --- /dev/null +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonCorsConfigurationSource.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsweb.security; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.LemonProperties.Cors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; + +/** + * CORS Configuration + */ +public class LemonCorsConfigurationSource implements CorsConfigurationSource { + + private static final Log log = LogFactory.getLog(LemonCorsConfigurationSource.class); + + private Cors cors; + + public LemonCorsConfigurationSource(LemonProperties properties) { + + this.cors = properties.getCors(); + log.info("Created"); + } + + @Override + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowCredentials(true); + config.setAllowedHeaders(Arrays.asList(cors.getAllowedHeaders())); + config.setAllowedMethods(Arrays.asList(cors.getAllowedMethods())); + config.setAllowedOrigins(Arrays.asList(cors.getAllowedOrigins())); + config.setExposedHeaders(Arrays.asList(cors.getExposedHeaders())); + config.setMaxAge(cors.getMaxAge()); + + return config; + } + +} diff --git a/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonWebAuditorAware.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonWebAuditorAware.java new file mode 100644 index 00000000..5fd170c2 --- /dev/null +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonWebAuditorAware.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsweb.security; + +import com.naturalprogrammer.spring.lemon.commons.domain.AbstractAuditorAware; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commonsweb.util.LecwUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.Serializable; + +/** + * Needed for auto-filling of the + * AbstractAuditable columns of AbstractUser + * + * @author Sanjay Patel + */ +public class LemonWebAuditorAware +extends AbstractAuditorAware { + + private static final Log log = LogFactory.getLog(LemonWebAuditorAware.class); + + public LemonWebAuditorAware() { + log.info("Created"); + } + + @Override + protected UserDto currentUser() { + return LecwUtils.currentUser(); + } +} diff --git a/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonWebSecurityConfig.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonWebSecurityConfig.java new file mode 100644 index 00000000..f044ca74 --- /dev/null +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/security/LemonWebSecurityConfig.java @@ -0,0 +1,148 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsweb.security; + +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * Security configuration class. Extend it in the + * application, and make a configuration class. Override + * protected methods, if you need any customization. + * + * @author Sanjay Patel + */ +public class LemonWebSecurityConfig { + + private static final Log log = LogFactory.getLog(LemonWebSecurityConfig.class); + + protected BlueTokenService blueTokenService; + + @Autowired + public void createLemonWebSecurityConfig(BlueTokenService blueTokenService) { + + this.blueTokenService = blueTokenService; + log.info("Created"); + } + + /* + * Security configuration, calling protected methods + */ + public HttpSecurity configure(HttpSecurity http) throws Exception { + + sessionCreationPolicy(http); // set session creation policy + logout(http); // logout related configuration + exceptionHandling(http); // exception handling + tokenAuthentication(http); // configure token authentication filter + csrf(http); // CSRF configuration + cors(http); // CORS configuration + authorizeRequests(http); // authorize requests + otherConfigurations(http); // override this to add more configurations + return http; + } + + + /* + * Configuring session creation policy + */ + protected void sessionCreationPolicy(HttpSecurity http) throws Exception { + + // No session + http.sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + + + /* + * Logout related configuration + */ + protected void logout(HttpSecurity http) throws Exception { + + http + .logout().disable(); // we are stateless; so /logout endpoint not needed + } + + + /* + * Configures exception-handling + */ + protected void exceptionHandling(HttpSecurity http) throws Exception { + + http + .exceptionHandling() + + /*********************************************** + * To prevent redirection to the login page + * when someone tries to access a restricted page + **********************************************/ + .authenticationEntryPoint(new Http403ForbiddenEntryPoint()); + } + + + /* + * Configuring token authentication filter + */ + protected void tokenAuthentication(HttpSecurity http) { + + http.addFilterBefore(new LemonCommonsWebTokenAuthenticationFilter(blueTokenService), + UsernamePasswordAuthenticationFilter.class); + } + + + /** + * Disables CSRF. We are stateless. + */ + protected void csrf(HttpSecurity http) throws Exception { + + http + .csrf().disable(); + } + + + /* + * Configures CORS + */ + protected void cors(HttpSecurity http) throws Exception { + + http + .cors(); + } + + + /* + * URL based authorization configuration. Override this if needed. + */ + protected void authorizeRequests(HttpSecurity http) throws Exception { + http.authorizeRequests() + .mvcMatchers("/**").permitAll(); + } + + + /* + * Override this to add more http configurations, + * such as more authentication methods. + */ + protected void otherConfigurations(HttpSecurity http) { + // Override this method to provide other configurations + } +} diff --git a/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/util/LecwUtils.java b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/util/LecwUtils.java new file mode 100644 index 00000000..11a60a96 --- /dev/null +++ b/spring-lemon-commons-web/src/main/java/com/naturalprogrammer/spring/lemon/commonsweb/util/LecwUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commonsweb.util; + +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.core.context.SecurityContextHolder; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.util.Optional; + +public class LecwUtils { + + private static final Log log = LogFactory.getLog(LecwUtils.class); + + public LecwUtils() { + + log.info("Created"); + } + + /** + * Fetches a cookie from the request + */ + public static Optional fetchCookie(HttpServletRequest request, String name) { + + Cookie[] cookies = request.getCookies(); + + if (cookies != null && cookies.length > 0) + for (int i = 0; i < cookies.length; i++) + if (cookies[i].getName().equals(name)) + return Optional.of(cookies[i]); + + return Optional.empty(); + } + + /** + * Gets the current-user + */ + public static UserDto currentUser() { + + return LecUtils.currentUser(SecurityContextHolder.getContext()); + } + +} diff --git a/spring-lemon-commons-web/src/main/resources/META-INF/spring.factories b/spring-lemon-commons-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..3c7dad2e --- /dev/null +++ b/spring-lemon-commons-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.naturalprogrammer.spring.lemon.commonsweb.LemonCommonsWebAutoConfiguration \ No newline at end of file diff --git a/spring-lemon-commons/pom.xml b/spring-lemon-commons/pom.xml index 2c516e5f..b1451d4d 100644 --- a/spring-lemon-commons/pom.xml +++ b/spring-lemon-commons/pom.xml @@ -1,69 +1,94 @@ - - - 4.0.0 - - com.naturalprogrammer.spring-lemon - spring-lemon-commons - jar - - spring-lemon-commons - Helper library for Spring Boot Web Applications - - - com.naturalprogrammer - spring-lemon - 1.0.0.M4 - - - - - - com.naturalprogrammer.spring-lemon - spring-lemon-exceptions - ${project.version} - - - - org.springframework.data - spring-data-commons - - - - org.springframework.boot - spring-boot-starter-mail - - - - org.springframework.boot - spring-boot-starter-security - - - - org.springframework.security - spring-security-oauth2-client - - - - org.springframework.security - spring-security-oauth2-jose - - - - com.github.fge - json-patch - RELEASE - - - - - org.springframework.boot - spring-boot-configuration-processor - true - - - - - + + + 4.0.0 + + com.naturalprogrammer.spring-lemon + spring-lemon-commons + jar + + ${project.groupId}:${project.artifactId} + Helper library for Spring Boot Web Applications + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + com.naturalprogrammer + spring-lemon + 1.0.2 + + + + + + com.naturalprogrammer.spring-lemon + spring-lemon-exceptions + ${project.version} + + + + org.springframework.data + spring-data-commons + + + + org.springframework.boot + spring-boot-starter-mail + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-oauth2-client + + + + org.springframework.security + spring-security-oauth2-jose + + + + com.github.java-json-tools + json-patch + 1.13 + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/AbstractLemonService.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/AbstractLemonService.java new file mode 100644 index 00000000..219c8f3d --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/AbstractLemonService.java @@ -0,0 +1,232 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties.Admin; +import com.naturalprogrammer.spring.lemon.commons.domain.LemonUser; +import com.naturalprogrammer.spring.lemon.commons.mail.LemonMailData; +import com.naturalprogrammer.spring.lemon.commons.mail.MailSender; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.GreenTokenService; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractLemonService + , ID extends Serializable> { + + private static final Log log = LogFactory.getLog(AbstractLemonService.class); + protected PasswordEncoder passwordEncoder; + protected LemonProperties properties; + protected BlueTokenService blueTokenService; + protected GreenTokenService greenTokenService; + protected MailSender mailSender; + + /** + * This method is called after the application is ready. + * Needs to be public - otherwise Spring screams. + */ + @EventListener + public void afterApplicationReady(ApplicationReadyEvent event) { + + log.info("Starting up Spring Lemon ..."); + onStartup(); // delegate to onStartup() + log.info("Spring Lemon started"); + } + + protected abstract void onStartup(); + + /** + * Creates the initial Admin user. + * Override this if needed. + */ + protected U createAdminUser() { + + // fetch data about the user to be created + Admin initialAdmin = properties.getAdmin(); + + log.info("Creating the first admin user: " + initialAdmin.getUsername()); + + // create the user + U user = newUser(); + user.setEmail(initialAdmin.getUsername()); + user.setPassword(passwordEncoder.encode( + properties.getAdmin().getPassword())); + user.getRoles().add(UserUtils.Role.ADMIN); + + return user; + } + + protected abstract U newUser(); + + protected Map buildContext() { + + // make the context + Map sharedProperties = new HashMap<>(2); + sharedProperties.put("reCaptchaSiteKey", properties.getRecaptcha().getSitekey()); + sharedProperties.put("shared", properties.getShared()); + + Map context = new HashMap<>(); + context.put("context", sharedProperties); + + return context; + } + + protected void initUser(U user) { + + log.debug("Initializing user: " + user); + + user.setPassword(passwordEncoder.encode(user.getPassword())); // encode the password + makeUnverified(user); // make the user unverified + } + + /** + * Makes a user unverified + */ + protected void makeUnverified(U user) { + + user.getRoles().add(UserUtils.Role.UNVERIFIED); + user.setCredentialsUpdatedMillis(System.currentTimeMillis()); + } + + /** + * Sends verification mail to a unverified user. + */ + protected void sendVerificationMail(final U user) { + try { + + log.debug("Sending verification mail to: " + user); + + String verificationCode = greenTokenService.createToken(GreenTokenService.VERIFY_AUDIENCE, + user.getId().toString(), properties.getJwt().getExpirationMillis(), + LecUtils.mapOf("email", user.getEmail())); + + // make the link + String verifyLink = properties.getApplicationUrl() + + "/users/" + user.getId() + "/verification?code=" + verificationCode; + + // send the mail + sendVerificationMail(user, verifyLink); + + log.debug("Verification mail to " + user.getEmail() + " queued."); + + } catch (Exception e) { + // In case of exception, just log the error and keep silent + log.error(ExceptionUtils.getStackTrace(e)); + } + } + + /** + * Sends verification mail to a unverified user. + * Override this method if you're using a different MailData + */ + protected void sendVerificationMail(final U user, String verifyLink) { + + // send the mail + mailSender.send(LemonMailData.of(user.getEmail(), + LexUtils.getMessage("com.naturalprogrammer.spring.verifySubject"), + LexUtils.getMessage( + "com.naturalprogrammer.spring.verifyEmail", verifyLink))); + } + + /** + * Mails the forgot password link. + */ + public void mailForgotPasswordLink(U user) { + + log.debug("Mailing forgot password link to user: " + user); + + String forgotPasswordCode = greenTokenService.createToken( + GreenTokenService.FORGOT_PASSWORD_AUDIENCE, + user.getEmail(), properties.getJwt().getExpirationMillis()); + + // make the link + String forgotPasswordLink = properties.getApplicationUrl() + + "/reset-password?code=" + forgotPasswordCode; + + mailForgotPasswordLink(user, forgotPasswordLink); + + log.debug("Forgot password link mail queued."); + } + + + /** + * Mails the forgot password link. + * + * Override this method if you're using a different MailData + */ + public void mailForgotPasswordLink(U user, String forgotPasswordLink) { + + // send the mail + mailSender.send(LemonMailData.of(user.getEmail(), + LexUtils.getMessage("com.naturalprogrammer.spring.forgotPasswordSubject"), + LexUtils.getMessage("com.naturalprogrammer.spring.forgotPasswordEmail", + forgotPasswordLink))); + } + + /** + * Extracts the email id from user attributes received from OAuth2 provider, e.g. Google + * + */ + public String getOAuth2Email(String registrationId, Map attributes) { + + return (String) attributes.get(StandardClaimNames.EMAIL); + } + + + /** + * Extracts additional fields, e.g. name from user attributes received from OAuth2 provider, e.g. Google + * Override this if you introduce more user fields, e.g. name + */ + public void fillAdditionalFields(String clientId, U user, Map attributes) { + + } + + + /** + * Checks if the account at the OAuth2 provider is verified + */ + public boolean getOAuth2AccountVerified(String registrationId, Map attributes) { + + // Facebook no more returns verified + // https://developers.facebook.com/docs/graph-api/reference/user + if ("facebook".equals(registrationId)) + return true; + + Object verified = attributes.get(StandardClaimNames.EMAIL_VERIFIED); + if (verified == null) + verified = attributes.get("verified"); + + try { + return (boolean) verified; + } catch (Exception e) { + return false; + } + } + +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/LemonCommonsAutoConfiguration.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/LemonCommonsAutoConfiguration.java index 688eb572..1dedaf9b 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/LemonCommonsAutoConfiguration.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/LemonCommonsAutoConfiguration.java @@ -1,9 +1,38 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.naturalprogrammer.spring.lemon.commons.exceptions.handlers.BadCredentialsExceptionHandler; +import com.naturalprogrammer.spring.lemon.commons.mail.MailSender; +import com.naturalprogrammer.spring.lemon.commons.mail.MockMailSender; +import com.naturalprogrammer.spring.lemon.commons.mail.SmtpMailSender; +import com.naturalprogrammer.spring.lemon.commons.security.*; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commons.validation.CaptchaValidator; +import com.naturalprogrammer.spring.lemon.exceptions.LemonExceptionsAutoConfiguration; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.KeyLengthException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @@ -13,19 +42,11 @@ import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.naturalprogrammer.spring.lemon.commons.exceptions.handlers.BadCredentialsExceptionHandler; -import com.naturalprogrammer.spring.lemon.commons.mail.MailSender; -import com.naturalprogrammer.spring.lemon.commons.mail.MockMailSender; -import com.naturalprogrammer.spring.lemon.commons.mail.SmtpMailSender; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.security.LemonPermissionEvaluator; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.nimbusds.jose.KeyLengthException; - @Configuration @ComponentScan(basePackageClasses=BadCredentialsExceptionHandler.class) @EnableAsync +@AutoConfigureBefore({ + LemonExceptionsAutoConfiguration.class}) public class LemonCommonsAutoConfiguration { private static final Log log = LogFactory.getLog(LemonCommonsAutoConfiguration.class); @@ -47,14 +68,26 @@ public LemonProperties lemonProperties() { /** - * Configures JwtService if missing + * Configures AuthTokenService if missing + */ + @Bean + @ConditionalOnMissingBean(BlueTokenService.class) + public BlueTokenService blueTokenService(LemonProperties properties) throws JOSEException { + + log.info("Configuring AuthTokenService"); + return new LemonJwsService(properties.getJwt().getSecret()); + } + + + /** + * Configures ExternalTokenService if missing */ @Bean - @ConditionalOnMissingBean(JwtService.class) - public JwtService jwtService(LemonProperties properties) throws KeyLengthException { + @ConditionalOnMissingBean(GreenTokenService.class) + public GreenTokenService greenTokenService(LemonProperties properties) throws KeyLengthException { - log.info("Configuring AuthenticationSuccessHandler"); - return new JwtService(properties.getJwt().getSecret()); + log.info("Configuring ExternalTokenService"); + return new LemonJweService(properties.getJwt().getSecret()); } @@ -110,7 +143,18 @@ public MailSender smtpMailSender(JavaMailSender javaMailSender) { } @Bean - public LecUtils lecUtils(ObjectMapper objectMapper) { - return new LecUtils(objectMapper); + public LecUtils lecUtils(ApplicationContext applicationContext, ObjectMapper objectMapper) { + return new LecUtils(applicationContext, objectMapper); + } + + /** + * Configures CaptchaValidator if missing + */ + @Bean + @ConditionalOnMissingBean(CaptchaValidator.class) + public CaptchaValidator captchaValidator(LemonProperties properties) { + + log.info("Configuring LemonUserDetailsService"); + return new CaptchaValidator(properties); } } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/LemonProperties.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/LemonProperties.java index 714859cb..57aa1180 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/LemonProperties.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/LemonProperties.java @@ -1,13 +1,31 @@ -package com.naturalprogrammer.spring.lemon.commons; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.util.Map; +package com.naturalprogrammer.spring.lemon.commons; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import lombok.Getter; +import lombok.Setter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpHeaders; import org.springframework.validation.annotation.Validated; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import java.util.Map; /** * Lemon Properties @@ -16,6 +34,7 @@ */ @Validated @ConfigurationProperties(prefix="lemon") +@Getter @Setter public class LemonProperties { private static final Log log = LogFactory.getLog(LemonProperties.class); @@ -36,6 +55,12 @@ public LemonProperties() { */ private String oauth2AuthenticationSuccessUrl = "http://localhost:9000/social-login-success?token="; + /** + * URL of the login endpoint + * e.g. POST /api/core/login + */ + private String loginUrl = "/api/core/login"; + /** * Recaptcha related properties */ @@ -64,66 +89,6 @@ public LemonProperties() { private Jwt jwt; - /************************** - * Gettrer and setters - **************************/ - public Recaptcha getRecaptcha() { - return recaptcha; - } - - public void setRecaptcha(Recaptcha recaptcha) { - this.recaptcha = recaptcha; - } - - - public Cors getCors() { - return cors; - } - - public void setCors(Cors cors) { - this.cors = cors; - } - - public Admin getAdmin() { - return admin; - } - - public void setAdmin(Admin admin) { - this.admin = admin; - } - - public Map getShared() { - return shared; - } - - public void setShared(Map shared) { - this.shared = shared; - } - - public String getApplicationUrl() { - return applicationUrl; - } - - public void setApplicationUrl(String applicationUrl) { - this.applicationUrl = applicationUrl; - } - - public String getOauth2AuthenticationSuccessUrl() { - return oauth2AuthenticationSuccessUrl; - } - - public void setOauth2AuthenticationSuccessUrl(String oauth2AuthenticationSuccessUrl) { - this.oauth2AuthenticationSuccessUrl = oauth2AuthenticationSuccessUrl; - } - - public Jwt getJwt() { - return jwt; - } - - public void setJwt(Jwt jwt) { - this.jwt = jwt; - } - /************************** * Static classes *************************/ @@ -131,6 +96,7 @@ public void setJwt(Jwt jwt) { /** * Recaptcha related properties */ + @Getter @Setter public static class Recaptcha { /** @@ -142,28 +108,13 @@ public static class Recaptcha { * Google ReCaptcha Secret Key */ private String secretkey; - - public String getSitekey() { - return sitekey; - } - - public void setSitekey(String sitekey) { - this.sitekey = sitekey; - } - - public String getSecretkey() { - return secretkey; - } - - public void setSecretkey(String secretkey) { - this.secretkey = secretkey; - } } /** * CORS configuration related properties */ + @Getter @Setter public static class Cors { /** @@ -196,7 +147,7 @@ public static class Cors { "Referer", "User-Agent", "x-requested-with", - LecUtils.TOKEN_REQUEST_HEADER_NAME}; + HttpHeaders.AUTHORIZATION}; /** * Response headers that you want to expose to the client JavaScript programmer, e.g. Lemon-Authorization. @@ -227,47 +178,6 @@ public static class Cors { * CORS maxAge long property */ private long maxAge = 3600L; - - public String[] getAllowedOrigins() { - return allowedOrigins; - } - - public void setAllowedOrigins(String[] allowedOrigins) { - this.allowedOrigins = allowedOrigins; - } - - public String[] getAllowedMethods() { - return allowedMethods; - } - - public void setAllowedMethods(String[] allowedMethods) { - this.allowedMethods = allowedMethods; - } - - public String[] getAllowedHeaders() { - return allowedHeaders; - } - - public void setAllowedHeaders(String[] allowedHeaders) { - this.allowedHeaders = allowedHeaders; - } - - public String[] getExposedHeaders() { - return exposedHeaders; - } - - public void setExposedHeaders(String[] exposedHeaders) { - this.exposedHeaders = exposedHeaders; - } - - public long getMaxAge() { - return maxAge; - } - - public void setMaxAge(long maxAge) { - this.maxAge = maxAge; - } - } @@ -276,6 +186,7 @@ public void setMaxAge(long maxAge) { * * @author Sanjay Patel */ + @Getter @Setter public static class Admin { /** @@ -287,22 +198,6 @@ public static class Admin { * Password of the initial Admin user to be created */ private String password; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } } /** @@ -310,6 +205,7 @@ public void setPassword(String password) { * * @author Sanjay Patel */ + @Getter @Setter public static class Jwt { /** @@ -326,29 +222,5 @@ public static class Jwt { * Expiration milliseconds for short-lived tokens and cookies */ private int shortLivedMillis = 120000; // Two minutes - - public String getSecret() { - return secret; - } - - public void setSecret(String secret) { - this.secret = secret; - } - - public long getExpirationMillis() { - return expirationMillis; - } - - public void setExpirationMillis(long expirationMillis) { - this.expirationMillis = expirationMillis; - } - - public int getShortLivedMillis() { - return shortLivedMillis; - } - - public void setShortLivedMillis(int shortLivedMillis) { - this.shortLivedMillis = shortLivedMillis; - } } } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/AbstractAuditorAware.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/AbstractAuditorAware.java new file mode 100644 index 00000000..460cefee --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/AbstractAuditorAware.java @@ -0,0 +1,60 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.domain; + +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.AuditorAware; + +import java.io.Serializable; +import java.util.Optional; + +/** + * Needed for auto-filling of the + * AbstractAuditable columns of AbstractUser + * + * @author Sanjay Patel + */ +public abstract class AbstractAuditorAware +implements AuditorAware { + + private static final Log log = LogFactory.getLog(AbstractAuditorAware.class); + + private IdConverter idConverter; + + @Autowired + public void setIdConverter(IdConverter idConverter) { + + this.idConverter = idConverter; + log.info("Created"); + } + + protected abstract UserDto currentUser(); + + @Override + public Optional getCurrentAuditor() { + + UserDto user = currentUser(); + + if (user == null) + return Optional.empty(); + + return Optional.of(idConverter.toId(user.getId())); + } +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/ChangePasswordForm.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/ChangePasswordForm.java index 8846cd72..1b951f96 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/ChangePasswordForm.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/ChangePasswordForm.java @@ -1,8 +1,28 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.domain; import com.naturalprogrammer.spring.lemon.commons.validation.Password; import com.naturalprogrammer.spring.lemon.commons.validation.RetypePassword; import com.naturalprogrammer.spring.lemon.commons.validation.RetypePasswordForm; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; /** * Change password form. @@ -10,16 +30,9 @@ * @author Sanjay Patel */ @RetypePassword +@Getter @Setter @NoArgsConstructor @AllArgsConstructor public class ChangePasswordForm implements RetypePasswordForm { - public ChangePasswordForm() {} - - public ChangePasswordForm(String oldPassword, String password, String retypePassword) { - this.oldPassword = oldPassword; - this.password = password; - this.retypePassword = retypePassword; - } - @Password private String oldPassword; @@ -28,29 +41,4 @@ public ChangePasswordForm(String oldPassword, String password, String retypePass @Password private String retypePassword; - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getOldPassword() { - return oldPassword; - } - - public void setOldPassword(String oldPassword) { - this.oldPassword = oldPassword; - } - - public String getRetypePassword() { - return retypePassword; - } - - public void setRetypePassword(String retypePassword) { - this.retypePassword = retypePassword; - } - } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/IdConverter.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/IdConverter.java new file mode 100644 index 00000000..97a35222 --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/IdConverter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.domain; + +import java.io.Serializable; + +@FunctionalInterface +public interface IdConverter { + + ID toId(String id); +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/LemonUser.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/LemonUser.java new file mode 100644 index 00000000..0831e825 --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/LemonUser.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.domain; + +import java.io.Serializable; +import java.util.Set; + +public interface LemonUser { + + void setEmail(String username); + void setPassword(String password); + Set getRoles(); + String getPassword(); + void setCredentialsUpdatedMillis(long currentTimeMillis); + ID getId(); + String getEmail(); + +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/ResetPasswordForm.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/ResetPasswordForm.java index caaef8e6..e8b43569 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/ResetPasswordForm.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/domain/ResetPasswordForm.java @@ -1,12 +1,27 @@ -package com.naturalprogrammer.spring.lemon.commons.domain; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.validation.constraints.NotBlank; +package com.naturalprogrammer.spring.lemon.commons.domain; import com.naturalprogrammer.spring.lemon.commons.validation.Password; - import lombok.Getter; import lombok.Setter; +import javax.validation.constraints.NotBlank; + @Getter @Setter public class ResetPasswordForm { diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/AccessDeniedExceptionHandler.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/AccessDeniedExceptionHandler.java index b283a7ef..91f69c7e 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/AccessDeniedExceptionHandler.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/AccessDeniedExceptionHandler.java @@ -1,20 +1,35 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.exceptions.handlers; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.stereotype.Component; -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; - @Component @Order(Ordered.LOWEST_PRECEDENCE) public class AccessDeniedExceptionHandler extends AbstractExceptionHandler { public AccessDeniedExceptionHandler() { - super(AccessDeniedException.class.getSimpleName()); + super(AccessDeniedException.class); log.info("Created"); } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/BadCredentialsExceptionHandler.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/BadCredentialsExceptionHandler.java index 69275c19..a1b99403 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/BadCredentialsExceptionHandler.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/BadCredentialsExceptionHandler.java @@ -1,20 +1,35 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.exceptions.handlers; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.stereotype.Component; -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; - @Component @Order(Ordered.LOWEST_PRECEDENCE) public class BadCredentialsExceptionHandler extends AbstractExceptionHandler { public BadCredentialsExceptionHandler() { - super(BadCredentialsException.class.getSimpleName()); + super(BadCredentialsException.class); log.info("Created"); } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonParseExceptionHandler.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonParseExceptionHandler.java index 911df02e..78f6752f 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonParseExceptionHandler.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonParseExceptionHandler.java @@ -1,19 +1,34 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.exceptions.handlers; +import com.fasterxml.jackson.core.JsonParseException; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractBadRequestExceptionHandler; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import com.fasterxml.jackson.core.JsonParseException; -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractBadRequestExceptionHandler; - @Component @Order(Ordered.LOWEST_PRECEDENCE) public class JsonParseExceptionHandler extends AbstractBadRequestExceptionHandler { public JsonParseExceptionHandler() { - super(JsonParseException.class.getSimpleName()); + super(JsonParseException.class); log.info("Created"); } } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonPatchExceptionHandler.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonPatchExceptionHandler.java index 5f309fbb..3168e94a 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonPatchExceptionHandler.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonPatchExceptionHandler.java @@ -1,19 +1,34 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.exceptions.handlers; +import com.github.fge.jsonpatch.JsonPatchException; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractBadRequestExceptionHandler; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import com.github.fge.jsonpatch.JsonPatchException; -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractBadRequestExceptionHandler; - @Component @Order(Ordered.LOWEST_PRECEDENCE) public class JsonPatchExceptionHandler extends AbstractBadRequestExceptionHandler { public JsonPatchExceptionHandler() { - super(JsonPatchException.class.getSimpleName()); + super(JsonPatchException.class); log.info("Created"); } } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonProcessingExceptionHandler.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonProcessingExceptionHandler.java index 8d57bbc9..888fd96e 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonProcessingExceptionHandler.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/JsonProcessingExceptionHandler.java @@ -1,19 +1,34 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.exceptions.handlers; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractBadRequestExceptionHandler; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractBadRequestExceptionHandler; - @Component @Order(Ordered.LOWEST_PRECEDENCE) public class JsonProcessingExceptionHandler extends AbstractBadRequestExceptionHandler { public JsonProcessingExceptionHandler() { - super(JsonProcessingException.class.getSimpleName()); + super(JsonProcessingException.class); log.info("Created"); } } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/UsernameNotFoundExceptionHandler.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/UsernameNotFoundExceptionHandler.java index c7f00440..6b56e2d8 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/UsernameNotFoundExceptionHandler.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/exceptions/handlers/UsernameNotFoundExceptionHandler.java @@ -1,20 +1,35 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.exceptions.handlers; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; - @Component @Order(Ordered.LOWEST_PRECEDENCE) public class UsernameNotFoundExceptionHandler extends AbstractExceptionHandler { public UsernameNotFoundExceptionHandler() { - super(UsernameNotFoundException.class.getSimpleName()); + super(UsernameNotFoundException.class); log.info("Created"); } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/LemonMailData.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/LemonMailData.java index 4105a235..f8d8cb62 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/LemonMailData.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/LemonMailData.java @@ -1,34 +1,35 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.mail; +import lombok.Getter; +import lombok.Setter; + /** * Data needed for sending a mail. * Override this if you need more data to be sent. */ +@Getter @Setter public class LemonMailData { private String to; private String subject; private String body; - - public String getTo() { - return to; - } - public void setTo(String to) { - this.to = to; - } - public String getSubject() { - return subject; - } - public void setSubject(String subject) { - this.subject = subject; - } - public String getBody() { - return body; - } - public void setBody(String body) { - this.body = body; - } - + public static LemonMailData of(String to, String subject, String body) { LemonMailData data = new LemonMailData(); diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/MailSender.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/MailSender.java index 7c8a3828..24f288bf 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/MailSender.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/MailSender.java @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.mail; /** diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/MockMailSender.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/MockMailSender.java index 959cc693..9bba134c 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/MockMailSender.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/MockMailSender.java @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.mail; import org.apache.commons.logging.Log; diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/SmtpMailSender.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/SmtpMailSender.java index e0ab00c1..f3dc2509 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/SmtpMailSender.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/mail/SmtpMailSender.java @@ -1,7 +1,20 @@ -package com.naturalprogrammer.spring.lemon.commons.mail; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; +package com.naturalprogrammer.spring.lemon.commons.mail; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -9,6 +22,9 @@ import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.scheduling.annotation.Async; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + /** * An SMTP mail sender, which sends mails diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/AbstractJwtService.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/AbstractJwtService.java new file mode 100644 index 00000000..447018fd --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/AbstractJwtService.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.security; + +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.nimbusds.jose.Payload; +import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * Common JWT Service + */ +public abstract class AbstractJwtService implements LemonTokenService { + + private static final Log log = LogFactory.getLog(AbstractJwtService.class); + + protected Payload createPayload(String aud, String subject, Long expirationMillis, Map claimMap) { + + JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); + + builder + //.issueTime(new Date()) + .expirationTime(new Date(System.currentTimeMillis() + expirationMillis)) + .audience(aud) + .subject(subject) + .claim(LEMON_IAT, System.currentTimeMillis()); + + claimMap.forEach(builder::claim); + + JWTClaimsSet claims = builder.build(); + + return new Payload(claims.toJSONObject()); + } + + + @Override + public String createToken(String audience, String subject, Long expirationMillis) { + + return createToken(audience, subject, expirationMillis, new HashMap<>()); + } + + + @Override + public JWTClaimsSet parseToken(String token, String audience) { + + JWTClaimsSet claims = parseToken(token); + LecUtils.ensureCredentials(audience != null && + claims.getAudience().contains(audience), + "com.naturalprogrammer.spring.wrong.audience"); + + long expirationTime = claims.getExpirationTime().getTime(); + long currentTime = System.currentTimeMillis(); + + log.debug("Parsing JWT. Expiration time = " + expirationTime + + ". Current time = " + currentTime); + + LecUtils.ensureCredentials(expirationTime >= currentTime, + "com.naturalprogrammer.spring.expiredToken"); + + return claims; + } + + + @Override + public JWTClaimsSet parseToken(String token, String audience, long issuedAfter) { + + JWTClaimsSet claims = parseToken(token, audience); + + long issueTime = (long) claims.getClaim(LEMON_IAT); + LecUtils.ensureCredentials(issueTime >= issuedAfter, + "com.naturalprogrammer.spring.obsoleteToken"); + + return claims; + } + + + @Override + public T parseClaim(String token, String claim) { + + JWTClaimsSet claims = parseToken(token); + return (T) claims.getClaim(claim); + } + + + protected abstract JWTClaimsSet parseToken(String token); +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/BlueTokenService.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/BlueTokenService.java new file mode 100644 index 00000000..26d25b6f --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/BlueTokenService.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.security; + +public interface BlueTokenService extends LemonTokenService { + + String USER_CLAIM = "user"; + String AUTH_AUDIENCE = "auth"; +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/GreenTokenService.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/GreenTokenService.java new file mode 100644 index 00000000..fdbeff84 --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/GreenTokenService.java @@ -0,0 +1,24 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.security; + +public interface GreenTokenService extends LemonTokenService { + + String VERIFY_AUDIENCE = "verify"; + String FORGOT_PASSWORD_AUDIENCE = "forgot-password"; + String CHANGE_EMAIL_AUDIENCE = "change-email"; +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/JwtAuthenticationToken.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/JwtAuthenticationToken.java deleted file mode 100644 index 4474eb21..00000000 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/JwtAuthenticationToken.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.naturalprogrammer.spring.lemon.commons.security; - -import java.util.Collection; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.userdetails.UserDetails; - -/** - * JWT Authentication token - */ -public class JwtAuthenticationToken extends AbstractAuthenticationToken { - - private static final long serialVersionUID = 7032335279756013130L; - - private UserDetails principal; - private String jwtToken; - - public JwtAuthenticationToken(String token) { - super(AuthorityUtils.NO_AUTHORITIES); - this.jwtToken = token; - } - - public JwtAuthenticationToken(UserDetails principal, String jwtToken, Collection authorities) { - - super(authorities); - this.principal = principal; - this.jwtToken = jwtToken; - setAuthenticated(true); - } - - @Override - public Object getCredentials() { - return jwtToken; - } - - @Override - public Object getPrincipal() { - return principal; - } -} \ No newline at end of file diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/JwtService.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/JwtService.java deleted file mode 100644 index ac268ce2..00000000 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/JwtService.java +++ /dev/null @@ -1,146 +0,0 @@ -package com.naturalprogrammer.spring.lemon.commons.security; - -import java.text.ParseException; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import org.springframework.security.authentication.BadCredentialsException; - -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.nimbusds.jose.EncryptionMethod; -import com.nimbusds.jose.JOSEException; -import com.nimbusds.jose.JWEAlgorithm; -import com.nimbusds.jose.JWEHeader; -import com.nimbusds.jose.JWEObject; -import com.nimbusds.jose.KeyLengthException; -import com.nimbusds.jose.Payload; -import com.nimbusds.jose.crypto.DirectEncrypter; -import com.nimbusds.jose.jwk.source.ImmutableSecret; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.BadJOSEException; -import com.nimbusds.jose.proc.JWEDecryptionKeySelector; -import com.nimbusds.jose.proc.JWEKeySelector; -import com.nimbusds.jose.proc.SimpleSecurityContext; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; -import com.nimbusds.jwt.proc.DefaultJWTProcessor; - -/** - * JWT Service - * - * References: - * - * https://connect2id.com/products/nimbus-jose-jwt/examples/jwe-with-shared-key - * https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens - */ -public class JwtService { - - public static final String LEMON_IAT = "lemon-iat"; - public static final String AUTH_AUDIENCE = "auth"; - public static final String VERIFY_AUDIENCE = "verify"; - public static final String FORGOT_PASSWORD_AUDIENCE = "forgot-password"; - public static final String CHANGE_EMAIL_AUDIENCE = "change-email"; - - private DirectEncrypter encrypter; - private JWEHeader header = new JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256); - private ConfigurableJWTProcessor jwtProcessor; - - public JwtService(String secret) throws KeyLengthException { - - byte[] secretKey = secret.getBytes(); - encrypter = new DirectEncrypter(secretKey); - jwtProcessor = new DefaultJWTProcessor(); - - // The JWE key source - JWKSource jweKeySource = new ImmutableSecret(secretKey); - - // Configure a key selector to handle the decryption phase - JWEKeySelector jweKeySelector = - new JWEDecryptionKeySelector(JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256, jweKeySource); - - jwtProcessor.setJWEKeySelector(jweKeySelector); - } - - /** - * Creates a token - */ - public String createToken(String aud, String subject, Long expirationMillis, Map claimMap) { - - JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(); - - builder - //.issueTime(new Date()) - .expirationTime(new Date(System.currentTimeMillis() + expirationMillis)) - .audience(aud) - .subject(subject) - .claim(LEMON_IAT, System.currentTimeMillis()); - - //claimMap.put("iat", new Date()); - claimMap.forEach(builder::claim); - - JWTClaimsSet claims = builder.build(); - - Payload payload = new Payload(claims.toJSONObject()); - - // Create the JWE object and encrypt it - JWEObject jweObject = new JWEObject(header, payload); - - try { - - jweObject.encrypt(encrypter); - - } catch (JOSEException e) { - - throw new RuntimeException(e); - } - - // Serialize to compact JOSE form... - return jweObject.serialize(); - } - - /** - * Creates a token - */ - public String createToken(String audience, String subject, Long expirationMillis) { - - return createToken(audience, subject, expirationMillis, new HashMap<>()); - } - - /** - * Parses a token - */ - public JWTClaimsSet parseToken(String token, String audience) { - - try { - - JWTClaimsSet claims = jwtProcessor.process(token, null); - LecUtils.ensureAuthority(audience != null && - claims.getAudience().contains(audience), - "com.naturalprogrammer.spring.wrong.audience"); - - LecUtils.ensureAuthority(claims.getExpirationTime().after(new Date()), - "com.naturalprogrammer.spring.expiredToken"); - - return claims; - - } catch (ParseException | BadJOSEException | JOSEException e) { - - throw new BadCredentialsException(e.getMessage()); - } - } - - /** - * Parses a token - */ - public JWTClaimsSet parseToken(String token, String audience, long issuedAfter) { - - JWTClaimsSet claims = parseToken(token, audience); - - long issueTime = (long) claims.getClaim(LEMON_IAT); - LecUtils.ensureAuthority(issueTime >= issuedAfter, - "com.naturalprogrammer.spring.obsoleteToken"); - - return claims; - } -} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonGrantedAuthority.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonGrantedAuthority.java index 7b3dd0a4..93a4becd 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonGrantedAuthority.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonGrantedAuthority.java @@ -1,10 +1,25 @@ -package com.naturalprogrammer.spring.lemon.commons.security; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.springframework.security.core.GrantedAuthority; +package com.naturalprogrammer.spring.lemon.commons.security; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import org.springframework.security.core.GrantedAuthority; /** * Our implementation of GrantedAuthority. diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJweService.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJweService.java new file mode 100644 index 00000000..f0cdebc1 --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJweService.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.security; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.DirectEncrypter; +import com.nimbusds.jose.jwk.source.ImmutableSecret; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.BadJOSEException; +import com.nimbusds.jose.proc.JWEDecryptionKeySelector; +import com.nimbusds.jose.proc.JWEKeySelector; +import com.nimbusds.jose.proc.SimpleSecurityContext; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; +import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import org.springframework.security.authentication.BadCredentialsException; + +import java.text.ParseException; +import java.util.Map; + +/** + * JWE Service + * + * References: + * + * https://connect2id.com/products/nimbus-jose-jwt/examples/jwe-with-shared-key + * https://connect2id.com/products/nimbus-jose-jwt/examples/validating-jwt-access-tokens + */ +public class LemonJweService extends AbstractJwtService implements GreenTokenService { + + private DirectEncrypter encrypter; + private JWEHeader header = new JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256); + private ConfigurableJWTProcessor jwtProcessor; + + public LemonJweService(String secret) throws KeyLengthException { + + byte[] secretKey = secret.getBytes(); + encrypter = new DirectEncrypter(secretKey); + jwtProcessor = new DefaultJWTProcessor(); + + // The JWE key source + JWKSource jweKeySource = new ImmutableSecret(secretKey); + + // Configure a key selector to handle the decryption phase + JWEKeySelector jweKeySelector = + new JWEDecryptionKeySelector(JWEAlgorithm.DIR, EncryptionMethod.A128CBC_HS256, jweKeySource); + + jwtProcessor.setJWEKeySelector(jweKeySelector); + } + + + @Override + public String createToken(String aud, String subject, Long expirationMillis, Map claimMap) { + + Payload payload = createPayload(aud, subject, expirationMillis, claimMap); + + // Create the JWE object and encrypt it + JWEObject jweObject = new JWEObject(header, payload); + + try { + + jweObject.encrypt(encrypter); + + } catch (JOSEException e) { + + throw new RuntimeException(e); + } + + // Serialize to compact JOSE form... + return jweObject.serialize(); + } + + + /** + * Parses a token + */ + protected JWTClaimsSet parseToken(String token) { + + try { + + return jwtProcessor.process(token, null); + + } catch (ParseException | BadJOSEException | JOSEException e) { + + throw new BadCredentialsException(e.getMessage()); + } + } +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJwsService.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJwsService.java new file mode 100644 index 00000000..ae0b7cd1 --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJwsService.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.security; + +import com.nimbusds.jose.*; +import com.nimbusds.jose.crypto.MACSigner; +import com.nimbusds.jose.crypto.MACVerifier; +import com.nimbusds.jwt.JWTClaimsSet; +import org.springframework.security.authentication.BadCredentialsException; + +import java.text.ParseException; +import java.util.Map; + +/** + * JWS Service + * + * Reference: https://connect2id.com/products/nimbus-jose-jwt/examples/jws-with-hmac + */ +public class LemonJwsService extends AbstractJwtService implements BlueTokenService { + + private JWSSigner signer; + private JWSVerifier verifier; + + public LemonJwsService(String secret) throws JOSEException { + + signer = new MACSigner(secret); + verifier = new MACVerifier(secret); + } + + @Override + public String createToken(String aud, String subject, Long expirationMillis, Map claimMap) { + + Payload payload = createPayload(aud, subject, expirationMillis, claimMap); + + // Prepare JWS object + JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.HS256), payload); + + try { + // Apply the HMAC + jwsObject.sign(signer); + + } catch (JOSEException e) { + + throw new RuntimeException(e); + } + + // To serialize to compact form, produces something like + // eyJhbGciOiJIUzI1NiJ9.SGVsbG8sIHdvcmxkIQ.onO9Ihudz3WkiauDO2Uhyuz0Y18UASXlSc1eS0NkWyA + return jwsObject.serialize(); + } + + /** + * Parses a token + */ + protected JWTClaimsSet parseToken(String token) { + + // Parse the JWS and verify it, e.g. on client-side + JWSObject jwsObject; + + try { + jwsObject = JWSObject.parse(token); + if (jwsObject.verify(verifier)) + return JWTClaimsSet.parse(jwsObject.getPayload().toJSONObject()); + + } catch (JOSEException | ParseException e) { + + throw new BadCredentialsException(e.getMessage()); + } + + throw new BadCredentialsException("JWS verification failed!"); + } +} diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonPermissionEvaluator.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonPermissionEvaluator.java index fc5726bb..20f2e7f4 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonPermissionEvaluator.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonPermissionEvaluator.java @@ -1,13 +1,28 @@ -package com.naturalprogrammer.spring.lemon.commons.security; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.io.Serializable; +package com.naturalprogrammer.spring.lemon.commons.security; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.core.Authentication; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import java.io.Serializable; /** * Needed to check the permission for the service methods diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonPrincipal.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonPrincipal.java index cce08e38..ee4c2a8e 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonPrincipal.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonPrincipal.java @@ -1,12 +1,26 @@ -package com.naturalprogrammer.spring.lemon.commons.security; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; +package com.naturalprogrammer.spring.lemon.commons.security; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; import org.springframework.security.core.CredentialsContainer; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -14,23 +28,22 @@ import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.Setter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; /** * Spring Security Principal, implementing both OidcUser, UserDetails */ @Getter @Setter @RequiredArgsConstructor -public class LemonPrincipal implements OidcUser, UserDetails, CredentialsContainer { +public class LemonPrincipal implements OidcUser, UserDetails, CredentialsContainer { private static final long serialVersionUID = -7849730155307434535L; @Getter(AccessLevel.NONE) - private final UserDto userDto; + private final UserDto userDto; private Map attributes; private String name; @@ -38,7 +51,7 @@ public class LemonPrincipal implements OidcUser, UserDe private OidcUserInfo userInfo; private OidcIdToken idToken; - public UserDto currentUser() { + public UserDto currentUser() { return userDto; } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonTokenService.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonTokenService.java new file mode 100644 index 00000000..fb844de9 --- /dev/null +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/LemonTokenService.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.security; + +import com.nimbusds.jwt.JWTClaimsSet; + +import java.util.Map; + +public interface LemonTokenService { + + String LEMON_IAT = "lemon-iat"; + + String createToken(String aud, String subject, Long expirationMillis, Map claimMap); + String createToken(String audience, String subject, Long expirationMillis); + JWTClaimsSet parseToken(String token, String audience); + JWTClaimsSet parseToken(String token, String audience, long issuedAfter); + T parseClaim(String token, String claim); +} \ No newline at end of file diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/PermissionEvaluatorEntity.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/PermissionEvaluatorEntity.java index f973ce5a..738636f6 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/PermissionEvaluatorEntity.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/PermissionEvaluatorEntity.java @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.security; public interface PermissionEvaluatorEntity { @@ -6,5 +22,5 @@ public interface PermissionEvaluatorEntity { * Whether the given user has the given permission for * this entity. Override this method where you need. */ - public boolean hasPermission(UserDto currentUser, String permission); + public boolean hasPermission(UserDto currentUser, String permission); } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/UserDto.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/UserDto.java index 2fcb1974..9a44c380 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/UserDto.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/UserDto.java @@ -1,23 +1,43 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.security; +import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + import java.io.Serializable; import java.util.HashSet; import java.util.Set; -import lombok.Getter; -import lombok.Setter; - /** * A lighter User class, * mainly used for holding logged-in user data */ -@Getter @Setter -public class UserDto { +@Getter @Setter @ToString +public class UserDto implements Serializable { - private ID id; + private static final long serialVersionUID = -9134054705405149534L; + + private String id; private String username; private String password; - private Set roles = new HashSet(); + private Set roles = new HashSet<>(); private Serializable tag; private boolean unverified = false; @@ -25,4 +45,13 @@ public class UserDto { private boolean admin = false; private boolean goodUser = false; private boolean goodAdmin = false; + + public void initialize() { + + unverified = roles.contains(UserUtils.Role.UNVERIFIED); + blocked = roles.contains(UserUtils.Role.BLOCKED); + admin = roles.contains(UserUtils.Role.ADMIN); + goodUser = !(unverified || blocked); + goodAdmin = goodUser && admin; + } } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/UserEditPermission.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/UserEditPermission.java index 36c05eb3..dccc3711 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/UserEditPermission.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/security/UserEditPermission.java @@ -1,10 +1,26 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.security; +import org.springframework.security.access.prepost.PreAuthorize; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import org.springframework.security.access.prepost.PreAuthorize; - @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasPermission(#user, 'edit')") public @interface UserEditPermission {} \ No newline at end of file diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/util/LecUtils.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/util/LecUtils.java index 0d315a1f..13569da0 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/util/LecUtils.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/util/LecUtils.java @@ -1,15 +1,20 @@ -package com.naturalprogrammer.spring.lemon.commons.util; - -import java.io.IOException; -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.core.Authentication; +package com.naturalprogrammer.spring.lemon.commons.util; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.TreeNode; @@ -17,9 +22,25 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.fge.jsonpatch.JsonPatch; import com.github.fge.jsonpatch.JsonPatchException; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.commons.lang3.SerializationUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.*; /** * Useful helper methods @@ -30,34 +51,50 @@ public class LecUtils { private static final Log log = LogFactory.getLog(LecUtils.class); + public static final String AUTHORIZATION_REQUEST_COOKIE_NAME = "lemon_oauth2_authorization_request"; + public static final String LEMON_REDIRECT_URI_COOKIE_PARAM_NAME = "lemon_redirect_uri"; + // Computed authorities public static final String GOOD_ADMIN = "GOOD_ADMIN"; public static final String GOOD_USER = "GOOD_USER"; // JWT Token related public static final String TOKEN_PREFIX = "Bearer "; - public static final String TOKEN_REQUEST_HEADER_NAME = "Authorization"; + public static final int TOKEN_PREFIX_LENGTH = 7; public static final String TOKEN_RESPONSE_HEADER_NAME = "Lemon-Authorization"; - private static ObjectMapper objectMapper; + + public static ApplicationContext applicationContext; + public static ObjectMapper objectMapper; - public LecUtils(ObjectMapper objectMapper) { + public LecUtils(ApplicationContext applicationContext, + ObjectMapper objectMapper) { + + LecUtils.applicationContext = applicationContext; + LecUtils.objectMapper = objectMapper; - LecUtils.objectMapper = objectMapper; log.info("Created"); } + + + /** + * Extracts the current-user from authentication object + */ + public static UserDto currentUser(SecurityContext context) { + + return currentUser(context.getAuthentication()); + } + + /** * Extracts the current-user from authentication object - * - * @param auth - * @return */ - public static UserDto currentUser(Authentication auth) { + public static UserDto currentUser(Authentication auth) { if (auth != null) { Object principal = auth.getPrincipal(); - if (principal instanceof LemonPrincipal) { - return ((LemonPrincipal) principal).currentUser(); + if (principal instanceof LemonPrincipal) { + return ((LemonPrincipal) principal).currentUser(); } } return null; @@ -66,9 +103,6 @@ public static UserDto currentUser(Authentication a /** * Throws AccessDeniedException is not authorized - * - * @param authorized - * @param messageKey */ public static void ensureAuthority(boolean authorized, String messageKey) { @@ -80,8 +114,6 @@ public static void ensureAuthority(boolean authorized, String messageKey) { /** * Constructs a map of the key-value pairs, * passed as parameters - * - * @param keyValPair */ @SuppressWarnings("unchecked") public static Map mapOf(Object... keyValPair) { @@ -89,7 +121,7 @@ public static Map mapOf(Object... keyValPair) { if(keyValPair.length % 2 != 0) throw new IllegalArgumentException("Keys and values must be in pairs"); - Map map = new HashMap(keyValPair.length / 2); + Map map = new HashMap<>(keyValPair.length / 2); for(int i = 0; i < keyValPair.length; i += 2){ map.put((K) keyValPair[i], (V) keyValPair[i+1]); @@ -101,9 +133,6 @@ public static Map mapOf(Object... keyValPair) { /** * Throws BadCredentialsException if not valid - * - * @param valid - * @param messageKey */ public static void ensureCredentials(boolean valid, String messageKey) { @@ -111,27 +140,124 @@ public static void ensureCredentials(boolean valid, String messageKey) { throw new BadCredentialsException(LexUtils.getMessage(messageKey)); } - + /** * Applies a JsonPatch to an object */ - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") public static T applyPatch(T originalObj, String patchString) - throws JsonProcessingException, IOException, JsonPatchException { + throws IOException, JsonPatchException { + + // Parse the patch to JsonNode + JsonNode patchNode = objectMapper.readTree(patchString); + + // Create the patch + JsonPatch patch = JsonPatch.fromJson(patchNode); - // Parse the patch to JsonNode - JsonNode patchNode = objectMapper.readTree(patchString); + // Convert the original object to JsonNode + JsonNode originalObjNode = objectMapper.valueToTree(originalObj); - // Create the patch - JsonPatch patch = JsonPatch.fromJson(patchNode); + // Apply the patch + TreeNode patchedObjNode = patch.apply(originalObjNode); - // Convert the original object to JsonNode - JsonNode originalObjNode = objectMapper.valueToTree(originalObj); + // Convert the patched node to an updated obj + return objectMapper.treeToValue(patchedObjNode, (Class) originalObj.getClass()); + } - // Apply the patch - TreeNode patchedObjNode = patch.apply(originalObjNode); - // Convert the patched node to an updated obj - return objectMapper.treeToValue(patchedObjNode, (Class) originalObj.getClass()); - } + /** + * Reads a resource into a String + */ + public static String toStr(Resource resource) throws IOException { + + String text = null; + try (Scanner scanner = new Scanner(resource.getInputStream(), StandardCharsets.UTF_8.name())) { + text = scanner.useDelimiter("\\A").next(); + } + + return text; + } + + public static ObjectMapper mapper() { + + return objectMapper; + } + + + /** + * Gets the reference to an application-context bean + */ + public static T getBean(Class clazz) { + return applicationContext.getBean(clazz); + } + + + /** + * Generates a random unique string + */ + public static String uid() { + + return UUID.randomUUID().toString(); + } + + + /** + * Serializes an object to JSON string + */ + public static String toJson(T obj) { + + try { + + return objectMapper.writeValueAsString(obj); + + } catch (JsonProcessingException e) { + + throw new RuntimeException(e); + } + } + + + /** + * Deserializes a JSON String + */ + public static T fromJson(String json, Class clazz) { + + try { + + return objectMapper.readValue(json, clazz); + + } catch (IOException e) { + + throw new RuntimeException(e); + } + } + + /** + * Serializes an object + */ + public static String serialize(Serializable obj) { + + return Base64.getUrlEncoder().encodeToString( + SerializationUtils.serialize(obj)); + } + + /** + * Deserializes an object + */ + public static T deserialize(String serializedObj) { + + return SerializationUtils.deserialize( + Base64.getUrlDecoder().decode(serializedObj)); + } + + + public static UserDto getUserDto(JWTClaimsSet claims) { + + Object userClaim = claims.getClaim(BlueTokenService.USER_CLAIM); + + if (userClaim == null) + return null; + + return LecUtils.deserialize((String) userClaim); + } } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/util/UserUtils.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/util/UserUtils.java index 0ef06f1a..dc1a3d46 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/util/UserUtils.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/util/UserUtils.java @@ -1,10 +1,25 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.util; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; - public class UserUtils { private static final Log log = LogFactory.getLog(UserUtils.class); @@ -44,7 +59,7 @@ public interface ChangeEmailValidation { public interface SignupInput { } - public static boolean hasPermission(ID id, UserDto currentUser, String permission) { + public static boolean hasPermission(ID id, UserDto currentUser, String permission) { log.debug("Computing " + permission + " permission for User " + id + "\n Logged in user: " + currentUser); @@ -53,7 +68,7 @@ public static boolean hasPermission(ID id, UserDto currentUser, String p if (currentUser == null) return false; - boolean isSelf = currentUser.getId().equals(id); + boolean isSelf = currentUser.getId().equals(id.toString()); return isSelf || currentUser.isGoodAdmin(); // self or admin; } diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/Captcha.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/Captcha.java similarity index 51% rename from spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/Captcha.java rename to spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/Captcha.java index 7c848b1e..63c93ed7 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/Captcha.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/Captcha.java @@ -1,9 +1,24 @@ -package com.naturalprogrammer.spring.lemon.validation; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +package com.naturalprogrammer.spring.lemon.commons.validation; import javax.validation.Constraint; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Captcha validation constraint annotation diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/CaptchaValidator.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/CaptchaValidator.java similarity index 77% rename from spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/CaptchaValidator.java rename to spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/CaptchaValidator.java index ce2bac86..42a60d53 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/CaptchaValidator.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/CaptchaValidator.java @@ -1,21 +1,34 @@ -package com.naturalprogrammer.spring.lemon.validation; - -import java.util.Collection; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +package com.naturalprogrammer.spring.lemon.commons.validation; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Collection; /** * Captcha validation constraint @@ -61,10 +74,10 @@ public void setErrorCodes(Collection errorCodes) { private LemonProperties properties; private RestTemplate restTemplate; - public CaptchaValidator(LemonProperties properties, RestTemplateBuilder restTemplateBuilder) { + public CaptchaValidator(LemonProperties properties) { this.properties = properties; - this.restTemplate = restTemplateBuilder.build();; + this.restTemplate = new RestTemplate(); log.info("Created"); } @@ -90,8 +103,7 @@ public boolean isValid(String captchaResponse, ConstraintValidatorContext contex return false; // Prepare the form data for sending to google - MultiValueMap formData = - new LinkedMultiValueMap(2); + MultiValueMap formData = new LinkedMultiValueMap<>(2); formData.add("response", captchaResponse); formData.add("secret", properties.getRecaptcha().getSecretkey()); @@ -115,8 +127,8 @@ public boolean isValid(String captchaResponse, ConstraintValidatorContext contex log.info("Captcha validation failed."); return false; - } catch (Throwable t) { - log.error(ExceptionUtils.getStackTrace(t)); + } catch (Exception e) { + log.error(ExceptionUtils.getStackTrace(e)); return false; } } diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/Password.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/Password.java index 04cea093..b7b16ab5 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/Password.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/Password.java @@ -1,15 +1,30 @@ -package com.naturalprogrammer.spring.lemon.commons.validation; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import static java.lang.annotation.RetentionPolicy.RUNTIME; +package com.naturalprogrammer.spring.lemon.commons.validation; -import java.lang.annotation.Retention; +import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; +import java.lang.annotation.Retention; -import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * Annotation for password constraint diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePassword.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePassword.java index bd98e3c9..1286b65f 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePassword.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePassword.java @@ -1,10 +1,25 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.validation; +import javax.validation.Constraint; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import javax.validation.Constraint; - /** * Annotation for retype password constraint * diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePasswordForm.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePasswordForm.java index 743d34cf..30136150 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePasswordForm.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePasswordForm.java @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.validation; /** diff --git a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePasswordValidator.java b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePasswordValidator.java index 7e89db99..0aeeacb0 100644 --- a/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePasswordValidator.java +++ b/spring-lemon-commons/src/main/java/com/naturalprogrammer/spring/lemon/commons/validation/RetypePasswordValidator.java @@ -1,12 +1,27 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.commons.validation; -import java.util.Objects; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import java.util.Objects; /** * Validator for RetypePassword constraint diff --git a/spring-lemon-commons/src/test/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJwtServiceTests.java b/spring-lemon-commons/src/test/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJwtServiceTests.java new file mode 100644 index 00000000..fcad100e --- /dev/null +++ b/spring-lemon-commons/src/test/java/com/naturalprogrammer/spring/lemon/commons/security/LemonJwtServiceTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.commons.security; + +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.BadCredentialsException; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class LemonJwtServiceTests { + + private static final Log log = LogFactory.getLog(LemonJwtServiceTests.class); + + // An aes-128-cbc key generated at https://asecuritysite.com/encryption/keygen (take the "key" field) + private static final String SECRET1 = "926D96C90030DD58429D2751AC1BDBBC"; + private static final String SECRET2 = "538518AB685B514685DA8055C03DDA63"; + + private final LemonJweService jweService1; + private final LemonJweService jweService2; + private final LemonJwsService jwsService1; + private final LemonJwsService jwsService2; + + public LemonJwtServiceTests() throws JOSEException { + + jweService1 = new LemonJweService(SECRET1); + jweService2 = new LemonJweService(SECRET2); + + jwsService1 = new LemonJwsService(SECRET1); + jwsService2 = new LemonJwsService(SECRET2); + } + + @Test + void testParseToken() { + + testParseToken(jweService1); + testParseToken(jwsService1); + } + + private void testParseToken(LemonTokenService service) { + + log.info("Creating token ..." + service.getClass().getSimpleName()); + String token = service.createToken("auth", "subject", 5000L, + LecUtils.mapOf("username", "abc@example.com")); + + log.info("Parsing token ..."); + JWTClaimsSet claims = service.parseToken(token, "auth"); + + log.info("Parsed token."); + assertEquals("subject", claims.getSubject()); + assertEquals("abc@example.com", claims.getClaim("username")); + } + + @Test + void testParseJweTokenWrongAudience() { + + Assertions.assertThrows(BadCredentialsException.class, () -> { + testParseTokenWrongAudience(jweService1); + }); + } + + @Test + void testParseJwsTokenWrongAudience() { + + Assertions.assertThrows(BadCredentialsException.class, () -> { + testParseTokenWrongAudience(jwsService1); + }); + } + + private void testParseTokenWrongAudience(LemonTokenService service) { + + String token = service.createToken("auth", "subject", 5000L); + service.parseToken(token, "auth2"); + } + + @Test + void testParseJweTokenExpired() throws InterruptedException { + + Assertions.assertThrows(BadCredentialsException.class, () -> { + testParseTokenExpired(jweService1); + }); + } + + @Test + void testParseJwsTokenExpired() throws InterruptedException { + + Assertions.assertThrows(BadCredentialsException.class, () -> { + testParseTokenExpired(jwsService1); + }); + } + + private void testParseTokenExpired(LemonTokenService service) throws InterruptedException { + + String token = service.createToken("auth", "subject", 1L); + Thread.sleep(1L); + service.parseToken(token, "auth"); + } + + @Test + void testParseJweTokenWrongSecret() { + + Assertions.assertThrows(BadCredentialsException.class, () -> { + testParseTokenWrongSecret(jweService1, jweService2); + }); + } + + @Test + void testParseJwsTokenWrongSecret() { + + Assertions.assertThrows(BadCredentialsException.class, () -> { + testParseTokenWrongSecret(jwsService1, jwsService2); + }); + } + + private void testParseTokenWrongSecret(LemonTokenService service1, LemonTokenService service2) { + + String token = service1.createToken("auth", "subject", 5000L); + service2.parseToken(token, "auth"); + } + + @Test + void testParseJweTokenCutoffTime() throws InterruptedException { + + Assertions.assertThrows(BadCredentialsException.class, () -> { + testParseTokenCutoffTime(jweService1); + }); + } + + @Test + void testParseJwsTokenCutoffTime() throws InterruptedException { + + Assertions.assertThrows(BadCredentialsException.class, () -> { + testParseTokenCutoffTime(jwsService1); + }); + } + + + private void testParseTokenCutoffTime(LemonTokenService service) throws InterruptedException { + + String token = service.createToken("auth", "subject", 5000L); + service.parseToken(token, "auth", System.currentTimeMillis() + 1); + } +} diff --git a/spring-lemon-exceptions/pom.xml b/spring-lemon-exceptions/pom.xml index 195cd838..bed4f5ec 100644 --- a/spring-lemon-exceptions/pom.xml +++ b/spring-lemon-exceptions/pom.xml @@ -1,39 +1,65 @@ - - - 4.0.0 - - com.naturalprogrammer.spring-lemon - spring-lemon-exceptions - jar - - spring-lemon-exceptions - Helper exception handling library for Spring Boot REST APIs - - - com.naturalprogrammer - spring-lemon - 1.0.0.M4 - - - - - - org.springframework - spring-web - - - - org.springframework.boot - spring-boot-starter-validation - - - - org.apache.commons - commons-lang3 - RELEASE - - - - - + + + 4.0.0 + + com.naturalprogrammer.spring-lemon + spring-lemon-exceptions + jar + + ${project.groupId}:${project.artifactId} + Helper exception handling library for Spring Boot REST APIs + + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + com.naturalprogrammer + spring-lemon + 1.0.2 + + + + + + org.springframework + spring-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.apache.commons + commons-lang3 + 3.11 + + + + + diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ErrorResponse.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ErrorResponse.java index bf2f2168..5e487c45 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ErrorResponse.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ErrorResponse.java @@ -1,14 +1,36 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.exceptions; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + import java.util.Collection; /** * Error DTO, to be sent as response body * in case of errors */ +@Getter @Setter @ToString public class ErrorResponse { - - private String exception; + + private String id; + private String exceptionId; private String error; private String message; private Integer status; // We'd need it as integer in JSON serialization @@ -18,35 +40,4 @@ public boolean incomplete() { return message == null || status == null; } - - public String getException() { - return exception; - } - public void setException(String exception) { - this.exception = exception; - } - public String getError() { - return error; - } - public void setError(String error) { - this.error = error; - } - public String getMessage() { - return message; - } - public void setMessage(String message) { - this.message = message; - } - public Integer getStatus() { - return status; - } - public void setStatus(Integer status) { - this.status = status; - } - public Collection getErrors() { - return errors; - } - public void setErrors(Collection errors) { - this.errors = errors; - } } diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ErrorResponseComposer.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ErrorResponseComposer.java index 9dd63ac8..bb492ff4 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ErrorResponseComposer.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ErrorResponseComposer.java @@ -1,17 +1,32 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.exceptions; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; + import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; - -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; - /** * Given an exception, builds a response. */ @@ -19,18 +34,18 @@ public class ErrorResponseComposer { private static final Log log = LogFactory.getLog(ErrorResponseComposer.class); - private final Map> handlers; + private final Map, AbstractExceptionHandler> handlers; public ErrorResponseComposer(List> handlers) { this.handlers = handlers.stream().collect( - Collectors.toMap(AbstractExceptionHandler::getExceptionName, - Function.identity(), (handler1, handler2) -> { + Collectors.toMap(AbstractExceptionHandler::getExceptionClass, + Function.identity(), (handler1, handler2) -> - return AnnotationAwareOrderComparator - .INSTANCE.compare(handler1, handler2) < 0 ? - handler1 : handler2; - })); + AnnotationAwareOrderComparator + .INSTANCE.compare(handler1, handler2) < 0 ? + handler1 : handler2 + )); log.info("Created"); } @@ -49,7 +64,7 @@ public Optional compose(T ex) { while (ex != null) { - handler = handlers.get(ex.getClass().getSimpleName()); + handler = handlers.get(ex.getClass()); if (handler != null) // found a handler break; diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ExceptionIdMaker.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ExceptionIdMaker.java new file mode 100644 index 00000000..6d3eb4e0 --- /dev/null +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ExceptionIdMaker.java @@ -0,0 +1,23 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.exceptions; + +@FunctionalInterface +public interface ExceptionIdMaker { + + String make(Throwable t); +} diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ExplicitConstraintViolationException.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ExplicitConstraintViolationException.java deleted file mode 100644 index f00122b1..00000000 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/ExplicitConstraintViolationException.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.naturalprogrammer.spring.lemon.exceptions; - -import java.util.Set; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; - -import lombok.Getter; - -public class ExplicitConstraintViolationException extends ConstraintViolationException { - - private static final long serialVersionUID = 3723548255231135762L; - - @Getter - private final String objectName; - - public ExplicitConstraintViolationException(Set> constraintViolations, String objectName) { - super(constraintViolations); - this.objectName = objectName; - } -} diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonExceptionsAutoConfiguration.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonExceptionsAutoConfiguration.java index 33354438..eebdb1d6 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonExceptionsAutoConfiguration.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonExceptionsAutoConfiguration.java @@ -1,25 +1,43 @@ -package com.naturalprogrammer.spring.lemon.exceptions; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.util.List; +package com.naturalprogrammer.spring.lemon.exceptions; +import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import java.util.List; @Configuration +@AutoConfigureBefore({ValidationAutoConfiguration.class}) @ComponentScan(basePackageClasses=AbstractExceptionHandler.class) +@Slf4j public class LemonExceptionsAutoConfiguration { - private static final Log log = LogFactory.getLog(LemonExceptionsAutoConfiguration.class); - public LemonExceptionsAutoConfiguration() { log.info("Created"); } @@ -36,15 +54,29 @@ ErrorResponseComposer errorResponseComposer(List> log.info("Configuring ErrorResponseComposer"); return new ErrorResponseComposer(handlers); } + + /** + * Configures ExceptionCodeMaker if missing + */ + @Bean + @ConditionalOnMissingBean(ExceptionIdMaker.class) + public ExceptionIdMaker exceptionIdMaker() { + + log.info("Configuring ExceptionIdMaker"); + return LexUtils.EXCEPTION_ID_MAKER; + } + /** - * Configures LemonUtils + * Configures LexUtils */ @Bean - public LexUtils lexUtils(MessageSource messageSource, LocalValidatorFactoryBean validator) { + public LexUtils lexUtils(MessageSource messageSource, + LocalValidatorFactoryBean validator, + ExceptionIdMaker exceptionIdMaker) { log.info("Configuring LexUtils"); - return new LexUtils(messageSource, validator); + return new LexUtils(messageSource, validator, exceptionIdMaker); } } diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonFieldError.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonFieldError.java index f32c7481..26d61aa9 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonFieldError.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/LemonFieldError.java @@ -1,63 +1,53 @@ -package com.naturalprogrammer.spring.lemon.exceptions; - -import java.util.Collection; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.validation.ConstraintViolation; +package com.naturalprogrammer.spring.lemon.exceptions; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; import org.apache.commons.lang3.StringUtils; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.bind.support.WebExchangeBindException; +import javax.validation.ConstraintViolation; +import java.io.Serializable; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + /** * Holds a field or form error - * - * @author Sanjay Patel */ -public class LemonFieldError { +@Getter @AllArgsConstructor @ToString +public class LemonFieldError implements Serializable { // Name of the field. Null in case of a form level error. - private String field; + private final String field; // Error code. Typically the I18n message-code. - private String code; + private final String code; // Error message - private String message; - - - public LemonFieldError(String field, String code, String message) { - this.field = field; - this.code = code; - this.message = message; - } - - public String getField() { - return field; - } - - public String getCode() { - return code; - } - - public String getMessage() { - return message; - } - - @Override - public String toString() { - return "FieldError {field=" + field + ", code=" + code + ", message=" + message + "}"; - } - + private final String message; /** * Converts a set of ConstraintViolations * to a list of FieldErrors - * - * @param constraintViolations */ public static List getErrors( Set> constraintViolations) { @@ -67,17 +57,6 @@ public static List getErrors( } - public static Collection getErrors(ExplicitConstraintViolationException ex) { - - return ex.getConstraintViolations().stream() - .map(constraintViolation -> - new LemonFieldError( - constraintViolation.getPropertyPath().toString(), - constraintViolation.getMessageTemplate(), - constraintViolation.getMessage())) - .collect(Collectors.toList()); - } - /** * Converts a ConstraintViolation * to a FieldError diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/MultiErrorException.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/MultiErrorException.java index d83e2fcf..a97acf7e 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/MultiErrorException.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/MultiErrorException.java @@ -1,11 +1,31 @@ -package com.naturalprogrammer.spring.lemon.exceptions; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.util.ArrayList; -import java.util.List; +package com.naturalprogrammer.spring.lemon.exceptions; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import lombok.AccessLevel; +import lombok.Getter; import org.springframework.http.HttpStatus; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import javax.validation.ConstraintViolation; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** * An exception class which can contain multiple errors. @@ -13,6 +33,7 @@ * * @author Sanjay Patel */ +@Getter public class MultiErrorException extends RuntimeException { private static final long serialVersionUID = 6020532846519363456L; @@ -23,15 +44,45 @@ public class MultiErrorException extends RuntimeException { // HTTP Status code to be returned private HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY; + // Set this if you need to customize exceptionId + private String exceptionId = null; + + // Set this if you're doing bean validation and using validation groups + @Getter(AccessLevel.NONE) + private Class[] validationGroups = {}; + + /** + * Overrides the standard getMessage + */ + @Override + public String getMessage() { + + if (errors.isEmpty()) + return null; + + // return the first message + return errors.get(0).getMessage(); + } + public MultiErrorException httpStatus(HttpStatus status) { this.status = status; return this; } + public MultiErrorException exceptionId(String exceptionId) { + this.exceptionId = exceptionId; + return this; + } + + public MultiErrorException validationGroups(Class... groups) { + validationGroups = groups; + return this; + } + /** * Adds a field-error if the given condition isn't true */ - public MultiErrorException validate(String fieldName, boolean valid, + public MultiErrorException validateField(String fieldName, boolean valid, String messageKey, Object... args) { if (!valid) @@ -41,14 +92,6 @@ public MultiErrorException validate(String fieldName, boolean valid, return this; } - /** - * Throws the exception, if there are accumulated errors - */ - public void go() { - if (!errors.isEmpty()) - throw this; - } - /** * Adds a global-error if the given condition isn't true */ @@ -56,31 +99,41 @@ public MultiErrorException validate(boolean valid, String messageKey, Object... args) { // delegate - return validate(null, valid, messageKey, args); + return validateField(null, valid, messageKey, args); } - /** - * Overrides the standard getMessage - */ - @Override - public String getMessage() { - - if (errors.isEmpty()) - return null; + public MultiErrorException validateBean(String beanName, T bean) { - // return the first message - return errors.get(0).getMessage(); + Set> constraintViolations = + LexUtils.validator().validate(bean, validationGroups); + + addErrors(constraintViolations, beanName); + return this; } - public HttpStatus getStatus() { - return status; - } + /** + * Throws the exception, if there are accumulated errors + */ + public void go() { + if (!errors.isEmpty()) + throw this; + } - public void setStatus(HttpStatus status) { - this.status = status; + /** + * Adds constraint violations + * + * @param constraintViolations + * @param objectName + * @return + */ + private void addErrors(Set> constraintViolations, String objectName) { + + errors.addAll(constraintViolations.stream() + .map(constraintViolation -> + new LemonFieldError( + objectName + "." + constraintViolation.getPropertyPath().toString(), + constraintViolation.getMessageTemplate(), + constraintViolation.getMessage())) + .collect(Collectors.toList())); } - - public List getErrors() { - return errors; - } } diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/VersionException.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/VersionException.java index fa74a57b..02a94800 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/VersionException.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/VersionException.java @@ -1,10 +1,25 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.exceptions; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; - /** * Version exception, to be thrown when concurrent * updates of an entity is noticed. diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractBadRequestExceptionHandler.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractBadRequestExceptionHandler.java index 974c400a..7bcfc478 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractBadRequestExceptionHandler.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractBadRequestExceptionHandler.java @@ -1,3 +1,19 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.exceptions.handlers; import org.springframework.core.Ordered; @@ -10,8 +26,8 @@ @Order(Ordered.LOWEST_PRECEDENCE) public abstract class AbstractBadRequestExceptionHandler extends AbstractExceptionHandler { - public AbstractBadRequestExceptionHandler(String exceptionName) { - super(exceptionName); + public AbstractBadRequestExceptionHandler(Class exceptionClass) { + super(exceptionClass); } @Override diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractExceptionHandler.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractExceptionHandler.java index b70cbe57..b60df021 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractExceptionHandler.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractExceptionHandler.java @@ -1,13 +1,30 @@ -package com.naturalprogrammer.spring.lemon.exceptions.handlers; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.util.Collection; +package com.naturalprogrammer.spring.lemon.exceptions.handlers; +import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponse; +import com.naturalprogrammer.spring.lemon.exceptions.LemonFieldError; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.HttpStatus; -import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponse; -import com.naturalprogrammer.spring.lemon.exceptions.LemonFieldError; +import java.util.Collection; +import java.util.UUID; /** * Extend this to code an exception handler @@ -16,16 +33,20 @@ public abstract class AbstractExceptionHandler { protected final Log log = LogFactory.getLog(this.getClass()); - private String exceptionName; + private final Class exceptionClass; - public AbstractExceptionHandler(String exceptionName) { - this.exceptionName = exceptionName; + public AbstractExceptionHandler(Class exceptionClass) { + this.exceptionClass = exceptionClass; } - public String getExceptionName() { - return exceptionName; + public Class getExceptionClass() { + return exceptionClass; } + protected String getExceptionId(T ex) { + return LexUtils.getExceptionId(ex); + } + protected String getMessage(T ex) { return ex.getMessage(); } @@ -39,9 +60,11 @@ protected Collection getErrors(T ex) { } public ErrorResponse getErrorResponse(T ex) { - + ErrorResponse errorResponse = new ErrorResponse(); - + + errorResponse.setId(UUID.randomUUID().toString()); + errorResponse.setExceptionId(getExceptionId(ex)); errorResponse.setMessage(getMessage(ex)); HttpStatus status = getStatus(ex); @@ -51,7 +74,6 @@ public ErrorResponse getErrorResponse(T ex) { } errorResponse.setErrors(getErrors(ex)); - return errorResponse; } } diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractValidationExceptionHandler.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractValidationExceptionHandler.java new file mode 100644 index 00000000..40b5fb82 --- /dev/null +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/AbstractValidationExceptionHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.exceptions.handlers; + +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; + +/** + * Extend this for any exception handler that should return a 400 response + */ +@Order(Ordered.LOWEST_PRECEDENCE) +public abstract class AbstractValidationExceptionHandler extends AbstractExceptionHandler { + + public AbstractValidationExceptionHandler(Class exceptionClass) { + super(exceptionClass); + } + + @Override + public HttpStatus getStatus(T ex) { + return HttpStatus.UNPROCESSABLE_ENTITY; + } + + @Override + public String getMessage(T ex) { + return LexUtils.getMessage("com.naturalprogrammer.spring.validationError"); + } +} diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/ConstraintViolationExceptionHandler.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/ConstraintViolationExceptionHandler.java index d35fa72d..966237b0 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/ConstraintViolationExceptionHandler.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/ConstraintViolationExceptionHandler.java @@ -1,43 +1,42 @@ -package com.naturalprogrammer.spring.lemon.exceptions.handlers; - -import java.util.Collection; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.validation.ConstraintViolationException; +package com.naturalprogrammer.spring.lemon.exceptions.handlers; +import com.naturalprogrammer.spring.lemon.exceptions.LemonFieldError; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import com.naturalprogrammer.spring.lemon.exceptions.LemonFieldError; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import javax.validation.ConstraintViolationException; +import java.util.Collection; @Component @Order(Ordered.LOWEST_PRECEDENCE) -public class ConstraintViolationExceptionHandler extends AbstractExceptionHandler { +public class ConstraintViolationExceptionHandler extends AbstractValidationExceptionHandler { public ConstraintViolationExceptionHandler() { - super(ConstraintViolationException.class.getSimpleName()); + super(ConstraintViolationException.class); log.info("Created"); } - public ConstraintViolationExceptionHandler(String exceptionName) { - super(exceptionName); - } - - @Override - public HttpStatus getStatus(E ex) { - return HttpStatus.UNPROCESSABLE_ENTITY; - } - @Override - public Collection getErrors(E ex) { + public Collection getErrors(ConstraintViolationException ex) { return LemonFieldError.getErrors(ex.getConstraintViolations()); } - - @Override - public String getMessage(E ex) { - return LexUtils.getMessage("com.naturalprogrammer.spring.validationError"); - } + } diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/ExplicitConstraintViolationExceptionHandler.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/ExplicitConstraintViolationExceptionHandler.java deleted file mode 100644 index 935f0756..00000000 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/ExplicitConstraintViolationExceptionHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.naturalprogrammer.spring.lemon.exceptions.handlers; - -import java.util.Collection; - -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; - -import com.naturalprogrammer.spring.lemon.exceptions.ExplicitConstraintViolationException; -import com.naturalprogrammer.spring.lemon.exceptions.LemonFieldError; - -@Component -@Order(Ordered.LOWEST_PRECEDENCE) -public class ExplicitConstraintViolationExceptionHandler - extends ConstraintViolationExceptionHandler { - - public ExplicitConstraintViolationExceptionHandler() { - - super(ExplicitConstraintViolationException.class.getSimpleName()); - log.info("Created"); - } - - @Override - public Collection getErrors(ExplicitConstraintViolationException ex) { - return LemonFieldError.getErrors(ex); - } -} diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/MultiErrorExceptionHandler.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/MultiErrorExceptionHandler.java index 5583b114..49a3bbba 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/MultiErrorExceptionHandler.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/handlers/MultiErrorExceptionHandler.java @@ -1,14 +1,29 @@ -package com.naturalprogrammer.spring.lemon.exceptions.handlers; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.util.Collection; +package com.naturalprogrammer.spring.lemon.exceptions.handlers; +import com.naturalprogrammer.spring.lemon.exceptions.LemonFieldError; +import com.naturalprogrammer.spring.lemon.exceptions.MultiErrorException; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import com.naturalprogrammer.spring.lemon.exceptions.LemonFieldError; -import com.naturalprogrammer.spring.lemon.exceptions.MultiErrorException; +import java.util.Collection; @Component @Order(Ordered.LOWEST_PRECEDENCE) @@ -16,10 +31,19 @@ public class MultiErrorExceptionHandler extends AbstractExceptionHandler { +public class WebExchangeBindExceptionHandler extends AbstractValidationExceptionHandler { public WebExchangeBindExceptionHandler() { - super(WebExchangeBindException.class.getSimpleName()); + super(WebExchangeBindException.class); log.info("Created"); } - @Override - public HttpStatus getStatus(WebExchangeBindException ex) { - return HttpStatus.UNPROCESSABLE_ENTITY; - } - @Override public Collection getErrors(WebExchangeBindException ex) { return LemonFieldError.getErrors(ex); } - - @Override - public String getMessage(WebExchangeBindException ex) { - return LexUtils.getMessage("com.naturalprogrammer.spring.validationError"); - } } diff --git a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/util/LexUtils.java b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/util/LexUtils.java index 389dcf6c..2b6dbbe1 100644 --- a/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/util/LexUtils.java +++ b/spring-lemon-exceptions/src/main/java/com/naturalprogrammer/spring/lemon/exceptions/util/LexUtils.java @@ -1,43 +1,69 @@ -package com.naturalprogrammer.spring.lemon.exceptions.util; - -import java.util.Set; -import java.util.function.Supplier; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.annotation.PostConstruct; -import javax.validation.ConstraintViolation; +package com.naturalprogrammer.spring.lemon.exceptions.util; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import com.naturalprogrammer.spring.lemon.exceptions.ExceptionIdMaker; +import com.naturalprogrammer.spring.lemon.exceptions.MultiErrorException; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.http.HttpStatus; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import com.naturalprogrammer.spring.lemon.exceptions.ExplicitConstraintViolationException; -import com.naturalprogrammer.spring.lemon.exceptions.MultiErrorException; +import javax.annotation.PostConstruct; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.function.Supplier; /** * Useful helper methods * * @author Sanjay Patel */ +@Slf4j public class LexUtils { - - private static final Log log = LogFactory.getLog(LexUtils.class); private static MessageSource messageSource; private static LocalValidatorFactoryBean validator; + private static ExceptionIdMaker exceptionIdMaker; + + private static final Validator DEFAULT_VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator(); + public static final ExceptionIdMaker EXCEPTION_ID_MAKER = ex -> { + + if (ex == null) + return null; + + return ex.getClass().getSimpleName(); + }; + + public static final MultiErrorException NOT_FOUND_EXCEPTION = new MultiErrorException(); /** * Constructor - * - * @param messageSource */ - public LexUtils(MessageSource messageSource, LocalValidatorFactoryBean validator) { + public LexUtils(MessageSource messageSource, + LocalValidatorFactoryBean validator, + ExceptionIdMaker exceptionIdMaker) { LexUtils.messageSource = messageSource; LexUtils.validator = validator; + LexUtils.exceptionIdMaker = exceptionIdMaker; log.info("Created"); } @@ -56,14 +82,11 @@ public void postConstruct() { /** * Gets a message from messages.properties - * - * @param messageKey the key of the message - * @param args any arguments */ public static String getMessage(String messageKey, Object... args) { - if (messageSource == null) - return "ApplicationContext unavailable, probably unit test going on"; + if (messageSource == null) // ApplicationContext unavailable, probably unit test going on + return messageKey; // http://stackoverflow.com/questions/10792551/how-to-obtain-a-current-user-locale-from-spring-without-passing-it-as-a-paramete return messageSource.getMessage(messageKey, args, @@ -73,57 +96,42 @@ public static String getMessage(String messageKey, Object... args) { /** * Creates a MultiErrorException out of the given parameters - * - * @param valid the condition to check for - * @param messageKey key of the error message - * @param args any message arguments */ - public static void validate(String name, T object, Class... groups) { - - Set> violations = validator.validate(object, groups); + public static MultiErrorException validate( + boolean valid, String messageKey, Object... args) { - if (!violations.isEmpty()) - throw new ExplicitConstraintViolationException(violations, name); + return validateField(null, valid, messageKey, args); } /** * Creates a MultiErrorException out of the given parameters - * - * @param valid the condition to check for - * @param messageKey key of the error message - * @param args any message arguments */ - public static MultiErrorException validate( - boolean valid, String messageKey, Object... args) { + public static MultiErrorException validateField( + String fieldName, boolean valid, String messageKey, Object... args) { - return LexUtils.validate(null, valid, messageKey, args); + return new MultiErrorException().validateField(fieldName, valid, messageKey, args); } /** - * Creates a MultiErrorException out of the given parameters - * - * @param fieldName the name of the field related to the error - * @param valid the condition to check for - * @param messageKey key of the error message - * @param args any message arguments + * Creates a MultiErrorException out of the constraint violations in the given bean */ - public static MultiErrorException validate( - String fieldName, boolean valid, String messageKey, Object... args) { + public static MultiErrorException validateBean(String beanName, T bean, Class... validationGroups) { - return new MultiErrorException().validate(fieldName, valid, messageKey, args); + return new MultiErrorException() + .exceptionId(getExceptionId(new ConstraintViolationException(null))) + .validationGroups(validationGroups) + .validateBean(beanName, bean); } /** * Throws 404 Error is the entity isn't found - * - * @param entity */ public static void ensureFound(T entity) { - LexUtils.validate(entity != null, + validate(entity != null, "com.naturalprogrammer.spring.notFound") .httpStatus(HttpStatus.NOT_FOUND).go(); } @@ -136,4 +144,33 @@ public static Supplier notFoundSupplier() { return () -> NOT_FOUND_EXCEPTION; } + + + public static String getExceptionId(Throwable ex) { + + Throwable root = getRootException(ex); + + if (exceptionIdMaker == null) // in unit tests + return EXCEPTION_ID_MAKER.make(ex); + + return exceptionIdMaker.make(root); + } + + + private static Throwable getRootException(Throwable ex) { + + if (ex == null) + return null; + + while(ex.getCause() != null) + ex = ex.getCause(); + + return ex; + } + + + public static Validator validator() { + return validator == null ? // e.g. in unit tests + DEFAULT_VALIDATOR : validator; + } } diff --git a/spring-lemon-jpa/pom.xml b/spring-lemon-jpa/pom.xml index f0cd37b6..8714255d 100644 --- a/spring-lemon-jpa/pom.xml +++ b/spring-lemon-jpa/pom.xml @@ -1,44 +1,59 @@ - - - 4.0.0 - - com.naturalprogrammer.spring-lemon - spring-lemon-jpa - jar - - spring-lemon-jpa - Helper library for Spring Boot JPA Web Applications - - - com.naturalprogrammer - spring-lemon - 1.0.0.M4 - - - - - - com.naturalprogrammer.spring-lemon - spring-lemon-commons - ${project.version} - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - org.springframework.boot - spring-boot-starter-web - - - - javax.xml.bind - jaxb-api - - - - - + + + 4.0.0 + + com.naturalprogrammer.spring-lemon + spring-lemon-jpa + jar + + ${project.groupId}:${project.artifactId} + Helper library for Spring Boot JPA Web Applications + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + com.naturalprogrammer + spring-lemon + 1.0.2 + + + + + + com.naturalprogrammer.spring-lemon + spring-lemon-commons-jpa + ${project.version} + + + + javax.xml.bind + jaxb-api + + + + + diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonAutoConfiguration.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonAutoConfiguration.java index 29270a2b..11d209fc 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonAutoConfiguration.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonAutoConfiguration.java @@ -1,60 +1,48 @@ -package com.naturalprogrammer.spring.lemon; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.io.Serializable; -import java.util.List; +package com.naturalprogrammer.spring.lemon; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.domain.IdConverter; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.validation.RetypePasswordValidator; +import com.naturalprogrammer.spring.lemon.commonsjpa.LemonCommonsJpaAutoConfiguration; +import com.naturalprogrammer.spring.lemon.commonsweb.security.LemonWebSecurityConfig; +import com.naturalprogrammer.spring.lemon.domain.AbstractUser; +import com.naturalprogrammer.spring.lemon.domain.AbstractUserRepository; +import com.naturalprogrammer.spring.lemon.security.*; +import com.naturalprogrammer.spring.lemon.util.LemonUtils; +import com.naturalprogrammer.spring.lemon.validation.UniqueEmailValidator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver; -import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.domain.AuditorAware; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -import org.springframework.data.web.config.EnableSpringDataWebSupport; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.transaction.annotation.EnableTransactionManagement; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.naturalprogrammer.spring.lemon.commons.LemonProperties; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.validation.RetypePasswordValidator; -import com.naturalprogrammer.spring.lemon.domain.AbstractUser; -import com.naturalprogrammer.spring.lemon.domain.AbstractUserRepository; -import com.naturalprogrammer.spring.lemon.domain.LemonAuditorAware; -import com.naturalprogrammer.spring.lemon.exceptions.DefaultExceptionHandlerControllerAdvice; -import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponseComposer; -import com.naturalprogrammer.spring.lemon.exceptions.LemonErrorAttributes; -import com.naturalprogrammer.spring.lemon.exceptions.LemonErrorController; -import com.naturalprogrammer.spring.lemon.exceptions.LemonExceptionsAutoConfiguration; -import com.naturalprogrammer.spring.lemon.security.AuthenticationSuccessHandler; -import com.naturalprogrammer.spring.lemon.security.JwtAuthenticationProvider; -import com.naturalprogrammer.spring.lemon.security.LemonCorsConfig; -import com.naturalprogrammer.spring.lemon.security.LemonOAuth2UserService; -import com.naturalprogrammer.spring.lemon.security.LemonOidcUserService; -import com.naturalprogrammer.spring.lemon.security.LemonSecurityConfig; -import com.naturalprogrammer.spring.lemon.security.LemonUserDetailsService; -import com.naturalprogrammer.spring.lemon.security.OAuth2AuthenticationFailureHandler; -import com.naturalprogrammer.spring.lemon.security.OAuth2AuthenticationSuccessHandler; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; -import com.naturalprogrammer.spring.lemon.validation.CaptchaValidator; -import com.naturalprogrammer.spring.lemon.validation.UniqueEmailValidator; +import java.io.Serializable; /** * Spring Lemon Auto Configuration @@ -62,109 +50,34 @@ * @author Sanjay Patel */ @Configuration -@EnableSpringDataWebSupport @EnableTransactionManagement @EnableJpaAuditing -@EnableGlobalMethodSecurity(prePostEnabled = true) -@AutoConfigureBefore({ - WebMvcAutoConfiguration.class, - ErrorMvcAutoConfiguration.class, - SecurityAutoConfiguration.class, - SecurityFilterAutoConfiguration.class, - LemonExceptionsAutoConfiguration.class}) +@AutoConfigureBefore({LemonCommonsJpaAutoConfiguration.class}) public class LemonAutoConfiguration { - /** - * For handling JSON vulnerability, - * JSON response bodies would be prefixed with - * this string. - */ - public final static String JSON_PREFIX = ")]}',\n"; - private static final Log log = LogFactory.getLog(LemonAutoConfiguration.class); public LemonAutoConfiguration() { log.info("Created"); } - /** - * Prefixes JSON responses for JSON vulnerability. Disabled by default. - * To enable, add this to your application properties: - * lemon.enabled.json-prefix: true - */ - @Bean - @ConditionalOnProperty(name="lemon.enabled.json-prefix") - public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( - ObjectMapper objectMapper) { - - log.info("Configuring JSON vulnerability prefix"); - - MappingJackson2HttpMessageConverter converter = - new MappingJackson2HttpMessageConverter(objectMapper); - converter.setJsonPrefix(JSON_PREFIX); - - return converter; - } - - /** - * Configures an Auditor Aware if missing - */ - @Bean - @ConditionalOnMissingBean(AuditorAware.class) - public , ID extends Serializable> - AuditorAware auditorAware(AbstractUserRepository userRepository) { - - log.info("Configuring LemonAuditorAware"); - return new LemonAuditorAware(userRepository); - } - - /** - * Configures DefaultExceptionHandlerControllerAdvice if missing - */ @Bean - @ConditionalOnMissingBean(DefaultExceptionHandlerControllerAdvice.class) - public - DefaultExceptionHandlerControllerAdvice defaultExceptionHandlerControllerAdvice(ErrorResponseComposer errorResponseComposer) { - - log.info("Configuring DefaultExceptionHandlerControllerAdvice"); - return new DefaultExceptionHandlerControllerAdvice(errorResponseComposer); - } - - /** - * Configures an Error Attributes if missing - */ - @Bean - @ConditionalOnMissingBean(ErrorAttributes.class) - public - ErrorAttributes errorAttributes(ErrorResponseComposer errorResponseComposer) { - - log.info("Configuring LemonErrorAttributes"); - return new LemonErrorAttributes(errorResponseComposer); + @ConditionalOnMissingBean(IdConverter.class) + public + IdConverter idConverter(LemonService lemonService) { + return lemonService::toId; } - /** - * Configures an Error Controller if missing - */ - @Bean - @ConditionalOnMissingBean(ErrorController.class) - public ErrorController errorController(ErrorAttributes errorAttributes, - ServerProperties serverProperties, - List errorViewResolvers) { - - log.info("Configuring LemonErrorController"); - return new LemonErrorController(errorAttributes, serverProperties, errorViewResolvers); - } - /** * Configures AuthenticationSuccessHandler if missing */ @Bean - @ConditionalOnMissingBean(AuthenticationSuccessHandler.class) - public AuthenticationSuccessHandler authenticationSuccessHandler( + @ConditionalOnMissingBean(LemonAuthenticationSuccessHandler.class) + public LemonAuthenticationSuccessHandler authenticationSuccessHandler( ObjectMapper objectMapper, LemonService lemonService, LemonProperties properties) { log.info("Configuring AuthenticationSuccessHandler"); - return new AuthenticationSuccessHandler(objectMapper, lemonService, properties); + return new LemonAuthenticationSuccessHandler(objectMapper, lemonService, properties); } /** @@ -172,11 +85,11 @@ public AuthenticationSuccessHandler authenticationSuccessHandler( */ @Bean @ConditionalOnMissingBean(OAuth2AuthenticationSuccessHandler.class) - public OAuth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler( - LemonProperties properties, JwtService jwtService) { + public OAuth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler( + LemonProperties properties, BlueTokenService blueTokenService) { log.info("Configuring OAuth2AuthenticationSuccessHandler"); - return new OAuth2AuthenticationSuccessHandler<>(properties, jwtService); + return new OAuth2AuthenticationSuccessHandler(properties, blueTokenService); } /** @@ -206,25 +119,13 @@ public AuthenticationFailureHandler authenticationFailureHandler() { */ @Bean @ConditionalOnMissingBean(UserDetailsService.class) - public , ID extends Serializable> - UserDetailsService userDetailService(AbstractUserRepository userRepository) { + public , ID extends Serializable> + LemonUserDetailsService userDetailService(AbstractUserRepository userRepository) { log.info("Configuring LemonUserDetailsService"); return new LemonUserDetailsService(userRepository); } - /** - * Configures LemonCorsConfig if missing and lemon.cors.allowed-origins is provided - */ - @Bean - @ConditionalOnProperty(name="lemon.cors.allowed-origins") - @ConditionalOnMissingBean(LemonCorsConfig.class) - public LemonCorsConfig lemonCorsConfig(LemonProperties properties) { - - log.info("Configuring LemonCorsConfig"); - return new LemonCorsConfig(properties); - } - /** * Configures LemonOidcUserService if missing */ @@ -241,7 +142,7 @@ public LemonOidcUserService lemonOidcUserService(LemonOAuth2UserService le */ @Bean @ConditionalOnMissingBean(LemonOAuth2UserService.class) - public , ID extends Serializable> + public , ID extends Serializable> LemonOAuth2UserService lemonOAuth2UserService( LemonUserDetailsService userDetailsService, LemonService lemonService, @@ -251,51 +152,26 @@ LemonOAuth2UserService lemonOAuth2UserService( return new LemonOAuth2UserService(userDetailsService, lemonService, passwordEncoder); } - /** - * Configures JwtAuthenticationProvider if missing - */ - @Bean - @ConditionalOnMissingBean(JwtAuthenticationProvider.class) - public , ID extends Serializable> - JwtAuthenticationProvider jwtAuthenticationProvider( - JwtService jwtService, - LemonUserDetailsService userDetailsService) { - - log.info("Configuring JwtAuthenticationProvider"); - return new JwtAuthenticationProvider(jwtService, userDetailsService); - } - /** * Configures LemonSecurityConfig if missing */ @Bean - @ConditionalOnMissingBean(LemonSecurityConfig.class) - public LemonSecurityConfig lemonSecurityConfig() { + @ConditionalOnMissingBean(LemonWebSecurityConfig.class) + public LemonWebSecurityConfig lemonSecurityConfig() { - log.info("Configuring LemonSecurityConfig"); - return new LemonSecurityConfig(); + log.info("Configuring LemonJpaSecurityConfig"); + return new LemonJpaSecurityConfig(); } /** * Configures LemonUtils */ @Bean - public LemonUtils lemonUtil(ApplicationContext applicationContext, + public LemonUtils lemonUtils(ApplicationContext applicationContext, ObjectMapper objectMapper) { - log.info("Configuring LemonUtil"); - return new LemonUtils(applicationContext, objectMapper); - } - - /** - * Configures CaptchaValidator if missing - */ - @Bean - @ConditionalOnMissingBean(CaptchaValidator.class) - public CaptchaValidator captchaValidator(LemonProperties properties, RestTemplateBuilder restTemplateBuilder) { - - log.info("Configuring LemonUserDetailsService"); - return new CaptchaValidator(properties, restTemplateBuilder); + log.info("Configuring LemonUtils"); + return new LemonUtils(); } /** @@ -317,5 +193,6 @@ public UniqueEmailValidator uniqueEmailValidator(AbstractUserRepository us log.info("Configuring UniqueEmailValidator"); return new UniqueEmailValidator(userRepository); - } + } + } diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonController.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonController.java index 835270fb..4b3045ef 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonController.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonController.java @@ -1,37 +1,44 @@ -package com.naturalprogrammer.spring.lemon; - -import java.io.IOException; -import java.io.Serializable; -import java.util.Map; -import java.util.Optional; - -import javax.servlet.http.HttpServletResponse; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; +package com.naturalprogrammer.spring.lemon; import com.fasterxml.jackson.annotation.JsonView; -import com.fasterxml.jackson.core.JsonProcessingException; import com.github.fge.jsonpatch.JsonPatchException; import com.naturalprogrammer.spring.lemon.commons.LemonProperties; import com.naturalprogrammer.spring.lemon.commons.domain.ChangePasswordForm; import com.naturalprogrammer.spring.lemon.commons.domain.ResetPasswordForm; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemon.commonsweb.util.LecwUtils; import com.naturalprogrammer.spring.lemon.domain.AbstractUser; import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Serializable; +import java.util.Map; +import java.util.Optional; /** * The Lemon API. See the @@ -41,23 +48,20 @@ * @author Sanjay Patel */ public abstract class LemonController - , ID extends Serializable> { + , ID extends Serializable> { private static final Log log = LogFactory.getLog(LemonController.class); private long jwtExpirationMillis; - private JwtService jwtService; private LemonService lemonService; @Autowired public void createLemonController( LemonProperties properties, - LemonService lemonService, - JwtService jwtService) { + LemonService lemonService) { this.jwtExpirationMillis = properties.getJwt().getExpirationMillis(); this.lemonService = lemonService; - this.jwtService = jwtService; log.info("Created"); } @@ -97,7 +101,7 @@ public Map getContext( */ @PostMapping("/users") @ResponseStatus(HttpStatus.CREATED) - public UserDto signup(@RequestBody @JsonView(UserUtils.SignupInput.class) U user, + public UserDto signup(@RequestBody @JsonView(UserUtils.SignupInput.class) U user, HttpServletResponse response) { log.debug("Signing up: " + user); @@ -125,7 +129,7 @@ public void resendVerificationMail(@PathVariable("id") U user) { * Verifies current-user */ @PostMapping("/users/{id}/verification") - public UserDto verifyUser( + public UserDto verifyUser( @PathVariable ID id, @RequestParam String code, HttpServletResponse response) { @@ -153,7 +157,7 @@ public void forgotPassword(@RequestParam String email) { * Resets password after it's forgotten */ @PostMapping("/reset-password") - public UserDto resetPassword( + public UserDto resetPassword( @RequestBody ResetPasswordForm form, HttpServletResponse response) { @@ -190,18 +194,18 @@ public U fetchUserById(@PathVariable("id") U user) { * Updates a user */ @PatchMapping("/users/{id}") - public UserDto updateUser( + public UserDto updateUser( @PathVariable("id") U user, @RequestBody String patch, HttpServletResponse response) - throws JsonProcessingException, IOException, JsonPatchException { + throws IOException, JsonPatchException { log.debug("Updating user ... "); // ensure that the user exists LexUtils.ensureFound(user); U updatedUser = LecUtils.applyPatch(user, patch); // create a patched form - UserDto userDto = lemonService.updateUser(user, updatedUser); + UserDto userDto = lemonService.updateUser(user, updatedUser); // Send a new token for logged in user in the response userWithToken(response); @@ -244,7 +248,7 @@ public void requestEmailChange(@PathVariable("id") U user, * Changes the email */ @PostMapping("/users/{userId}/email") - public UserDto changeEmail( + public UserDto changeEmail( @PathVariable ID userId, @RequestParam String code, HttpServletResponse response) { @@ -271,12 +275,23 @@ public Map fetchNewToken( } + /** + * Fetch a self-sufficient token with embedded UserDto - for interservice communications + */ + @GetMapping("/fetch-full-token") + public Map fetchFullToken(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader) { + + log.debug("Fetching a micro token"); + return lemonService.fetchFullToken(authHeader); + } + + /** * returns the current user and a new authorization token in the response */ - protected UserDto userWithToken(HttpServletResponse response) { + protected UserDto userWithToken(HttpServletResponse response) { - UserDto currentUser = LemonUtils.currentUser(); + UserDto currentUser = LecwUtils.currentUser(); lemonService.addAuthHeader(response, currentUser.getUsername(), jwtExpirationMillis); return currentUser; } diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonService.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonService.java index fd484b43..2515f7c6 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonService.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/LemonService.java @@ -1,47 +1,61 @@ -package com.naturalprogrammer.spring.lemon; - -import java.io.Serializable; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import javax.servlet.http.HttpServletResponse; -import javax.validation.Valid; -import javax.validation.constraints.Email; -import javax.validation.constraints.NotBlank; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.core.oidc.StandardClaimNames; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.validation.annotation.Validated; +package com.naturalprogrammer.spring.lemon; +import com.naturalprogrammer.spring.lemon.commons.AbstractLemonService; import com.naturalprogrammer.spring.lemon.commons.LemonProperties; -import com.naturalprogrammer.spring.lemon.commons.LemonProperties.Admin; import com.naturalprogrammer.spring.lemon.commons.domain.ChangePasswordForm; import com.naturalprogrammer.spring.lemon.commons.domain.ResetPasswordForm; import com.naturalprogrammer.spring.lemon.commons.mail.LemonMailData; import com.naturalprogrammer.spring.lemon.commons.mail.MailSender; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.GreenTokenService; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemon.commons.security.UserEditPermission; import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemon.commonsjpa.LecjUtils; +import com.naturalprogrammer.spring.lemon.commonsweb.util.LecwUtils; import com.naturalprogrammer.spring.lemon.domain.AbstractUser; import com.naturalprogrammer.spring.lemon.domain.AbstractUserRepository; import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; import com.naturalprogrammer.spring.lemon.util.LemonUtils; import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; /** * The Lemon Service class @@ -51,16 +65,13 @@ @Validated @Transactional(propagation=Propagation.SUPPORTS, readOnly=true) public abstract class LemonService - , ID extends Serializable> { + , ID extends Serializable> + extends AbstractLemonService { private static final Log log = LogFactory.getLog(LemonService.class); - private LemonProperties properties; - private PasswordEncoder passwordEncoder; - private MailSender mailSender; private AbstractUserRepository userRepository; private UserDetailsService userDetailsService; - private JwtService jwtService; @Autowired public void createLemonService(LemonProperties properties, @@ -68,34 +79,21 @@ public void createLemonService(LemonProperties properties, MailSender mailSender, AbstractUserRepository userRepository, UserDetailsService userDetailsService, - JwtService jwtService) { + BlueTokenService blueTokenService, + GreenTokenService greenTokenService) { this.properties = properties; this.passwordEncoder = passwordEncoder; this.mailSender = mailSender; this.userRepository = userRepository; this.userDetailsService = userDetailsService; - this.jwtService = jwtService; + this.blueTokenService = blueTokenService; + this.greenTokenService = greenTokenService; log.info("Created"); } - /** - * This method is called after the application is ready. - * Needs to be public - otherwise Spring screams. - * - * @param event - */ - @EventListener - public void afterApplicationReady(ApplicationReadyEvent event) { - - log.info("Starting up Spring Lemon ..."); - onStartup(); // delegate to onStartup() - log.info("Spring Lemon started"); - } - - /** * Creates the initial Admin user, if not found. * Override this method if needed. @@ -118,28 +116,6 @@ public void onStartup() { } - /** - * Creates the initial Admin user. - * Override this if needed. - */ - protected U createAdminUser() { - - // fetch data about the user to be created - Admin initialAdmin = properties.getAdmin(); - - log.info("Creating the first admin user: " + initialAdmin.getUsername()); - - // create the user - U user = newUser(); - user.setEmail(initialAdmin.getUsername()); - user.setPassword(passwordEncoder.encode( - properties.getAdmin().getPassword())); - user.getRoles().add(UserUtils.Role.ADMIN); - - return user; - } - - /** * Creates a new user object. Must be overridden in the * subclass, like this: @@ -171,19 +147,16 @@ public Map getContext(Optional expirationMillis, HttpServl log.debug("Getting context ..."); - // make the context - Map sharedProperties = new HashMap(2); - sharedProperties.put("reCaptchaSiteKey", properties.getRecaptcha().getSitekey()); - sharedProperties.put("shared", properties.getShared()); + Map context = buildContext(); - UserDto currentUser = LemonUtils.currentUser(); - if (currentUser != null) + UserDto currentUser = LecwUtils.currentUser(); + if (currentUser != null) { addAuthHeader(response, currentUser.getUsername(), expirationMillis.orElse(properties.getJwt().getExpirationMillis())); + context.put("user", currentUser); + } - return LecUtils.mapOf( - "context", sharedProperties, - "user", LemonUtils.currentUser()); + return context; } @@ -200,7 +173,7 @@ public void signup(@Valid U user) { userRepository.save(user); // if successfully committed - LemonUtils.afterCommit(() -> { + LecjUtils.afterCommit(() -> { LemonUtils.login(user); // log the user in log.debug("Signed up user: " + user); @@ -212,6 +185,7 @@ public void signup(@Valid U user) { * Initializes the user based on the input data, * e.g. encrypts the password */ + @Override protected void initUser(U user) { log.debug("Initializing user: " + user); @@ -224,56 +198,14 @@ protected void initUser(U user) { /** * Makes a user unverified */ + @Override protected void makeUnverified(U user) { - user.getRoles().add(UserUtils.Role.UNVERIFIED); - user.setCredentialsUpdatedMillis(System.currentTimeMillis()); - LemonUtils.afterCommit(() -> sendVerificationMail(user)); // send a verification mail to the user + super.makeUnverified(user); + LecjUtils.afterCommit(() -> sendVerificationMail(user)); // send a verification mail to the user } - /** - * Sends verification mail to a unverified user. - */ - protected void sendVerificationMail(final U user) { - try { - - log.debug("Sending verification mail to: " + user); - - String verificationCode = jwtService.createToken(JwtService.VERIFY_AUDIENCE, - user.getId().toString(), properties.getJwt().getExpirationMillis(), - LecUtils.mapOf("email", user.getEmail())); - - // make the link - String verifyLink = properties.getApplicationUrl() - + "/users/" + user.getId() + "/verification?code=" + verificationCode; - - // send the mail - sendVerificationMail(user, verifyLink); - - log.debug("Verification mail to " + user.getEmail() + " queued."); - - } catch (Throwable e) { - // In case of exception, just log the error and keep silent - log.error(ExceptionUtils.getStackTrace(e)); - } - } - - - /** - * Sends verification mail to a unverified user. - * Override this method if you're using a different MailData - */ - protected void sendVerificationMail(final U user, String verifyLink) { - - // send the mail - mailSender.send(LemonMailData.of(user.getEmail(), - LexUtils.getMessage("com.naturalprogrammer.spring.verifySubject"), - LexUtils.getMessage( - "com.naturalprogrammer.spring.verifyEmail", verifyLink))); - } - - /** * Resends verification mail to the user. */ @@ -333,7 +265,8 @@ public void verifyUser(ID userId, String verificationCode) { LexUtils.validate(user.hasRole(UserUtils.Role.UNVERIFIED), "com.naturalprogrammer.spring.alreadyVerified").go(); - JWTClaimsSet claims = jwtService.parseToken(verificationCode, JwtService.VERIFY_AUDIENCE, user.getCredentialsUpdatedMillis()); + JWTClaimsSet claims = greenTokenService.parseToken(verificationCode, + GreenTokenService.VERIFY_AUDIENCE, user.getCredentialsUpdatedMillis()); LecUtils.ensureAuthority( claims.getSubject().equals(user.getId().toString()) && @@ -345,7 +278,7 @@ public void verifyUser(ID userId, String verificationCode) { userRepository.save(user); // after successful commit, - LemonUtils.afterCommit(() -> { + LecjUtils.afterCommit(() -> { // Re-login the user, so that the UNVERIFIED role is removed LemonUtils.login(user); @@ -372,42 +305,6 @@ public void forgotPassword(@Valid @Email @NotBlank String email) { } - /** - * Mails the forgot password link. - * - * @param user - */ - public void mailForgotPasswordLink(U user) { - - log.debug("Mailing forgot password link to user: " + user); - - String forgotPasswordCode = jwtService.createToken(JwtService.FORGOT_PASSWORD_AUDIENCE, - user.getEmail(), properties.getJwt().getExpirationMillis()); - - // make the link - String forgotPasswordLink = properties.getApplicationUrl() - + "/reset-password?code=" + forgotPasswordCode; - - mailForgotPasswordLink(user, forgotPasswordLink); - - log.debug("Forgot password link mail queued."); - } - - - /** - * Mails the forgot password link. - * - * Override this method if you're using a different MailData - */ - public void mailForgotPasswordLink(U user, String forgotPasswordLink) { - - // send the mail - mailSender.send(LemonMailData.of(user.getEmail(), - LexUtils.getMessage("com.naturalprogrammer.spring.forgotPasswordSubject"), - LexUtils.getMessage("com.naturalprogrammer.spring.forgotPasswordEmail", - forgotPasswordLink))); - } - /** * Resets the password. */ @@ -416,8 +313,8 @@ public void resetPassword(@Valid ResetPasswordForm form) { log.debug("Resetting password ..."); - JWTClaimsSet claims = jwtService.parseToken(form.getCode(), - JwtService.FORGOT_PASSWORD_AUDIENCE); + JWTClaimsSet claims = greenTokenService.parseToken(form.getCode(), + GreenTokenService.FORGOT_PASSWORD_AUDIENCE); String email = claims.getSubject(); @@ -433,13 +330,8 @@ public void resetPassword(@Valid ResetPasswordForm form) { userRepository.save(user); // after successful commit, - LemonUtils.afterCommit(() -> { - - // Login the user - LemonUtils.login(user); - }); - - log.debug("Password reset."); + LecjUtils.afterCommit(() -> LemonUtils.login(user)); + log.debug("Password reset."); } @@ -449,20 +341,20 @@ public void resetPassword(@Valid ResetPasswordForm form) { @UserEditPermission @Validated(UserUtils.UpdateValidation.class) @Transactional(propagation=Propagation.REQUIRED, readOnly=false) - public UserDto updateUser(U user, @Valid U updatedUser) { + public UserDto updateUser(U user, @Valid U updatedUser) { log.debug("Updating user: " + user); // checks - LemonUtils.ensureCorrectVersion(user, updatedUser); + LecjUtils.ensureCorrectVersion(user, updatedUser); // delegates to updateUserFields - updateUserFields(user, updatedUser, LemonUtils.currentUser()); + updateUserFields(user, updatedUser, LecwUtils.currentUser()); userRepository.save(user); log.debug("Updated user: " + user); - UserDto userDto = user.toUserDto(); + UserDto userDto = user.toUserDto(); userDto.setPassword(null); return userDto; } @@ -478,13 +370,13 @@ public String changePassword(U user, @Valid ChangePasswordForm changePasswordFor log.debug("Changing password for user: " + user); // Get the old password of the logged in user (logged in user may be an ADMIN) - UserDto currentUser = LemonUtils.currentUser(); - U loggedIn = userRepository.findById(currentUser.getId()).get(); + UserDto currentUser = LecwUtils.currentUser(); + U loggedIn = userRepository.findById(toId(currentUser.getId())).get(); String oldPassword = loggedIn.getPassword(); // checks LexUtils.ensureFound(user); - LexUtils.validate("changePasswordForm.oldPassword", + LexUtils.validateField("changePasswordForm.oldPassword", passwordEncoder.matches(changePasswordForm.getOldPassword(), oldPassword), "com.naturalprogrammer.spring.wrong.password").go(); @@ -499,16 +391,18 @@ public String changePassword(U user, @Valid ChangePasswordForm changePasswordFor } + public abstract ID toId(String id); + /** * Updates the fields of the users. Override this if you have more fields. */ - protected void updateUserFields(U user, U updatedUser, UserDto currentUser) { + protected void updateUserFields(U user, U updatedUser, UserDto currentUser) { log.debug("Updating user fields for user: " + user); // Another good admin must be logged in to edit roles if (currentUser.isGoodAdmin() && - !currentUser.getId().equals(user.getId())) { + !currentUser.getId().equals(user.getId().toString())) { log.debug("Updating roles for user: " + user); @@ -547,7 +441,7 @@ public void requestEmailChange(U user, @Valid U updatedUser) { // checks LexUtils.ensureFound(user); - LexUtils.validate("updatedUser.password", + LexUtils.validateField("updatedUser.password", passwordEncoder.matches(updatedUser.getPassword(), user.getPassword()), "com.naturalprogrammer.spring.wrong.password").go(); @@ -558,7 +452,7 @@ public void requestEmailChange(U user, @Valid U updatedUser) { userRepository.save(user); // after successful commit, mails a link to the user - LemonUtils.afterCommit(() -> mailChangeEmailLink(user)); + LecjUtils.afterCommit(() -> mailChangeEmailLink(user)); log.debug("Requested email change: " + user); } @@ -569,7 +463,8 @@ public void requestEmailChange(U user, @Valid U updatedUser) { */ protected void mailChangeEmailLink(U user) { - String changeEmailCode = jwtService.createToken(JwtService.CHANGE_EMAIL_AUDIENCE, + String changeEmailCode = greenTokenService.createToken( + GreenTokenService.CHANGE_EMAIL_AUDIENCE, user.getId().toString(), properties.getJwt().getExpirationMillis(), LecUtils.mapOf("newEmail", user.getNewEmail())); @@ -587,7 +482,7 @@ protected void mailChangeEmailLink(U user) { log.debug("Change email link mail queued."); - } catch (Throwable e) { + } catch (Exception e) { // In case of exception, just log the error and keep silent log.error(ExceptionUtils.getStackTrace(e)); } @@ -620,9 +515,9 @@ public void changeEmail(ID userId, @Valid @NotBlank String changeEmailCode) { log.debug("Changing email of current user ..."); // fetch the current-user - UserDto currentUser = LemonUtils.currentUser(); + UserDto currentUser = LecwUtils.currentUser(); - LexUtils.validate(userId.equals(currentUser.getId()), + LexUtils.validate(userId.equals(toId(currentUser.getId())), "com.naturalprogrammer.spring.wrong.login").go(); U user = userRepository.findById(userId).orElseThrow(LexUtils.notFoundSupplier()); @@ -630,8 +525,8 @@ public void changeEmail(ID userId, @Valid @NotBlank String changeEmailCode) { LexUtils.validate(StringUtils.isNotBlank(user.getNewEmail()), "com.naturalprogrammer.spring.blank.newEmail").go(); - JWTClaimsSet claims = jwtService.parseToken(changeEmailCode, - JwtService.CHANGE_EMAIL_AUDIENCE, + JWTClaimsSet claims = greenTokenService.parseToken(changeEmailCode, + GreenTokenService.CHANGE_EMAIL_AUDIENCE, user.getCredentialsUpdatedMillis()); LecUtils.ensureAuthority( @@ -657,48 +552,11 @@ public void changeEmail(ID userId, @Valid @NotBlank String changeEmailCode) { userRepository.save(user); // after successful commit, - LemonUtils.afterCommit(() -> { - - // Login the user - LemonUtils.login(user); - }); - + LecjUtils.afterCommit(() -> LemonUtils.login(user)); log.debug("Changed email of user: " + user); } - /** - * Extracts the email id from user attributes received from OAuth2 provider, e.g. Google - * - */ - public String getOAuth2Email(String registrationId, Map attributes) { - - return (String) attributes.get(StandardClaimNames.EMAIL); - } - - - /** - * Extracts additional fields, e.g. name from user attributes received from OAuth2 provider, e.g. Google - * Override this if you introduce more user fields, e.g. name - */ - public void fillAdditionalFields(String clientId, U user, Map attributes) { - - } - - - /** - * Checks if the account at the OAuth2 provider is verified - */ - public boolean getOAuth2AccountVerified(String registrationId, Map attributes) { - - Object verified = attributes.get(StandardClaimNames.EMAIL_VERIFIED); - if (verified == null) - verified = attributes.get("verified"); - - return (boolean) verified; - } - - /** * Fetches a new token - for session scrolling etc. * @return @@ -707,14 +565,14 @@ public boolean getOAuth2AccountVerified(String registrationId, Map expirationMillis, Optional optionalUsername) { - UserDto currentUser = LemonUtils.currentUser(); + UserDto currentUser = LecwUtils.currentUser(); String username = optionalUsername.orElse(currentUser.getUsername()); LecUtils.ensureAuthority(currentUser.getUsername().equals(username) || currentUser.isGoodAdmin(), "com.naturalprogrammer.spring.notGoodAdminOrSameUser"); return LecUtils.TOKEN_PREFIX + - jwtService.createToken(JwtService.AUTH_AUDIENCE, username, + blueTokenService.createToken(BlueTokenService.AUTH_AUDIENCE, username, expirationMillis.orElse(properties.getJwt().getExpirationMillis())); } @@ -736,20 +594,42 @@ protected void hideConfidentialFields(U user) { user.setPassword(null); // JsonIgnore didn't work - if (!user.hasPermission(LemonUtils.currentUser(), UserUtils.Permission.EDIT)) + if (!user.hasPermission(LecwUtils.currentUser(), UserUtils.Permission.EDIT)) user.setEmail(null); log.debug("Hid confidential fields for user: " + user); } + @PreAuthorize("isAuthenticated()") + public Map fetchFullToken(String authHeader) { + + LecUtils.ensureCredentials(blueTokenService.parseClaim(authHeader.substring(LecUtils.TOKEN_PREFIX_LENGTH), + BlueTokenService.USER_CLAIM) == null, "com.naturalprogrammer.spring.fullTokenNotAllowed"); + + UserDto currentUser = LecwUtils.currentUser(); + + Map claimMap = Collections.singletonMap(BlueTokenService.USER_CLAIM, + LecUtils.serialize(currentUser)); // Not serializing converts it to a JsonNode + + return Collections.singletonMap("token", LecUtils.TOKEN_PREFIX + + blueTokenService.createToken(BlueTokenService.AUTH_AUDIENCE, currentUser.getUsername(), + Long.valueOf(properties.getJwt().getShortLivedMillis()), + claimMap)); + } + + /** * Adds a Lemon-Authorization header to the response */ public void addAuthHeader(HttpServletResponse response, String username, Long expirationMillis) { - response.addHeader(LecUtils.TOKEN_RESPONSE_HEADER_NAME, - LecUtils.TOKEN_PREFIX + - jwtService.createToken(JwtService.AUTH_AUDIENCE, username, expirationMillis)); + response.addHeader(LecUtils.TOKEN_RESPONSE_HEADER_NAME, LecUtils.TOKEN_PREFIX + + blueTokenService.createToken(BlueTokenService.AUTH_AUDIENCE, username, expirationMillis)); + } + + + public Optional findUserById(String id) { + return userRepository.findById(toId(id)); } } diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/AbstractUser.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/AbstractUser.java index 7683e1d5..9fcb94cf 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/AbstractUser.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/AbstractUser.java @@ -1,28 +1,38 @@ -package com.naturalprogrammer.spring.lemon.domain; - -import java.io.Serializable; -import java.util.HashSet; -import java.util.Set; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.persistence.CollectionTable; -import javax.persistence.Column; -import javax.persistence.ElementCollection; -import javax.persistence.FetchType; -import javax.persistence.JoinColumn; -import javax.persistence.MappedSuperclass; -import javax.persistence.Transient; +package com.naturalprogrammer.spring.lemon.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; +import com.naturalprogrammer.spring.lemon.commons.domain.LemonUser; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemon.commons.validation.Captcha; import com.naturalprogrammer.spring.lemon.commons.validation.Password; -import com.naturalprogrammer.spring.lemon.validation.Captcha; +import com.naturalprogrammer.spring.lemon.commonsjpa.LemonEntity; import com.naturalprogrammer.spring.lemon.validation.UniqueEmail; - import lombok.Getter; import lombok.Setter; +import javax.persistence.*; +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + /** * Base class for User entity @@ -31,10 +41,9 @@ */ @Getter @Setter @MappedSuperclass -public class AbstractUser - , - ID extends Serializable> -extends VersionedEntity { +public class AbstractUser + extends LemonEntity + implements LemonUser { // email @JsonView(UserUtils.SignupInput.class) @@ -80,7 +89,7 @@ public final boolean hasRole(String role) { * on this entity. */ @Override - public boolean hasPermission(UserDto currentUser, String permission) { + public boolean hasPermission(UserDto currentUser, String permission) { return UserUtils.hasPermission(getId(), currentUser, permission); } @@ -98,28 +107,23 @@ public String toString() { /** * Makes a User DTO */ - public UserDto toUserDto() { + public UserDto toUserDto() { - UserDto userDto = new UserDto<>(); + UserDto userDto = new UserDto(); - userDto.setId(getId()); + userDto.setId(getId().toString()); userDto.setUsername(email); userDto.setPassword(password); - userDto.setRoles(roles); + + // roles would be org.hibernate.collection.internal.PersistentSet, + // which is not in another microservices not having Hibernate. + // So, let's convert it to HashSet + userDto.setRoles(new HashSet(roles)); + userDto.setTag(toTag()); - boolean unverified = hasRole(UserUtils.Role.UNVERIFIED); - boolean blocked = hasRole(UserUtils.Role.BLOCKED); - boolean admin = hasRole(UserUtils.Role.ADMIN); - boolean goodUser = !(unverified || blocked); - boolean goodAdmin = goodUser && admin; + userDto.initialize(); - userDto.setAdmin(admin); - userDto.setBlocked(blocked); - userDto.setGoodAdmin(goodAdmin); - userDto.setGoodUser(goodUser); - userDto.setUnverified(unverified); - return userDto; } diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/AbstractUserRepository.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/AbstractUserRepository.java index 9bf138d8..4837b71a 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/AbstractUserRepository.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/AbstractUserRepository.java @@ -1,11 +1,27 @@ -package com.naturalprogrammer.spring.lemon.domain; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.io.Serializable; -import java.util.Optional; +package com.naturalprogrammer.spring.lemon.domain; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.NoRepositoryBean; +import java.io.Serializable; +import java.util.Optional; + /** * Abstract UserRepository interface * @@ -14,7 +30,7 @@ */ @NoRepositoryBean public interface AbstractUserRepository - , ID extends Serializable> + , ID extends Serializable> extends JpaRepository { Optional findByEmail(String email); diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/LemonAuditorAware.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/LemonAuditorAware.java deleted file mode 100644 index 19c955d9..00000000 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/LemonAuditorAware.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.naturalprogrammer.spring.lemon.domain; - -import java.io.Serializable; -import java.util.Optional; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.data.domain.AuditorAware; - -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; - -/** - * Needed for auto-filling of the - * AbstractAuditable columns of AbstractUser - * - * @author Sanjay Patel - */ -public class LemonAuditorAware - , - ID extends Serializable> -implements AuditorAware { - - private static final Log log = LogFactory.getLog(LemonAuditorAware.class); - - private AbstractUserRepository userRepository; - - public LemonAuditorAware(AbstractUserRepository userRepository) { - - this.userRepository = userRepository; - log.info("Created"); - } - - @Override - public Optional getCurrentAuditor() { - - UserDto currentUser = LemonUtils.currentUser(); - - if (currentUser == null) - return Optional.empty(); - - return userRepository.findById(currentUser.getId()); - } -} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/LemonEntity.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/LemonEntity.java deleted file mode 100644 index 41ea0597..00000000 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/LemonEntity.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.naturalprogrammer.spring.lemon.domain; - -import java.io.Serializable; - -import javax.persistence.MappedSuperclass; - -import org.springframework.data.jpa.domain.AbstractAuditable; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.naturalprogrammer.spring.lemon.commons.security.PermissionEvaluatorEntity; -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; - -/** - * Base class for all entities. - * - * @author Sanjay Patel - */ -@MappedSuperclass -@JsonIgnoreProperties({ "createdBy", "lastModifiedBy", "createdDate", "lastModifiedDate" }) -public class LemonEntity, ID extends Serializable> extends AbstractAuditable implements PermissionEvaluatorEntity { - - private static final long serialVersionUID = -8151190931948396443L; - - /** - * Whether the given user has the given permission for - * this entity. Override this method where you need. - */ - @Override - public boolean hasPermission(UserDto user, String permission) { - return false; - } - -} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/VersionedEntity.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/VersionedEntity.java deleted file mode 100644 index c2895b6f..00000000 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/domain/VersionedEntity.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.naturalprogrammer.spring.lemon.domain; - -import java.io.Serializable; - -import javax.persistence.MappedSuperclass; -import javax.persistence.Version; - - -/** - * Base class for all entities needing optimistic locking. - * - * @author Sanjay Patel - */ -@MappedSuperclass -public abstract class VersionedEntity, ID extends Serializable> extends LemonEntity { - - private static final long serialVersionUID = 4310555782328370192L; - - @Version - private Long version; - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } -} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/HttpCookieOAuth2AuthorizationRequestRepository.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/HttpCookieOAuth2AuthorizationRequestRepository.java index 0e2f3f91..69a94900 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/HttpCookieOAuth2AuthorizationRequestRepository.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/HttpCookieOAuth2AuthorizationRequestRepository.java @@ -1,115 +1,126 @@ -package com.naturalprogrammer.spring.lemon.security; - -import java.util.Base64; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.lang3.SerializationUtils; -import org.apache.commons.lang3.StringUtils; -import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; -import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; -import org.springframework.util.Assert; - -import com.naturalprogrammer.spring.lemon.commons.LemonProperties; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; - -/** - * Cookie based repository for storing Authorization requests - */ -public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { - - private static final String AUTHORIZATION_REQUEST_COOKIE_NAME = "lemon_oauth2_authorization_request"; - public static final String LEMON_REDIRECT_URI_COOKIE_PARAM_NAME = "lemon_redirect_uri"; - - private int cookieExpirySecs; - - public HttpCookieOAuth2AuthorizationRequestRepository(LemonProperties properties) { - - cookieExpirySecs = properties.getJwt().getShortLivedMillis() / 1000; - } - - /** - * Load authorization request from cookie - */ - @Override - public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { - - Assert.notNull(request, "request cannot be null"); - - return LemonUtils.fetchCookie(request, AUTHORIZATION_REQUEST_COOKIE_NAME) - .map(this::deserialize) - .orElse(null); - } - - /** - * Save authorization request in cookie - */ - @Override - public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, - HttpServletResponse response) { - - Assert.notNull(request, "request cannot be null"); - Assert.notNull(response, "response cannot be null"); - - if (authorizationRequest == null) { - - deleteCookies(request, response); - return; - } - - Cookie cookie = new Cookie(AUTHORIZATION_REQUEST_COOKIE_NAME, serialize(authorizationRequest)); - cookie.setPath("/"); - cookie.setHttpOnly(true); - cookie.setMaxAge(cookieExpirySecs); - response.addCookie(cookie); - - String lemonRedirectUri = request.getParameter(LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); - if (StringUtils.isNotBlank(lemonRedirectUri)) { - - cookie = new Cookie(LEMON_REDIRECT_URI_COOKIE_PARAM_NAME, lemonRedirectUri); - cookie.setPath("/"); - cookie.setHttpOnly(true); - cookie.setMaxAge(cookieExpirySecs); - response.addCookie(cookie); - } - } - - @Override - public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { - - return loadAuthorizationRequest(request); - } - - /** - * Utility for deleting related cookies - */ - public static void deleteCookies(HttpServletRequest request, HttpServletResponse response) { - - Cookie[] cookies = request.getCookies(); - - if (cookies != null && cookies.length > 0) - for (int i = 0; i < cookies.length; i++) - if (cookies[i].getName().equals(AUTHORIZATION_REQUEST_COOKIE_NAME) || - cookies[i].getName().equals(LEMON_REDIRECT_URI_COOKIE_PARAM_NAME)) { - - cookies[i].setValue(""); - cookies[i].setPath("/"); - cookies[i].setMaxAge(0); - response.addCookie(cookies[i]); - } - } - - private String serialize(OAuth2AuthorizationRequest authorizationRequest) { - - return Base64.getUrlEncoder().encodeToString( - SerializationUtils.serialize(authorizationRequest)); - } - - private OAuth2AuthorizationRequest deserialize(Cookie cookie) { - - return SerializationUtils.deserialize( - Base64.getUrlDecoder().decode(cookie.getValue())); - } -} +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.security; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commonsweb.util.LecwUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.util.Assert; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Cookie based repository for storing Authorization requests + */ +public class HttpCookieOAuth2AuthorizationRequestRepository implements AuthorizationRequestRepository { + + private int cookieExpirySecs; + + public HttpCookieOAuth2AuthorizationRequestRepository(LemonProperties properties) { + + cookieExpirySecs = properties.getJwt().getShortLivedMillis() / 1000; + } + + /** + * Load authorization request from cookie + */ + @Override + public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) { + + Assert.notNull(request, "request cannot be null"); + + return LecwUtils.fetchCookie(request, LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME) + .map(this::deserialize) + .orElse(null); + } + + /** + * Save authorization request in cookie + */ + @Override + public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, + HttpServletResponse response) { + + Assert.notNull(request, "request cannot be null"); + Assert.notNull(response, "response cannot be null"); + + if (authorizationRequest == null) { + + deleteCookies(request, response, LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME, LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); + return; + } + + Cookie cookie = new Cookie(LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME, LecUtils.serialize(authorizationRequest)); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(cookieExpirySecs); + response.addCookie(cookie); + + String lemonRedirectUri = request.getParameter(LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); + if (StringUtils.isNotBlank(lemonRedirectUri)) { + + cookie = new Cookie(LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME, lemonRedirectUri); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(cookieExpirySecs); + response.addCookie(cookie); + } + } + + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) { + + OAuth2AuthorizationRequest originalRequest = loadAuthorizationRequest(request); + deleteCookies(request, response, LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME); + return originalRequest; + } + + /** + * Utility for deleting related cookies + */ + public static void deleteCookies(HttpServletRequest request, HttpServletResponse response, String... cookiesToDelete) { + + Cookie[] cookies = request.getCookies(); + + if (cookies != null && cookies.length > 0) + for (int i = 0; i < cookies.length; i++) + if (ArrayUtils.contains(cookiesToDelete, cookies[i].getName())) { + + cookies[i].setValue(""); + cookies[i].setPath("/"); + cookies[i].setMaxAge(0); + response.addCookie(cookies[i]); + } + } + + private OAuth2AuthorizationRequest deserialize(Cookie cookie) { + + return LecUtils.deserialize(cookie.getValue()); + } + + @Deprecated + @Override + public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request) { + throw new UnsupportedOperationException("Spring Security shouldn't have called the deprecated removeAuthorizationRequest(request)"); + } +} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/JwtAuthenticationProvider.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/JwtAuthenticationProvider.java deleted file mode 100644 index 69e3ff7d..00000000 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/JwtAuthenticationProvider.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.naturalprogrammer.spring.lemon.security; - -import java.io.Serializable; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import com.naturalprogrammer.spring.lemon.commons.security.JwtAuthenticationToken; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; -import com.naturalprogrammer.spring.lemon.domain.AbstractUser; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; -import com.nimbusds.jwt.JWTClaimsSet; - -/** - * Authentication provider for JWT token authentication - */ -public class JwtAuthenticationProvider -, ID extends Serializable> implements AuthenticationProvider { - - private static final Log log = LogFactory.getLog(JwtAuthenticationProvider.class); - - private final JwtService jwtService; - private LemonUserDetailsService userDetailsService; - - public JwtAuthenticationProvider(JwtService jwtService, LemonUserDetailsService userDetailsService) { - - this.jwtService = jwtService; - this.userDetailsService = userDetailsService; - - log.debug("Created"); - } - - @Override - public Authentication authenticate(Authentication auth) { - - log.debug("Authenticating ..."); - - String token = (String) auth.getCredentials(); - - JWTClaimsSet claims = jwtService.parseToken(token, JwtService.AUTH_AUDIENCE); - - String username = claims.getSubject(); - U user = userDetailsService.findUserByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException(username)); - - log.debug("User found ..."); - - LemonUtils.ensureCredentialsUpToDate(claims, user); - LemonPrincipal principal = new LemonPrincipal(user.toUserDto()); - - return new JwtAuthenticationToken(principal, token, principal.getAuthorities()); - } - - @Override - public boolean supports(Class authentication) { - - return (JwtAuthenticationToken.class.isAssignableFrom(authentication)); - } -} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/AuthenticationSuccessHandler.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonAuthenticationSuccessHandler.java similarity index 67% rename from spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/AuthenticationSuccessHandler.java rename to spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonAuthenticationSuccessHandler.java index 989450cf..190060c0 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/AuthenticationSuccessHandler.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonAuthenticationSuccessHandler.java @@ -1,22 +1,36 @@ -package com.naturalprogrammer.spring.lemon.security; - -import java.io.IOException; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +package com.naturalprogrammer.spring.lemon.security; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.naturalprogrammer.spring.lemon.LemonService; +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commonsweb.util.LecwUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.naturalprogrammer.spring.lemon.LemonService; -import com.naturalprogrammer.spring.lemon.commons.LemonProperties; -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; /** * Authentication success handler for sending the response @@ -24,16 +38,16 @@ * * @author Sanjay Patel */ -public class AuthenticationSuccessHandler +public class LemonAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { - private static final Log log = LogFactory.getLog(AuthenticationSuccessHandler.class); + private static final Log log = LogFactory.getLog(LemonAuthenticationSuccessHandler.class); private ObjectMapper objectMapper; private LemonService lemonService; private long defaultExpirationMillis; - public AuthenticationSuccessHandler(ObjectMapper objectMapper, LemonService lemonService, LemonProperties properties) { + public LemonAuthenticationSuccessHandler(ObjectMapper objectMapper, LemonService lemonService, LemonProperties properties) { this.objectMapper = objectMapper; this.lemonService = lemonService; @@ -59,7 +73,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, defaultExpirationMillis : Long.valueOf(expirationMillisStr); // get the current-user - UserDto currentUser = LemonUtils.currentUser(); + UserDto currentUser = LecwUtils.currentUser(); lemonService.addAuthHeader(response, currentUser.getUsername(), expirationMillis); diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonCorsConfig.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonCorsConfig.java deleted file mode 100644 index d4fc7067..00000000 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonCorsConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.naturalprogrammer.spring.lemon.security; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import com.naturalprogrammer.spring.lemon.commons.LemonProperties; -import com.naturalprogrammer.spring.lemon.commons.LemonProperties.Cors; - -/** - * CORS Configuration - */ -public class LemonCorsConfig implements WebMvcConfigurer { - - private static final Log log = LogFactory.getLog(LemonCorsConfig.class); - - private Cors cors; - - public LemonCorsConfig(LemonProperties properties) { - - this.cors = properties.getCors(); - log.info("Created"); - } - - @Override - public void addCorsMappings(CorsRegistry registry) { - - registry.addMapping("/**") - .allowedOrigins(cors.getAllowedOrigins()) - .allowedMethods(cors.getAllowedMethods()) - .allowedHeaders(cors.getAllowedHeaders()) - .exposedHeaders(cors.getExposedHeaders()) - .allowCredentials(true) - .maxAge(cors.getMaxAge()); - } -} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonJpaSecurityConfig.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonJpaSecurityConfig.java new file mode 100644 index 00000000..51d87fa2 --- /dev/null +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonJpaSecurityConfig.java @@ -0,0 +1,137 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.security; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commonsweb.security.LemonWebSecurityConfig; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * Security configuration class. Extend it in the + * application, and make a configuration class. Override + * protected methods, if you need any customization. + * + * @author Sanjay Patel + */ +public class LemonJpaSecurityConfig extends LemonWebSecurityConfig { + + private static final Log log = LogFactory.getLog(LemonJpaSecurityConfig.class); + + private LemonProperties properties; + private LemonUserDetailsService userDetailsService; + private LemonAuthenticationSuccessHandler authenticationSuccessHandler; + private LemonOidcUserService oidcUserService; + private LemonOAuth2UserService oauth2UserService; + private OAuth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler; + private OAuth2AuthenticationFailureHandler oauth2AuthenticationFailureHandler; + + @Autowired + public void createLemonSecurityConfig(LemonProperties properties, LemonUserDetailsService userDetailsService, + LemonAuthenticationSuccessHandler authenticationSuccessHandler, + LemonOidcUserService oidcUserService, + LemonOAuth2UserService oauth2UserService, + OAuth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler, + OAuth2AuthenticationFailureHandler oauth2AuthenticationFailureHandler) { + + this.properties = properties; + this.userDetailsService = userDetailsService; + this.authenticationSuccessHandler = authenticationSuccessHandler; + this.oidcUserService = oidcUserService; + this.oauth2UserService = oauth2UserService; + this.oauth2AuthenticationSuccessHandler = oauth2AuthenticationSuccessHandler; + this.oauth2AuthenticationFailureHandler = oauth2AuthenticationFailureHandler; + + log.info("Created"); + } + + /** + * Security configuration, calling protected methods + */ + @Override + public HttpSecurity configure(HttpSecurity http) throws Exception { + + super.configure(http); + login(http); // authentication + exceptionHandling(http); // exception handling + oauth2Client(http); + return http; + } + + + /** + * Configuring authentication. + */ + protected void login(HttpSecurity http) throws Exception { + + http + .formLogin() // form login + .loginPage(loginPage()) + + /****************************************** + * Setting a successUrl would redirect the user there. Instead, + * let's send 200 and the userDto along with an Authorization token. + *****************************************/ + .successHandler(authenticationSuccessHandler) + + /******************************************* + * Setting the failureUrl will redirect the user to + * that url if login fails. Instead, we need to send + * 401. So, let's set failureHandler instead. + *******************************************/ + .failureHandler(new SimpleUrlAuthenticationFailureHandler()); + } + + + /** + * Override this to change login URL + * + * @return String + */ + protected String loginPage() { + + return properties.getLoginUrl(); + } + + + protected void oauth2Client(HttpSecurity http) throws Exception { + + http.oauth2Login() + .authorizationEndpoint() + .authorizationRequestRepository(new HttpCookieOAuth2AuthorizationRequestRepository(properties)).and() + .successHandler(oauth2AuthenticationSuccessHandler) + .failureHandler(oauth2AuthenticationFailureHandler) + .userInfoEndpoint() + .oidcUserService(oidcUserService) + .userService(oauth2UserService); + } + + + /** + * Configuring token authentication filter + */ + @Override + protected void tokenAuthentication(HttpSecurity http) { + + http.addFilterBefore(new LemonJpaTokenAuthenticationFilter(blueTokenService, userDetailsService), + UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonJpaTokenAuthenticationFilter.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonJpaTokenAuthenticationFilter.java new file mode 100644 index 00000000..607f62fe --- /dev/null +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonJpaTokenAuthenticationFilter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.security; + +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commonsweb.security.LemonCommonsWebTokenAuthenticationFilter; +import com.naturalprogrammer.spring.lemon.domain.AbstractUser; +import com.naturalprogrammer.spring.lemon.util.LemonUtils; +import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.io.Serializable; + +public class LemonJpaTokenAuthenticationFilter, ID extends Serializable> + extends LemonCommonsWebTokenAuthenticationFilter { + + private static final Log log = LogFactory.getLog(LemonJpaTokenAuthenticationFilter.class); + + private LemonUserDetailsService userDetailsService; + + public LemonJpaTokenAuthenticationFilter(BlueTokenService blueTokenService, + LemonUserDetailsService userDetailsService) { + + super(blueTokenService); + this.userDetailsService = userDetailsService; + + log.info("Created"); + } + + @Override + protected UserDto fetchUserDto(JWTClaimsSet claims) { + + String username = claims.getSubject(); + U user = userDetailsService.findUserByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException(username)); + + log.debug("User found ..."); + + LemonUtils.ensureCredentialsUpToDate(claims, user); + UserDto userDto = user.toUserDto(); + userDto.setPassword(null); + + return userDto; + } +} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonOAuth2UserService.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonOAuth2UserService.java index ce6ac784..cab4ef0e 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonOAuth2UserService.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonOAuth2UserService.java @@ -1,95 +1,143 @@ -package com.naturalprogrammer.spring.lemon.security; - -import java.io.Serializable; -import java.util.Map; - -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; -import org.springframework.security.oauth2.core.user.OAuth2User; - -import com.naturalprogrammer.spring.lemon.LemonService; -import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; -import com.naturalprogrammer.spring.lemon.domain.AbstractUser; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; - -/** - * Logs in or registers a user after OAuth2 SignIn/Up - */ -public class LemonOAuth2UserService, ID extends Serializable> extends DefaultOAuth2UserService { - - private static final Log log = LogFactory.getLog(LemonOAuth2UserService.class); - - private LemonUserDetailsService userDetailsService; - private LemonService lemonService; - private PasswordEncoder passwordEncoder; - - public LemonOAuth2UserService( - LemonUserDetailsService userDetailsService, - LemonService lemonService, - PasswordEncoder passwordEncoder) { - - this.userDetailsService = userDetailsService; - this.lemonService = lemonService; - this.passwordEncoder = passwordEncoder; - - log.info("Created"); - } - - @Override - public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - - OAuth2User oath2User = super.loadUser(userRequest); - return buildPrincipal(oath2User, userRequest.getClientRegistration().getRegistrationId()); - } - - /** - * Builds the security principal from the given userReqest. - * Registers the user if not already reqistered - */ - public LemonPrincipal buildPrincipal(OAuth2User oath2User, String registrationId) { - - Map attributes = oath2User.getAttributes(); - String email = lemonService.getOAuth2Email(registrationId, attributes); - LexUtils.validate(email != null, "com.naturalprogrammer.spring.oauth2EmailNeeded", registrationId).go(); - - boolean emailVerified = lemonService.getOAuth2AccountVerified(registrationId, attributes); - LexUtils.validate(emailVerified, "com.naturalprogrammer.spring.oauth2EmailNotVerified", registrationId).go(); - - U user = userDetailsService.findUserByUsername(email).orElseGet(() -> { - - // register a new user - U newUser = lemonService.newUser(); - newUser.setEmail(email); - newUser.setPassword(passwordEncoder.encode(LemonUtils.uid())); - - lemonService.fillAdditionalFields(registrationId, newUser, attributes); - lemonService.save(newUser); - - try { - - lemonService.mailForgotPasswordLink(newUser); - - } catch (Throwable e) { - - // In case of exception, just log the error and keep silent - log.error(ExceptionUtils.getStackTrace(e)); - } - - return newUser; - }); - - UserDto userDto = user.toUserDto(); - LemonPrincipal principal = new LemonPrincipal<>(userDto); - principal.setAttributes(attributes); - principal.setName(oath2User.getName()); - - return principal; - } -} +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.security; + +import com.naturalprogrammer.spring.lemon.LemonService; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.domain.AbstractUser; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.util.MimeType; +import org.springframework.web.client.RestTemplate; + +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Logs in or registers a user after OAuth2 SignIn/Up + */ +public class LemonOAuth2UserService, ID extends Serializable> extends DefaultOAuth2UserService { + + private static final Log log = LogFactory.getLog(LemonOAuth2UserService.class); + + private LemonUserDetailsService userDetailsService; + private LemonService lemonService; + private PasswordEncoder passwordEncoder; + + public LemonOAuth2UserService( + LemonUserDetailsService userDetailsService, + LemonService lemonService, + PasswordEncoder passwordEncoder) { + + this.userDetailsService = userDetailsService; + this.lemonService = lemonService; + this.passwordEncoder = passwordEncoder; + + replaceRestOperarions(); + log.info("Created"); + } + + protected void replaceRestOperarions() { + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); + restTemplate.setMessageConverters(makeMessageConverters()); + setRestOperations(restTemplate); + + log.info("Rest Operations replaced"); + } + + protected List> makeMessageConverters() { + + log.info("Making message converters"); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + + List mediaTypes = new ArrayList<>(converter.getSupportedMediaTypes()); + mediaTypes.add(MediaType.asMediaType(new MimeType("text", "javascript", StandardCharsets.UTF_8))); // Facebook returns text/javascript + + converter.setSupportedMediaTypes(mediaTypes); + return Collections.singletonList(converter); + } + + @Override + public OAuth2User loadUser(OAuth2UserRequest userRequest) { + + OAuth2User oath2User = super.loadUser(userRequest); + return buildPrincipal(oath2User, userRequest.getClientRegistration().getRegistrationId()); + } + + /** + * Builds the security principal from the given userReqest. + * Registers the user if not already registered + */ + public LemonPrincipal buildPrincipal(OAuth2User oath2User, String registrationId) { + + Map attributes = oath2User.getAttributes(); + String email = lemonService.getOAuth2Email(registrationId, attributes); + LexUtils.validate(email != null, "com.naturalprogrammer.spring.oauth2EmailNeeded", registrationId).go(); + + boolean emailVerified = lemonService.getOAuth2AccountVerified(registrationId, attributes); + LexUtils.validate(emailVerified, "com.naturalprogrammer.spring.oauth2EmailNotVerified", registrationId).go(); + + U user = userDetailsService.findUserByUsername(email).orElseGet(() -> { + + // register a new user + U newUser = lemonService.newUser(); + newUser.setEmail(email); + newUser.setPassword(passwordEncoder.encode(LecUtils.uid())); + + lemonService.fillAdditionalFields(registrationId, newUser, attributes); + lemonService.save(newUser); + + try { + + lemonService.mailForgotPasswordLink(newUser); + + } catch (Exception e) { + + // In case of exception, just log the error and keep silent + log.error(ExceptionUtils.getStackTrace(e)); + } + + return newUser; + }); + + UserDto userDto = user.toUserDto(); + LemonPrincipal principal = new LemonPrincipal(userDto); + principal.setAttributes(attributes); + principal.setName(oath2User.getName()); + + return principal; + } +} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonOidcUserService.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonOidcUserService.java index 6c81ee6f..f39333f3 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonOidcUserService.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonOidcUserService.java @@ -1,14 +1,28 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.security; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; - /** * Logs in or registers a user after OpenID Connect SignIn/Up */ @@ -25,10 +39,10 @@ public LemonOidcUserService(LemonOAuth2UserService oauth2UserService) { } @Override - public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { + public OidcUser loadUser(OidcUserRequest userRequest) { OidcUser oidcUser = super.loadUser(userRequest); - LemonPrincipal principal = oauth2UserService.buildPrincipal(oidcUser, + LemonPrincipal principal = oauth2UserService.buildPrincipal(oidcUser, userRequest.getClientRegistration().getRegistrationId()); principal.setClaims(oidcUser.getClaims()); diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonSecurityConfig.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonSecurityConfig.java deleted file mode 100644 index c4a9beab..00000000 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonSecurityConfig.java +++ /dev/null @@ -1,230 +0,0 @@ -package com.naturalprogrammer.spring.lemon.security; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.authentication.AuthenticationFailureHandler; -import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; - -import com.naturalprogrammer.spring.lemon.commons.LemonProperties; - -/** - * Security configuration class. Extend it in the - * application, and make a configuration class. Override - * protected methods, if you need any customization. - * - * @author Sanjay Patel - */ -public class LemonSecurityConfig extends WebSecurityConfigurerAdapter { - - private static final Log log = LogFactory.getLog(LemonSecurityConfig.class); - - private LemonProperties properties; - private UserDetailsService userDetailsService; - private AuthenticationSuccessHandler authenticationSuccessHandler; - private AuthenticationFailureHandler authenticationFailureHandler; - private LemonOidcUserService oidcUserService; - private LemonOAuth2UserService oauth2UserService; - private OAuth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler; - private OAuth2AuthenticationFailureHandler oauth2AuthenticationFailureHandler; - private JwtAuthenticationProvider jwtAuthenticationProvider; - private PasswordEncoder passwordEncoder; - - @Autowired - public void createLemonSecurityConfig(LemonProperties properties, UserDetailsService userDetailsService, - AuthenticationSuccessHandler authenticationSuccessHandler, AuthenticationFailureHandler authenticationFailureHandler, - LemonOidcUserService oidcUserService, - LemonOAuth2UserService oauth2UserService, - OAuth2AuthenticationSuccessHandler oauth2AuthenticationSuccessHandler, - OAuth2AuthenticationFailureHandler oauth2AuthenticationFailureHandler, - JwtAuthenticationProvider jwtAuthenticationProvider, - PasswordEncoder passwordEncoder - ) { - - this.properties = properties; - this.userDetailsService = userDetailsService; - this.authenticationSuccessHandler = authenticationSuccessHandler; - this.authenticationFailureHandler = authenticationFailureHandler; - this.oidcUserService = oidcUserService; - this.oauth2UserService = oauth2UserService; - this.oauth2AuthenticationSuccessHandler = oauth2AuthenticationSuccessHandler; - this.oauth2AuthenticationFailureHandler = oauth2AuthenticationFailureHandler; - this.jwtAuthenticationProvider = jwtAuthenticationProvider; - this.passwordEncoder = passwordEncoder; - - log.info("Created"); - } - - /** - * Security configuration, calling protected methods - */ - @Override - protected void configure(HttpSecurity http) throws Exception { - - sessionCreationPolicy(http); // set session creation policy - login(http); // authentication - logout(http); // logout related configuration - exceptionHandling(http); // exception handling - tokenAuthentication(http); // configure token authentication filter - csrf(http); // CSRF configuration - cors(http); // CORS configuration - oauth2Client(http); - authorizeRequests(http); // authorize requests - otherConfigurations(http); // override this to add more configurations - } - - - /** - * Configuring session creation policy - */ - protected void sessionCreationPolicy(HttpSecurity http) throws Exception { - - // No session - http.sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.STATELESS); - } - - - /** - * Configuring authentication. - */ - protected void login(HttpSecurity http) throws Exception { - - http - .formLogin() // form login - .loginPage(loginPage()) - - /****************************************** - * Setting a successUrl would redirect the user there. Instead, - * let's send 200 and the userDto along with an Authorization token. - *****************************************/ - .successHandler(authenticationSuccessHandler) - - /******************************************* - * Setting the failureUrl will redirect the user to - * that url if login fails. Instead, we need to send - * 401. So, let's set failureHandler instead. - *******************************************/ - .failureHandler(authenticationFailureHandler); - } - - - /** - * Override this to change login URL - * - * @return - */ - protected String loginPage() { - - return "/api/core/login"; - } - - /** - * Logout related configuration - */ - protected void logout(HttpSecurity http) throws Exception { - - http - .logout().disable(); // we are stateless; so /logout endpoint not needed - } - - - /** - * Configures exception-handling - */ - protected void exceptionHandling(HttpSecurity http) throws Exception { - - http - .exceptionHandling() - - /*********************************************** - * To prevent redirection to the login page - * when someone tries to access a restricted page - **********************************************/ - .authenticationEntryPoint(new Http403ForbiddenEntryPoint()); - } - - - /** - * Configuring token authentication filter - */ - protected void tokenAuthentication(HttpSecurity http) throws Exception { - - http.addFilterBefore(new LemonTokenAuthenticationFilter( - super.authenticationManager()), - UsernamePasswordAuthenticationFilter.class); - } - - - /** - * Disables CSRF. We are stateless. - */ - protected void csrf(HttpSecurity http) throws Exception { - - http - .csrf().disable(); - } - - - /** - * Configures CORS - */ - protected void cors(HttpSecurity http) throws Exception { - - http - .cors(); - } - - - private void oauth2Client(HttpSecurity http) throws Exception { - - http.oauth2Login() - .authorizationEndpoint() - .authorizationRequestRepository(new HttpCookieOAuth2AuthorizationRequestRepository(properties)).and() - .successHandler(oauth2AuthenticationSuccessHandler) - .failureHandler(oauth2AuthenticationFailureHandler) - .userInfoEndpoint() - .oidcUserService(oidcUserService) - .userService(oauth2UserService); - } - - - /** - * URL based authorization configuration. Override this if needed. - */ - protected void authorizeRequests(HttpSecurity http) throws Exception { - http.authorizeRequests() - .mvcMatchers("/**").permitAll(); - } - - - /** - * Override this to add more http configurations, - * such as more authentication methods. - * - * @param http - * @throws Exception - */ - protected void otherConfigurations(HttpSecurity http) throws Exception { - - } - - - /** - * Needed for configuring JwtAuthenticationProvider - */ - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - - auth.userDetailsService(userDetailsService) - .passwordEncoder(passwordEncoder).and() - .authenticationProvider(jwtAuthenticationProvider); - } -} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonTokenAuthenticationFilter.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonTokenAuthenticationFilter.java deleted file mode 100644 index 3c8cf63c..00000000 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonTokenAuthenticationFilter.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.naturalprogrammer.spring.lemon.security; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.OncePerRequestFilter; - -import com.naturalprogrammer.spring.lemon.commons.security.JwtAuthenticationToken; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; - -/** - * Filter for token authentication - */ -public class LemonTokenAuthenticationFilter extends OncePerRequestFilter { - - private static final Log log = LogFactory.getLog(LemonTokenAuthenticationFilter.class); - - private AuthenticationManager authenticationManager; - - public LemonTokenAuthenticationFilter(AuthenticationManager authenticationManager) { - - this.authenticationManager = authenticationManager; - log.info("Created"); - } - - /** - * Checks if a "Bearer " token is present - */ - protected boolean tokenPresent(HttpServletRequest request) { - - String header = request.getHeader(LecUtils.TOKEN_REQUEST_HEADER_NAME); - return header != null && header.startsWith(LecUtils.TOKEN_PREFIX); - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - - log.debug("Inside LemonTokenAuthenticationFilter ..."); - - if (tokenPresent(request)) { - - log.debug("Found a token"); - - String token = request.getHeader(LecUtils.TOKEN_REQUEST_HEADER_NAME).substring(7); - JwtAuthenticationToken authRequest = new JwtAuthenticationToken(token); - - try { - - Authentication auth = authenticationManager.authenticate(authRequest); - SecurityContextHolder.getContext().setAuthentication(auth); - - log.debug("Token authentication successful"); - - } catch (Exception e) { - - log.debug("Token authentication failed - " + e.getMessage()); - - response.sendError(HttpServletResponse.SC_UNAUTHORIZED, - "Authentication Failed: " + e.getMessage()); - - return; - } - - } else - - log.debug("Token authentication skipped"); - - filterChain.doFilter(request, response); - } -} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonUserDetailsService.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonUserDetailsService.java index 19e7a1ee..8a48090a 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonUserDetailsService.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/LemonUserDetailsService.java @@ -1,17 +1,32 @@ -package com.naturalprogrammer.spring.lemon.security; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.io.Serializable; -import java.util.Optional; +package com.naturalprogrammer.spring.lemon.security; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; +import com.naturalprogrammer.spring.lemon.domain.AbstractUser; +import com.naturalprogrammer.spring.lemon.domain.AbstractUserRepository; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; -import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; -import com.naturalprogrammer.spring.lemon.domain.AbstractUser; -import com.naturalprogrammer.spring.lemon.domain.AbstractUserRepository; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import java.io.Serializable; +import java.util.Optional; /** * UserDetailsService, as required by Spring Security. @@ -19,7 +34,7 @@ * @author Sanjay Patel */ public class LemonUserDetailsService - , ID extends Serializable> + , ID extends Serializable> implements UserDetailsService { private static final Log log = LogFactory.getLog(LemonUserDetailsService.class); @@ -33,8 +48,7 @@ public LemonUserDetailsService(AbstractUserRepository userRepository) { } @Override - public LemonPrincipal loadUserByUsername(String username) - throws UsernameNotFoundException { + public LemonPrincipal loadUserByUsername(String username) { log.debug("Loading user having username: " + username); @@ -45,7 +59,7 @@ public LemonPrincipal loadUserByUsername(String username) log.debug("Loaded user having username: " + username); - return new LemonPrincipal<>(user.toUserDto()); + return new LemonPrincipal(user.toUserDto()); } /** diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/OAuth2AuthenticationFailureHandler.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/OAuth2AuthenticationFailureHandler.java index 3b3ba99f..0a305581 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/OAuth2AuthenticationFailureHandler.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/OAuth2AuthenticationFailureHandler.java @@ -1,27 +1,47 @@ -package com.naturalprogrammer.spring.lemon.security; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; - -/** - * OAuth2 Authentication failure handler for removing oauth2 related cookies - * - * @author Sanjay Patel - */ -public class OAuth2AuthenticationFailureHandler - extends SimpleUrlAuthenticationFailureHandler { - - public void onAuthenticationFailure(HttpServletRequest request, - HttpServletResponse response, AuthenticationException exception) - throws IOException, ServletException { - - HttpCookieOAuth2AuthorizationRequestRepository.deleteCookies(request, response); - super.onAuthenticationFailure(request, response, exception); - } -} +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.security; + +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * OAuth2 Authentication failure handler for removing oauth2 related cookies + * + * @author Sanjay Patel + */ +public class OAuth2AuthenticationFailureHandler + extends SimpleUrlAuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, + HttpServletResponse response, AuthenticationException exception) + throws IOException, ServletException { + + HttpCookieOAuth2AuthorizationRequestRepository.deleteCookies(request, response, + LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME, + LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); + + super.onAuthenticationFailure(request, response, exception); + } +} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/OAuth2AuthenticationSuccessHandler.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/OAuth2AuthenticationSuccessHandler.java index 6bb33f52..61538256 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/OAuth2AuthenticationSuccessHandler.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/security/OAuth2AuthenticationSuccessHandler.java @@ -1,59 +1,70 @@ -package com.naturalprogrammer.spring.lemon.security; - -import java.io.Serializable; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; - -import com.naturalprogrammer.spring.lemon.commons.LemonProperties; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; -import com.naturalprogrammer.spring.lemon.util.LemonUtils; - -/** - * Authentication success handler for redirecting the - * OAuth2 signed in user to a URL with a short lived auth token - * - * @author Sanjay Patel - */ -public class OAuth2AuthenticationSuccessHandler - extends SimpleUrlAuthenticationSuccessHandler { - - private static final Log log = LogFactory.getLog(OAuth2AuthenticationSuccessHandler.class); - - private LemonProperties properties; - private JwtService jwtService; - - public OAuth2AuthenticationSuccessHandler(LemonProperties properties, JwtService jwtService) { - - this.properties = properties; - this.jwtService = jwtService; - - log.info("Created"); - } - - @Override - protected String determineTargetUrl(HttpServletRequest request, - HttpServletResponse response) { - - UserDto currentUser = LemonUtils.currentUser(); - - String shortLivedAuthToken = jwtService.createToken( - JwtService.AUTH_AUDIENCE, - currentUser.getUsername(), - (long) properties.getJwt().getShortLivedMillis()); - - String targetUrl = LemonUtils.fetchCookie(request, - HttpCookieOAuth2AuthorizationRequestRepository.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME) - .map(Cookie::getValue) - .orElse(properties.getOauth2AuthenticationSuccessUrl()); - - HttpCookieOAuth2AuthorizationRequestRepository.deleteCookies(request, response); - return targetUrl + shortLivedAuthToken; - } -} +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemon.security; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commonsweb.util.LecwUtils; +import lombok.AllArgsConstructor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Authentication success handler for redirecting the + * OAuth2 signed in user to a URL with a short lived auth token + * + * @author Sanjay Patel + */ +@AllArgsConstructor +public class OAuth2AuthenticationSuccessHandler + extends SimpleUrlAuthenticationSuccessHandler { + + private static final Log log = LogFactory.getLog(OAuth2AuthenticationSuccessHandler.class); + + private LemonProperties properties; + private BlueTokenService blueTokenService; + + @Override + protected String determineTargetUrl(HttpServletRequest request, + HttpServletResponse response) { + + UserDto currentUser = LecwUtils.currentUser(); + + String shortLivedAuthToken = blueTokenService.createToken( + BlueTokenService.AUTH_AUDIENCE, + currentUser.getUsername(), + (long) properties.getJwt().getShortLivedMillis()); + + String targetUrl = LecwUtils.fetchCookie(request, + LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME) + .map(Cookie::getValue) + .orElse(properties.getOauth2AuthenticationSuccessUrl()); + + HttpCookieOAuth2AuthorizationRequestRepository.deleteCookies(request, response, + LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME, + LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); + + return targetUrl + shortLivedAuthToken; + } +} diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/util/LemonUtils.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/util/LemonUtils.java index 18b23c5c..ef30321b 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/util/LemonUtils.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/util/LemonUtils.java @@ -1,37 +1,33 @@ -package com.naturalprogrammer.spring.lemon.util; - -import java.io.IOException; -import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.util.Optional; -import java.util.Scanner; -import java.util.UUID; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; +package com.naturalprogrammer.spring.lemon.util; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; +import com.naturalprogrammer.spring.lemon.commons.security.LemonTokenService; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.domain.AbstractUser; +import com.nimbusds.jwt.JWTClaimsSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.context.ApplicationContext; -import org.springframework.core.io.Resource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.transaction.support.TransactionSynchronizationAdapter; -import org.springframework.transaction.support.TransactionSynchronizationManager; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.naturalprogrammer.spring.lemon.domain.AbstractUser; -import com.naturalprogrammer.spring.lemon.domain.VersionedEntity; -import com.naturalprogrammer.spring.lemon.exceptions.VersionException; -import com.nimbusds.jwt.JWTClaimsSet; +import java.io.Serializable; /** * Useful helper methods @@ -42,58 +38,21 @@ public class LemonUtils { private static final Log log = LogFactory.getLog(LemonUtils.class); - private static ApplicationContext applicationContext; - private static ObjectMapper objectMapper; - - public LemonUtils(ApplicationContext applicationContext, - ObjectMapper objectMapper) { - - LemonUtils.applicationContext = applicationContext; - LemonUtils.objectMapper = objectMapper; + public LemonUtils() { log.info("Created"); } - public static ObjectMapper getMapper() { - - return objectMapper; - } - - - /** - * Gets the reference to an application-context bean - * - * @param clazz the type of the bean - */ - public static T getBean(Class clazz) { - return applicationContext.getBean(clazz); - } - - - /** - * Gets the current-user - */ - public static UserDto currentUser() { - - // get the authentication object - Authentication auth = SecurityContextHolder - .getContext().getAuthentication(); - - // get the user from the authentication object - return LecUtils.currentUser(auth); - } - - /** * Signs a user in * * @param user */ - public static , ID extends Serializable> + public static , ID extends Serializable> void login(U user) { - LemonPrincipal principal = new LemonPrincipal<>(user.toUserDto()); + LemonPrincipal principal = new LemonPrincipal(user.toUserDto()); Authentication authentication = // make the authentication object new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities()); @@ -101,109 +60,18 @@ void login(U user) { SecurityContextHolder.getContext().setAuthentication(authentication); // put that in the security context principal.eraseCredentials(); } - - - /** - * Throws a VersionException if the versions of the - * given entities aren't same. - * - * @param original - * @param updated - */ - public static , ID extends Serializable> - void ensureCorrectVersion(VersionedEntity original, VersionedEntity updated) { - - if (original.getVersion() != updated.getVersion()) - throw new VersionException(original.getClass().getSimpleName(), original.getId().toString()); - } - /** - * A convenient method for running code - * after successful database commit. - * - * @param runnable - */ - public static void afterCommit(Runnable runnable) { - - TransactionSynchronizationManager.registerSynchronization( - new TransactionSynchronizationAdapter() { - @Override - public void afterCommit() { - - runnable.run(); - } - }); - } - - - /** - * Generates a random unique string - */ - public static String uid() { - - return UUID.randomUUID().toString(); - } - - - /** - * Serializes an object to JSON string - */ - public static String toJson(T obj) throws JsonProcessingException { - - return objectMapper.writeValueAsString(obj); - } - - - /** - * Deserializes a JSON String - */ - public static T fromJson(String json, Class clazz) - throws JsonParseException, JsonMappingException, IOException { - - return objectMapper.readValue(json, clazz); - } - /** * Throws BadCredentialsException if * user's credentials were updated after the JWT was issued */ - public static , ID extends Serializable> + public static , ID extends Serializable> void ensureCredentialsUpToDate(JWTClaimsSet claims, U user) { - long issueTime = (long) claims.getClaim(JwtService.LEMON_IAT); + long issueTime = (long) claims.getClaim(LemonTokenService.LEMON_IAT); LecUtils.ensureCredentials(issueTime >= user.getCredentialsUpdatedMillis(), "com.naturalprogrammer.spring.obsoleteToken"); } - - - /** - * Reads a resource into a String - */ - public static String toString(Resource resource) throws IOException { - - String text = null; - try (Scanner scanner = new Scanner(resource.getInputStream(), StandardCharsets.UTF_8.name())) { - text = scanner.useDelimiter("\\A").next(); - } - - return text; - } - - - /** - * Fetches a cookie from the request - */ - public static Optional fetchCookie(HttpServletRequest request, String name) { - - Cookie[] cookies = request.getCookies(); - - if (cookies != null && cookies.length > 0) - for (int i = 0; i < cookies.length; i++) - if (cookies[i].getName().equals(name)) - return Optional.of(cookies[i]); - - return Optional.empty(); - } } diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/UniqueEmail.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/UniqueEmail.java index bde77376..dc1c6ec8 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/UniqueEmail.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/UniqueEmail.java @@ -1,14 +1,29 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemon.validation; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; import javax.validation.Constraint; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; - -import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Annotation for unique-email constraint, diff --git a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/UniqueEmailValidator.java b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/UniqueEmailValidator.java index 7ddbc356..fbb76450 100644 --- a/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/UniqueEmailValidator.java +++ b/spring-lemon-jpa/src/main/java/com/naturalprogrammer/spring/lemon/validation/UniqueEmailValidator.java @@ -1,12 +1,27 @@ -package com.naturalprogrammer.spring.lemon.validation; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +package com.naturalprogrammer.spring.lemon.validation; +import com.naturalprogrammer.spring.lemon.domain.AbstractUserRepository; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import com.naturalprogrammer.spring.lemon.domain.AbstractUserRepository; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; /** * Validator for unique-email diff --git a/spring-lemon-jpa/src/test/java/com/naturalprogrammer/spring/lemon/security/JwtServiceTests.java b/spring-lemon-jpa/src/test/java/com/naturalprogrammer/spring/lemon/security/JwtServiceTests.java deleted file mode 100644 index 2dc58133..00000000 --- a/spring-lemon-jpa/src/test/java/com/naturalprogrammer/spring/lemon/security/JwtServiceTests.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.naturalprogrammer.spring.lemon.security; - -import org.junit.Assert; -import org.junit.Test; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.security.authentication.BadCredentialsException; - -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.nimbusds.jose.KeyLengthException; -import com.nimbusds.jwt.JWTClaimsSet; - -public class JwtServiceTests { - - // An aes-128-cbc key generated at https://asecuritysite.com/encryption/keygen (take the "key" field) - private static final String SECRET1 = "926D96C90030DD58429D2751AC1BDBBC"; - private static final String SECRET2 = "538518AB685B514685DA8055C03DDA63"; - - private JwtService service1; - private JwtService service2; - - public JwtServiceTests() throws KeyLengthException { - - service1 = new JwtService(SECRET1); - service2 = new JwtService(SECRET2); - } - - @Test - public void testJwtParseToken() { - - String token = service1.createToken("auth", "subject", 5000L, - LecUtils.mapOf("username", "abc@example.com")); - JWTClaimsSet claims = service1.parseToken(token, "auth"); - - Assert.assertEquals("subject", claims.getSubject()); - Assert.assertEquals("abc@example.com", claims.getClaim("username")); - } - - @Test(expected = AccessDeniedException.class) - public void testJwtParseTokenWrongAudience() { - - String token = service1.createToken("auth", "subject", 5000L); - service1.parseToken(token, "auth2"); - } - - @Test(expected = AccessDeniedException.class) - public void testJwtParseTokenExpired() throws InterruptedException { - - String token = service1.createToken("auth", "subject", 1L); - Thread.sleep(1L); - service1.parseToken(token, "auth"); - } - - @Test(expected = BadCredentialsException.class) - public void testJwtParseTokenWrongSecret() { - - String token = service1.createToken("auth", "subject", 5000L); - service2.parseToken(token, "auth"); - } - - @Test(expected = AccessDeniedException.class) - public void testParseTokenCutoffTime() throws InterruptedException { - - String token = service1.createToken("auth", "subject", 5000L); - Thread.sleep(1L); - service1.parseToken(token, "auth", System.currentTimeMillis()); - } -} diff --git a/spring-lemon-reactive/pom.xml b/spring-lemon-reactive/pom.xml index 42b2ef6e..78bc4b7b 100644 --- a/spring-lemon-reactive/pom.xml +++ b/spring-lemon-reactive/pom.xml @@ -1,45 +1,60 @@ - - - 4.0.0 - - com.naturalprogrammer.spring-lemon - spring-lemon-reactive - jar - - spring-lemon-reactive - Helper reactive library for Spring Boot REST APIs - - - com.naturalprogrammer - spring-lemon - 1.0.0.M4 - - - - - - com.naturalprogrammer.spring-lemon - spring-lemon-commons - ${project.version} - - - - org.springframework.boot - spring-boot-starter-webflux - - - - org.springframework.boot - spring-boot-starter-data-mongodb-reactive - - - - io.projectreactor - reactor-test - test - - - - - + + + 4.0.0 + + com.naturalprogrammer.spring-lemon + spring-lemon-reactive + jar + + ${project.groupId}:${project.artifactId} + Helper reactive library for Spring Boot REST APIs + https://github.com/naturalprogrammer/spring-lemon + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + Sanjay Patel + skpatel20@gmail.com + naturalprogrammer.com + https://www.naturalprogrammer.com + + + + + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + scm:git:git://github.com/naturalprogrammer/spring-lemon.git + https://github.com/naturalprogrammer/spring-lemon + HEAD + + + + com.naturalprogrammer + spring-lemon + 1.0.2 + + + + + + com.naturalprogrammer.spring-lemon + spring-lemon-commons-mongo + ${project.version} + + + + io.projectreactor + reactor-test + test + + + + + diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveAutoConfiguration.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveAutoConfiguration.java index 049da887..226162e0 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveAutoConfiguration.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveAutoConfiguration.java @@ -1,54 +1,48 @@ -package com.naturalprogrammer.spring.lemonreactive; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.io.Serializable; +package com.naturalprogrammer.spring.lemonreactive; +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.domain.IdConverter; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commonsmongo.LemonCommonsMongoAutoConfiguration; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; +import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUserRepository; +import com.naturalprogrammer.spring.lemonreactive.security.LemonReactiveSecurityConfig; +import com.naturalprogrammer.spring.lemonreactive.security.LemonReactiveUserDetailsService; +import com.naturalprogrammer.spring.lemonreactive.security.ReactiveOAuth2AuthenticationSuccessHandler; +import com.naturalprogrammer.spring.lemonreactive.util.LerUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.bson.types.ObjectId; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration; -import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.config.EnableMongoAuditing; -import org.springframework.security.access.PermissionEvaluator; -import org.springframework.security.access.expression.AbstractSecurityExpressionHandler; -import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity; -import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.crypto.password.PasswordEncoder; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import com.naturalprogrammer.spring.lemon.commons.LemonCommonsAutoConfiguration; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.exceptions.ErrorResponseComposer; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; -import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; -import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUserRepository; -import com.naturalprogrammer.spring.lemonreactive.exceptions.LemonReactiveErrorAttributes; -import com.naturalprogrammer.spring.lemonreactive.exceptions.handlers.VersionExceptionHandler; -import com.naturalprogrammer.spring.lemonreactive.security.LemonReactiveSecurityConfig; -import com.naturalprogrammer.spring.lemonreactive.security.LemonReactiveUserDetailsService; -import com.naturalprogrammer.spring.lemonreactive.util.LerUtils; +import java.io.Serializable; @Configuration -@EnableMongoAuditing -@EnableReactiveMethodSecurity @AutoConfigureBefore({ - WebFluxAutoConfiguration.class, - ErrorWebFluxAutoConfiguration.class, - SecurityAutoConfiguration.class, - ReactiveSecurityAutoConfiguration.class, ReactiveUserDetailsServiceAutoConfiguration.class, - LemonCommonsAutoConfiguration.class}) -@ComponentScan(basePackageClasses=VersionExceptionHandler.class) + LemonCommonsMongoAutoConfiguration.class}) public class LemonReactiveAutoConfiguration { private static final Log log = LogFactory.getLog(LemonReactiveAutoConfiguration.class); @@ -56,49 +50,53 @@ public class LemonReactiveAutoConfiguration { public LemonReactiveAutoConfiguration() { log.info("Created"); } - - - /** - * Configures an Error Attributes if missing - */ + @Bean - @ConditionalOnMissingBean(ErrorAttributes.class) - public - ErrorAttributes errorAttributes(ErrorResponseComposer errorResponseComposer) { - - log.info("Configuring LemonErrorAttributes"); - return new LemonReactiveErrorAttributes(errorResponseComposer); + @ConditionalOnMissingBean(IdConverter.class) + public + IdConverter idConverter(LemonReactiveService lemonService) { + return lemonService::toId; } - + @Bean + @ConditionalOnMissingBean(ReactiveOAuth2AuthenticationSuccessHandler.class) + public , ID extends Serializable> + ReactiveOAuth2AuthenticationSuccessHandler reactiveOAuth2AuthenticationSuccessHandler( + BlueTokenService blueTokenService, + AbstractMongoUserRepository userRepository, + LemonReactiveUserDetailsService userDetailsService, + LemonReactiveService lemonService, + PasswordEncoder passwordEncoder, + LemonProperties properties +) { + + log.info("Configuring ReactiveOAuth2AuthenticationSuccessHandler ..."); + return new ReactiveOAuth2AuthenticationSuccessHandler( + blueTokenService, + userRepository, + userDetailsService, + lemonService, + passwordEncoder, + properties); + } @Bean @ConditionalOnMissingBean(LemonReactiveSecurityConfig.class) public , ID extends Serializable> LemonReactiveSecurityConfig lemonReactiveSecurityConfig( - JwtService jwtService, - LemonReactiveUserDetailsService userDetailsService) { + BlueTokenService blueTokenService, + LemonReactiveUserDetailsService userDetailsService, + ReactiveOAuth2AuthenticationSuccessHandler reactiveOAuth2AuthenticationSuccessHandler, + LemonProperties properties) { log.info("Configuring LemonReactiveSecurityConfig ..."); - return new LemonReactiveSecurityConfig(jwtService, userDetailsService); - } - - - /** - * Configures SecurityWebFilterChain if missing - */ - @Bean - public SecurityWebFilterChain springSecurityFilterChain( - ServerHttpSecurity http, - LemonReactiveSecurityConfig securityConfig, - AbstractSecurityExpressionHandler expressionHandler, - PermissionEvaluator permissionEvaluator) { - - log.info("Configuring SecurityWebFilterChain ..."); - expressionHandler.setPermissionEvaluator(permissionEvaluator); - return securityConfig.springSecurityFilterChain(http); + return new LemonReactiveSecurityConfig( + blueTokenService, + userDetailsService, + reactiveOAuth2AuthenticationSuccessHandler, + properties); } @@ -114,16 +112,7 @@ LemonReactiveUserDetailsService userDetailService(AbstractMongoUserReposi return new LemonReactiveUserDetailsService(userRepository); } - @Bean - public SimpleModule objectIdModule() { - - SimpleModule module = new SimpleModule(); - module.addSerializer(ObjectId.class, new ToStringSerializer()); - - return module; - } - - + /** * Configures LeeUtils */ diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveController.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveController.java index d0c9f7ac..e577c33d 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveController.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveController.java @@ -1,36 +1,47 @@ -package com.naturalprogrammer.spring.lemonreactive; - -import java.io.Serializable; -import java.util.Map; -import java.util.Optional; - -import javax.validation.Valid; -import javax.validation.constraints.NotBlank; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.server.ServerWebExchange; +package com.naturalprogrammer.spring.lemonreactive; import com.naturalprogrammer.spring.lemon.commons.LemonProperties; import com.naturalprogrammer.spring.lemon.commons.domain.ChangePasswordForm; import com.naturalprogrammer.spring.lemon.commons.domain.ResetPasswordForm; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; import com.naturalprogrammer.spring.lemonreactive.forms.EmailForm; -import com.naturalprogrammer.spring.lemonreactive.util.LerUtils; - +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import java.io.Serializable; +import java.util.Map; +import java.util.Optional; /** * The Lemon Mongo API. See the @@ -44,37 +55,41 @@ public class LemonReactiveController private static final Log log = LogFactory.getLog(LemonReactiveController.class); - private long jwtExpirationMillis; - private JwtService jwtService; - private LemonReactiveService lemonReactiveService; - + protected long jwtExpirationMillis; + protected LemonReactiveService lemonReactiveService; @Autowired public void createLemonController( LemonProperties properties, - LemonReactiveService lemonReactiveService, - JwtService jwtService) { + LemonReactiveService lemonReactiveService) { this.jwtExpirationMillis = properties.getJwt().getExpirationMillis(); this.lemonReactiveService = lemonReactiveService; - this.jwtService = jwtService; log.info("Created"); } /** - * A simple function for pinging this server. + * Afgter a successful login, returns the current user with an authorization header. */ @PostMapping("/login") - public Mono> login(ServerWebExchange exchange) { + public Mono login(ServerWebExchange exchange) { log.debug("Returning current user ... "); - long expirationMillis = exchange.getAttributeOrDefault("expirationMillis", jwtExpirationMillis); - Mono>> currentUser = LerUtils.currentUser(); - return lemonReactiveService.userWithToken( - currentUser.map(Optional::get), exchange.getResponse(), expirationMillis); + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(Authentication::getPrincipal) + .cast(LemonPrincipal.class) + .doOnNext(LemonPrincipal::eraseCredentials) + .map(LemonPrincipal::currentUser) + .zipWith(exchange.getFormData()) + .doOnNext(tuple -> { + long expirationMillis = lemonReactiveService.getExpirationMillis(tuple.getT2()); + lemonReactiveService.addAuthHeader(exchange.getResponse(), tuple.getT1(), expirationMillis); + }) + .map(Tuple2::getT1); } @@ -103,9 +118,7 @@ public Mono> getContext( return lemonReactiveService .getContext(expirationMillis, response) - .doOnNext(context -> { - log.debug("Returning context " + context); - }); + .doOnNext(context -> log.debug("Returning context " + context)); } @@ -115,7 +128,7 @@ public Mono> getContext( */ @PostMapping("/users") @ResponseStatus(HttpStatus.CREATED) - protected Mono> signup(Mono user, ServerHttpResponse response) { + protected Mono signup(Mono user, ServerHttpResponse response) { log.debug("Signing up: " + user); @@ -140,7 +153,7 @@ public Mono resendVerificationMail(@PathVariable("id") ID userId) { * Verifies current-user */ @PostMapping("/users/{id}/verification") - public Mono> verifyUser( + public Mono verifyUser( @PathVariable ID id, ServerWebExchange exchange) { @@ -166,7 +179,7 @@ public Mono forgotPassword(ServerWebExchange exchange) { * Resets password after it's forgotten */ @PostMapping("/reset-password") - public Mono> resetPassword( + public Mono resetPassword( @RequestBody @Valid Mono form, ServerHttpResponse response) { @@ -200,8 +213,8 @@ public Mono fetchUserById(@PathVariable ID id) { /** * Updates a user */ - @PatchMapping("/users/{id}") - public Mono> updateUser( + @PatchMapping(value = "/users/{id}") + public Mono updateUser( @PathVariable ID id, @RequestBody @NotBlank Mono patch, ServerHttpResponse response) { @@ -242,7 +255,7 @@ public Mono requestEmailChange(@PathVariable ID id, * Changes the email */ @PostMapping("/users/{userId}/email") - public Mono> changeEmail( + public Mono changeEmail( @PathVariable ID userId, ServerWebExchange exchange) { @@ -266,12 +279,23 @@ public Mono> fetchNewToken(ServerWebExchange exchange) { //return LecUtils.mapOf("token", lemonService.fetchNewToken(expirationMillis, username)); } + + + /** + * Fetch a self-sufficient token with embedded UserDto - for interservice communications + */ + @GetMapping("/fetch-full-token") + public Mono> fetchFullToken(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader) { + + log.debug("Fetching a micro token"); + return lemonReactiveService.fetchFullToken(authHeader); + } /** * returns the current user and a new authorization token in the response */ - protected Mono> userWithToken(Mono> userDto, + protected Mono userWithToken(Mono userDto, ServerHttpResponse response) { return lemonReactiveService.userWithToken(userDto, response, jwtExpirationMillis); diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveService.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveService.java index 91f2ae91..38e19039 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveService.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/LemonReactiveService.java @@ -1,59 +1,70 @@ -package com.naturalprogrammer.spring.lemonreactive; - -import java.io.Serializable; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.userdetails.ReactiveUserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.util.MultiValueMap; -import org.springframework.web.server.ServerWebExchange; +package com.naturalprogrammer.spring.lemonreactive; +import com.naturalprogrammer.spring.lemon.commons.AbstractLemonService; import com.naturalprogrammer.spring.lemon.commons.LemonProperties; -import com.naturalprogrammer.spring.lemon.commons.LemonProperties.Admin; import com.naturalprogrammer.spring.lemon.commons.domain.ChangePasswordForm; import com.naturalprogrammer.spring.lemon.commons.domain.ResetPasswordForm; import com.naturalprogrammer.spring.lemon.commons.mail.LemonMailData; import com.naturalprogrammer.spring.lemon.commons.mail.MailSender; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.GreenTokenService; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemon.commonsmongo.LecmUtils; +import com.naturalprogrammer.spring.lemon.commonsreactive.util.LecrUtils; import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUserRepository; import com.naturalprogrammer.spring.lemonreactive.forms.EmailForm; import com.naturalprogrammer.spring.lemonreactive.util.LerUtils; import com.nimbusds.jwt.JWTClaimsSet; - +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.MultiValueMap; +import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuple3; import reactor.util.function.Tuples; +import java.io.Serializable; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + public abstract class LemonReactiveService - , ID extends Serializable> { + , ID extends Serializable> + extends AbstractLemonService { private static final Log log = LogFactory.getLog(LemonReactiveService.class); - private LemonProperties properties; - private PasswordEncoder passwordEncoder; - private MailSender mailSender; - private AbstractMongoUserRepository userRepository; - private ReactiveUserDetailsService userDetailsService; - private JwtService jwtService; + protected AbstractMongoUserRepository userRepository; + protected ReactiveUserDetailsService userDetailsService; @Autowired public void createLemonService(LemonProperties properties, @@ -61,68 +72,37 @@ public void createLemonService(LemonProperties properties, MailSender mailSender, AbstractMongoUserRepository userRepository, ReactiveUserDetailsService userDetailsService, - JwtService jwtService) { + BlueTokenService blueTokenService, + GreenTokenService greenTokenService) { this.properties = properties; this.passwordEncoder = passwordEncoder; this.mailSender = mailSender; this.userRepository = userRepository; this.userDetailsService = userDetailsService; - this.jwtService = jwtService; + this.blueTokenService = blueTokenService; + this.greenTokenService = greenTokenService; log.info("Created"); } - /** - * This method is called after the application is ready. - * Needs to be public - otherwise Spring screams. - * - * @param event - */ - @EventListener - public void afterApplicationReady(ApplicationReadyEvent event) { - - log.info("Starting up Spring Lemon ..."); - onStartup(); // delegate to onStartup() - log.info("Spring Lemon started"); - } - - public void onStartup() { userDetailsService .findByUsername(properties.getAdmin().getUsername()) // Check if the user already exists .doOnError(e -> e instanceof UsernameNotFoundException, e -> { // Doesn't exist. So, create it. + log.debug("Creating first admin ... "); U user = createAdminUser(); - userRepository.insert(user).subscribe(); + userRepository.insert(user) + .doOnError(err -> log.warn("Error creating initial admin " + err)) + .subscribe(); + log.debug("Created first admin."); }).subscribe(); } - /** - * Creates the initial Admin user. - * Override this if needed. - */ - protected U createAdminUser() { - - // fetch data about the user to be created - Admin initialAdmin = properties.getAdmin(); - - log.info("Creating the first admin user: " + initialAdmin.getUsername()); - - // create the user - U user = newUser(); - user.setEmail(initialAdmin.getUsername()); - user.setPassword(passwordEncoder.encode( - properties.getAdmin().getPassword())); - user.getRoles().add(UserUtils.Role.ADMIN); - - return user; - } - - /** * Creates a new user object. Must be overridden in the * subclass, like this: @@ -154,7 +134,7 @@ public Mono> getContext(Optional expirationMillis, Ser log.debug("Getting context ..."); - Mono>> userDtoMono = LerUtils.currentUser(); + Mono> userDtoMono = LecrUtils.currentUser(); return userDtoMono.map(optionalUser -> { Map context = buildContext(); @@ -169,110 +149,36 @@ public Mono> getContext(Optional expirationMillis, Ser } - protected Map buildContext() { - - // make the context - Map sharedProperties = new HashMap(2); - sharedProperties.put("reCaptchaSiteKey", properties.getRecaptcha().getSitekey()); - sharedProperties.put("shared", properties.getShared()); - - Map context = new HashMap<>(); - context.put("context", sharedProperties); - - return context; - } - - /** * Signs up a user. */ - public Mono> signup(Mono user) { + public Mono signup(Mono user) { log.debug("Signing up user: " + user); return user .doOnNext(this::initUser) .flatMap(userRepository::insert) - .doOnSuccess(this::sendVerificationMail) + .doOnNext(this::sendVerificationMail) + .doOnNext(AbstractMongoUser::eraseCredentials) .map(AbstractMongoUser::toUserDto); } - protected void initUser(U user) { - - log.debug("Initializing user: " + user); - - user.setPassword(passwordEncoder.encode(user.getPassword())); // encode the password - makeUnverified(user); // make the user unverified - } - - - /** - * Makes a user unverified - */ - protected void makeUnverified(U user) { - - user.getRoles().add(UserUtils.Role.UNVERIFIED); - user.setCredentialsUpdatedMillis(System.currentTimeMillis()); - } - - - /** - * Sends verification mail to a unverified user. - */ - protected void sendVerificationMail(final U user) { - try { - - log.debug("Sending verification mail to: " + user); - - String verificationCode = jwtService.createToken(JwtService.VERIFY_AUDIENCE, - user.getId().toString(), properties.getJwt().getExpirationMillis(), - LecUtils.mapOf("email", user.getEmail())); - - // make the link - String verifyLink = properties.getApplicationUrl() - + "/users/" + user.getId() + "/verification?code=" + verificationCode; - - // send the mail - sendVerificationMail(user, verifyLink); - - log.debug("Verification mail to " + user.getEmail() + " queued."); - - } catch (Throwable e) { - // In case of exception, just log the error and keep silent - log.error(ExceptionUtils.getStackTrace(e)); - } - } - - - /** - * Sends verification mail to a unverified user. - * Override this method if you're using a different MailData - */ - protected void sendVerificationMail(final U user, String verifyLink) { - - // send the mail - mailSender.send(LemonMailData.of(user.getEmail(), - LexUtils.getMessage("com.naturalprogrammer.spring.verifySubject"), - LexUtils.getMessage( - "com.naturalprogrammer.spring.verifyEmail", verifyLink))); - } - - /** * Resends verification mail to the user. */ public Mono resendVerificationMail(ID userId) { return findUserById(userId) - .zipWith(LerUtils.currentUser()) + .zipWith(LecrUtils.currentUser()) .doOnNext(this::ensureEditable) .map(Tuple2::getT1) .doOnNext(this::resendVerificationMail).then(); } - protected void ensureEditable(Tuple2>> tuple) { + protected void ensureEditable(Tuple2> tuple) { LecUtils.ensureAuthority( tuple.getT1().hasPermission(tuple.getT2().orElse(null), UserUtils.Permission.EDIT), @@ -294,7 +200,7 @@ protected void resendVerificationMail(U user) { } - public Mono> verifyUser(ID userId, Mono> formData) { + public Mono verifyUser(ID userId, Mono> formData) { log.debug("Verifying user ..."); @@ -319,7 +225,8 @@ public U verifyUser(Tuple2> tuple) { LexUtils.validate(user.hasRole(UserUtils.Role.UNVERIFIED), "com.naturalprogrammer.spring.alreadyVerified").go(); - JWTClaimsSet claims = jwtService.parseToken(verificationCode, JwtService.VERIFY_AUDIENCE, user.getCredentialsUpdatedMillis()); + JWTClaimsSet claims = greenTokenService.parseToken( + verificationCode, GreenTokenService.VERIFY_AUDIENCE, user.getCredentialsUpdatedMillis()); LecUtils.ensureAuthority( claims.getSubject().equals(user.getId().toString()) && @@ -346,51 +253,14 @@ public Mono forgotPassword(Mono> formData) { } - /** - * Mails the forgot password link. - * - * @param user - */ - public void mailForgotPasswordLink(U user) { - - log.debug("Mailing forgot password link to user: " + user); - - String forgotPasswordCode = jwtService.createToken(JwtService.FORGOT_PASSWORD_AUDIENCE, - user.getEmail(), properties.getJwt().getExpirationMillis()); - - // make the link - String forgotPasswordLink = properties.getApplicationUrl() - + "/reset-password?code=" + forgotPasswordCode; - - mailForgotPasswordLink(user, forgotPasswordLink); - - log.debug("Forgot password link mail queued."); - } - - - /** - * Mails the forgot password link. - * - * Override this method if you're using a different MailData - */ - public void mailForgotPasswordLink(U user, String forgotPasswordLink) { - - // send the mail - mailSender.send(LemonMailData.of(user.getEmail(), - LexUtils.getMessage("com.naturalprogrammer.spring.forgotPasswordSubject"), - LexUtils.getMessage("com.naturalprogrammer.spring.forgotPasswordEmail", - forgotPasswordLink))); - } - - - public Mono> resetPassword(Mono resetPasswordForm) { + public Mono resetPassword(Mono resetPasswordForm) { return resetPasswordForm.map(form -> { log.debug("Resetting password ..."); - JWTClaimsSet claims = jwtService.parseToken(form.getCode(), - JwtService.FORGOT_PASSWORD_AUDIENCE); + JWTClaimsSet claims = greenTokenService.parseToken(form.getCode(), + GreenTokenService.FORGOT_PASSWORD_AUDIENCE); String email = claims.getSubject(); @@ -429,7 +299,7 @@ public U resetPassword(Tuple3 tuple) { /** * returns the current user and a new authorization token in the response */ - public Mono> userWithToken(Mono> userDto, + public Mono userWithToken(Mono userDto, ServerHttpResponse response, long expirationMillis) { return userDto.doOnNext(user -> { @@ -439,12 +309,12 @@ public Mono> userWithToken(Mono> userDto, } - protected void addAuthHeader(ServerHttpResponse response, UserDto userDto, long expirationMillis) { + protected void addAuthHeader(ServerHttpResponse response, UserDto userDto, long expirationMillis) { log.debug("Adding auth header for " + userDto.getUsername()); response.getHeaders().add(LecUtils.TOKEN_RESPONSE_HEADER_NAME, LecUtils.TOKEN_PREFIX + - jwtService.createToken(JwtService.AUTH_AUDIENCE, userDto.getUsername(), expirationMillis)); + blueTokenService.createToken(BlueTokenService.AUTH_AUDIENCE, userDto.getUsername(), expirationMillis)); } @@ -462,7 +332,7 @@ public Mono fetchUserByEmail(Mono> formData) { return email; }) .flatMap(this::findUserByEmail) - .zipWith(LerUtils.currentUser()) + .zipWith(LecrUtils.currentUser()) .doOnNext(this::hideConfidentialFields) .map(Tuple2::getT1); } @@ -471,36 +341,36 @@ public Mono fetchUserByEmail(Mono> formData) { public Mono fetchUserById(ID userId) { // fetch the user return findUserById(userId) - .zipWith(LerUtils.currentUser()) + .zipWith(LecrUtils.currentUser()) .doOnNext(this::hideConfidentialFields) .map(Tuple2::getT1); } - public Mono> updateUser(ID userId, Mono patch) { + public Mono updateUser(ID userId, Mono patch) { - return Mono.zip(findUserById(userId), LerUtils.currentUser(), patch) + return Mono.zip(findUserById(userId), LecrUtils.currentUser(), patch) .doOnNext(this::ensureEditable) - .map((Tuple3>, String> tuple3) -> + .map((Tuple3, String> tuple3) -> this.updateUser(tuple3.getT1(), tuple3.getT2(), tuple3.getT3())) .flatMap(userRepository::save) .map(user -> { - UserDto userDto = user.toUserDto(); + UserDto userDto = user.toUserDto(); userDto.setPassword(null); return userDto; }); } - protected U updateUser(U user, Optional> currentUser, String patch) { + protected U updateUser(U user, Optional currentUser, String patch) { log.debug("Updating user: " + user); - U updatedUser = LerUtils.applyPatch(user, patch); // create a patched form - LexUtils.validate("updatedUser", updatedUser, UserUtils.UpdateValidation.class); - LerUtils.ensureCorrectVersion(user, updatedUser); + U updatedUser = LecrUtils.applyPatch(user, patch); // create a patched form + LexUtils.validateBean("updatedUser", updatedUser, UserUtils.UpdateValidation.class).go(); + LecmUtils.ensureCorrectVersion(user, updatedUser); - updateUserFields(user, updatedUser, (UserDto) currentUser.get()); + updateUserFields(user, updatedUser, currentUser.get()); log.debug("Updated user: " + user); return user; @@ -510,13 +380,13 @@ protected U updateUser(U user, Optional> currentUser, Stri /** * Updates the fields of the users. Override this if you have more fields. */ - protected void updateUserFields(U user, U updatedUser, UserDto currentUser) { + protected void updateUserFields(U user, U updatedUser, UserDto currentUser) { log.debug("Updating user fields for user: " + user); // Another good admin must be logged in to edit roles if (currentUser.isGoodAdmin() && - !currentUser.getId().equals(user.getId())) { + !currentUser.getId().equals(user.getId().toString())) { log.debug("Updating roles for user: " + user); @@ -546,25 +416,25 @@ protected void updateUserFields(U user, U updatedUser, UserDto currentUser) public Mono findUserByEmail(String email) { return userRepository .findByEmail(email) - .switchIfEmpty(LerUtils.notFoundMono()); + .switchIfEmpty(LecrUtils.notFoundMono()); } public Mono findUserById(ID id) { return userRepository .findById(id) - .switchIfEmpty(LerUtils.notFoundMono()); + .switchIfEmpty(LecrUtils.notFoundMono()); } /** * Hides the confidential fields before sending to client */ - protected void hideConfidentialFields(Tuple2>> tuple) { + protected void hideConfidentialFields(Tuple2> tuple) { U user = tuple.getT1(); - user.setPassword(null); // JsonIgnore didn't work + user.eraseCredentials(); if (!user.hasPermission(tuple.getT2().orElse(null), UserUtils.Permission.EDIT)) user.setEmail(null); @@ -573,13 +443,13 @@ protected void hideConfidentialFields(Tuple2>> } - public Mono> changePassword(ID userId, Mono changePasswordForm) { + public Mono changePassword(ID userId, Mono changePasswordForm) { - return Mono.zip(findUserById(userId), LerUtils.currentUser()) + return Mono.zip(findUserById(userId), LecrUtils.currentUser()) .doOnNext(this::ensureEditable) .flatMap(tuple -> Mono.zip( Mono.just(tuple.getT1()), - findUserById(((UserDto)tuple.getT2().get()).getId()), + findUserById(toId(tuple.getT2().get().getId())), changePasswordForm) .doOnNext(this::changePassword)) .map(Tuple2::getT1) @@ -597,7 +467,7 @@ protected void changePassword(Tuple3 tuple) { String oldPassword = loggedIn.getPassword(); - LexUtils.validate("changePasswordForm.oldPassword", + LexUtils.validateField("changePasswordForm.oldPassword", passwordEncoder.matches(changePasswordForm.getOldPassword(), oldPassword), "com.naturalprogrammer.spring.wrong.password").go(); @@ -611,11 +481,11 @@ protected void changePassword(Tuple3 tuple) { public Mono requestEmailChange(ID userId, Mono emailForm) { - return Mono.zip(findUserById(userId), LerUtils.currentUser()) + return Mono.zip(findUserById(userId), LecrUtils.currentUser()) .doOnNext(this::ensureEditable) .flatMap(tuple -> Mono.zip( Mono.just(tuple.getT1()), - findUserById(((UserDto)tuple.getT2().get()).getId()), + findUserById(toId(tuple.getT2().get().getId())), emailForm) .doOnNext(this::requestEmailChange)) .map(Tuple2::getT1) @@ -624,23 +494,23 @@ public Mono requestEmailChange(ID userId, Mono emailForm) { .then(); } + protected abstract ID toId(String id); + protected void requestEmailChange(Tuple3 tuple) { U user = tuple.getT1(); - U loggedIn = tuple.getT2(); EmailForm emailForm = tuple.getT3(); log.debug("Requesting email change: " + user); // checks - LexUtils.validate("updatedUser.password", + LexUtils.validateField("emailFormMono.password", passwordEncoder.matches(emailForm.getPassword(), user.getPassword()), "com.naturalprogrammer.spring.wrong.password").go(); // preserves the new email id user.setNewEmail(emailForm.getNewEmail()); - log.debug("Requested email change: " + user); } @@ -650,7 +520,8 @@ protected void requestEmailChange(Tuple3 tuple) { */ protected void mailChangeEmailLink(U user) { - String changeEmailCode = jwtService.createToken(JwtService.CHANGE_EMAIL_AUDIENCE, + String changeEmailCode = greenTokenService.createToken( + GreenTokenService.CHANGE_EMAIL_AUDIENCE, user.getId().toString(), properties.getJwt().getExpirationMillis(), LecUtils.mapOf("newEmail", user.getNewEmail())); @@ -668,7 +539,7 @@ protected void mailChangeEmailLink(U user) { log.debug("Change email link mail queued."); - } catch (Throwable e) { + } catch (Exception e) { // In case of exception, just log the error and keep silent log.error(ExceptionUtils.getStackTrace(e)); } @@ -692,15 +563,14 @@ protected void mailChangeEmailLink(U user, String changeEmailLink) { @PreAuthorize("isAuthenticated()") - public Mono> changeEmail(ID userId, Mono> formData) { + public Mono changeEmail(ID userId, Mono> formData) { log.debug("Changing email of current user ..."); - return LerUtils.currentUser() - .doOnNext(currentUser -> { - LexUtils.validate(userId.equals(currentUser.get().getId()), - "com.naturalprogrammer.spring.wrong.login").go(); - }) + return LecrUtils.currentUser() + .doOnNext(currentUser -> + LexUtils.validate(userId.equals(toId(currentUser.get().getId())), + "com.naturalprogrammer.spring.wrong.login").go()) .then(Mono.zip(findUserById(userId), formData)) .map(this::validateChangeEmail) .flatMap(user -> Mono.zip(Mono.just(user), @@ -725,8 +595,8 @@ protected U validateChangeEmail(Tuple2> tuple) LexUtils.validate(StringUtils.isNotBlank(user.getNewEmail()), "com.naturalprogrammer.spring.blank.newEmail").go(); - JWTClaimsSet claims = jwtService.parseToken(code, - JwtService.CHANGE_EMAIL_AUDIENCE, + JWTClaimsSet claims = greenTokenService.parseToken(code, + GreenTokenService.CHANGE_EMAIL_AUDIENCE, user.getCredentialsUpdatedMillis()); LecUtils.ensureAuthority( @@ -764,24 +634,53 @@ protected U changeEmail(Tuple2> tuple) { @PreAuthorize("isAuthenticated()") public Mono> fetchNewToken(ServerWebExchange exchange) { - return Mono.zip(LerUtils.currentUser(), exchange.getFormData()).map(tuple -> { + return Mono.zip(LecrUtils.currentUser(), exchange.getFormData()).map(tuple -> { - UserDto currentUser = (UserDto) tuple.getT1().get(); + UserDto currentUser = tuple.getT1().get(); String username = tuple.getT2().getFirst("username"); - if (StringUtils.isNotBlank(username)) + if (StringUtils.isBlank(username)) username = currentUser.getUsername(); - long expirationMillis = properties.getJwt().getExpirationMillis(); - String expirationMillisStr = tuple.getT2().getFirst("expirationMillis"); - if (StringUtils.isNotBlank(expirationMillisStr)) - expirationMillis = Long.parseLong(expirationMillisStr); + long expirationMillis = getExpirationMillis(tuple.getT2()); LecUtils.ensureAuthority(currentUser.getUsername().equals(username) || currentUser.isGoodAdmin(), "com.naturalprogrammer.spring.notGoodAdminOrSameUser"); return Collections.singletonMap("token", LecUtils.TOKEN_PREFIX + - jwtService.createToken(JwtService.AUTH_AUDIENCE, username, expirationMillis)); + blueTokenService.createToken(blueTokenService.AUTH_AUDIENCE, username, expirationMillis)); }); } + + + @PreAuthorize("isAuthenticated()") + public Mono> fetchFullToken(String authHeader) { + + LecUtils.ensureCredentials(blueTokenService.parseClaim(authHeader.substring(LecUtils.TOKEN_PREFIX_LENGTH), + BlueTokenService.USER_CLAIM) == null, "com.naturalprogrammer.spring.fullTokenNotAllowed"); + + return LecrUtils.currentUser().map(optionalUser -> { + + UserDto currentUser = optionalUser.get(); + + Map claimMap = Collections.singletonMap(BlueTokenService.USER_CLAIM, + LecUtils.serialize(currentUser)); // Not serializing converts it to a JsonNode + + return Collections.singletonMap("token", LecUtils.TOKEN_PREFIX + + blueTokenService.createToken(BlueTokenService.AUTH_AUDIENCE, currentUser.getUsername(), + Long.valueOf(properties.getJwt().getShortLivedMillis()), + claimMap)); + }); + } + + + public long getExpirationMillis(MultiValueMap formData) { + + long expirationMillis = properties.getJwt().getExpirationMillis(); + String expirationMillisStr = formData.getFirst("expirationMillis"); + if (StringUtils.isNotBlank(expirationMillisStr)) + expirationMillis = Long.parseLong(expirationMillisStr); + + return expirationMillis; + } } diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractDocument.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractDocument.java deleted file mode 100644 index 4047e7fb..00000000 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractDocument.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.naturalprogrammer.spring.lemonreactive.domain; - -import java.io.Serializable; -import java.time.Instant; - -import org.springframework.data.annotation.CreatedBy; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.annotation.Version; -import org.springframework.data.mongodb.core.mapping.Document; - -import com.naturalprogrammer.spring.lemon.commons.security.PermissionEvaluatorEntity; -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; - -import lombok.Getter; -import lombok.Setter; - -@Document -@Getter @Setter -public abstract class AbstractDocument implements PermissionEvaluatorEntity { - - @Id - protected ID id; - - @CreatedBy - protected ID createdBy; - - @CreatedDate - protected Instant createdDate; - - @LastModifiedBy - protected ID lastModifiedBy; - - @LastModifiedDate - protected Instant lastModifiedDate; - - @Version - protected long version; - - /** - * Whether the given user has the given permission for - * this entity. Override this method where you need. - */ - @Override - public boolean hasPermission(UserDto currentUser, String permission) { - - return false; - } - -} diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractMongoUser.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractMongoUser.java index 9fd5711d..2623d4e2 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractMongoUser.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractMongoUser.java @@ -1,26 +1,45 @@ -package com.naturalprogrammer.spring.lemonreactive.domain; - -import java.io.Serializable; -import java.util.HashSet; -import java.util.Set; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.springframework.data.annotation.Transient; -import org.springframework.data.mongodb.core.index.Indexed; +package com.naturalprogrammer.spring.lemonreactive.domain; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; +import com.naturalprogrammer.spring.lemon.commons.domain.LemonUser; import com.naturalprogrammer.spring.lemon.commons.security.UserDto; import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import com.naturalprogrammer.spring.lemon.commons.validation.Captcha; import com.naturalprogrammer.spring.lemon.commons.validation.Password; +import com.naturalprogrammer.spring.lemon.commonsmongo.AbstractDocument; import com.naturalprogrammer.spring.lemonreactive.validation.UniqueEmail; - import lombok.Getter; import lombok.Setter; +import org.springframework.data.annotation.Transient; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.security.core.CredentialsContainer; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; @Getter @Setter public abstract class AbstractMongoUser - extends AbstractDocument { + extends AbstractDocument + implements CredentialsContainer, LemonUser { // email @JsonView(UserUtils.SignupInput.class) @@ -44,6 +63,7 @@ public abstract class AbstractMongoUser // holds reCAPTCHA response while signing up @Transient @JsonView(UserUtils.SignupInput.class) + @Captcha private String captchaResponse; public final boolean hasRole(String role) { @@ -56,7 +76,7 @@ public final boolean hasRole(String role) { * on this entity. */ @Override - public boolean hasPermission(UserDto currentUser, String permission) { + public boolean hasPermission(UserDto currentUser, String permission) { return UserUtils.hasPermission(getId(), currentUser, permission); } @@ -74,27 +94,17 @@ public String toString() { /** * Makes a User DTO */ - public UserDto toUserDto() { + public UserDto toUserDto() { - UserDto userDto = new UserDto<>(); + UserDto userDto = new UserDto(); - userDto.setId(getId()); + userDto.setId(getId().toString()); userDto.setUsername(email); userDto.setPassword(password); userDto.setRoles(roles); userDto.setTag(toTag()); - boolean unverified = hasRole(UserUtils.Role.UNVERIFIED); - boolean blocked = hasRole(UserUtils.Role.BLOCKED); - boolean admin = hasRole(UserUtils.Role.ADMIN); - boolean goodUser = !(unverified || blocked); - boolean goodAdmin = goodUser && admin; - - userDto.setAdmin(admin); - userDto.setBlocked(blocked); - userDto.setGoodAdmin(goodAdmin); - userDto.setGoodUser(goodUser); - userDto.setUnverified(unverified); + userDto.initialize(); return userDto; } @@ -107,4 +117,9 @@ protected Serializable toTag() { return null; } + + @Override + public void eraseCredentials() { + password = null; + } } diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractMongoUserRepository.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractMongoUserRepository.java index 53f3a78c..84a69a73 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractMongoUserRepository.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/domain/AbstractMongoUserRepository.java @@ -1,12 +1,27 @@ -package com.naturalprogrammer.spring.lemonreactive.domain; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.io.Serializable; +package com.naturalprogrammer.spring.lemonreactive.domain; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.repository.NoRepositoryBean; - import reactor.core.publisher.Mono; +import java.io.Serializable; + /** * Abstract UserRepository interface * diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/exceptions/handlers/VersionExceptionHandler.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/exceptions/handlers/VersionExceptionHandler.java deleted file mode 100644 index 27cd4359..00000000 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/exceptions/handlers/VersionExceptionHandler.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.naturalprogrammer.spring.lemonreactive.exceptions.handlers; - -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; - -import com.naturalprogrammer.spring.lemon.exceptions.VersionException; -import com.naturalprogrammer.spring.lemon.exceptions.handlers.AbstractExceptionHandler; - -@Component -@Order(Ordered.LOWEST_PRECEDENCE) -public class VersionExceptionHandler extends AbstractExceptionHandler { - - public VersionExceptionHandler() { - - super(VersionException.class.getSimpleName()); - log.info("Created"); - } - - @Override - public HttpStatus getStatus(VersionException ex) { - return HttpStatus.CONFLICT; - } -} \ No newline at end of file diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/forms/EmailForm.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/forms/EmailForm.java index 4e727acf..e49b335b 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/forms/EmailForm.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/forms/EmailForm.java @@ -1,8 +1,23 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemonreactive.forms; import com.naturalprogrammer.spring.lemon.commons.validation.Password; import com.naturalprogrammer.spring.lemonreactive.validation.UniqueEmail; - import lombok.Getter; import lombok.Setter; diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/LemonReactiveSecurityConfig.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/LemonReactiveSecurityConfig.java index fd72f979..c604cf54 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/LemonReactiveSecurityConfig.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/LemonReactiveSecurityConfig.java @@ -1,140 +1,116 @@ -package com.naturalprogrammer.spring.lemonreactive.security; - -import java.io.Serializable; -import java.util.function.Function; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.security.authentication.ReactiveAuthenticationManager; -import org.springframework.security.config.web.server.SecurityWebFiltersOrder; -import org.springframework.security.config.web.server.ServerHttpSecurity; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.security.web.server.ServerAuthenticationEntryPoint; -import org.springframework.security.web.server.authentication.AuthenticationWebFilter; -import org.springframework.security.web.server.authentication.ServerAuthenticationFailureHandler; -import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler; -import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler; -import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; -import org.springframework.web.server.ServerWebExchange; - -import com.naturalprogrammer.spring.lemon.commons.security.JwtAuthenticationToken; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; -import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; -import com.naturalprogrammer.spring.lemonreactive.util.LerUtils; -import com.nimbusds.jwt.JWTClaimsSet; - -import lombok.AllArgsConstructor; -import reactor.core.publisher.Mono; - -@AllArgsConstructor -public class LemonReactiveSecurityConfig , ID extends Serializable> { - - private static final Log log = LogFactory.getLog(LemonReactiveSecurityConfig.class); - - private JwtService jwtService; - private LemonReactiveUserDetailsService userDetailsService; - - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { - - log.info("Configuring SecurityWebFilterChain ..."); - - return http - .authorizeExchange() - .anyExchange().permitAll() - .and() - .formLogin() - .loginPage(loginPage()) // Should be "/login" by default, but not providing that overwrites our AuthenticationFailureHandler, because this is called later - .authenticationFailureHandler(authenticationFailureHandler()) - .authenticationSuccessHandler(new WebFilterChainServerAuthenticationSuccessHandler()) - .and() - .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) - .exceptionHandling() - .accessDeniedHandler(accessDeniedHandler()) - .authenticationEntryPoint(authenticationEntryPoint()) - .and() - .csrf().disable() - .addFilterAt(tokenAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION) - .build(); - } - - - /** - * Override this to change login URL - * - * @return - */ - protected String loginPage() { - - return "/api/core/login"; - } - - - protected AuthenticationWebFilter tokenAuthenticationFilter() { - - AuthenticationWebFilter filter = new AuthenticationWebFilter(tokenAuthenticationManager()); - filter.setAuthenticationConverter(tokenAuthenticationConverter()); - filter.setAuthenticationFailureHandler(authenticationFailureHandler()); - - return filter; - } - - protected ReactiveAuthenticationManager tokenAuthenticationManager() { - - return authentication -> { - - log.debug("Authenticating with token ..."); - - String token = (String) authentication.getCredentials(); - - JWTClaimsSet claims = jwtService.parseToken(token, JwtService.AUTH_AUDIENCE); - - String username = claims.getSubject(); - - return userDetailsService.findUserByUsername(username) - .switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(username)))) - .doOnNext(user -> { - log.debug("User found ..."); - LerUtils.ensureCredentialsUpToDate(claims, user); - }) - .map(AbstractMongoUser::toUserDto) - .map(LemonPrincipal::new) - .doOnNext(LemonPrincipal::eraseCredentials) - .map(principal -> new JwtAuthenticationToken(principal, token, principal.getAuthorities())); - }; - } - - protected Function> tokenAuthenticationConverter() { - - return serverWebExchange -> { - - String authorization = serverWebExchange.getRequest() - .getHeaders().getFirst(HttpHeaders.AUTHORIZATION); - - if(authorization == null || !authorization.startsWith(LecUtils.TOKEN_PREFIX)) - return Mono.empty(); - - return Mono.just(new JwtAuthenticationToken(authorization.substring(7))); - }; - } - - protected ServerAuthenticationFailureHandler authenticationFailureHandler() { - - return (webFilterExchange, exception) -> Mono.error(exception); - } - - protected ServerAccessDeniedHandler accessDeniedHandler() { - - return (webFilterExchange, exception) -> Mono.error(exception); - } - - protected ServerAuthenticationEntryPoint authenticationEntryPoint() { - - return (webFilterExchange, exception) -> Mono.error(exception); - } - -} +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemonreactive.security; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commonsreactive.security.LemonCommonsReactiveSecurityConfig; +import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; +import com.naturalprogrammer.spring.lemonreactive.util.LerUtils; +import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.WebFilterChainServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; +import reactor.core.publisher.Mono; + +import java.io.Serializable; + +public class LemonReactiveSecurityConfig, ID extends Serializable> extends LemonCommonsReactiveSecurityConfig { + + private static final Log log = LogFactory.getLog(LemonReactiveSecurityConfig.class); + + protected LemonReactiveUserDetailsService userDetailsService; + private LemonProperties properties; + private ReactiveOAuth2AuthenticationSuccessHandler reactiveOAuth2AuthenticationSuccessHandler; + + public LemonReactiveSecurityConfig(BlueTokenService blueTokenService, + LemonReactiveUserDetailsService userDetailsService, + ReactiveOAuth2AuthenticationSuccessHandler reactiveOAuth2AuthenticationSuccessHandler, + LemonProperties properties) { + + super(blueTokenService); + this.userDetailsService = userDetailsService; + this.reactiveOAuth2AuthenticationSuccessHandler = reactiveOAuth2AuthenticationSuccessHandler; + this.properties = properties; + + log.info("Created"); + } + + /** + * Configure form login + */ + @Override + protected void formLogin(ServerHttpSecurity http) { + + http.formLogin() + .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) + .loginPage(loginPage()) // Should be "/login" by default, but not providing that overwrites our AuthenticationFailureHandler, because this is called later + .authenticationFailureHandler((exchange, exception) -> Mono.error(exception)) + .authenticationSuccessHandler(new WebFilterChainServerAuthenticationSuccessHandler()); + } + + /** + * Override this to change login URL + */ + protected String loginPage() { + + return "/api/core/login"; + } + + /** + * Configure OAuth2 login + */ + @Override + protected void oauth2Login(ServerHttpSecurity http) { + + http.oauth2Login() + .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) + .authorizedClientRepository(new ReactiveCookieServerOAuth2AuthorizedClientRepository(properties)) + .authenticationSuccessHandler(reactiveOAuth2AuthenticationSuccessHandler) + .authenticationFailureHandler(this::onOauth2AuthenticationFailure); + } + + @Override + protected Mono fetchUserDto(JWTClaimsSet claims) { + + String username = claims.getSubject(); + + return userDetailsService.findUserByUsername(username) + .switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException(username)))) + .doOnNext(user -> { + log.debug("User found ..."); + LerUtils.ensureCredentialsUpToDate(claims, user); + }) + .map(AbstractMongoUser::toUserDto); + } + + protected Mono onOauth2AuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException exception) { + + ReactiveCookieServerOAuth2AuthorizedClientRepository.deleteCookies(webFilterExchange.getExchange(), + LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME, + LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); + + return Mono.error(exception); + } +} diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/LemonReactiveUserDetailsService.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/LemonReactiveUserDetailsService.java index 8b72a9e1..c0fd9a9f 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/LemonReactiveUserDetailsService.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/LemonReactiveUserDetailsService.java @@ -1,20 +1,34 @@ -package com.naturalprogrammer.spring.lemonreactive.security; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import java.io.Serializable; +package com.naturalprogrammer.spring.lemonreactive.security; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; +import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUserRepository; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; - -import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; -import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; -import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUserRepository; - import reactor.core.publisher.Mono; +import java.io.Serializable; + public class LemonReactiveUserDetailsService, ID extends Serializable> implements ReactiveUserDetailsService { diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/ReactiveCookieServerOAuth2AuthorizedClientRepository.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/ReactiveCookieServerOAuth2AuthorizedClientRepository.java new file mode 100644 index 00000000..ca6b97b0 --- /dev/null +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/ReactiveCookieServerOAuth2AuthorizedClientRepository.java @@ -0,0 +1,136 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemonreactive.security; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commonsreactive.util.LecrUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpCookie; +import org.springframework.http.ResponseCookie; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Collections; + +public class ReactiveCookieServerOAuth2AuthorizedClientRepository implements ServerOAuth2AuthorizedClientRepository { + + private static final Log log = LogFactory.getLog(ReactiveCookieServerOAuth2AuthorizedClientRepository.class); + + private int cookieExpirySecs; + + public ReactiveCookieServerOAuth2AuthorizedClientRepository(LemonProperties properties) { + + cookieExpirySecs = properties.getJwt().getShortLivedMillis() / 1000; + } + + @Override + public Mono loadAuthorizedClient(String clientRegistrationId, + Authentication principal, ServerWebExchange exchange) { + + log.debug("Loading authorized client for clientRegistrationId " + clientRegistrationId + + ", principal " + principal + ", and exchange " + exchange); + + return LecrUtils.fetchCookie(exchange, LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME) + .map(this::deserialize) + .orElse(Mono.empty()); + } + + @Override + public Mono saveAuthorizedClient(OAuth2AuthorizedClient authorizedClient, Authentication principal, + ServerWebExchange exchange) { + + log.debug("Saving authorized client " + authorizedClient + + " for principal " + principal + ", and exchange " + exchange); + + ServerHttpResponse response = exchange.getResponse(); + + Assert.notNull(exchange, "exchange cannot be null"); + if (authorizedClient == null) { + + deleteCookies(exchange, LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME, LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); + return Mono.empty(); + } + + ResponseCookie cookie = ResponseCookie + .from(LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME, LecUtils.serialize(authorizedClient)) + .path("/") + .httpOnly(true) + .maxAge(cookieExpirySecs) + .build(); + + response.addCookie(cookie); + + String lemonRedirectUri = exchange.getRequest() + .getQueryParams().getFirst(LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); + + if (StringUtils.isNotBlank(lemonRedirectUri)) { + + cookie = ResponseCookie + .from(LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME, lemonRedirectUri) + .path("/") + .httpOnly(true) + .maxAge(cookieExpirySecs) + .build(); + + response.addCookie(cookie); + } + + return Mono.empty(); + } + + @Override + public Mono removeAuthorizedClient(String clientRegistrationId, Authentication principal, + ServerWebExchange exchange) { + + log.debug("Deleting authorized client for clientRegistrationId " + clientRegistrationId + + ", principal " + principal + ", and exchange " + exchange); + + deleteCookies(exchange, LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME); + return Mono.empty(); + } + + public static void deleteCookies(ServerWebExchange exchange, String ...cookiesToDelete) { + + MultiValueMap cookies = exchange.getRequest().getCookies(); + MultiValueMap responseCookies = exchange.getResponse().getCookies(); + + for (int i = 0; i < cookiesToDelete.length; i++) + if (cookies.getFirst(cookiesToDelete[i]) != null) { + + ResponseCookie cookie = ResponseCookie.from(cookiesToDelete[i], "") + .path("/") + .maxAge(0L) + .build(); + + responseCookies.put(cookiesToDelete[i], Collections.singletonList(cookie)); + } + } + + private Mono deserialize(HttpCookie cookie) { + return Mono.just(LecUtils.deserialize(cookie.getValue())); + } + +} diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/ReactiveOAuth2AuthenticationSuccessHandler.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/ReactiveOAuth2AuthenticationSuccessHandler.java new file mode 100644 index 00000000..4609a312 --- /dev/null +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/security/ReactiveOAuth2AuthenticationSuccessHandler.java @@ -0,0 +1,156 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.naturalprogrammer.spring.lemonreactive.security; + +import com.naturalprogrammer.spring.lemon.commons.LemonProperties; +import com.naturalprogrammer.spring.lemon.commons.security.BlueTokenService; +import com.naturalprogrammer.spring.lemon.commons.security.LemonPrincipal; +import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; +import com.naturalprogrammer.spring.lemon.commonsreactive.util.LecrUtils; +import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; +import com.naturalprogrammer.spring.lemonreactive.LemonReactiveService; +import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; +import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUserRepository; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpCookie; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.server.DefaultServerRedirectStrategy; +import org.springframework.security.web.server.ServerRedirectStrategy; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.io.Serializable; +import java.net.URI; +import java.util.Map; + +/** + * Authentication success handler for redirecting the + * OAuth2 signed in user to a URL with a short lived auth token + * + * @author Sanjay Patel + */ +@AllArgsConstructor +public class ReactiveOAuth2AuthenticationSuccessHandler, ID extends Serializable> + implements ServerAuthenticationSuccessHandler { + + private static final Log log = LogFactory.getLog(ReactiveOAuth2AuthenticationSuccessHandler.class); + private static final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy(); + + private BlueTokenService blueTokenService; + private AbstractMongoUserRepository userRepository; + private LemonReactiveUserDetailsService userDetailsService; + private LemonReactiveService lemonService; + private PasswordEncoder passwordEncoder; + private LemonProperties properties; + + @Override + public Mono onAuthenticationSuccess(WebFilterExchange webFilterExchange, + Authentication authentication) { + + ServerWebExchange exchange = webFilterExchange.getExchange(); + + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .cast(OAuth2AuthenticationToken.class) + .flatMap(token -> buildPrincipal(token.getPrincipal(), token.getAuthorizedClientRegistrationId())) + .map(LemonPrincipal::currentUser) + .map(this::getAuthToken) + .map(authToken -> getTargetUrl(exchange, authToken)) + .map(URI::create) + .flatMap(location -> redirectStrategy.sendRedirect(exchange, location)); + } + + /** + * Builds the security principal from the given userReqest. + * Registers the user if not already registered + */ + public Mono buildPrincipal(OAuth2User oath2User, String registrationId) { + + Map attributes = oath2User.getAttributes(); + String email = lemonService.getOAuth2Email(registrationId, attributes); + LexUtils.validate(email != null, "com.naturalprogrammer.spring.oauth2EmailNeeded", registrationId).go(); + + boolean emailVerified = lemonService.getOAuth2AccountVerified(registrationId, attributes); + LexUtils.validate(emailVerified, "com.naturalprogrammer.spring.oauth2EmailNotVerified", registrationId).go(); + + return userDetailsService.findUserByUsername(email) + .switchIfEmpty(newUser(email, registrationId, attributes)) + .map(U::toUserDto) + .map(userDto -> { + + LemonPrincipal principal = new LemonPrincipal(userDto); + principal.setAttributes(attributes); + principal.setName(oath2User.getName()); + + return principal; + }); + } + + private Mono newUser(String email, String registrationId, Map attributes) { + + // register a new user + U newUser = lemonService.newUser(); + newUser.setEmail(email); + newUser.setPassword(passwordEncoder.encode(LecUtils.uid())); + + lemonService.fillAdditionalFields(registrationId, newUser, attributes); + return userRepository.insert(newUser).doOnSuccess(user -> { + try { + + lemonService.mailForgotPasswordLink(newUser); + + } catch (Exception e) { + + // In case of exception, just log the error and keep silent + log.error(ExceptionUtils.getStackTrace(e)); + } + }); + } + + private String getAuthToken(UserDto user) { + + return blueTokenService.createToken( + BlueTokenService.AUTH_AUDIENCE, + user.getUsername(), + (long) properties.getJwt().getShortLivedMillis()); + } + + private String getTargetUrl(ServerWebExchange exchange, String shortLivedAuthToken) { + + String targetUrl = LecrUtils.fetchCookie(exchange, + LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME) + .map(HttpCookie::getValue) + .orElse(properties.getOauth2AuthenticationSuccessUrl()); + + ReactiveCookieServerOAuth2AuthorizedClientRepository.deleteCookies(exchange, + LecUtils.AUTHORIZATION_REQUEST_COOKIE_NAME, + LecUtils.LEMON_REDIRECT_URI_COOKIE_PARAM_NAME); + + return targetUrl + shortLivedAuthToken; + } +} diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/util/LerUtils.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/util/LerUtils.java index 37c38a0c..7ce951e6 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/util/LerUtils.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/util/LerUtils.java @@ -1,27 +1,29 @@ -package com.naturalprogrammer.spring.lemonreactive.util; - -import java.io.IOException; -import java.io.Serializable; -import java.util.Optional; - -import javax.annotation.PostConstruct; +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.security.core.context.ReactiveSecurityContextHolder; -import org.springframework.security.core.context.SecurityContext; +package com.naturalprogrammer.spring.lemonreactive.util; -import com.github.fge.jsonpatch.JsonPatchException; -import com.naturalprogrammer.spring.lemon.commons.security.JwtService; -import com.naturalprogrammer.spring.lemon.commons.security.UserDto; +import com.naturalprogrammer.spring.lemon.commons.security.LemonTokenService; import com.naturalprogrammer.spring.lemon.commons.util.LecUtils; -import com.naturalprogrammer.spring.lemon.exceptions.VersionException; -import com.naturalprogrammer.spring.lemon.exceptions.util.LexUtils; -import com.naturalprogrammer.spring.lemonreactive.domain.AbstractDocument; import com.naturalprogrammer.spring.lemonreactive.domain.AbstractMongoUser; import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; -import reactor.core.publisher.Mono; +import java.io.Serializable; /** * Useful helper methods @@ -32,26 +34,6 @@ public class LerUtils { private static final Log log = LogFactory.getLog(LerUtils.class); - private static Mono NOT_FOUND_MONO; - - @PostConstruct - public void postConstruct() { - NOT_FOUND_MONO = Mono.error(LexUtils.NOT_FOUND_EXCEPTION); - } - - /** - * Gets the current-user - */ - public static Mono>> currentUser() { - - return ReactiveSecurityContextHolder.getContext() - .map(SecurityContext::getAuthentication) - .map(LecUtils::currentUser) - .map(user -> Optional.of((UserDto) user)) - .defaultIfEmpty(Optional.empty()); - } - - /** * Throws BadCredentialsException if * user's credentials were updated after the JWT was issued @@ -59,38 +41,12 @@ public static Mono>> currentUser( public static , ID extends Serializable> void ensureCredentialsUpToDate(JWTClaimsSet claims, U user) { - long issueTime = (long) claims.getClaim(JwtService.LEMON_IAT); + long issueTime = (long) claims.getClaim(LemonTokenService.LEMON_IAT); + log.debug("Ensuring credentials up to date. Issue time = " + + issueTime + ". User's credentials updated at" + user.getCredentialsUpdatedMillis()); + LecUtils.ensureCredentials(issueTime >= user.getCredentialsUpdatedMillis(), "com.naturalprogrammer.spring.obsoleteToken"); } - - - public static Mono notFoundMono() { - return (Mono) NOT_FOUND_MONO; - } - - - /** - * Throws a VersionException if the versions of the - * given entities aren't same. - * - * @param original - * @param updated - */ - public static - void ensureCorrectVersion(AbstractDocument original, AbstractDocument updated) { - - if (original.getVersion() != updated.getVersion()) - throw new VersionException(original.getClass().getSimpleName(), original.getId().toString()); - } - - public static T applyPatch(T originalObj, String patchString) { - - try { - return LecUtils.applyPatch(originalObj, patchString); - } catch (IOException | JsonPatchException e) { - throw new RuntimeException(e); - } - } } diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/validation/UniqueEmail.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/validation/UniqueEmail.java index c29e8631..b67bf6c1 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/validation/UniqueEmail.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/validation/UniqueEmail.java @@ -1,14 +1,29 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemonreactive.validation; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; import javax.validation.Constraint; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; - -import com.naturalprogrammer.spring.lemon.commons.util.UserUtils; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; /** * Annotation for unique-email constraint, diff --git a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/validation/UniqueEmailValidator.java b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/validation/UniqueEmailValidator.java index ef6c2e3e..39ad5def 100644 --- a/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/validation/UniqueEmailValidator.java +++ b/spring-lemon-reactive/src/main/java/com/naturalprogrammer/spring/lemonreactive/validation/UniqueEmailValidator.java @@ -1,16 +1,35 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this artifact or file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.naturalprogrammer.spring.lemonreactive.validation; -import static org.springframework.data.mongodb.core.query.Criteria.where; -import static org.springframework.data.mongodb.core.query.Query.query; +import com.naturalprogrammer.spring.lemonreactive.LemonReactiveService; +import lombok.SneakyThrows; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.data.mongodb.core.MongoTemplate; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; -import com.naturalprogrammer.spring.lemonreactive.LemonReactiveService; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; /** * Validator for unique-email @@ -22,10 +41,10 @@ public class UniqueEmailValidator private static final Log log = LogFactory.getLog(UniqueEmailValidator.class); - private MongoTemplate mongoTemplate; + private ReactiveMongoTemplate mongoTemplate; private Class userClass; - public UniqueEmailValidator(MongoTemplate mongoTemplate, + public UniqueEmailValidator(ReactiveMongoTemplate mongoTemplate, LemonReactiveService lemonReactiveService) { this.mongoTemplate = mongoTemplate; @@ -33,10 +52,22 @@ public UniqueEmailValidator(MongoTemplate mongoTemplate, log.info("Created"); } + @SneakyThrows @Override public boolean isValid(String email, ConstraintValidatorContext context) { - + log.debug("Validating whether email is unique: " + email); - return !mongoTemplate.exists(query(where("email").is(email)), userClass); + + final AtomicBoolean unique = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(1); + + mongoTemplate.exists(query(where("email").is(email)), userClass) + .subscribe(exists -> { + unique.set(!exists); + latch.countDown(); + }); + + latch.await(); + return unique.get(); } }