Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for device grant #394

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions cli/src/main/java/com/okta/cli/commands/Start.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@
import com.okta.cli.common.service.DefaultSdkConfigurationService;
import com.okta.cli.common.service.DefaultSetupService;
import com.okta.cli.common.service.DefaultStartRestClient;
import com.okta.cli.common.service.SetupService;
import com.okta.cli.common.service.TarballExtractor;
import com.okta.cli.console.ConsoleOutput;
import com.okta.cli.console.PromptOption;
import com.okta.commons.lang.Assert;
import com.okta.commons.lang.Strings;
import com.okta.sdk.client.Client;
import com.okta.sdk.client.Clients;
import com.okta.sdk.resource.application.OpenIdConnectApplicationType;
import picocli.CommandLine;

import java.io.File;
Expand All @@ -48,7 +48,6 @@
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -114,8 +113,12 @@ public int runCommand() throws Exception {

// parse the `.okta.yaml` file
OktaSampleConfig config = new DefaultSampleConfigParser().loadConfig(projectDirectory, sampleContext);
OpenIdConnectApplicationType applicationType = OpenIdConnectApplicationType.valueOf(
config.getOAuthClient().getApplicationType().toUpperCase(Locale.ENGLISH));
String applicationType = config.getOAuthClient().getApplicationType();

// remap device key, because the actual grant name is long
if (applicationType.equals("device")) {
applicationType = SetupService.APP_TYPE_DEVICE;
}

// create the Okta application
Client client = Clients.builder().build();
Expand Down
34 changes: 30 additions & 4 deletions cli/src/main/java/com/okta/cli/commands/apps/AppsCreate.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.okta.cli.common.service.ClientConfigurationException;
import com.okta.cli.common.service.DefaultSdkConfigurationService;
import com.okta.cli.common.service.DefaultSetupService;
import com.okta.cli.common.service.SetupService;
import com.okta.cli.console.ConsoleOutput;
import com.okta.cli.console.Prompter;
import com.okta.commons.lang.Assert;
Expand Down Expand Up @@ -86,6 +87,8 @@ public int runCommand() throws Exception {
return createSpaApp(appName);
case NATIVE:
return createNativeApp(appName);
case DEVICE:
return createDeviceApp(appName);
case SERVICE:
return createServiceApp(appName,(ServiceAppTemplate) appTemplate);
default:
Expand Down Expand Up @@ -114,7 +117,7 @@ private int createWebApp(String appName, WebAppTemplate webAppTemplate) throws I
OidcProperties oidcProperties = appTemplate.getOidcProperties();

MutablePropertySource propertySource = appCreationMixin.getPropertySource(appTemplate.getDefaultConfigFileName());
new DefaultSetupService(oidcProperties).createOidcApplication(propertySource, appName, baseUrl, groupClaimName, groupsToCreate, issuer.getIssuer(), issuer.getId(), true, OpenIdConnectApplicationType.WEB, redirectUris, postLogoutRedirectUris, client);
new DefaultSetupService(oidcProperties).createOidcApplication(propertySource, appName, baseUrl, groupClaimName, groupsToCreate, issuer.getIssuer(), issuer.getId(), true, OpenIdConnectApplicationType.WEB.toString(), redirectUris, postLogoutRedirectUris, client);

out.writeLine("Okta application configuration has been written to: " + propertySource.getName());

Expand All @@ -134,7 +137,28 @@ private Integer createNativeApp(String appName) throws IOException {
AuthorizationServer issuer = getIssuer(client);

MutablePropertySource propertySource = new MapPropertySource();
new DefaultSetupService(OidcProperties.oktaEnv()).createOidcApplication(propertySource, appName, baseUrl, null, Collections.emptySet(), issuer.getIssuer(), issuer.getId(), getEnvironment().isInteractive(), OpenIdConnectApplicationType.NATIVE, redirectUris, postLogoutRedirectUris, client);
new DefaultSetupService(OidcProperties.oktaEnv()).createOidcApplication(propertySource, appName, baseUrl, null, Collections.emptySet(), issuer.getIssuer(), issuer.getId(), getEnvironment().isInteractive(), OpenIdConnectApplicationType.NATIVE.toString(), redirectUris, postLogoutRedirectUris, client);

out.writeLine("Okta application configuration: ");
propertySource.getProperties().forEach((key, value) -> {
out.bold(key);
out.write(": ");
out.writeLine(value);
});

return 0;
}

private Integer createDeviceApp(String appName) throws IOException {

ConsoleOutput out = getConsoleOutput();
String baseUrl = getBaseUrl();

Client client = Clients.builder().build();
AuthorizationServer issuer = getIssuer(client);

MutablePropertySource propertySource = new MapPropertySource();
new DefaultSetupService(OidcProperties.oktaEnv()).createOidcApplication(propertySource, appName, baseUrl, null, Collections.emptySet(), issuer.getIssuer(), issuer.getId(), getEnvironment().isInteractive(), SetupService.APP_TYPE_DEVICE, client);

out.writeLine("Okta application configuration: ");
propertySource.getProperties().forEach((key, value) -> {
Expand All @@ -158,7 +182,7 @@ private Integer createServiceApp(String appName, ServiceAppTemplate appTemplate)
AuthorizationServer issuer = getIssuer(client);

MutablePropertySource propertySource = appCreationMixin.getPropertySource(appTemplate.getDefaultConfigFileName());
new DefaultSetupService(appTemplate.getOidcProperties()).createOidcApplication(propertySource, appName, baseUrl, null, Collections.emptySet(), issuer.getIssuer(), issuer.getId(), getEnvironment().isInteractive(), OpenIdConnectApplicationType.SERVICE, client);
new DefaultSetupService(appTemplate.getOidcProperties()).createOidcApplication(propertySource, appName, baseUrl, null, Collections.emptySet(), issuer.getIssuer(), issuer.getId(), getEnvironment().isInteractive(), OpenIdConnectApplicationType.SERVICE.toString(), client);

out.writeLine("Okta application configuration has been written to: " + propertySource.getName());

Expand All @@ -177,7 +201,7 @@ private Integer createSpaApp(String appName) throws IOException {
List<String> trustedOrigins = redirectUris.stream().map(URIs::baseUrlOf).collect(Collectors.toList());

MutablePropertySource propertySource = new MapPropertySource();
new DefaultSetupService(OidcProperties.oktaEnv()).createOidcApplication(propertySource, appName, baseUrl, null, Collections.emptySet(), authorizationServer.getIssuer(), authorizationServer.getId(), getEnvironment().isInteractive(), OpenIdConnectApplicationType.BROWSER, redirectUris, postLogoutRedirectUris, trustedOrigins, client);
new DefaultSetupService(OidcProperties.oktaEnv()).createOidcApplication(propertySource, appName, baseUrl, null, Collections.emptySet(), authorizationServer.getIssuer(), authorizationServer.getId(), getEnvironment().isInteractive(), OpenIdConnectApplicationType.BROWSER.toString(), redirectUris, postLogoutRedirectUris, trustedOrigins, client);

out.writeLine("Okta application configuration: ");
out.bold("Issuer: ");
Expand Down Expand Up @@ -267,6 +291,8 @@ private enum QuickTemplate {
SPA("spa", AppType.SPA, SpaAppTemplate.GENERIC),
// native
NATIVE("native", AppType.NATIVE, NativeAppTemplate.GENERIC),
// CLI, TV, other device
DEVICE("device", AppType.DEVICE, DeviceAppTemplate.GENERIC),
// service
SPRING_BOOT_SERVICE("spring-boot-service", AppType.SERVICE, ServiceAppTemplate.SPRING_BOOT),
JHIPSTER_SERVICE("jhipster-service", AppType.SERVICE, ServiceAppTemplate.JHIPSTER),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public enum AppType implements PromptOption<AppType> {
WEB("Web"),
SPA("Single Page App"),
NATIVE("Native App (mobile)"),
DEVICE("Device App (TV, CLI, or other device)"),
SERVICE("Service (Machine-to-Machine)");

private final String friendlyName;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2022-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.cli.commands.apps.templates;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public enum DeviceAppTemplate {

GENERIC("Device Grant App");

private static final List<String> names = Arrays.stream(values()).map(it -> it.friendlyName).collect(Collectors.toList());

private final String friendlyName;

DeviceAppTemplate(String friendlyName) {
this.friendlyName = friendlyName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.okta.sdk.impl.io.FileResource;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;

import java.io.File;
import java.io.FileInputStream;
Expand Down Expand Up @@ -47,11 +49,11 @@ public String getName() {
@Override
public void addProperties(Map<String, String> properties) throws IOException {

Yaml springAppYaml = new Yaml(yamlOptions());
Yaml springAppYaml = new Yaml(new Constructor(Map.class), new Representer(yamlOptions()));
Map<String, Object> existingProperties = new HashMap<>();
if (yamlFile.exists()) {
try (Reader reader = new InputStreamReader(new FileInputStream(yamlFile), StandardCharsets.UTF_8)) {
Map loadedProperties = springAppYaml.loadAs(reader, Map.class);
Map<? extends String,?> loadedProperties = springAppYaml.load(reader);

// null if the file is empty
if (loadedProperties != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,18 @@

import com.okta.cli.common.model.AuthorizationServer;
import com.okta.sdk.client.Client;
import com.okta.sdk.resource.authorization.server.policy.AuthorizationServerPolicyRule;

import java.util.Map;
import java.util.Optional;

public interface AuthorizationServerService {

Map<String, AuthorizationServer> authorizationServersMap(Client client);

void createGroupClaim(Client client, String groupClaimName, String authorizationServerId);

Optional<AuthorizationServerPolicyRule> getSinglePolicyRule(Client client, String authorizationServerId);

void enableDeviceGrant(Client client, String authorizationServerId, AuthorizationServerPolicyRule rule);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@
import com.okta.commons.lang.Assert;
import com.okta.sdk.client.Client;
import com.okta.sdk.resource.ExtensibleResource;
import com.okta.sdk.resource.authorization.server.AuthorizationServerPolicy;
import com.okta.sdk.resource.authorization.server.AuthorizationServerPolicyList;
import com.okta.sdk.resource.authorization.server.policy.AuthorizationServerPolicyRule;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class DefaultAuthorizationServerService implements AuthorizationServerService {
Expand Down Expand Up @@ -57,6 +61,35 @@ public void createGroupClaim(Client client, String groupClaimName, String author
}
}

@Override
public Optional<AuthorizationServerPolicyRule> getSinglePolicyRule(Client client, String authorizationServerId) {

List<AuthorizationServerPolicy> policies = client.http()
.get("/api/v1/authorizationServers/" + authorizationServerId + "/policies", AuthorizationServerPolicyList.class)
.stream()
.toList();

if (policies.size() != 1) {
return Optional.empty();
}

List<AuthorizationServerPolicyRule> rules = policies.get(0).listPolicyRules(authorizationServerId).stream().toList();
if (rules.size() != 1) {
return Optional.empty();
}

return Optional.of(rules.get(0));
}

@Override
public void enableDeviceGrant(Client client, String authorizationServerId, AuthorizationServerPolicyRule rule) {
List<String> grantTypes = rule.getConditions().getGrantTypes().getInclude();
if (!grantTypes.contains(SetupService.APP_TYPE_DEVICE)) {
grantTypes.add(SetupService.APP_TYPE_DEVICE);
rule.update(authorizationServerId);
}
}

private void createClaim(Client client, String groupClaimName, String authorizationServerId, String claimType) {
ExtensibleResource claimResource = client.instantiate(ExtensibleResource.class);
claimResource.put("name", groupClaimName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,36 @@ public ExtensibleResource createOidcServiceApp(Client client, String oidcAppName
return getClientCredentials(client, oidcApplication);
}

@Override
public ExtensibleResource createDeviceCodeApp(Client client, String oidcAppName) {

Optional<Application> existingApp = getApplication(client, oidcAppName);

// create a new OIDC app if one does NOT exist
Application oidcApplication = existingApp.orElseGet(() -> {

OpenIdConnectApplicationSettingsClient oidcClient = client.instantiate(OpenIdConnectApplicationSettingsClient.class)
.setApplicationType(OpenIdConnectApplicationType.NATIVE);
oidcClient.put("grant_types", List.of("urn:ietf:params:oauth:grant-type:device_code"));

Application app = client.instantiate(OpenIdConnectApplication.class)
.setSettings(client.instantiate(OpenIdConnectApplicationSettings.class)
.setOAuthClient(oidcClient))
.setCredentials(client.instantiate(OAuthApplicationCredentials.class)
.setOAuthClient(client.instantiate(ApplicationCredentialsOAuthClient.class)
.setTokenEndpointAuthMethod(OAuthEndpointAuthenticationMethod.NONE)))
.setLabel(oidcAppName);

app = client.createApplication(app);
assignAppToEveryoneGroup(client, app);

return app;
});

// lookup the credentials for this application
return getClientCredentials(client, oidcApplication);
}

private Optional<Application> getApplication(Client client, String appName) {
return client.listApplications(appName, null, null, null).stream()
.filter(app -> appName.equals(app.getLabel()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
package com.okta.cli.common.service;

import com.okta.cli.common.model.OktaSampleConfig;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.representer.Representer;

import java.io.File;
Expand Down Expand Up @@ -45,10 +47,10 @@ public OktaSampleConfig parseConfig(File configFile, Map<String, String> context
configFileContent = new DefaultInterpolator().interpolate(configFileContent, context);

// ignore unknown properties, so we can add additional features and not break older clients
Representer representer = new Representer();
Representer representer = new Representer(new DumperOptions());
representer.getPropertyUtils().setSkipMissingProperties(true);

OktaSampleConfig config = new Yaml(representer).loadAs(configFileContent, OktaSampleConfig.class);
OktaSampleConfig config = new Yaml(new Constructor(OktaSampleConfig.class), representer).loadAs(configFileContent, OktaSampleConfig.class);

// TODO improve validation of configuration
if (config.getOAuthClient() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,10 @@
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
Expand All @@ -50,19 +47,7 @@ public boolean isConfigured() throws ClientConfigurationException {

@Override
public ClientConfiguration loadUnvalidatedConfiguration() throws ClientConfigurationException {
try {
Field field = DefaultClientBuilder.class.getDeclaredField("clientConfig");

AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
field.setAccessible(true);
return null;
});

return (ClientConfiguration) field.get(clientBuilder());
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new ClientConfigurationException("Could not load Okta SDK configuration, ensure okta-sdk-api version has " +
"not been changed in this plugin's configuration: " + e.getMessage(), e);
}
return clientBuilder().getClientConfiguration();
}

@Override
Expand Down
Loading