Commit 2b4e8dad authored by devteria's avatar devteria

issue JWT token

parent 13398df6
...@@ -53,6 +53,11 @@ ...@@ -53,6 +53,11 @@
<artifactId>mapstruct</artifactId> <artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version> <version>${mapstruct.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.30.1</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
......
...@@ -2,8 +2,11 @@ package com.devteria.identityservice.controller; ...@@ -2,8 +2,11 @@ package com.devteria.identityservice.controller;
import com.devteria.identityservice.dto.request.ApiResponse; import com.devteria.identityservice.dto.request.ApiResponse;
import com.devteria.identityservice.dto.request.AuthenticationRequest; import com.devteria.identityservice.dto.request.AuthenticationRequest;
import com.devteria.identityservice.dto.request.IntrospectRequest;
import com.devteria.identityservice.dto.response.AuthenticationResponse; import com.devteria.identityservice.dto.response.AuthenticationResponse;
import com.devteria.identityservice.dto.response.IntrospectResponse;
import com.devteria.identityservice.service.AuthenticationService; import com.devteria.identityservice.service.AuthenticationService;
import com.nimbusds.jose.JOSEException;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
...@@ -12,6 +15,8 @@ import org.springframework.web.bind.annotation.RequestBody; ...@@ -12,6 +15,8 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.text.ParseException;
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
...@@ -19,13 +24,20 @@ import org.springframework.web.bind.annotation.RestController; ...@@ -19,13 +24,20 @@ import org.springframework.web.bind.annotation.RestController;
public class AuthenticationController { public class AuthenticationController {
AuthenticationService authenticationService; AuthenticationService authenticationService;
@PostMapping("/log-in") @PostMapping("/token")
ApiResponse<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request){ ApiResponse<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request){
boolean result = authenticationService.authenticate(request); var result = authenticationService.authenticate(request);
return ApiResponse.<AuthenticationResponse>builder() return ApiResponse.<AuthenticationResponse>builder()
.result(AuthenticationResponse.builder() .result(result)
.authenticated(result) .build();
.build()) }
@PostMapping("/introspect")
ApiResponse<IntrospectResponse> authenticate(@RequestBody IntrospectRequest request)
throws ParseException, JOSEException {
var result = authenticationService.introspect(request);
return ApiResponse.<IntrospectResponse>builder()
.result(result)
.build(); .build();
} }
} }
package com.devteria.identityservice.dto.request;
import lombok.*;
import lombok.experimental.FieldDefaults;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class IntrospectRequest {
String token;
}
...@@ -9,5 +9,6 @@ import lombok.experimental.FieldDefaults; ...@@ -9,5 +9,6 @@ import lombok.experimental.FieldDefaults;
@Builder @Builder
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class AuthenticationResponse { public class AuthenticationResponse {
String token;
boolean authenticated; boolean authenticated;
} }
package com.devteria.identityservice.dto.response;
import lombok.*;
import lombok.experimental.FieldDefaults;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class IntrospectResponse {
boolean valid;
}
...@@ -7,6 +7,7 @@ public enum ErrorCode { ...@@ -7,6 +7,7 @@ public enum ErrorCode {
USERNAME_INVALID(1003, "Username must be at least 3 characters"), USERNAME_INVALID(1003, "Username must be at least 3 characters"),
INVALID_PASSWORD(1004, "Password must be at least 8 characters"), INVALID_PASSWORD(1004, "Password must be at least 8 characters"),
USER_NOT_EXISTED(1005, "User not existed"), USER_NOT_EXISTED(1005, "User not existed"),
UNAUTHENTICATED(1006, "Unauthenticated"),
; ;
ErrorCode(int code, String message) { ErrorCode(int code, String message) {
......
package com.devteria.identityservice.service; package com.devteria.identityservice.service;
import com.devteria.identityservice.dto.request.AuthenticationRequest; import com.devteria.identityservice.dto.request.AuthenticationRequest;
import com.devteria.identityservice.dto.request.IntrospectRequest;
import com.devteria.identityservice.dto.response.AuthenticationResponse;
import com.devteria.identityservice.dto.response.IntrospectResponse;
import com.devteria.identityservice.exception.AppException; import com.devteria.identityservice.exception.AppException;
import com.devteria.identityservice.exception.ErrorCode; import com.devteria.identityservice.exception.ErrorCode;
import com.devteria.identityservice.repository.UserRepository; import com.devteria.identityservice.repository.UserRepository;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import lombok.experimental.NonFinal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.text.ParseException;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class AuthenticationService { public class AuthenticationService {
UserRepository userRepository; UserRepository userRepository;
public boolean authenticate(AuthenticationRequest request){ @NonFinal
@Value("${jwt.signerKey}")
protected String SIGNER_KEY;
public IntrospectResponse introspect(IntrospectRequest request)
throws JOSEException, ParseException {
var token = request.getToken();
JWSVerifier verifier = new MACVerifier(SIGNER_KEY.getBytes());
SignedJWT signedJWT = SignedJWT.parse(token);
Date expiryTime = signedJWT.getJWTClaimsSet().getExpirationTime();
var verified = signedJWT.verify(verifier);
return IntrospectResponse.builder()
.valid(verified && expiryTime.after(new Date()))
.build();
}
public AuthenticationResponse authenticate(AuthenticationRequest request){
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
var user = userRepository.findByUsername(request.getUsername()) var user = userRepository.findByUsername(request.getUsername())
.orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED));
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10); boolean authenticated = passwordEncoder.matches(request.getPassword(),
return passwordEncoder.matches(request.getPassword(), user.getPassword()); user.getPassword());
if (!authenticated)
throw new AppException(ErrorCode.UNAUTHENTICATED);
var token = generateToken(request.getUsername());
return AuthenticationResponse.builder()
.token(token)
.authenticated(true)
.build();
}
private String generateToken(String username) {
JWSHeader header = new JWSHeader(JWSAlgorithm.HS512);
JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()
.subject(username)
.issuer("devteria.com")
.issueTime(new Date())
.expirationTime(new Date(
Instant.now().plus(1, ChronoUnit.HOURS).toEpochMilli()
))
.claim("userId", "Custom")
.build();
Payload payload = new Payload(jwtClaimsSet.toJSONObject());
JWSObject jwsObject = new JWSObject(header, payload);
try {
jwsObject.sign(new MACSigner(SIGNER_KEY.getBytes()));
return jwsObject.serialize();
} catch (JOSEException e) {
log.error("Cannot create token", e);
throw new RuntimeException(e);
}
} }
} }
...@@ -12,3 +12,6 @@ spring: ...@@ -12,3 +12,6 @@ spring:
hibernate: hibernate:
ddl-auto: update ddl-auto: update
show-sql: true show-sql: true
jwt:
signerKey: "1TjXchw5FloESb63Kc+DFhTARvpWL4jUGCwfGWxuG5SIf/1y/LgJxHnMqaF6A/ij"
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment