Skip to content

Commit

Permalink
fixes #2280 rollback the jwt issuer and verifier with local jks files (
Browse files Browse the repository at this point in the history
…#2281)

* fixes #2280 rollback the jwt issuer and verifier with local jks files

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
stevehu and pre-commit-ci[bot] committed Jul 8, 2024
1 parent dc5e0ea commit 24e679b
Show file tree
Hide file tree
Showing 12 changed files with 562 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
package com.networknt.security;

import com.networknt.config.Config;
import com.networknt.config.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

/**
Expand All @@ -29,6 +31,7 @@
public class JwtConfig {
private static final Logger logger = LoggerFactory.getLogger(JwtConfig.class);
public static final String CONFIG_NAME = "jwt";
public static final String KEY = "key";
public static final String ISSUER = "issuer";
public static final String AUDIENCE = "audience";
public static final String VERSION = "version";
Expand All @@ -42,12 +45,14 @@ public class JwtConfig {
String audience;
String version;
int expiredInMinutes;
Key key;
String providerId;

private JwtConfig(String configName) {
config = Config.getInstance();
mappedConfig = config.getJsonMapConfigNoCache(configName);
setConfigData();
setConfigMap();
}
public static JwtConfig load() {
return new JwtConfig(CONFIG_NAME);
Expand Down Expand Up @@ -97,6 +102,14 @@ public void setExpiredInMinutes(int expiredInMinutes) {
this.expiredInMinutes = expiredInMinutes;
}

public Key getKey() {
return key;
}

public void setKey(Key key) {
this.key = key;
}

public String getProviderId() { return providerId; }

public void setProviderId(String providerId) { this.providerId = providerId; }
Expand All @@ -123,4 +136,64 @@ private void setConfigData() {
}
}

private void setConfigMap() {
if(getMappedConfig() != null) {
Object object = getMappedConfig().get(KEY);
if(object != null) {
if(object instanceof Map) {
key = Config.getInstance().getMapper().convertValue(object, Key.class);
} else if(object instanceof String) {
try {
key = Config.getInstance().getMapper().readValue((String)object, Key.class);
} catch (Exception e) {
logger.error("Exception:", e);
}
} else {
logger.error("key in jwt.yml is not a map or string");
}
}
}
}

public static class Key {
String kid;
String filename;
String password;
String keyName;

public Key() {
}

public String getKid() {
return kid;
}

public void setKid(String kid) {
this.kid = kid;
}

public String getFilename() {
return filename;
}

public void setFilename(String filename) {
this.filename = filename;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getKeyName() {
return keyName;
}

public void setKeyName(String keyName) {
this.keyName = keyName;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@
import com.networknt.client.oauth.OauthHelper;
import com.networknt.client.oauth.SignKeyRequest;
import com.networknt.client.oauth.TokenKeyRequest;
import com.networknt.config.Config;
import com.networknt.config.ConfigException;
import com.networknt.config.JsonMapper;
import com.networknt.exception.ClientException;
import com.networknt.exception.ExpiredTokenException;
import com.networknt.status.Status;
import com.networknt.utility.FingerPrintUtil;
import org.jose4j.jwk.JsonWebKey;
import org.jose4j.jwk.JsonWebKeySet;
import org.jose4j.jwt.JwtClaims;
Expand All @@ -37,16 +39,22 @@
import org.jose4j.keys.resolvers.VerificationKeyResolver;
import org.jose4j.keys.resolvers.X509VerificationKeyResolver;
import org.jose4j.lang.JoseException;
import org.owasp.encoder.Encode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import java.util.stream.Stream;

/**
* This is a new class that is designed as non-static to replace the JwtHelper which is a static class. The reason
Expand Down Expand Up @@ -84,6 +92,7 @@ public class JwtVerifier extends TokenVerifier {
static Map<String, X509Certificate> certMap;
static String audience; // this is the audience from the client.yml with single oauth provider.
static Map<String, String> audienceMap; // this is the audience map from the client.yml with multiple oauth providers.
static List<String> fingerPrints;

public JwtVerifier(SecurityConfig cfg) {
config = cfg;
Expand All @@ -94,6 +103,9 @@ public JwtVerifier(SecurityConfig cfg) {
// init getting JWK during the initialization. The other part is in the resolver for OAuth 2.0 provider to
// rotate keys when the first token is received with the new kid.
String keyResolver = config.getKeyResolver();

this.cacheCertificates();

// if KeyResolver is jwk and bootstrap from jwk is true, load jwk during server startup.
if(logger.isTraceEnabled())
logger.trace("keyResolver = {} bootstrapFromKeyService = {}", keyResolver, bootstrapFromKeyService);
Expand All @@ -102,6 +114,63 @@ public JwtVerifier(SecurityConfig cfg) {
}
}


/**
* Caches cert.
*/
private void cacheCertificates() {
// cache the certificates
certMap = new HashMap<>();
fingerPrints = new ArrayList<>();
if (config.getCertificate() != null) {
Map<String, Object> keyMap = config.getCertificate();
for (String kid : keyMap.keySet()) {
X509Certificate cert = null;
try {
cert = readCertificate((String) keyMap.get(kid));
} catch (Exception e) {
logger.error("Exception:", e);
}
certMap.put(kid, cert);
fingerPrints.add(FingerPrintUtil.getCertFingerPrint(cert));
}
}
logger.debug("Successfully cached Certificate");
}

/**
* Read certificate from a file and convert it into X509Certificate object
*
* @param filename certificate file name
* @return X509Certificate object
* @throws Exception Exception while reading certificate
*/
public X509Certificate readCertificate(String filename)
throws Exception {
InputStream inStream = null;
X509Certificate cert = null;
try {
inStream = Config.getInstance().getInputStreamFromFile(filename);
if (inStream != null) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
cert = (X509Certificate) cf.generateCertificate(inStream);
} else {
logger.info("Certificate " + Encode.forJava(filename) + " not found.");
}
} catch (Exception e) {
logger.error("Exception: ", e);
} finally {
if (inStream != null) {
try {
inStream.close();
} catch (IOException ioe) {
logger.error("Exception: ", ioe);
}
}
}
return cert;
}

/**
* This method is to keep backward compatible for those call without VerificationKeyResolver. The single
* auth server is used in this case.
Expand Down Expand Up @@ -716,4 +785,18 @@ public X509Certificate getCertForSign(String kid) {
}
return certificate;
}

/**
* Get a list of certificate fingerprints for server info endpoint so that certification process in light-portal
* can detect if your service still use the default public key certificates provided by the light-4j framework.
* <p>
* The default public key certificates are for dev only and should be replaced on any other environment or
* set bootstrapFromKeyService: true if you are using light-oauth2 so that key can be dynamically loaded.
*
* @return List of certificate fingerprints
*/
public List<String> getFingerPrints() {
return fingerPrints;
}

}
6 changes: 6 additions & 0 deletions security-config/src/main/resources/config/jwt.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# This is the default JWT configuration and some of the properties need to be overwritten when used to issue JWT tokens.
# It is a component that used to issue JWT token. Normally, it should be used by light-oauth2 or oauth-kafka only.
# Signature private key that used to sign JWT tokens. It is here to ensure backward compatibility only.
key: ${jwt.key:{"kid":"100","filename":"primary.jks","keyName":"selfsigned","password":"password"}}
# kid: '100' # kid that used to sign the JWT tokens. It will be shown up in the token header.
# filename: "primary.jks" # private key that is used to sign JWT tokens.
# keyName: selfsigned # key name that is used to identify the right key in keystore.
# password: password # private key store password and private key password is the same
# issuer of the JWT token
issuer: ${jwt.issuer:urn:com:networknt:oauth2:v1}
# audience of the JWT token
Expand Down
51 changes: 51 additions & 0 deletions security/src/main/java/com/networknt/security/JwtIssuer.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.util.Map;

/**
* JWT token issuer helper utility that use by light-ouath2 token and code services to
Expand All @@ -36,6 +38,55 @@ public class JwtIssuer {
private static final Logger logger = LoggerFactory.getLogger(JwtIssuer.class);
private static final JwtConfig jwtConfig = JwtConfig.load();

/**
* A static method that generate JWT token from JWT claims object. This method is deprecated, and it
* is replaced by the method that takes kid and private key as parameters.
*
* @param claims JwtClaims object
* @return A string represents jwt token
* @throws JoseException JoseException
*/
@Deprecated
public static String getJwt(JwtClaims claims) throws JoseException {
String jwt;
RSAPrivateKey privateKey = (RSAPrivateKey) getPrivateKey(
jwtConfig.getKey().getFilename(),jwtConfig.getKey().getPassword(), jwtConfig.getKey().getKeyName());

// A JWT is a JWS and/or a JWE with JSON claims as the payload.
// In this example it is a JWS nested inside a JWE
// So we first create a JsonWebSignature object.
JsonWebSignature jws = new JsonWebSignature();

// The payload of the JWS is JSON content of the JWT Claims
jws.setPayload(claims.toJson());

// The JWT is signed using the sender's private key
jws.setKey(privateKey);

// Get provider from security config file, it should be two digit
// And the provider id will set as prefix for keyid in the token header, for example: 05100
// if there is no provider id, we use "00" for the default value
String provider_id = "";
if (jwtConfig.getProviderId() != null) {
provider_id = jwtConfig.getProviderId();
if (provider_id.length() == 1) {
provider_id = "0" + provider_id;
} else if (provider_id.length() > 2) {
logger.error("provider_id defined in the security.yml file is invalid; the length should be 2");
provider_id = provider_id.substring(0, 2);
}
}
jws.setKeyIdHeaderValue(provider_id + jwtConfig.getKey().getKid());

// Set the signature algorithm on the JWT/JWS that will integrity protect the claims
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);

// Sign the JWS and produce the compact serialization, which will be the inner JWT/JWS
// representation, which is a string consisting of three dot ('.') separated
// base64url-encoded parts in the form Header.Payload.Signature
jwt = jws.getCompactSerialization();
return jwt;
}

/**
* A static method that generate JWT token from JWT claims object and a given private key. This private key
Expand Down
68 changes: 68 additions & 0 deletions security/src/main/java/com/networknt/security/JwtMockHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2016 Network New Technologies Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.networknt.security;

import com.networknt.config.Config;
import com.networknt.handler.LightHttpHandler;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import org.jose4j.jwt.JwtClaims;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* This is a jwt token provider for testing only. It should be injected into the server
* after it is started. Do not use it on production runtime. If you need an external
* OAuth2 server, please take a look at https://github.com/networknt/light-oauth2
*
* @author Steve Hu
*/
public class JwtMockHandler implements LightHttpHandler {

public static final String ENABLE_MOCK_JWT = "enableMockJwt";

public JwtMockHandler() {}

@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
exchange.getResponseHeaders().put(
Headers.CONTENT_TYPE, "application/json");

Map<String, Object> resMap = new HashMap<>();
resMap.put("access_token", JwtIssuer.getJwt(mockClaims()));
resMap.put("token_type", "bearer");
resMap.put("expires_in", 600);
exchange.getResponseSender().send(ByteBuffer.wrap(
Config.getInstance().getMapper().writeValueAsBytes(
resMap)));
}

public JwtClaims mockClaims() {
JwtClaims claims = JwtIssuer.getDefaultJwtClaims();
claims.setClaim("user_id", "steve");
claims.setClaim("user_type", "EMPLOYEE");
claims.setClaim("client_id", "aaaaaaaa-1234-1234-1234-bbbbbbbb");
List<String> scope = Arrays.asList("api.r", "api.w");
claims.setStringListClaim("scope", scope); // multi-valued claims work too and will end up as a JSON array
return claims;
}
}
Loading

0 comments on commit 24e679b

Please sign in to comment.