Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT/#20] Public Key 조회 API 구현 #22

Open
wants to merge 9 commits into
base: dev
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package sopt.makers.authentication.application.auth.api;

import org.springframework.http.ResponseEntity;

public interface AuthKeyApi {

ResponseEntity<?> retrievePublicJwks();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package sopt.makers.authentication.application.auth.api;

import sopt.makers.authentication.usecase.auth.port.in.JwksRetrieveUsecase;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping("/.well-known")
@RequiredArgsConstructor
public class AuthKeyApiController implements AuthKeyApi {

private final JwksRetrieveUsecase jwksRetrieveUsecase;

@Override
@GetMapping(value = "/jwks.json")
public ResponseEntity<?> retrievePublicJwks() {
return ResponseEntity.status(HttpStatus.OK).body(jwksRetrieveUsecase.retrievePublicKey());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package sopt.makers.authentication.support.code.support.failure;

import static lombok.AccessLevel.PRIVATE;

import sopt.makers.authentication.support.code.base.FailureCode;

import org.springframework.http.HttpStatus;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor(access = PRIVATE)
public enum ResourceFailure implements FailureCode {
INVALID_LOCATION(HttpStatus.BAD_REQUEST, "키 파일 위치가 잘못되었습니다."),
INVALID_SUBJECT(HttpStatus.BAD_REQUEST, "주체 정보가 잘못되었습니다."),
INVALID_ALGORITHM(HttpStatus.BAD_REQUEST, "알고리즘이 잘못되었습니다."),
;
private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@
public enum TokenFailure implements FailureCode {
TOKEN_EXPIRED(HttpStatus.BAD_REQUEST, "토큰이 만료되었습니다."),
UNSUPPORTED_ISSUER(HttpStatus.BAD_REQUEST, "신뢰할 수 없는 발급자입니다."),
INVALID_SUBJECT(HttpStatus.BAD_REQUEST, "주체 정보가 잘못되었습니다."),
INVALID_PREFIX(HttpStatus.BAD_REQUEST, "토큰 접두사가 잘못되었습니다."),
INVALID_ALGORITHM(HttpStatus.BAD_REQUEST, "알고리즘이 잘못되었습니다."),
INVALID_SIGNATURE(HttpStatus.BAD_REQUEST, "서명이 잘못되었습니다."),
INVALID_LOCATION(HttpStatus.BAD_REQUEST, "키 파일 위치가 잘못되었습니다."),
;
;
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
package sopt.makers.authentication.support.config;

import static sopt.makers.authentication.support.code.support.failure.TokenFailure.INVALID_ALGORITHM;
import static sopt.makers.authentication.support.code.support.failure.TokenFailure.INVALID_LOCATION;
import static sopt.makers.authentication.support.code.support.failure.TokenFailure.INVALID_SUBJECT;
import sopt.makers.authentication.support.jwt.RSAKeyManager;

import sopt.makers.authentication.support.exception.support.TokenException;
import sopt.makers.authentication.support.value.JwtProperty;

import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
Expand All @@ -38,49 +20,17 @@
import com.nimbusds.jose.proc.SecurityContext;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Configuration
@EnableConfigurationProperties(JwtProperty.class)
@RequiredArgsConstructor
@Slf4j
public class JwtRSAKeyConfiguration {

private final JwtProperty jwtProperty;
private final ResourceLoader resourceLoader;

public RSAPublicKey createPublicKeyFromProperty() {
try {
Resource resource = loadPublicKeyResource();
PemObject pemObject = readPublicPemFile(resource);
return generatePublicKey(pemObject);
} catch (IOException e) {
throw new TokenException(INVALID_LOCATION);
} catch (NoSuchAlgorithmException e) {
throw new TokenException(INVALID_ALGORITHM);
} catch (InvalidKeySpecException e) {
throw new TokenException(INVALID_SUBJECT);
}
}

public RSAPrivateKey createPrivateKeyFromProperty() {
try {
Resource resource = loadPrivateKeyResource();
PemObject pemObject = readPrivatePemFile(resource);
return generatePrivateKey(pemObject);
} catch (IOException e) {
throw new TokenException(INVALID_LOCATION);
} catch (NoSuchAlgorithmException e) {
throw new TokenException(INVALID_ALGORITHM);
} catch (InvalidKeySpecException e) {
throw new TokenException(INVALID_SUBJECT);
}
}
private final RSAKeyManager keyManager;

@Bean
public JwtEncoder jwtEncoder() {
RSAPublicKey publicKey = createPublicKeyFromProperty();
RSAPrivateKey privateKey = createPrivateKeyFromProperty();
RSAPublicKey publicKey = keyManager.getPublicKey();
RSAPrivateKey privateKey = keyManager.getPrivateKey();

JWK jwk = new RSAKey.Builder(publicKey).privateKey(privateKey).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
Expand All @@ -89,44 +39,6 @@ public JwtEncoder jwtEncoder() {

@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(createPublicKeyFromProperty()).build();
}

private Resource loadPublicKeyResource() {
return resourceLoader.getResource(jwtProperty.secret().rsa().publicKey());
}

private PemObject readPublicPemFile(Resource resource) throws IOException {
try (PemReader pemReader =
new PemReader(new StringReader(resource.getContentAsString(StandardCharsets.UTF_8)))) {
return pemReader.readPemObject();
}
}

private RSAPublicKey generatePublicKey(PemObject pemObject)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] publicKeyBytes = pemObject.getContent();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
}

private Resource loadPrivateKeyResource() {
return resourceLoader.getResource(jwtProperty.secret().rsa().privateKey());
}

private PemObject readPrivatePemFile(Resource resource) throws IOException {
try (PemReader pemReader =
new PemReader(new StringReader(resource.getContentAsString(StandardCharsets.UTF_8)))) {
return pemReader.readPemObject();
}
}

private RSAPrivateKey generatePrivateKey(PemObject pemObject)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] privateKeyBytes = pemObject.getContent();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
return NimbusJwtDecoder.withPublicKey(keyManager.getPublicKey()).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package sopt.makers.authentication.support.config;

import static sopt.makers.authentication.support.code.support.failure.ResourceFailure.INVALID_ALGORITHM;
import static sopt.makers.authentication.support.code.support.failure.ResourceFailure.INVALID_LOCATION;
import static sopt.makers.authentication.support.code.support.failure.ResourceFailure.INVALID_SUBJECT;

import sopt.makers.authentication.support.exception.support.ResourceException;
import sopt.makers.authentication.support.jwt.RSAKeyManager;
import sopt.makers.authentication.support.value.JwtProperty;

import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Component
@EnableConfigurationProperties(JwtProperty.class)
@RequiredArgsConstructor
@Slf4j
public class LocalRSAKeyManager implements RSAKeyManager {

private final JwtProperty jwtProperty;
private final ResourceLoader resourceLoader;

@Override
public RSAPublicKey getPublicKey() {
try {
Resource resource = loadPublicKeyResource();
PemObject pemObject = readPublicPemFile(resource);
return parsePublicKey(pemObject);
} catch (IOException e) {
throw new ResourceException(INVALID_LOCATION);
} catch (NoSuchAlgorithmException e) {
throw new ResourceException(INVALID_ALGORITHM);
} catch (InvalidKeySpecException e) {
throw new ResourceException(INVALID_SUBJECT);
}
}

@Override
public RSAPrivateKey getPrivateKey() {
try {
Resource resource = loadPrivateKeyResource();
PemObject pemObject = readPrivatePemFile(resource);
return generatePrivateKey(pemObject);
} catch (IOException e) {
throw new ResourceException(INVALID_LOCATION);
} catch (NoSuchAlgorithmException e) {
throw new ResourceException(INVALID_ALGORITHM);
} catch (InvalidKeySpecException e) {
throw new ResourceException(INVALID_SUBJECT);
}
}

private Resource loadPublicKeyResource() {
return resourceLoader.getResource(jwtProperty.secret().rsa().publicKey());
}

private PemObject readPublicPemFile(final Resource resource) throws IOException {
try (PemReader pemReader =
new PemReader(new StringReader(resource.getContentAsString(StandardCharsets.UTF_8)))) {
return pemReader.readPemObject();
}
}

private RSAPublicKey parsePublicKey(final PemObject pemObject)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] publicKeyBytes = pemObject.getContent();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
}

private Resource loadPrivateKeyResource() {
return resourceLoader.getResource(jwtProperty.secret().rsa().privateKey());
}

private PemObject readPrivatePemFile(final Resource resource) throws IOException {
try (PemReader pemReader =
new PemReader(new StringReader(resource.getContentAsString(StandardCharsets.UTF_8)))) {
return pemReader.readPemObject();
}
}

private RSAPrivateKey generatePrivateKey(final PemObject pemObject)
throws NoSuchAlgorithmException, InvalidKeySpecException {
byte[] privateKeyBytes = pemObject.getContent();
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
public class JwtConstant {

public static final String TOKEN_HEADER = "Bearer ";
public static final String[] SERVICE_NAMES = {"playground", "crew", "app", "admin"};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sopt.makers.authentication.support.exception.support;

import sopt.makers.authentication.support.code.support.failure.ResourceFailure;
import sopt.makers.authentication.support.exception.base.BaseException;

public class ResourceException extends BaseException {

public ResourceException(final ResourceFailure failure) {
super(failure);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sopt.makers.authentication.support.jwt;

import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

public interface RSAKeyManager {

RSAPublicKey getPublicKey();

RSAPrivateKey getPrivateKey();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import static org.springframework.security.oauth2.jwt.JwtClaimNames.ISS;
import static org.springframework.security.oauth2.jwt.JwtClaimNames.SUB;
import static sopt.makers.authentication.support.code.support.failure.TokenFailure.INVALID_SUBJECT;
import static sopt.makers.authentication.support.code.support.failure.ResourceFailure.INVALID_SUBJECT;
import static sopt.makers.authentication.support.code.support.failure.TokenFailure.TOKEN_EXPIRED;
import static sopt.makers.authentication.support.code.support.failure.TokenFailure.UNSUPPORTED_ISSUER;

import sopt.makers.authentication.support.exception.support.ResourceException;
import sopt.makers.authentication.support.exception.support.TokenException;
import sopt.makers.authentication.support.security.authentication.CustomAuthentication;
import sopt.makers.authentication.support.value.JwtProperty;
Expand Down Expand Up @@ -73,7 +74,7 @@ private void validateIssuer(JwtProperty jwtProperty) {
private void validateSubject() {
String subject = jwt.getClaim(SUB);
if (subject == null) {
throw new TokenException(INVALID_SUBJECT);
throw new ResourceException(INVALID_SUBJECT);
}
}
}
Loading