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