diff --git a/adapters/oidc/installed/pom.xml b/adapters/oidc/installed/pom.xml
index d740fc14cf..e1d82794de 100755
--- a/adapters/oidc/installed/pom.xml
+++ b/adapters/oidc/installed/pom.xml
@@ -63,6 +63,15 @@
org.jboss.logging
jboss-logging
+
+ org.jboss.resteasy
+ resteasy-client
+
+
+ org.jboss.spec.javax.ws.rs
+ jboss-jaxrs-api_2.0_spec
+
+
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
index 3c1d3655a1..fcd191d529 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakCliSso.java
@@ -53,6 +53,8 @@ public class KeycloakCliSso {
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")) {
@@ -69,6 +71,7 @@ public class KeycloakCliSso {
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);
@@ -110,7 +113,7 @@ public class KeycloakCliSso {
return config;
}
- public boolean checkToken() throws Exception {
+ public boolean checkToken(boolean outputToken) throws Exception {
String token = getTokenResponse();
if (token == null) return false;
@@ -127,7 +130,7 @@ public class KeycloakCliSso {
AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
installed.refreshToken(tokenResponse.getRefreshToken());
- processResponse(installed);
+ processResponse(installed, outputToken);
return true;
} catch (Exception e) {
System.err.println("Error processing existing token");
@@ -184,11 +187,19 @@ public class KeycloakCliSso {
}
public void login() throws Exception {
- if (checkToken()) return;
+ if (checkToken(true)) return;
AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
installed.login();
- processResponse(installed);
+ 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() {
@@ -210,7 +221,7 @@ public class KeycloakCliSso {
return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile();
}
- private void processResponse(KeycloakInstalled installed) throws IOException {
+ private void processResponse(KeycloakInstalled installed, boolean outputToken) throws IOException {
AccessTokenResponse tokenResponse = installed.getTokenResponse();
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
tokenResponse.setIdToken(null);
@@ -220,16 +231,16 @@ public class KeycloakCliSso {
fos.write(output.getBytes("UTF-8"));
fos.flush();
fos.close();
- System.out.println(tokenResponse.getToken());
+ if (outputToken) System.out.println(tokenResponse.getToken());
}
public void loginManual() throws Exception {
- if (checkToken()) return;
+ if (checkToken(true)) return;
AdapterConfig config = getConfig();
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
KeycloakInstalled installed = new KeycloakInstalled(deployment);
installed.loginManual();
- processResponse(installed);
+ processResponse(installed, true);
}
public void logout() throws Exception {
diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
index a0606858ef..443a45c7af 100644
--- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
+++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java
@@ -17,6 +17,8 @@
package org.keycloak.adapters.installed;
+import org.jboss.resteasy.client.jaxrs.ResteasyClient;
+import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.adapters.KeycloakDeployment;
@@ -31,6 +33,11 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+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;
@@ -39,6 +46,7 @@ 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.net.ServerSocket;
import java.net.Socket;
@@ -47,6 +55,8 @@ import java.net.URISyntaxException;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* @author Stian Thorgersen
@@ -76,6 +86,9 @@ public class KeycloakInstalled {
private Locale locale;
private HttpResponseWriter loginResponseWriter;
private HttpResponseWriter logoutResponseWriter;
+ Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
+ Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"\\s+mask=(\\S+)");
+ Pattern codePattern = Pattern.compile("code=([^&]+)");
@@ -289,6 +302,86 @@ public class KeycloakInstalled {
status = Status.LOGGED_MANUAL;
}
+ public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
+ String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
+
+ return loginCommandLine(redirectUri);
+ }
+
+
+
+ /**
+ * Proprietary WWW-Authentication challenge protocol.
+ * WWW-Authentication: X-Text-Form-Challenge callback="{url}" param="{param-name}" label="{param-display-label}"
+ *
+ * @param redirectUri
+ * @return
+ * @throws IOException
+ * @throws ServerRequest.HttpFailure
+ * @throws VerificationException
+ */
+ public boolean loginCommandLine(String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException {
+ String authUrl = deployment.getAuthUrl().clone()
+ .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
+ .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
+ .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
+ .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
+ .build().toString();
+ ResteasyClient client = new ResteasyClientBuilder().disableTrustManager().build();
+ try {
+ 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);
+ } else {
+ value = System.console().readLine(label);
+ }
+ form.param(param, value);
+ }
+ 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;
+ }
+ } finally {
+ client.close();
+
+ }
+ }
+
+
public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
return tokenString;
}
@@ -381,6 +474,86 @@ 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 {
diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
index 2f5576efd3..f9b49c3ac3 100755
--- a/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
+++ b/server-spi-private/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java
@@ -79,6 +79,15 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/
URI getActionUrl(String code);
+ /**
+ * Get the action URL for the required action.
+ *
+ * @param code authentication session access code
+ * @param authSessionIdParam will include auth_session query param for clients that don't process cookies
+ * @return
+ */
+ URI getActionUrl(String code, boolean authSessionIdParam);
+
/**
* Get the action URL for the action token executor.
*
@@ -94,6 +103,14 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/
URI getRefreshExecutionUrl();
+ /**
+ * Get the refresh URL for the flow.
+ *
+ * @param authSessionIdParam will include auth_session query param for clients that don't process cookies
+ * @return
+ */
+ URI getRefreshUrl(boolean authSessionIdParam);
+
/**
* End the flow and redirect browser based on protocol specific respones. This should only be executed
* in browser-based flows.
diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
index af20f5b3c6..256b87fe7f 100755
--- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java
@@ -52,6 +52,8 @@ public interface LoginFormsProvider extends Provider {
Response createForm(String form);
+ String getMessage(String message);
+
Response createLogin();
Response createPasswordReset();
@@ -122,6 +124,8 @@ public interface LoginFormsProvider extends Provider {
LoginFormsProvider setStatus(Response.Status status);
+ LoginFormsProvider setMediaType(javax.ws.rs.core.MediaType type);
+
LoginFormsProvider setActionUri(URI requestUri);
LoginFormsProvider setExecution(String execution);
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
index 4a29ae1971..537581a72f 100755
--- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
+++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java
@@ -60,6 +60,7 @@ import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.HashMap;
@@ -487,32 +488,72 @@ public class AuthenticationProcessor {
@Override
public URI getActionUrl(String code) {
- return LoginActionsService.loginActionsBaseUrl(getUriInfo())
+ UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(AuthenticationProcessor.this.flowPath)
.queryParam(LoginActionsService.SESSION_CODE, code)
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
- .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
+ .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+ if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+ uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+ }
+ return uriBuilder
.build(getRealm().getName());
}
@Override
public URI getActionTokenUrl(String tokenString) {
- return LoginActionsService.actionTokenProcessor(getUriInfo())
+ UriBuilder uriBuilder = LoginActionsService.actionTokenProcessor(getUriInfo())
.queryParam(Constants.KEY, tokenString)
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
- .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
+ .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+ if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+ uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+ }
+ return uriBuilder
+ .build(getRealm().getName());
+ }
+
+ @Override
+ public URI getActionUrl(String code, boolean authSessionIdParam) {
+ UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
+ .path(AuthenticationProcessor.this.flowPath)
+ .queryParam(LoginActionsService.SESSION_CODE, code)
+ .queryParam(Constants.EXECUTION, getExecution().getId())
+ .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
+ .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+ if (authSessionIdParam) {
+ uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+ }
+ return uriBuilder
.build(getRealm().getName());
}
@Override
public URI getRefreshExecutionUrl() {
- return LoginActionsService.loginActionsBaseUrl(getUriInfo())
+ UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(AuthenticationProcessor.this.flowPath)
.queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
- .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
+ .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+ if (getUriInfo().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+ uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+ }
+ return uriBuilder
+ .build(getRealm().getName());
+ }
+
+ @Override
+ public URI getRefreshUrl(boolean authSessionIdParam) {
+ UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
+ .path(AuthenticationProcessor.this.flowPath)
+ .queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
+ .queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId());
+ if (authSessionIdParam) {
+ uriBuilder.queryParam(LoginActionsService.AUTH_SESSION_ID, getAuthenticationSession().getParentSession().getId());
+ }
+ return uriBuilder
.build(getRealm().getName());
}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
index cb2ce3b085..f5b846483a 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
@@ -46,7 +46,7 @@ public class ActionTokenContext {
@FunctionalInterface
public interface ProcessBrokerFlow {
- Response brokerLoginFlow(String code, String execution, String clientId, String tabId, String flowPath);
+ Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath);
};
private final KeycloakSession session;
@@ -160,8 +160,8 @@ public class ActionTokenContext {
return processAuthenticateFlow.processFlow(action, getExecutionId(), getAuthenticationSession(), flowPath, flow, errorMessage, processor);
}
- public Response brokerFlow(String code, String flowPath) {
+ public Response brokerFlow(String authSessionId, String code, String flowPath) {
ClientModel client = authenticationSession.getClient();
- return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), flowPath);
+ return processBrokerFlow.brokerLoginFlow(authSessionId, code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), flowPath);
}
}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
index ebd4700c52..306dd05f8e 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
@@ -122,7 +122,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
- return tokenContext.brokerFlow(null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
+ return tokenContext.brokerFlow(null, null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
}
}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticator.java
new file mode 100755
index 0000000000..0989d94ac7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticator.java
@@ -0,0 +1,149 @@
+/*
+ * 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.cli;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.Authenticator;
+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 Bill Burke
+ * @version $Revision: 1 $
+ */
+public class CliUsernamePasswordAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ String header = getHeader(context);
+ Response response = context.form()
+ .setStatus(Response.Status.UNAUTHORIZED)
+ .setMediaType(MediaType.TEXT_PLAIN_TYPE)
+ .setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, header)
+ .createForm("cli_splash.ftl");
+ context.challenge(response);
+
+
+ }
+
+ private String getHeader(AuthenticationFlowContext context) {
+ URI callback = getCallbackUrl(context);
+ return "X-Text-Form-Challenge callback=\"" + callback + "\" param=\"username\" label=\"Username: \" mask=false param=\"password\" label=\"Password: \" mask=true";
+ }
+
+ private URI getCallbackUrl(AuthenticationFlowContext context) {
+ return context.getActionUrl(context.generateAccessCode(), true);
+ }
+
+ @Override
+ protected Response invalidUser(AuthenticationFlowContext context) {
+ String header = getHeader(context);
+ Response response = Response.status(401)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .header(HttpHeaders.WWW_AUTHENTICATE, header)
+ .entity("\n" + context.form().getMessage(Messages.INVALID_USER) + "\n")
+ .build();
+ return response;
+ }
+
+ @Override
+ protected Response disabledUser(AuthenticationFlowContext context) {
+ String header = getHeader(context);
+ Response response = Response.status(401)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .header(HttpHeaders.WWW_AUTHENTICATE, header)
+ .entity("\n" + context.form().getMessage(Messages.ACCOUNT_DISABLED) + "\n")
+ .build();
+ return response;
+ }
+
+ @Override
+ protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
+ String header = getHeader(context);
+ Response response = Response.status(401)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .header(HttpHeaders.WWW_AUTHENTICATE, header)
+ .entity("\n" + context.form().getMessage(Messages.INVALID_USER) + "\n")
+ .build();
+ return response;
+ }
+
+ @Override
+ protected Response invalidCredentials(AuthenticationFlowContext context) {
+ String header = getHeader(context);
+ Response response = Response.status(401)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .header(HttpHeaders.WWW_AUTHENTICATE, header)
+ .entity("\n" + context.form().getMessage(Messages.INVALID_USER) + "\n")
+ .build();
+ return response;
+ }
+
+ @Override
+ protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
+ context.getEvent().error(eventError);
+ String header = getHeader(context);
+ Response challengeResponse = Response.status(401)
+ .type(MediaType.TEXT_PLAIN_TYPE)
+ .header(HttpHeaders.WWW_AUTHENTICATE, header)
+ .entity("\n" + context.form().getMessage(loginFormError) + "\n")
+ .build();
+
+ context.failureChallenge(authenticatorError, challengeResponse);
+ return challengeResponse;
+ }
+
+ @Override
+ public void action(AuthenticationFlowContext context) {
+ MultivaluedMap 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() {
+
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticatorFactory.java
new file mode 100755
index 0000000000..cce13c0ed9
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/cli/CliUsernamePasswordAuthenticatorFactory.java
@@ -0,0 +1,104 @@
+/*
+ * 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.cli;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm;
+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 Bill Burke
+ * @version $Revision: 1 $
+ */
+public class CliUsernamePasswordAuthenticatorFactory implements AuthenticatorFactory {
+
+ public static final String PROVIDER_ID = "cli-username-password";
+ public static final CliUsernamePasswordAuthenticator SINGLETON = new CliUsernamePasswordAuthenticator();
+
+ @Override
+ public Authenticator create(KeycloakSession session) {
+ return 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 getConfigProperties() {
+ return null;
+ }
+
+ @Override
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
index 9ef57341af..cf480fcbcf 100755
--- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
+++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java
@@ -74,6 +74,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
protected String accessCode;
protected Response.Status status;
+ protected javax.ws.rs.core.MediaType contentType;
protected List realmRolesRequested;
protected MultivaluedMap resourceRolesRequested;
protected List protocolMappersRequested;
@@ -316,6 +317,23 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
attributes.put("messagesPerField", messagesPerField);
}
+
+ @Override
+ public String getMessage(String message) {
+ 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(null, message);
+ return formatMessage(msg, messagesBundle, locale);
+
+ }
/**
* Create common attributes used in all templates.
@@ -390,7 +408,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
protected Response processTemplate(Theme theme, String templateName, Locale locale) {
try {
String result = freeMarker.processTemplate(attributes, templateName, theme);
- Response.ResponseBuilder builder = Response.status(status == null ? Response.Status.OK : status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result);
+ javax.ws.rs.core.MediaType mediaType = contentType == null ? MediaType.TEXT_HTML_UTF_8_TYPE : contentType;
+ Response.ResponseBuilder builder = Response.status(status == null ? Response.Status.OK : status).type(mediaType).language(locale).entity(result);
BrowserSecurityHeaderSetup.headers(builder, realm);
for (Map.Entry entry : httpResponseHeaders.entrySet()) {
builder.header(entry.getKey(), entry.getValue());
@@ -603,6 +622,14 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
this.status = status;
return this;
}
+ @Override
+ public LoginFormsProvider setMediaType(javax.ws.rs.core.MediaType type) {
+ this.contentType = type;
+ return this;
+ }
+
+
+
@Override
public LoginFormsProvider setActionUri(URI actionUri) {
diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index 7a8479cae1..46a4508113 100755
--- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -91,29 +91,51 @@ public class ClientSessionCode
try {
CodeGenerateUtil.ClientSessionParser clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
result.clientSession = getClientSession(code, tabId, session, realm, client, event, clientSessionParser);
- if (result.clientSession == null) {
- result.authSessionNotFound = true;
- return result;
- }
-
- if (!clientSessionParser.verifyCode(session, code, result.clientSession)) {
- result.illegalHash = true;
- return result;
- }
-
- if (clientSessionParser.isExpired(session, code, result.clientSession)) {
- result.expiredToken = true;
- return result;
- }
-
- result.code = new ClientSessionCode(session, realm, result.clientSession);
- return result;
+ return parseResult(code, session, realm, result, clientSessionParser);
} catch (RuntimeException e) {
result.illegalHash = true;
return result;
}
}
+ public static ParseResult parseResult(String code, String tabId,
+ KeycloakSession session, RealmModel realm, ClientModel client,
+ EventBuilder event, CLIENT_SESSION clientSession) {
+ ParseResult result = new ParseResult<>();
+ result.clientSession = clientSession;
+ if (code == null) {
+ result.illegalHash = true;
+ return result;
+ }
+ try {
+ CodeGenerateUtil.ClientSessionParser clientSessionParser = CodeGenerateUtil.getParser((Class)clientSession.getClass());
+ return parseResult(code, session, realm, result, clientSessionParser);
+ } catch (RuntimeException e) {
+ result.illegalHash = true;
+ return result;
+ }
+ }
+
+ private static ParseResult parseResult(String code, KeycloakSession session, RealmModel realm, ParseResult result, CodeGenerateUtil.ClientSessionParser clientSessionParser) {
+ if (result.clientSession == null) {
+ result.authSessionNotFound = true;
+ return result;
+ }
+
+ if (!clientSessionParser.verifyCode(session, code, result.clientSession)) {
+ result.illegalHash = true;
+ return result;
+ }
+
+ if (clientSessionParser.isExpired(session, code, result.clientSession)) {
+ result.expiredToken = true;
+ return result;
+ }
+
+ result.code = new ClientSessionCode(session, realm, result.clientSession);
+ return result;
+ }
+
public static CLIENT_SESSION getClientSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client,
EventBuilder event, Class sessionClass) {
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index c827f945b0..07db430e8c 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -991,7 +991,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return ParsedCodeContext.response(staleCodeError);
}
- SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, request, clientConnection, session, event, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH);
+ SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, request, clientConnection, session, event, null, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH);
checks.initialVerify();
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index d09badf6c3..9e3fbbe23d 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -116,6 +116,7 @@ public class LoginActionsService {
public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage";
public static final String SESSION_CODE = "session_code";
+ public static final String AUTH_SESSION_ID = "auth_session_id";
private RealmModel realm;
@@ -186,8 +187,8 @@ public class LoginActionsService {
}
}
- private SessionCodeChecks checksForCode(String code, String execution, String clientId, String tabId, String flowPath) {
- SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, code, execution, clientId, tabId, flowPath);
+ private SessionCodeChecks checksForCode(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
+ SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, authSessionId, code, execution, clientId, tabId, flowPath);
res.initialVerify();
return res;
}
@@ -206,10 +207,11 @@ public class LoginActionsService {
*/
@Path(RESTART_PATH)
@GET
- public Response restartSession(@QueryParam("client_id") String clientId,
+ public Response restartSession(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
event.event(EventType.RESTART_AUTHENTICATION);
- SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, null, null, clientId, tabId, null);
+ SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, authSessionId, null, null, clientId, tabId, null);
AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
if (authSession == null) {
@@ -237,13 +239,14 @@ public class LoginActionsService {
*/
@Path(AUTHENTICATE_PATH)
@GET
- public Response authenticate(@QueryParam(SESSION_CODE) String code,
+ public Response authenticate(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
event.event(EventType.LOGIN);
- SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, AUTHENTICATE_PATH);
+ SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, AUTHENTICATE_PATH);
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse();
}
@@ -307,16 +310,18 @@ public class LoginActionsService {
*/
@Path(AUTHENTICATE_PATH)
@POST
- public Response authenticateForm(@QueryParam(SESSION_CODE) String code,
+ public Response authenticateForm(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return authenticate(code, execution, clientId, tabId);
+ return authenticate(authSessionId, code, execution, clientId, tabId);
}
@Path(RESET_CREDENTIALS_PATH)
@POST
- public Response resetCredentialsPOST(@QueryParam(SESSION_CODE) String code,
+ public Response resetCredentialsPOST(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId,
@@ -327,7 +332,7 @@ public class LoginActionsService {
event.event(EventType.RESET_PASSWORD);
- return resetCredentials(code, execution, clientId, tabId);
+ return resetCredentials(authSessionId, code, execution, clientId, tabId);
}
/**
@@ -340,7 +345,8 @@ public class LoginActionsService {
*/
@Path(RESET_CREDENTIALS_PATH)
@GET
- public Response resetCredentialsGET(@QueryParam(SESSION_CODE) String code,
+ public Response resetCredentialsGET(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
@@ -360,7 +366,7 @@ public class LoginActionsService {
}
event.event(EventType.RESET_PASSWORD);
- return resetCredentials(code, execution, clientId, tabId);
+ return resetCredentials(authSessionId, code, execution, clientId, tabId);
}
AuthenticationSessionModel createAuthenticationSessionForClient()
@@ -390,8 +396,8 @@ public class LoginActionsService {
* @param execution
* @return
*/
- protected Response resetCredentials(String code, String execution, String clientId, String tabId) {
- SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, RESET_CREDENTIALS_PATH);
+ protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId) {
+ SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, RESET_CREDENTIALS_PATH);
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.getResponse();
}
@@ -619,11 +625,12 @@ public class LoginActionsService {
*/
@Path(REGISTRATION_PATH)
@GET
- public Response registerPage(@QueryParam(SESSION_CODE) String code,
+ public Response registerPage(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return registerRequest(code, execution, clientId, tabId,false);
+ return registerRequest(authSessionId, code, execution, clientId, tabId,false);
}
@@ -635,22 +642,23 @@ public class LoginActionsService {
*/
@Path(REGISTRATION_PATH)
@POST
- public Response processRegister(@QueryParam(SESSION_CODE) String code,
+ public Response processRegister(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return registerRequest(code, execution, clientId, tabId,true);
+ return registerRequest(authSessionId, code, execution, clientId, tabId,true);
}
- private Response registerRequest(String code, String execution, String clientId, String tabId, boolean isPostRequest) {
+ private Response registerRequest(String authSessionId, String code, String execution, String clientId, String tabId, boolean isPostRequest) {
event.event(EventType.REGISTER);
if (!realm.isRegistrationAllowed()) {
event.error(Errors.REGISTRATION_DISABLED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED);
}
- SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, REGISTRATION_PATH);
+ SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, REGISTRATION_PATH);
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse();
}
@@ -665,48 +673,52 @@ public class LoginActionsService {
@Path(FIRST_BROKER_LOGIN_PATH)
@GET
- public Response firstBrokerLoginGet(@QueryParam(SESSION_CODE) String code,
+ public Response firstBrokerLoginGet(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return brokerLoginFlow(code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
+ return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
}
@Path(FIRST_BROKER_LOGIN_PATH)
@POST
- public Response firstBrokerLoginPost(@QueryParam(SESSION_CODE) String code,
+ public Response firstBrokerLoginPost(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return brokerLoginFlow(code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
+ return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
}
@Path(POST_BROKER_LOGIN_PATH)
@GET
- public Response postBrokerLoginGet(@QueryParam(SESSION_CODE) String code,
+ public Response postBrokerLoginGet(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return brokerLoginFlow(code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
+ return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
}
@Path(POST_BROKER_LOGIN_PATH)
@POST
- public Response postBrokerLoginPost(@QueryParam(SESSION_CODE) String code,
+ public Response postBrokerLoginPost(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) String code,
@QueryParam("execution") String execution,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return brokerLoginFlow(code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
+ return brokerLoginFlow(authSessionId, code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
}
- protected Response brokerLoginFlow(String code, String execution, String clientId, String tabId, String flowPath) {
+ protected Response brokerLoginFlow(String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType);
- SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, flowPath);
+ SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, flowPath);
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse();
}
@@ -788,7 +800,7 @@ public class LoginActionsService {
String code = formData.getFirst(SESSION_CODE);
String clientId = uriInfo.getQueryParameters().getFirst(Constants.CLIENT_ID);
String tabId = uriInfo.getQueryParameters().getFirst(Constants.TAB_ID);
- SessionCodeChecks checks = checksForCode(code, null, clientId, tabId, REQUIRED_ACTION);
+ SessionCodeChecks checks = checksForCode(null, code, null, clientId, tabId, REQUIRED_ACTION);
if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) {
return checks.getResponse();
}
@@ -876,26 +888,28 @@ public class LoginActionsService {
@Path(REQUIRED_ACTION)
@POST
- public Response requiredActionPOST(@QueryParam(SESSION_CODE) final String code,
+ public Response requiredActionPOST(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) final String code,
@QueryParam("execution") String action,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return processRequireAction(code, action, clientId, tabId);
+ return processRequireAction(authSessionId, code, action, clientId, tabId);
}
@Path(REQUIRED_ACTION)
@GET
- public Response requiredActionGET(@QueryParam(SESSION_CODE) final String code,
+ public Response requiredActionGET(@QueryParam(AUTH_SESSION_ID) String authSessionId, // optional, can get from cookie instead
+ @QueryParam(SESSION_CODE) final String code,
@QueryParam("execution") String action,
@QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) {
- return processRequireAction(code, action, clientId, tabId);
+ return processRequireAction(authSessionId, code, action, clientId, tabId);
}
- private Response processRequireAction(final String code, String action, String clientId, String tabId) {
+ private Response processRequireAction(final String authSessionId, final String code, String action, String clientId, String tabId) {
event.event(EventType.CUSTOM_REQUIRED_ACTION);
- SessionCodeChecks checks = checksForCode(code, action, clientId, tabId, REQUIRED_ACTION);
+ SessionCodeChecks checks = checksForCode(authSessionId, code, action, clientId, tabId, REQUIRED_ACTION);
if (!checks.verifyRequiredAction(action)) {
return checks.getResponse();
}
diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
index 2f04bf408c..bb4097f459 100644
--- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java
@@ -71,10 +71,11 @@ public class SessionCodeChecks {
private final String clientId;
private final String tabId;
private final String flowPath;
+ private final String authSessionId;
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
- String code, String execution, String clientId, String tabId, String flowPath) {
+ String authSessionId, String code, String execution, String clientId, String tabId, String flowPath) {
this.realm = realm;
this.uriInfo = uriInfo;
this.request = request;
@@ -87,6 +88,7 @@ public class SessionCodeChecks {
this.clientId = clientId;
this.tabId = tabId;
this.flowPath = flowPath;
+ this.authSessionId = authSessionId;
}
@@ -146,14 +148,32 @@ public class SessionCodeChecks {
session.getContext().setClient(client);
}
+
// object retrieve
AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
- AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
+ AuthenticationSessionModel authSession = null;
+ if (authSessionId != null) authSession = authSessionManager.getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId);
+ AuthenticationSessionModel authSessionCookie = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
+
+ if (authSession != null && authSessionCookie != null && !authSession.getParentSession().getId().equals(authSessionCookie.getParentSession().getId())) {
+ event.detail(Details.REASON, "cookie does not match auth_session query parameter");
+ event.error(Errors.INVALID_CODE);
+ response = ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_CODE);
+ return null;
+
+ }
+
if (authSession != null) {
session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession);
return authSession;
}
+ if (authSessionCookie != null) {
+ session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSessionCookie);
+ return authSessionCookie;
+
+ }
+
// See if we are already authenticated and userSession with same ID exists.
String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm);
RootAuthenticationSessionModel existingRootAuthSession = null;
@@ -250,7 +270,7 @@ public class SessionCodeChecks {
return false;
}
} else {
- ClientSessionCode.ParseResult result = ClientSessionCode.parseResult(code, tabId, session, realm, client, event, AuthenticationSessionModel.class);
+ ClientSessionCode.ParseResult result = ClientSessionCode.parseResult(code, tabId, session, realm, client, event, authSession);
clientCode = result.getCode();
if (clientCode == null) {
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index 2b11382be1..76b75076ee 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -38,3 +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
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 8c832942df..2642a11ef7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -146,6 +146,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a username and password from login form.");
addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form",
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
+ addProviderInfo(result, "cli-username-password", "Username Password Challenge",
+ "Proprietary challenge protocol for CLI clients that queries for username password");
addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
addProviderInfo(result, "direct-grant-validate-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java
new file mode 100644
index 0000000000..72024f63f1
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ChallengeFlowTest.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2017 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.testsuite.forms;
+
+import org.jboss.arquillian.container.test.api.Deployment;
+import org.jboss.arquillian.graphene.page.Page;
+import org.jboss.shrinkwrap.api.spec.WebArchive;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
+import org.keycloak.authentication.authenticators.cli.CliUsernamePasswordAuthenticatorFactory;
+import org.keycloak.events.Details;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationFlowBindings;
+import org.keycloak.models.AuthenticationFlowModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
+import org.keycloak.testsuite.pages.AppPage;
+import org.keycloak.testsuite.pages.ErrorPage;
+import org.keycloak.testsuite.pages.LoginPage;
+import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.util.BasicAuthHelper;
+import org.openqa.selenium.By;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+import java.net.URL;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test that clients can override auth flows
+ *
+ * @author Bill Burke
+ */
+public class ChallengeFlowTest extends AbstractTestRealmKeycloakTest {
+
+ public static final String TEST_APP_DIRECT_OVERRIDE = "test-app-direct-override";
+ public static final String TEST_APP_FLOW = "test-app-flow";
+ @Rule
+ public AssertEvents events = new AssertEvents(this);
+
+ @Page
+ protected AppPage appPage;
+
+ @Page
+ protected LoginPage loginPage;
+
+ @Page
+ protected ErrorPage errorPage;
+
+ @Override
+ public void configureTestRealm(RealmRepresentation testRealm) {
+ }
+
+ @Deployment
+ public static WebArchive deploy() {
+ return RunOnServerDeployment.create(UserResource.class)
+ .addPackages(true, "org.keycloak.testsuite");
+ }
+
+
+ @Before
+ public void setupFlows() {
+ testingClient.server().run(session -> {
+ RealmModel realm = session.realms().getRealmByName("test");
+
+ ClientModel client = session.realms().getClientByClientId("test-app-flow", realm);
+ if (client != null) {
+ return;
+ }
+
+ // Parent flow
+ AuthenticationFlowModel browser = new AuthenticationFlowModel();
+ browser.setAlias("cli-challenge");
+ browser.setDescription("challenge based authentication");
+ browser.setProviderId("basic-flow");
+ browser.setTopLevel(true);
+ browser.setBuiltIn(true);
+ browser = realm.addAuthenticationFlow(browser);
+
+ // Subflow2 - push the button
+ AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+ execution.setParentFlow(browser.getId());
+ execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+ execution.setAuthenticator(CliUsernamePasswordAuthenticatorFactory.PROVIDER_ID);
+ execution.setPriority(10);
+ execution.setAuthenticatorFlow(false);
+ realm.addAuthenticatorExecution(execution);
+
+ client = realm.addClient(TEST_APP_FLOW);
+ client.setSecret("password");
+ client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
+ client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin");
+ client.setEnabled(true);
+ client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*");
+ client.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
+ client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId());
+ client.setPublicClient(false);
+ });
+ }
+
+ //@Test
+ public void testRunConsole() throws Exception {
+ Thread.sleep(10000000);
+ }
+
+
+ @Test
+ public void testChallengeFlow() throws Exception {
+ oauth.clientId(TEST_APP_FLOW);
+ String loginFormUrl = oauth.getLoginFormUrl();
+ Client client = ClientBuilder.newClient();
+ WebTarget loginTarget = client.target(loginFormUrl);
+ Response response = loginTarget.request().get();
+ Assert.assertEquals(401, response.getStatus());
+ String authenticateHeader = response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE);
+ Assert.assertNotNull(authenticateHeader);
+ //System.out.println(authenticateHeader);
+ String splash = response.readEntity(String.class);
+ //System.out.println(splash);
+ response.close();
+
+ // respin Client to make absolutely sure no cookie caching. need to test that it works with null auth_session_id cookie.
+ client.close();
+ client = ClientBuilder.newClient();
+
+
+ authenticateHeader = authenticateHeader.trim();
+ Pattern callbackPattern = Pattern.compile("callback\\s*=\\s*\"([^\"]+)\"");
+ Pattern paramPattern = Pattern.compile("param=\"([^\"]+)\"\\s+label=\"([^\"]+)\"");
+ Matcher m = callbackPattern.matcher(authenticateHeader);
+ String callback = null;
+ if (m.find()) {
+ callback = m.group(1);
+ //System.out.println("------");
+ //System.out.println("callback:");
+ //System.out.println(" " + callback);
+ }
+ m = paramPattern.matcher(authenticateHeader);
+ List params = new LinkedList<>();
+ List labels = new LinkedList<>();
+ while (m.find()) {
+ String param = m.group(1);
+ String label = m.group(2);
+ params.add(param);
+ labels.add(label);
+ //System.out.println("------");
+ //System.out.println("param:" + param);
+ //System.out.println("label:" + label);
+ }
+ Assert.assertEquals("username", params.get(0));
+ Assert.assertEquals("Username:", labels.get(0).trim());
+ Assert.assertEquals("password", params.get(1));
+ Assert.assertEquals("Password:", labels.get(1).trim());
+
+ Form form = new Form();
+ form.param("username", "test-user@localhost");
+ form.param("password", "password");
+ response = client.target(callback)
+ .request()
+ .post(Entity.form(form));
+ Assert.assertEquals(302, response.getStatus());
+ String redirect = response.getHeaderString(HttpHeaders.LOCATION);
+ System.out.println("------");
+ System.out.println(redirect);
+ Pattern codePattern = Pattern.compile("code=([^&]+)");
+ m = codePattern.matcher(redirect);
+ Assert.assertTrue(m.find());
+ String code = m.group(1);
+ OAuthClient.AccessTokenResponse oauthResponse = oauth.doAccessTokenRequest(code, "password");
+ Assert.assertNotNull(oauthResponse.getAccessToken());
+ client.close();
+
+
+ }
+
+
+}
diff --git a/themes/src/main/resources/theme/base/login/cli_splash.ftl b/themes/src/main/resources/theme/base/login/cli_splash.ftl
new file mode 100644
index 0000000000..cd9ebbb7a2
--- /dev/null
+++ b/themes/src/main/resources/theme/base/login/cli_splash.ftl
@@ -0,0 +1,7 @@
+ _ __ _ _
+| |/ /___ _ _ ___| | ___ __ _| | __
+| ' // _ \ | | |/ __| |/ _ \ / _` | |/ /
+| . \ __/ |_| | (__| | (_) | (_| | <
+|_|\_\___|\__, |\___|_|\___/ \__,_|_|\_\
+ |___/
+