server driven success page

This commit is contained in:
Bill Burke 2018-03-31 10:16:44 -04:00
parent 06f32a47ec
commit 4078e84fb6
11 changed files with 100 additions and 94 deletions

View file

@ -98,70 +98,12 @@ public class KeycloakInstalled {
this.deployment = deployment;
}
private static HttpResponseWriter defaultLoginWriter = new HttpResponseWriter() {
@Override
public void success(PrintWriter pw, KeycloakInstalled ki) {
pw.println("HTTP/1.1 200 OK");
pw.println("Content-Type: text/html");
pw.println();
pw.println("<html><h1>Login completed.</h1><div>");
pw.println("This browser will remain logged in until you close it, logout, or the session expires.");
pw.println("</div></html>");
pw.flush();
}
@Override
public void failure(PrintWriter pw, KeycloakInstalled ki) {
pw.println("HTTP/1.1 200 OK");
pw.println("Content-Type: text/html");
pw.println();
pw.println("<html><h1>Login attempt failed.</h1><div>");
pw.println("</div></html>");
pw.flush();
}
};
private static HttpResponseWriter defaultLogoutWriter = new HttpResponseWriter() {
@Override
public void success(PrintWriter pw, KeycloakInstalled ki) {
pw.println("HTTP/1.1 200 OK");
pw.println("Content-Type: text/html");
pw.println();
pw.println("<html><h1>Logout completed.</h1><div>");
pw.println("You may close this browser tab.");
pw.println("</div></html>");
pw.flush();
}
@Override
public void failure(PrintWriter pw, KeycloakInstalled ki) {
pw.println("HTTP/1.1 200 OK");
pw.println("Content-Type: text/html");
pw.println();
pw.println("<html><h1>Logout failed.</h1><div>");
pw.println("You may close this browser tab.");
pw.println("</div></html>");
pw.flush();
}
};
public HttpResponseWriter getLoginResponseWriter() {
if (loginResponseWriter == null) {
return defaultLoginWriter;
} else {
return loginResponseWriter;
}
return null;
}
public HttpResponseWriter getLogoutResponseWriter() {
if (logoutResponseWriter == null) {
return defaultLogoutWriter;
} else {
return logoutResponseWriter;
}
return null;
}
public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
@ -709,11 +651,26 @@ public class KeycloakInstalled {
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
PrintWriter pw = new PrintWriter(out);
if (writer != null) {
System.err.println("Using a writer is deprecated. Please remove its usage. This is now handled by endpoint on server");
}
if (error == null) {
writer.success(pw, KeycloakInstalled.this);
if (writer != null) {
writer.success(pw, KeycloakInstalled.this);
} else {
pw.println("HTTP/1.1 302 Found");
pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated"));
}
} else {
writer.failure(pw, KeycloakInstalled.this);
if (writer != null) {
writer.failure(pw, KeycloakInstalled.this);
} else {
pw.println("HTTP/1.1 302 Found");
pw.println("Location: " + deployment.getTokenUrl().replace("/token", "/delegated?error=true"));
}
}
pw.flush();
socket.close();

View file

@ -60,7 +60,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
}
protected Authenticator createAuthenticator(AuthenticatorFactory factory) {
String display = processor.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
String display = processor.getAuthenticationSession().getAuthNote(OAuth2Constants.DISPLAY);
if (display == null) return factory.create(processor.getSession());
@ -70,7 +70,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
}
// todo create a provider for handling lack of display support
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
processor.getAuthenticationSession().removeClientNote(OAuth2Constants.DISPLAY);
processor.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED,
ConsoleDisplayMode.browserContinue(processor.getSession(), processor.getRefreshUrl(true).toString()));

View file

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

View file

@ -20,6 +20,7 @@ package org.keycloak.protocol.oidc;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.jose.jwk.JSONWebKeySet;
@ -27,6 +28,7 @@ import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKBuilder;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
@ -34,6 +36,8 @@ import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint;
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CacheControlUtil;
@ -75,6 +79,9 @@ public class OIDCLoginProtocolService {
@Context
private HttpRequest request;
@Context
private ClientConnection clientConnection;
public OIDCLoginProtocolService(RealmModel realm, EventBuilder event) {
this.realm = realm;
this.tokenManager = new TokenManager();
@ -228,4 +235,31 @@ public class OIDCLoginProtocolService {
}
}
/**
* For KeycloakInstalled and kcinit login where command line login is delegated to a browser.
* This clears login cookies and outputs login success or failure messages.
*
* @param error
* @return
*/
@GET
@Path("delegated")
public Response kcinitBrowserLoginComplete(@QueryParam("error") boolean error) {
AuthenticationManager.expireIdentityCookie(realm, uriInfo, clientConnection);
AuthenticationManager.expireRememberMeCookie(realm, uriInfo, clientConnection);
if (error) {
LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class);
return forms
.setAttribute("messageHeader", forms.getMessage(Messages.DELEGATION_FAILED_HEADER))
.setAttribute(Constants.SKIP_LINK, true).setError(Messages.DELEGATION_FAILED).createInfoPage();
} else {
LoginFormsProvider forms = session.getProvider(LoginFormsProvider.class);
return forms
.setAttribute("messageHeader", forms.getMessage(Messages.DELEGATION_COMPLETE_HEADER))
.setAttribute(Constants.SKIP_LINK, true)
.setSuccess(Messages.DELEGATION_COMPLETE).createInfoPage();
}
}
}

View file

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

View file

@ -967,7 +967,7 @@ public class AuthenticationManager {
}
public static RequiredActionProvider createRequiredAction(RequiredActionContextResult context) {
String display = context.getAuthenticationSession().getClientNote(OAuth2Constants.DISPLAY);
String display = context.getAuthenticationSession().getAuthNote(OAuth2Constants.DISPLAY);
if (display == null) return context.getFactory().create(context.getSession());
@ -977,7 +977,7 @@ public class AuthenticationManager {
}
// todo create a provider for handling lack of display support
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
context.getAuthenticationSession().removeClientNote(OAuth2Constants.DISPLAY);
context.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, ConsoleDisplayMode.browserContinue(context.getSession(), context.getUriInfo().getRequestUri().toString()));
} else {

View file

@ -225,4 +225,9 @@ public class Messages {
public static final String INTERNAL_SERVER_ERROR = "internalServerError";
public static final String DELEGATION_COMPLETE = "delegationCompleteMessage";
public static final String DELEGATION_COMPLETE_HEADER = "delegationCompleteHeader";
public static final String DELEGATION_FAILED = "delegationFailedMessage";
public static final String DELEGATION_FAILED_HEADER = "delegationFailedHeader";
}

View file

@ -239,7 +239,7 @@ public abstract class AbstractExec {
}
}
throw new RuntimeException("Timed while waiting for content to appear in stdout");
throw new RuntimeException("Timed while waiting for content to appear in stderr");
}
public void sendToStdin(String s) {

View file

@ -107,11 +107,9 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
}
ClientModel kcinit = realm.addClient(KCINIT_CLIENT);
kcinit.setSecret("password");
kcinit.setEnabled(true);
kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
kcinit.addRedirectUri("http://localhost:*");
kcinit.setPublicClient(false);
kcinit.setPublicClient(true);
ClientModel app = realm.addClient(APP);
app.setSecret("password");
@ -272,13 +270,10 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
String current = driver.getCurrentUrl();
Pattern codePattern = Pattern.compile("code=([^&]+)");
Matcher m = codePattern.matcher(current);
Assert.assertTrue(m.find());
exe.waitForStderr("Login successful");
exe.waitCompletion();
Assert.assertEquals(0, exe.exitCode());
Assert.assertTrue(driver.getPageSource().contains("Login Successful"));
} finally {
testingClient.server().run(session -> {
@ -325,6 +320,7 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
exe.waitForStderr("Login successful");
exe.waitCompletion();
Assert.assertEquals(0, exe.exitCode());
Assert.assertTrue(driver.getPageSource().contains("Login Successful"));
}
@ -356,16 +352,36 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
exe.waitForStderr("client id [kcinit]:");
exe.sendLine("");
//System.out.println(exe.stderrString());
exe.waitForStderr("Client secret [none]:");
exe.sendLine("password");
exe.waitForStderr("secret [none]:");
exe.sendLine("");
//System.out.println(exe.stderrString());
exe.waitCompletion();
Assert.assertEquals(0, exe.exitCode());
}
@Test
public void testOffline() throws Exception {
testInstall();
// login
//System.out.println("login....");
KcinitExec exe = KcinitExec.newBuilder()
.argsLine("login --offline")
.executeAsync();
//System.out.println(exe.stderrString());
exe.waitForStderr("Username:");
exe.sendLine("wburke");
//System.out.println(exe.stderrString());
exe.waitForStderr("Password:");
exe.sendLine("password");
//System.out.println(exe.stderrString());
exe.waitForStderr("Offline tokens not allowed for the user or client");
exe.waitCompletion();
Assert.assertEquals(1, exe.exitCode());
}
@Test
public void testBasic() throws Exception {
testInstall();
// login
@ -390,12 +406,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(1, exe.stdoutLines().size());
String token = exe.stdoutLines().get(0).trim();
//System.out.println("token: " + token);
String introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", token);
Map json = JsonSerialization.readValue(introspect, Map.class);
Assert.assertTrue(json.containsKey("active"));
Assert.assertTrue((Boolean)json.get("active"));
//System.out.println("introspect");
//System.out.println(introspect);
exe = KcinitExec.execute("token app");
Assert.assertEquals(0, exe.exitCode());
@ -403,10 +413,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
String appToken = exe.stdoutLines().get(0).trim();
Assert.assertFalse(appToken.equals(token));
//System.out.println("token: " + token);
introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", appToken);
json = JsonSerialization.readValue(introspect, Map.class);
Assert.assertTrue(json.containsKey("active"));
Assert.assertTrue((Boolean)json.get("active"));
exe = KcinitExec.execute("token badapp");
@ -418,10 +424,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
exe = KcinitExec.execute("logout");
Assert.assertEquals(0, exe.exitCode());
introspect = oauth.introspectAccessTokenWithClientCredential("kcinit", "password", token);
json = JsonSerialization.readValue(introspect, Map.class);
Assert.assertTrue(json.containsKey("active"));
Assert.assertFalse((Boolean)json.get("active"));

View file

@ -1,7 +1,11 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=false; section>
<#if section = "header">
<#if messageHeader??>
${messageHeader}
<#else>
${message.summary}
</#if>
<#elseif section = "form">
<div id="kc-info-message">
<p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></b></#list><#else></#if></p>

View file

@ -190,6 +190,11 @@ emailSendErrorMessage=Failed to send email, please try again later.
accountUpdatedMessage=Your account has been updated.
accountPasswordUpdatedMessage=Your password has been updated.
delegationCompleteHeader=Login Successful
delegationCompleteMessage=You may close this browser window and go back to your console application.
delegationFailedHeader=Login Failed
delegationFailedMessage=You may close this browser window and go back to your console application and try logging in again.
noAccessMessage=No access
invalidPasswordMinLengthMessage=Invalid password: minimum length {0}.