Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2018-03-29 09:46:16 -04:00
commit 5d74b776d6
171 changed files with 6041 additions and 2191 deletions

View file

@ -19,7 +19,6 @@ package org.keycloak.adapters.authorization;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
@ -30,10 +29,12 @@ import org.keycloak.adapters.spi.HttpFacade.Request;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthorizationContext;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessToken.Authorization;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.EnforcementMode;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.MethodConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.ScopeEnforcementMode;
import org.keycloak.representations.idm.authorization.Permission;
/**
@ -42,31 +43,23 @@ import org.keycloak.representations.idm.authorization.Permission;
public abstract class AbstractPolicyEnforcer {
private static Logger LOGGER = Logger.getLogger(AbstractPolicyEnforcer.class);
private final PolicyEnforcerConfig enforcerConfig;
private static final String HTTP_METHOD_DELETE = "DELETE";
private final PolicyEnforcer policyEnforcer;
private Map<String, PathConfig> paths;
private AuthzClient authzClient;
private PathMatcher pathMatcher;
public AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
protected AbstractPolicyEnforcer(PolicyEnforcer policyEnforcer) {
this.policyEnforcer = policyEnforcer;
this.enforcerConfig = policyEnforcer.getEnforcerConfig();
this.authzClient = policyEnforcer.getClient();
this.pathMatcher = policyEnforcer.getPathMatcher();
this.paths = policyEnforcer.getPaths();
}
public AuthorizationContext authorize(OIDCHttpFacade httpFacade) {
EnforcementMode enforcementMode = this.enforcerConfig.getEnforcementMode();
EnforcementMode enforcementMode = getEnforcerConfig().getEnforcementMode();
if (EnforcementMode.DISABLED.equals(enforcementMode)) {
return createEmptyAuthorizationContext(true);
}
Request request = httpFacade.getRequest();
String path = getPath(request);
PathConfig pathConfig = this.pathMatcher.matches(path, this.paths);
PathConfig pathConfig = getPathConfig(request);
KeycloakSecurityContext securityContext = httpFacade.getSecurityContext();
if (securityContext == null) {
@ -79,16 +72,20 @@ public abstract class AbstractPolicyEnforcer {
AccessToken accessToken = securityContext.getToken();
if (accessToken != null) {
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Checking permissions for path [%s] with config [%s].", request.getURI(), pathConfig);
}
if (pathConfig == null) {
if (EnforcementMode.PERMISSIVE.equals(enforcementMode)) {
return createAuthorizationContext(accessToken, null);
}
LOGGER.debugf("Could not find a configuration for path [%s]", path);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Could not find a configuration for path [%s]", getPath(request));
}
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
if (isDefaultAccessDeniedUri(request)) {
return createAuthorizationContext(accessToken, null);
}
@ -111,10 +108,18 @@ public abstract class AbstractPolicyEnforcer {
}
}
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
if (methodConfig != null && ScopeEnforcementMode.DISABLED.equals(methodConfig.getScopesEnforcementMode())) {
return createEmptyAuthorizationContext(true);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Sending challenge to the client. Path [%s]", pathConfig);
}
if (!challenge(pathConfig, methodConfig, httpFacade)) {
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Challenge not sent, sending default forbidden response. Path [%s]", pathConfig);
}
handleAccessDenied(httpFacade);
}
}
@ -126,22 +131,21 @@ public abstract class AbstractPolicyEnforcer {
protected boolean isAuthorized(PathConfig actualPathConfig, MethodConfig methodConfig, AccessToken accessToken, OIDCHttpFacade httpFacade) {
Request request = httpFacade.getRequest();
PolicyEnforcerConfig enforcerConfig = getEnforcerConfig();
if (isDefaultAccessDeniedUri(request, enforcerConfig)) {
if (isDefaultAccessDeniedUri(request)) {
return true;
}
AccessToken.Authorization authorization = accessToken.getAuthorization();
Authorization authorization = accessToken.getAuthorization();
if (authorization == null) {
return false;
}
List<Permission> permissions = authorization.getPermissions();
boolean hasPermission = false;
List<Permission> grantedPermissions = authorization.getPermissions();
for (Permission permission : permissions) {
for (Permission permission : grantedPermissions) {
if (permission.getResourceId() != null) {
if (isResourcePermission(actualPathConfig, permission)) {
hasPermission = true;
@ -151,9 +155,11 @@ public abstract class AbstractPolicyEnforcer {
}
if (hasResourceScopePermission(methodConfig, permission)) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
this.paths.remove(actualPathConfig);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, grantedPermissions);
}
if (HTTP_METHOD_DELETE.equalsIgnoreCase(request.getMethod()) && actualPathConfig.isInstance()) {
policyEnforcer.getPaths().remove(actualPathConfig);
}
return true;
}
@ -170,7 +176,9 @@ public abstract class AbstractPolicyEnforcer {
return true;
}
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, permissions);
if (LOGGER.isDebugEnabled()) {
LOGGER.debugf("Authorization FAILED for path [%s]. Not enough permissions [%s].", actualPathConfig, grantedPermissions);
}
return false;
}
@ -179,15 +187,21 @@ public abstract class AbstractPolicyEnforcer {
httpFacade.getResponse().sendError(403);
}
private boolean isDefaultAccessDeniedUri(Request request, PolicyEnforcerConfig enforcerConfig) {
String accessDeniedPath = enforcerConfig.getOnDenyRedirectTo();
protected AuthzClient getAuthzClient() {
return policyEnforcer.getClient();
}
if (accessDeniedPath != null) {
if (request.getURI().contains(accessDeniedPath)) {
return true;
}
}
return false;
protected PolicyEnforcerConfig getEnforcerConfig() {
return policyEnforcer.getEnforcerConfig();
}
protected PolicyEnforcer getPolicyEnforcer() {
return policyEnforcer;
}
private boolean isDefaultAccessDeniedUri(Request request) {
String accessDeniedPath = getEnforcerConfig().getOnDenyRedirectTo();
return accessDeniedPath != null && request.getURI().contains(accessDeniedPath);
}
private boolean hasResourceScopePermission(MethodConfig methodConfig, Permission permission) {
@ -215,20 +229,8 @@ public abstract class AbstractPolicyEnforcer {
return requiredScopes.isEmpty();
}
protected AuthzClient getAuthzClient() {
return this.authzClient;
}
protected PolicyEnforcerConfig getEnforcerConfig() {
return enforcerConfig;
}
protected PolicyEnforcer getPolicyEnforcer() {
return policyEnforcer;
}
private AuthorizationContext createEmptyAuthorizationContext(final boolean granted) {
return new ClientAuthorizationContext(authzClient) {
return new ClientAuthorizationContext(getAuthzClient()) {
@Override
public boolean hasPermission(String resourceName, String scopeName) {
return granted;
@ -279,7 +281,7 @@ public abstract class AbstractPolicyEnforcer {
}
private AuthorizationContext createAuthorizationContext(AccessToken accessToken, PathConfig pathConfig) {
return new ClientAuthorizationContext(accessToken, pathConfig, this.paths, authzClient);
return new ClientAuthorizationContext(accessToken, pathConfig, policyEnforcer.getPaths(), getAuthzClient());
}
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
@ -297,4 +299,8 @@ public abstract class AbstractPolicyEnforcer {
private boolean matchResourcePermission(PathConfig actualPathConfig, Permission permission) {
return permission.getResourceId().equals(actualPathConfig.getId());
}
private PathConfig getPathConfig(Request request) {
return isDefaultAccessDeniedUri(request) ? null : policyEnforcer.getPathMatcher().matches(getPath(request));
}
}

View file

@ -24,6 +24,8 @@ import java.util.concurrent.locks.LockSupport;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
/**
* A simple LRU cache implementation supporting expiration and maximum number of entries.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PathCache {
@ -39,15 +41,6 @@ public class PathCache {
private final long maxAge;
/**
* Creates a new instance.
*
* @param maxEntries the maximum number of entries to keep in the cache
*/
public PathCache(int maxEntries) {
this(maxEntries, -1);
}
/**
* Creates a new instance.
*
@ -80,6 +73,10 @@ public class PathCache {
}
}
public boolean containsKey(String uri) {
return cache.containsKey(uri);
}
public PathConfig get(String uri) {
if (parkForReadAndCheckInterrupt()) {
return null;

View file

@ -17,11 +17,11 @@
*/
package org.keycloak.adapters.authorization;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -34,13 +34,13 @@ import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.ClientAuthenticator;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ScopeRepresentation;
import org.keycloak.authorization.client.resource.ProtectedResource;
import org.keycloak.common.util.PathMatcher;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import org.keycloak.representations.idm.authorization.Permission;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@ -69,8 +69,9 @@ public class PolicyEnforcer {
}
}
});
this.pathMatcher = new PathMatcher(this.authzClient);
this.paths = configurePaths(this.authzClient.protection().resource(), this.enforcerConfig);
this.pathMatcher = createPathMatcher(authzClient);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialization complete. Path configurations:");
@ -104,11 +105,11 @@ public class PolicyEnforcer {
return context;
}
PolicyEnforcerConfig getEnforcerConfig() {
public PolicyEnforcerConfig getEnforcerConfig() {
return enforcerConfig;
}
AuthzClient getClient() {
public AuthzClient getClient() {
return authzClient;
}
@ -116,11 +117,11 @@ public class PolicyEnforcer {
return paths;
}
void addPath(PathConfig pathConfig) {
paths.put(pathConfig.getPath(), pathConfig);
public PathMatcher<PathConfig> getPathMatcher() {
return pathMatcher;
}
KeycloakDeployment getDeployment() {
public KeycloakDeployment getDeployment() {
return deployment;
}
@ -144,7 +145,7 @@ public class PolicyEnforcer {
}
private Map<String, PathConfig> configureDefinedPaths(ProtectedResource protectedResource, PolicyEnforcerConfig enforcerConfig) {
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
Map<String, PathConfig> paths = Collections.synchronizedMap(new LinkedHashMap<String, PathConfig>());
for (PathConfig pathConfig : enforcerConfig.getPaths()) {
ResourceRepresentation resource;
@ -168,36 +169,11 @@ public class PolicyEnforcer {
}
if (resource == null) {
if (enforcerConfig.isCreateResources()) {
LOGGER.debugf("Creating resource on server for path [%s].", pathConfig);
ResourceRepresentation representation = new ResourceRepresentation();
representation.setName(resourceName);
representation.setType(pathConfig.getType());
representation.setUri(path);
HashSet<ScopeRepresentation> scopes = new HashSet<>();
for (String scopeName : pathConfig.getScopes()) {
ScopeRepresentation scope = new ScopeRepresentation();
scope.setName(scopeName);
scopes.add(scope);
}
representation.setScopes(scopes);
ResourceRepresentation registrationResponse = protectedResource.create(representation);
pathConfig.setId(registrationResponse.getId());
} else {
throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration.");
}
} else {
pathConfig.setId(resource.getId());
throw new RuntimeException("Could not find matching resource on server with uri [" + path + "] or name [" + resourceName + "]. Make sure you have created a resource on the server that matches with the path configuration.");
}
pathConfig.setId(resource.getId());
PathConfig existingPath = null;
for (PathConfig current : paths.values()) {
@ -222,45 +198,85 @@ public class PolicyEnforcer {
LOGGER.info("Querying the server for all resources associated with this application.");
Map<String, PathConfig> paths = Collections.synchronizedMap(new HashMap<String, PathConfig>());
for (String id : protectedResource.findAll()) {
ResourceRepresentation resourceDescription = protectedResource.findById(id);
if (!enforcerConfig.getLazyLoadPaths()) {
for (String id : protectedResource.findAll()) {
ResourceRepresentation resourceDescription = protectedResource.findById(id);
if (resourceDescription.getUri() != null) {
PathConfig pathConfig = createPathConfig(resourceDescription);
paths.put(pathConfig.getPath(), pathConfig);
if (resourceDescription.getUri() != null) {
PathConfig pathConfig = PathConfig.createPathConfig(resourceDescription);
paths.put(pathConfig.getPath(), pathConfig);
}
}
}
return paths;
}
static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
PathConfig pathConfig = new PathConfig();
private PathMatcher<PathConfig> createPathMatcher(final AuthzClient authzClient) {
final PathCache pathCache = new PathCache(100, 30000);
pathConfig.setId(resourceDescription.getId());
pathConfig.setName(resourceDescription.getName());
return new PathMatcher<PathConfig>() {
@Override
public PathConfig matches(String targetUri) {
PathConfig pathConfig = pathCache.get(targetUri);
String uri = resourceDescription.getUri();
if (pathCache.containsKey(targetUri) || pathConfig != null) {
return pathConfig;
}
if (uri == null || "".equals(uri.trim())) {
throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
}
pathConfig = super.matches(targetUri);
pathConfig.setPath(uri);
if (enforcerConfig.getLazyLoadPaths() && (pathConfig == null || pathConfig.getPath().contains("*"))) {
try {
List<ResourceRepresentation> matchingResources = authzClient.protection().resource().findByMatchingUri(targetUri);
List<String> scopeNames = new ArrayList<>();
if (!matchingResources.isEmpty()) {
pathConfig = PathConfig.createPathConfig(matchingResources.get(0));
paths.put(pathConfig.getPath(), pathConfig);
}
} catch (Exception cause) {
LOGGER.errorf(cause, "Could not lazy load paths from server");
return null;
}
}
for (ScopeRepresentation scope : resourceDescription.getScopes()) {
scopeNames.add(scope.getName());
}
pathCache.put(targetUri, pathConfig);
pathConfig.setScopes(scopeNames);
pathConfig.setType(resourceDescription.getType());
return pathConfig;
}
return pathConfig;
}
@Override
protected String getPath(PathConfig entry) {
return entry.getPath();
}
public PathMatcher getPathMatcher() {
return pathMatcher;
@Override
protected Collection<PathConfig> getPaths() {
return paths.values();
}
@Override
protected PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
if (originalConfig.hasPattern()) {
ProtectedResource resource = authzClient.protection().resource();
List<ResourceRepresentation> search = resource.findByUri(path);
if (!search.isEmpty()) {
// resource does exist on the server, cache it
ResourceRepresentation targetResource = search.get(0);
PathConfig config = PathConfig.createPathConfig(targetResource);
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
config.setEnforcementMode(originalConfig.getEnforcementMode());
return config;
}
}
return null;
}
};
}
}

View file

@ -1,10 +0,0 @@
#!/bin/sh
export KC_AUTH_SERVER=http://localhost:8080/auth
export KC_REALM=master
export KC_CLIENT=cli
export KC_ACCESS_TOKEN=`java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar login`

View file

@ -1,9 +0,0 @@
#!/bin/sh
java -DKEYCLOAK_AUTH_SERVER=$KC_AUTH_SERVER -DKEYCLOAK_REALM=$KC_REALM -DKEYCLOAK_CLIENT=$KC_CLIENT -jar target/keycloak-cli-sso-3.3.0.CR1-SNAPSHOT.jar logout
unset KC_ACCESS_TOKEN

View file

@ -0,0 +1,702 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.adapters.installed;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Time;
import org.keycloak.jose.jwe.*;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.util.JsonSerialization;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.*;
import java.nio.file.Paths;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.*;
/**
* All kcinit commands that take input ask for
* <p>
* 1. . kcinit
* - setup and export KC_SESSION_KEY env var if not set.
* - checks to see if master token valid, refresh is possible, exit if token valid
* - performs command line login
* - stores master token for master client
* 2. app.sh is a wrapper for app cli.
* - token=`kcinit token app`
* - checks to see if token for app client has been fetched, refresh if valid, output token to sys.out if exists
* - if no token, login. Prompts go to stderr.
* - pass token as cmd line param to app or as environment variable.
* <p>
* 3. kcinit password {password}
* - outputs password key that is used for encryption.
* - can be used in .bashrc as export KC_SESSSION_KEY=`kcinit password {password}` or just set it in .bat file
* <p>
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KcinitDriver {
public static final String KC_SESSION_KEY = "KC_SESSION_KEY";
public static final String KC_LOGIN_CONFIG_PATH = "KC_LOGIN_CONFIG_PATH";
protected Map<String, String> config;
protected boolean debug = true;
protected static byte[] salt = new byte[]{-4, 88, 66, -101, 78, -94, 21, 105};
String[] args = null;
protected boolean forceLogin;
protected boolean browserLogin;
public void mainCmd(String[] args) throws Exception {
this.args = args;
if (args.length == 0) {
printHelp();
return;
}
if (args[0].equalsIgnoreCase("token")) {
//System.err.println("executing token");
token();
} else if (args[0].equalsIgnoreCase("login")) {
login();
} else if (args[0].equalsIgnoreCase("logout")) {
logout();
} else if (args[0].equalsIgnoreCase("env")) {
System.out.println(System.getenv().toString());
} else if (args[0].equalsIgnoreCase("install")) {
install();
} else if (args[0].equalsIgnoreCase("uninstall")) {
uninstall();
} else if (args[0].equalsIgnoreCase("password")) {
passwordKey();
} else {
KeycloakInstalled.console().writer().println("Unknown command: " + args[0]);
KeycloakInstalled.console().writer().println();
printHelp();
}
}
public String getHome() {
String home = System.getenv("HOME");
if (home == null) {
home = System.getProperty("HOME");
if (home == null) {
home = Paths.get("").toAbsolutePath().normalize().toString();
}
}
return home;
}
public void passwordKey() {
if (args.length < 2) {
printHelp();
System.exit(1);
}
String password = args[1];
try {
String encodedKey = generateEncryptionKey(password);
System.out.printf(encodedKey);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
protected String generateEncryptionKey(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
SecretKey tmp = factory.generateSecret(spec);
byte[] aeskey = tmp.getEncoded();
return Base64.encodeBytes(aeskey);
}
public JWE createJWE() {
String key = getEncryptionKey();
if (key == null) {
throw new RuntimeException(KC_SESSION_KEY + " env var not set");
}
byte[] aesKey = null;
try {
aesKey = Base64.decode(key.getBytes("UTF-8"));
} catch (IOException e) {
throw new RuntimeException("invalid " + KC_SESSION_KEY + "env var");
}
JWE jwe = new JWE();
final SecretKey aesSecret = new SecretKeySpec(aesKey, "AES");
jwe.getKeyStorage()
.setEncryptionKey(aesSecret);
return jwe;
}
protected String encryptionKey;
protected String getEncryptionKey() {
if (encryptionKey != null) return encryptionKey;
return System.getenv(KC_SESSION_KEY);
}
public String encrypt(String payload) {
JWE jwe = createJWE();
JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
try {
jwe.header(jweHeader).content(payload.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("cannot encode payload as UTF-8");
}
try {
return jwe.encodeJwe();
} catch (JWEException e) {
throw new RuntimeException("cannot encrypt payload", e);
}
}
public String decrypt(String encoded) {
JWE jwe = createJWE();
try {
jwe.verifyAndDecodeJwe(encoded);
byte[] content = jwe.getContent();
if (content == null) return null;
return new String(content, "UTF-8");
} catch (Exception ex) {
throw new RuntimeException("cannot decrypt payload", ex);
}
}
public static String getenv(String name, String defaultValue) {
String val = System.getenv(name);
return val == null ? defaultValue : val;
}
public File getConfigDirectory() {
return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit").toFile();
}
public File getConfigFile() {
return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "config.json").toFile();
}
public File getTokenFilePath(String client) {
return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "tokens", client).toFile();
}
public File getTokenDirectory() {
return Paths.get(getHome(), getenv(KC_LOGIN_CONFIG_PATH, ".keycloak"), "kcinit", "tokens").toFile();
}
protected boolean encrypted = false;
protected void checkEnv() {
File configFile = getConfigFile();
if (!configFile.exists()) {
KeycloakInstalled.console().writer().println("You have not configured kcinit. Please run 'kcinit install' to configure.");
System.exit(1);
}
byte[] data = new byte[0];
try {
data = readFileRaw(configFile);
} catch (IOException e) {
}
if (data == null) {
KeycloakInstalled.console().writer().println("Config file unreadable. Please run 'kcinit install' to configure.");
System.exit(1);
}
String encodedJwe = null;
try {
encodedJwe = new String(data, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (encodedJwe.contains("realm")) {
encrypted = false;
return;
} else {
encrypted = true;
}
if (System.getenv(KC_SESSION_KEY) == null) {
promptLocalPassword();
}
}
protected void promptLocalPassword() {
String password = KeycloakInstalled.console().passwordPrompt("Enter password to unlock kcinit config files: ");
try {
encryptionKey = generateEncryptionKey(password);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected String readFile(File fp) {
try {
byte[] data = readFileRaw(fp);
if (data == null) return null;
String file = new String(data, "UTF-8");
if (!encrypted) {
return file;
}
String decrypted = decrypt(file);
if (decrypted == null)
throw new RuntimeException("Unable to decrypt file. Did you set your local password correctly?");
return decrypted;
} catch (IOException e) {
throw new RuntimeException("failed to decrypt file: " + fp.getAbsolutePath() + " Did you set your local password correctly?", e);
}
}
protected byte[] readFileRaw(File fp) throws IOException {
if (!fp.exists()) return null;
FileInputStream fis = new FileInputStream(fp);
byte[] data = new byte[(int) fp.length()];
fis.read(data);
fis.close();
return data;
}
protected void writeFile(File fp, String payload) {
try {
String data = payload;
if (encrypted) data = encrypt(payload);
FileOutputStream fos = new FileOutputStream(fp);
fos.write(data.getBytes("UTF-8"));
fos.flush();
fos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void install() {
if (getEncryptionKey() == null) {
if (KeycloakInstalled.console().confirm("Do you want to protect tokens stored locally with a password? (y/n): ")) {
String password = "p";
String confirm = "c";
do {
password = KeycloakInstalled.console().passwordPrompt("Enter local password: ");
confirm = KeycloakInstalled.console().passwordPrompt("Confirm local password: ");
if (!password.equals(confirm)) {
KeycloakInstalled.console().writer().println();
KeycloakInstalled.console().writer().println("Confirmation does not match. Try again.");
KeycloakInstalled.console().writer().println();
}
} while (!password.equals(confirm));
try {
this.encrypted = true;
this.encryptionKey = generateEncryptionKey(password);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
}
} else {
if (!KeycloakInstalled.console().confirm("KC_SESSION_KEY env var already set. Do you want to use this as your local encryption key? (y/n): ")) {
KeycloakInstalled.console().writer().println("Unset KC_SESSION_KEY env var and run again");
System.exit(1);
}
this.encrypted = true;
this.encryptionKey = getEncryptionKey();
}
String server = KeycloakInstalled.console().readLine("Authentication server URL [http://localhost:8080/auth]: ").trim();
String realm = KeycloakInstalled.console().readLine("Name of realm [master]: ").trim();
String client = KeycloakInstalled.console().readLine("CLI client id [kcinit]: ").trim();
String secret = KeycloakInstalled.console().readLine("CLI client secret [none]: ").trim();
if (server.equals("")) {
server = "http://localhost:8080/auth";
}
if (realm.equals("")) {
realm = "master";
}
if (client.equals("")) {
client = "kcinit";
}
File configDir = getTokenDirectory();
configDir.mkdirs();
File configFile = getConfigFile();
Map<String, String> props = new HashMap<>();
props.put("server", server);
props.put("realm", realm);
props.put("client", client);
props.put("secret", secret);
try {
String json = JsonSerialization.writeValueAsString(props);
writeFile(configFile, json);
} catch (Exception e) {
e.printStackTrace();
}
KeycloakInstalled.console().writer().println();
KeycloakInstalled.console().writer().println("Installation complete!");
KeycloakInstalled.console().writer().println();
}
public void printHelp() {
KeycloakInstalled.console().writer().println("Commands:");
KeycloakInstalled.console().writer().println(" login [-f] -f forces login");
KeycloakInstalled.console().writer().println(" logout");
KeycloakInstalled.console().writer().println(" token [client] - print access token of desired client. Defaults to default master client. Will print either 'error', 'not-allowed', or 'login-required' on error.");
KeycloakInstalled.console().writer().println(" install - Install this utility. Will store in $HOME/.keycloak/kcinit unless " + KC_LOGIN_CONFIG_PATH + " env var is set");
System.exit(1);
}
public AdapterConfig getConfig() {
File configFile = getConfigFile();
if (!configFile.exists()) {
KeycloakInstalled.console().writer().println("You have not configured kcinit. Please run 'kcinit install' to configure.");
System.exit(1);
return null;
}
AdapterConfig config = new AdapterConfig();
config.setAuthServerUrl((String) getConfigProperties().get("server"));
config.setRealm((String) getConfigProperties().get("realm"));
config.setResource((String) getConfigProperties().get("client"));
config.setSslRequired("external");
String secret = (String) getConfigProperties().get("secret");
if (secret != null && !secret.trim().equals("")) {
Map<String, Object> creds = new HashMap<>();
creds.put("secret", secret);
config.setCredentials(creds);
} else {
config.setPublicClient(true);
}
return config;
}
private Map<String, String> getConfigProperties() {
if (this.config != null) return this.config;
if (!getConfigFile().exists()) {
KeycloakInstalled.console().writer().println();
KeycloakInstalled.console().writer().println(("Config file does not exist. Run kcinit install to set it up."));
System.exit(1);
}
String json = readFile(getConfigFile());
try {
Map map = JsonSerialization.readValue(json, Map.class);
config = (Map<String, String>) map;
} catch (IOException e) {
throw new RuntimeException(e);
}
return this.config;
}
public String readToken(String client) throws Exception {
String json = getTokenResponse(client);
if (json == null) return null;
if (json != null) {
try {
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
return tokenResponse.getToken();
}
AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
installed.refreshToken(tokenResponse.getRefreshToken());
processResponse(installed, client);
return tokenResponse.getToken();
} catch (Exception e) {
File tokenFile = getTokenFilePath(client);
if (tokenFile.exists()) {
tokenFile.delete();
}
return null;
}
}
return null;
}
public String readRefreshToken(String client) throws Exception {
String json = getTokenResponse(client);
if (json == null) return null;
if (json != null) {
try {
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
return tokenResponse.getRefreshToken();
} catch (Exception e) {
if (debug) {
e.printStackTrace();
}
File tokenFile = getTokenFilePath(client);
if (tokenFile.exists()) {
tokenFile.delete();
}
return null;
}
}
return null;
}
private String getTokenResponse(String client) throws IOException {
File tokenFile = getTokenFilePath(client);
try {
return readFile(tokenFile);
} catch (Exception e) {
if (debug) {
System.err.println("Failed to read encrypted file");
e.printStackTrace();
}
if (tokenFile.exists()) tokenFile.delete();
return null;
}
}
public void token() throws Exception {
KeycloakInstalled.console().stderrOutput();
checkEnv();
String masterClient = getMasterClient();
String client = masterClient;
if (args.length > 1) {
client = args[1];
}
//System.err.println("readToken: " + client);
String token = readToken(client);
if (token != null) {
System.out.print(token);
return;
}
if (token == null && client.equals(masterClient)) {
//System.err.println("not logged in, logging in.");
doConsoleLogin();
token = readToken(client);
if (token != null) {
System.out.print(token);
return;
}
}
String masterToken = readToken(masterClient);
if (masterToken == null) {
//System.err.println("not logged in, logging in.");
doConsoleLogin();
masterToken = readToken(masterClient);
if (masterToken == null) {
System.err.println("Login failed. Cannot retrieve token");
System.exit(1);
}
}
//System.err.println("exchange: " + client);
Client httpClient = getHttpClient();
WebTarget exchangeUrl = httpClient.target(getServer())
.path("/realms")
.path(getRealm())
.path("protocol/openid-connect/token");
Form form = new Form()
.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE)
.param(OAuth2Constants.CLIENT_ID, masterClient)
.param(OAuth2Constants.SUBJECT_TOKEN, masterToken)
.param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE)
.param(OAuth2Constants.REQUESTED_TOKEN_TYPE, OAuth2Constants.REFRESH_TOKEN_TYPE)
.param(OAuth2Constants.AUDIENCE, client);
if (getMasterClientSecret() != null) {
form.param(OAuth2Constants.CLIENT_SECRET, getMasterClientSecret());
}
Response response = exchangeUrl.request().post(Entity.form(
form
));
if (response.getStatus() == 401 || response.getStatus() == 403) {
response.close();
System.err.println("Not allowed to exchange for client token");
System.exit(1);
}
if (response.getStatus() != 200) {
if (response.getMediaType() != null && response.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
try {
String json = response.readEntity(String.class);
OAuth2ErrorRepresentation error = JsonSerialization.readValue(json, OAuth2ErrorRepresentation.class);
System.err.println("Failed to exchange token: " + error.getError() + ". " + error.getErrorDescription());
System.exit(1);
} catch (Exception ignore) {
ignore.printStackTrace();
}
}
response.close();
System.err.println("Unknown error exchanging for client token: " + response.getStatus());
System.exit(1);
}
String json = response.readEntity(String.class);
response.close();
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
if (tokenResponse.getToken() != null) {
getTokenDirectory().mkdirs();
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
tokenResponse.setIdToken(null);
json = JsonSerialization.writeValueAsString(tokenResponse);
writeFile(getTokenFilePath(client), json);
System.out.printf(tokenResponse.getToken());
} else {
System.err.println("Error processing token");
System.exit(1);
}
}
protected String getMasterClientSecret() {
return getProperty("secret");
}
protected String getServer() {
return getProperty("server");
}
protected String getRealm() {
return getProperty("realm");
}
public String getProperty(String name) {
return (String) getConfigProperties().get(name);
}
protected boolean forceLogin() {
return args.length > 0 && args[0].equals("-f");
}
public Client getHttpClient() {
return new ResteasyClientBuilder().disableTrustManager().build();
}
public void login() throws Exception {
checkEnv();
this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
for (String arg : args) {
if (arg.equals("-f") || arg.equals("-force")) {
forceLogin = true;
this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
} else if (arg.equals("-browser") || arg.equals("-b")) {
browserLogin = true;
this.args = Arrays.copyOfRange(this.args, 1, this.args.length);
} else {
System.err.println("Illegal argument: " + arg);
printHelp();
System.exit(1);
}
}
String masterClient = getMasterClient();
if (!forceLogin && readToken(masterClient) != null) {
KeycloakInstalled.console().writer().println("Already logged in. `kcinit -f` to force relogin");
return;
}
doConsoleLogin();
KeycloakInstalled.console().writer().println("Login successful!");
}
public void doConsoleLogin() throws Exception {
String masterClient = getMasterClient();
AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
//System.err.println("calling loginCommandLine");
if (!installed.loginCommandLine()) {
System.exit(1);
}
processResponse(installed, masterClient);
}
private String getMasterClient() {
return getProperty("client");
}
private void processResponse(KeycloakInstalled installed, String client) throws IOException {
AccessTokenResponse tokenResponse = installed.getTokenResponse();
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
tokenResponse.setIdToken(null);
String json = JsonSerialization.writeValueAsString(tokenResponse);
getTokenDirectory().mkdirs();
writeFile(getTokenFilePath(client), json);
}
public void logout() throws Exception {
String token = readRefreshToken(getMasterClient());
if (token != null) {
try {
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getConfig());
ServerRequest.invokeLogout(deployment, token);
} catch (Exception e) {
if (debug) {
e.printStackTrace();
}
}
}
if (getTokenDirectory().exists()) {
for (File fp : getTokenDirectory().listFiles()) fp.delete();
}
}
public void uninstall() throws Exception {
File configFile = getConfigFile();
if (configFile.exists()) configFile.delete();
if (getTokenDirectory().exists()) {
for (File fp : getTokenDirectory().listFiles()) fp.delete();
}
}
}

View file

@ -1,281 +0,0 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.adapters.installed;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest;
import org.keycloak.common.util.Time;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.JsonSerialization;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakCliSso {
public void mainCmd(String[] args) throws Exception {
if (args.length != 1) {
printHelp();
return;
}
if (args[0].equalsIgnoreCase("login")) {
login();
} else if (args[0].equalsIgnoreCase("login-manual")) {
loginManual();
}
/*
else if (args[0].equalsIgnoreCase("login-cli")) {
loginCli();
}
*/
else if (args[0].equalsIgnoreCase("token")) {
token();
} else if (args[0].equalsIgnoreCase("logout")) {
logout();
} else if (args[0].equalsIgnoreCase("env")) {
System.out.println(System.getenv().toString());
} else {
printHelp();
}
}
public void printHelp() {
System.err.println("Commands:");
System.err.println(" login - login with desktop browser if available, otherwise do manual login. Output is access token.");
System.err.println(" login-manual - manual login");
//System.err.println(" login-cli - attempt Keycloak proprietary cli protocol. Otherwise do normal login");
System.err.println(" token - print access token if logged in");
System.err.println(" logout - logout.");
System.exit(1);
}
public AdapterConfig getConfig() {
String url = System.getProperty("KEYCLOAK_AUTH_SERVER");
if (url == null) {
System.err.println("KEYCLOAK_AUTH_SERVER property not set");
System.exit(1);
}
String realm = System.getProperty("KEYCLOAK_REALM");
if (realm == null) {
System.err.println("KEYCLOAK_REALM property not set");
System.exit(1);
}
String client = System.getProperty("KEYCLOAK_CLIENT");
if (client == null) {
System.err.println("KEYCLOAK_CLIENT property not set");
System.exit(1);
}
String secret = System.getProperty("KEYCLOAK_CLIENT_SECRET");
AdapterConfig config = new AdapterConfig();
config.setAuthServerUrl(url);
config.setRealm(realm);
config.setResource(client);
config.setSslRequired("external");
if (secret != null) {
Map<String, Object> creds = new HashMap<>();
creds.put("secret", secret);
config.setCredentials(creds);
} else {
config.setPublicClient(true);
}
return config;
}
public boolean checkToken(boolean outputToken) throws Exception {
String token = getTokenResponse();
if (token == null) return false;
if (token != null) {
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
if (m.find()) {
String json = m.group(0);
try {
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
return true;
}
AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
installed.refreshToken(tokenResponse.getRefreshToken());
processResponse(installed, outputToken);
return true;
} catch (Exception e) {
System.err.println("Error processing existing token");
e.printStackTrace();
}
}
}
return false;
}
private String getTokenResponse() throws IOException {
String token = null;
File tokenFile = getTokenFilePath();
if (tokenFile.exists()) {
FileInputStream fis = new FileInputStream(tokenFile);
byte[] data = new byte[(int) tokenFile.length()];
fis.read(data);
fis.close();
token = new String(data, "UTF-8");
}
return token;
}
public void token() throws Exception {
String token = getTokenResponse();
if (token == null) {
System.err.println("There is no token for client");
System.exit(1);
} else {
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
if (m.find()) {
String json = m.group(0);
try {
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
if (Time.currentTime() < tokenResponse.getExpiresIn()) {
System.out.println(tokenResponse.getToken());
return;
} else {
System.err.println("token in response file is expired");
System.exit(1);
}
} catch (Exception e) {
System.err.println("Failure processing token response file");
e.printStackTrace();
System.exit(1);
}
} else {
System.err.println("Could not find json within token response file");
System.exit(1);
}
}
}
public void login() throws Exception {
if (checkToken(true)) return;
AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
installed.login();
processResponse(installed, true);
}
public void loginCli() throws Exception {
if (checkToken(false)) return;
AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
if (!installed.loginCommandLine()) installed.login();
processResponse(installed, false);
}
public String getHome() {
String home = System.getenv("HOME");
if (home == null) {
home = System.getProperty("HOME");
if (home == null) {
home = Paths.get("").toAbsolutePath().normalize().toString();
}
}
return home;
}
public File getTokenDirectory() {
return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM")).toFile();
}
public File getTokenFilePath() {
return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile();
}
private void processResponse(KeycloakInstalled installed, boolean outputToken) throws IOException {
AccessTokenResponse tokenResponse = installed.getTokenResponse();
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
tokenResponse.setIdToken(null);
String output = JsonSerialization.writeValueAsString(tokenResponse);
getTokenDirectory().mkdirs();
FileOutputStream fos = new FileOutputStream(getTokenFilePath());
fos.write(output.getBytes("UTF-8"));
fos.flush();
fos.close();
if (outputToken) System.out.println(tokenResponse.getToken());
}
public void loginManual() throws Exception {
if (checkToken(true)) return;
AdapterConfig config = getConfig();
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
KeycloakInstalled installed = new KeycloakInstalled(deployment);
installed.loginManual();
processResponse(installed, true);
}
public void logout() throws Exception {
String token = getTokenResponse();
if (token != null) {
Matcher m = Pattern.compile("\\{.*\\}\\z").matcher(token);
if (m.find()) {
String json = m.group(0);
try {
AccessTokenResponse tokenResponse = JsonSerialization.readValue(json, AccessTokenResponse.class);
if (Time.currentTime() > tokenResponse.getExpiresIn()) {
System.err.println("Login is expired");
System.exit(1);
}
AdapterConfig config = getConfig();
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
ServerRequest.invokeLogout(deployment, tokenResponse.getRefreshToken());
for (File fp : getTokenDirectory().listFiles()) fp.delete();
System.out.println("logout complete");
} catch (Exception e) {
System.err.println("Failure processing token response file");
e.printStackTrace();
System.exit(1);
}
} else {
System.err.println("Could not find json within token response file");
System.exit(1);
}
} else {
System.err.println("Not logged in");
System.exit(1);
}
}
}

View file

@ -39,15 +39,7 @@ import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.awt.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.PushbackInputStream;
import java.io.Reader;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
@ -65,6 +57,7 @@ public class KeycloakInstalled {
public interface HttpResponseWriter {
void success(PrintWriter pw, KeycloakInstalled ki);
void failure(PrintWriter pw, KeycloakInstalled ki);
}
@ -86,12 +79,12 @@ public class KeycloakInstalled {
private Locale locale;
private HttpResponseWriter loginResponseWriter;
private HttpResponseWriter logoutResponseWriter;
private ResteasyClient resteasyClient;
Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
Pattern codePattern = Pattern.compile("code=([^&]+)");
public KeycloakInstalled() {
InputStream config = Thread.currentThread().getContextClassLoader().getResourceAsStream(KEYCLOAK_JSON);
deployment = KeycloakDeploymentBuilder.build(config);
@ -179,6 +172,10 @@ public class KeycloakInstalled {
this.logoutResponseWriter = logoutResponseWriter;
}
public void setResteasyClient(ResteasyClient resteasyClient) {
this.resteasyClient = resteasyClient;
}
public Locale getLocale() {
return locale;
}
@ -302,6 +299,139 @@ public class KeycloakInstalled {
status = Status.LOGGED_MANUAL;
}
public static class Console {
protected java.io.Console console = System.console();
protected PrintWriter writer;
protected BufferedReader reader;
static Console SINGLETON = new Console();
private Console() {
}
public PrintWriter writer() {
if (console == null) {
if (writer == null) {
writer = new PrintWriter(System.err, true);
}
return writer;
}
return console.writer();
}
public Reader reader() {
if (console == null) {
return getReader();
}
return console.reader();
}
protected BufferedReader getReader() {
if (reader != null) return reader;
reader = new BufferedReader(new BufferedReader(new InputStreamReader(System.in)));
return reader;
}
public Console format(String fmt, Object... args) {
if (console == null) {
writer().format(fmt, args);
return this;
}
console.format(fmt, args);
return this;
}
public Console printf(String format, Object... args) {
if (console == null) {
writer().printf(format, args);
return this;
}
console.printf(format, args);
return this;
}
public String readLine(String fmt, Object... args) {
if (console == null) {
format(fmt, args);
return readLine();
}
return console.readLine(fmt, args);
}
public boolean confirm(String fmt, Object... args) {
String prompt = "";
while (!"y".equals(prompt) && !"n".equals(prompt)) {
prompt = readLine(fmt, args);
}
return "y".equals(prompt);
}
public String prompt(String fmt, Object... args) {
String prompt = "";
while (prompt.equals("")) {
prompt = readLine(fmt, args).trim();
}
return prompt;
}
public String passwordPrompt(String fmt, Object... args) {
String prompt = "";
while (prompt.equals("")) {
char[] val = readPassword(fmt, args);
prompt = new String(val);
prompt = prompt.trim();
}
return prompt;
}
public String readLine() {
if (console == null) {
try {
return getReader().readLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return console.readLine();
}
public char[] readPassword(String fmt, Object... args) {
if (console == null) {
return readLine(fmt, args).toCharArray();
}
return console.readPassword(fmt, args);
}
public char[] readPassword() {
if (console == null) {
return readLine().toCharArray();
}
return console.readPassword();
}
public void flush() {
if (console == null) {
System.err.flush();
return;
}
console.flush();
}
public void stderrOutput() {
//System.err.println("not using System.console()");
console = null;
}
}
public static Console console() {
return Console.SINGLETON;
}
public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
@ -309,7 +439,6 @@ public class KeycloakInstalled {
}
/**
* Experimental proprietary WWW-Authentication challenge protocol.
* WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
@ -325,62 +454,116 @@ public class KeycloakInstalled {
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
.queryParam("display", "console")
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
.build().toString();
ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
ResteasyClient client = createResteasyClient();
try {
//System.err.println("initial request");
Response response = client.target(authUrl).request().get();
if (response.getStatus() != 401) {
return false;
}
while (true) {
String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
if (authenticationHeader == null) {
return false;
}
if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
return false;
}
if (response.getMediaType() != null) {
String splash = response.readEntity(String.class);
System.console().writer().println(splash);
}
Matcher m = callbackPattern.matcher(authenticationHeader);
if (!m.find()) return false;
String callback = m.group(1);
//System.err.println("callback: " + callback);
m = paramPattern.matcher(authenticationHeader);
Form form = new Form();
while (m.find()) {
String param = m.group(1);
String label = m.group(2);
String mask = m.group(3).trim();
boolean maskInput = mask.equals("true");
String value = null;
if (maskInput) {
char[] txt = System.console().readPassword(label);
value = new String(txt);
if (response.getStatus() == 403) {
if (response.getMediaType() != null) {
String splash = response.readEntity(String.class);
console().writer().println(splash);
} else {
value = System.console().readLine(label);
System.err.println("Forbidden to login");
}
form.param(param, value);
return false;
} else if (response.getStatus() == 401) {
String authenticationHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
if (authenticationHeader == null) {
System.err.println("Failure: Invalid protocol. No WWW-Authenticate header");
return false;
}
//System.err.println("got header: " + authenticationHeader);
if (!authenticationHeader.contains("X-Text-Form-Challenge")) {
System.err.println("Failure: Invalid WWW-Authenticate header.");
return false;
}
if (response.getMediaType() != null) {
String splash = response.readEntity(String.class);
console().writer().println(splash);
} else {
response.close();
}
Matcher m = callbackPattern.matcher(authenticationHeader);
if (!m.find()) {
System.err.println("Failure: Invalid WWW-Authenticate header.");
return false;
}
String callback = m.group(1);
//System.err.println("callback: " + callback);
m = paramPattern.matcher(authenticationHeader);
Form form = new Form();
while (m.find()) {
String param = m.group(1);
String label = m.group(2);
String mask = m.group(3).trim();
boolean maskInput = mask.equals("true");
String value = null;
if (maskInput) {
char[] txt = console().readPassword(label);
value = new String(txt);
} else {
value = console().readLine(label);
}
form.param(param, value);
}
response.close();
client.close();
client = createResteasyClient();
response = client.target(callback).request().post(Entity.form(form));
} else if (response.getStatus() == 302) {
int redirectCount = 0;
do {
String location = response.getLocation().toString();
Matcher m = codePattern.matcher(location);
if (!m.find()) {
response.close();
client.close();
client = createResteasyClient();
response = client.target(location).request().get();
} else {
response.close();
client.close();
String code = m.group(1);
processCode(code, redirectUri);
return true;
}
if (response.getStatus() == 302 && redirectCount++ > 4) {
System.err.println("Too many redirects. Aborting");
return false;
}
} while (response.getStatus() == 302);
} else {
System.err.println("Unknown response from server: " + response.getStatus());
return false;
}
response = client.target(callback).request().post(Entity.form(form));
if (response.getStatus() == 401) continue;
if (response.getStatus() != 302) return false;
String location = response.getLocation().toString();
m = codePattern.matcher(location);
if (!m.find()) return false;
String code = m.group(1);
processCode(code, redirectUri);
return true;
}
} catch (Exception ex) {
throw ex;
} finally {
client.close();
}
}
protected ResteasyClient getResteasyClient() {
if (this.resteasyClient == null) {
this.resteasyClient = createResteasyClient();
}
return this.resteasyClient;
}
protected ResteasyClient createResteasyClient() {
return new ResteasyClientBuilder()
.connectionCheckoutTimeout(1, TimeUnit.HOURS)
.connectionTTL(1, TimeUnit.HOURS)
.socketTimeout(1, TimeUnit.HOURS)
.disableTrustManager().build();
}
public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
return tokenString;
@ -400,7 +583,7 @@ public class KeycloakInstalled {
parseAccessToken(tokenResponse);
}
public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
public void refreshToken(String refreshToken) throws IOException, ServerRequest.HttpFailure, VerificationException {
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
parseAccessToken(tokenResponse);
@ -452,7 +635,6 @@ public class KeycloakInstalled {
}
private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
parseAccessToken(tokenResponse);
@ -474,86 +656,6 @@ public class KeycloakInstalled {
return sb.toString();
}
public static class MaskingThread extends Thread {
private volatile boolean stop;
private char echochar = '*';
public MaskingThread() {
}
/**
* Begin masking until asked to stop.
*/
public void run() {
int priority = Thread.currentThread().getPriority();
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
try {
stop = true;
while(stop) {
System.out.print("\010" + echochar);
try {
// attempt masking at this rate
Thread.currentThread().sleep(1);
}catch (InterruptedException iex) {
Thread.currentThread().interrupt();
return;
}
}
} finally { // restore the original priority
Thread.currentThread().setPriority(priority);
}
}
/**
* Instruct the thread to stop masking.
*/
public void stopMasking() {
this.stop = false;
}
}
public static String readMasked(Reader reader) {
MaskingThread et = new MaskingThread();
Thread mask = new Thread(et);
mask.start();
BufferedReader in = new BufferedReader(reader);
String password = "";
try {
password = in.readLine();
} catch (IOException ioe) {
ioe.printStackTrace();
}
// stop masking
et.stopMasking();
// return the password entered by the user
return password;
}
private String readLine(Reader reader, boolean mask) throws IOException {
if (mask) {
System.out.print(" ");
return readMasked(reader);
}
StringBuilder sb = new StringBuilder();
char cb[] = new char[1];
while (reader.read(cb) != -1) {
char c = cb[0];
if ((c == '\n') || (c == '\r')) {
break;
} else {
sb.append(c);
}
}
return sb.toString();
}
public class CallbackListener extends Thread {

View file

@ -26,7 +26,7 @@
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-cli-sso</artifactId>
<artifactId>kcinit</artifactId>
<name>Keycloak CLI SSO Framework</name>
<description/>
@ -54,7 +54,7 @@
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.keycloak.adapters.KeycloakCliSsoMain</mainClass>
<mainClass>org.keycloak.adapters.KcinitMain</mainClass>
</transformer>
</transformers>

View file

@ -0,0 +1,26 @@
#!/bin/bash
case "`uname`" in
CYGWIN*)
CFILE = `cygpath "$0"`
RESOLVED_NAME=`readlink -f "$CFILE"`
;;
Darwin*)
RESOLVED_NAME=`readlink "$0"`
;;
FreeBSD)
RESOLVED_NAME=`readlink -f "$0"`
;;
Linux)
RESOLVED_NAME=`readlink -f "$0"`
;;
esac
if [ "x$RESOLVED_NAME" = "x" ]; then
RESOLVED_NAME="$0"
fi
SCRIPTPATH=`dirname "$RESOLVED_NAME"`
JAR=$SCRIPTPATH/kcinit-${project.version}.jar
java -jar $JAR $@

View file

@ -0,0 +1,8 @@
@echo off
if "%OS%" == "Windows_NT" (
set "DIRNAME=%~dp0%"
) else (
set DIRNAME=.\
)
java -jar %DIRNAME%\kcinit-${project.version}.jar %*

View file

@ -16,30 +16,15 @@
*/
package org.keycloak.adapters;
import org.keycloak.adapters.installed.KeycloakCliSso;
import org.keycloak.adapters.installed.KeycloakInstalled;
import org.keycloak.common.util.Time;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.util.JsonSerialization;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.keycloak.adapters.installed.KcinitDriver;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakCliSsoMain extends KeycloakCliSso {
public class KcinitMain extends KcinitDriver {
public static void main(String[] args) throws Exception {
new KeycloakCliSsoMain().mainCmd(args);
new KcinitMain().mainCmd(args);
}
}

View file

@ -34,7 +34,7 @@
<module>adapter-core</module>
<module>as7-eap6</module>
<module>installed</module>
<module>cli-sso</module>
<module>kcinit</module>
<module>jaxrs-oauth-client</module>
<module>jetty</module>
<module>js</module>

View file

@ -1,218 +0,0 @@
/*
* JBoss, Home of Professional Open Source
*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* 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 org.keycloak.authorization.client.representation;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* <p>One or more resources that the resource server manages as a set of protected resources.
*
* <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.2">OAuth-resource-reg</a>.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ResourceRepresentation {
@JsonProperty("_id")
private String id;
private String name;
private String displayName;
private String uri;
private String type;
@JsonProperty("resource_scopes")
private Set<ScopeRepresentation> scopes;
@JsonProperty("icon_uri")
private String iconUri;
private String owner;
private Boolean ownerManagedAccess;
private Map<String, List<String>> attributes;
/**
* Creates a new instance.
*
* @param name a human-readable string describing a set of one or more resources
* @param uri a {@link URI} that provides the network location for the resource set being registered
* @param type a string uniquely identifying the semantics of the resource set
* @param scopes the available scopes for this resource set
* @param iconUri a {@link URI} for a graphic icon representing the resource set
*/
public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type, String iconUri) {
this.name = name;
this.scopes = scopes;
this.uri = uri;
this.type = type;
this.iconUri = iconUri;
}
/**
* Creates a new instance.
*
* @param name a human-readable string describing a set of one or more resources
* @param uri a {@link URI} that provides the network location for the resource set being registered
* @param type a string uniquely identifying the semantics of the resource set
* @param scopes the available scopes for this resource set
*/
public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type) {
this(name, scopes, uri, type, null);
}
/**
* Creates a new instance.
*
* @param name a human-readable string describing a set of one or more resources
* @param serverUri a {@link URI} that identifies this resource server
* @param scopes the available scopes for this resource set
*/
public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes) {
this(name, scopes, null, null, null);
}
/**
* Creates a new instance.
*
*/
public ResourceRepresentation() {
this(null, null, null, null, null);
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getDisplayName() {
return displayName;
}
public String getUri() {
return this.uri;
}
public String getType() {
return this.type;
}
public Set<ScopeRepresentation> getScopes() {
if (this.scopes == null) {
this.scopes = Collections.emptySet();
}
return Collections.unmodifiableSet(this.scopes);
}
public String getIconUri() {
return this.iconUri;
}
public void setName(String name) {
this.name = name;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public void setUri(String uri) {
this.uri = uri;
}
public void setType(String type) {
this.type = type;
}
public void setScopes(Set<ScopeRepresentation> scopes) {
this.scopes = scopes;
}
public void setIconUri(String iconUri) {
this.iconUri = iconUri;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public void setOwnerManagedAccess(Boolean ownerManagedAccess) {
this.ownerManagedAccess = ownerManagedAccess;
}
public Boolean getOwnerManagedAccess() {
return ownerManagedAccess;
}
public void addScope(ScopeRepresentation scopeRepresentation) {
if (this.scopes == null) {
this.scopes = new HashSet<>();
}
this.scopes.add(scopeRepresentation);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ResourceRepresentation that = (ResourceRepresentation) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "ResourceRepresentation{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", uri='" + uri + '\'' +
", type='" + type + '\'' +
", owner='" + owner + '\'' +
", scopes=" + scopes +
'}';
}
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
public Map<String, List<String>> getAttributes() {
return attributes;
}
}

View file

@ -1,98 +0,0 @@
/*
* JBoss, Home of Professional Open Source
*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* 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 org.keycloak.authorization.client.representation;
import java.net.URI;
import java.util.Objects;
/**
* <p>A bounded extent of access that is possible to perform on a resource set. In authorization policy terminology,
* a scope is one of the potentially many "verbs" that can logically apply to a resource set ("object").
*
* <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.1">OAuth-resource-reg</a>.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class ScopeRepresentation {
private String id;
private String name;
private String iconUri;
/**
* Creates an instance.
*
* @param name the a human-readable string describing some scope (extent) of access
* @param iconUri a {@link URI} for a graphic icon representing the scope
*/
public ScopeRepresentation(String name, String iconUri) {
this.name = name;
this.iconUri = iconUri;
}
/**
* Creates an instance.
*
* @param name the a human-readable string describing some scope (extent) of access
*/
public ScopeRepresentation(String name) {
this(name, null);
}
/**
* Creates an instance.
*/
public ScopeRepresentation() {
this(null, null);
}
public String getName() {
return this.name;
}
public String getIconUri() {
return this.iconUri;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ScopeRepresentation scope = (ScopeRepresentation) o;
return Objects.equals(getName(), scope.getName());
}
public int hashCode() {
return Objects.hash(getName());
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setIconUri(String iconUri) {
this.iconUri = iconUri;
}
}

View file

@ -23,11 +23,11 @@ import java.util.List;
import java.util.concurrent.Callable;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.representation.ServerConfiguration;
import org.keycloak.authorization.client.util.Http;
import org.keycloak.authorization.client.util.Throwables;
import org.keycloak.authorization.client.util.TokenCallable;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.util.JsonSerialization;
/**
@ -124,11 +124,11 @@ public class ProtectedResource {
/**
* Query the server for a resource given its <code>name</code> where the owner is the resource server itself.
*
* @param id the resource name
* @param name the resource name
* @return a {@link ResourceRepresentation}
*/
public ResourceRepresentation findByName(String name) {
String[] representations = find(null, name, null, configuration.getResource(), null, null, null, null);
String[] representations = find(null, name, null, configuration.getResource(), null, null, false, null, null);
if (representations.length == 0) {
return null;
@ -145,7 +145,7 @@ public class ProtectedResource {
* @return a {@link ResourceRepresentation}
*/
public ResourceRepresentation findByName(String name, String ownerId) {
String[] representations = find(null, name, null, ownerId, null, null, null, null);
String[] representations = find(null, name, null, ownerId, null, null, false, null, null);
if (representations.length == 0) {
return null;
@ -163,11 +163,12 @@ public class ProtectedResource {
* @param owner the resource owner
* @param type the resource type
* @param scope the resource scope
* @param matchingUri the resource uri. Use this parameter to lookup a resource that best match the given uri
* @param firstResult the position of the first resource to retrieve
* @param maxResult the maximum number of resources to retrieve
* @return an array of strings with the resource ids
*/
public String[] find(final String id, final String name, final String uri, final String owner, final String type, final String scope, final Integer firstResult, final Integer maxResult) {
public String[] find(final String id, final String name, final String uri, final String owner, final String type, final String scope, final boolean matchingUri, final Integer firstResult, final Integer maxResult) {
Callable<String[]> callable = new Callable<String[]>() {
@Override
public String[] call() throws Exception {
@ -179,6 +180,7 @@ public class ProtectedResource {
.param("owner", owner)
.param("type", type)
.param("scope", scope)
.param("matchingUri", Boolean.valueOf(matchingUri).toString())
.param("deep", Boolean.FALSE.toString())
.param("first", firstResult != null ? firstResult.toString() : null)
.param("max", maxResult != null ? maxResult.toString() : null)
@ -199,7 +201,7 @@ public class ProtectedResource {
*/
public String[] findAll() {
try {
return find(null,null , null, null, null, null, null, null);
return find(null,null , null, null, null, null, false, null, null);
} catch (Exception cause) {
throw Throwables.handleWrapException("Could not find resource", cause);
}
@ -233,7 +235,30 @@ public class ProtectedResource {
* @param uri the resource uri
*/
public List<ResourceRepresentation> findByUri(String uri) {
String[] ids = find(null, null, uri, null, null, null, null, null);
String[] ids = find(null, null, uri, null, null, null, false, null, null);
if (ids.length == 0) {
return Collections.emptyList();
}
List<ResourceRepresentation> representations = new ArrayList<>();
for (String id : ids) {
representations.add(findById(id));
}
return representations;
}
/**
* Returns a list of resources that best matches the given {@code uri}. This method queries the server for resources whose
* {@link ResourceRepresentation#uri} best matches the given {@code uri}.
*
* @param uri the resource uri to match
* @return a list of resources
*/
public List<ResourceRepresentation> findByMatchingUri(String uri) {
String[] ids = find(null, null, uri, null, null, null, true, null, null);
if (ids.length == 0) {
return Collections.emptyList();

View file

@ -1,57 +1,44 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
* Copyright 2018 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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
* 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.
* 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 org.keycloak.adapters.authorization;
package org.keycloak.common.util;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.representation.ResourceRepresentation;
import org.keycloak.authorization.client.resource.ProtectedResource;
import org.keycloak.representations.adapters.config.PolicyEnforcerConfig.PathConfig;
import java.util.Collection;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
class PathMatcher {
public abstract class PathMatcher<P> {
private static final char WILDCARD = '*';
private final AuthzClient authzClient;
// TODO: make this configurable
private PathCache cache = new PathCache(100, 30000);
public PathMatcher(AuthzClient authzClient) {
this.authzClient = authzClient;
}
public P matches(final String targetUri) {
int patternCount = 0;
P matchingPath = null;
P matchingAnyPath = null;
P matchingAnySuffixPath = null;
public PathConfig matches(final String targetUri, Map<String, PathConfig> paths) {
PathConfig pathConfig = paths.get(targetUri) == null ? cache.get(targetUri) : paths.get(targetUri);
for (P entry : getPaths()) {
String expectedUri = getPath(entry);
if (pathConfig != null) {
return pathConfig;
}
if (expectedUri == null) {
continue;
}
PathConfig matchingAnyPath = null;
PathConfig matchingAnySuffixPath = null;
for (PathConfig entry : paths.values()) {
String expectedUri = entry.getPath();
String matchingUri = null;
if (exactMatch(expectedUri, targetUri, expectedUri)) {
@ -62,9 +49,17 @@ class PathMatcher {
String templateUri = buildUriFromTemplate(expectedUri, targetUri);
if (templateUri != null) {
if (exactMatch(expectedUri, targetUri, templateUri)) {
int length = expectedUri.split("\\/").length;
if (exactMatch(expectedUri, targetUri, templateUri) && (patternCount == 0 || length > patternCount)) {
matchingUri = templateUri;
entry = resolvePathConfig(entry, targetUri);
P resolved = resolvePathConfig(entry, targetUri);
if (resolved != null) {
entry = resolved;
}
patternCount = length;
}
}
}
@ -90,12 +85,15 @@ class PathMatcher {
}
if (matchingUri.equals(targetUri) || pathString.equals(targetUri)) {
cache.put(targetUri, entry);
return entry;
if (patternCount == 0) {
return entry;
} else {
matchingPath = entry;
}
}
if (WILDCARD == expectedUri.charAt(expectedUri.length() - 1)) {
if (matchingAnyPath == null || matchingAnyPath.getPath().length() < matchingUri.length()) {
if (matchingAnyPath == null || getPath(matchingAnyPath).length() < matchingUri.length()) {
matchingAnyPath = entry;
}
} else {
@ -112,18 +110,21 @@ class PathMatcher {
}
}
if (matchingAnySuffixPath != null) {
cache.put(targetUri, matchingAnySuffixPath);
return matchingAnySuffixPath;
if (matchingPath != null) {
return matchingPath;
}
if (matchingAnyPath != null) {
cache.put(targetUri, matchingAnyPath);
if (matchingAnySuffixPath != null) {
return matchingAnySuffixPath;
}
return matchingAnyPath;
}
protected abstract String getPath(P entry);
protected abstract Collection<P> getPaths();
private boolean exactMatch(String expectedUri, String targetUri, String value) {
if (targetUri.equals(value)) {
return value.equals(targetUri);
@ -213,32 +214,16 @@ class PathMatcher {
}
public boolean endsWithWildcard(String expectedUri) {
return WILDCARD == expectedUri.charAt(expectedUri.length() - 1);
int length = expectedUri.length();
return length > 0 && WILDCARD == expectedUri.charAt(length - 1);
}
private boolean isTemplate(String uri) {
return uri.indexOf("{") != -1;
}
private PathConfig resolvePathConfig(PathConfig originalConfig, String path) {
if (originalConfig.hasPattern()) {
ProtectedResource resource = this.authzClient.protection().resource();
List<ResourceRepresentation> search = resource.findByUri(path);
if (!search.isEmpty()) {
// resource does exist on the server, cache it
ResourceRepresentation targetResource = search.get(0);
PathConfig config = PolicyEnforcer.createPathConfig(targetResource);
config.setScopes(originalConfig.getScopes());
config.setMethods(originalConfig.getMethods());
config.setParentConfig(originalConfig);
config.setEnforcementMode(originalConfig.getEnforcementMode());
return config;
}
}
return originalConfig;
protected P resolvePathConfig(P entry, String path) {
return entry;
}
}

View file

@ -0,0 +1,66 @@
package org.keycloak.common.util;
import java.security.SecureRandom;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
public class RandomString {
/**
* Generate a random string.
*/
public String nextString() {
for (int idx = 0; idx < buf.length; ++idx)
buf[idx] = symbols[random.nextInt(symbols.length)];
return new String(buf);
}
public static final String upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static final String lower = upper.toLowerCase(Locale.ROOT);
public static final String digits = "0123456789";
public static final String alphanum = upper + lower + digits;
private final Random random;
private final char[] symbols;
private final char[] buf;
public RandomString(int length, Random random, String symbols) {
if (length < 1) throw new IllegalArgumentException();
if (symbols.length() < 2) throw new IllegalArgumentException();
this.random = Objects.requireNonNull(random);
this.symbols = symbols.toCharArray();
this.buf = new char[length];
}
/**
* Create an alphanumeric string generator.
*/
public RandomString(int length, Random random) {
this(length, random, alphanum);
}
/**
* Create an alphanumeric strings from a secure generator.
*/
public RandomString(int length) {
this(length, new SecureRandom());
}
/**
* Create session identifiers.
*/
public RandomString() {
this(21);
}
public static String randomCode(int length) {
return new RandomString(length).nextString();
}
}

View file

@ -59,21 +59,21 @@ public class AuthorizationContext {
return false;
}
if (current != null) {
if (current.getName().equals(resourceName)) {
return true;
for (Permission permission : authorization.getPermissions()) {
if (resourceName.equalsIgnoreCase(permission.getResourceName()) || resourceName.equalsIgnoreCase(permission.getResourceId())) {
if (scopeName == null) {
return true;
}
if (permission.getScopes().contains(scopeName)) {
return true;
}
}
}
if (hasResourcePermission(resourceName)) {
for (Permission permission : authorization.getPermissions()) {
for (PathConfig pathHolder : paths.values()) {
if (pathHolder.getId().equals(permission.getResourceId())) {
if (permission.getScopes().contains(scopeName)) {
return true;
}
}
}
if (current != null) {
if (current.getName().equals(resourceName)) {
return true;
}
}
@ -81,29 +81,7 @@ public class AuthorizationContext {
}
public boolean hasResourcePermission(String resourceName) {
if (this.authzToken == null) {
return false;
}
Authorization authorization = this.authzToken.getAuthorization();
if (authorization == null) {
return false;
}
if (current != null) {
if (current.getName().equals(resourceName)) {
return true;
}
}
for (Permission permission : authorization.getPermissions()) {
if (permission.getResourceName().equals(resourceName) || permission.getResourceId().equals(resourceName)) {
return true;
}
}
return false;
return hasPermission(resourceName, null);
}
public boolean hasScopePermission(String scopeName) {

View file

@ -34,6 +34,8 @@ public interface OAuth2Constants {
String REDIRECT_URI = "redirect_uri";
String DISPLAY = "display";
String SCOPE = "scope";
String STATE = "state";
@ -114,6 +116,7 @@ public interface OAuth2Constants {
String UMA_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:uma-ticket";
String DISPLAY_CONSOLE = "console";
}

View file

@ -18,13 +18,23 @@
package org.keycloak.jose.jwe;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.jose.jwe.alg.JWEAlgorithmProvider;
import org.keycloak.jose.jwe.enc.JWEEncryptionProvider;
import org.keycloak.util.JsonSerialization;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@ -193,4 +203,66 @@ public class JWE {
}
}
public static String encryptUTF8(String password, String saltString, String payload) {
byte[] bytes = null;
try {
bytes = payload.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return encrypt(password, saltString, bytes);
}
public static String encrypt(String password, String saltString, byte[] payload) {
try {
byte[] salt = Base64.decode(saltString);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey aesKey = new SecretKeySpec(tmp.getEncoded(), "AES");
JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
JWE jwe = new JWE()
.header(jweHeader)
.content(payload);
jwe.getKeyStorage()
.setEncryptionKey(aesKey);
return jwe.encodeJwe();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static byte[] decrypt(String password, String saltString, String encodedJwe) {
try {
byte[] salt = Base64.decode(saltString);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 100, 128);
SecretKey tmp = factory.generateSecret(spec);
SecretKey aesKey = new SecretKeySpec(tmp.getEncoded(), "AES");
JWE jwe = new JWE();
jwe.getKeyStorage()
.setEncryptionKey(aesKey);
jwe.verifyAndDecodeJwe(encodedJwe);
return jwe.getContent();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static String decryptUTF8(String password, String saltString, String encodedJwe) {
byte[] payload = decrypt(password, saltString, encodedJwe);
try {
return new String(payload, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -23,16 +23,14 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class PolicyEnforcerConfig {
@JsonProperty("create-resources")
@JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean createResources = Boolean.FALSE;
@JsonProperty("enforcement-mode")
private EnforcementMode enforcementMode = EnforcementMode.ENFORCING;
@ -40,6 +38,9 @@ public class PolicyEnforcerConfig {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<PathConfig> paths = new ArrayList<>();
@JsonProperty("lazy-load-paths")
private Boolean lazyLoadPaths = Boolean.FALSE;
@JsonProperty("on-deny-redirect-to")
@JsonInclude(JsonInclude.Include.NON_NULL)
private String onDenyRedirectTo;
@ -48,14 +49,18 @@ public class PolicyEnforcerConfig {
@JsonInclude(JsonInclude.Include.NON_NULL)
private UserManagedAccessConfig userManagedAccess;
public Boolean isCreateResources() {
return this.createResources;
}
public List<PathConfig> getPaths() {
return this.paths;
}
public Boolean getLazyLoadPaths() {
return lazyLoadPaths;
}
public void setLazyLoadPaths(Boolean lazyLoadPaths) {
this.lazyLoadPaths = lazyLoadPaths;
}
public EnforcementMode getEnforcementMode() {
return this.enforcementMode;
}
@ -68,10 +73,6 @@ public class PolicyEnforcerConfig {
return this.userManagedAccess;
}
public void setCreateResources(Boolean createResources) {
this.createResources = createResources;
}
public void setPaths(List<PathConfig> paths) {
this.paths = paths;
}
@ -90,6 +91,32 @@ public class PolicyEnforcerConfig {
public static class PathConfig {
public static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
PathConfig pathConfig = new PathConfig();
pathConfig.setId(resourceDescription.getId());
pathConfig.setName(resourceDescription.getName());
String uri = resourceDescription.getUri();
if (uri == null || "".equals(uri.trim())) {
throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
}
pathConfig.setPath(uri);
List<String> scopeNames = new ArrayList<>();
for (ScopeRepresentation scope : resourceDescription.getScopes()) {
scopeNames.add(scope.getName());
}
pathConfig.setScopes(scopeNames);
pathConfig.setType(resourceDescription.getType());
return pathConfig;
}
private String name;
private String type;
private String path;
@ -231,7 +258,8 @@ public class PolicyEnforcerConfig {
public enum ScopeEnforcementMode {
ALL,
ANY
ANY,
DISABLED
}
public static class UserManagedAccessConfig {

View file

@ -24,8 +24,10 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.keycloak.json.StringListMapDeserializer;
@ -45,6 +47,7 @@ public class ResourceRepresentation {
private String uri;
private String type;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonProperty("scopes")
private Set<ScopeRepresentation> scopes;
@JsonProperty("icon_uri")
@ -52,9 +55,6 @@ public class ResourceRepresentation {
private ResourceOwnerRepresentation owner;
private Boolean ownerManagedAccess;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<PolicyRepresentation> policies;
private String displayName;
@JsonDeserialize(using = StringListMapDeserializer.class)
@ -162,17 +162,31 @@ public class ResourceRepresentation {
}
public void setUri(String uri) {
this.uri = uri;
if (uri != null && !"".equalsIgnoreCase(uri.trim())) {
this.uri = uri;
}
}
public void setType(String type) {
this.type = type;
if (type != null && !"".equalsIgnoreCase(type.trim())) {
this.type = type;
}
}
public void setScopes(Set<ScopeRepresentation> scopes) {
this.scopes = scopes;
}
/**
* TODO: This is a workaround to allow deserialization of UMA resource representation. Jackson 2.19+ support aliases, once we upgrade, change this.
*
* @param scopes
*/
@JsonSetter("resource_scopes")
private void setScopesUma(Set<ScopeRepresentation> scopes) {
this.scopes = scopes;
}
public void setIconUri(String iconUri) {
this.iconUri = iconUri;
}
@ -181,10 +195,25 @@ public class ResourceRepresentation {
return this.owner;
}
@JsonProperty
public void setOwner(ResourceOwnerRepresentation owner) {
this.owner = owner;
}
@JsonIgnore
public void setOwner(String ownerId) {
if (ownerId == null) {
owner = null;
return;
}
if (owner == null) {
owner = new ResourceOwnerRepresentation();
}
owner.setId(ownerId);
}
public Boolean getOwnerManagedAccess() {
return ownerManagedAccess;
}

View file

@ -19,18 +19,18 @@ package org.keycloak.jose;
import java.io.UnsupportedEncodingException;
import java.security.Key;
import java.security.spec.KeySpec;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.common.util.Base64;
import org.keycloak.common.util.Base64Url;
import org.keycloak.jose.jwe.JWE;
import org.keycloak.jose.jwe.JWEConstants;
import org.keycloak.jose.jwe.JWEException;
import org.keycloak.jose.jwe.JWEHeader;
import org.keycloak.jose.jwe.JWEKeyStorage;
import org.keycloak.jose.jwe.*;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -53,7 +53,6 @@ public class JWETest {
testDirectEncryptAndDecrypt(aesKey, hmacKey, JWEConstants.A128CBC_HS256, PAYLOAD, true);
}
// Works just on OpenJDK 8. Other JDKs (IBM, Oracle) have restrictions on maximum key size of AES to be 128
// @Test
public void testDirect_Aes256CbcHmacSha512() throws Exception {
@ -118,10 +117,25 @@ public class JWETest {
System.out.println("Iterations: " + iterations + ", took: " + took);
}
@Test
public void testPassword() throws Exception {
byte[] salt = JWEUtils.generateSecret(8);
String encodedSalt = Base64.encodeBytes(salt);
String jwe = JWE.encryptUTF8("geheim", encodedSalt, PAYLOAD);
String decodedContent = JWE.decryptUTF8("geheim", encodedSalt, jwe);
Assert.assertEquals(PAYLOAD, decodedContent);
}
@Test
public void testAesKW_Aes128CbcHmacSha256() throws Exception {
SecretKey aesKey = new SecretKeySpec(AES_128_KEY, "AES");
testAesKW_Aes128CbcHmacSha256(aesKey);
}
private void testAesKW_Aes128CbcHmacSha256(SecretKey aesKey) throws UnsupportedEncodingException, JWEException {
JWEHeader jweHeader = new JWEHeader(JWEConstants.A128KW, JWEConstants.A128CBC_HS256, null);
JWE jwe = new JWE()
.header(jweHeader)
@ -146,6 +160,15 @@ public class JWETest {
Assert.assertEquals(PAYLOAD, decodedContent);
}
@Test
public void testSalt() {
byte[] random = JWEUtils.generateSecret(8);
System.out.print("new byte[] = {");
for (byte b : random) {
System.out.print(""+Byte.toString(b)+",");
}
}
@Test
public void externalJweAes128CbcHmacSha256Test() throws UnsupportedEncodingException, JWEException {

View file

@ -41,6 +41,7 @@ import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.hibernate.annotations.Nationalized;
import org.keycloak.representations.idm.authorization.DecisionStrategy;
import org.keycloak.representations.idm.authorization.Logic;
@ -77,6 +78,7 @@ public class PolicyEntity {
@Column(name = "NAME")
private String name;
@Nationalized
@Column(name = "DESCRIPTION")
private String description;

View file

@ -181,6 +181,10 @@ public class JPAResourceStore implements ResourceStore {
predicates.add(root.join("scopes").get("id").in(value));
} else if ("ownerManagedAccess".equals(name)) {
predicates.add(builder.equal(root.get(name), Boolean.valueOf(value[0])));
} else if ("uri".equals(name)) {
predicates.add(builder.equal(builder.lower(root.get(name)), value[0].toLowerCase()));
} else if ("uri_not_null".equals(name)) {
predicates.add(builder.isNotNull(root.get("uri")));
} else {
predicates.add(builder.like(builder.lower(root.get(name)), "%" + value[0].toLowerCase() + "%"));
}

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
@ -53,6 +55,7 @@ public class AuthenticationFlowEntity {
@Column(name="PROVIDER_ID")
protected String providerId;
@Nationalized
@Column(name="DESCRIPTION")
protected String description;

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
@ -61,8 +63,10 @@ public class ClientEntity {
@Column(name="ID", length = 36)
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
private String id;
@Nationalized
@Column(name = "NAME")
private String name;
@Nationalized
@Column(name = "DESCRIPTION")
private String description;
@Column(name = "CLIENT_ID")

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
@ -51,6 +53,7 @@ public class ClientTemplateEntity {
private String id;
@Column(name = "NAME")
private String name;
@Nationalized
@Column(name = "DESCRIPTION")
private String description;
@OneToMany(cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "clientTemplate")

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@ -46,6 +48,7 @@ public class ComponentConfigEntity {
@Column(name = "NAME")
protected String name;
@Nationalized
@Column(name = "VALUE")
protected String value;

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@ -51,6 +53,7 @@ public class GroupAttributeEntity {
@Column(name = "NAME")
protected String name;
@Nationalized
@Column(name = "VALUE")
protected String value;

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
@ -40,6 +42,7 @@ public class GroupEntity {
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
protected String id;
@Nationalized
@Column(name = "NAME")
protected String name;

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@ -49,6 +51,7 @@ public class RealmAttributeEntity {
@Id
@Column(name = "NAME")
protected String name;
@Nationalized
@Column(name = "VALUE")
protected String value;

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@ -61,8 +63,10 @@ public class RoleEntity {
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
private String id;
@Nationalized
@Column(name = "NAME")
private String name;
@Nationalized
@Column(name = "DESCRIPTION")
private String description;
@Column(name = "SCOPE_PARAM_REQUIRED")

View file

@ -17,6 +17,8 @@
package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Nationalized;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
@ -54,6 +56,7 @@ public class UserAttributeEntity {
@Column(name = "NAME")
protected String name;
@Nationalized
@Column(name = "VALUE")
protected String value;

View file

@ -19,6 +19,7 @@ package org.keycloak.models.jpa.entities;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.Nationalized;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.Access;
@ -69,12 +70,15 @@ public class UserEntity {
@Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
protected String id;
@Nationalized
@Column(name = "USERNAME")
protected String username;
@Nationalized
@Column(name = "FIRST_NAME")
protected String firstName;
@Column(name = "CREATED_TIMESTAMP")
protected Long createdTimestamp;
@Nationalized
@Column(name = "LAST_NAME")
protected String lastName;
@Column(name = "EMAIL")

11
pom.xml
View file

@ -1414,6 +1414,17 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>kcinit</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>kcinit-dist</artifactId>
<version>${project.version}</version>
<type>zip</type>
</dependency>
</dependencies>
</dependencyManagement>

View file

@ -43,5 +43,6 @@ public enum AuthenticationFlowError {
IDENTITY_PROVIDER_NOT_FOUND,
IDENTITY_PROVIDER_DISABLED,
IDENTITY_PROVIDER_ERROR
IDENTITY_PROVIDER_ERROR,
DISPLAY_NOT_SUPPORTED
}

View file

@ -17,6 +17,8 @@
package org.keycloak.authentication;
import javax.ws.rs.core.Response;
/**
* Throw this exception from an Authenticator, FormAuthenticator, or FormAction if you want to completely abort the flow.
*
@ -25,11 +27,17 @@ package org.keycloak.authentication;
*/
public class AuthenticationFlowException extends RuntimeException {
private AuthenticationFlowError error;
private Response response;
public AuthenticationFlowException(AuthenticationFlowError error) {
this.error = error;
}
public AuthenticationFlowException(AuthenticationFlowError error, Response response) {
this.error = error;
this.response = response;
}
public AuthenticationFlowException(String message, AuthenticationFlowError error) {
super(message);
this.error = error;
@ -53,4 +61,8 @@ public class AuthenticationFlowException extends RuntimeException {
public AuthenticationFlowError getError() {
return error;
}
public Response getResponse() {
return response;
}
}

View file

@ -0,0 +1,21 @@
package org.keycloak.authentication;
import org.keycloak.models.KeycloakSession;
/**
* Implement this interface when declaring your authenticator factory
* if your provider has support for multiple oidc display query parameter parameter types
* if the display query parameter is set and your factory implements this interface, this method
* will be called.
*
*/
public interface DisplayTypeAuthenticatorFactory {
/**
*
*
* @param session
* @param displayType i.e. "console", "wap", "popup" are examples
* @return null if display type isn't support.
*/
Authenticator createDisplay(KeycloakSession session, String displayType);
}

View file

@ -0,0 +1,13 @@
package org.keycloak.authentication;
import org.keycloak.models.KeycloakSession;
/**
* Implement this interface when declaring your required action factory
* has support for multiple oidc display query parameter parameter types
* if the display query parameter is set and your factory implements this interface, this method
* will be called.
*/
public interface DisplayTypeRequiredActionFactory {
RequiredActionProvider createDisplay(KeycloakSession session, String displayType);
}

View file

@ -59,6 +59,15 @@ public interface RequiredActionContext {
*/
URI getActionUrl();
/**
* Get the action URL for the required action. This auto-generates the access code.
*
* @param authSessionIdParam if true, will embed session id as query param. Useful for clients that don't support cookies (i.e. console)
*
* @return
*/
URI getActionUrl(boolean authSessionIdParam);
/**
* Create a Freemarker form builder that presets the user, action URI, and a generated access code
*

View file

@ -0,0 +1,303 @@
package org.keycloak.authentication;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
* This class encapsulates a proprietary HTTP challenge protocol designed by keycloak team which is used by text-based console
* clients to dynamically render and prompt for information in a textual manner. The class is a builder which can
* build the challenge response (the header and response body).
*
* When doing code to token flow in OAuth, server could respond with
*
* 401
* WWW-Authenticate: X-Text-Form-Challenge callback="http://localhost/..."
* param="username" label="Username: " mask=false
* param="password" label="Password: " mask=true
* Content-Type: text/plain
*
* Please login with your username and password
*
*
* The client receives this challenge. It first outputs whatever the text body of the message contains. It will
* then prompt for username and password using the label values as prompt messages for each parameter.
*
* After the input has been entered by the user, the client does a form POST to the callback url with the values of the
* input parameters entered.
*
* The server can challenge with 401 as many times as it wants. The client will look for 302 responses. It will will
* follow all redirects unless the Location url has an OAuth "code" parameter. If there is a code parameter, then the
* client will stop and finish the OAuth flow to obtain a token. Any other response code other than 401 or 302 the client
* should abort with an error message.
*
*/
public class TextChallenge {
/**
* Browser is required to login. This will abort client from doing a console login.
*
* @param session
* @return
*/
public static Response browserRequired(KeycloakSession session) {
return Response.status(Response.Status.UNAUTHORIZED)
.header("WWW-Authenticate", "X-Text-Form-Challenge browserRequired")
.type(MediaType.TEXT_PLAIN)
.entity("\n" + session.getProvider(LoginFormsProvider.class).getMessage("browserRequired") + "\n").build();
}
/**
* Build challenge response for required actions
*
* @param context
* @return
*/
public static TextChallenge challenge(RequiredActionContext context) {
return new TextChallenge(context);
}
/**
* Build challenge response for authentication flows
*
* @param context
* @return
*/
public static TextChallenge challenge(AuthenticationFlowContext context) {
return new TextChallenge(context);
}
/**
* Build challenge response header only for required actions
*
* @param context
* @return
*/
public static HeaderBuilder header(RequiredActionContext context) {
return new TextChallenge(context).header();
}
/**
* Build challenge response header only for authentication flows
*
* @param context
* @return
*/
public static HeaderBuilder header(AuthenticationFlowContext context) {
return new TextChallenge(context).header();
}
TextChallenge(RequiredActionContext requiredActionContext) {
this.requiredActionContext = requiredActionContext;
}
TextChallenge(AuthenticationFlowContext flowContext) {
this.flowContext = flowContext;
}
protected RequiredActionContext requiredActionContext;
protected AuthenticationFlowContext flowContext;
protected HeaderBuilder header;
/**
* Create a theme form pre-populated with challenge
*
* @return
*/
public LoginFormsProvider form() {
if (header == null) throw new RuntimeException("Header Not Set");
return formInternal()
.setStatus(Response.Status.UNAUTHORIZED)
.setMediaType(MediaType.TEXT_PLAIN_TYPE)
.setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, header.build());
}
/**
* Create challenge response with a body generated from localized
* message.properties of your theme
*
* @param msg message id
* @param params parameters to use to format the message
*
* @return
*/
public Response message(String msg, String... params) {
if (header == null) throw new RuntimeException("Header Not Set");
Response response = Response.status(401)
.header(HttpHeaders.WWW_AUTHENTICATE, header.build())
.type(MediaType.TEXT_PLAIN)
.entity("\n" + formInternal().getMessage(msg, params) + "\n").build();
return response;
}
/**
* Create challenge response with a text message body
*
* @param text plain text of http response body
*
* @return
*/
public Response text(String text) {
if (header == null) throw new RuntimeException("Header Not Set");
Response response = Response.status(401)
.header(HttpHeaders.WWW_AUTHENTICATE, header.build())
.type(MediaType.TEXT_PLAIN)
.entity("\n" + text + "\n").build();
return response;
}
/**
* Generate response with empty http response body
*
* @return
*/
public Response response() {
if (header == null) throw new RuntimeException("Header Not Set");
Response response = Response.status(401)
.header(HttpHeaders.WWW_AUTHENTICATE, header.build()).build();
return response;
}
protected LoginFormsProvider formInternal() {
if (requiredActionContext != null) {
return requiredActionContext.form();
} else {
return flowContext.form();
}
}
/**
* Start building the header
*
* @return
*/
public HeaderBuilder header() {
String callback;
if (requiredActionContext != null) {
callback = requiredActionContext.getActionUrl(true).toString();
} else {
callback = flowContext.getActionUrl(flowContext.generateAccessCode(), true).toString();
}
header = new HeaderBuilder(callback);
return header;
}
public class HeaderBuilder {
protected StringBuilder builder = new StringBuilder();
protected HeaderBuilder(String callback) {
builder.append("X-Text-Form-Challenge callback=\"").append(callback).append("\" ");
}
protected ParamBuilder param;
protected void checkParam() {
if (param != null) {
param.buildInternal();
param = null;
}
}
/**
* Build header string
*
* @return
*/
public String build() {
checkParam();
return builder.toString();
}
/**
* Define a param
*
* @param name
* @return
*/
public ParamBuilder param(String name) {
checkParam();
builder.append("param=\"").append(name).append("\" ");
param = new ParamBuilder(name);
return param;
}
public class ParamBuilder {
protected boolean mask;
protected String label;
protected ParamBuilder(String name) {
this.label = name;
}
public ParamBuilder label(String msg) {
this.label = formInternal().getMessage(msg);
return this;
}
public ParamBuilder labelText(String txt) {
this.label = txt;
return this;
}
/**
* Should input be masked by the client. For example, when entering password, you don't want to show password on console.
*
* @param mask
* @return
*/
public ParamBuilder mask(boolean mask) {
this.mask = mask;
return this;
}
public void buildInternal() {
builder.append("label=\"").append(label).append(" \" ");
builder.append("mask=").append(mask).append(" ");
}
/**
* Build header string
*
* @return
*/
public String build() {
return HeaderBuilder.this.build();
}
public TextChallenge challenge() {
return TextChallenge.this;
}
public LoginFormsProvider form() {
return TextChallenge.this.form();
}
public Response message(String msg, String... params) {
return TextChallenge.this.message(msg, params);
}
public Response text(String text) {
return TextChallenge.this.text(text);
}
public ParamBuilder param(String name) {
return HeaderBuilder.this.param(name);
}
}
}
}

View file

@ -23,6 +23,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
import org.keycloak.sessions.AuthenticationSessionModel;
import java.util.List;
import java.util.Map;
/**
@ -76,4 +77,24 @@ public interface EmailTemplateProvider extends Provider {
public void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
/**
* Send formatted email
*
* @param subjectFormatKey message property that will be used to format email subject
* @param bodyTemplate freemarker template file
* @param bodyAttributes attributes used to fill template
* @throws EmailException
*/
void send(String subjectFormatKey, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException;
/**
* Send formatted email
*
* @param subjectFormatKey message property that will be used to format email subject
* @param subjectAttributes attributes used to fill subject format message
* @param bodyTemplate freemarker template file
* @param bodyAttributes attributes used to fill template
* @throws EmailException
*/
void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException;
}

View file

@ -90,5 +90,6 @@ public interface Errors {
String NOT_LOGGED_IN = "not_logged_in";
String UNKNOWN_IDENTITY_PROVIDER = "unknown_identity_provider";
String ILLEGAL_ORIGIN = "illegal_origin";
String DISPLAY_UNSUPPORTED = "display_unsupported";
}

View file

@ -54,6 +54,8 @@ public interface LoginFormsProvider extends Provider {
String getMessage(String message);
String getMessage(String message, String... parameters);
Response createLogin();
Response createPasswordReset();

View file

@ -65,5 +65,6 @@ public class AdminRoles {
ALL_ROLES.add(ADMIN);
ALL_ROLES.add(CREATE_REALM);
ALL_ROLES.add(CREATE_CLIENT);
ALL_ROLES.add(REALM_ADMIN);
}
}

View file

@ -52,6 +52,7 @@ public interface Constants {
int DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT = 2592000;
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
String VERIFY_EMAIL_CODE = "VERIFY_EMAIL_CODE";
String EXECUTION = "execution";
String CLIENT_ID = "client_id";
String TAB_ID = "tab_id";

View file

@ -2314,7 +2314,7 @@ public class RepresentationToModel {
String ownerId = owner.getId();
if (ownerId == null) {
throw new RuntimeException("No owner specified for resource [" + resource.getName() + "].");
ownerId = resourceServer.getId();
}
if (!resourceServer.getId().equals(ownerId)) {

View file

@ -653,27 +653,33 @@ public class AuthenticationProcessor {
public Response handleBrowserException(Exception failure) {
if (failure instanceof AuthenticationFlowException) {
AuthenticationFlowException e = (AuthenticationFlowException) failure;
if (e.getError() == AuthenticationFlowError.INVALID_USER) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.USER_NOT_FOUND);
if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
} else if (e.getError() == AuthenticationFlowError.USER_DISABLED) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.USER_DISABLED);
if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.ACCOUNT_DISABLED);
} else if (e.getError() == AuthenticationFlowError.USER_TEMPORARILY_DISABLED) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.USER_TEMPORARILY_DISABLED);
if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session,authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
} else if (e.getError() == AuthenticationFlowError.INVALID_CLIENT_SESSION) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.INVALID_CODE);
if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE);
} else if (e.getError() == AuthenticationFlowError.EXPIRED_CODE) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.EXPIRED_CODE);
if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.EXPIRED_CODE);
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
@ -701,9 +707,15 @@ public class AuthenticationProcessor {
CacheControlUtil.noBackButtonCacheControlHeader();
return processor.authenticate();
} else if (e.getError() == AuthenticationFlowError.DISPLAY_NOT_SUPPORTED) {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.DISPLAY_UNSUPPORTED);
if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED);
} else {
ServicesLogger.LOGGER.failedAuthentication(e);
event.error(Errors.INVALID_USER_CREDENTIALS);
if (e.getResponse() != null) return e.getResponse();
return ErrorPage.error(session, authenticationSession, Response.Status.BAD_REQUEST, Messages.INVALID_USER);
}

View file

@ -18,6 +18,7 @@
package org.keycloak.authentication;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.UserModel;
@ -58,6 +59,24 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
|| status == AuthenticationSessionModel.ExecutionStatus.SETUP_REQUIRED;
}
protected Authenticator createAuthenticator(AuthenticatorFactory factory) {
String display = processor.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
if (display == null) return factory.create(processor.getSession());
if (factory instanceof DisplayTypeAuthenticatorFactory) {
Authenticator authenticator = ((DisplayTypeAuthenticatorFactory)factory).createDisplay(processor.getSession(), display);
if (authenticator != null) return authenticator;
}
// todo create a provider for handling lack of display support
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(processor.getSession()));
} else {
return factory.create(processor.getSession());
}
}
@Override
public Response processAction(String actionExecution) {
@ -86,7 +105,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
if (factory == null) {
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
}
Authenticator authenticator = factory.create(processor.getSession());
Authenticator authenticator = createAuthenticator(factory);
AuthenticationProcessor.Result result = processor.createAuthenticatorContext(model, authenticator, executions);
logger.debugv("action: {0}", model.getAuthenticator());
authenticator.action(result);
@ -161,7 +180,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
if (factory == null) {
throw new RuntimeException("Unable to find factory for AuthenticatorFactory: " + model.getAuthenticator() + " did you forget to declare it in a META-INF/services file?");
}
Authenticator authenticator = factory.create(processor.getSession());
Authenticator authenticator = createAuthenticator(factory);
logger.debugv("authenticator: {0}", factory.getId());
UserModel authUser = processor.getAuthenticationSession().getAuthenticatedUser();

View file

@ -33,6 +33,7 @@ import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
@ -162,6 +163,16 @@ public class RequiredActionContextResult implements RequiredActionContext {
}
@Override
public URI getActionUrl(boolean authSessionIdParam) {
URI uri = getActionUrl();
if (authSessionIdParam) {
uri = UriBuilder.fromUri(uri).queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId()).build();
}
return uri;
}
@Override
public LoginFormsProvider form() {
String accessCode = generateCode();

View file

@ -0,0 +1,46 @@
package org.keycloak.authentication.authenticators;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
/**
* Pass-thru atheneticator that just sets the context to attempted.
*/
public class AttemptedAuthenticator implements Authenticator {
public static final AttemptedAuthenticator SINGLETON = new AttemptedAuthenticator();
@Override
public void authenticate(AuthenticationFlowContext context) {
context.attempted();
}
@Override
public void action(AuthenticationFlowContext context) {
throw new RuntimeException("Unreachable!");
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
}

View file

@ -18,8 +18,11 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
import org.keycloak.authentication.authenticators.AttemptedAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -31,7 +34,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class CookieAuthenticatorFactory implements AuthenticatorFactory {
public class CookieAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-cookie";
static CookieAuthenticator SINGLETON = new CookieAuthenticator();
@ -40,6 +43,13 @@ public class CookieAuthenticatorFactory implements AuthenticatorFactory {
return SINGLETON;
}
@Override
public Authenticator createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return SINGLETON;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return AttemptedAuthenticator.SINGLETON; // ignore this authenticator
}
@Override
public void init(Config.Scope config) {

View file

@ -18,6 +18,7 @@
package org.keycloak.authentication.authenticators.browser;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.constants.AdapterConstants;
@ -25,10 +26,13 @@ import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.ClientSessionCode;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.util.List;
/**
@ -66,8 +70,11 @@ public class IdentityProviderAuthenticator implements Authenticator {
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
String clientId = context.getAuthenticationSession().getClient().getClientId();
String tabId = context.getAuthenticationSession().getTabId();
Response response = Response.seeOther(
Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId))
URI location = Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId);
if (context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY) != null) {
location = UriBuilder.fromUri(location).queryParam(OAuth2Constants.DISPLAY, context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY)).build();
}
Response response = Response.seeOther(location)
.build();
LOG.debugf("Redirecting to %s", providerId);

View file

@ -18,8 +18,11 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
import org.keycloak.authentication.authenticators.AttemptedAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -33,7 +36,7 @@ import static org.keycloak.provider.ProviderConfigProperty.STRING_TYPE;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory {
public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
protected static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED
@ -82,6 +85,13 @@ public class IdentityProviderAuthenticatorFactory implements AuthenticatorFactor
return new IdentityProviderAuthenticator();
}
@Override
public Authenticator createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return new IdentityProviderAuthenticator();
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return AttemptedAuthenticator.SINGLETON; // ignore this authenticator
}
@Override
public void init(Config.Scope config) {
}

View file

@ -18,8 +18,11 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
import org.keycloak.authentication.authenticators.console.ConsoleOTPFormAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -32,7 +35,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
public class OTPFormAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-otp-form";
public static final OTPFormAuthenticator SINGLETON = new OTPFormAuthenticator();
@ -42,6 +45,13 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory {
return SINGLETON;
}
@Override
public Authenticator createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return SINGLETON;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsoleOTPFormAuthenticator.SINGLETON;
}
@Override
public void init(Config.Scope config) {

View file

@ -18,8 +18,10 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -32,7 +34,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
public class SpnegoAuthenticatorFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-spnego";
public static final SpnegoAuthenticator SINGLETON = new SpnegoAuthenticator();
@ -42,6 +44,13 @@ public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
return SINGLETON;
}
@Override
public Authenticator createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return SINGLETON;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return SINGLETON;
}
@Override
public void init(Config.Scope config) {

View file

@ -19,7 +19,6 @@ package org.keycloak.authentication.authenticators.browser;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;

View file

@ -18,8 +18,11 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.DisplayTypeAuthenticatorFactory;
import org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -32,7 +35,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UsernamePasswordFormFactory implements AuthenticatorFactory {
public class UsernamePasswordFormFactory implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory {
public static final String PROVIDER_ID = "auth-username-password-form";
public static final UsernamePasswordForm SINGLETON = new UsernamePasswordForm();
@ -42,6 +45,13 @@ public class UsernamePasswordFormFactory implements AuthenticatorFactory {
return SINGLETON;
}
@Override
public Authenticator createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return SINGLETON;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsoleUsernamePasswordAuthenticator.SINGLETON;
}
@Override
public void init(Config.Scope config) {

View file

@ -0,0 +1,73 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authentication.authenticators.console;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.TextChallenge;
import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticator;
import org.keycloak.representations.idm.CredentialRepresentation;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ConsoleOTPFormAuthenticator extends OTPFormAuthenticator implements Authenticator {
public static final ConsoleOTPFormAuthenticator SINGLETON = new ConsoleOTPFormAuthenticator();
public static URI getCallbackUrl(AuthenticationFlowContext context) {
return context.getActionUrl(context.generateAccessCode(), true);
}
protected TextChallenge challenge(AuthenticationFlowContext context) {
return TextChallenge.challenge(context)
.header()
.param(CredentialRepresentation.TOTP)
.label("console-otp")
.challenge();
}
@Override
public void action(AuthenticationFlowContext context) {
validateOTP(context);
}
@Override
public void authenticate(AuthenticationFlowContext context) {
Response challengeResponse = challenge(context, null);
context.challenge(challengeResponse);
}
@Override
protected Response challenge(AuthenticationFlowContext context, String msg) {
if (msg == null) {
return challenge(context).response();
}
return challenge(context).message(msg);
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authentication.authenticators.console;
import org.keycloak.authentication.*;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ConsoleUsernamePasswordAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
public static final ConsoleUsernamePasswordAuthenticator SINGLETON = new ConsoleUsernamePasswordAuthenticator();
@Override
public boolean requiresUser() {
return false;
}
protected TextChallenge challenge(AuthenticationFlowContext context) {
return TextChallenge.challenge(context)
.header()
.param("username")
.label("console-username")
.param("password")
.label("console-password")
.mask(true)
.challenge();
}
@Override
public void authenticate(AuthenticationFlowContext context) {
Response response = challenge(context).form().createForm("cli_splash.ftl");
context.challenge(response);
}
@Override
protected Response invalidUser(AuthenticationFlowContext context) {
Response response = challenge(context).message(Messages.INVALID_USER);
return response;
}
@Override
protected Response disabledUser(AuthenticationFlowContext context) {
Response response = challenge(context).message(Messages.ACCOUNT_DISABLED);
return response;
}
@Override
protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
Response response = challenge(context).message(Messages.INVALID_USER);
return response;
}
@Override
protected Response invalidCredentials(AuthenticationFlowContext context) {
Response response = challenge(context).message(Messages.INVALID_USER);
return response;
}
@Override
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
context.getEvent().error(eventError);
Response response = challenge(context).message(loginFormError);
context.failureChallenge(authenticatorError, response);
return response;
}
@Override
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (!validateUserAndPassword(context, formData)) {
return;
}
context.success();
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,102 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authentication.authenticators.console;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ConsoleUsernamePasswordAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "console-username-password";
@Override
public Authenticator create(KeycloakSession session) {
return ConsoleUsernamePasswordAuthenticator.SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return UserCredentialModel.PASSWORD;
}
@Override
public boolean isConfigurable() {
return false;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Username Password Challenge";
}
@Override
public String getHelpText() {
return "Proprietary challenge protocol for CLI clients that queries for username password";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.TextChallenge;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import javax.ws.rs.core.Response;
import java.util.Arrays;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ConsoleTermsAndConditions implements RequiredActionProvider {
public static final ConsoleTermsAndConditions SINGLETON = new ConsoleTermsAndConditions();
public static final String USER_ATTRIBUTE = TermsAndConditions.PROVIDER_ID;
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
Response challenge = TextChallenge.challenge(context)
.header()
.param("accept")
.label("console-accept-terms")
.message("termsPlainText");
context.challenge(challenge);
}
@Override
public void processAction(RequiredActionContext context) {
String accept = context.getHttpRequest().getDecodedFormParameters().getFirst("accept");
String yes = context.form().getMessage("console-accept");
if (!accept.equals(yes)) {
context.getUser().removeAttribute(USER_ATTRIBUTE);
requiredActionChallenge(context);
return;
}
context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(Integer.toString(Time.currentTime())));
context.success();
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authentication.requiredactions;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.*;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.*;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ConsoleUpdatePassword extends UpdatePassword implements RequiredActionProvider {
public static final ConsoleUpdatePassword SINGLETON = new ConsoleUpdatePassword();
private static final Logger logger = Logger.getLogger(ConsoleUpdatePassword.class);
public static final String PASSWORD_NEW = "password-new";
public static final String PASSWORD_CONFIRM = "password-confirm";
protected TextChallenge challenge(RequiredActionContext context) {
return TextChallenge.challenge(context)
.header()
.param(PASSWORD_NEW)
.label("console-new-password")
.mask(true)
.param(PASSWORD_CONFIRM)
.label("console-confirm-password")
.mask(true)
.challenge();
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
context.challenge(
challenge(context).message("console-update-password"));
}
@Override
public void processAction(RequiredActionContext context) {
EventBuilder event = context.getEvent();
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
event.event(EventType.UPDATE_PASSWORD);
String passwordNew = formData.getFirst(PASSWORD_NEW);
String passwordConfirm = formData.getFirst(PASSWORD_CONFIRM);
EventBuilder errorEvent = event.clone().event(EventType.UPDATE_PASSWORD_ERROR)
.client(context.getAuthenticationSession().getClient())
.user(context.getAuthenticationSession().getAuthenticatedUser());
if (Validation.isBlank(passwordNew)) {
context.challenge(challenge(context).message(Messages.MISSING_PASSWORD));
errorEvent.error(Errors.PASSWORD_MISSING);
return;
} else if (!passwordNew.equals(passwordConfirm)) {
context.challenge(challenge(context).message(Messages.NOTMATCH_PASSWORD));
errorEvent.error(Errors.PASSWORD_CONFIRM_ERROR);
return;
}
try {
context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), UserCredentialModel.password(passwordNew, false));
context.success();
} catch (ModelException me) {
errorEvent.detail(Details.REASON, me.getMessage()).error(Errors.PASSWORD_REJECTED);
context.challenge(challenge(context).text(me.getMessage()));
return;
} catch (Exception ape) {
errorEvent.detail(Details.REASON, ape.getMessage()).error(Errors.PASSWORD_REJECTED);
context.challenge(challenge(context).text(ape.getMessage()));
return;
}
}
}

View file

@ -0,0 +1,68 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.AttributeFormDataProcessor;
import org.keycloak.services.validation.Validation;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ConsoleUpdateProfile implements RequiredActionProvider {
public static final ConsoleUpdateProfile SINGLETON = new ConsoleUpdateProfile();
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
// do nothing right now. I think this behavior is ok. We just defer this action until a browser login happens.
context.ignore();
}
@Override
public void processAction(RequiredActionContext context) {
throw new RuntimeException("Should be unreachable");
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.TextChallenge;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.forms.login.freemarker.model.TotpBean;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.CredentialValidation;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.net.URI;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ConsoleUpdateTotp implements RequiredActionProvider {
public static final ConsoleUpdateTotp SINGLETON = new ConsoleUpdateTotp();
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
TotpBean totpBean = new TotpBean(context.getSession(), context.getRealm(), context.getUser(), context.getUriInfo().getRequestUriBuilder());
String totpSecret = totpBean.getTotpSecret();
context.getAuthenticationSession().setAuthNote("totpSecret", totpSecret);
Response challenge = challenge(context).form()
.setAttribute("totp", totpBean)
.createForm("login-config-totp-text.ftl");
context.challenge(challenge);
}
protected TextChallenge challenge(RequiredActionContext context) {
return TextChallenge.challenge(context)
.header()
.param("totp")
.label("console-otp")
.challenge();
}
@Override
public void processAction(RequiredActionContext context) {
EventBuilder event = context.getEvent();
event.event(EventType.UPDATE_TOTP);
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String totp = formData.getFirst("totp");
String totpSecret = context.getAuthenticationSession().getAuthNote("totpSecret");
if (Validation.isBlank(totp)) {
context.challenge(
challenge(context).message(Messages.MISSING_TOTP)
);
return;
} else if (!CredentialValidation.validOTP(context.getRealm(), totp, totpSecret)) {
context.challenge(
challenge(context).message(Messages.INVALID_TOTP)
);
return;
}
UserCredentialModel credentials = new UserCredentialModel();
credentials.setType(context.getRealm().getOTPPolicy().getType());
credentials.setValue(totpSecret);
context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credentials);
// if type is HOTP, to update counter we execute validation based on supplied token
UserCredentialModel cred = new UserCredentialModel();
cred.setType(context.getRealm().getOTPPolicy().getType());
cred.setValue(totp);
context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(), cred);
context.getAuthenticationSession().removeAuthNote("totpSecret");
context.success();
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,152 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authentication.requiredactions;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.TextChallenge;
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
import org.keycloak.common.util.RandomString;
import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.*;
import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
import org.keycloak.sessions.AuthenticationSessionCompoundId;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.*;
import java.net.URI;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ConsoleVerifyEmail implements RequiredActionProvider {
public static final ConsoleVerifyEmail SINGLETON = new ConsoleVerifyEmail();
private static final Logger logger = Logger.getLogger(ConsoleVerifyEmail.class);
@Override
public void evaluateTriggers(RequiredActionContext context) {
if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) {
context.getUser().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL);
logger.debug("User is required to verify email");
}
}
@Override
public void requiredActionChallenge(RequiredActionContext context) {
AuthenticationSessionModel authSession = context.getAuthenticationSession();
if (context.getUser().isEmailVerified()) {
context.success();
authSession.removeAuthNote(Constants.VERIFY_EMAIL_KEY);
return;
}
String email = context.getUser().getEmail();
if (Validation.isBlank(email)) {
context.ignore();
return;
}
Response challenge = sendVerifyEmail(context);
context.challenge(challenge);
}
@Override
public void processAction(RequiredActionContext context) {
EventBuilder event = context.getEvent().clone().event(EventType.VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail());
String code = context.getAuthenticationSession().getAuthNote(Constants.VERIFY_EMAIL_CODE);
if (code == null) {
requiredActionChallenge(context);
return;
}
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String emailCode = formData.getFirst(EMAIL_CODE);
if (!code.equals(emailCode)) {
context.challenge(
challenge(context).message(Messages.INVALID_CODE)
);
event.error(Errors.INVALID_CODE);
return;
}
event.success();
context.success();
}
@Override
public void close() {
}
public static String EMAIL_CODE="email_code";
protected TextChallenge challenge(RequiredActionContext context) {
return TextChallenge.challenge(context)
.header()
.param(EMAIL_CODE)
.label("console-email-code")
.challenge();
}
private Response sendVerifyEmail(RequiredActionContext context) throws UriBuilderException, IllegalArgumentException {
KeycloakSession session = context.getSession();
UserModel user = context.getUser();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
EventBuilder event = context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail());
String code = RandomString.randomCode(8);
authSession.setAuthNote(Constants.VERIFY_EMAIL_CODE, code);
RealmModel realm = session.getContext().getRealm();
Map<String, Object> attributes = new HashMap<>();
attributes.put("code", code);
try {
session
.getProvider(EmailTemplateProvider.class)
.setAuthenticationSession(authSession)
.setRealm(realm)
.setUser(user)
.send("emailVerificationSubject", "email-verification-with-code.ftl", attributes);
event.success();
} catch (EmailException e) {
logger.error("Failed to send verification email", e);
event.error(Errors.EMAIL_SEND_FAILED);
}
return challenge(context).text(context.form().getMessage("console-verify-email", user.getEmail()));
}
}

View file

@ -18,9 +18,8 @@
package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.*;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@ -32,7 +31,7 @@ import java.util.Arrays;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
public static final String PROVIDER_ID = "terms_and_conditions";
public static final String USER_ATTRIBUTE = PROVIDER_ID;
@ -41,6 +40,15 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
return this;
}
@Override
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return this;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsoleTermsAndConditions.SINGLETON;
}
@Override
public void init(Config.Scope config) {

View file

@ -19,9 +19,8 @@ package org.keycloak.authentication.requiredactions;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.*;
import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.credential.CredentialProvider;
@ -47,7 +46,7 @@ import java.util.concurrent.TimeUnit;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory {
public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
private static final Logger logger = Logger.getLogger(UpdatePassword.class);
@Override
public void evaluateTriggers(RequiredActionContext context) {
@ -142,6 +141,15 @@ public class UpdatePassword implements RequiredActionProvider, RequiredActionFac
return this;
}
@Override
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return this;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsoleUpdatePassword.SINGLETON;
}
@Override
public void init(Config.Scope config) {

View file

@ -18,9 +18,8 @@
package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.*;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
@ -41,7 +40,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory {
public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@ -142,6 +141,16 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
return this;
}
@Override
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return this;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsoleUpdateProfile.SINGLETON;
}
@Override
public void init(Config.Scope config) {

View file

@ -18,9 +18,8 @@
package org.keycloak.authentication.requiredactions;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.*;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
@ -38,7 +37,7 @@ import javax.ws.rs.core.Response;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory {
public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
@Override
public void evaluateTriggers(RequiredActionContext context) {
}
@ -105,6 +104,15 @@ public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory
return this;
}
@Override
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return this;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsoleUpdateTotp.SINGLETON;
}
@Override
public void init(Config.Scope config) {

View file

@ -19,9 +19,8 @@ package org.keycloak.authentication.requiredactions;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.*;
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException;
@ -45,7 +44,7 @@ import javax.ws.rs.core.*;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory {
public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
private static final Logger logger = Logger.getLogger(VerifyEmail.class);
@Override
public void evaluateTriggers(RequiredActionContext context) {
@ -107,6 +106,14 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
return this;
}
@Override
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
if (displayType == null) return this;
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
return ConsoleVerifyEmail.SINGLETON;
}
@Override
public void init(Config.Scope config) {

View file

@ -21,6 +21,8 @@ import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
import static org.keycloak.models.utils.RepresentationToModel.toModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -46,6 +48,7 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.OAuthErrorException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
@ -54,6 +57,7 @@ import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.common.util.PathMatcher;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.ClientModel;
@ -64,7 +68,7 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
@ -90,32 +94,18 @@ public class ResourceSetService {
@Consumes("application/json")
@Produces("application/json")
public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource) {
return create(uriInfo, resource, (Function<Resource, ResourceRepresentation>) resource1 -> {
ResourceRepresentation representation = new ResourceRepresentation();
if (resource == null) {
return Response.status(Status.BAD_REQUEST).build();
}
representation.setId(resource1.getId());
ResourceRepresentation newResource = create(resource);
return representation;
});
}
public Response create(@Context UriInfo uriInfo, ResourceRepresentation resource, Function<Resource, ?> toRepresentation) {
Response response = create(resource, toRepresentation);
audit(uriInfo, resource, resource.getId(), OperationType.CREATE);
return response;
return Response.status(Status.CREATED).entity(newResource).build();
}
public Response create(ResourceRepresentation resource) {
return create(resource, (Function<Resource, ResourceRepresentation>) resource1 -> {
ResourceRepresentation representation = new ResourceRepresentation();
representation.setId(resource1.getId());
return representation;
});
}
public Response create(ResourceRepresentation resource, Function<Resource, ?> toRepresentation) {
public ResourceRepresentation create(ResourceRepresentation resource) {
requireManage();
StoreFactory storeFactory = this.authorization.getStoreFactory();
ResourceOwnerRepresentation owner = resource.getOwner();
@ -123,21 +113,22 @@ public class ResourceSetService {
if (owner == null) {
owner = new ResourceOwnerRepresentation();
owner.setId(resourceServer.getId());
resource.setOwner(owner);
}
String ownerId = owner.getId();
if (ownerId == null) {
return ErrorResponse.error("You must specify the resource owner.", Status.BAD_REQUEST);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "You must specify the resource owner.", Status.BAD_REQUEST);
}
Resource existingResource = storeFactory.getResourceStore().findByName(resource.getName(), ownerId, this.resourceServer.getId());
if (existingResource != null) {
return ErrorResponse.exists("Resource with name [" + resource.getName() + "] already exists.");
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Resource with name [" + resource.getName() + "] already exists.", Status.CONFLICT);
}
return Response.status(Status.CREATED).entity(toRepresentation.apply(toModel(resource, this.resourceServer, authorization))).build();
return toRepresentation(toModel(resource, this.resourceServer, authorization), resourceServer, authorization);
}
@Path("{id}")
@ -198,10 +189,10 @@ public class ResourceSetService {
@NoCache
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
return findById(id, (Function<Resource, ResourceRepresentation>) resource -> toRepresentation(resource, resourceServer, authorization, true));
return findById(id, resource -> toRepresentation(resource, resourceServer, authorization, true));
}
public Response findById(@PathParam("id") String id, Function<Resource, ?> toRepresentation) {
public Response findById(String id, Function<Resource, ? extends ResourceRepresentation> toRepresentation) {
requireView();
StoreFactory storeFactory = authorization.getStoreFactory();
Resource model = storeFactory.getResourceStore().findById(id, resourceServer.getId());
@ -340,10 +331,11 @@ public class ResourceSetService {
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
@QueryParam("matchingUri") Boolean matchingUri,
@QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
return find(id, name, uri, owner, type, scope, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, ResourceRepresentation>) (resource, deep1) -> toRepresentation(resource, resourceServer, authorization, deep1));
return find(id, name, uri, owner, type, scope, matchingUri, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, ResourceRepresentation>) (resource, deep1) -> toRepresentation(resource, resourceServer, authorization, deep1));
}
public Response find(@QueryParam("_id") String id,
@ -352,6 +344,7 @@ public class ResourceSetService {
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
@QueryParam("matchingUri") Boolean matchingUri,
@QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult,
@ -413,9 +406,38 @@ public class ResourceSetService {
search.put("scope", scopes.stream().map(Scope::getId).toArray(String[]::new));
}
List<Resource> resources = storeFactory.getResourceStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS);
if (matchingUri != null && matchingUri && resources.isEmpty()) {
HashMap<String, String[]> attributes = new HashMap<>();
attributes.put("uri_not_null", new String[] {"true"});
attributes.put("owner", new String[] {resourceServer.getId()});
List<Resource> serverResources = storeFactory.getResourceStore().findByResourceServer(attributes, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS);
PathMatcher<Resource> pathMatcher = new PathMatcher<Resource>() {
@Override
protected String getPath(Resource entry) {
return entry.getUri();
}
@Override
protected Collection<Resource> getPaths() {
return serverResources;
}
};
Resource matches = pathMatcher.matches(uri);
if (matches != null) {
resources = Arrays.asList(matches);
}
}
Boolean finalDeep = deep;
return Response.ok(
storeFactory.getResourceStore().findByResourceServer(search, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS).stream()
resources.stream()
.map(resource -> toRepresentation.apply(resource, finalDeep))
.collect(Collectors.toList()))
.build();
@ -437,7 +459,7 @@ public class ResourceSetService {
audit(uriInfo, resource, null, operation);
}
private void audit(@Context UriInfo uriInfo, ResourceRepresentation resource, String id, OperationType operation) {
public void audit(@Context UriInfo uriInfo, ResourceRepresentation resource, String id, OperationType operation) {
if (authorization.getRealm().isAdminEventsEnabled()) {
if (id != null) {
adminEvent.operation(operation).resourcePath(uriInfo, id).representation(resource).success();

View file

@ -65,7 +65,7 @@ public class ProtectionService {
ResteasyProviderFactory.getInstance().injectProperties(resourceManager);
ResourceService resource = new ResourceService(resourceServer, identity, resourceManager, this.authorization);
ResourceService resource = new ResourceService(resourceServer, identity, resourceManager);
ResteasyProviderFactory.getInstance().injectProperties(resource);

View file

@ -1,50 +0,0 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.keycloak.authorization.protection.resource;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class RegistrationResponse {
private final UmaResourceRepresentation resourceDescription;
public RegistrationResponse(UmaResourceRepresentation resourceDescription) {
this.resourceDescription = resourceDescription;
}
public RegistrationResponse() {
this(null);
}
@JsonUnwrapped
public UmaResourceRepresentation getResourceDescription() {
return this.resourceDescription;
}
public String getId() {
if (this.resourceDescription != null) {
return this.resourceDescription.getId();
}
return null;
}
}

View file

@ -18,8 +18,6 @@
package org.keycloak.authorization.protection.resource;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@ -36,16 +34,13 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.admin.ResourceSetService;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation;
import org.keycloak.authorization.protection.resource.representation.UmaScopeRepresentation;
import org.keycloak.events.admin.OperationType;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.ErrorResponseException;
/**
@ -56,39 +51,51 @@ public class ResourceService {
private final ResourceServer resourceServer;
private final ResourceSetService resourceManager;
private final Identity identity;
private final AuthorizationProvider authorization;
public ResourceService(ResourceServer resourceServer, Identity identity, ResourceSetService resourceManager, AuthorizationProvider authorization) {
public ResourceService(ResourceServer resourceServer, Identity identity, ResourceSetService resourceManager) {
this.identity = identity;
this.resourceServer = resourceServer;
this.resourceManager = resourceManager;
this.authorization = authorization;
}
@POST
@Consumes("application/json")
@Produces("application/json")
public Response create(@Context UriInfo uriInfo, UmaResourceRepresentation umaResource) {
public Response create(@Context UriInfo uriInfo, UmaResourceRepresentation resource) {
checkResourceServerSettings();
if (umaResource == null) {
if (resource == null) {
return Response.status(Status.BAD_REQUEST).build();
}
return this.resourceManager.create(uriInfo, toResourceRepresentation(umaResource), (Function<Resource, UmaResourceRepresentation>) this::toUmaRepresentation);
ResourceOwnerRepresentation owner = resource.getOwner();
if (owner == null) {
owner = new ResourceOwnerRepresentation();
resource.setOwner(owner);
}
String ownerId = owner.getId();
if (ownerId == null) {
ownerId = this.identity.getId();
}
owner.setId(ownerId);
ResourceRepresentation newResource = resourceManager.create(resource);
resourceManager.audit(uriInfo, resource, resource.getId(), OperationType.CREATE);
return Response.status(Status.CREATED).entity(new UmaResourceRepresentation(newResource)).build();
}
@Path("{id}")
@PUT
@Consumes("application/json")
@Produces("application/json")
public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, UmaResourceRepresentation representation) {
ResourceRepresentation resource = toResourceRepresentation(representation);
Response response = this.resourceManager.update(uriInfo, id, resource);
if (response.getEntity() instanceof ResourceRepresentation) {
return Response.noContent().build();
}
return response;
public Response update(@Context UriInfo uriInfo, @PathParam("id") String id, ResourceRepresentation resource) {
return this.resourceManager.update(uriInfo, id, resource);
}
@Path("/{id}")
@ -102,7 +109,7 @@ public class ResourceService {
@GET
@Produces("application/json")
public Response findById(@PathParam("id") String id) {
return this.resourceManager.findById(id, (Function<Resource, UmaResourceRepresentation>) resource -> toUmaRepresentation(resource));
return this.resourceManager.findById(id, UmaResourceRepresentation::new);
}
@GET
@ -114,75 +121,11 @@ public class ResourceService {
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
@QueryParam("matchingUri") Boolean matchingUri,
@QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
return resourceManager.find(id, name, uri, owner, type, scope, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, String>) (resource, deep1) -> resource.getId());
}
private ResourceRepresentation toResourceRepresentation(UmaResourceRepresentation umaResource) {
ResourceRepresentation resource = new ResourceRepresentation();
resource.setId(umaResource.getId());
resource.setIconUri(umaResource.getIconUri());
resource.setName(umaResource.getName());
resource.setUri(umaResource.getUri());
resource.setType(umaResource.getType());
resource.setOwnerManagedAccess(umaResource.getOwnerManagedAccess());
ResourceOwnerRepresentation owner = new ResourceOwnerRepresentation();
String ownerId = umaResource.getOwner();
if (ownerId == null) {
ownerId = this.identity.getId();
}
owner.setId(ownerId);
resource.setOwner(owner);
resource.setScopes(umaResource.getScopes().stream().map(representation -> {
ScopeRepresentation scopeRepresentation = new ScopeRepresentation();
scopeRepresentation.setId(representation.getId());
scopeRepresentation.setName(representation.getName());
scopeRepresentation.setIconUri(representation.getIconUri());
return scopeRepresentation;
}).collect(Collectors.toSet()));
resource.setAttributes(umaResource.getAttributes());
return resource;
}
private UmaResourceRepresentation toUmaRepresentation(Resource model) {
if (model == null) {
return null;
}
UmaResourceRepresentation resource = new UmaResourceRepresentation();
resource.setId(model.getId());
resource.setIconUri(model.getIconUri());
resource.setName(model.getName());
resource.setUri(model.getUri());
resource.setType(model.getType());
if (model.getOwner() != null) {
resource.setOwner(model.getOwner());
}
resource.setScopes(model.getScopes().stream().map(scopeRepresentation -> {
UmaScopeRepresentation umaScopeRep = new UmaScopeRepresentation();
umaScopeRep.setId(scopeRepresentation.getId());
umaScopeRep.setName(scopeRepresentation.getName());
umaScopeRep.setIconUri(scopeRepresentation.getIconUri());
return umaScopeRep;
}).collect(Collectors.toSet()));
resource.setAttributes(model.getAttributes());
return resource;
return resourceManager.find(id, name, uri, owner, type, scope, matchingUri, deep, firstResult, maxResult, (BiFunction<Resource, Boolean, String>) (resource, deep1) -> resource.getId());
}
private void checkResourceServerSettings() {

View file

@ -0,0 +1,72 @@
/*
* Copyright 2018 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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 org.keycloak.authorization.protection.resource;
import java.util.Set;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.authorization.model.Resource;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class UmaResourceRepresentation extends ResourceRepresentation {
public UmaResourceRepresentation() {
}
public UmaResourceRepresentation(ResourceRepresentation resource) {
setId(resource.getId());
setName(resource.getName());
setType(resource.getType());
setUri(resource.getUri());
setIconUri(resource.getIconUri());
setOwner(resource.getOwner());
setScopes(resource.getScopes());
setDisplayName(resource.getDisplayName());
setOwnerManagedAccess(resource.getOwnerManagedAccess());
}
public UmaResourceRepresentation(Resource resource) {
setId(resource.getId());
setName(resource.getName());
setType(resource.getType());
setUri(resource.getUri());
setIconUri(resource.getIconUri());
setOwner(resource.getOwner());
setScopes(resource.getScopes().stream().map(scope -> new ScopeRepresentation(scope.getName())).collect(Collectors.toSet()));
setDisplayName(resource.getDisplayName());
setOwnerManagedAccess(resource.isOwnerManagedAccess());
setAttributes(resource.getAttributes());
}
@JsonProperty("resource_scopes")
@Override
public Set<ScopeRepresentation> getScopes() {
return super.getScopes();
}
@JsonProperty("resource_scopes")
@Override
public void setScopes(Set<ScopeRepresentation> scopes) {
super.setScopes(scopes);
}
}

View file

@ -1,50 +0,0 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.keycloak.authorization.protection.resource.representation;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class RegistrationResponse {
private final UmaResourceRepresentation resourceDescription;
public RegistrationResponse(UmaResourceRepresentation resourceDescription) {
this.resourceDescription = resourceDescription;
}
public RegistrationResponse() {
this(null);
}
@JsonUnwrapped
public UmaResourceRepresentation getResourceDescription() {
return this.resourceDescription;
}
public String getId() {
if (this.resourceDescription != null) {
return this.resourceDescription.getId();
}
return null;
}
}

View file

@ -1,176 +0,0 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.keycloak.authorization.protection.resource.representation;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* <p>One or more resources that the resource server manages as a set of protected resources.
*
* <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.2">OAuth-resource-reg</a>.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class UmaResourceRepresentation {
@JsonProperty("_id")
private String id;
private String name;
private String uri;
private String type;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonProperty("resource_scopes")
private Set<UmaScopeRepresentation> scopes;
@JsonProperty("icon_uri")
private String iconUri;
private String owner;
private Boolean ownerManagedAccess;
private Map<String, List<String>> attributes;
/**
* Creates a new instance.
*
* @param name a human-readable string describing a set of one or more resources
* @param uri a {@link URI} that provides the network location for the resource set being registered
* @param type a string uniquely identifying the semantics of the resource set
* @param scopes the available scopes for this resource set
* @param iconUri a {@link URI} for a graphic icon representing the resource set
*/
public UmaResourceRepresentation(String name, Set<UmaScopeRepresentation> scopes, String uri, String type, String iconUri) {
this.name = name;
this.scopes = scopes;
this.uri = uri;
this.type = type;
this.iconUri = iconUri;
}
/**
* Creates a new instance.
*
* @param name a human-readable string describing a set of one or more resources
* @param uri a {@link URI} that provides the network location for the resource set being registered
* @param type a string uniquely identifying the semantics of the resource set
* @param scopes the available scopes for this resource set
*/
public UmaResourceRepresentation(String name, Set<UmaScopeRepresentation> scopes, String uri, String type) {
this(name, scopes, uri, type, null);
}
/**
* Creates a new instance.
*
* @param name a human-readable string describing a set of one or more resources
* @param serverUri a {@link URI} that identifies this resource server
* @param scopes the available scopes for this resource set
*/
public UmaResourceRepresentation(String name, Set<UmaScopeRepresentation> scopes) {
this(name, scopes, null, null, null);
}
/**
* Creates a new instance.
*
*/
public UmaResourceRepresentation() {
this(null, null, null, null, null);
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getUri() {
return this.uri;
}
public String getType() {
return this.type;
}
public Set<UmaScopeRepresentation> getScopes() {
return Collections.unmodifiableSet(this.scopes);
}
public String getIconUri() {
return this.iconUri;
}
public void setName(String name) {
this.name = name;
}
public void setUri(String uri) {
this.uri = uri;
}
public void setType(String type) {
this.type = type;
}
public void setScopes(Set<UmaScopeRepresentation> scopes) {
this.scopes = scopes;
}
public void setIconUri(String iconUri) {
this.iconUri = iconUri;
}
public String getOwner() {
return owner;
}
public void setOwner(String owner) {
this.owner = owner;
}
public void setOwnerManagedAccess(Boolean ownerManagedAccess) {
this.ownerManagedAccess = ownerManagedAccess;
}
public Boolean getOwnerManagedAccess() {
return ownerManagedAccess;
}
public Map<String, List<String>> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
}

View file

@ -1,98 +0,0 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2016 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* 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 org.keycloak.authorization.protection.resource.representation;
import java.net.URI;
import java.util.Objects;
/**
* <p>A bounded extent of access that is possible to perform on a resource set. In authorization policy terminology,
* a scope is one of the potentially many "verbs" that can logically apply to a resource set ("object").
*
* <p>For more details, <a href="https://docs.kantarainitiative.org/uma/draft-oauth-resource-reg.html#rfc.section.2.1">OAuth-resource-reg</a>.
*
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class UmaScopeRepresentation {
private String id;
private String name;
private String iconUri;
/**
* Creates an instance.
*
* @param name the a human-readable string describing some scope (extent) of access
* @param iconUri a {@link URI} for a graphic icon representing the scope
*/
public UmaScopeRepresentation(String name, String iconUri) {
this.name = name;
this.iconUri = iconUri;
}
/**
* Creates an instance.
*
* @param name the a human-readable string describing some scope (extent) of access
*/
public UmaScopeRepresentation(String name) {
this(name, null);
}
/**
* Creates an instance.
*/
public UmaScopeRepresentation() {
this(null, null);
}
public String getName() {
return this.name;
}
public String getIconUri() {
return this.iconUri;
}
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UmaScopeRepresentation scope = (UmaScopeRepresentation) o;
return Objects.equals(getName(), scope.getName());
}
public int hashCode() {
return Objects.hash(getName());
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setIconUri(String iconUri) {
this.iconUri = iconUri;
}
}

View file

@ -192,8 +192,9 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
}
}
protected void send(String subjectKey, String template, Map<String, Object> attributes) throws EmailException {
send(subjectKey, Collections.emptyList(), template, attributes);
@Override
public void send(String subjectFormatKey, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
send(subjectFormatKey, Collections.emptyList(), bodyTemplate, bodyAttributes);
}
protected EmailTemplate processTemplate(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
@ -229,9 +230,10 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
return session.theme().getTheme(Theme.Type.EMAIL);
}
protected void send(String subjectKey, List<Object> subjectAttributes, String template, Map<String, Object> attributes) throws EmailException {
@Override
public void send(String subjectFormatKey, List<Object> subjectAttributes, String bodyTemplate, Map<String, Object> bodyAttributes) throws EmailException {
try {
EmailTemplate email = processTemplate(subjectKey, subjectAttributes, template, attributes);
EmailTemplate email = processTemplate(subjectFormatKey, subjectAttributes, bodyTemplate, bodyAttributes);
send(email.getSubject(), email.getTextBody(), email.getHtmlBody());
} catch (EmailException e) {
throw e;

View file

@ -68,6 +68,7 @@ import org.keycloak.representations.idm.ScopeMappingRepresentation;
import org.keycloak.representations.idm.UserConsentRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceOwnerRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ResourceServerRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
@ -315,7 +316,7 @@ public class ExportUtils {
ResourceRepresentation rep = toRepresentation(resource, settingsModel, authorization);
if (rep.getOwner().getId().equals(settingsModel.getId())) {
rep.setOwner(null);
rep.setOwner((ResourceOwnerRepresentation) null);
} else {
rep.getOwner().setId(null);
}

View file

@ -332,9 +332,25 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
Properties messagesBundle = handleThemeResources(theme, locale);
FormMessage msg = new FormMessage(null, message);
return formatMessage(msg, messagesBundle, locale);
}
@Override
public String getMessage(String message, String... parameters) {
Theme theme;
try {
theme = getTheme();
} catch (IOException e) {
logger.error("Failed to create theme", e);
throw new RuntimeException("Failed to create theme");
}
Locale locale = session.getContext().resolveLocale(user);
Properties messagesBundle = handleThemeResources(theme, locale);
FormMessage msg = new FormMessage(message, parameters);
return formatMessage(msg, messagesBundle, locale);
}
/**
* Create common attributes used in all templates.
*

View file

@ -71,6 +71,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
public static final String MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
public static final String PROMPT_PARAM = OAuth2Constants.PROMPT;
public static final String LOGIN_HINT_PARAM = "login_hint";
public static final String DISPLAY_PARAM = "display";
public static final String REQUEST_PARAM = "request";
public static final String REQUEST_URI_PARAM = "request_uri";
public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM;

View file

@ -371,6 +371,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr());
if (request.getDisplay() != null) authenticationSession.setClientNote(OAuth2Constants.DISPLAY, request.getDisplay());
// https://tools.ietf.org/html/rfc7636#section-4
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());

View file

@ -164,12 +164,11 @@ public class LogoutEndpoint {
*
* returns 204 if successful, 400 if not with a json error response.
*
* @param authorizationHeader
* @return
*/
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response logoutToken(final @HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader) {
public Response logoutToken() {
MultivaluedMap<String, String> form = request.getDecodedFormParameters();
checkSsl();

View file

@ -776,8 +776,15 @@ public class TokenEndpoint {
String audience = formParams.getFirst(OAuth2Constants.AUDIENCE);
if (audience != null) {
targetClient = realm.getClientByClientId(audience);
if (targetClient == null) {
event.detail(Details.REASON, "audience not found");
event.error(Errors.CLIENT_NOT_FOUND);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Audience not found", Response.Status.BAD_REQUEST);
}
}
if (targetClient.isConsentRequired()) {
event.detail(Details.REASON, "audience requires consent");
event.error(Errors.CONSENT_DENIED);

View file

@ -32,6 +32,7 @@ public class AuthorizationEndpointRequest {
String state;
String scope;
String loginHint;
String display;
String prompt;
String nonce;
Integer maxAge;
@ -111,4 +112,7 @@ public class AuthorizationEndpointRequest {
return codeChallengeMethod;
}
public String getDisplay() {
return display;
}
}

View file

@ -18,6 +18,7 @@
package org.keycloak.protocol.oidc.endpoints.request;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -91,6 +92,7 @@ abstract class AuthzEndpointRequestParser {
request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
request.claims = replaceIfNotNull(request.claims, getParameter(OIDCLoginProtocol.CLAIMS_PARAM));
request.acr = replaceIfNotNull(request.acr, getParameter(OIDCLoginProtocol.ACR_PARAM));
request.display = replaceIfNotNull(request.display, getParameter(OAuth2Constants.DISPLAY));
// https://tools.ietf.org/html/rfc7636#section-6.1
request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));

View file

@ -155,7 +155,7 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide
PolicyEnforcerConfig enforcerConfig = new PolicyEnforcerConfig();
enforcerConfig.setEnforcementMode(null);
enforcerConfig.setCreateResources(null);
enforcerConfig.setLazyLoadPaths(null);
rep.setEnforcerConfig(enforcerConfig);

View file

@ -21,11 +21,7 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.*;
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.keycloak.broker.provider.IdentityProvider;
import org.keycloak.common.ClientConnection;
@ -761,6 +757,11 @@ public class AuthenticationManager {
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
if (uriInfo.getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, authSession.getParentSession().getId());
}
URI redirect = uriBuilder.build(realm.getName());
return Response.status(302).location(redirect).build();
@ -965,6 +966,25 @@ public class AuthenticationManager {
authSession.setProtocolMappers(requestedProtocolMappers);
}
public static RequiredActionProvider createRequiredAction(KeycloakSession session, RequiredActionFactory factory, AuthenticationSessionModel authSession) {
String display = authSession.getClientNote(OAuth2Constants.DISPLAY);
if (display == null) return factory.create(session);
if (factory instanceof DisplayTypeRequiredActionFactory) {
RequiredActionProvider provider = ((DisplayTypeRequiredActionFactory)factory).createDisplay(session, display);
if (provider != null) return provider;
}
// todo create a provider for handling lack of display support
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, TextChallenge.browserRequired(session));
} else {
return factory.create(session);
}
}
protected static Response executionActions(KeycloakSession session, AuthenticationSessionModel authSession,
HttpRequest request, EventBuilder event, RealmModel realm, UserModel user,
Set<String> requiredActions) {
@ -982,7 +1002,15 @@ public class AuthenticationManager {
if (factory == null) {
throw new RuntimeException("Unable to find factory for Required Action: " + model.getProviderId() + " did you forget to declare it in a META-INF/services file?");
}
RequiredActionProvider actionProvider = factory.create(session);
RequiredActionProvider actionProvider = null;
try {
actionProvider = createRequiredAction(session, factory, authSession);
} catch (AuthenticationFlowException e) {
if (e.getResponse() != null) {
return e.getResponse();
}
throw e;
}
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, user, factory);
actionProvider.requiredActionChallenge(context);

View file

@ -21,6 +21,7 @@ package org.keycloak.services.messages;
*/
public class Messages {
public static final String DISPLAY_UNSUPPORTED = "displayUnsupported";
public static final String LOGIN_TIMEOUT = "loginTimeout";
public static final String INVALID_USER = "invalidUserMessage";

View file

@ -16,17 +16,12 @@
*/
package org.keycloak.services.resources;
import org.keycloak.authentication.*;
import org.keycloak.authentication.actiontoken.DefaultActionTokenKey;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionContextResult;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.TokenVerifier;
import org.keycloak.authentication.ExplainedVerificationException;
import org.keycloak.authentication.actiontoken.*;
import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
@ -934,7 +929,15 @@ public class LoginActionsService {
event.error(Errors.INVALID_CODE);
throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.INVALID_CODE));
}
RequiredActionProvider provider = factory.create(session);
RequiredActionProvider provider = null;
try {
provider = AuthenticationManager.createRequiredAction(session, factory, authSession);
} catch (AuthenticationFlowException e) {
if (e.getResponse() != null) {
return e.getResponse();
}
throw new WebApplicationException(ErrorPage.error(session, authSession, Response.Status.BAD_REQUEST, Messages.DISPLAY_UNSUPPORTED));
}
RequiredActionContextResult context = new RequiredActionContextResult(authSession, realm, event, session, request, authSession.getAuthenticatedUser(), factory) {
@Override

View file

@ -40,6 +40,7 @@ import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
import org.keycloak.theme.beans.MessageFormatterMethod;
/**
* Created by st on 29/03/17.
@ -101,7 +102,8 @@ public class AccountConsole {
if (auth != null) {
Locale locale = session.getContext().resolveLocale(auth.getUser());
map.put("locale", locale.toLanguageTag());
map.put("msg", messagesToJsonString(theme.getMessages(locale)));
Properties messages = theme.getMessages(locale);
map.put("msg", messagesToJsonString(messages));
}
} catch (Exception e) {
logger.warn("Failed to load messages", e);

View file

@ -196,6 +196,10 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
return admin;
}
public RealmModel adminsRealm() {
return adminsRealm;
}
@Override
public RolePermissions roles() {

View file

@ -230,7 +230,20 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
} else {
return true;
}
} else {
} else if (role.getName().equals(AdminRoles.REALM_ADMIN)) {
// check to see if we have masterRealm.admin role. Otherwise abort
if (root.adminsRealm() == null || !root.adminsRealm().getName().equals(Config.getAdminRealm())) {
return adminConflictMessage(role);
}
RealmModel masterRealm = root.adminsRealm();
RoleModel adminRole = masterRealm.getRole(AdminRoles.ADMIN);
if (root.admin().hasRole(adminRole)) {
return true;
} else {
return adminConflictMessage(role);
}
} else {
return adminConflictMessage(role);
}
@ -239,6 +252,7 @@ class RolePermissions implements RolePermissionEvaluator, RolePermissionManageme
if (role.getContainer() instanceof RealmModel) {
RealmModel realm = (RealmModel)role.getContainer();
// If realm role is master admin role then abort
// if realm name is master realm, than we know this is a admin role in master realm.
if (realm.getName().equals(Config.getAdminRealm())) {
return adminConflictMessage(role);
}

View file

@ -45,6 +45,12 @@ public class TotpUtils {
return sb.toString();
}
public static String decode(String totpSecretEncoded) {
String encoded = totpSecretEncoded.replace(" ", "");
byte[] bytes = Base32.decode(encoded);
return new String(bytes);
}
public static String qrCode(String totpSecret, RealmModel realm, UserModel user) {
try {
String keyUri = realm.getOTPPolicy().getKeyURI(realm, user, totpSecret);

View file

@ -38,4 +38,4 @@ org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFacto
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
org.keycloak.protocol.docker.DockerAuthenticatorFactory
org.keycloak.authentication.authenticators.cli.CliUsernamePasswordAuthenticatorFactory
org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory

Some files were not shown because too many files have changed in this diff Show more