EmailAuthenticatorForm.java:
Код: Выделить всё
package com.mesutpiskin.keycloak.auth.email;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Errors;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.common.util.SecretGenerator;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@JBossLog
public class EmailAuthenticatorForm extends AbstractUsernameFormAuthenticator {
private final KeycloakSession session;
public EmailAuthenticatorForm(KeycloakSession session) {
this.session = session;
}
@Override
public void authenticate(AuthenticationFlowContext context) {
challenge(context, null);
}
@Override
protected Response challenge(AuthenticationFlowContext context, String error, String field) {
generateAndSendEmailCode(context);
LoginFormsProvider form = context.form().setExecution(context.getExecution().getId());
if (error != null) {
if (field != null) {
form.addError(new FormMessage(field, error));
} else {
form.setError(error);
}
}
Response response = form.createForm("email-code-form.ftl");
context.challenge(response);
return response;
}
private void generateAndSendEmailCode(AuthenticationFlowContext context) {
AuthenticatorConfigModel config = context.getAuthenticatorConfig();
AuthenticationSessionModel session = context.getAuthenticationSession();
if (session.getAuthNote(EmailConstants.CODE) != null) {
// skip sending email code
return;
}
int length = EmailConstants.DEFAULT_LENGTH;
int ttl = EmailConstants.DEFAULT_TTL;
if (config != null) {
// get config values
length = Integer.parseInt(config.getConfig().get(EmailConstants.CODE_LENGTH));
ttl = Integer.parseInt(config.getConfig().get(EmailConstants.CODE_TTL));
}
String code = SecretGenerator.getInstance().randomString(length, SecretGenerator.DIGITS);
sendEmailWithCode(context.getRealm(), context.getUser(), code, ttl);
session.setAuthNote(EmailConstants.CODE, code);
session.setAuthNote(EmailConstants.CODE_TTL, Long.toString(System.currentTimeMillis() + (ttl * 1000L)));
}
@Override
public void action(AuthenticationFlowContext context) {
UserModel userModel = context.getUser();
if (!enabledUser(context, userModel)) {
// error in context is set in enabledUser/isDisabledByBruteForce
return;
}
MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("resend")) {
resetEmailCode(context);
challenge(context, null);
return;
}
if (formData.containsKey("cancel")) {
resetEmailCode(context);
context.resetFlow();
return;
}
AuthenticationSessionModel session = context.getAuthenticationSession();
String code = session.getAuthNote(EmailConstants.CODE);
String ttl = session.getAuthNote(EmailConstants.CODE_TTL);
String enteredCode = formData.getFirst(EmailConstants.CODE);
if (enteredCode.equals(code)) {
if (Long.parseLong(ttl) < System.currentTimeMillis()) {
// expired
context.getEvent().user(userModel).error(Errors.EXPIRED_CODE);
Response challengeResponse = challenge(context, Messages.EXPIRED_ACTION_TOKEN_SESSION_EXISTS, EmailConstants.CODE);
context.failureChallenge(AuthenticationFlowError.EXPIRED_CODE, challengeResponse);
} else {
// valid
resetEmailCode(context);
context.success();
}
} else {
// invalid
AuthenticationExecutionModel execution = context.getExecution();
if (execution.isRequired()) {
context.getEvent().user(userModel).error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = challenge(context, Messages.INVALID_ACCESS_CODE, EmailConstants.CODE);
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
} else if (execution.isConditional() || execution.isAlternative()) {
context.attempted();
}
}
}
@Override
protected String disabledByBruteForceError() {
return Messages.INVALID_ACCESS_CODE;
}
private void resetEmailCode(AuthenticationFlowContext context) {
context.getAuthenticationSession().removeAuthNote(EmailConstants.CODE);
}
@Override
public boolean requiresUser() {
return true;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return user.getEmail() != null;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
// NOOP
}
@Override
public void close() {
// NOOP
}
private void sendEmailWithCode(RealmModel realm, UserModel user, String code, int ttl) {
if (user.getEmail() == null) {
log.warnf("Could not send access code email due to missing email. realm=%s user=%s", realm.getId(), user.getUsername());
throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_USER);
}
Map mailBodyAttributes = new HashMap();
mailBodyAttributes.put("username", user.getUsername());
mailBodyAttributes.put("code", code);
mailBodyAttributes.put("ttl", ttl);
String realmName = realm.getDisplayName() != null ? realm.getDisplayName() : realm.getName();
List subjectParams = List.of(realmName);
try {
EmailTemplateProvider emailProvider = session.getProvider(EmailTemplateProvider.class);
emailProvider.setRealm(realm);
emailProvider.setUser(user);
// Don't forget to add the welcome-email.ftl (html and text) template to your theme.
emailProvider.send("emailCodeSubject", subjectParams, "code-email.ftl", mailBodyAttributes);
} catch (EmailException eex) {
log.errorf(eex, "Failed to send access code email. realm=%s user=%s", realm.getId(), user.getUsername());
}
}
}
Код: Выделить всё
package com.mesutpiskin.keycloak.auth.email;
import com.google.auto.service.AutoService;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
@AutoService(AuthenticatorFactory.class)
public class EmailAuthenticatorFormFactory implements AuthenticatorFactory {
@Override
public String getId() {
return "email-authenticator";
}
@Override
public String getDisplayType() {
return "Email OTP";
}
@Override
public String getReferenceCategory() {
return "otp";
}
@Override
public boolean isConfigurable() {
return true;
}
public static final 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 false;
}
@Override
public String getHelpText() {
return "Email otp authenticator.";
}
@Override
public List
getConfigProperties() {
return List.of(
new ProviderConfigProperty(EmailConstants.CODE_LENGTH, "Code length",
"The number of digits of the generated code.",
ProviderConfigProperty.STRING_TYPE, String.valueOf(EmailConstants.DEFAULT_LENGTH)),
new ProviderConfigProperty(EmailConstants.CODE_TTL, "Time-to-live",
"The time to live in seconds for the code to be valid.", ProviderConfigProperty.STRING_TYPE,
String.valueOf(EmailConstants.DEFAULT_TTL)));
}
@Override
public void close() {
// NOOP
}
@Override
public Authenticator create(KeycloakSession session) {
return new EmailAuthenticatorForm(session);
}
@Override
public void init(Config.Scope config) {
// NOOP
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// NOOP
}
}
Код: Выделить всё
import {UserProfile} from "./user-profile";
import Keycloak from "keycloak-js";
import {Injectable} from "@angular/core";
import {Router} from "@angular/router";
import {UserInfo} from "./user-info";
import {Wallet} from "../models/wallet";
import {WalletControllerService} from "../services/wallet-controller.service";
@Injectable({
providedIn: 'root'
})
export class KeycloakService {
private readonly _keycloak: Keycloak;
private _profile: UserProfile | undefined;
private _userInfo: UserInfo | undefined;
get keycloak() {
return this._keycloak;
}
get profile(): UserProfile | undefined {
return this._profile;
}
get userInfo(): UserInfo | undefined {
return this._userInfo;
}
constructor(private router: Router, private walletService: WalletControllerService) {
this._keycloak = new Keycloak({
url: 'http://localhost:9090',
realm: 'realm-id',
clientId: 'client-id',
});
}
async init(): Promise {
console.log('Authenticating user... ')
const authenticated = await this.keycloak.init({
onLoad: 'login-required',
checkLoginIframe: false,
// redirectUri: 'http://localhost:4200/account'
});
if (authenticated) {
this._profile = (await this.keycloak.loadUserProfile()) as UserProfile;
this._userInfo = (await this.keycloak.loadUserInfo()) as UserInfo;
}
}
async login() {
try {
await this.keycloak.login({
redirectUri: 'http://localhost:4200/account'
});
} catch (error) {
console.error('Login failed:', error);
}
}
fetchUserWallet(): Promise {
return new Promise((resolve, reject) => {
this.walletService.getWalletByUser().subscribe({
next: data => {
resolve(data.data);
},
error: err => {
console.error(err);
reject(err);
}
})
});
}
logout(){
return this.keycloak.logout();
}
}

Помогите, пожалуйста, что-то я делаю не так?
Подробнее здесь: https://stackoverflow.com/questions/786 ... -2fa-email