refactor auth-spi, auth demo, and docs
This commit is contained in:
parent
1f13f6372a
commit
6d7be80930
9 changed files with 83 additions and 19 deletions
|
@ -624,8 +624,8 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
|
||||||
<title>Enable Required Action</title>
|
<title>Enable Required Action</title>
|
||||||
<para>
|
<para>
|
||||||
The final thing you have to do is go into the admin console. Click on the Authentication left menu.
|
The final thing you have to do is go into the admin console. Click on the Authentication left menu.
|
||||||
Click on the Required Actions tab. Find your required action, and enable. Alternatively, if you
|
Click on the Required Actions tab. Click on the Register button and choose your new Required Action.
|
||||||
click on the default action checkbox, this required action will be applied anytime a new user is created.
|
Your new required action should now be displayed and enabled in the required actions list.
|
||||||
</para>
|
</para>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -14,13 +14,19 @@ Then registering the provider by editing keycloak-server.json and adding the mod
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
You then have to copy the secret-question.ftl file to the standalone/configuration/themes/base/login directory.
|
You then have to copy the secret-question.ftl and secret-question-config.ftl files to the standalone/configuration/themes/base/login directory.
|
||||||
|
|
||||||
After you do all this, you then have to reboot keycloak. When reboot is complete, you will need to log into
|
After you do all this, you then have to reboot keycloak. When reboot is complete, you will need to log into
|
||||||
the admin console to create a new flow with your new authenticator.
|
the admin console to create a new flow with your new authenticator.
|
||||||
|
|
||||||
If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently
|
If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently
|
||||||
defined flows. You cannot modify an built in flows, so, to add the Authenticator you
|
defined flows. You cannot modify an built in flows, so, to add the Authenticator you
|
||||||
have to copy an existing flow or create your own. I'm hoping the UI is intuitive enough so that you
|
have to copy an existing flow or create your own.
|
||||||
can figure out for yourself how to create a flow and add the Authenticator. We're looking to add a screencast
|
|
||||||
|
Next you have to register your required action.
|
||||||
|
Click on the Required Actions tab. Click on the Register button and choose your new Required Action.
|
||||||
|
Your new required action should now be displayed and enabled in the required actions list.
|
||||||
|
|
||||||
|
I'm hoping the UI is intuitive enough so that you
|
||||||
|
can figure out for yourself how to create a flow and add the Authenticator and Required Action. We're looking to add a screencast
|
||||||
to show this in action.
|
to show this in action.
|
||||||
|
|
33
examples/providers/authenticator/secret-question-config.ftl
Executable file
33
examples/providers/authenticator/secret-question-config.ftl
Executable file
|
@ -0,0 +1,33 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<@layout.registrationLayout; section>
|
||||||
|
<#if section = "title">
|
||||||
|
${msg("loginTitle",realm.name)}
|
||||||
|
<#elseif section = "header">
|
||||||
|
Setup Secret Question
|
||||||
|
<#elseif section = "form">
|
||||||
|
<form id="kc-totp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||||
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
|
<div class="${properties.kcLabelWrapperClass!}">
|
||||||
|
<label for="totp" class="${properties.kcLabelClass!}">What is your mom's first name?</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="${properties.kcInputWrapperClass!}">
|
||||||
|
<input id="totp" name="secret_answer" type="text" class="${properties.kcInputClass!}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
|
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
||||||
|
<div class="${properties.kcFormOptionsWrapperClass!}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
|
||||||
|
<div class="${properties.kcFormButtonsWrapperClass!}">
|
||||||
|
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doSubmit")}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
|
@ -13,6 +13,8 @@ import org.keycloak.services.util.CookieHelper;
|
||||||
import javax.ws.rs.core.Cookie;
|
import javax.ws.rs.core.Cookie;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -24,7 +26,11 @@ public class SecretQuestionAuthenticator implements Authenticator {
|
||||||
|
|
||||||
protected boolean hasCookie(AuthenticationFlowContext context) {
|
protected boolean hasCookie(AuthenticationFlowContext context) {
|
||||||
Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
|
Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED");
|
||||||
return cookie != null;
|
boolean result = cookie != null;
|
||||||
|
if (result) {
|
||||||
|
System.out.println("Bypassing secret question because cookie as set");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -33,12 +39,17 @@ public class SecretQuestionAuthenticator implements Authenticator {
|
||||||
context.success();
|
context.success();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Response challenge = context.form().createForm("secret_question.ftl");
|
Response challenge = context.form().createForm("secret-question.ftl");
|
||||||
context.challenge(challenge);
|
context.challenge(challenge);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void action(AuthenticationFlowContext context) {
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
if (formData.containsKey("cancel")) {
|
||||||
|
context.cancelLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
boolean validated = validateAnswer(context);
|
boolean validated = validateAnswer(context);
|
||||||
if (!validated) {
|
if (!validated) {
|
||||||
Response challenge = context.form()
|
Response challenge = context.form()
|
||||||
|
@ -58,11 +69,12 @@ public class SecretQuestionAuthenticator implements Authenticator {
|
||||||
maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));
|
maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build();
|
||||||
CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true",
|
CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true",
|
||||||
context.getUriInfo().getBaseUri().getPath() + "/realms/" + context.getRealm().getName(),
|
uri.getRawPath(),
|
||||||
null, null,
|
null, null,
|
||||||
maxCookieAge,
|
maxCookieAge,
|
||||||
true, true);
|
false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean validateAnswer(AuthenticationFlowContext context) {
|
protected boolean validateAnswer(AuthenticationFlowContext context) {
|
||||||
|
|
|
@ -20,14 +20,14 @@ public class SecretQuestionRequiredAction implements RequiredActionProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void requiredActionChallenge(RequiredActionContext context) {
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
Response challenge = context.form().createForm("secret_question_config.ftl");
|
Response challenge = context.form().createForm("secret-question-config.ftl");
|
||||||
context.challenge(challenge);
|
context.challenge(challenge);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processAction(RequiredActionContext context) {
|
public void processAction(RequiredActionContext context) {
|
||||||
String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("answer"));
|
String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer"));
|
||||||
UserCredentialValueModel model = new UserCredentialValueModel();
|
UserCredentialValueModel model = new UserCredentialValueModel();
|
||||||
model.setValue(answer);
|
model.setValue(answer);
|
||||||
model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);
|
model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE);
|
||||||
|
|
|
@ -217,4 +217,11 @@ public interface AuthenticationFlowContext {
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
URI getActionUrl();
|
URI getActionUrl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* End the flow and redirect browser based on protocol specific respones. This should only be executed
|
||||||
|
* in browser-based flows.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void cancelLogin();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
@ -367,6 +368,17 @@ public class AuthenticationProcessor {
|
||||||
public URI getActionUrl() {
|
public URI getActionUrl() {
|
||||||
return getActionUrl(generateAccessCode());
|
return getActionUrl(generateAccessCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelLogin() {
|
||||||
|
getEvent().error(Errors.REJECTED_BY_USER);
|
||||||
|
LoginProtocol protocol = getSession().getProvider(LoginProtocol.class, getClientSession().getAuthMethod());
|
||||||
|
protocol.setRealm(getRealm())
|
||||||
|
.setHttpHeaders(getHttpRequest().getHttpHeaders())
|
||||||
|
.setUriInfo(getUriInfo());
|
||||||
|
Response response = protocol.cancelLogin(getClientSession());
|
||||||
|
forceChallenge(response);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void logFailure() {
|
public void logFailure() {
|
||||||
|
|
|
@ -26,13 +26,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
|
||||||
public void action(AuthenticationFlowContext context) {
|
public void action(AuthenticationFlowContext context) {
|
||||||
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
if (formData.containsKey("cancel")) {
|
if (formData.containsKey("cancel")) {
|
||||||
context.getEvent().error(Errors.REJECTED_BY_USER);
|
context.cancelLogin();
|
||||||
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
|
|
||||||
protocol.setRealm(context.getRealm())
|
|
||||||
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
|
|
||||||
.setUriInfo(context.getUriInfo());
|
|
||||||
Response response = protocol.cancelLogin(context.getClientSession());
|
|
||||||
context.forceChallenge(response);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!validateForm(context, formData)) {
|
if (!validateForm(context, formData)) {
|
||||||
|
|
|
@ -167,7 +167,7 @@ public class AccountTest {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
//@Test
|
||||||
public void ideTesting() throws Exception {
|
public void ideTesting() throws Exception {
|
||||||
Thread.sleep(100000000);
|
Thread.sleep(100000000);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue