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; 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() { public HttpResponseWriter getLoginResponseWriter() {
if (loginResponseWriter == null) { return null;
return defaultLoginWriter;
} else {
return loginResponseWriter;
}
} }
public HttpResponseWriter getLogoutResponseWriter() { public HttpResponseWriter getLogoutResponseWriter() {
if (logoutResponseWriter == null) { return null;
return defaultLogoutWriter;
} else {
return logoutResponseWriter;
}
} }
public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) { public void setLoginResponseWriter(HttpResponseWriter loginResponseWriter) {
@ -709,11 +651,26 @@ public class KeycloakInstalled {
OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream()); OutputStreamWriter out = new OutputStreamWriter(socket.getOutputStream());
PrintWriter pw = new PrintWriter(out); 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) { 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 { } 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(); pw.flush();
socket.close(); socket.close();

View file

@ -60,7 +60,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
} }
protected Authenticator createAuthenticator(AuthenticatorFactory factory) { 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()); 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 // todo create a provider for handling lack of display support
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) { if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) {
processor.getAuthenticationSession().removeClientNote(OAuth2Constants.DISPLAY); processor.getAuthenticationSession().removeAuthNote(OAuth2Constants.DISPLAY);
throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED,
ConsoleDisplayMode.browserContinue(processor.getSession(), processor.getRefreshUrl(true).toString())); 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 MAX_AGE_PARAM = OAuth2Constants.MAX_AGE;
public static final String PROMPT_PARAM = OAuth2Constants.PROMPT; public static final String PROMPT_PARAM = OAuth2Constants.PROMPT;
public static final String LOGIN_HINT_PARAM = "login_hint"; 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_PARAM = "request";
public static final String REQUEST_URI_PARAM = "request_uri"; public static final String REQUEST_URI_PARAM = "request_uri";
public static final String UI_LOCALES_PARAM = OAuth2Constants.UI_LOCALES_PARAM; 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.annotations.cache.NoCache;
import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.forms.login.LoginFormsProvider; import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.jose.jwk.JSONWebKeySet; 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.jose.jwk.JWKBuilder;
import org.keycloak.keys.KeyMetadata; import org.keycloak.keys.KeyMetadata;
import org.keycloak.keys.RsaKeyMetadata; import org.keycloak.keys.RsaKeyMetadata;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; 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.LogoutEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint; import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint; 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.Cors;
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.CacheControlUtil;
@ -75,6 +79,9 @@ public class OIDCLoginProtocolService {
@Context @Context
private HttpRequest request; private HttpRequest request;
@Context
private ClientConnection clientConnection;
public OIDCLoginProtocolService(RealmModel realm, EventBuilder event) { public OIDCLoginProtocolService(RealmModel realm, EventBuilder event) {
this.realm = realm; this.realm = realm;
this.tokenManager = new TokenManager(); 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.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims()); if (request.getClaims()!= null) authenticationSession.setClientNote(OIDCLoginProtocol.CLAIMS_PARAM, request.getClaims());
if (request.getAcr() != null) authenticationSession.setClientNote(OIDCLoginProtocol.ACR_PARAM, request.getAcr()); 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 // https://tools.ietf.org/html/rfc7636#section-4
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge()); 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) { 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()); 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 // todo create a provider for handling lack of display support
if (OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(display)) { 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())); throw new AuthenticationFlowException(AuthenticationFlowError.DISPLAY_NOT_SUPPORTED, ConsoleDisplayMode.browserContinue(context.getSession(), context.getUriInfo().getRequestUri().toString()));
} else { } else {

View file

@ -225,4 +225,9 @@ public class Messages {
public static final String INTERNAL_SERVER_ERROR = "internalServerError"; 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) { public void sendToStdin(String s) {

View file

@ -107,11 +107,9 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
} }
ClientModel kcinit = realm.addClient(KCINIT_CLIENT); ClientModel kcinit = realm.addClient(KCINIT_CLIENT);
kcinit.setSecret("password");
kcinit.setEnabled(true); kcinit.setEnabled(true);
kcinit.addRedirectUri("urn:ietf:wg:oauth:2.0:oob");
kcinit.addRedirectUri("http://localhost:*"); kcinit.addRedirectUri("http://localhost:*");
kcinit.setPublicClient(false); kcinit.setPublicClient(true);
ClientModel app = realm.addClient(APP); ClientModel app = realm.addClient(APP);
app.setSecret("password"); app.setSecret("password");
@ -272,13 +270,10 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
String current = driver.getCurrentUrl(); String current = driver.getCurrentUrl();
Pattern codePattern = Pattern.compile("code=([^&]+)");
Matcher m = codePattern.matcher(current);
Assert.assertTrue(m.find());
exe.waitForStderr("Login successful"); exe.waitForStderr("Login successful");
exe.waitCompletion(); exe.waitCompletion();
Assert.assertEquals(0, exe.exitCode()); Assert.assertEquals(0, exe.exitCode());
Assert.assertTrue(driver.getPageSource().contains("Login Successful"));
} finally { } finally {
testingClient.server().run(session -> { testingClient.server().run(session -> {
@ -325,6 +320,7 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
exe.waitForStderr("Login successful"); exe.waitForStderr("Login successful");
exe.waitCompletion(); exe.waitCompletion();
Assert.assertEquals(0, exe.exitCode()); 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.waitForStderr("client id [kcinit]:");
exe.sendLine(""); exe.sendLine("");
//System.out.println(exe.stderrString()); //System.out.println(exe.stderrString());
exe.waitForStderr("Client secret [none]:"); exe.waitForStderr("secret [none]:");
exe.sendLine("password"); exe.sendLine("");
//System.out.println(exe.stderrString()); //System.out.println(exe.stderrString());
exe.waitCompletion(); exe.waitCompletion();
Assert.assertEquals(0, exe.exitCode()); Assert.assertEquals(0, exe.exitCode());
} }
@Test @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 { public void testBasic() throws Exception {
testInstall(); testInstall();
// login // login
@ -390,12 +406,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(1, exe.stdoutLines().size()); Assert.assertEquals(1, exe.stdoutLines().size());
String token = exe.stdoutLines().get(0).trim(); String token = exe.stdoutLines().get(0).trim();
//System.out.println("token: " + token); //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"); exe = KcinitExec.execute("token app");
Assert.assertEquals(0, exe.exitCode()); Assert.assertEquals(0, exe.exitCode());
@ -403,10 +413,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
String appToken = exe.stdoutLines().get(0).trim(); String appToken = exe.stdoutLines().get(0).trim();
Assert.assertFalse(appToken.equals(token)); Assert.assertFalse(appToken.equals(token));
//System.out.println("token: " + 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"); exe = KcinitExec.execute("token badapp");
@ -418,10 +424,6 @@ public class KcinitTest extends AbstractTestRealmKeycloakTest {
exe = KcinitExec.execute("logout"); exe = KcinitExec.execute("logout");
Assert.assertEquals(0, exe.exitCode()); 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> <#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=false; section> <@layout.registrationLayout displayMessage=false; section>
<#if section = "header"> <#if section = "header">
<#if messageHeader??>
${messageHeader}
<#else>
${message.summary} ${message.summary}
</#if>
<#elseif section = "form"> <#elseif section = "form">
<div id="kc-info-message"> <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> <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. accountUpdatedMessage=Your account has been updated.
accountPasswordUpdatedMessage=Your password 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 noAccessMessage=No access
invalidPasswordMinLengthMessage=Invalid password: minimum length {0}. invalidPasswordMinLengthMessage=Invalid password: minimum length {0}.