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

fix: P4ADEV-391 Generate access token #17

Merged
merged 2 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ val nimbusJoseJwtVersion = "9.38-rc5"
val jjwtVersion = "0.12.5"
val wiremockVersion = "3.5.4"
val findbugsVersion = "3.0.2"
val bouncycastleVersion = "1.78.1"

dependencies {
implementation("org.springframework.boot:spring-boot-starter")
Expand All @@ -56,6 +57,7 @@ dependencies {
implementation("com.auth0:jwks-rsa:$jwksRsaVersion")
implementation("com.nimbusds:nimbus-jose-jwt:$nimbusJoseJwtVersion")
implementation("io.jsonwebtoken:jjwt:$jjwtVersion")
implementation("org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion")

compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
Expand Down
1 change: 1 addition & 0 deletions gradle.lockfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ org.apache.logging.log4j:log4j-to-slf4j:2.21.1=compileClasspath
org.apache.tomcat.embed:tomcat-embed-core:10.1.20=compileClasspath
org.apache.tomcat.embed:tomcat-embed-el:10.1.20=compileClasspath
org.apache.tomcat.embed:tomcat-embed-websocket:10.1.20=compileClasspath
org.bouncycastle:bcprov-jdk18on:1.78.1=compileClasspath
org.codehaus.janino:commons-compiler:3.1.12=compileClasspath
org.codehaus.janino:janino:3.1.12=compileClasspath
org.openapitools:jackson-databind-nullable:0.2.6=compileClasspath
Expand Down
3 changes: 3 additions & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ microservice-chart:
envSecret:
APPLICATIONINSIGHTS_CONNECTION_STRING: appinsights-connection-string

JWT_TOKEN_PRIVATE_KEY: jwt-private-key
JWT_TOKEN_PUBLIC_KEY: jwt-public-key

# nodeSelector: {}

# tolerations: []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,56 @@
package it.gov.pagopa.payhub.auth.service.exchange;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import it.gov.pagopa.payhub.auth.utils.CertUtils;
import it.gov.pagopa.payhub.model.generated.AccessToken;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.time.Instant;
import java.util.UUID;

@Service
public class AccessTokenBuilderService {

public AccessToken build(Map<String, String> subjectTokenClaims){
return null; //TODO
private final String allowedAudience;
private final int expireIn;

private final RSAPublicKey rsaPublicKey;
private final RSAPrivateKey rsaPrivateKey;

public AccessTokenBuilderService(
@Value("${jwt.audience}") String allowedAudience,
@Value("${jwt.expire-in") int expireIn,
@Value("${jwt.access-token.private-key}") String privateKey,
@Value("${jwt.access-token.public-key}") String publicKey
) {
this.allowedAudience = allowedAudience;
this.expireIn = expireIn;

try {
rsaPrivateKey = CertUtils.pemKey2PrivateKey(privateKey);
rsaPublicKey = CertUtils.pemPub2PublicKey(publicKey);
} catch (InvalidKeySpecException | NoSuchAlgorithmException | IOException e) {
throw new IllegalStateException("Cannot load private and/or public key", e);
}
}

public AccessToken build(){
Algorithm algorithm = Algorithm.RSA512(rsaPublicKey, rsaPrivateKey);
String tokenType = "bearer";
String token = JWT.create()
.withClaim("typ", tokenType)
.withIssuer(allowedAudience)
.withJWTId(UUID.randomUUID().toString())
.withIssuedAt(Instant.now())
.withExpiresAt(Instant.now().plusSeconds(expireIn))
.sign(algorithm);
return new AccessToken(token, tokenType, expireIn);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ public AccessToken postToken(String clientId, String grantType, String subjectTo
log.info("Client {} requested to exchange a {} token provided by {} asking for grant type {} and scope {}",
clientId, subjectTokenType, subjectIssuer, grantType, scope);
Map<String, String> claims = validateExternalTokenService.validate(clientId, grantType, subjectToken, subjectIssuer, subjectTokenType, scope);
return accessTokenBuilderService.build(claims);
return accessTokenBuilderService.build();
}
}
53 changes: 53 additions & 0 deletions src/main/java/it/gov/pagopa/payhub/auth/utils/CertUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package it.gov.pagopa.payhub.auth.utils;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
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 java.util.Base64;

public class CertUtils {

static {
Security.addProvider(new BouncyCastleProvider());
}
private CertUtils(){}

public static RSAPrivateKey pemKey2PrivateKey(String privateKey) throws InvalidKeySpecException, NoSuchAlgorithmException, IOException {
String keyStringFormat = extractInlinePemBody(privateKey);
try(
InputStream is = new ByteArrayInputStream(Base64.getDecoder().decode(keyStringFormat))
) {
PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(is.readAllBytes());
KeyFactory kf = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) kf.generatePrivate(encodedKeySpec);
}
}

public static RSAPublicKey pemPub2PublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
String pubStringFormat = extractInlinePemBody(publicKey);
try(
InputStream is = new ByteArrayInputStream(Base64.getDecoder().decode(pubStringFormat))
) {
X509EncodedKeySpec encodedKeySpec = new X509EncodedKeySpec(is.readAllBytes());
KeyFactory kf = KeyFactory.getInstance("RSA");
return (RSAPublicKey) kf.generatePublic(encodedKeySpec);
}
}

public static String extractInlinePemBody(String target) {
return target
.replaceAll("^-----BEGIN[A-Z|\\s]+-----", "")
.replaceAll("\\s+", "")
.replaceAll("-----END[A-Z|\\s]+-----$", "");
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ spring:

jwt:
audience: "\${JWT_TOKEN_AUDIENCE:application-audience}"
access-token:
expire-in: "\${JWT_TOKEN_EXPIRATION_SECONDS:3600}"
private-key: "\${JWT_TOKEN_PRIVATE_KEY:}"
public-key: "\${JWT_TOKEN_PUBLIC_KEY:}"
external-token:
base-url: "\${JWT_EXTERNAL_TOKEN_BASE_URL:https://auth.server.com}"
issuer: "\${JWT_EXTERNAL_TOKEN_ISS:externalauthentication-server-issuer}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package it.gov.pagopa.payhub.auth.service.exchange;

import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import it.gov.pagopa.payhub.model.generated.AccessToken;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Base64;
import java.util.regex.Pattern;

public class AccessTokenBuilderServiceTest {

public static final int EXPIRE_IN = 3600;

private final static String PRIVATE_KEY= """
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA2ovm/rd3g69dq9PisinQ6mWy8ZttT8D+GKXCsHZycsGnN7b7
4TPyYy+4+h+9cgJeizp8RDRrufHjiBrqi/2reOk/rD7ZHbpfQvHK8MYfgIVdtTxY
MX/GGdOrX6/5TV2b8e2aCG6GmxF0UuEvxY9oTmcZUxnIeDtl/ixz4DQ754eS363q
WfEA92opW+jcYzr07sbQtR86e+Z/s/CUeX6W1PHNvBqdlAgp2ecr/1DOLq1D9hEA
NBPSwbt+FM6FNe4vLphi7GTwiB0yaAuy+jE8odND6HPvvvmgbK1/2qTHn/HJjWUm
11LUC73BszR32BKbdEEhxPQnnwswVekWzPi1IwIDAQABAoIBAFH4aWKeY8hTjTm2
lm+muYJBNNXkKyLfyy5pddWEB7c9JUADdQPp3P8Q1juSjhbmBpoIDLX0R3eN336c
Qd7R/W+zZLtxMzQwRCyyziBy3zvwSc6BXL7sItxrBPs14LcA5k3ehYimE/yzlkLD
zYw3FrNZfikqIYPfG4kzGR892D4lbSMTTzXgBtPEyM3TmBTDwbJ5hk6xMx9AwGec
mwz8izWNFhgc3LxrI4KnMZz6dikhScCThTHL58ZdgSdMHPjTK+Xhh7pnwFF5D3bt
H5jVsuqDX107mo/TeKyovt2H6xQMleVfjWUW6JJUxEoRtPSYBg0ogtbK337k8AoG
BqldO6ECgYEA6oXwrG+lAEe/wLwgLqT2kpjPiK8JHw0MzfzljHI0H9Sh7/ga5yrK
WZyMEm2JlhyGSXdMzm4CCeNsP4fJVVo1uZiYGZkl0wOlPSsd6DkXfLfC4qiyU2qm
Vk0BgiQRKwoWrFv2mPl9/AJWRSbrCk4223K2yWzieYgwyfY4nJ5h8BECgYEA7o9p
QYbzNhrDMyObawNz4UwR3zhMzmmnopGHn8XO9OmARDDM1CGci37TJUYK9tajjFIz
H0YwLcL/6/cu8kxs35hXBdmZgYv+jmT8zekMNp37kqjm4u+Ac8UnleCUqufdEpIY
Uvnak08C9XZtPkYvVJlQ8KyotXRsXvLYo5i5hfMCgYA/NPAjmUdwJuZATLOjvqQR
6ItufDZKHxtHXRSE4La5qXYnlceya+7zbeS2hr0hLvjmTffuXum/voKLMM6LaW+3
YLAFnif6ki3zqW47C0AQRfqJWgwNvV2tPr3cVFooLmTj+TkiC4Pv6rVTl+Sa92+D
f4xSBz2WoaT8mZayZ2Ff8QKBgDHtacYBDF3CdB/7z8cxzcrVNNhW3BxHGIJ5mrzh
lVLEm8epvvSWpEC9pksiwaCvg0MW4QQmmGa7bPxhmz2yqQaSx4O96tamCfybPh2K
LLgxkDk9iDTukx+nn4VKn1K1fBsq4FRdXlV+L8xXoL1ryvQVsk7sk9KGLzgf8x8q
E4npAoGABrR0F9hWlqGaENoXujNEN7LTzsVWXRD490dn0oawVYPzpuX0l2vCjNp/
veDYA00ElNOLz9WdkniU7BuTf9+9oWZCDXbJvK7HvidxVv0owbx02CxDL+UV98kr
XtIA8ZH/XPiyad0JjP4wDTgygKzmYdXgmVjO9/NcOA2jpvyugdw=
-----END RSA PRIVATE KEY-----
""";

private final static String PUBLIC_KEY= """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2ovm/rd3g69dq9PisinQ
6mWy8ZttT8D+GKXCsHZycsGnN7b74TPyYy+4+h+9cgJeizp8RDRrufHjiBrqi/2r
eOk/rD7ZHbpfQvHK8MYfgIVdtTxYMX/GGdOrX6/5TV2b8e2aCG6GmxF0UuEvxY9o
TmcZUxnIeDtl/ixz4DQ754eS363qWfEA92opW+jcYzr07sbQtR86e+Z/s/CUeX6W
1PHNvBqdlAgp2ecr/1DOLq1D9hEANBPSwbt+FM6FNe4vLphi7GTwiB0yaAuy+jE8
odND6HPvvvmgbK1/2qTHn/HJjWUm11LUC73BszR32BKbdEEhxPQnnwswVekWzPi1
IwIDAQAB
-----END PUBLIC KEY-----
""";

private AccessTokenBuilderService accessTokenBuilderService;

@BeforeEach
void init(){
accessTokenBuilderService = new AccessTokenBuilderService("APPLICATION_AUDIENCE", EXPIRE_IN, PRIVATE_KEY, PUBLIC_KEY);
}

@Test
void test(){
// When
AccessToken result = accessTokenBuilderService.build();

// Then
Assertions.assertEquals("bearer", result.getTokenType());
Assertions.assertEquals(EXPIRE_IN, result.getExpiresIn());

DecodedJWT decodedAccessToken = JWT.decode(result.getAccessToken());
String decodedHeader = new String(Base64.getDecoder().decode(decodedAccessToken.getHeader()));
String decodedPayload = new String(Base64.getDecoder().decode(decodedAccessToken.getPayload()));

Assertions.assertEquals("{\"alg\":\"RS512\",\"typ\":\"JWT\"}", decodedHeader);
Assertions.assertEquals(EXPIRE_IN, (decodedAccessToken.getExpiresAtAsInstant().toEpochMilli() - decodedAccessToken.getIssuedAtAsInstant().toEpochMilli()) / 1_000);
Assertions.assertTrue(Pattern.compile("\\{\"typ\":\"bearer\",\"iss\":\"APPLICATION_AUDIENCE\",\"jti\":\"[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}\",\"iat\":[0-9]+,\"exp\":[0-9]+}").matcher(decodedPayload).matches(), "Payload not matches requested pattern: " + decodedPayload);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,6 @@ void givenValidTokenWhenPostTokenThenSuccess(){
service.postToken(clientId, grantType, subjectToken, subjectIssuer, subjectTokenType, scope);

// Then
Mockito.verify(accessTokenBuilderServiceMock).build(Mockito.same(claims));
Mockito.verify(accessTokenBuilderServiceMock).build();
}
}