diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java b/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java index 8f81a6f0d8..761decc1d0 100644 --- a/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java @@ -31,6 +31,7 @@ public class FederatedIdentity { private String lastName; private String email; private String token; + private String identityProviderId; public FederatedIdentity(String id) { if (id == null) { @@ -92,4 +93,25 @@ public class FederatedIdentity { public String getToken() { return this.token; } + + public String getIdentityProviderId() { + return this.identityProviderId; + } + + public void setIdentityProviderId(String identityProviderId) { + this.identityProviderId = identityProviderId; + } + + @Override + public String toString() { + return "{" + + "id='" + id + '\'' + + ", username='" + username + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", token='" + token + '\'' + + ", identityProviderId='" + identityProviderId + '\'' + + '}'; + } } diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityBrokerException.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityBrokerException.java new file mode 100644 index 0000000000..858c820af8 --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityBrokerException.java @@ -0,0 +1,32 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * 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.broker.provider; + +/** + * @author pedroigor + */ +public class IdentityBrokerException extends RuntimeException { + + public IdentityBrokerException(String message) { + super(message); + } + + public IdentityBrokerException(String message, Throwable t) { + super(message, t); + } +} diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java index f84765af91..ac1e3dcecf 100644 --- a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java @@ -68,5 +68,12 @@ public interface IdentityProvider extends Provi */ AuthenticationResponse handleResponse(AuthenticationRequest request); + /** + *

Returns a {@link javax.ws.rs.core.Response} containing the token previously stored during the authentication process for a + * specific user.

+ * + * @param identity + * @return + */ Response retrieveToken(FederatedIdentityModel identity); } diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 14bc76753c..34075e9eb3 100644 --- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -25,6 +25,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationResponse; import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.models.FederatedIdentityModel; import javax.ws.rs.core.Response; @@ -68,7 +69,7 @@ public abstract class AbstractOAuth2IdentityProvider assertions = responseType.getAssertions(); if (assertions.isEmpty()) { - throw new RuntimeException("No assertion from response."); + throw new IdentityBrokerException("No assertion from response."); } RTChoiceType rtChoiceType = assertions.get(0); @@ -234,7 +235,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider formData) { - if (formData.containsKey("cancel")) { - return Flows.errors().error("Permission not approved.", Response.Status.FORBIDDEN); - } - - return getToken(realmName, providerId, true); - } - - private Response handleResponse(String realmName, String providerId) { - RealmManager realmManager = new RealmManager(session); - RealmModel realm = realmManager.getRealmByName(realmName); - - IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(realm, providerId); - - try { - IdentityProvider identityProvider = getIdentityProvider(realm, providerId); - - if (identityProvider == null) { - return Flows.forms(session, realm, null, uriInfo).setError("Social identityProvider not found").createErrorPage(); - } - - String relayState = identityProvider.getRelayState(createAuthenticationRequest(providerId, null, realm, null)); - - if (relayState == null) { - return redirectToErrorPage(realm, "No relay state from identity identityProvider."); - } - - ClientSessionCode clientCode = isValidAuthorizationCode(relayState, realm); - - if (clientCode == null) { - return redirectToErrorPage(realm, "Invalid authorization code, please login again through your application."); - } - - ClientSessionModel clientSession = clientCode.getClientSession(); - ClientModel clientModel = clientSession.getClient(); - Response response = checkClientPermissions(clientModel, providerId); - - if (response != null) { - return response; - } - - AuthenticationResponse authenticationResponse = identityProvider.handleResponse(createAuthenticationRequest(providerId, null, realm, clientSession)); - - response = authenticationResponse.getResponse(); - - if (response != null) { - return response; - } - - FederatedIdentity identity = authenticationResponse.getUser(); - - if (!identityProviderConfig.isStoreToken()) { - identity.setToken(null); - } - - return performLocalAuthentication(realm, providerId, identity, clientCode); - } catch (Exception e) { - logger.error("Could not authenticate user against provider " + providerId, e); - if (session.getTransaction().isActive()) { - session.getTransaction().rollback(); - } - - return Flows.forms(session, realm, null, uriInfo).setError("Authentication failed. Could not authenticate against Identity Provider [" + identityProviderConfig.getName() + "].").createErrorPage(); - } finally { - if (session.getTransaction().isActive()) { - session.getTransaction().commit(); - } - } - } - - private Response performLocalAuthentication(RealmModel realm, String providerId, FederatedIdentity updatedIdentity, ClientSessionCode clientCode) { - ClientSessionModel clientSession = clientCode.getClientSession(); - FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, updatedIdentity.getId(), - updatedIdentity.getUsername(), updatedIdentity.getToken()); - UserModel federatedUser = session.users().getUserByFederatedIdentity(federatedIdentityModel, realm); - IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(realm, providerId); - - String authMethod = federatedIdentityModel.getUserId() + "@" + identityProviderConfig.getId(); - EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder() - .event(EventType.LOGIN) - .client(clientSession.getClient()) - .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) - .detail(Details.AUTH_METHOD, authMethod); - - event.detail(Details.USERNAME, authMethod); - - // Check if federatedUser is already authenticated (this means linking social into existing federatedUser account) - if (clientSession.getUserSession() != null) { - UserModel authenticatedUser = clientSession.getUserSession().getUser(); - - if (federatedUser != null) { - String message = "The updatedIdentity returned by the Identity Provider [" + identityProviderConfig.getName() + "] is already linked to other user"; - event.error(message); - return redirectToErrorPage(realm, message); - } - - if (!authenticatedUser.isEnabled()) { - event.error(Errors.USER_DISABLED); - return redirectToErrorPage(realm, "User is disabled"); - } - - if (!authenticatedUser.hasRole(realm.getApplicationByName(ACCOUNT_MANAGEMENT_APP).getRole(MANAGE_ACCOUNT))) { - event.error(Errors.NOT_ALLOWED); - return redirectToErrorPage(realm, "Insufficient permissions to link updatedIdentity"); - } - - session.users().addFederatedIdentity(realm, authenticatedUser, federatedIdentityModel); - - event.success(); - - return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); - } - - if (federatedUser == null) { - - String errorMessage = null; - - // Check if no user already exists with this username or email - UserModel existingUser = session.users().getUserByEmail(updatedIdentity.getEmail(), realm); - if (existingUser != null) { - event.error(Errors.FEDERATED_IDENTITY_EMAIL_EXISTS); - errorMessage = "federatedIdentityEmailExists"; - } else { - existingUser = session.users().getUserByUsername(updatedIdentity.getUsername(), realm); - if (existingUser != null) { - event.error(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS); - errorMessage = "federatedIdentityUsernameExists"; - } - } - - // Check if realm registration is allowed - if (!realm.isRegistrationAllowed()) { - event.error(Errors.FEDERATED_IDENTITY_DISABLED_REGISTRATION); - errorMessage = "federatedIdentityDisabledRegistration"; - } - - if (errorMessage == null) { - logger.debug("Creating user " + updatedIdentity.getUsername() + " and linking to federation provider " + providerId); - federatedUser = session.users().addUser(realm, updatedIdentity.getUsername()); - federatedUser.setEnabled(true); - federatedUser.setFirstName(updatedIdentity.getFirstName()); - federatedUser.setLastName(updatedIdentity.getLastName()); - federatedUser.setEmail(updatedIdentity.getEmail()); - - session.users().addFederatedIdentity(realm, federatedUser, federatedIdentityModel); - - event.clone().user(federatedUser).event(EventType.REGISTER) - .detail(Details.REGISTER_METHOD, authMethod) - .detail(Details.EMAIL, federatedUser.getEmail()) - .removeDetail("auth_method") - .success(); - - if (identityProviderConfig.isUpdateProfileFirstLogin()) { - federatedUser.addRequiredAction(UPDATE_PROFILE); - } - } else { - return Flows.forms(session, realm, clientSession.getClient(), uriInfo) - .setClientSessionCode(clientCode.getCode()) - .setError(errorMessage) - .createLogin(); - } - } - - federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, providerId, realm); - - federatedIdentityModel.setToken(updatedIdentity.getToken()); - - this.session.users().updateFederatedIdentity(realm, federatedUser, federatedIdentityModel); - - event.user(federatedUser); - - String username = federatedIdentityModel.getUserId() + "@" + identityProviderConfig.getName(); - - UserSessionModel userSession = session.sessions() - .createUserSession(realm, federatedUser, username, clientConnection.getRemoteAddr(), "broker", false); - - event.session(userSession); - - TokenManager.attachClientSession(userSession, clientSession); - - AuthenticationManager authManager = new AuthenticationManager(); - - return authManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, - uriInfo, event); - } - - private ClientSessionCode isValidAuthorizationCode(String code, RealmModel realm) { - ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, realm); - - if (clientCode != null && clientCode.isValid(AUTHENTICATE)) { - return clientCode; - } - - return null; - } - - private AuthenticationRequest createAuthenticationRequest(String providerId, String code, RealmModel realm, ClientSessionModel clientSession) { - return new AuthenticationRequest(this.session, realm, clientSession, this.request, this.uriInfo, code, getRedirectUri(providerId, realm)); - } - - private String getRedirectUri(String providerId, RealmModel realm) { - return UriBuilder.fromUri(this.uriInfo.getBaseUri()) - .path(AuthenticationBrokerResource.class) - .path(AuthenticationBrokerResource.class, "handleResponseGet") - .build(realm.getName(), providerId) - .toString(); - } - - private Response redirectToErrorPage(RealmModel realm, String message) { - return Flows.forwardToSecurityFailurePage(this.session, realm, uriInfo, message); - } - - private IdentityProvider getIdentityProvider(RealmModel realm, String providerId) { - IdentityProviderModel identityProviderModel = realm.getIdentityProviderById(providerId); - - if (identityProviderModel != null) { - IdentityProviderFactory providerFactory = getIdentityProviderFactory(identityProviderModel); - - if (providerFactory == null) { - throw new RuntimeException("Could not find provider factory for identity provider [" + providerId + "]."); - } - - return providerFactory.create(identityProviderModel); - } - - return null; - } - - private IdentityProviderFactory getIdentityProviderFactory(IdentityProviderModel model) { - Map availableProviders = new HashMap(); - List allProviders = new ArrayList(); - - allProviders.addAll(this.session.getKeycloakSessionFactory().getProviderFactories(IdentityProvider.class)); - allProviders.addAll(this.session.getKeycloakSessionFactory().getProviderFactories(SocialIdentityProvider.class)); - - for (ProviderFactory providerFactory : allProviders) { - availableProviders.put(providerFactory.getId(), (IdentityProviderFactory) providerFactory); - } - - return availableProviders.get(model.getProviderId()); - } - - private IdentityProviderModel getIdentityProviderConfig(RealmModel realm, String providerId) { - for (IdentityProviderModel model : realm.getIdentityProviders()) { - if (model.getId().equals(providerId)) { - return model; - } - } - - return null; - } - - private Response checkClientPermissions(ClientModel clientModel, String providerId) { - if (clientModel == null) { - return Flows.errors().error("Invalid client.", Response.Status.FORBIDDEN); - } - - if (!clientModel.hasIdentityProvider(providerId)) { - return Flows.errors().error("Client [" + clientModel.getClientId() + "] not authorized.", Response.Status.FORBIDDEN); - } - - return null; - } - - private Response corsResponse(Response response, ClientModel clientModel) { - return Cors.add(request, Response.fromResponse(response)).auth().allowedOrigins(clientModel).build(); - } -} diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java new file mode 100644 index 0000000000..1bbb5811d5 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -0,0 +1,605 @@ +/* + * JBoss, Home of Professional Open Source + * + * Copyright 2013 Red Hat, Inc. and/or its affiliates. + * + * 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.services.resources; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.ClientConnection; +import org.keycloak.broker.provider.AuthenticationRequest; +import org.keycloak.broker.provider.AuthenticationResponse; +import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.broker.provider.IdentityBrokerException; +import org.keycloak.broker.provider.IdentityProvider; +import org.keycloak.broker.provider.IdentityProviderFactory; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.OAuthClientModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationManager.AuthResult; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.managers.EventsManager; +import org.keycloak.services.resources.flows.Flows; +import org.keycloak.services.resources.flows.Urls; +import org.keycloak.social.SocialIdentityProvider; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.OPTIONS; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT; +import static org.keycloak.models.ClientSessionModel.Action.AUTHENTICATE; +import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_APP; +import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PROFILE; + +/** + *

+ * + * @author Pedro Igor + */ +@Path("/broker") +public class IdentityBrokerService { + + private static final Logger LOGGER = Logger.getLogger(IdentityBrokerService.class); + + private final RealmModel realmModel; + + @Context + private UriInfo uriInfo; + + @Context + private KeycloakSession session; + + @Context + private ClientConnection clientConnection; + + @Context + private HttpRequest request; + private EventBuilder event; + + public IdentityBrokerService(RealmModel realmModel) { + if (realmModel == null) { + throw new IllegalArgumentException("Realm can not be null."); + } + + this.realmModel = realmModel; + } + + public void init() { + this.event = new EventsManager(this.realmModel, this.session, this.clientConnection).createEventBuilder().event(EventType.IDENTITY_PROVIDER_LOGIN); + } + + @GET + @Path("/{provider_id}/login") + public Response performLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code) { + this.event.detail(Details.IDENTITY_PROVIDER, providerId); + + if (isDebugEnabled()) { + LOGGER.debugf("Sending authentication request to identity provider [%s].", providerId); + } + + try { + ClientSessionCode clientSessionCode = parseClientSessionCode(code, providerId); + IdentityProvider identityProvider = getIdentityProvider(providerId); + AuthenticationResponse authenticationResponse = identityProvider.handleRequest(createAuthenticationRequest(providerId, clientSessionCode)); + + Response response = authenticationResponse.getResponse(); + + if (response != null) { + this.event.success(); + if (isDebugEnabled()) { + LOGGER.debugf("Identity provider [%s] is going to send a request [%s].", identityProvider, response); + } + return response; + } + } catch (IdentityBrokerException e) { + return redirectToErrorPage("Could not send authentication request to identity provider [" + providerId + "].", e); + } catch (Exception e) { + return redirectToErrorPage("Unexpected error when handling authentication request to identity provider [" + providerId + "].", e); + } + + return redirectToErrorPage("Could not proceed with authentication request to identity provider."); + } + + @GET + @Path("{provider_id}") + public Response handleResponseGet(@PathParam("provider_id") String providerId) { + return handleResponse(providerId); + } + + @POST + @Path("{provider_id}") + public Response handleResponsePost(@PathParam("provider_id") String providerId) { + return handleResponse(providerId); + } + + @Path("{provider_id}/token") + @OPTIONS + public Response retrieveTokenPreflight() { + return Cors.add(this.request, Response.ok()).auth().preflight().build(); + } + + @GET + @Path("{provider_id}/token") + public Response retrieveToken(@PathParam("provider_id") String providerId) { + return getToken(providerId, false); + } + + private Response getToken(String providerId, boolean forceRetrieval) { + this.event.event(EventType.IDENTITY_PROVIDER_RETRIEVE_TOKEN); + + try { + AppAuthManager authManager = new AppAuthManager(); + AuthResult authResult = authManager.authenticateBearerToken(this.session, this.realmModel, this.uriInfo, this.clientConnection, this.request.getHttpHeaders()); + + if (authResult != null) { + String audience = authResult.getToken().getAudience(); + ClientModel clientModel = this.realmModel.findClient(audience); + + if (clientModel == null) { + return badRequest("Invalid client."); + } + + if (!clientModel.hasIdentityProvider(providerId)) { + return corsResponse(badRequest("Client [" + audience + "] not authorized."), clientModel); + } + + if (OAuthClientModel.class.isInstance(clientModel) && !forceRetrieval) { + return corsResponse(Flows.forms(this.session, this.realmModel, clientModel, this.uriInfo) + .setClientSessionCode(authManager.extractAuthorizationHeaderToken(this.request.getHttpHeaders())) + .setAccessRequest("Your information from " + providerId + " identity provider.") + .setClient(clientModel) + .setUriInfo(this.uriInfo) + .setActionUri(this.uriInfo.getRequestUri()) + .createOAuthGrant(), clientModel); + } + + IdentityProvider identityProvider = getIdentityProvider(providerId); + IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(providerId); + + if (identityProviderConfig.isStoreToken()) { + FederatedIdentityModel identity = this.session.users().getFederatedIdentity(authResult.getUser(), providerId, this.realmModel); + + if (identity == null) { + return corsResponse(badRequest("User [" + authResult.getUser().getId() + "] is not associated with identity provider [" + providerId + "]."), clientModel); + } + + this.event.success(); + + return corsResponse(identityProvider.retrieveToken(identity), clientModel); + } + + return corsResponse(badRequest("Identity Provider [" + providerId + "] does not support this operation."), clientModel); + } + + return badRequest("Invalid token."); + } catch (IdentityBrokerException e) { + return redirectToErrorPage("Could not obtain token fron identity provider [" + providerId + "].", e); + } catch (Exception e) { + return redirectToErrorPage("Unexpected error when retrieving token from identity provider [" + providerId + "].", e); + } + } + + @POST + @Path("{provider_id}/token") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response consentTokenRetrieval(@PathParam("provider_id") String providerId, + MultivaluedMap formData) { + if (formData.containsKey("cancel")) { + return redirectToErrorPage("Permission not approved."); + } + + return getToken(providerId, true); + } + + private Response handleResponse(String providerId) { + if (isDebugEnabled()) { + LOGGER.debugf("Handling authentication response from identity provider [%s].", providerId); + } + this.event.event(EventType.IDENTITY_PROVIDER_RESPONSE); + this.event.detail(Details.IDENTITY_PROVIDER, providerId); + IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(providerId); + + try { + IdentityProvider identityProvider = getIdentityProvider(providerId); + String relayState = identityProvider.getRelayState(createAuthenticationRequest(providerId, null)); + + if (relayState == null) { + return redirectToErrorPage("No relay state in response from identity identity [" + providerId + "."); + } + + if (isDebugEnabled()) { + LOGGER.debugf("Relay state is valid: [%s].", relayState); + } + + ClientSessionCode clientSessionCode = parseClientSessionCode(relayState, providerId); + AuthenticationResponse authenticationResponse = identityProvider.handleResponse(createAuthenticationRequest(providerId, clientSessionCode)); + Response response = authenticationResponse.getResponse(); + + if (response != null) { + if (isDebugEnabled()) { + LOGGER.debugf("Identity provider [%s] is going to send a response [%s].", identityProvider, response); + } + return response; + } + + FederatedIdentity identity = authenticationResponse.getUser(); + + if (isDebugEnabled()) { + LOGGER.debugf("Identity provider [%s] returned with identity [%s].", providerId, identity); + } + + if (!identityProviderConfig.isStoreToken()) { + if (isDebugEnabled()) { + LOGGER.debugf("Token will not be stored for identity provider [%s].", providerId); + } + identity.setToken(null); + } + + identity.setIdentityProviderId(providerId); + + return performLocalAuthentication(identity, clientSessionCode); + } catch (IdentityBrokerException e) { + rollback(); + return redirectToErrorPage("Authentication failed. Could not authenticate with identity provider [" + providerId + "].", e); + } catch (Exception e) { + rollback(); + return redirectToErrorPage("Unexpected error when handling response from identity provider [" + providerId + "].", e); + } finally { + if (this.session.getTransaction().isActive()) { + this.session.getTransaction().commit(); + } + } + } + + private Response performLocalAuthentication(FederatedIdentity updatedIdentity, ClientSessionCode clientCode) { + ClientSessionModel clientSession = clientCode.getClientSession(); + IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(updatedIdentity.getIdentityProviderId()); + String providerId = identityProviderConfig.getId(); + FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(providerId, updatedIdentity.getId(), + updatedIdentity.getUsername(), updatedIdentity.getToken()); + + this.event.event(EventType.IDENTITY_PROVIDER_LOGIN) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) + .detail(Details.IDENTITY_PROVIDER_IDENTITY, updatedIdentity.getUsername()); + + UserModel federatedUser = this.session.users().getUserByFederatedIdentity(federatedIdentityModel, this.realmModel); + + // Check if federatedUser is already authenticated (this means linking social into existing federatedUser account) + if (clientSession.getUserSession() != null) { + return performAccountLinking(clientSession, providerId, federatedIdentityModel, federatedUser); + } + + if (federatedUser == null) { + try { + federatedUser = createUser(updatedIdentity); + + if (identityProviderConfig.isUpdateProfileFirstLogin()) { + if (isDebugEnabled()) { + LOGGER.debugf("Identity provider requires update profile action.", federatedUser); + } + federatedUser.addRequiredAction(UPDATE_PROFILE); + } + } catch (Exception e) { + return redirectToLoginPage(e.getMessage(), clientCode); + } + } + + updateFederatedIdentity(updatedIdentity, federatedUser); + + UserSessionModel userSession = this.session.sessions() + .createUserSession(this.realmModel, federatedUser, federatedUser.getUsername(), this.clientConnection.getRemoteAddr(), "broker", false); + + this.event.user(federatedUser); + this.event.session(userSession); + + TokenManager.attachClientSession(userSession, clientSession); + + if (isDebugEnabled()) { + LOGGER.debugf("Performing local authentication for user [%s].", federatedUser); + } + + return AuthenticationManager.nextActionAfterAuthentication(this.session, userSession, clientSession, this.clientConnection, this.request, + this.uriInfo, event); + } + + private Response performAccountLinking(ClientSessionModel clientSession, String providerId, FederatedIdentityModel federatedIdentityModel, UserModel federatedUser) { + this.event.event(EventType.IDENTITY_PROVIDER_ACCCOUNT_LINKING); + + if (federatedUser != null) { + return redirectToErrorPage("The identity returned by the identity provider [" + providerId + "] is already linked to other user."); + } + + UserModel authenticatedUser = clientSession.getUserSession().getUser(); + + if (isDebugEnabled()) { + LOGGER.debugf("Linking account [%s] from identity provider [%s] to user [%s].", federatedIdentityModel, providerId, authenticatedUser); + } + + if (!authenticatedUser.isEnabled()) { + fireErrorEvent(Errors.USER_DISABLED); + return redirectToErrorPage("User is disabled."); + } + + if (!authenticatedUser.hasRole(this.realmModel.getApplicationByName(ACCOUNT_MANAGEMENT_APP).getRole(MANAGE_ACCOUNT))) { + fireErrorEvent(Errors.NOT_ALLOWED); + return redirectToErrorPage("Insufficient permissions to link identities."); + } + + this.session.users().addFederatedIdentity(this.realmModel, authenticatedUser, federatedIdentityModel); + + this.event.success(); + + return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); + } + + private void updateFederatedIdentity(FederatedIdentity updatedIdentity, UserModel federatedUser) { + FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, updatedIdentity.getIdentityProviderId(), this.realmModel); + + federatedIdentityModel.setToken(updatedIdentity.getToken()); + + this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel); + + if (isDebugEnabled()) { + LOGGER.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, updatedIdentity.getIdentityProviderId()); + } + } + + private ClientSessionCode parseClientSessionCode(String code, String providerId) { + ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel); + + if (clientCode != null && clientCode.isValid(AUTHENTICATE)) { + validateClientPermissions(clientCode, providerId); + ClientSessionModel clientSession = clientCode.getClientSession(); + + if (clientSession != null) { + ClientModel client = clientSession.getClient(); + + if (client != null) { + LOGGER.debugf("Got authorization code from client [%s].", client.getClientId()); + this.event.client(client); + } + + if (clientSession.getUserSession() != null) { + this.event.session(clientSession.getUserSession()); + } + } + + if (isDebugEnabled()) { + LOGGER.debugf("Authorization code is valid."); + } + + return clientCode; + } + + throw new IdentityBrokerException("Invalid code, please login again through your application."); + } + + private AuthenticationRequest createAuthenticationRequest(String providerId, ClientSessionCode clientSessionCode) { + ClientSessionModel clientSession = null; + String relayState = null; + + if (clientSessionCode != null) { + clientSession = clientSessionCode.getClientSession(); + relayState = clientSessionCode.getCode(); + } + + return new AuthenticationRequest(this.session, this.realmModel, clientSession, this.request, this.uriInfo, relayState, getRedirectUri(providerId)); + } + + private String getRedirectUri(String providerId) { + return Urls.identityProviderAuthnResponse(this.uriInfo.getBaseUri(), providerId, this.realmModel.getName()).toString(); + } + + private Response redirectToErrorPage(String message) { + return redirectToErrorPage(message, null); + } + + private Response redirectToErrorPage(String message, Throwable throwable) { + fireErrorEvent(message, throwable); + return Flows.forwardToSecurityFailurePage(this.session, this.realmModel, this.uriInfo, message); + } + + private Response badRequest(String message) { + fireErrorEvent(message); + return Flows.errors().error(message, Status.BAD_REQUEST); + } + + private Response redirectToLoginPage(String message, ClientSessionCode clientCode) { + fireErrorEvent(message); + return Flows.forms(this.session, this.realmModel, clientCode.getClientSession().getClient(), this.uriInfo) + .setClientSessionCode(clientCode.getCode()) + .setError(message) + .createLogin(); + } + + private IdentityProvider getIdentityProvider(String providerId) { + IdentityProviderModel identityProviderModel = this.realmModel.getIdentityProviderById(providerId); + + if (identityProviderModel != null) { + IdentityProviderFactory providerFactory = getIdentityProviderFactory(identityProviderModel); + + if (providerFactory == null) { + throw new IdentityBrokerException("Could not find factory for identity provider [" + providerId + "]."); + } + + return providerFactory.create(identityProviderModel); + } + + throw new IdentityBrokerException("Identity Provider [" + providerId + "] not found."); + } + + private IdentityProviderFactory getIdentityProviderFactory(IdentityProviderModel model) { + Map availableProviders = new HashMap(); + List allProviders = new ArrayList(); + + allProviders.addAll(this.session.getKeycloakSessionFactory().getProviderFactories(IdentityProvider.class)); + allProviders.addAll(this.session.getKeycloakSessionFactory().getProviderFactories(SocialIdentityProvider.class)); + + for (ProviderFactory providerFactory : allProviders) { + availableProviders.put(providerFactory.getId(), (IdentityProviderFactory) providerFactory); + } + + return availableProviders.get(model.getProviderId()); + } + + private IdentityProviderModel getIdentityProviderConfig(String providerId) { + for (IdentityProviderModel model : this.realmModel.getIdentityProviders()) { + if (model.getId().equals(providerId)) { + return model; + } + } + + throw new IdentityBrokerException("Configuration for identity provider [" + providerId + "] not found."); + } + + private void validateClientPermissions(ClientSessionCode clientSessionCode, String providerId) { + ClientSessionModel clientSession = clientSessionCode.getClientSession(); + ClientModel clientModel = clientSession.getClient(); + + if (clientModel == null) { + throw new IdentityBrokerException("Invalid client."); + } + + if (!clientModel.hasIdentityProvider(providerId)) { + throw new IdentityBrokerException("Client [" + clientModel.getClientId() + "] not authorized to authenticate with identity provider [" + providerId + "]."); + } + } + + private UserModel createUser(FederatedIdentity updatedIdentity) { + FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(updatedIdentity.getIdentityProviderId(), updatedIdentity.getId(), + updatedIdentity.getUsername(), updatedIdentity.getToken()); + // Check if no user already exists with this username or email + UserModel existingUser = this.session.users().getUserByEmail(updatedIdentity.getEmail(), this.realmModel); + + if (existingUser != null) { + fireErrorEvent(Errors.FEDERATED_IDENTITY_EMAIL_EXISTS); + throw new IdentityBrokerException("federatedIdentityEmailExists"); + } + + existingUser = this.session.users().getUserByUsername(updatedIdentity.getUsername(), this.realmModel); + + if (existingUser != null) { + fireErrorEvent(Errors.FEDERATED_IDENTITY_USERNAME_EXISTS); + throw new IdentityBrokerException("federatedIdentityUsernameExists"); + } + + // Check if realm registration is allowed + if (!this.realmModel.isRegistrationAllowed()) { + fireErrorEvent(Errors.FEDERATED_IDENTITY_DISABLED_REGISTRATION); + throw new IdentityBrokerException("federatedIdentityDisabledRegistration"); + } + + if (isDebugEnabled()) { + LOGGER.debugf("Creating account from identity [%s].", federatedIdentityModel); + } + + UserModel federatedUser = this.session.users().addUser(this.realmModel, updatedIdentity.getUsername()); + + if (isDebugEnabled()) { + LOGGER.debugf("Account [%s] created.", federatedUser); + } + + federatedUser.setEnabled(true); + federatedUser.setFirstName(updatedIdentity.getFirstName()); + federatedUser.setLastName(updatedIdentity.getLastName()); + federatedUser.setEmail(updatedIdentity.getEmail()); + + this.session.users().addFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel); + + this.event.clone().user(federatedUser).event(EventType.REGISTER) + .detail(Details.IDENTITY_PROVIDER, federatedIdentityModel.getIdentityProvider()) + .detail(Details.IDENTITY_PROVIDER_IDENTITY, updatedIdentity.getUsername()) + .removeDetail("auth_method") + .success(); + + return federatedUser; + } + + private Response corsResponse(Response response, ClientModel clientModel) { + return Cors.add(this.request, Response.fromResponse(response)).auth().allowedOrigins(clientModel).build(); + } + + private void fireErrorEvent(String message, Throwable throwable) { + if (!this.event.getEvent().getType().toString().endsWith("_ERROR")) { + boolean newTransaction = !this.session.getTransaction().isActive(); + + try { + if (newTransaction) { + this.session.getTransaction().begin(); + } + + this.event.error(message); + + if (newTransaction) { + this.session.getTransaction().commit(); + } + } catch (Exception e) { + LOGGER.error("Could not fire event.", e); + rollback(); + } + } + + if (throwable != null) { + LOGGER.error(message, throwable); + } else { + LOGGER.error(message); + } + } + + private void fireErrorEvent(String message) { + fireErrorEvent(message, null); + } + + private boolean isDebugEnabled() { + return LOGGER.isDebugEnabled(); + } + + private void rollback() { + if (this.session.getTransaction().isActive()) { + this.session.getTransaction().rollback(); + } + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index f3044ea26d..11b59c806b 100755 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -72,7 +72,6 @@ public class KeycloakApplication extends Application { singletons.add(new ServerVersionResource()); singletons.add(new RealmsResource()); - singletons.add(new AuthenticationBrokerResource()); singletons.add(new AdminRoot()); classes.add(SkeletonKeyContextResolver.class); classes.add(QRCodeResource.class); diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index a7fe6c4855..de6620c203 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -1,14 +1,11 @@ package org.keycloak.services.resources; import org.jboss.logging.Logger; -import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.ClientConnection; -import org.keycloak.Config; import org.keycloak.events.EventBuilder; import org.keycloak.models.ApplicationModel; -import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -20,22 +17,18 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.EventsManager; import org.keycloak.services.managers.RealmManager; -import org.keycloak.util.StreamUtil; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import javax.ws.rs.core.CacheControl; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; -import java.io.IOException; -import java.io.InputStream; /** * @author Bill Burke @@ -190,7 +183,18 @@ public class RealmsResource { return realmResource; } + @Path("{realm}/broker") + public IdentityBrokerService getBrokerService(final @PathParam("realm") String name) { + RealmManager realmManager = new RealmManager(session); + RealmModel realm = locateRealm(name, realmManager); + IdentityBrokerService brokerService = new IdentityBrokerService(realm); + ResteasyProviderFactory.getInstance().injectProperties(brokerService); + + brokerService.init(); + + return brokerService; + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java index 42c3497410..a8ea3a4c15 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProvidersResource.java @@ -106,7 +106,12 @@ public class IdentityProvidersResource { String providerId = formDataMap.get("providerId").get(0).getBodyAsString(); String enabled = formDataMap.get("enabled").get(0).getBodyAsString(); String updateProfileFirstLogin = formDataMap.get("updateProfileFirstLogin").get(0).getBodyAsString(); - String storeToken = formDataMap.get("storeToken").get(0).getBodyAsString(); + String storeToken = "false"; + + if (formDataMap.containsKey("storeToken")) { + storeToken = formDataMap.get("storeToken").get(0).getBodyAsString(); + } + InputPart file = formDataMap.get("file").get(0); InputStream inputStream = file.getBody(InputStream.class, null); IdentityProviderFactory providerFactory = getProviderFactorytById(providerId); diff --git a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java index 5dc22d2680..3d7b19c069 100755 --- a/services/src/main/java/org/keycloak/services/resources/flows/Urls.java +++ b/services/src/main/java/org/keycloak/services/resources/flows/Urls.java @@ -22,12 +22,10 @@ package org.keycloak.services.resources.flows; import org.keycloak.OAuth2Constants; -import org.keycloak.models.IdentityProviderModel; -import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.OpenIDConnect; import org.keycloak.protocol.oidc.OpenIDConnectService; import org.keycloak.services.resources.AccountService; -import org.keycloak.services.resources.AuthenticationBrokerResource; +import org.keycloak.services.resources.IdentityBrokerService; import org.keycloak.services.resources.LoginActionsService; import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.ThemeResource; @@ -68,21 +66,31 @@ public class Urls { return accountBase(baseUri).path(AccountService.class, "processFederatedIdentityUpdate").build(realmName); } - public static URI identityProviderAuthnRequest(URI baseURI, IdentityProviderModel identityProvider, RealmModel realm, String accessCode) { - UriBuilder uriBuilder = UriBuilder.fromUri(baseURI) - .path(AuthenticationBrokerResource.class) - .path(AuthenticationBrokerResource.class, "performLogin") - .replaceQueryParam("provider_id", identityProvider.getId()); + public static URI identityProviderAuthnResponse(URI baseUri, String providerId, String realmName) { + return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") + .path(IdentityBrokerService.class, "handleResponseGet") + .build(realmName, providerId); + } + + public static URI identityProviderAuthnRequest(URI baseUri, String providerId, String realmName, String accessCode) { + UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService") + .path(IdentityBrokerService.class, "performLogin"); if (accessCode != null) { uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); } - return uriBuilder.build(realm.getName()); + return uriBuilder.build(realmName, providerId); } - public static URI identityProviderAuthnRequest(URI baseURI, IdentityProviderModel identityProvider, RealmModel realm) { - return identityProviderAuthnRequest(baseURI, identityProvider, realm, null); + public static URI identityProviderRetrieveToken(URI baseUri, String providerId, String realmName) { + return realmBase(baseUri).path(RealmsResource.class, "getBrokerService") + .path(IdentityBrokerService.class, "retrieveToken") + .build(realmName, providerId); + } + + public static URI identityProviderAuthnRequest(URI baseURI, String providerId, String realmName) { + return identityProviderAuthnRequest(baseURI, providerId, realmName, null); } public static URI accountTotpPage(URI baseUri, String realmId) { diff --git a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java index 5266c68a4e..8ebea5096b 100755 --- a/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java +++ b/social/facebook/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java @@ -5,6 +5,7 @@ import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.util.SimpleHttp; import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.social.SocialIdentityProvider; /** @@ -61,7 +62,7 @@ public class FacebookIdentityProvider extends AbstractOAuth2IdentityProvider imp return user; } catch (Exception e) { - throw new RuntimeException(e); + throw new IdentityBrokerException("Could not obtain user profile from facebook.", e); } } diff --git a/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java index 7be16dcfbf..df6a4d031d 100755 --- a/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java +++ b/social/github/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java @@ -5,6 +5,7 @@ import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.util.SimpleHttp; import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.social.SocialIdentityProvider; /** @@ -37,7 +38,7 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple return user; } catch (Exception e) { - throw new RuntimeException(e); + throw new IdentityBrokerException("Could not obtain user profile from github.", e); } } diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index d9ee567e29..c7d34922d2 100755 --- a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -26,6 +26,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider; import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationResponse; import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.social.SocialIdentityProvider; @@ -68,7 +69,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider queryParameters = request.getUriInfo().getQueryParameters(); if (queryParameters.getFirst("denied") != null) { - throw new RuntimeException("Access denied."); + throw new IdentityBrokerException("Access denied."); } try { @@ -121,7 +122,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider