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
@@ -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.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.exception.support.TokenException;
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.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

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

@Configuration
@EnableConfigurationProperties(JwtProperty.class)
hyunw9 marked this conversation as resolved.
Show resolved Hide resolved
@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 TokenException(INVALID_LOCATION);
hyunw9 marked this conversation as resolved.
Show resolved Hide resolved
} catch (NoSuchAlgorithmException e) {
throw new TokenException(INVALID_ALGORITHM);
} catch (InvalidKeySpecException e) {
throw new TokenException(INVALID_SUBJECT);
}
}

@Override
public RSAPrivateKey getPrivateKey() {
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 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 parsePublicKey(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 {
hyunw9 marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
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[] serviceNames = {"playground", "crew", "app", "admin"};
hyunw9 marked this conversation as resolved.
Show resolved Hide resolved
}
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
@@ -1,9 +1,11 @@
package sopt.makers.authentication.support.security.filter;

import sopt.makers.authentication.support.constant.JwtConstant;
import sopt.makers.authentication.support.jwt.provider.JwtAuthAccessTokenProvider;
import sopt.makers.authentication.support.security.authentication.CustomAuthentication;

import java.io.IOException;
import java.util.Arrays;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -28,6 +30,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
protected void doFilterInternal(
final HttpServletRequest request, final HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

String authorizationToken = getAuthorizationToken(request);
CustomAuthentication authentication = authTokenProvider.parse(authorizationToken);

Expand All @@ -37,10 +40,23 @@ protected void doFilterInternal(
filterChain.doFilter(request, response);
}

@Override
public boolean shouldNotFilter(HttpServletRequest request) {
return isJwksRequest(request);
}

private String getAuthorizationToken(final HttpServletRequest request) {
String authorizationHeaderValue =
request.getHeader(HttpHeaders.AUTHORIZATION).substring(HttpHeaders.AUTHORIZATION.length());
String authorizationToken = authorizationHeaderValue.trim();
return authorizationToken;
return authorizationHeaderValue.trim();
hyunw9 marked this conversation as resolved.
Show resolved Hide resolved
}

private static boolean isJwksRequest(HttpServletRequest request) {
boolean isCorrectUrl = request.getRequestURI().equals("/.well-known/jwks.json");
boolean isCorrectHeader =
Arrays.stream(JwtConstant.serviceNames)
.anyMatch(request.getHeader(HttpHeaders.SERVER)::contains);

return isCorrectUrl && isCorrectHeader;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package sopt.makers.authentication.usecase.auth.port.in;

import com.nimbusds.jose.jwk.JWKSet;

public interface JwksRetrieveUsecase {

JWKSet retrievePublicKey();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package sopt.makers.authentication.usecase.auth.service;

import sopt.makers.authentication.support.jwt.RSAKeyManager;
import sopt.makers.authentication.usecase.auth.port.in.JwksRetrieveUsecase;

import org.springframework.stereotype.Service;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class JwksRetrieveService implements JwksRetrieveUsecase {

private final RSAKeyManager rsaKeyManager;

public JWKSet retrievePublicKey() {

RSAKey jwk =
new RSAKey.Builder(rsaKeyManager.getPublicKey())
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS512)
.keyID("makers-auth-1")
hyunw9 marked this conversation as resolved.
Show resolved Hide resolved
.build();
return new JWKSet(jwk);
}
}
Loading