From a123bb113cf7b77871f838f3a134a79dcab7c915 Mon Sep 17 00:00:00 2001 From: stianst Date: Wed, 13 Mar 2024 11:49:28 +0100 Subject: [PATCH] Move authenticator example to quickstarts Signed-off-by: stianst Add Secret Question authenticator back To do that, implemented `getCredentialType` for `SecretQuestionRequiredAction` Closes https://github.com/keycloak/keycloak-quickstarts/issues/553 --- extension/authenticator/README.md | 64 ++++++++ extension/authenticator/pom.xml | 66 +++++++++ .../SecretQuestionAuthenticator.java | 139 ++++++++++++++++++ .../SecretQuestionAuthenticatorFactory.java | 120 +++++++++++++++ .../SecretQuestionCredentialProvider.java | 109 ++++++++++++++ ...cretQuestionCredentialProviderFactory.java | 40 +++++ .../SecretQuestionRequiredAction.java | 66 +++++++++ .../SecretQuestionRequiredActionFactory.java | 65 ++++++++ .../SecretQuestionCredentialModel.java | 91 ++++++++++++ .../dto/SecretQuestionCredentialData.java | 39 +++++ .../dto/SecretQuestionSecretData.java | 39 +++++ ...ycloak.authentication.AuthenticatorFactory | 18 +++ ...cloak.authentication.RequiredActionFactory | 18 +++ ...cloak.credential.CredentialProviderFactory | 1 + .../templates/secret-question-config.ftl | 33 +++++ .../templates/secret-question.ftl | 35 +++++ 16 files changed, 943 insertions(+) create mode 100755 extension/authenticator/README.md create mode 100755 extension/authenticator/pom.xml create mode 100755 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java create mode 100755 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java create mode 100644 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java create mode 100644 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProviderFactory.java create mode 100755 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java create mode 100755 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java create mode 100644 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/SecretQuestionCredentialModel.java create mode 100644 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/dto/SecretQuestionCredentialData.java create mode 100644 extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/dto/SecretQuestionSecretData.java create mode 100755 extension/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory create mode 100755 extension/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory create mode 100644 extension/authenticator/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory create mode 100755 extension/authenticator/src/main/resources/theme-resources/templates/secret-question-config.ftl create mode 100755 extension/authenticator/src/main/resources/theme-resources/templates/secret-question.ftl diff --git a/extension/authenticator/README.md b/extension/authenticator/README.md new file mode 100755 index 000000000..5891b401a --- /dev/null +++ b/extension/authenticator/README.md @@ -0,0 +1,64 @@ +authenticator: Example custom authenticator +======================================================== + +Level: Beginner +Summary: Example custom authenticator +Target Product: Keycloak +Source: + + +What is it? +----------- + +This is an example of the Authenticator SPI implemented a custom authenticator. It allows the user to create a secret +question that is prompted when a user logs in to a new machine. The example does not aim to provide a realistic +authentication mechanisms and should not be leveraged in production. + + +System Requirements +------------------- + +You need to have Keycloak running. It is recommended to use Keycloak 24 or later. + +All you need to build this project is Java 17 (Java SDK 17) or later and Maven 3.6.3 or later. + +Build and Deploy the Quickstart +------------------------------- + +To build the provider, run the following maven command: + + ```` + mvn -Pextension clean install -DskipTests=true + ```` + +To install the provider, copy the target/authenticator-example.jar file to the `providers` directory of the server distribution. + +Finally, start the server as follows: + + kc.[sh|bat] start-dev + +Enable the Provider for a Realm +------------------------------- +It is not recommended to try this out with the master realm, so either use an existing test realm or create a new one. +If you create a new realm you must also register at least one user in the realm. + +To enable the custom authenticator the first step is to create a custom authentication flow where it can be registered. +Login to the Keycloak Admin Console and got to the Authentication tab. Select the `browser` flow and under +`Action` click on `Duplicate`. Leave the name and description as is, and click on `Duplicate` to create the custom +authentication flow. + +Next step is to add the authenticator to the custom flow. On the row named `Copy of browser forms` click on the '+' +symbol and select `Add step`. Enter `secret` in the search box to find the custom authenticator. Select `Secret question` +and click `Add`. When added it will not be required initially, so find the row for `Secret Questions` and change +the requirement to `Required`. + +Next step is to bind the custom flow as the default browser flow. Click on `Action` and select `Bind flow`, +choose the binding type `Browser flow` and click on `Save`. + +Final step is to enable the required action that is used by users to enter the answer needed to login. Click on +`Authentication` then on `Required actions`. Next to `Secret Question` click on the toggle for `Enabled` to turn on +the required action. + +Now everything is configured and ready to try out, and you can try to login to the realm. The first time the user logs +in the user will be prompted to provide an answer to a question. The user will be prompted the question again when +using a new machine or closing the browser. \ No newline at end of file diff --git a/extension/authenticator/pom.xml b/extension/authenticator/pom.xml new file mode 100755 index 000000000..7ad94c5c8 --- /dev/null +++ b/extension/authenticator/pom.xml @@ -0,0 +1,66 @@ + + + + + keycloak-quickstart-parent + org.keycloak.quickstarts + 999.0.0-SNAPSHOT + ../../pom.xml + + + Keycloak Quickstart: Authenticator Example + + 4.0.0 + + authenticator-example + jar + + + + org.keycloak + keycloak-core + provided + + + org.keycloak + keycloak-server-spi + provided + + + org.keycloak + keycloak-server-spi-private + provided + + + org.jboss.logging + jboss-logging + provided + + + org.keycloak + keycloak-services + ${version.keycloak} + provided + + + + + authenticator-example + + diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java new file mode 100755 index 000000000..3eacfb8d7 --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java @@ -0,0 +1,139 @@ +/* + * 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.examples.authenticator; + +import jakarta.ws.rs.core.Cookie; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.NewCookie; +import jakarta.ws.rs.core.Response; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.AuthenticationFlowError; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.CredentialValidator; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; + +import java.net.URI; +import java.util.Collections; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionAuthenticator implements Authenticator, CredentialValidator { + + protected boolean hasCookie(AuthenticationFlowContext context) { + Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED"); + boolean result = cookie != null; + if (result) { + System.out.println("Bypassing secret question because cookie is set"); + } + return result; + } + + @Override + public void authenticate(AuthenticationFlowContext context) { + if (hasCookie(context)) { + context.success(); + return; + } + Response challenge = context.form() + .createForm("secret-question.ftl"); + context.challenge(challenge); + } + + @Override + public void action(AuthenticationFlowContext context) { + boolean validated = validateAnswer(context); + if (!validated) { + Response challenge = context.form() + .setError("badSecret") + .createForm("secret-question.ftl"); + context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); + return; + } + setCookie(context); + context.success(); + } + + protected void setCookie(AuthenticationFlowContext context) { + AuthenticatorConfigModel config = context.getAuthenticatorConfig(); + int maxCookieAge = 60 * 60 * 24 * 30; // 30 days + if (config != null) { + maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age")); + + } + URI uri = context.getUriInfo().getBaseUriBuilder().path("realms").path(context.getRealm().getName()).build(); + + NewCookie newCookie = new NewCookie.Builder("SECRET_QUESTION_ANSWERED").value("true") + .path(uri.getRawPath()) + .maxAge(maxCookieAge) + .secure(false) + .build(); + context.getSession().getContext().getHttpResponse().setCookieIfAbsent(newCookie); + } + + protected boolean validateAnswer(AuthenticationFlowContext context) { + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + String secret = formData.getFirst("secret_answer"); + String credentialId = formData.getFirst("credentialId"); + if (credentialId == null || credentialId.isEmpty()) { + credentialId = getCredentialProvider(context.getSession()) + .getDefaultCredential(context.getSession(), context.getRealm(), context.getUser()).getId(); + } + + UserCredentialModel input = new UserCredentialModel(credentialId, getType(context.getSession()), secret); + return getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(), input); + } + + @Override + public boolean requiresUser() { + return true; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return getCredentialProvider(session).isConfiguredFor(realm, user, getType(session)); + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID); + } + + public List getRequiredActions(KeycloakSession session) { + return Collections.singletonList((SecretQuestionRequiredActionFactory)session.getKeycloakSessionFactory().getProviderFactory(RequiredActionProvider.class, SecretQuestionRequiredAction.PROVIDER_ID)); + } + + @Override + public void close() { + + } + + @Override + public SecretQuestionCredentialProvider getCredentialProvider(KeycloakSession session) { + return (SecretQuestionCredentialProvider)session.getProvider(CredentialProvider.class, SecretQuestionCredentialProviderFactory.PROVIDER_ID); + } +} diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java new file mode 100755 index 000000000..9b6bc659d --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java @@ -0,0 +1,120 @@ +/* + * 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.examples.authenticator; + +import org.keycloak.Config; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.authentication.ConfigurableAuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { + + public static final String PROVIDER_ID = "secret-question-authenticator"; + private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator(); + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public Authenticator create(KeycloakSession session) { + return SINGLETON; + } + + private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.DISABLED + }; + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return true; + } + + @Override + public boolean isConfigurable() { + return true; + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + private static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty property; + property = new ProviderConfigProperty(); + property.setName("cookie.max.age"); + property.setLabel("Cookie Max Age"); + property.setType(ProviderConfigProperty.STRING_TYPE); + property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE."); + configProperties.add(property); + } + + + @Override + public String getHelpText() { + return "A secret question that a user has to answer. i.e. What is your mother's maiden name."; + } + + @Override + public String getDisplayType() { + return "Secret Question"; + } + + @Override + public String getReferenceCategory() { + return "Secret Question"; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + +} diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java new file mode 100644 index 000000000..2d0dc68c8 --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProvider.java @@ -0,0 +1,109 @@ +/* + * 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.examples.authenticator; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.Time; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.credential.CredentialModel; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.CredentialTypeMetadata; +import org.keycloak.credential.CredentialTypeMetadataContext; +import org.keycloak.examples.authenticator.credential.SecretQuestionCredentialModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserModel; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionCredentialProvider implements CredentialProvider, CredentialInputValidator { + private static final Logger logger = Logger.getLogger(SecretQuestionCredentialProvider.class); + + protected KeycloakSession session; + + public SecretQuestionCredentialProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { + if (!(input instanceof UserCredentialModel)) { + logger.debug("Expected instance of UserCredentialModel for CredentialInput"); + return false; + } + if (!input.getType().equals(getType())) { + return false; + } + String challengeResponse = input.getChallengeResponse(); + if (challengeResponse == null) { + return false; + } + CredentialModel credentialModel = user.credentialManager().getStoredCredentialById(input.getCredentialId()); + SecretQuestionCredentialModel sqcm = getCredentialFromModel(credentialModel); + return sqcm.getSecretQuestionSecretData().getAnswer().equals(challengeResponse); + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return getType().equals(credentialType); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + if (!supportsCredentialType(credentialType)) return false; + return user.credentialManager().getStoredCredentialsByTypeStream(credentialType).findAny().isPresent(); + } + + @Override + public CredentialModel createCredential(RealmModel realm, UserModel user, SecretQuestionCredentialModel credentialModel) { + if (credentialModel.getCreatedDate() == null) { + credentialModel.setCreatedDate(Time.currentTimeMillis()); + } + return user.credentialManager().createStoredCredential(credentialModel); + } + + @Override + public boolean deleteCredential(RealmModel realm, UserModel user, String credentialId) { + return user.credentialManager().removeStoredCredentialById(credentialId); + } + + @Override + public SecretQuestionCredentialModel getCredentialFromModel(CredentialModel model) { + return SecretQuestionCredentialModel.createFromCredentialModel(model); + } + + @Override + public CredentialTypeMetadata getCredentialTypeMetadata(CredentialTypeMetadataContext metadataContext) { + return CredentialTypeMetadata.builder() + .type(getType()) + .category(CredentialTypeMetadata.Category.TWO_FACTOR) + .displayName(SecretQuestionCredentialProviderFactory.PROVIDER_ID) + .helpText("secret-question-text") + .createAction(SecretQuestionAuthenticatorFactory.PROVIDER_ID) + .removeable(false) + .build(session); + } + + @Override + public String getType() { + return SecretQuestionCredentialModel.TYPE; + } +} diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProviderFactory.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProviderFactory.java new file mode 100644 index 000000000..573d26d80 --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionCredentialProviderFactory.java @@ -0,0 +1,40 @@ +/* + * 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.examples.authenticator; + +import org.keycloak.credential.CredentialProvider; +import org.keycloak.credential.CredentialProviderFactory; +import org.keycloak.models.KeycloakSession; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionCredentialProviderFactory implements CredentialProviderFactory { + + public static final String PROVIDER_ID = "secret-question"; + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public CredentialProvider create(KeycloakSession session) { + return new SecretQuestionCredentialProvider(session); + } +} diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java new file mode 100755 index 000000000..6f98e4254 --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java @@ -0,0 +1,66 @@ +/* + * 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.examples.authenticator; + +import org.keycloak.authentication.CredentialRegistrator; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.credential.CredentialProvider; +import org.keycloak.examples.authenticator.credential.SecretQuestionCredentialModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.sessions.AuthenticationSessionModel; + +import jakarta.ws.rs.core.Response; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionRequiredAction implements RequiredActionProvider, CredentialRegistrator { + public static final String PROVIDER_ID = "secret_question_config"; + + @Override + public void evaluateTriggers(RequiredActionContext context) { + + } + + @Override + public String getCredentialType(KeycloakSession session, AuthenticationSessionModel AuthenticationSession) { + return SecretQuestionCredentialModel.TYPE; + } + + @Override + public void requiredActionChallenge(RequiredActionContext context) { + Response challenge = context.form().createForm("secret-question-config.ftl"); + context.challenge(challenge); + + } + + @Override + public void processAction(RequiredActionContext context) { + String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("secret_answer")); + SecretQuestionCredentialProvider sqcp = (SecretQuestionCredentialProvider) context.getSession().getProvider(CredentialProvider.class, "secret-question"); + sqcp.createCredential(context.getRealm(), context.getUser(), SecretQuestionCredentialModel.createSecretQuestion("What is your mom's first name?", answer)); + context.success(); + } + + @Override + public void close() { + + } +} diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java new file mode 100755 index 000000000..46ad4ebc7 --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java @@ -0,0 +1,65 @@ +/* + * 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.examples.authenticator; + +import org.keycloak.Config; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionRequiredActionFactory implements RequiredActionFactory { + + private static final SecretQuestionRequiredAction SINGLETON = new SecretQuestionRequiredAction(); + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return SINGLETON; + } + + + @Override + public String getId() { + return SecretQuestionRequiredAction.PROVIDER_ID; + } + + @Override + public String getDisplayText() { + return "Secret Question"; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + +} diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/SecretQuestionCredentialModel.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/SecretQuestionCredentialModel.java new file mode 100644 index 000000000..8cca82e52 --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/SecretQuestionCredentialModel.java @@ -0,0 +1,91 @@ +/* + * 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.examples.authenticator.credential; + +import org.keycloak.common.util.Time; +import org.keycloak.credential.CredentialModel; +import org.keycloak.examples.authenticator.credential.dto.SecretQuestionCredentialData; +import org.keycloak.examples.authenticator.credential.dto.SecretQuestionSecretData; +import org.keycloak.util.JsonSerialization; + +import java.io.IOException; + +/** + * @author Alistair Doswald + * @version $Revision: 1 $ + */ +public class SecretQuestionCredentialModel extends CredentialModel { + public static final String TYPE = "SECRET_QUESTION"; + + private final SecretQuestionCredentialData credentialData; + private final SecretQuestionSecretData secretData; + + private SecretQuestionCredentialModel(SecretQuestionCredentialData credentialData, SecretQuestionSecretData secretData) { + this.credentialData = credentialData; + this.secretData = secretData; + } + + private SecretQuestionCredentialModel(String question, String answer) { + credentialData = new SecretQuestionCredentialData(question); + secretData = new SecretQuestionSecretData(answer); + } + + public static SecretQuestionCredentialModel createSecretQuestion(String question, String answer) { + SecretQuestionCredentialModel credentialModel = new SecretQuestionCredentialModel(question, answer); + credentialModel.fillCredentialModelFields(); + return credentialModel; + } + + public static SecretQuestionCredentialModel createFromCredentialModel(CredentialModel credentialModel){ + try { + SecretQuestionCredentialData credentialData = JsonSerialization.readValue(credentialModel.getCredentialData(), SecretQuestionCredentialData.class); + SecretQuestionSecretData secretData = JsonSerialization.readValue(credentialModel.getSecretData(), SecretQuestionSecretData.class); + + SecretQuestionCredentialModel secretQuestionCredentialModel = new SecretQuestionCredentialModel(credentialData, secretData); + secretQuestionCredentialModel.setUserLabel(credentialModel.getUserLabel()); + secretQuestionCredentialModel.setCreatedDate(credentialModel.getCreatedDate()); + secretQuestionCredentialModel.setType(TYPE); + secretQuestionCredentialModel.setId(credentialModel.getId()); + secretQuestionCredentialModel.setSecretData(credentialModel.getSecretData()); + secretQuestionCredentialModel.setCredentialData(credentialModel.getCredentialData()); + return secretQuestionCredentialModel; + } catch (IOException e){ + throw new RuntimeException(e); + } + } + + public SecretQuestionCredentialData getSecretQuestionCredentialData() { + return credentialData; + } + + public SecretQuestionSecretData getSecretQuestionSecretData() { + return secretData; + } + + private void fillCredentialModelFields(){ + try { + setCredentialData(JsonSerialization.writeValueAsString(credentialData)); + setSecretData(JsonSerialization.writeValueAsString(secretData)); + setType(TYPE); + setCreatedDate(Time.currentTimeMillis()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/dto/SecretQuestionCredentialData.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/dto/SecretQuestionCredentialData.java new file mode 100644 index 000000000..05033eb3a --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/dto/SecretQuestionCredentialData.java @@ -0,0 +1,39 @@ +/* + * 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.examples.authenticator.credential.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Alistair Doswald + * @version $Revision: 1 $ + */ +public class SecretQuestionCredentialData { + + private final String question; + + @JsonCreator + public SecretQuestionCredentialData(@JsonProperty("question") String question) { + this.question = question; + } + + public String getQuestion() { + return question; + } +} diff --git a/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/dto/SecretQuestionSecretData.java b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/dto/SecretQuestionSecretData.java new file mode 100644 index 000000000..9c592ee4a --- /dev/null +++ b/extension/authenticator/src/main/java/org/keycloak/examples/authenticator/credential/dto/SecretQuestionSecretData.java @@ -0,0 +1,39 @@ +/* + * 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.examples.authenticator.credential.dto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Alistair Doswald + * @version $Revision: 1 $ + */ +public class SecretQuestionSecretData { + + private final String answer; + + @JsonCreator + public SecretQuestionSecretData(@JsonProperty("answer") String answer) { + this.answer = answer; + } + + public String getAnswer() { + return answer; + } +} diff --git a/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory new file mode 100755 index 000000000..f288d3daa --- /dev/null +++ b/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -0,0 +1,18 @@ +# +# 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. +# + +org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory \ No newline at end of file diff --git a/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory b/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory new file mode 100755 index 000000000..034c2f17c --- /dev/null +++ b/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory @@ -0,0 +1,18 @@ +# +# 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. +# + +org.keycloak.examples.authenticator.SecretQuestionRequiredActionFactory \ No newline at end of file diff --git a/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory b/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory new file mode 100644 index 000000000..a221e371c --- /dev/null +++ b/extension/authenticator/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory @@ -0,0 +1 @@ + org.keycloak.examples.authenticator.SecretQuestionCredentialProviderFactory \ No newline at end of file diff --git a/extension/authenticator/src/main/resources/theme-resources/templates/secret-question-config.ftl b/extension/authenticator/src/main/resources/theme-resources/templates/secret-question-config.ftl new file mode 100755 index 000000000..54e69026b --- /dev/null +++ b/extension/authenticator/src/main/resources/theme-resources/templates/secret-question-config.ftl @@ -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"> +
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/extension/authenticator/src/main/resources/theme-resources/templates/secret-question.ftl b/extension/authenticator/src/main/resources/theme-resources/templates/secret-question.ftl new file mode 100755 index 000000000..b8ca1c901 --- /dev/null +++ b/extension/authenticator/src/main/resources/theme-resources/templates/secret-question.ftl @@ -0,0 +1,35 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "title"> + ${msg("loginTitle",realm.name)} + <#elseif section = "header"> + ${msg("loginTitleHtml",realm.name)} + <#elseif section = "form"> +
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ value="${auth.selectedCredential}"/> + +
+
+
+
+ + \ No newline at end of file