Skip to content

Commit

Permalink
[MERGE] feat/#20 -> dev
Browse files Browse the repository at this point in the history
[FEAT/#20] Public Key 조회 API 구현
  • Loading branch information
yummygyudon authored Dec 7, 2024
2 parents bd21bd9 + 802b883 commit 8d671ad
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 100 deletions.
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,5 +3,6 @@
public class JwtConstant {

public static final String TOKEN_HEADER = "Bearer ";
public static final String[] SERVICE_NAMES = {"playground", "crew", "app", "admin"};
public static final String REFRESH_TOKEN_HEADER = "refresh-token";
}
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

0 comments on commit 8d671ad

Please sign in to comment.