server driven success page
This commit is contained in:
parent
06f32a47ec
commit
4078e84fb6
11 changed files with 100 additions and 94 deletions
|
@ -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();
|
||||
|
|
|
@ -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()));
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"));
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}.
|
||||
|
|
Loading…
Reference in a new issue