com.okta.cli.commands.apps.AppsCreate Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2020-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;
import com.okta.cli.commands.BaseCommand;
import com.okta.cli.commands.apps.templates.*;
import com.okta.cli.common.URIs;
import com.okta.cli.common.config.MapPropertySource;
import com.okta.cli.common.config.MutablePropertySource;
import com.okta.cli.common.model.AuthorizationServer;
import com.okta.cli.common.model.OidcProperties;
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.console.ConsoleOutput;
import com.okta.cli.console.Prompter;
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.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@CommandLine.Command(name = "create",
description = "Create a new Okta app")
public class AppsCreate extends BaseCommand {
private static final String NO_BASE_URL_ERROR = "Unable to find base URL, run `okta login` and try again";
@CommandLine.Mixin
private AppCreationMixin appCreationMixin;
@CommandLine.Parameters(hidden = true, converter = EnumTypeConverter.class)
List quickTemplates;
@Override
public int runCommand() throws Exception {
if (quickTemplates != null && quickTemplates.size() > 1) {
throw new IllegalArgumentException("Only one positional parameter is allowed");
}
QuickTemplate quickTemplate = quickTemplates == null
? null
: quickTemplates.get(0);
Prompter prompter = getPrompter();
// prompt (if needed) for the applicaiton name first
String appName = getAppName();
AppType appType;
Object appTemplate = null;
if (quickTemplate == null) {
appType = prompter.prompt("Type of Application\n(The Okta CLI only supports a subset of application types and properties):", Arrays.asList(AppType.values()), AppType.WEB);
} else {
appType = quickTemplate.appType;
appTemplate = quickTemplate.appTemplate;
}
switch(appType) {
case WEB:
return createWebApp(appName, (WebAppTemplate) appTemplate);
case SPA:
return createSpaApp(appName);
case NATIVE:
return createNativeApp(appName);
case SERVICE:
return createServiceApp(appName,(ServiceAppTemplate) appTemplate);
default:
throw new IllegalStateException("Unsupported AppType: "+ appType);
}
}
private int createWebApp(String appName, WebAppTemplate webAppTemplate) throws IOException {
ConsoleOutput out = getConsoleOutput();
Prompter prompter = getPrompter();
WebAppTemplate appTemplate = prompter.promptIfEmpty(webAppTemplate, "Framework of Application", WebAppTemplate.values(), WebAppTemplate.GENERIC);
List redirectUris = getRedirectUris(Map.of("Spring Security", "http://localhost:8080/login/oauth2/code/okta",
"Quarkus OIDC", "http://localhost:8080/callback",
"JHipster", "http://localhost:8080/login/oauth2/code/oidc"),
appTemplate.getDefaultRedirectUris());
List postLogoutRedirectUris = getPostLogoutRedirectUris(redirectUris, appTemplate.getDefaultPostLogoutEndpoint());
Client client = Clients.builder().build();
AuthorizationServer issuer = getIssuer(client);
String baseUrl = getBaseUrl();
String groupClaimName = appTemplate.getGroupsClaim();
Set groupsToCreate = appTemplate.getGroupsToCreate();
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);
out.writeLine("Okta application configuration has been written to: " + propertySource.getName());
return 0;
}
private Integer createNativeApp(String appName) throws IOException {
ConsoleOutput out = getConsoleOutput();
String baseUrl = getBaseUrl();
String reverseDomain = URIs.reverseDomain(baseUrl);
String defaultRedirectUri = reverseDomain + ":/callback";
List redirectUris = getRedirectUris(Map.of("Reverse Domain name", defaultRedirectUri), defaultRedirectUri);
List postLogoutRedirectUris = getPostLogoutRedirectUris(redirectUris);
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(), OpenIdConnectApplicationType.NATIVE, 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 createServiceApp(String appName, ServiceAppTemplate appTemplate) throws IOException {
ConsoleOutput out = getConsoleOutput();
Prompter prompter = getPrompter();
appTemplate = prompter.promptIfEmpty(appTemplate, "Framework of Application", ServiceAppTemplate.values(), ServiceAppTemplate.GENERIC);
String baseUrl = getBaseUrl();
Client client = Clients.builder().build();
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);
out.writeLine("Okta application configuration has been written to: " + propertySource.getName());
return 0;
}
private Integer createSpaApp(String appName) throws IOException {
ConsoleOutput out = getConsoleOutput();
String baseUrl = getBaseUrl();
List redirectUris = getRedirectUris(Map.of("/callback", "http://localhost:8080/callback"), SpaAppTemplate.GENERIC.getDefaultRedirectUri());
List postLogoutRedirectUris = getPostLogoutRedirectUris(redirectUris);
Client client = Clients.builder().build();
AuthorizationServer authorizationServer = getIssuer(client);
List 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);
out.writeLine("Okta application configuration: ");
out.bold("Issuer: ");
out.writeLine(propertySource.getProperty("okta.oauth2.issuer"));
out.bold("Client ID: ");
out.writeLine(propertySource.getProperty("okta.oauth2.client-id"));
return 0;
}
private String getAppName() {
Prompter prompter = getPrompter();
return prompter.promptUntilIfEmpty(appCreationMixin.appName,"Application name", appCreationMixin.getDefaultAppName());
}
private AuthorizationServer getIssuer(Client client) {
Prompter prompter = getPrompter();
return CommonAppsPrompts.getIssuer(client, prompter, appCreationMixin.authorizationServerId);
}
private String getBaseUrl() {
try {
String baseUrl = new DefaultSdkConfigurationService().loadUnvalidatedConfiguration().getBaseUrl();
if (Strings.isEmpty(baseUrl)) {
throw new IllegalStateException(NO_BASE_URL_ERROR);
}
return baseUrl;
} catch (ClientConfigurationException e) {
throw new IllegalStateException(NO_BASE_URL_ERROR, e);
}
}
private List getRedirectUris(Map commonExamples, List defaultRedirectUris) {
Prompter prompter = getPrompter();
StringBuilder redirectUriPrompt = new StringBuilder("Redirect URI\nCommon defaults:\n");
commonExamples.forEach((key, value) -> {
redirectUriPrompt.append(" ").append(key).append(" - ").append(value).append("\n");
});
redirectUriPrompt.append("Enter your Redirect URI(s)");
String redirectUrisString = String.join(", ", defaultRedirectUris);
String result = prompter.promptIfEmpty(appCreationMixin.redirectUri, redirectUriPrompt.toString(), redirectUrisString).trim();
return split(result);
}
private List getRedirectUris(Map commonExamples, String defaultRedirectUri) {
return getRedirectUris(commonExamples, Collections.singletonList(defaultRedirectUri));
}
private List getPostLogoutRedirectUris(List redirectUris) {
return getPostLogoutRedirectUris(redirectUris, "/");
}
private List getPostLogoutRedirectUris(List redirectUris, String defaultPostLogoutUri) {
Prompter prompter = getPrompter();
Assert.notEmpty(redirectUris, "Redirect Uris cannot be empty");
String defaultPostLogoutUris = redirectUris.stream()
.map(uri -> URIs.resolveUrl(uri, defaultPostLogoutUri))
.collect(Collectors.joining(", "));
String result = prompter.promptIfEmpty(appCreationMixin.redirectUri, "Enter your Post Logout Redirect URI(s)", defaultPostLogoutUris).trim();
return split(result);
}
private List split(String input) {
String result = input.replaceFirst("^\\[", "");
result = result.replaceFirst("]$", "");
return Arrays.stream(result.split(","))
.map(String::trim)
.filter(it -> !it.isEmpty())
.collect(Collectors.toList());
}
/**
* Quick templates are meant to reduce prompts for the end user, for example you could instruct a user to run
* {@code okta apps create spring-boot-service} and they would be minimally prompted.
*/
private enum QuickTemplate {
// web
OKTA_SPRING_BOOT("okta-spring-boot", AppType.WEB, WebAppTemplate.OKTA_SPRING_BOOT),
SPRING_BOOT("spring-boot", AppType.WEB, WebAppTemplate.SPRING_BOOT),
QUARKUS("quarkus", AppType.WEB, WebAppTemplate.QUARKUS),
JHIPSTER("jhipster", AppType.WEB, WebAppTemplate.JHIPSTER),
GENERIC_WEB("web", AppType.WEB, WebAppTemplate.GENERIC),
SPA("spa", AppType.SPA, SpaAppTemplate.GENERIC),
// native
NATIVE("native", AppType.NATIVE, NativeAppTemplate.GENERIC),
// service
SPRING_BOOT_SERVICE("spring-boot-service", AppType.SERVICE, ServiceAppTemplate.SPRING_BOOT),
JHIPSTER_SERVICE("jhipster-service", AppType.SERVICE, ServiceAppTemplate.JHIPSTER),
GENERIC_SERVICE("service", AppType.SERVICE, ServiceAppTemplate.GENERIC);
private static final List names = Arrays.stream(values()).map(it -> it.friendlyName).collect(Collectors.toList());
private final String friendlyName;
private final AppType appType;
private final Object appTemplate; // TODO, this is ugly (needs a base type)
QuickTemplate(String friendlyName, AppType appType, Object appTemplate) {
this.friendlyName = friendlyName;
this.appType = appType;
this.appTemplate = appTemplate;
}
static QuickTemplate fromName(String name) {
return Arrays.stream(values())
.filter(it -> it.friendlyName.equals(name))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("template must be empty or one of: " + names));
}
}
static class EnumTypeConverter implements CommandLine.ITypeConverter {
@Override
public QuickTemplate convert(String value) throws Exception {
return QuickTemplate.fromName(value);
}
}
}