Merge pull request #4991 from patriot1burke/challenge-support
KEYCLOAK-6355
This commit is contained in:
commit
5d5373454c
23 changed files with 924 additions and 97 deletions
|
@ -63,6 +63,15 @@
|
|||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
</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>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -53,7 +53,13 @@ public class KeycloakCliSso {
|
|||
login();
|
||||
} else if (args[0].equalsIgnoreCase("login-manual")) {
|
||||
loginManual();
|
||||
} else if (args[0].equalsIgnoreCase("token")) {
|
||||
}
|
||||
/*
|
||||
else if (args[0].equalsIgnoreCase("login-cli")) {
|
||||
loginCli();
|
||||
}
|
||||
*/
|
||||
else if (args[0].equalsIgnoreCase("token")) {
|
||||
token();
|
||||
} else if (args[0].equalsIgnoreCase("logout")) {
|
||||
logout();
|
||||
|
@ -69,6 +75,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 +117,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 +134,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 +191,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 +225,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 +235,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 {
|
||||
|
|
|
@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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 {
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(OAuth2Constants.CODE, code)
|
||||
.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());
|
||||
}
|
||||
|
||||
|
|
|
@ -266,7 +266,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
|
|||
public URI getActionUrl(String executionId, String code) {
|
||||
ClientModel client = processor.getAuthenticationSession().getClient();
|
||||
return LoginActionsService.registrationFormProcessor(processor.getUriInfo())
|
||||
.queryParam(OAuth2Constants.CODE, code)
|
||||
.queryParam(LoginActionsService.SESSION_CODE, code)
|
||||
.queryParam(Constants.EXECUTION, executionId)
|
||||
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||
.queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId())
|
||||
|
|
|
@ -136,7 +136,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
|||
public URI getActionUrl(String code) {
|
||||
ClientModel client = authenticationSession.getClient();
|
||||
return LoginActionsService.requiredActionProcessor(getUriInfo())
|
||||
.queryParam(OAuth2Constants.CODE, code)
|
||||
.queryParam(LoginActionsService.SESSION_CODE, code)
|
||||
.queryParam(Constants.EXECUTION, getExecution())
|
||||
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
||||
|
|
|
@ -46,7 +46,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
|||
|
||||
@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<T extends JsonWebToken> {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -39,6 +39,7 @@ import org.keycloak.models.*;
|
|||
import org.keycloak.models.utils.FormMessage;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.theme.BrowserSecurityHeaderSetup;
|
||||
import org.keycloak.theme.FreeMarkerException;
|
||||
|
@ -73,6 +74,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
|
||||
protected String accessCode;
|
||||
protected Response.Status status;
|
||||
protected javax.ws.rs.core.MediaType contentType;
|
||||
protected List<RoleModel> realmRolesRequested;
|
||||
protected MultivaluedMap<String, RoleModel> resourceRolesRequested;
|
||||
protected List<ProtocolMapperModel> protocolMappersRequested;
|
||||
|
@ -315,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.
|
||||
|
@ -329,7 +348,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
protected void createCommonAttributes(Theme theme, Locale locale, Properties messagesBundle, UriBuilder baseUriBuilder, LoginFormsPages page) {
|
||||
URI baseUri = baseUriBuilder.build();
|
||||
if (accessCode != null) {
|
||||
baseUriBuilder.queryParam(OAuth2Constants.CODE, accessCode);
|
||||
baseUriBuilder.queryParam(LoginActionsService.SESSION_CODE, accessCode);
|
||||
}
|
||||
URI baseUriWithCodeAndClientId = baseUriBuilder.build();
|
||||
|
||||
|
@ -389,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<String, String> entry : httpResponseHeaders.entrySet()) {
|
||||
builder.header(entry.getKey(), entry.getValue());
|
||||
|
@ -602,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) {
|
||||
|
|
|
@ -79,7 +79,7 @@ public class Urls {
|
|||
.path(IdentityBrokerService.class, "performLogin");
|
||||
|
||||
if (accessCode != null) {
|
||||
uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode);
|
||||
uriBuilder.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode);
|
||||
}
|
||||
if (clientId != null) {
|
||||
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) {
|
||||
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
|
||||
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
||||
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
|
||||
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
||||
.replaceQueryParam(Constants.TAB_ID, tabId)
|
||||
.build(realmName);
|
||||
|
@ -121,7 +121,7 @@ public class Urls {
|
|||
public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
|
||||
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
|
||||
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
||||
.replaceQueryParam(LoginActionsService.SESSION_CODE, accessCode)
|
||||
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
||||
.replaceQueryParam(Constants.TAB_ID, tabId)
|
||||
.build(realmName);
|
||||
|
|
|
@ -106,7 +106,7 @@ public class AuthenticationSessionManager {
|
|||
}
|
||||
|
||||
|
||||
private String getAuthSessionCookieDecoded(RealmModel realm) {
|
||||
public String getAuthSessionCookieDecoded(RealmModel realm) {
|
||||
String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
|
||||
|
||||
if (cookieVal != null) {
|
||||
|
|
|
@ -91,29 +91,51 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
|||
try {
|
||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> 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<CLIENT_SESSION>(session, realm, result.clientSession);
|
||||
return result;
|
||||
return parseResult(code, session, realm, result, clientSessionParser);
|
||||
} catch (RuntimeException e) {
|
||||
result.illegalHash = true;
|
||||
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,
|
||||
EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
||||
|
|
|
@ -341,7 +341,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
@POST
|
||||
@Path("/{provider_id}/login")
|
||||
public Response performPostLogin(@PathParam("provider_id") String providerId,
|
||||
@QueryParam("code") String code,
|
||||
@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return performLogin(providerId, code, clientId, tabId);
|
||||
|
@ -351,7 +351,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
@NoCache
|
||||
@Path("/{provider_id}/login")
|
||||
public Response performLogin(@PathParam("provider_id") String providerId,
|
||||
@QueryParam("code") String code,
|
||||
@QueryParam(LoginActionsService.SESSION_CODE) String code,
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
this.event.detail(Details.IDENTITY_PROVIDER, providerId);
|
||||
|
@ -594,7 +594,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
@GET
|
||||
@NoCache
|
||||
@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(Constants.TAB_ID) String tabId) {
|
||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
|
||||
|
@ -725,7 +725,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
@GET
|
||||
@NoCache
|
||||
@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(Constants.TAB_ID) String tabId) {
|
||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
|
||||
|
@ -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)) {
|
||||
|
||||
|
|
|
@ -115,6 +115,9 @@ 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;
|
||||
|
||||
@Context
|
||||
|
@ -184,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;
|
||||
}
|
||||
|
@ -204,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) {
|
||||
|
@ -235,13 +239,14 @@ public class LoginActionsService {
|
|||
*/
|
||||
@Path(AUTHENTICATE_PATH)
|
||||
@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("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();
|
||||
}
|
||||
|
@ -305,27 +310,29 @@ public class LoginActionsService {
|
|||
*/
|
||||
@Path(AUTHENTICATE_PATH)
|
||||
@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("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("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,
|
||||
@QueryParam(Constants.KEY) String key) {
|
||||
if (key != null) {
|
||||
return handleActionToken(key, execution, clientId, tabId);
|
||||
return handleActionToken(authSessionId, key, execution, clientId, tabId);
|
||||
}
|
||||
|
||||
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)
|
||||
@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("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
|
@ -358,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()
|
||||
|
@ -388,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();
|
||||
}
|
||||
|
@ -414,14 +422,15 @@ public class LoginActionsService {
|
|||
*/
|
||||
@Path("action-token")
|
||||
@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("client_id") String clientId,
|
||||
@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;
|
||||
ActionTokenHandler<T> handler;
|
||||
ActionTokenContext<T> tokenContext;
|
||||
|
@ -435,9 +444,11 @@ public class LoginActionsService {
|
|||
if (clientId != null) {
|
||||
client = realm.getClientByClientId(clientId);
|
||||
}
|
||||
AuthenticationSessionManager authenticationSessionManager = new AuthenticationSessionManager(session);
|
||||
if (client != null) {
|
||||
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);
|
||||
|
@ -520,7 +531,7 @@ public class LoginActionsService {
|
|||
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, authSession, tokenAuthSessionCompoundId)) {
|
||||
// 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());
|
||||
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
|
||||
authenticationSessionManager.removeAuthenticationSession(realm, authSession, false);
|
||||
|
||||
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
||||
tokenContext.setAuthenticationSession(authSession, true);
|
||||
|
@ -617,11 +628,12 @@ public class LoginActionsService {
|
|||
*/
|
||||
@Path(REGISTRATION_PATH)
|
||||
@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("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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -633,22 +645,23 @@ public class LoginActionsService {
|
|||
*/
|
||||
@Path(REGISTRATION_PATH)
|
||||
@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("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();
|
||||
}
|
||||
|
@ -663,48 +676,52 @@ public class LoginActionsService {
|
|||
|
||||
@Path(FIRST_BROKER_LOGIN_PATH)
|
||||
@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("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("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("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("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();
|
||||
}
|
||||
|
@ -783,10 +800,10 @@ public class LoginActionsService {
|
|||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
public Response processConsent(final MultivaluedMap<String, String> formData) {
|
||||
event.event(EventType.LOGIN);
|
||||
String code = formData.getFirst("code");
|
||||
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();
|
||||
}
|
||||
|
@ -874,26 +891,28 @@ public class LoginActionsService {
|
|||
|
||||
@Path(REQUIRED_ACTION)
|
||||
@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("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("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();
|
||||
}
|
||||
|
|
|
@ -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<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();
|
||||
if (clientCode == null) {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
_ __ _ _
|
||||
| |/ /___ _ _ ___| | ___ __ _| | __
|
||||
| ' // _ \ | | |/ __| |/ _ \ / _` | |/ /
|
||||
| . \ __/ |_| | (__| | (_) | (_| | <
|
||||
|_|\_\___|\__, |\___|_|\___/ \__,_|_|\_\
|
||||
|___/
|
||||
|
Loading…
Reference in a new issue