forked from keycloak/keycloak-quickstarts
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move authenticator example to quickstarts
Signed-off-by: stianst <[email protected]> Add Secret Question authenticator back To do that, implemented `getCredentialType` for `SecretQuestionRequiredAction` Closes keycloak#553
- Loading branch information
1 parent
813687f
commit a123bb1
Showing
16 changed files
with
943 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
authenticator: Example custom authenticator | ||
======================================================== | ||
|
||
Level: Beginner | ||
Summary: Example custom authenticator | ||
Target Product: <span>Keycloak</span> | ||
Source: <https://github.com/keycloak/keycloak-quickstarts> | ||
|
||
|
||
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 <span>Keycloak</span> 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 <span>Keycloak</span> 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
--> | ||
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> | ||
<parent> | ||
<artifactId>keycloak-quickstart-parent</artifactId> | ||
<groupId>org.keycloak.quickstarts</groupId> | ||
<version>999.0.0-SNAPSHOT</version> | ||
<relativePath>../../pom.xml</relativePath> | ||
</parent> | ||
|
||
<name>Keycloak Quickstart: Authenticator Example</name> | ||
<description/> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<artifactId>authenticator-example</artifactId> | ||
<packaging>jar</packaging> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-core</artifactId> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-server-spi</artifactId> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-server-spi-private</artifactId> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.jboss.logging</groupId> | ||
<artifactId>jboss-logging</artifactId> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-services</artifactId> | ||
<version>${version.keycloak}</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<finalName>authenticator-example</finalName> | ||
</build> | ||
</project> |
139 changes: 139 additions & 0 deletions
139
...icator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <a href="mailto:[email protected]">Bill Burke</a> | ||
* @version $Revision: 1 $ | ||
*/ | ||
public class SecretQuestionAuthenticator implements Authenticator, CredentialValidator<SecretQuestionCredentialProvider> { | ||
|
||
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<String, String> 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<RequiredActionFactory> 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); | ||
} | ||
} |
120 changes: 120 additions & 0 deletions
120
...src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 <a href="mailto:[email protected]">Bill Burke</a> | ||
* @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<ProviderConfigProperty> getConfigProperties() { | ||
return configProperties; | ||
} | ||
|
||
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>(); | ||
|
||
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() { | ||
|
||
} | ||
|
||
|
||
} |
Oops, something went wrong.