diff --git a/README.md b/README.md index 0d07a98ee37..c71d4017ee4 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.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")) + // (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..3f616084f07 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.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")) + // (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..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 @@ -15,6 +15,8 @@ */ package com.okta.sdk.impl.oauth2; +import com.fasterxml.jackson.annotation.JsonProperty; + import java.time.Duration; import java.time.Instant; @@ -28,19 +30,25 @@ 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"; - /* 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(ID_TOKEN_KEY) + private String idToken; + + @JsonProperty(SCOPE_KEY) private String scope; private Instant issuedAt = Instant.now(); @@ -61,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; } @@ -96,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() {