/*
* 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.authentication.authenticators.broker;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
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.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import java.net.URI;
import java.util.Objects;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.core.*;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator {
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
public static final String VERIFY_ACCOUNT_IDP_USERNAME = "VERIFY_ACCOUNT_IDP_USERNAME";
@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
if (realm.getSmtpConfig().isEmpty()) {
ServicesLogger.LOGGER.smtpNotConfigured();
context.attempted();
return;
}
if (Objects.equals(authSession.getAuthNote(VERIFY_ACCOUNT_IDP_USERNAME), brokerContext.getUsername())) {
UserModel existingUser = getExistingUser(session, realm, authSession);
logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
context.setUser(existingUser);
context.success();
return;
}
UserModel existingUser = getExistingUser(session, realm, authSession);
// Do not allow resending e-mail by simple page refresh
if (! Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), existingUser.getEmail())) {
authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, existingUser.getEmail());
sendVerifyEmail(session, context, existingUser, brokerContext);
} else {
showEmailSentPage(context, brokerContext);
}
}
@Override
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
logger.debugf("Re-sending email requested for user, details follow");
// This will allow user to re-send email again
context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
authenticateImpl(context, serializedCtx, brokerContext);
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
}
private void sendVerifyEmail(KeycloakSession session, AuthenticationFlowContext context, UserModel existingUser, BrokeredIdentityContext brokerContext) throws UriBuilderException, IllegalArgumentException {
RealmModel realm = session.getContext().getRealm();
UriInfo uriInfo = session.getContext().getUri();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
int validityInSecs = realm.getActionTokenGeneratedByAdminLifespan();
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
.user(existingUser)
.detail(Details.USERNAME, existingUser.getUsername())
.detail(Details.EMAIL, existingUser.getEmail())
.detail(Details.CODE_ID, authSession.getId())
.removeDetail(Details.AUTH_METHOD)
.removeDetail(Details.AUTH_TYPE);
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
existingUser.getId(), absoluteExpirationInSecs, authSession.getId(),
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
);
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
String link = builder.queryParam("execution", context.getExecution().getId()).build(realm.getName()).toString();
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
try {
context.getSession().getProvider(EmailTemplateProvider.class)
.setRealm(realm)
.setUser(existingUser)
.setAttribute(EmailTemplateProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
.sendConfirmIdentityBrokerLink(link, expirationInMinutes);
event.success();
} catch (EmailException e) {
event.error(Errors.EMAIL_SEND_FAILED);
ServicesLogger.LOGGER.confirmBrokerEmailFailed(e);
Response challenge = context.form()
.setError(Messages.EMAIL_SENT_ERROR)
.createErrorPage();
context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
return;
}
showEmailSentPage(context, brokerContext);
}
protected void showEmailSentPage(AuthenticationFlowContext context, BrokeredIdentityContext brokerContext) {
String accessCode = context.generateAccessCode();
URI action = context.getActionUrl(accessCode);
Response challenge = context.form()
.setStatus(Response.Status.OK)
.setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
.setActionUri(action)
.createIdpLinkEmailPage();
context.forceChallenge(challenge);
}
}