diff --git a/build.gradle b/build.gradle index 0812f95..fb6a5c7 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation 'org.springframework.boot:spring-boot-starter-amqp' implementation 'org.springframework:spring-context-support' - implementation 'org.springframework.boot:spring-boot-starter-security' +// implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.vertx:vertx-pg-client:3.9.0' implementation 'log4j:log4j:1.2.17' implementation 'com.google.guava:guava:29.0-jre' diff --git a/src/main/java/in/projecteka/uos/Error.java b/src/main/java/in/projecteka/uos/Error.java new file mode 100644 index 0000000..cebdd3e --- /dev/null +++ b/src/main/java/in/projecteka/uos/Error.java @@ -0,0 +1,17 @@ +package in.projecteka.uos; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@JsonIgnoreProperties(ignoreUnknown = true) +@AllArgsConstructor +@Builder +@NoArgsConstructor +@Data +public class Error { + private ErrorCode code; + private String message; +} diff --git a/src/main/java/in/projecteka/uos/ErrorCode.java b/src/main/java/in/projecteka/uos/ErrorCode.java index 33b4ead..6ff4c71 100644 --- a/src/main/java/in/projecteka/uos/ErrorCode.java +++ b/src/main/java/in/projecteka/uos/ErrorCode.java @@ -7,7 +7,10 @@ public enum ErrorCode { UNKNOWN_ERROR(1002), - UNAUTHORIZED_REQUESTER(1005); + UNAUTHORIZED_REQUESTER(1005), + NETWORK_SERVICE_ERROR(2000), + USERNAME_OR_PASSWORD_INCORRECT(1018); + private final int value; ErrorCode(int val) { diff --git a/src/main/java/in/projecteka/uos/UOSApplication.java b/src/main/java/in/projecteka/uos/UOSApplication.java index c592553..a87590a 100644 --- a/src/main/java/in/projecteka/uos/UOSApplication.java +++ b/src/main/java/in/projecteka/uos/UOSApplication.java @@ -1,11 +1,13 @@ package in.projecteka.uos; +import in.projecteka.uos.clients.properties.IdentityServiceProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication -@EnableConfigurationProperties({UOSProperties.class}) +@EnableConfigurationProperties({UOSProperties.class, + IdentityServiceProperties.class}) public class UOSApplication { public static void main(String[] args) { diff --git a/src/main/java/in/projecteka/uos/UOSConfiguration.java b/src/main/java/in/projecteka/uos/UOSConfiguration.java new file mode 100644 index 0000000..a97e316 --- /dev/null +++ b/src/main/java/in/projecteka/uos/UOSConfiguration.java @@ -0,0 +1,18 @@ +package in.projecteka.uos; + +import in.projecteka.uos.clients.IdentityServiceClient; +import in.projecteka.uos.clients.properties.IdentityServiceProperties; +import in.projecteka.uos.user.TokenService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class UOSConfiguration { + + @Bean + public TokenService tokenService(IdentityServiceProperties identityServiceProperties, + IdentityServiceClient identityServiceClient) { + return new TokenService(identityServiceProperties, identityServiceClient); + } +} diff --git a/src/main/java/in/projecteka/uos/UOSProperties.java b/src/main/java/in/projecteka/uos/UOSProperties.java index bbf4aa9..714a4f9 100644 --- a/src/main/java/in/projecteka/uos/UOSProperties.java +++ b/src/main/java/in/projecteka/uos/UOSProperties.java @@ -13,6 +13,5 @@ @Getter @Setter @NoArgsConstructor(access = AccessLevel.PACKAGE) -@AllArgsConstructor public class UOSProperties { } diff --git a/src/main/java/in/projecteka/uos/clients/ClientError.java b/src/main/java/in/projecteka/uos/clients/ClientError.java new file mode 100644 index 0000000..ad5bbe4 --- /dev/null +++ b/src/main/java/in/projecteka/uos/clients/ClientError.java @@ -0,0 +1,36 @@ +package in.projecteka.uos.clients; + +import in.projecteka.uos.Error; +import in.projecteka.uos.ErrorRepresentation; +import lombok.Getter; +import lombok.ToString; +import org.springframework.http.HttpStatus; + +import static in.projecteka.uos.ErrorCode.NETWORK_SERVICE_ERROR; +import static in.projecteka.uos.ErrorCode.USERNAME_OR_PASSWORD_INCORRECT; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; +import static org.springframework.http.HttpStatus.UNAUTHORIZED; + +@Getter +@ToString +public class ClientError extends Throwable { + private static final String CANNOT_PROCESS_REQUEST_TRY_LATER = "Cannot process the request at the moment, please try later."; + private final HttpStatus httpStatus; + private final ErrorRepresentation error; + + public ClientError(HttpStatus httpStatus, ErrorRepresentation errorRepresentation) { + this.httpStatus = httpStatus; + error = errorRepresentation; + } + + public static ClientError unAuthorizedRequest() { + return new ClientError(UNAUTHORIZED, + new ErrorRepresentation(new Error(USERNAME_OR_PASSWORD_INCORRECT, + "Username or password is incorrect"))); + } + + public static ClientError networkServiceCallFailed() { + return new ClientError(INTERNAL_SERVER_ERROR, + new ErrorRepresentation(new Error(NETWORK_SERVICE_ERROR, CANNOT_PROCESS_REQUEST_TRY_LATER))); + } +} diff --git a/src/main/java/in/projecteka/uos/clients/IdentityServiceClient.java b/src/main/java/in/projecteka/uos/clients/IdentityServiceClient.java new file mode 100644 index 0000000..add04b8 --- /dev/null +++ b/src/main/java/in/projecteka/uos/clients/IdentityServiceClient.java @@ -0,0 +1,33 @@ +package in.projecteka.uos.clients; + +import in.projecteka.uos.clients.model.Session; +import in.projecteka.uos.clients.properties.IdentityServiceProperties; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +public class IdentityServiceClient { + private final WebClient.Builder webClientBuilder; + + public IdentityServiceClient(WebClient.Builder webClientBuilder, + IdentityServiceProperties identityServiceProperties) { + this.webClientBuilder = webClientBuilder; + this.webClientBuilder.baseUrl(identityServiceProperties.getBaseUrl()); + } + + public Mono getToken(MultiValueMap formData) { + return webClientBuilder.build() + .post() + .uri(uriBuilder -> + uriBuilder.path("/realms/Uos-manager/protocol/openid-connect/token").build()) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .accept(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromFormData(formData)) + .retrieve() + .onStatus(HttpStatus::isError, clientResponse -> Mono.error(ClientError.networkServiceCallFailed())) + .bodyToMono(Session.class); + } +} diff --git a/src/main/java/in/projecteka/uos/clients/model/ErrorCode.java b/src/main/java/in/projecteka/uos/clients/model/ErrorCode.java new file mode 100644 index 0000000..9aa113b --- /dev/null +++ b/src/main/java/in/projecteka/uos/clients/model/ErrorCode.java @@ -0,0 +1,32 @@ +package in.projecteka.uos.clients.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.Arrays; + +public enum ErrorCode { + UNKNOWN_ERROR_OCCURRED(1012), + NETWORK_SERVICE_ERROR(2000), + USERNAME_OR_PASSWORD_INCORRECT(1018); + + private final int value; + + ErrorCode(int val) { + value = val; + } + + // Adding @JsonValue annotation that tells the 'value' to be of integer type while de-serializing. + @JsonValue + public int getValue() { + return value; + } + + @JsonCreator + public static ErrorCode getNameByValue(int value) { + return Arrays.stream(ErrorCode.values()) + .filter(errorCode -> errorCode.value == value) + .findAny() + .orElse(ErrorCode.UNKNOWN_ERROR_OCCURRED); + } +} diff --git a/src/main/java/in/projecteka/uos/clients/model/Session.java b/src/main/java/in/projecteka/uos/clients/model/Session.java new file mode 100644 index 0000000..285d4a0 --- /dev/null +++ b/src/main/java/in/projecteka/uos/clients/model/Session.java @@ -0,0 +1,30 @@ +package in.projecteka.uos.clients.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class Session { + @JsonAlias({"access_token"}) + private String accessToken; + + @JsonAlias({"expires_in"}) + private int expiresIn; + + @JsonAlias({"refresh_expires_in"}) + private int refreshExpiresIn; + + @JsonAlias({"refresh_token"}) + private String refreshToken; + + @JsonAlias({"token_type"}) + private String tokenType; +} diff --git a/src/main/java/in/projecteka/uos/clients/properties/IdentityServiceProperties.java b/src/main/java/in/projecteka/uos/clients/properties/IdentityServiceProperties.java new file mode 100644 index 0000000..783ea0b --- /dev/null +++ b/src/main/java/in/projecteka/uos/clients/properties/IdentityServiceProperties.java @@ -0,0 +1,20 @@ +package in.projecteka.uos.clients.properties; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +@Builder +@ConfigurationProperties(prefix = "uos.keycloak") +@AllArgsConstructor +@ConstructorBinding +@Getter +public class IdentityServiceProperties { + private final String baseUrl; + private final String clientId; + private final String clientSecret; + private final String jwkUrl; + private final String issuer; +} diff --git a/src/main/java/in/projecteka/uos/user/SessionController.java b/src/main/java/in/projecteka/uos/user/SessionController.java new file mode 100644 index 0000000..feaf33f --- /dev/null +++ b/src/main/java/in/projecteka/uos/user/SessionController.java @@ -0,0 +1,20 @@ +package in.projecteka.uos.user; + +import in.projecteka.uos.clients.model.Session; +import in.projecteka.uos.user.model.SessionRequest; +import lombok.AllArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +@AllArgsConstructor +public class SessionController { + private final SessionService sessionService; + + @PostMapping("/sessions") + public Mono forNew(@RequestBody SessionRequest sessionRequest) { + return sessionService.forNew(sessionRequest); + } +} diff --git a/src/main/java/in/projecteka/uos/user/SessionService.java b/src/main/java/in/projecteka/uos/user/SessionService.java new file mode 100644 index 0000000..3470865 --- /dev/null +++ b/src/main/java/in/projecteka/uos/user/SessionService.java @@ -0,0 +1,24 @@ +package in.projecteka.uos.user; + +import in.projecteka.uos.clients.ClientError; +import in.projecteka.uos.clients.model.Session; +import in.projecteka.uos.user.model.SessionRequest; +import lombok.AllArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +public class SessionService { + private final TokenService tokenService; + private final Logger logger = LoggerFactory.getLogger(SessionService.class); + + public Mono forNew(SessionRequest request) { + if (StringUtils.isEmpty(request.getUsername()) || StringUtils.isEmpty(request.getPassword())) + return Mono.error(ClientError.unAuthorizedRequest()); + return tokenService.tokenForUser(request.getUsername(), request.getPassword()) + .doOnError(error -> logger.error(error.getMessage(), error)) + .onErrorResume(error -> Mono.error(ClientError.unAuthorizedRequest())); + } +} diff --git a/src/main/java/in/projecteka/uos/user/TokenService.java b/src/main/java/in/projecteka/uos/user/TokenService.java new file mode 100644 index 0000000..f39e95b --- /dev/null +++ b/src/main/java/in/projecteka/uos/user/TokenService.java @@ -0,0 +1,30 @@ +package in.projecteka.uos.user; + +import in.projecteka.uos.clients.IdentityServiceClient; +import in.projecteka.uos.clients.model.Session; +import in.projecteka.uos.clients.properties.IdentityServiceProperties; +import lombok.AllArgsConstructor; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +public class TokenService { + private final IdentityServiceProperties keyCloakProperties; + private final IdentityServiceClient identityServiceClient; + + public Mono tokenForUser(String userName, String password) { + return identityServiceClient.getToken(loginRequestWith(userName, password)); + } + + private MultiValueMap loginRequestWith(String username, String password) { + var formData = new LinkedMultiValueMap(); + formData.add("grant_type", "password"); + formData.add("scope", "openid"); + formData.add("client_id", keyCloakProperties.getClientId()); + formData.add("client_secret", keyCloakProperties.getClientSecret()); + formData.add("username", username); + formData.add("password", password); + return formData; + } +} diff --git a/src/main/java/in/projecteka/uos/user/UserConfiguration.java b/src/main/java/in/projecteka/uos/user/UserConfiguration.java new file mode 100644 index 0000000..66f86e5 --- /dev/null +++ b/src/main/java/in/projecteka/uos/user/UserConfiguration.java @@ -0,0 +1,21 @@ +package in.projecteka.uos.user; + +import in.projecteka.uos.clients.IdentityServiceClient; +import in.projecteka.uos.clients.properties.IdentityServiceProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class UserConfiguration { + @Bean + public IdentityServiceClient keycloakClient(WebClient.Builder builder, + IdentityServiceProperties identityServiceProperties) { + return new IdentityServiceClient(builder, identityServiceProperties); + } + + @Bean + public SessionService sessionService(TokenService tokenService) { + return new SessionService(tokenService); + } +} diff --git a/src/main/java/in/projecteka/uos/user/model/GrantType.java b/src/main/java/in/projecteka/uos/user/model/GrantType.java new file mode 100644 index 0000000..4887c7c --- /dev/null +++ b/src/main/java/in/projecteka/uos/user/model/GrantType.java @@ -0,0 +1,18 @@ +package in.projecteka.uos.user.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum GrantType { + PASSWORD("password"); + + private final String grantType; + + GrantType(String value) { + grantType = value; + } + + @JsonValue + public String getValue() { + return grantType; + } +} diff --git a/src/main/java/in/projecteka/uos/user/model/SessionRequest.java b/src/main/java/in/projecteka/uos/user/model/SessionRequest.java new file mode 100644 index 0000000..2563d2c --- /dev/null +++ b/src/main/java/in/projecteka/uos/user/model/SessionRequest.java @@ -0,0 +1,14 @@ +package in.projecteka.uos.user.model; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class SessionRequest { + GrantType grantType; + + String username; + + String password; +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b71b7da..395397a 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,7 +1,16 @@ server: port: 9060 + uos: id: CM_USER_ONBOARDING_SERVICE + keycloak: + baseUrl: http://localhost:9001/auth + clientId: uos-service + clientSecret: 75ab5d05-3422-43ff-ba85-11f442aa22cf + jwkUrl: http://localhost:9001/auth/realms/Uos-manager/protocol/openid-connect/certs + issuer: http://localhost:9001/auth/realms/Uos-manager + spring: codec: - max-in-memory-size: 500MB \ No newline at end of file + max-in-memory-size: 500MB + diff --git a/src/main/resources/static/api.yaml b/src/main/resources/static/api.yaml new file mode 100644 index 0000000..d250092 --- /dev/null +++ b/src/main/resources/static/api.yaml @@ -0,0 +1,114 @@ +openapi: 3.0.0 +info: + version: 0.0.1 + title: User Onboarding Service + description: > + +servers: + - url: someurl + description: Dev + +tags: + - name: account + +paths: + /sessions: + post: + tags: + - account + summary: Login for users. + description: All the users can login. User types are super admin/admin/lab assistants. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + application/xml: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: User login successfull. + content: + application/json: + schema: + $ref: '#/components/schemas/AuthResponse' + application/xml: + schema: + $ref: '#/components/schemas/AuthResponse' + '400': + description: > + **Causes:** + * Invalid Request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + application/xml: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: > + **Causes:** + * Downstream services are down + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + application/xml: + schema: + $ref: '#/components/schemas/ErrorResponse' +components: + schemas: + LoginRequest: + type: object + properties: + username: + type: string + format: text + password: + type: string + format: text + + AuthResponse: + type: object + properties: + accessToken: + type: string + example: eyJhbGciOiJSUzI1Ni.IsInR5cCIgOiAiSldUIiwia2lkIiA6ICJrVVp.2MXJQMjRyYXN1UW9wU2lWbkdZQUZIVFowYVZGVWpYNXFLMnNibTk0In0 + expiresIn: + type: integer + example: 1800 + description: In Minutes + refreshExpiresIn: + type: integer + example: 1800 + description: In Minutes + refreshToken: + type: string + example: eyJhbGciOiJSUzI1Ni.IsInR5cCIgOiAiSldUIiwia2lkIiA6ICJrVVp.2MXJQMjRyYXN1UW9wU2lWbkdZQUZIVFowYVZGVWpYNXFLMnNibTk0In0 + tokenType: + type: string + example: bearer + + ErrorResponse: + type: object + properties: + error: + $ref: '#/components/schemas/Error' + xml: + name: ErrorResponse + Error: + type: object + properties: + code: + type: integer + enum: [1000, 1001] + description: > + 1. Error code 1000 : Invalid User credentials + 2. Error code 1001 : Internal server error + message: + type: string + xml: + name: Error diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html index 6ec34e8..6134321 100755 --- a/src/main/resources/static/index.html +++ b/src/main/resources/static/index.html @@ -34,7 +34,7 @@ - + @@ -70,22 +70,22 @@