From c5a80d293a2d6b395f0231de257538bf9ddd1424 Mon Sep 17 00:00:00 2001 From: sergiishamrai-okta <71881713+sergiishamrai-okta@users.noreply.github.com> Date: Wed, 2 Dec 2020 20:50:25 +0200 Subject: [PATCH] Add ability to set PEM content to Client Configuration (#497) * 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 Co-authored-by: Arvind Krishnakumar <61501885+arvindkrishnakumar-okta@users.noreply.github.com> --- README.md | 13 +- THIRD-PARTY-NOTICES | 1 + .../com/okta/sdk/client/ClientBuilder.java | 42 +++++- .../main/java/quickstart/ReadmeSnippets.java | 4 + .../sdk/impl/client/DefaultClientBuilder.java | 110 ++++++++++++-- .../AccessTokenRetrieverServiceImpl.java | 26 ++-- .../com/okta/sdk/impl/util/ConfigUtil.java | 34 +++++ .../client/DefaultClientBuilderTest.groovy | 138 +++++++++++++++--- ...AccessTokenRetrieverServiceImplTest.groovy | 37 ++++- 9 files changed, 358 insertions(+), 47 deletions(-) create mode 100644 impl/src/main/java/com/okta/sdk/impl/util/ConfigUtil.java diff --git a/README.md b/README.md index ac966a09637..50dc334e303 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 9729fe2228c..bffe83a5b90 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -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 diff --git a/api/src/main/java/com/okta/sdk/client/ClientBuilder.java b/api/src/main/java/com/okta/sdk/client/ClientBuilder.java index 2cc2c5bd238..d334ca04533 100644 --- a/api/src/main/java/com/okta/sdk/client/ClientBuilder.java +++ b/api/src/main/java/com/okta/sdk/client/ClientBuilder.java @@ -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; /** @@ -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}. diff --git a/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java b/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java index 9bd79522e54..a0f70247f78 100644 --- a/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java +++ b/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java @@ -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(); } diff --git a/impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java b/impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java index 96af44fff5a..86011036aa4 100644 --- a/impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java +++ b/impl/src/main/java/com/okta/sdk/impl/client/DefaultClientBuilder.java @@ -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; /** @@ -80,6 +79,9 @@ *
  • Programmatically
  • * * + * 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 { @@ -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 @@ -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); diff --git a/impl/src/main/java/com/okta/sdk/impl/oauth2/AccessTokenRetrieverServiceImpl.java b/impl/src/main/java/com/okta/sdk/impl/oauth2/AccessTokenRetrieverServiceImpl.java index 56c2c971df0..a1689bf3ba6 100644 --- a/impl/src/main/java/com/okta/sdk/impl/oauth2/AccessTokenRetrieverServiceImpl.java +++ b/impl/src/main/java/com/okta/sdk/impl/oauth2/AccessTokenRetrieverServiceImpl.java @@ -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; @@ -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; @@ -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() @@ -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") && @@ -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. * diff --git a/impl/src/main/java/com/okta/sdk/impl/util/ConfigUtil.java b/impl/src/main/java/com/okta/sdk/impl/util/ConfigUtil.java new file mode 100644 index 00000000000..60d81b888b0 --- /dev/null +++ b/impl/src/main/java/com/okta/sdk/impl/util/ConfigUtil.java @@ -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"); + } +} diff --git a/impl/src/test/groovy/com/okta/sdk/impl/client/DefaultClientBuilderTest.groovy b/impl/src/test/groovy/com/okta/sdk/impl/client/DefaultClientBuilderTest.groovy index c616a53f6b0..2b2c1b94da5 100644 --- a/impl/src/test/groovy/com/okta/sdk/impl/client/DefaultClientBuilderTest.groovy +++ b/impl/src/test/groovy/com/okta/sdk/impl/client/DefaultClientBuilderTest.groovy @@ -34,6 +34,7 @@ import org.mockito.stubbing.Answer import org.testng.annotations.Listeners import org.testng.annotations.Test +import java.nio.file.Path import java.security.KeyPair import java.security.KeyPairGenerator import java.security.PrivateKey @@ -225,7 +226,7 @@ class DefaultClientBuilderTest { .setOrgUrl("https://okta.example.com") .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) .setClientId("client12345") - .setScopes(new HashSet()) + .setScopes([] as Set) .build() } } @@ -238,28 +239,28 @@ class DefaultClientBuilderTest { .setOrgUrl("https://okta.example.com") .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) .setClientId("client12345") - .setScopes(new HashSet<>(Arrays.asList({"okta.apps.read"}))) - .setPrivateKey(null) + .setScopes(["okta.apps.read"] as Set) + .setPrivateKey(null as Path) .build() } } @Test - void testOAuth2InvalidPrivateKeyPemFilePath() { + void testOAuth2InvalidPrivateKey_PemFilePath() { clearOktaEnvAndSysProps() Util.expect(IllegalArgumentException) { new DefaultClientBuilder(noDefaultYamlNoAppYamlResourceFactory()) .setOrgUrl("https://okta.example.com") .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) .setClientId("client12345") - .setScopes(new HashSet<>(Arrays.asList({"okta.apps.read"}))) - .setPrivateKey("blahblah.pem") + .setScopes(["okta.apps.read"] as Set) + .setPrivateKey("/some/invalid/path/privateKey.pem") .build() } } @Test - void testOAuth2InvalidPrivateKeyPemFileContent() { + void testOAuth2InvalidPrivateKey_PemFileContent() { clearOktaEnvAndSysProps() File privateKeyFile = File.createTempFile("tmp",".pem") privateKeyFile.write("-----INVALID PEM CONTENT-----") @@ -268,8 +269,8 @@ class DefaultClientBuilderTest { .setOrgUrl("https://okta.example.com") .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) .setClientId("client12345") - .setScopes(new HashSet<>(Arrays.asList({"okta.apps.read"}))) - .setPrivateKey(privateKeyFile.path) + .setScopes(["okta.apps.read"] as Set) + .setPrivateKey(privateKeyFile.text) .build() } @@ -283,16 +284,12 @@ class DefaultClientBuilderTest { // DSA algorithm is unsupported (we support only RSA & EC) File privateKeyFile = generatePrivateKey("DSA", 2048, "privateKey", ".pem") - Set scopes = new HashSet<>(); - scopes.add("okta.apps.read") - scopes.add("okta.apps.manage") - Util.expect(OAuth2TokenRetrieverException) { new DefaultClientBuilder(noDefaultYamlNoAppYamlResourceFactory()) .setOrgUrl("https://okta.example.com") .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) .setClientId("client12345") - .setScopes(scopes) + .setScopes(["okta.apps.read", "okta.apps.manage"] as Set) .setPrivateKey(privateKeyFile.path) .build() } @@ -301,22 +298,18 @@ class DefaultClientBuilderTest { } @Test - void testOAuth2SemanticallyValidInputParams() { + void testOAuth2SemanticallyValidInputParams_withPrivateKeyPemFilePath() { clearOktaEnvAndSysProps() File privateKeyFile = generatePrivateKey("RSA", 2048, "privateKey", ".pem") - Set scopes = new HashSet<>(); - scopes.add("okta.apps.read") - scopes.add("okta.apps.manage") - // expected because the URL is not an actual endpoint Util.expect(OAuth2TokenRetrieverException) { new DefaultClientBuilder(noDefaultYamlNoAppYamlResourceFactory()) .setOrgUrl("https://okta.example.com") .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) .setClientId("client12345") - .setScopes(scopes) + .setScopes(["okta.apps.read", "okta.apps.manage"] as Set) .setPrivateKey(privateKeyFile.path) .build() } @@ -324,6 +317,103 @@ class DefaultClientBuilderTest { privateKeyFile.delete() } + @Test + void testOAuth2SemanticallyValidInputParams_withPrivateKeyPemFileContent() { + clearOktaEnvAndSysProps() + + File privateKeyFile = generatePrivateKey("RSA", 2048, "anotherPrivateKey", ".pem") + + // expected because the URL is not an actual endpoint + Util.expect(OAuth2TokenRetrieverException) { + new DefaultClientBuilder(noDefaultYamlNoAppYamlResourceFactory()) + .setOrgUrl("https://okta.example.com") + .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) + .setClientId("client12345") + .setScopes(["okta.apps.read", "okta.apps.manage"] as Set) + .setPrivateKey(privateKeyFile.text) + .build() + } + + privateKeyFile.delete() + } + + @Test + void testOAuth2SemanticallyValidInputParams_withPrivateKeyPemPath() { + clearOktaEnvAndSysProps() + + File privateKeyFile = generatePrivateKey("RSA", 2048, "anotherPrivateKey", ".pem") + + // expected because the URL is not an actual endpoint + Util.expect(OAuth2TokenRetrieverException) { + new DefaultClientBuilder(noDefaultYamlNoAppYamlResourceFactory()) + .setOrgUrl("https://okta.example.com") + .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) + .setClientId("client12345") + .setScopes(["okta.apps.read", "okta.apps.manage"] as Set) + .setPrivateKey(privateKeyFile.toPath()) + .build() + } + + privateKeyFile.delete() + } + + @Test + void testOAuth2SemanticallyValidInputParams_withPrivateKeyPemInputStream() { + clearOktaEnvAndSysProps() + + File privateKeyFile = generatePrivateKey("RSA", 2048, "anotherPrivateKey", ".pem") + InputStream privateKeyFileInputStream = new FileInputStream(privateKeyFile); + + // expected because the URL is not an actual endpoint + Util.expect(OAuth2TokenRetrieverException) { + new DefaultClientBuilder(noDefaultYamlNoAppYamlResourceFactory()) + .setOrgUrl("https://okta.example.com") + .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) + .setClientId("client12345") + .setScopes(["okta.apps.read", "okta.apps.manage"] as Set) + .setPrivateKey(privateKeyFileInputStream) + .build() + } + + privateKeyFile.delete() + } + + @Test + void testOAuth2SemanticallyValidInputParams_withPrivateKeyRSA() { + clearOktaEnvAndSysProps() + + PrivateKey privateKey = generatePrivateKey("RSA", 2048) + + // expected because the URL is not an actual endpoint + Util.expect(OAuth2TokenRetrieverException) { + new DefaultClientBuilder(noDefaultYamlNoAppYamlResourceFactory()) + .setOrgUrl("https://okta.example.com") + .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) + .setClientId("client12345") + .setScopes(["okta.apps.read", "okta.apps.manage"] as Set) + .setPrivateKey(privateKey) + .build() + } + } + + @Test + void testOAuth2SemanticallyValidInputParams_withPrivateKeyEC() { + clearOktaEnvAndSysProps() + + PrivateKey privateKey = generatePrivateKey("EC", 256) + + // expected because the URL is not an actual endpoint + Util.expect(OAuth2TokenRetrieverException) { + new DefaultClientBuilder(noDefaultYamlNoAppYamlResourceFactory()) + .setOrgUrl("https://okta.example.com") + .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) + .setClientId("client12345") + .setScopes(["okta.apps.read", "okta.apps.manage"] as Set) + .setPrivateKey(privateKey) + .build() + } + } + @Test void testOAuth2WithEnvVariables() { RestoreEnvironmentVariables.setEnvironmentVariable("OKTA_CLIENT_ORGURL", @@ -362,6 +452,14 @@ class DefaultClientBuilderTest { return file } + static generatePrivateKey(String algorithm, int keySize) { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm) + keyGen.initialize(keySize) + KeyPair key = keyGen.generateKeyPair() + PrivateKey privateKey = key.getPrivate() + return privateKey + } + static ResourceFactory noDefaultYamlNoAppYamlResourceFactory() { def resourceFactory = spy(new DefaultResourceFactory()) doAnswer(new Answer() { diff --git a/impl/src/test/groovy/com/okta/sdk/impl/oauth2/AccessTokenRetrieverServiceImplTest.groovy b/impl/src/test/groovy/com/okta/sdk/impl/oauth2/AccessTokenRetrieverServiceImplTest.groovy index 07b9bc2df11..7b2b41e6085 100644 --- a/impl/src/test/groovy/com/okta/sdk/impl/oauth2/AccessTokenRetrieverServiceImplTest.groovy +++ b/impl/src/test/groovy/com/okta/sdk/impl/oauth2/AccessTokenRetrieverServiceImplTest.groovy @@ -77,8 +77,9 @@ class AccessTokenRetrieverServiceImplTest { void testParsePrivateKey() { PrivateKey generatedPrivateKey = generatePrivateKey("RSA", 2048) File privateKeyPemFile = writePrivateKeyToPemFile(generatedPrivateKey, "privateKey") + Reader reader = new BufferedReader(new FileReader(privateKeyPemFile)) - PrivateKey parsedPrivateKey = getAccessTokenRetrieverServiceInstance().parsePrivateKey(privateKeyPemFile.path) + PrivateKey parsedPrivateKey = getAccessTokenRetrieverServiceInstance().parsePrivateKey(reader) privateKeyPemFile.deleteOnExit() @@ -134,6 +135,32 @@ class AccessTokenRetrieverServiceImplTest { assertThat(claims.get("jti"), notNullValue()) } + @Test + void testCreateSignedJWTUsingPrivateKeyFromString() { + def clientConfig = mock(ClientConfiguration) + + PrivateKey generatedPrivateKey = generatePrivateKey("RSA", 2048) + + String baseUrl = "https://sample.okta.com" + BaseUrlResolver baseUrlResolver = new BaseUrlResolver() { + @Override + String getBaseUrl() { + return baseUrl + } + } + + when(clientConfig.getBaseUrl()).thenReturn(baseUrl) + when(clientConfig.getClientId()).thenReturn("client12345") + when(clientConfig.getPrivateKey()).thenReturn(createPemFileContent(generatedPrivateKey)) + when(clientConfig.getBaseUrlResolver()).thenReturn(baseUrlResolver) + when(clientConfig.getClientCredentialsResolver()).thenReturn( + new DefaultClientCredentialsResolver({ -> Optional.empty() })) + + String signedJwt = getAccessTokenRetrieverServiceInstance(clientConfig).createSignedJWT() + + assertThat(signedJwt, notNullValue()) + } + @Test(expectedExceptions = OAuth2TokenRetrieverException.class) void testGetOAuth2TokenRetrieverRuntimeException() { def tokenClient = mock(OAuth2TokenClient) @@ -227,12 +254,16 @@ class AccessTokenRetrieverServiceImplTest { return privateKey } - File writePrivateKeyToPemFile(PrivateKey privateKey, String fileNamePrefix) { + String createPemFileContent(PrivateKey privateKey) { String encodedString = "-----BEGIN PRIVATE KEY-----\n" encodedString = encodedString + Base64.getEncoder().encodeToString(privateKey.getEncoded()) + "\n" encodedString = encodedString + "-----END PRIVATE KEY-----\n" + return encodedString + + } + File writePrivateKeyToPemFile(PrivateKey privateKey, String fileNamePrefix) { File privateKeyPemFile = File.createTempFile(fileNamePrefix,".pem") - privateKeyPemFile.write(encodedString) + privateKeyPemFile.write(createPemFileContent(privateKey)) return privateKeyPemFile }