Skip to content

Commit

Permalink
Add ability to set PEM content to Client Configuration (#497)
Browse files Browse the repository at this point in the history
* Allow PEM-content of private key to be passed directly.

Avoids reading from a filesystem which is problematic in cloud-native environments.

* Allow use of single setPrivateKey method to either accept file path or pem content

* Ability to read PEM file from InputStream, Path and PrivateKey. ITs updated.

* README.md and code-samples updated

Co-authored-by: Hans Westerbeek <hwesterbeek@ripe.net>
Co-authored-by: Arvind Krishnakumar <61501885+arvindkrishnakumar-okta@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 2, 2020
1 parent 2d182b8 commit c5a80d2
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 47 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ Client client = Clients.builder()
.setClientId("{clientId}")
.setScopes(new HashSet<>(Arrays.asList("okta.users.read", "okta.apps.read")))
.setPrivateKey("/path/to/yourPrivateKey.pem")
// (or) .setPrivateKey("full PEM payload")
// (or) .setPrivateKey(Paths.get("/path/to/yourPrivateKey.pem"))
// (or) .setPrivateKey(inputStream)
// (or) .setPrivateKey(privateKey)
.build();
```
[//]: # (end: createOAuth2Client)
Expand Down Expand Up @@ -449,7 +453,14 @@ okta:
authorizationMode: "PrivateKey"
clientId: "yourClientId"
scopes: "okta.users.read okta.apps.read"
privateKey: "/path/to/yourPrivateKey.pem" # PEM format. This SDK supports RSA AND EC algorithms - RS256, RS384, RS512, ES256, ES384, ES512.
privateKey: |
-----BEGIN PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
...b3BlbnNzaC1rZXktdjEAAAAAAAAAAAAABAAABFwAAAAdzc2gtcn-myN3AmcmmPMS...
CO7Hnjlg77HRNFXPAAAAFWxrYW1pcmVkZHlAdm13YXJlLmNvbQECAwQF
-----END PRIVATE KEY-----
# or specify a path to a PEM file
# privateKey: "/path/to/yourPrivateKey.pem" # PEM format. This SDK supports RSA AND EC algorithms - RS256, RS384, RS512, ES256, ES384, ES512.
requestTimeout: 0 # seconds
rateLimit:
maxRetries: 4
Expand Down
1 change: 1 addition & 0 deletions THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and
limitations under the License.

This project includes:
Animal Sniffer Annotations under MIT license
Apache Commons Codec under The Apache Software License, Version 2.0
Apache Commons Lang under Apache License, Version 2.0
Apache HttpClient under Apache License, Version 2.0
Expand Down
42 changes: 41 additions & 1 deletion api/src/main/java/com/okta/sdk/client/ClientBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import com.okta.sdk.authc.credentials.ClientCredentials;
import com.okta.sdk.cache.CacheManager;

import java.io.InputStream;
import java.nio.file.Path;
import java.security.PrivateKey;
import java.util.Set;

/**
Expand Down Expand Up @@ -325,13 +328,50 @@ public interface ClientBuilder {
* of relying on the default location + override/fallback behavior defined
* in the {@link ClientBuilder documentation above}.
*
* @param privateKey the fully qualified string path to the private key (PEM file).
* @param privateKey either the fully qualified string path to the private key PEM file (or)
* the full PEM payload content.
* @return the ClientBuilder instance for method chaining.
*
* @since 1.6.0
*/
ClientBuilder setPrivateKey(String privateKey);

/**
* Allows specifying the private key (PEM file) path (for private key jwt authentication) directly instead
* of relying on the default location + override/fallback behavior defined
* in the {@link ClientBuilder documentation above}.
*
* @param privateKeyPath representing the path to private key PEM file.
* @return the ClientBuilder instance for method chaining.
*
* @since 3.0.0
*/
ClientBuilder setPrivateKey(Path privateKeyPath);

/**
* Allows specifying the private key (PEM file) path (for private key jwt authentication) directly instead
* of relying on the default location + override/fallback behavior defined
* in the {@link ClientBuilder documentation above}.
*
* @param privateKeyInputStream representing an InputStream with private key PEM file content.
* @return the ClientBuilder instance for method chaining.
*
* @since 3.0.0
*/
ClientBuilder setPrivateKey(InputStream privateKeyInputStream);

/**
* Allows specifying the private key (PEM file) path (for private key jwt authentication) directly instead
* of relying on the default location + override/fallback behavior defined
* in the {@link ClientBuilder documentation above}.
*
* @param privateKey the {@link java.security.PrivateKey} instance.
* @return the ClientBuilder instance for method chaining.
*
* @since 3.0.0
*/
ClientBuilder setPrivateKey(PrivateKey privateKey);

/**
* Allows specifying the client ID instead of relying on the default location + override/fallback behavior defined
* in the {@link ClientBuilder documentation above}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ private void createOAuth2Client() {
.setClientId("{clientId}")
.setScopes(new HashSet<>(Arrays.asList("okta.users.read", "okta.apps.read")))
.setPrivateKey("/path/to/yourPrivateKey.pem")
// (or) .setPrivateKey("full PEM payload");
// (or) .setPrivateKey(Paths.get("/path/to/yourPrivateKey.pem"));
// (or) .setPrivateKey(inputStream);
// (or) .setPrivateKey(privateKey);
.build();
}

Expand Down
110 changes: 98 additions & 12 deletions impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,21 @@
import com.okta.sdk.impl.oauth2.AccessTokenRetrieverService;
import com.okta.sdk.impl.oauth2.AccessTokenRetrieverServiceImpl;
import com.okta.sdk.impl.oauth2.OAuth2ClientCredentials;
import com.okta.sdk.impl.util.ConfigUtil;
import com.okta.sdk.impl.util.DefaultBaseUrlResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.security.PrivateKey;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
Expand All @@ -80,6 +79,9 @@
* <li>Programmatically</li>
* </ul>
*
* Please be aware that, in general, loading secrets (such as api-keys or PEM-content) from environment variables
* or system properties can lead to those secrets being leaked.
*
* @since 0.5.0
*/
public class DefaultClientBuilder implements ClientBuilder {
Expand Down Expand Up @@ -353,10 +355,20 @@ private void validateOAuth2ClientConfig(ClientConfiguration clientConfiguration)
Assert.notNull(clientConfiguration.getClientId(), "clientId cannot be null");
Assert.isTrue(clientConfiguration.getScopes() != null && !clientConfiguration.getScopes().isEmpty(),
"At least one scope is required");
Assert.notNull(clientConfiguration.getPrivateKey(), "privateKey cannot be null");
Path privateKeyPemFilePath = Paths.get(clientConfiguration.getPrivateKey());
boolean privateKeyPemFileExists = Files.exists(privateKeyPemFilePath, new LinkOption[]{ LinkOption.NOFOLLOW_LINKS });
Assert.isTrue(privateKeyPemFileExists, "privateKey file does not exist");
String privateKey = clientConfiguration.getPrivateKey();
Assert.hasText(privateKey, "privateKey cannot be null (either PEM file path (or) full PEM content must be supplied)");

if (!ConfigUtil.hasPrivateKeyContentWrapper(privateKey)) {
// privateKey is a file path, check if the file exists
Path privateKeyPemFilePath;
try {
privateKeyPemFilePath = Paths.get(privateKey);
} catch (InvalidPathException ipe) {
throw new IllegalArgumentException("Invalid privateKey file path", ipe);
}
boolean privateKeyPemFileExists = Files.exists(privateKeyPemFilePath, LinkOption.NOFOLLOW_LINKS);
Assert.isTrue(privateKeyPemFileExists, "privateKey file does not exist");
}
}

@Override
Expand Down Expand Up @@ -391,6 +403,80 @@ public ClientBuilder setPrivateKey(String privateKey) {
return this;
}

@Override
public ClientBuilder setPrivateKey(Path privateKeyPath) {
if (isOAuth2Flow()) {
Assert.notNull(privateKeyPath, "Missing privateKeyFile");
this.clientConfig.setPrivateKey(getFileContent(privateKeyPath));
}
return this;
}

@Override
public ClientBuilder setPrivateKey(InputStream privateKeyStream) {
if (isOAuth2Flow()) {
Assert.notNull(privateKeyStream, "Missing privateKeyFile");
this.clientConfig.setPrivateKey(getFileContent(privateKeyStream));
}
return this;
}

@Override
public ClientBuilder setPrivateKey(PrivateKey privateKey) {
if (isOAuth2Flow()) {
Assert.notNull(privateKey, "Missing privateKeyFile");
String algorithm = privateKey.getAlgorithm();
if (algorithm.equals("RSA")) {
String encodedString = ConfigUtil.RSA_PRIVATE_KEY_HEADER + "\n"
+ Base64.getEncoder().encodeToString(privateKey.getEncoded()) + "\n"
+ ConfigUtil.RSA_PRIVATE_KEY_FOOTER;
this.clientConfig.setPrivateKey(encodedString);
} else if(algorithm.equals("EC")) {
String encodedString = ConfigUtil.EC_PRIVATE_KEY_HEADER + "\n"
+ Base64.getEncoder().encodeToString(privateKey.getEncoded()) + "\n"
+ ConfigUtil.EC_PRIVATE_KEY_FOOTER;
this.clientConfig.setPrivateKey(encodedString);
} else {
throw new IllegalArgumentException("Supplied privateKey is not an RSA or EC key - " + algorithm);
}
}
return this;
}

private String getFileContent(File file) {
try (InputStream inputStream = new FileInputStream(file)) {
return readFromInputStream(inputStream);
} catch (IOException e) {
throw new IllegalArgumentException("Could not read from supplied private key file");
}
}

private String getFileContent(Path path) {
Assert.notNull(path, "The path to the privateKey cannot be null.");
return getFileContent(path.toFile());
}

private String getFileContent(InputStream privateKeyStream) {
try {
return readFromInputStream(privateKeyStream);
} catch (IOException e) {
throw new IllegalArgumentException("Could not read from supplied privateKey input stream");
}
}

private String readFromInputStream(InputStream inputStream) throws IOException {
Assert.notNull(inputStream, "InputStream cannot be null.");
StringBuilder resultStringBuilder = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(
inputStream, Charset.forName(StandardCharsets.UTF_8.name())))) {
String line;
while ((line = br.readLine()) != null) {
resultStringBuilder.append(line).append("\n");
}
}
return resultStringBuilder.toString();
}

@Override
public ClientBuilder setClientId(String clientId) {
ConfigurationValidator.assertClientId(clientId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.okta.sdk.impl.api.DefaultClientCredentialsResolver;
import com.okta.sdk.impl.config.ClientConfiguration;
import com.okta.sdk.impl.error.DefaultError;
import com.okta.sdk.impl.util.ConfigUtil;
import com.okta.sdk.resource.ExtensibleResource;
import com.okta.sdk.resource.ResourceException;
import io.jsonwebtoken.Jwts;
Expand All @@ -35,9 +36,9 @@

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.InvalidKeyException;
import java.security.KeyPair;
Expand Down Expand Up @@ -130,7 +131,7 @@ public OAuth2AccessToken getOAuth2AccessToken() throws IOException, InvalidKeyEx
*/
String createSignedJWT() throws InvalidKeyException, IOException {
String clientId = tokenClientConfiguration.getClientId();
PrivateKey privateKey = parsePrivateKey(tokenClientConfiguration.getPrivateKey());
PrivateKey privateKey = parsePrivateKey(getPemReader());
Instant now = Instant.now();

String jwt = Jwts.builder()
Expand All @@ -147,20 +148,16 @@ String createSignedJWT() throws InvalidKeyException, IOException {
}

/**
* Parse private key from the supplied path.
* Parse private key from the supplied configuration.
*
* @param privateKeyFilePath
* @param pemReader a {@link Reader} that has access to a full PEM resource
* @return {@link PrivateKey}
* @throws IOException
* @throws InvalidKeyException
*/
PrivateKey parsePrivateKey(String privateKeyFilePath) throws IOException, InvalidKeyException {
Assert.notNull(privateKeyFilePath, "privateKeyFilePath may not be null");
PrivateKey parsePrivateKey(Reader pemReader) throws IOException, InvalidKeyException {

Path privateKeyPemFilePath = Paths.get(privateKeyFilePath);
Reader reader = Files.newBufferedReader(privateKeyPemFilePath, Charset.defaultCharset());

PrivateKey privateKey = getPrivateKeyFromPEM(reader);
PrivateKey privateKey = getPrivateKeyFromPEM(pemReader);
String algorithm = privateKey.getAlgorithm();

if (!algorithm.equals("RSA") &&
Expand All @@ -171,6 +168,15 @@ PrivateKey parsePrivateKey(String privateKeyFilePath) throws IOException, Invali
return privateKey;
}

private Reader getPemReader() throws IOException {
String privateKey = tokenClientConfiguration.getPrivateKey();
if (ConfigUtil.hasPrivateKeyContentWrapper(privateKey)) {
return new StringReader(privateKey);
} else {
return Files.newBufferedReader(Paths.get(privateKey), Charset.defaultCharset());
}
}

/**
* Get Private key from input PEM file.
*
Expand Down
34 changes: 34 additions & 0 deletions impl/src/main/java/com/okta/sdk/impl/util/ConfigUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2018-Present Okta, 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.okta.sdk.impl.util;

public class ConfigUtil {

public static final String RSA_PRIVATE_KEY_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
public static final String RSA_PRIVATE_KEY_FOOTER = "-----END RSA PRIVATE KEY-----";
public static final String EC_PRIVATE_KEY_HEADER = "-----BEGIN EC PRIVATE KEY-----";
public static final String EC_PRIVATE_KEY_FOOTER = "-----END EC PRIVATE KEY-----";

/**
* Check if the PEM key has BEGIN content wrapper.
*
* @param key the supplied key has BEGIN wrapper/header
* @return
*/
public static boolean hasPrivateKeyContentWrapper(String key) {
return key.startsWith("-----BEGIN");
}
}
Loading

0 comments on commit c5a80d2

Please sign in to comment.