Merge pull request #4991 from patriot1burke/challenge-support

KEYCLOAK-6355
This commit is contained in:
Bill Burke 2018-02-13 09:38:45 -05:00 committed by GitHub
commit 5d5373454c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 924 additions and 97 deletions

View file

@ -63,6 +63,15 @@
<groupId>org.jboss.logging</groupId> <groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId> <artifactId>jboss-logging</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.ws.rs</groupId>
<artifactId>jboss-jaxrs-api_2.0_spec</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View file

@ -53,7 +53,13 @@ public class KeycloakCliSso {
login(); login();
} else if (args[0].equalsIgnoreCase("login-manual")) { } else if (args[0].equalsIgnoreCase("login-manual")) {
loginManual(); loginManual();
} else if (args[0].equalsIgnoreCase("token")) { }
/*
else if (args[0].equalsIgnoreCase("login-cli")) {
loginCli();
}
*/
else if (args[0].equalsIgnoreCase("token")) {
token(); token();
} else if (args[0].equalsIgnoreCase("logout")) { } else if (args[0].equalsIgnoreCase("logout")) {
logout(); logout();
@ -69,6 +75,7 @@ public class KeycloakCliSso {
System.err.println("Commands:"); 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 - 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-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(" token - print access token if logged in");
System.err.println(" logout - logout."); System.err.println(" logout - logout.");
System.exit(1); System.exit(1);
@ -110,7 +117,7 @@ public class KeycloakCliSso {
return config; return config;
} }
public boolean checkToken() throws Exception { public boolean checkToken(boolean outputToken) throws Exception {
String token = getTokenResponse(); String token = getTokenResponse();
if (token == null) return false; if (token == null) return false;
@ -127,7 +134,7 @@ public class KeycloakCliSso {
AdapterConfig config = getConfig(); AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config)); KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
installed.refreshToken(tokenResponse.getRefreshToken()); installed.refreshToken(tokenResponse.getRefreshToken());
processResponse(installed); processResponse(installed, outputToken);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
System.err.println("Error processing existing token"); System.err.println("Error processing existing token");
@ -184,11 +191,19 @@ public class KeycloakCliSso {
} }
public void login() throws Exception { public void login() throws Exception {
if (checkToken()) return; if (checkToken(true)) return;
AdapterConfig config = getConfig(); AdapterConfig config = getConfig();
KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config)); KeycloakInstalled installed = new KeycloakInstalled(KeycloakDeploymentBuilder.build(config));
installed.login(); 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() { public String getHome() {
@ -210,7 +225,7 @@ public class KeycloakCliSso {
return Paths.get(getHome(), System.getProperty("basepath", ".keycloak-sso"), System.getProperty("KEYCLOAK_REALM"), System.getProperty("KEYCLOAK_CLIENT") + ".json").toFile(); 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(); AccessTokenResponse tokenResponse = installed.getTokenResponse();
tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn()); tokenResponse.setExpiresIn(Time.currentTime() + tokenResponse.getExpiresIn());
tokenResponse.setIdToken(null); tokenResponse.setIdToken(null);
@ -220,16 +235,16 @@ public class KeycloakCliSso {
fos.write(output.getBytes("UTF-8")); fos.write(output.getBytes("UTF-8"));
fos.flush(); fos.flush();
fos.close(); fos.close();
System.out.println(tokenResponse.getToken()); if (outputToken) System.out.println(tokenResponse.getToken());
} }
public void loginManual() throws Exception { public void loginManual() throws Exception {
if (checkToken()) return; if (checkToken(true)) return;
AdapterConfig config = getConfig(); AdapterConfig config = getConfig();
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config); KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
KeycloakInstalled installed = new KeycloakInstalled(deployment); KeycloakInstalled installed = new KeycloakInstalled(deployment);
installed.loginManual(); installed.loginManual();
processResponse(installed); processResponse(installed, true);
} }
public void logout() throws Exception { public void logout() throws Exception {

View file

@ -17,6 +17,8 @@
package org.keycloak.adapters.installed; 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.OAuth2Constants;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeployment;
@ -31,6 +33,11 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken; 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.awt.*;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
@ -39,6 +46,7 @@ import java.io.InputStreamReader;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.PrintStream; import java.io.PrintStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.PushbackInputStream;
import java.io.Reader; import java.io.Reader;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
@ -47,6 +55,8 @@ import java.net.URISyntaxException;
import java.util.Locale; import java.util.Locale;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -76,6 +86,9 @@ public class KeycloakInstalled {
private Locale locale; private Locale locale;
private HttpResponseWriter loginResponseWriter; private HttpResponseWriter loginResponseWriter;
private HttpResponseWriter logoutResponseWriter; 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; status = Status.LOGGED_MANUAL;
} }
public boolean loginCommandLine() throws IOException, ServerRequest.HttpFailure, VerificationException {
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
return loginCommandLine(redirectUri);
}
/**
* Experimental 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 { public String getTokenString() throws VerificationException, IOException, ServerRequest.HttpFailure {
return tokenString; return tokenString;
} }
@ -381,6 +474,86 @@ public class KeycloakInstalled {
return sb.toString(); 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 { public class CallbackListener extends Thread {

View file

@ -79,6 +79,15 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/ */
URI getActionUrl(String code); 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. * Get the action URL for the action token executor.
* *
@ -94,6 +103,14 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/ */
URI getRefreshExecutionUrl(); 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 * End the flow and redirect browser based on protocol specific respones. This should only be executed
* in browser-based flows. * in browser-based flows.

View file

@ -52,6 +52,8 @@ public interface LoginFormsProvider extends Provider {
Response createForm(String form); Response createForm(String form);
String getMessage(String message);
Response createLogin(); Response createLogin();
Response createPasswordReset(); Response createPasswordReset();
@ -122,6 +124,8 @@ public interface LoginFormsProvider extends Provider {
LoginFormsProvider setStatus(Response.Status status); LoginFormsProvider setStatus(Response.Status status);
LoginFormsProvider setMediaType(javax.ws.rs.core.MediaType type);
LoginFormsProvider setActionUri(URI requestUri); LoginFormsProvider setActionUri(URI requestUri);
LoginFormsProvider setExecution(String execution); LoginFormsProvider setExecution(String execution);

View file

@ -60,6 +60,7 @@ import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.RootAuthenticationSessionModel;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
import java.util.HashMap; import java.util.HashMap;
@ -487,32 +488,72 @@ public class AuthenticationProcessor {
@Override @Override
public URI getActionUrl(String code) { public URI getActionUrl(String code) {
return LoginActionsService.loginActionsBaseUrl(getUriInfo()) UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(AuthenticationProcessor.this.flowPath) .path(AuthenticationProcessor.this.flowPath)
.queryParam(OAuth2Constants.CODE, code) .queryParam(LoginActionsService.SESSION_CODE, code)
.queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .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()); .build(getRealm().getName());
} }
@Override @Override
public URI getActionTokenUrl(String tokenString) { public URI getActionTokenUrl(String tokenString) {
return LoginActionsService.actionTokenProcessor(getUriInfo()) UriBuilder uriBuilder = LoginActionsService.actionTokenProcessor(getUriInfo())
.queryParam(Constants.KEY, tokenString) .queryParam(Constants.KEY, tokenString)
.queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .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()); .build(getRealm().getName());
} }
@Override @Override
public URI getRefreshExecutionUrl() { public URI getRefreshExecutionUrl() {
return LoginActionsService.loginActionsBaseUrl(getUriInfo()) UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(AuthenticationProcessor.this.flowPath) .path(AuthenticationProcessor.this.flowPath)
.queryParam(Constants.EXECUTION, getExecution().getId()) .queryParam(Constants.EXECUTION, getExecution().getId())
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId()) .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()); .build(getRealm().getName());
} }

View file

@ -266,7 +266,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
public URI getActionUrl(String executionId, String code) { public URI getActionUrl(String executionId, String code) {
ClientModel client = processor.getAuthenticationSession().getClient(); ClientModel client = processor.getAuthenticationSession().getClient();
return LoginActionsService.registrationFormProcessor(processor.getUriInfo()) return LoginActionsService.registrationFormProcessor(processor.getUriInfo())
.queryParam(OAuth2Constants.CODE, code) .queryParam(LoginActionsService.SESSION_CODE, code)
.queryParam(Constants.EXECUTION, executionId) .queryParam(Constants.EXECUTION, executionId)
.queryParam(Constants.CLIENT_ID, client.getClientId()) .queryParam(Constants.CLIENT_ID, client.getClientId())
.queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId()) .queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId())

View file

@ -136,7 +136,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
public URI getActionUrl(String code) { public URI getActionUrl(String code) {
ClientModel client = authenticationSession.getClient(); ClientModel client = authenticationSession.getClient();
return LoginActionsService.requiredActionProcessor(getUriInfo()) return LoginActionsService.requiredActionProcessor(getUriInfo())
.queryParam(OAuth2Constants.CODE, code) .queryParam(LoginActionsService.SESSION_CODE, code)
.queryParam(Constants.EXECUTION, getExecution()) .queryParam(Constants.EXECUTION, getExecution())
.queryParam(Constants.CLIENT_ID, client.getClientId()) .queryParam(Constants.CLIENT_ID, client.getClientId())
.queryParam(Constants.TAB_ID, authenticationSession.getTabId()) .queryParam(Constants.TAB_ID, authenticationSession.getTabId())

View file

@ -46,7 +46,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
@FunctionalInterface @FunctionalInterface
public interface ProcessBrokerFlow { 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; private final KeycloakSession session;
@ -160,8 +160,8 @@ public class ActionTokenContext<T extends JsonWebToken> {
return processAuthenticateFlow.processFlow(action, getExecutionId(), getAuthenticationSession(), flowPath, flow, errorMessage, processor); 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(); 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);
} }
} }

View file

@ -122,7 +122,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername()); 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));
} }
} }

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<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,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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @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<ProviderConfigProperty> getConfigProperties() {
return null;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}

View file

@ -39,6 +39,7 @@ import org.keycloak.models.*;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.Urls; import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.theme.BrowserSecurityHeaderSetup; import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.theme.FreeMarkerException; import org.keycloak.theme.FreeMarkerException;
@ -73,6 +74,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
protected String accessCode; protected String accessCode;
protected Response.Status status; protected Response.Status status;
protected javax.ws.rs.core.MediaType contentType;
protected List<RoleModel> realmRolesRequested; protected List<RoleModel> realmRolesRequested;
protected MultivaluedMap<String, RoleModel> resourceRolesRequested; protected MultivaluedMap<String, RoleModel> resourceRolesRequested;
protected List<ProtocolMapperModel> protocolMappersRequested; protected List<ProtocolMapperModel> protocolMappersRequested;
@ -315,6 +317,23 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
} }
attributes.put("messagesPerField", messagesPerField); 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. * Create common attributes used in all templates.
@ -329,7 +348,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
protected void createCommonAttributes(Theme theme, Locale locale, Properties messagesBundle, UriBuilder baseUriBuilder, LoginFormsPages page) { protected void createCommonAttributes(Theme theme, Locale locale, Properties messagesBundle, UriBuilder baseUriBuilder, LoginFormsPages page) {
URI baseUri = baseUriBuilder.build(); URI baseUri = baseUriBuilder.build();
if (accessCode != null) { if (accessCode != null) {
baseUriBuilder.queryParam(OAuth2Constants.CODE, accessCode); baseUriBuilder.queryParam(LoginActionsService.SESSION_CODE, accessCode);
} }
URI baseUriWithCodeAndClientId = baseUriBuilder.build(); URI baseUriWithCodeAndClientId = baseUriBuilder.build();
@ -389,7 +408,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
protected Response processTemplate(Theme theme, String templateName, Locale locale) { protected Response processTemplate(Theme theme, String templateName, Locale locale) {
try { try {
String result = freeMarker.processTemplate(attributes, templateName, theme); 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); BrowserSecurityHeaderSetup.headers(builder, realm);
for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) { for (Map.Entry<String, String> entry : httpResponseHeaders.entrySet()) {
builder.header(entry.getKey(), entry.getValue()); builder.header(entry.getKey(), entry.getValue());
@ -602,6 +622,14 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
this.status = status; this.status = status;
return this; return this;
} }
@Override
public LoginFormsProvider setMediaType(javax.ws.rs.core.MediaType type) {
this.contentType = type;
return this;
}
@Override @Override
public LoginFormsProvider setActionUri(URI actionUri) { public LoginFormsProvider setActionUri(URI actionUri) {

View file

@ -79,7 +79,7 @@ public class Urls {
.path(IdentityBrokerService.class, "performLogin"); .path(IdentityBrokerService.class, "performLogin");
if (accessCode != null) { if (accessCode != null) {
uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); uriBuilder.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode);
} }
if (clientId != null) { if (clientId != null) {
uriBuilder.replaceQueryParam(Constants.CLIENT_ID, clientId); uriBuilder.replaceQueryParam(Constants.CLIENT_ID, clientId);
@ -112,7 +112,7 @@ public class Urls {
public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) { public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
.path(IdentityBrokerService.class, "afterFirstBrokerLogin") .path(IdentityBrokerService.class, "afterFirstBrokerLogin")
.replaceQueryParam(OAuth2Constants.CODE, accessCode) .replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
.replaceQueryParam(Constants.CLIENT_ID, clientId) .replaceQueryParam(Constants.CLIENT_ID, clientId)
.replaceQueryParam(Constants.TAB_ID, tabId) .replaceQueryParam(Constants.TAB_ID, tabId)
.build(realmName); .build(realmName);
@ -121,7 +121,7 @@ public class Urls {
public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) { public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow") .path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
.replaceQueryParam(OAuth2Constants.CODE, accessCode) .replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
.replaceQueryParam(Constants.CLIENT_ID, clientId) .replaceQueryParam(Constants.CLIENT_ID, clientId)
.replaceQueryParam(Constants.TAB_ID, tabId) .replaceQueryParam(Constants.TAB_ID, tabId)
.build(realmName); .build(realmName);

View file

@ -106,7 +106,7 @@ public class AuthenticationSessionManager {
} }
private String getAuthSessionCookieDecoded(RealmModel realm) { public String getAuthSessionCookieDecoded(RealmModel realm) {
String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID); String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
if (cookieVal != null) { if (cookieVal != null) {

View file

@ -91,29 +91,51 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
try { try {
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass); CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
result.clientSession = getClientSession(code, tabId, session, realm, client, event, clientSessionParser); result.clientSession = getClientSession(code, tabId, session, realm, client, event, clientSessionParser);
if (result.clientSession == null) { return parseResult(code, session, realm, result, clientSessionParser);
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<CLIENT_SESSION>(session, realm, result.clientSession);
return result;
} catch (RuntimeException e) { } catch (RuntimeException e) {
result.illegalHash = true; result.illegalHash = true;
return result; return result;
} }
} }
public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, String tabId,
KeycloakSession session, RealmModel realm, ClientModel client,
EventBuilder event, CLIENT_SESSION clientSession) {
ParseResult<CLIENT_SESSION> result = new ParseResult<>();
result.clientSession = clientSession;
if (code == null) {
result.illegalHash = true;
return result;
}
try {
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser((Class<CLIENT_SESSION>)clientSession.getClass());
return parseResult(code, session, realm, result, clientSessionParser);
} catch (RuntimeException e) {
result.illegalHash = true;
return result;
}
}
private static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, ParseResult<CLIENT_SESSION> result, CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> 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<CLIENT_SESSION>(session, realm, result.clientSession);
return result;
}
public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client, public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client,
EventBuilder event, Class<CLIENT_SESSION> sessionClass) { EventBuilder event, Class<CLIENT_SESSION> sessionClass) {

View file

@ -341,7 +341,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@POST @POST
@Path("/{provider_id}/login") @Path("/{provider_id}/login")
public Response performPostLogin(@PathParam("provider_id") String providerId, public Response performPostLogin(@PathParam("provider_id") String providerId,
@QueryParam("code") String code, @QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
return performLogin(providerId, code, clientId, tabId); return performLogin(providerId, code, clientId, tabId);
@ -351,7 +351,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@NoCache @NoCache
@Path("/{provider_id}/login") @Path("/{provider_id}/login")
public Response performLogin(@PathParam("provider_id") String providerId, public Response performLogin(@PathParam("provider_id") String providerId,
@QueryParam("code") String code, @QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
this.event.detail(Details.IDENTITY_PROVIDER, providerId); this.event.detail(Details.IDENTITY_PROVIDER, providerId);
@ -594,7 +594,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@GET @GET
@NoCache @NoCache
@Path("/after-first-broker-login") @Path("/after-first-broker-login")
public Response afterFirstBrokerLogin(@QueryParam("code") String code, public Response afterFirstBrokerLogin(@QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId); ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
@ -725,7 +725,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
@GET @GET
@NoCache @NoCache
@Path("/after-post-broker-login") @Path("/after-post-broker-login")
public Response afterPostBrokerLoginFlow(@QueryParam("code") String code, public Response afterPostBrokerLoginFlow(@QueryParam(LoginActionsService.SESSION_CODE) String code,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId); ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
@ -991,7 +991,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
return ParsedCodeContext.response(staleCodeError); 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(); checks.initialVerify();
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {

View file

@ -115,6 +115,9 @@ public class LoginActionsService {
public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage"; 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; private RealmModel realm;
@Context @Context
@ -184,8 +187,8 @@ public class LoginActionsService {
} }
} }
private SessionCodeChecks checksForCode(String code, String execution, String clientId, String tabId, String 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, code, execution, clientId, tabId, flowPath); SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, authSessionId, code, execution, clientId, tabId, flowPath);
res.initialVerify(); res.initialVerify();
return res; return res;
} }
@ -204,10 +207,11 @@ public class LoginActionsService {
*/ */
@Path(RESTART_PATH) @Path(RESTART_PATH)
@GET @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) { @QueryParam(Constants.TAB_ID) String tabId) {
event.event(EventType.RESTART_AUTHENTICATION); 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(); AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
if (authSession == null) { if (authSession == null) {
@ -235,13 +239,14 @@ public class LoginActionsService {
*/ */
@Path(AUTHENTICATE_PATH) @Path(AUTHENTICATE_PATH)
@GET @GET
public Response authenticate(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
event.event(EventType.LOGIN); 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)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse(); return checks.getResponse();
} }
@ -305,27 +310,29 @@ public class LoginActionsService {
*/ */
@Path(AUTHENTICATE_PATH) @Path(AUTHENTICATE_PATH)
@POST @POST
public Response authenticateForm(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
return authenticate(code, execution, clientId, tabId); return authenticate(authSessionId, code, execution, clientId, tabId);
} }
@Path(RESET_CREDENTIALS_PATH) @Path(RESET_CREDENTIALS_PATH)
@POST @POST
public Response resetCredentialsPOST(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId, @QueryParam(Constants.TAB_ID) String tabId,
@QueryParam(Constants.KEY) String key) { @QueryParam(Constants.KEY) String key) {
if (key != null) { if (key != null) {
return handleActionToken(key, execution, clientId, tabId); return handleActionToken(authSessionId, key, execution, clientId, tabId);
} }
event.event(EventType.RESET_PASSWORD); event.event(EventType.RESET_PASSWORD);
return resetCredentials(code, execution, clientId, tabId); return resetCredentials(authSessionId, code, execution, clientId, tabId);
} }
/** /**
@ -338,7 +345,8 @@ public class LoginActionsService {
*/ */
@Path(RESET_CREDENTIALS_PATH) @Path(RESET_CREDENTIALS_PATH)
@GET @GET
public Response resetCredentialsGET(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
@ -358,7 +366,7 @@ public class LoginActionsService {
} }
event.event(EventType.RESET_PASSWORD); event.event(EventType.RESET_PASSWORD);
return resetCredentials(code, execution, clientId, tabId); return resetCredentials(authSessionId, code, execution, clientId, tabId);
} }
AuthenticationSessionModel createAuthenticationSessionForClient() AuthenticationSessionModel createAuthenticationSessionForClient()
@ -388,8 +396,8 @@ public class LoginActionsService {
* @param execution * @param execution
* @return * @return
*/ */
protected Response resetCredentials(String code, String execution, String clientId, String tabId) { protected Response resetCredentials(String authSessionId, String code, String execution, String clientId, String tabId) {
SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, RESET_CREDENTIALS_PATH); SessionCodeChecks checks = checksForCode(authSessionId, code, execution, clientId, tabId, RESET_CREDENTIALS_PATH);
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.getResponse(); return checks.getResponse();
} }
@ -414,14 +422,15 @@ public class LoginActionsService {
*/ */
@Path("action-token") @Path("action-token")
@GET @GET
public Response executeActionToken(@QueryParam("key") String key, public Response executeActionToken(@QueryParam(AUTH_SESSION_ID) String authSessionId,
@QueryParam("key") String key,
@QueryParam("execution") String execution, @QueryParam("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
return handleActionToken(key, execution, clientId, tabId); return handleActionToken(authSessionId, key, execution, clientId, tabId);
} }
protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId) { protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String authSessionId, String tokenString, String execution, String clientId, String tabId) {
T token; T token;
ActionTokenHandler<T> handler; ActionTokenHandler<T> handler;
ActionTokenContext<T> tokenContext; ActionTokenContext<T> tokenContext;
@ -435,9 +444,11 @@ public class LoginActionsService {
if (clientId != null) { if (clientId != null) {
client = realm.getClientByClientId(clientId); client = realm.getClientByClientId(clientId);
} }
AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session);
if (client != null) { if (client != null) {
session.getContext().setClient(client); session.getContext().setClient(client);
authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId); authSessionId = authSessionId == null ? authenticationSessionManager.getAuthSessionCookieDecoded(realm) : authSessionId;
authSession = authenticationSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
} }
event.event(EventType.EXECUTE_ACTION_TOKEN); event.event(EventType.EXECUTE_ACTION_TOKEN);
@ -520,7 +531,7 @@ public class LoginActionsService {
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, authSession, tokenAuthSessionCompoundId)) { ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, authSession, tokenAuthSessionCompoundId)) {
// There exists an authentication session but no auth session ID was received in the action token // There exists an authentication session but no auth session ID was received in the action token
logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId()); logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false); authenticationSessionManager.removeAuthenticationSession(realm, authSession, false);
authSession = handler.startFreshAuthenticationSession(token, tokenContext); authSession = handler.startFreshAuthenticationSession(token, tokenContext);
tokenContext.setAuthenticationSession(authSession, true); tokenContext.setAuthenticationSession(authSession, true);
@ -617,11 +628,12 @@ public class LoginActionsService {
*/ */
@Path(REGISTRATION_PATH) @Path(REGISTRATION_PATH)
@GET @GET
public Response registerPage(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
return registerRequest(code, execution, clientId, tabId,false); return registerRequest(authSessionId, code, execution, clientId, tabId,false);
} }
@ -633,22 +645,23 @@ public class LoginActionsService {
*/ */
@Path(REGISTRATION_PATH) @Path(REGISTRATION_PATH)
@POST @POST
public Response processRegister(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @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); event.event(EventType.REGISTER);
if (!realm.isRegistrationAllowed()) { if (!realm.isRegistrationAllowed()) {
event.error(Errors.REGISTRATION_DISABLED); event.error(Errors.REGISTRATION_DISABLED);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED); 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)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse(); return checks.getResponse();
} }
@ -663,48 +676,52 @@ public class LoginActionsService {
@Path(FIRST_BROKER_LOGIN_PATH) @Path(FIRST_BROKER_LOGIN_PATH)
@GET @GET
public Response firstBrokerLoginGet(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @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) @Path(FIRST_BROKER_LOGIN_PATH)
@POST @POST
public Response firstBrokerLoginPost(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @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) @Path(POST_BROKER_LOGIN_PATH)
@GET @GET
public Response postBrokerLoginGet(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @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) @Path(POST_BROKER_LOGIN_PATH)
@POST @POST
public Response postBrokerLoginPost(@QueryParam("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("execution") String execution,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @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); boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN; EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType); 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)) { if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.getResponse(); return checks.getResponse();
} }
@ -783,10 +800,10 @@ public class LoginActionsService {
@Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processConsent(final MultivaluedMap<String, String> formData) { public Response processConsent(final MultivaluedMap<String, String> formData) {
event.event(EventType.LOGIN); event.event(EventType.LOGIN);
String code = formData.getFirst("code"); String code = formData.getFirst(SESSION_CODE);
String clientId = uriInfo.getQueryParameters().getFirst(Constants.CLIENT_ID); String clientId = uriInfo.getQueryParameters().getFirst(Constants.CLIENT_ID);
String tabId = uriInfo.getQueryParameters().getFirst(Constants.TAB_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())) { if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) {
return checks.getResponse(); return checks.getResponse();
} }
@ -874,26 +891,28 @@ public class LoginActionsService {
@Path(REQUIRED_ACTION) @Path(REQUIRED_ACTION)
@POST @POST
public Response requiredActionPOST(@QueryParam("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("execution") String action,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @QueryParam(Constants.TAB_ID) String tabId) {
return processRequireAction(code, action, clientId, tabId); return processRequireAction(authSessionId, code, action, clientId, tabId);
} }
@Path(REQUIRED_ACTION) @Path(REQUIRED_ACTION)
@GET @GET
public Response requiredActionGET(@QueryParam("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("execution") String action,
@QueryParam("client_id") String clientId, @QueryParam("client_id") String clientId,
@QueryParam(Constants.TAB_ID) String tabId) { @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); 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)) { if (!checks.verifyRequiredAction(action)) {
return checks.getResponse(); return checks.getResponse();
} }

View file

@ -71,10 +71,11 @@ public class SessionCodeChecks {
private final String clientId; private final String clientId;
private final String tabId; private final String tabId;
private final String flowPath; private final String flowPath;
private final String authSessionId;
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, 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.realm = realm;
this.uriInfo = uriInfo; this.uriInfo = uriInfo;
this.request = request; this.request = request;
@ -87,6 +88,7 @@ public class SessionCodeChecks {
this.clientId = clientId; this.clientId = clientId;
this.tabId = tabId; this.tabId = tabId;
this.flowPath = flowPath; this.flowPath = flowPath;
this.authSessionId = authSessionId;
} }
@ -146,14 +148,32 @@ public class SessionCodeChecks {
session.getContext().setClient(client); session.getContext().setClient(client);
} }
// object retrieve // object retrieve
AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session); 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) { if (authSession != null) {
session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession); session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession);
return 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. // See if we are already authenticated and userSession with same ID exists.
String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm); String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm);
RootAuthenticationSessionModel existingRootAuthSession = null; RootAuthenticationSessionModel existingRootAuthSession = null;
@ -250,7 +270,7 @@ public class SessionCodeChecks {
return false; return false;
} }
} else { } else {
ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, tabId, session, realm, client, event, AuthenticationSessionModel.class); ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, tabId, session, realm, client, event, authSession);
clientCode = result.getCode(); clientCode = result.getCode();
if (clientCode == null) { if (clientCode == null) {

View file

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

View file

@ -146,6 +146,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a username and password from login form."); "Validates a username and password from login form.");
addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username 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."); "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", 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."); "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"); addProviderInfo(result, "direct-grant-validate-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request");

View file

@ -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 <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/
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<String> params = new LinkedList<>();
List<String> 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();
}
}

View file

@ -0,0 +1,7 @@
_ __ _ _
| |/ /___ _ _ ___| | ___ __ _| | __
| ' // _ \ | | |/ __| |/ _ \ / _` | |/ /
| . \ __/ |_| | (__| | (_) | (_| | <
|_|\_\___|\__, |\___|_|\___/ \__,_|_|\_\
|___/