From 53ce5d0105a0564aa7958692abc101d85c693ac3 Mon Sep 17 00:00:00 2001 From: Arvind Krishnakumar Date: Tue, 20 Dec 2022 11:37:09 -0600 Subject: [PATCH 1/3] fix oauth token retrieval issue --- README.md | 27 +++++++++++++++++++ .../main/java/quickstart/ReadmeSnippets.java | 17 ++++++++++++ .../sdk/impl/client/DefaultClientBuilder.java | 2 +- .../AccessTokenRetrieverServiceImpl.java | 17 +++++------- .../sdk/impl/oauth2/OAuth2AccessToken.java | 10 ++++--- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 0d07a98ee37..e06e5ead4f6 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,33 @@ ApiClient client = Clients.builder() [//]: # (end: createClient) Hard-coding the Okta domain and API token works for quick tests, but for real projects you should use a more secure way of storing these values (such as environment variables). This library supports a few different configuration sources, covered in the [configuration reference](#configuration-reference) section. + +## OAuth 2.0 + +Okta allows you to interact with Okta APIs using scoped OAuth 2.0 access tokens. Each access token enables the bearer to perform specific actions on specific Okta endpoints, with that ability controlled by which scopes the access token contains. + +This SDK supports this feature only for service-to-service applications. Check out [our guides](https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/overview/) to learn more about how to register a new service application using a private and public key pair. + +Check out [our guide](https://developer.okta.com/docs/guides/implement-oauth-for-okta-serviceapp/main/#generate-the-jwk-using-the-admin-console) to learn how to generate a JWK and convert the same to PEM format which would be used as PrivateKey in `Client` creation. + +When using this approach, you will not need an API Token because the SDK will request an access token for you. In order to use OAuth 2.0, construct a client instance by passing the following parameters: + +[//]: # (method: createOAuth2Client) +```java +ApiClient client = Clients.builder() + .setOrgUrl("https://{yourOktaDomain}") // e.g. https://dev-123456.okta.com + .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) + .setClientId("{clientId}") + .setKid("{kid}") // optional + .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) ## Usage guide diff --git a/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java b/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java index a9ef0c28e7a..ca6e8cbf8b6 100644 --- a/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java +++ b/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java @@ -17,6 +17,7 @@ import com.okta.sdk.authc.credentials.TokenClientCredentials; import com.okta.sdk.cache.Caches; +import com.okta.sdk.client.AuthorizationMode; import com.okta.sdk.client.Clients; import com.okta.sdk.resource.common.PagedList; import com.okta.sdk.resource.group.GroupBuilder; @@ -56,6 +57,7 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.concurrent.TimeUnit; @@ -81,6 +83,21 @@ private void createClient() { .build(); } + private void createOAuth2Client() { + ApiClient client = Clients.builder() + .setOrgUrl("https://{yourOktaDomain}") // e.g. https://dev-123456.okta.com + .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) + .setClientId("{clientId}") + .setKid("{kid}") // optional + .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(); + } + private void getUser() { UserApi userApi = new UserApi(client); 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 4a7b0456203..cb7044a041c 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 @@ -349,6 +349,7 @@ public ApiClient build() { } ApiClient apiClient = new ApiClient(restTemplate(this.clientConfig), this.cacheManager); + apiClient.setBasePath(this.clientConfig.getBaseUrl()); if (!isOAuth2Flow()) { if (this.clientConfig.getClientCredentialsResolver() == null && this.clientCredentials != null) { @@ -357,7 +358,6 @@ public ApiClient build() { this.clientConfig.setClientCredentialsResolver(new DefaultClientCredentialsResolver(this.clientConfig)); } - apiClient.setBasePath(this.clientConfig.getBaseUrl()); apiClient.setApiKeyPrefix("SSWS"); apiClient.setApiKey((String) this.clientConfig.getClientCredentialsResolver().getClientCredentials().getCredentials()); } else { 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 80d384d6b67..c11306fc601 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 @@ -60,9 +60,7 @@ /** * Implementation of {@link AccessTokenRetrieverService} interface. - * * This has logic to fetch OAuth2 access token from the Authorization server endpoint. - * * @since 1.6.0 */ public class AccessTokenRetrieverServiceImpl implements AccessTokenRetrieverService { @@ -92,10 +90,6 @@ public OAuth2AccessToken getOAuth2AccessToken() throws IOException, InvalidKeyEx String scope = String.join(" ", tokenClientConfiguration.getScopes()); try { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); - httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - MultiValueMap queryParams = new LinkedMultiValueMap<>(); queryParams.add("grant_type", "client_credentials"); queryParams.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); @@ -107,12 +101,12 @@ public OAuth2AccessToken getOAuth2AccessToken() throws IOException, InvalidKeyEx Collections.emptyMap(), queryParams, null, - httpHeaders, + new HttpHeaders(), new LinkedMultiValueMap<>(), null, Collections.singletonList(MediaType.APPLICATION_JSON), - MediaType.APPLICATION_JSON, - new String[] { "OAuth_2.0" }, + MediaType.APPLICATION_FORM_URLENCODED, + new String[] { "oauth2" }, new ParameterizedTypeReference() {}); OAuth2AccessToken oAuth2AccessToken = responseEntity.getBody(); @@ -120,6 +114,8 @@ public OAuth2AccessToken getOAuth2AccessToken() throws IOException, InvalidKeyEx log.debug("Got OAuth2 access token for client id {} from {}", tokenClientConfiguration.getClientId(), tokenClientConfiguration.getBaseUrl() + TOKEN_URI); + apiClient.setAccessToken(oAuth2AccessToken.getAccessToken()); + return oAuth2AccessToken; } catch (ResourceException e) { Error defaultError = e.getError(); @@ -252,7 +248,8 @@ ClientConfiguration constructTokenClientConfig(ClientConfiguration apiClientConf if (apiClientConfiguration.getProxy() != null) tokenClientConfiguration.setProxy(apiClientConfiguration.getProxy()); - tokenClientConfiguration.setAuthenticationScheme(AuthenticationScheme.NONE); + tokenClientConfiguration.setBaseUrl(apiClientConfiguration.getBaseUrl()); + tokenClientConfiguration.setAuthenticationScheme(AuthenticationScheme.OAUTH2_PRIVATE_KEY); tokenClientConfiguration.setAuthorizationMode(AuthorizationMode.get(tokenClientConfiguration.getAuthenticationScheme())); tokenClientConfiguration.setClientId(apiClientConfiguration.getClientId()); tokenClientConfiguration.setScopes(apiClientConfiguration.getScopes()); diff --git a/impl/src/main/java/com/okta/sdk/impl/oauth2/OAuth2AccessToken.java b/impl/src/main/java/com/okta/sdk/impl/oauth2/OAuth2AccessToken.java index 8428022df9b..e902c3717f7 100644 --- a/impl/src/main/java/com/okta/sdk/impl/oauth2/OAuth2AccessToken.java +++ b/impl/src/main/java/com/okta/sdk/impl/oauth2/OAuth2AccessToken.java @@ -15,6 +15,8 @@ */ package com.okta.sdk.impl.oauth2; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.time.Duration; import java.time.Instant; @@ -31,16 +33,16 @@ public class OAuth2AccessToken { public static final String ACCESS_TOKEN_KEY = "access_token"; public static final String SCOPE_KEY = "scope"; - /* Token error constants */ - public static final String ERROR_KEY = "error"; - public static final String ERROR_DESCRIPTION = "error_description"; - + @JsonProperty(TOKEN_TYPE_KEY) private String tokenType; + @JsonProperty(EXPIRES_IN_KEY) private Integer expiresIn; + @JsonProperty(ACCESS_TOKEN_KEY) private String accessToken; + @JsonProperty(SCOPE_KEY) private String scope; private Instant issuedAt = Instant.now(); From 33aaa84497796ee87fa435efa24338f954f114b4 Mon Sep 17 00:00:00 2001 From: Arvind Krishnakumar Date: Tue, 20 Dec 2022 11:42:42 -0600 Subject: [PATCH 2/3] minor refactor --- README.md | 2 +- .../quickstart/src/main/java/quickstart/ReadmeSnippets.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e06e5ead4f6..c71d4017ee4 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ ApiClient client = Clients.builder() .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) .setClientId("{clientId}") .setKid("{kid}") // optional - .setScopes(new HashSet<>(Arrays.asList("okta.users.read", "okta.apps.read"))) + .setScopes(new HashSet<>(Arrays.asList("okta.users.manage", "okta.apps.manage", "okta.groups.manage"))) .setPrivateKey("/path/to/yourPrivateKey.pem") // (or) .setPrivateKey("full PEM payload") // (or) .setPrivateKey(Paths.get("/path/to/yourPrivateKey.pem")) diff --git a/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java b/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java index ca6e8cbf8b6..3f616084f07 100644 --- a/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java +++ b/examples/quickstart/src/main/java/quickstart/ReadmeSnippets.java @@ -89,7 +89,7 @@ private void createOAuth2Client() { .setAuthorizationMode(AuthorizationMode.PRIVATE_KEY) .setClientId("{clientId}") .setKid("{kid}") // optional - .setScopes(new HashSet<>(Arrays.asList("okta.users.read", "okta.apps.read"))) + .setScopes(new HashSet<>(Arrays.asList("okta.users.manage", "okta.apps.manage", "okta.groups.manage"))) .setPrivateKey("/path/to/yourPrivateKey.pem") // (or) .setPrivateKey("full PEM payload") // (or) .setPrivateKey(Paths.get("/path/to/yourPrivateKey.pem")) From 3fba5578eef18924955f2118264257f16cf18299 Mon Sep 17 00:00:00 2001 From: Arvind Krishnakumar Date: Tue, 3 Jan 2023 11:21:24 -0600 Subject: [PATCH 3/3] added unit test --- .../sdk/impl/oauth2/OAuth2AccessToken.java | 13 ++++++ ...AccessTokenRetrieverServiceImplTest.groovy | 42 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/impl/src/main/java/com/okta/sdk/impl/oauth2/OAuth2AccessToken.java b/impl/src/main/java/com/okta/sdk/impl/oauth2/OAuth2AccessToken.java index e902c3717f7..f5babb59019 100644 --- a/impl/src/main/java/com/okta/sdk/impl/oauth2/OAuth2AccessToken.java +++ b/impl/src/main/java/com/okta/sdk/impl/oauth2/OAuth2AccessToken.java @@ -30,6 +30,9 @@ public class OAuth2AccessToken { /* Token body constants */ public static final String TOKEN_TYPE_KEY = "token_type"; public static final String EXPIRES_IN_KEY = "expires_in"; + + public static final String ID_TOKEN_KEY = "id_token"; + public static final String ACCESS_TOKEN_KEY = "access_token"; public static final String SCOPE_KEY = "scope"; @@ -42,6 +45,9 @@ public class OAuth2AccessToken { @JsonProperty(ACCESS_TOKEN_KEY) private String accessToken; + @JsonProperty(ID_TOKEN_KEY) + private String idToken; + @JsonProperty(SCOPE_KEY) private String scope; @@ -63,6 +69,12 @@ public void setExpiresIn(Integer expiresIn) { this.expiresIn = expiresIn; } + public String getIdToken() { return idToken; } + + public void setIdToken(String idToken) { + this.idToken = idToken; + } + public String getAccessToken() { return accessToken; } @@ -98,6 +110,7 @@ public String toString() { return "OAuth2AccessToken [tokenType=" + tokenType + ", issuedAt=" + issuedAt + ", expiresIn=" + expiresIn + + ", idToken=xxxxx" + ", accessToken=xxxxx" + ", scope=" + scope + "]"; } 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 8b682fd2627..2844b2c91bf 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 @@ -33,8 +33,12 @@ import org.bouncycastle.openssl.PEMException import org.hamcrest.MatcherAssert import org.mockito.ArgumentMatchers import org.openapitools.client.ApiClient +import org.springframework.core.ParameterizedTypeReference import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.util.LinkedMultiValueMap import org.springframework.util.MultiValueMap import org.testng.annotations.Test @@ -160,6 +164,44 @@ class AccessTokenRetrieverServiceImplTest { MatcherAssert.assertThat(parsedPrivateKey.getFormat(), is("PKCS#8")) } + @Test + void testAccessTokenRetrieval() { + + def apiClient = mock(ApiClient) + def clientConfiguration = mock(ClientConfiguration) + + when(clientConfiguration.getPrivateKey()).thenReturn(PRIVATE_KEY) + + def accessTokenRetrievalService = new AccessTokenRetrieverServiceImpl(clientConfiguration, apiClient) + + OAuth2AccessToken oAuth2AccessToken = new OAuth2AccessToken() + oAuth2AccessToken.setAccessToken("accessToken") + oAuth2AccessToken.setIdToken("idToken") + oAuth2AccessToken.setTokenType("Bearer") + oAuth2AccessToken.setExpiresIn(3600) + oAuth2AccessToken.setScope("openid") + + when(apiClient.invokeAPI(contains("oauth2/v1/token"), + eq(HttpMethod.POST), + anyMap(), + any(MultiValueMap.class), + isNull(), + any(HttpHeaders.class), + any(LinkedMultiValueMap.class), + isNull(), + eq(Collections.singletonList(MediaType.APPLICATION_JSON)), + eq(MediaType.APPLICATION_FORM_URLENCODED), + any(), + any())).thenReturn(ResponseEntity.ok(oAuth2AccessToken)) + + OAuth2AccessToken mockResponseAccessToken = accessTokenRetrievalService.getOAuth2AccessToken() + assertEquals(mockResponseAccessToken.getAccessToken(), "accessToken") + assertEquals(mockResponseAccessToken.getIdToken(), "idToken") + assertEquals(mockResponseAccessToken.getTokenType(), "Bearer") + assertEquals(mockResponseAccessToken.getExpiresIn(), 3600) + assertEquals(mockResponseAccessToken.getScope(), "openid") + } + @Test void testCreateSignedJWT() {