![JAR search and dependency download from the Maven repository](/logo.png)
io.phasetwo.keycloak.magic.MagicLink Maven / Gradle / Ivy
package io.phasetwo.keycloak.magic;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import io.phasetwo.keycloak.magic.auth.token.MagicLinkActionToken;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakSessionTask;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.services.Urls;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.sessions.AuthenticationSessionModel;
/** common utilities for Magic Link authentication, used by the authenticator and resource */
@JBossLog
public class MagicLink {
public static Consumer registerEvent(final EventBuilder event) {
return new Consumer() {
@Override
public void accept(UserModel user) {
event
.event(EventType.REGISTER)
.detail(Details.REGISTER_METHOD, "magic")
.detail(Details.USERNAME, user.getUsername())
.detail(Details.EMAIL, user.getEmail())
.user(user)
.success();
}
};
}
public static UserModel getOrCreate(
KeycloakSession session,
RealmModel realm,
String email,
boolean forceCreate,
boolean updateProfile,
boolean updatePassword) {
return getOrCreate(session, realm, email, forceCreate, updateProfile, updatePassword, null);
}
public static UserModel getOrCreate(
KeycloakSession session,
RealmModel realm,
String email,
boolean forceCreate,
boolean updateProfile,
boolean updatePassword,
Consumer onNew) {
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(session, realm, email);
if (user == null && forceCreate) {
user = session.users().addUser(realm, email);
user.setEnabled(true);
user.setEmail(email);
if (updatePassword) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
}
if (updateProfile) {
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
}
if (onNew != null) {
onNew.accept(user);
}
}
return user;
}
public static MagicLinkActionToken createActionToken(
UserModel user,
String clientId,
OptionalInt validity,
Boolean rememberMe,
AuthenticationSessionModel authSession) {
String redirectUri = authSession.getRedirectUri();
String scope = authSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
String nonce = authSession.getClientNote(OIDCLoginProtocol.NONCE_PARAM);
log.debugf(
"Attempting MagicLinkAuthenticator for %s, %s, %s", user.getEmail(), clientId, redirectUri);
log.debugf("MagicLinkAuthenticator extra vars %s %s %s %b", scope, state, nonce, rememberMe);
return createActionToken(
user, clientId, redirectUri, validity, scope, nonce, state, rememberMe);
}
public static MagicLinkActionToken createActionToken(
UserModel user,
String clientId,
String redirectUri,
OptionalInt validity,
String scope,
String nonce,
String state,
Boolean rememberMe) {
// build the action token
int validityInSecs = validity.orElse(60 * 60 * 24); // 1 day
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
MagicLinkActionToken token =
new MagicLinkActionToken(
user.getId(),
absoluteExpirationInSecs,
clientId,
redirectUri,
scope,
nonce,
state,
rememberMe);
return token;
}
public static MagicLinkActionToken createActionToken(
UserModel user, String clientId, String redirectUri, OptionalInt validity) {
return createActionToken(user, clientId, redirectUri, validity, null, null, null, false);
}
public static String linkFromActionToken(
KeycloakSession session, RealmModel realm, MagicLinkActionToken token) {
UriInfo uriInfo = session.getContext().getUri();
// This is a workaround for situations where the realm you are using to call this (e.g. master)
// is different than the one you are generating the action token for. Because the
// SignatureProvider
// assumes the value that is set in session.getContext().getRealm() has the keys it should use,
// we
// need to temporarily reset it
RealmModel r = session.getContext().getRealm();
log.debugf("realm %s session.context.realm %s", realm.getName(), r.getName());
// Because of the risk, throw an exception for master realm
if (Config.getAdminRealm().equals(realm.getName())) {
throw new IllegalStateException(
String.format("Magic links not allowed for %s realm", Config.getAdminRealm()));
}
session.getContext().setRealm(realm);
UriBuilder builder =
actionTokenBuilder(
uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), token.getIssuedFor());
// and then set it back
session.getContext().setRealm(r);
return builder.build(realm.getName()).toString();
}
public static boolean validateRedirectUri(
KeycloakSession session, String redirectUri, ClientModel client) {
String redirect = RedirectUtils.verifyRedirectUri(session, redirectUri, client);
log.debugf("Redirect after verify %s -> %s", redirectUri, redirect);
return (redirectUri.equals(redirect));
}
private static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId) {
log.debugf("baseUri: %s, tokenString: %s, clientId: %s", baseUri, tokenString, clientId);
return Urls.realmBase(baseUri)
.path(RealmsResource.class, "getLoginActionsService")
.path(LoginActionsService.class, "executeActionToken")
.queryParam(Constants.KEY, tokenString)
.queryParam(Constants.CLIENT_ID, clientId);
}
public static boolean sendMagicLinkEmail(KeycloakSession session, UserModel user, String link) {
RealmModel realm = session.getContext().getRealm();
try {
EmailTemplateProvider emailTemplateProvider =
session.getProvider(EmailTemplateProvider.class);
String realmName = getRealmName(realm);
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy