Merge pull request #970 from pedroigor/master

[KEYCLOAK-883] - Code cleanup and refactoring.
This commit is contained in:
Pedro Igor 2015-02-13 01:45:31 -02:00
commit 2b7c7f63b6
30 changed files with 771 additions and 569 deletions

View file

@ -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 + '\'' +
'}';
}
}

View file

@ -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);
}
}

View file

@ -68,5 +68,12 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
*/
AuthenticationResponse handleResponse(AuthenticationRequest request);
/**
* <p>Returns a {@link javax.ws.rs.core.Response} containing the token previously stored during the authentication process for a
* specific user.</p>
*
* @param identity
* @return
*/
Response retrieveToken(FederatedIdentityModel identity);
}

View file

@ -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<C extends OAuth2IdentityPro
return AuthenticationResponse.temporaryRedirect(authorizationUrl);
} catch (Exception e) {
throw new RuntimeException("Could not create authentication request.", e);
throw new IdentityBrokerException("Could not create authentication request.", e);
}
}
@ -85,9 +86,9 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
if (error != null) {
if (error.equals("access_denied")) {
throw new RuntimeException("Access denied.");
throw new IdentityBrokerException("Access denied.");
} else {
throw new RuntimeException(error);
throw new IdentityBrokerException(error);
}
}
@ -111,9 +112,9 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
return AuthenticationResponse.end(federatedIdentity);
}
throw new RuntimeException("No authorization code from identity provider.");
throw new IdentityBrokerException("No authorization code from identity provider.");
} catch (Exception e) {
throw new RuntimeException("Could not process response from identity provider.", e);
throw new IdentityBrokerException("Could not process response from identity provider.", e);
}
}
@ -132,7 +133,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
try {
return mapper.readTree(response).get(tokenName).getTextValue();
} catch (IOException e) {
throw new RuntimeException("Could not extract token [" + tokenName + "] from response [" + response + "].", e);
throw new IdentityBrokerException("Could not extract token [" + tokenName + "] from response [" + response + "].", e);
}
} else {
Matcher matcher = Pattern.compile(tokenName + "=([^&]+)").matcher(response);
@ -149,7 +150,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
if (accessToken == null) {
throw new RuntimeException("No access token from server.");
throw new IdentityBrokerException("No access token from server.");
}
return doGetFederatedIdentity(accessToken);

View file

@ -21,6 +21,7 @@ import org.codehaus.jackson.JsonNode;
import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.FederatedIdentity;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.jose.jws.JWSInput;
import javax.ws.rs.core.UriBuilder;
@ -62,7 +63,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
if (accessToken == null) {
throw new RuntimeException("No access_token from server.");
throw new IdentityBrokerException("No access_token from server.");
}
String idToken = extractTokenFromResponse(response, OIDC_PARAMETER_ID_TOKEN);
@ -101,13 +102,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return identity;
} catch (Exception e) {
throw new RuntimeException("Could not fetch attributes from userinfo endpoint.", e);
throw new IdentityBrokerException("Could not fetch attributes from userinfo endpoint.", e);
}
}
private void validateIdToken(String idToken) {
if (idToken == null) {
throw new RuntimeException("No id_token from server.");
throw new IdentityBrokerException("No id_token from server.");
}
try {
@ -131,10 +132,10 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
}
throw new RuntimeException("Wrong issuer from id_token..");
throw new IdentityBrokerException("Wrong issuer from id_token..");
}
} catch (IOException e) {
throw new RuntimeException("Could not decode id token.", e);
throw new IdentityBrokerException("Could not decode id token.", e);
}
}

View file

@ -22,6 +22,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 org.keycloak.protocol.saml.SAML2AuthnRequestBuilder;
import org.keycloak.protocol.saml.SAML2NameIDPolicyBuilder;
@ -112,11 +113,11 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
PublicKey publicKey = request.getRealm().getPublicKey();
if (privateKey == null) {
throw new RuntimeException("Identity Provider [" + getConfig().getName() + "] wants a signed authentication request. But the Realm [" + request.getRealm().getName() + "] does not have a private key.");
throw new IdentityBrokerException("Identity Provider [" + getConfig().getName() + "] wants a signed authentication request. But the Realm [" + request.getRealm().getName() + "] does not have a private key.");
}
if (publicKey == null) {
throw new RuntimeException("Identity Provider [" + getConfig().getName() + "] wants a signed authentication request. But the Realm [" + request.getRealm().getName() + "] does not have a public key.");
throw new IdentityBrokerException("Identity Provider [" + getConfig().getName() + "] wants a signed authentication request. But the Realm [" + request.getRealm().getName() + "] does not have a public key.");
}
KeyPair keypair = new KeyPair(publicKey, privateKey);
@ -131,7 +132,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
return AuthenticationResponse.fromResponse(authnRequestBuilder.redirectBinding().request());
}
} catch (Exception e) {
throw new RuntimeException("Could not create authentication request.", e);
throw new IdentityBrokerException("Could not create authentication request.", e);
}
}
@ -145,7 +146,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
String samlResponse = getRequestParameter(request, SAML_RESPONSE_PARAMETER);
if (samlResponse == null) {
throw new RuntimeException("No response from SAML identity provider.");
throw new IdentityBrokerException("No response from SAML identity provider.");
}
try {
@ -167,7 +168,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
return AuthenticationResponse.end(identity);
} catch (Exception e) {
throw new RuntimeException("Could not process response from SAML identity provider.", e);
throw new IdentityBrokerException("Could not process response from SAML identity provider.", e);
}
}
@ -194,7 +195,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
List<RTChoiceType> 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<SAMLIdentityP
detailMessage.append("none");
}
throw new RuntimeException("Authentication failed with code [" + statusCode.getValue() + " and detail [" + detailMessage.toString() + ".");
throw new IdentityBrokerException("Authentication failed with code [" + statusCode.getValue() + " and detail [" + detailMessage.toString() + ".");
}
}
@ -246,7 +247,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get()));
if (enc == null) {
throw new RuntimeException("No encrypted assertion found.");
throw new IdentityBrokerException("No encrypted assertion found.");
}
String oldID = enc.getAttribute(JBossSAMLConstants.ID.get());
@ -265,7 +266,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
return responseType;
} catch (Exception e) {
throw new RuntimeException("Could not decrypt assertion.", e);
throw new IdentityBrokerException("Could not decrypt assertion.", e);
}
}

View file

@ -12,6 +12,8 @@ public interface Details {
String REDIRECT_URI = "redirect_uri";
String RESPONSE_TYPE = "response_type";
String AUTH_METHOD = "auth_method";
String IDENTITY_PROVIDER = "identity_provider";
String IDENTITY_PROVIDER_IDENTITY = "identity_provider_identity";
String REGISTER_METHOD = "register_method";
String USERNAME = "username";
String REMEMBER_ME = "remember_me";

View file

@ -35,7 +35,6 @@ public interface Errors {
String NOT_ALLOWED = "not_allowed";
String IDENTITY_PROVIDER_NOT_FOUND = "identity_provider_not_found";
String FEDERATED_IDENTITY_EMAIL_EXISTS = "federated_identity_email_exists";
String FEDERATED_IDENTITY_USERNAME_EXISTS = "federated_identity_username_exists";
String FEDERATED_IDENTITY_DISABLED_REGISTRATION = "federated_identity_disabled_registration";

View file

@ -48,5 +48,14 @@ public enum EventType {
UNREGISTER_NODE,
USER_INFO_REQUEST,
USER_INFO_REQUEST_ERROR
USER_INFO_REQUEST_ERROR,
IDENTITY_PROVIDER_LOGIN,
IDENTITY_PROVIDER_LOGIN_ERROR,
IDENTITY_PROVIDER_RESPONSE,
IDENTITY_PROVIDER_RESPONSE_ERROR,
IDENTITY_PROVIDER_RETRIEVE_TOKEN,
IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR,
IDENTITY_PROVIDER_ACCCOUNT_LINKING,
IDENTITY_PROVIDER_ACCCOUNT_LINKING_ERROR,
}

View file

@ -83,7 +83,7 @@ module.controller('GlobalCtrl', function($scope, $http, $location, Auth) {
$scope.identity = Auth.getIdentity();
$scope.loadSocialProfile = function() {
$http.get('http://localhost:8081/auth/broker/facebook-identity-provider-realm/facebook/token').success(function(data) {
$http.get('http://localhost:8081/auth/realms/facebook-identity-provider-realm/broker/facebook/token').success(function(data) {
var accessTokenParameter = 'access_token=';
var accessToken = data.substring(data.indexOf(accessTokenParameter) + accessTokenParameter.length, data.indexOf('&'));

View file

@ -83,7 +83,7 @@ module.controller('GlobalCtrl', function($scope, $http, $location, Auth) {
$scope.identity = Auth.getIdentity();
$scope.loadSocialProfile = function() {
$http.get('http://localhost:8081/auth/broker/google-identity-provider-realm/google/token').success(function(data) {
$http.get('http://localhost:8081/auth/realms/google-identity-provider-realm/broker/google/token').success(function(data) {
var accessToken = data.access_token;
var req = {

View file

@ -31,7 +31,7 @@
"name": "http://localhost:8080/auth/",
"enabled": true,
"redirectUris": [
"http://localhost:8080/auth/broker/saml-broker-authentication-realm/saml-identity-provider"
"http://localhost:8080/auth/realms/saml-broker-authentication-realm/broker/saml-identity-provider"
],
"attributes": {
"saml.assertion.signature": "true",

View file

@ -114,7 +114,7 @@ public class TwitterShowUserServlet extends HttpServlet {
}
private String getIdentityProviderTokenUrl() {
return this.authServer + "/broker/" + this.realmName + "/" + this.identityProvider.getId() + "/token";
return this.authServer + "/realms/" + this.realmName + "/broker/" + this.identityProvider.getId() + "/token";
}
private void initKeyCloakClient(ServletConfig config) {

View file

@ -719,7 +719,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
}
}, true);
$scope.callbackUrl = $location.absUrl().replace(/\/admin.*/, "/broker/") + realm.realm + "/" ;
$scope.callbackUrl = $location.absUrl().replace(/\/admin.*/, "/realms/") + realm.realm + "/broker/" ;
$scope.addProvider = function(provider) {
$location.url("/create/identity-provider/" + realm.realm + "/" + provider.id);

View file

@ -61,7 +61,7 @@ public class IdentityProviderBean {
}
}
String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider, realm).toString();
String loginUrl = Urls.identityProviderAuthnRequest(baseURI, identityProvider.getId(), realm.getName()).toString();
providers.add(new IdentityProvider(identityProvider.getId(), identityProvider.getName(), loginUrl));
}
}

View file

@ -879,7 +879,7 @@ public class OpenIDConnectService {
}
return Response.temporaryRedirect(
Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), identityProviderModel, realm, accessCode)).build();
Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), idpHint, this.realm.getName(), accessCode)).build();
}
response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event);
@ -898,7 +898,7 @@ public class OpenIDConnectService {
if (!identityProviders.isEmpty()) {
if (identityProviders.size() == 1) {
return Response.temporaryRedirect(
Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), identityProviders.get(0), this.realm, accessCode))
Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), identityProviders.get(0).getId(), this.realm.getName(), accessCode))
.build();
}

View file

@ -46,7 +46,6 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OpenIDConnect;
@ -684,14 +683,9 @@ public class AccountService {
clientSession.setRedirectUri(redirectUri);
clientSession.setNote(OpenIDConnect.STATE_PARAM, UUID.randomUUID().toString());
URI url = UriBuilder.fromUri(this.uriInfo.getBaseUri())
.path(AuthenticationBrokerResource.class)
.path(AuthenticationBrokerResource.class, "performLogin")
.queryParam("provider_id", providerId)
.queryParam("code", clientSessionCode.getCode())
.build(this.realm.getName());
return Response.temporaryRedirect(url).build();
return Response.temporaryRedirect(
Urls.identityProviderAuthnRequest(this.uriInfo.getBaseUri(), providerId, realm.getName(), clientSessionCode.getCode()))
.build();
} catch (Exception spe) {
setReferrerOnPage();
return account.setError(Messages.IDENTITY_PROVIDER_REDIRECT_ERROR).createResponse(AccountPages.FEDERATED_IDENTITY);

View file

@ -1,495 +0,0 @@
/*
* 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.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.managers.RealmManager;
import org.keycloak.services.resources.flows.Flows;
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.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 AuthenticationBrokerResource {
private static final Logger logger = Logger.getLogger(AuthenticationBrokerResource.class);
@Context
private UriInfo uriInfo;
@Context
private KeycloakSession session;
@Context
private ClientConnection clientConnection;
@Context
private HttpRequest request;
@GET
@Path("{realm}/login")
public Response performLogin(@PathParam("realm") String realmName,
@QueryParam("provider_id") String providerId,
@QueryParam("code") String code) {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName);
ClientSessionCode clientCode = isValidAuthorizationCode(code, realm);
if (clientCode == null) {
return redirectToErrorPage(realm, "Invalid code, please login again through your application.");
}
EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder()
.event(EventType.LOGIN)
.detail(Details.AUTH_METHOD, "unknown_id@" + providerId);
try {
ClientSessionModel clientSession = clientCode.getClientSession();
ClientModel clientModel = clientSession.getClient();
Response response = checkClientPermissions(clientModel, providerId);
if (response != null) {
return response;
}
IdentityProvider identityProvider = getIdentityProvider(realm, providerId);
if (identityProvider == null) {
event.error(Errors.IDENTITY_PROVIDER_NOT_FOUND);
return Flows.forms(session, realm, null, uriInfo).setError("Identity Provider not found").createErrorPage();
}
AuthenticationResponse authenticationResponse = identityProvider.handleRequest(createAuthenticationRequest(providerId, code, realm,
clientSession));
response = authenticationResponse.getResponse();
if (response != null) {
event.success();
return response;
}
} catch (Exception e) {
logger.error("Could not send authentication request to identity provider " + providerId, e);
String message = "Could not send authentication request to identity provider";
event.error(message);
return redirectToErrorPage(realm, message);
}
String message = "Could not proceed with authentication request to identity provider.";
event.error(message);
return redirectToErrorPage(realm, message);
}
@GET
@Path("{realm}/{provider_id}")
public Response handleResponseGet(@PathParam("realm") final String realmName,
@PathParam("provider_id") String providerId) {
return handleResponse(realmName, providerId);
}
@POST
@Path("{realm}/{provider_id}")
public Response handleResponsePost(@PathParam("realm") final String realmName,
@PathParam("provider_id") String providerId) {
return handleResponse(realmName, providerId);
}
@Path("{realm}/{provider_id}/token")
@OPTIONS
public Response retrieveTokenPreflight() {
return Cors.add(this.request, Response.ok()).auth().preflight().build();
}
@GET
@Path("{realm}/{provider_id}/token")
public Response retrieveToken(@PathParam("realm") final String realmName, @PathParam("provider_id") String providerId) {
return getToken(realmName, providerId, false);
}
private Response getToken(String realmName, String providerId, boolean forceRetrieval) {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName);
AppAuthManager authManager = new AppAuthManager();
AuthResult authResult = authManager.authenticateBearerToken(session, realm, uriInfo, clientConnection, request.getHttpHeaders());
if (authResult != null) {
String audience = authResult.getToken().getAudience();
ClientModel clientModel = realm.findClient(audience);
if (clientModel == null) {
return corsResponse(Flows.errors().error("Invalid client.", Response.Status.FORBIDDEN), clientModel);
}
if (!clientModel.hasIdentityProvider(providerId)) {
return corsResponse(Flows.errors().error("Client [" + audience + "] not authorized.", Response.Status.FORBIDDEN), clientModel);
}
if (OAuthClientModel.class.isInstance(clientModel) && !forceRetrieval) {
return corsResponse(Flows.forms(session, realm, clientModel, uriInfo)
.setClientSessionCode(authManager.extractAuthorizationHeaderToken(request.getHttpHeaders()))
.setAccessRequest("Your information from " + providerId + " identity provider.")
.setClient(clientModel)
.setUriInfo(this.uriInfo)
.setActionUri(this.uriInfo.getRequestUri())
.createOAuthGrant(), clientModel);
}
IdentityProvider identityProvider = getIdentityProvider(realm, providerId);
IdentityProviderModel identityProviderConfig = getIdentityProviderConfig(realm, providerId);
if (identityProviderConfig.isStoreToken()) {
FederatedIdentityModel identity = this.session.users().getFederatedIdentity(authResult.getUser(), providerId, realm);
if (identity == null) {
return corsResponse(Flows.errors().error("User [" + authResult.getUser().getId() + "] is not associated with identity provider [" + providerId + "].", Response.Status.FORBIDDEN), clientModel);
}
return corsResponse(identityProvider.retrieveToken(identity), clientModel);
}
return corsResponse(Flows.errors().error("Identity Provider [" + providerId + "] does not support this operation.", Response.Status.FORBIDDEN), clientModel);
}
return Flows.errors().error("Invalid code.", Response.Status.FORBIDDEN);
}
@POST
@Path("{realm}/{provider_id}/token")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response consentTokenRetrieval(@PathParam("realm") final String realmName, @PathParam("provider_id") String providerId,
final MultivaluedMap<String, String> 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<String, IdentityProviderFactory> availableProviders = new HashMap<String, IdentityProviderFactory>();
List<ProviderFactory> allProviders = new ArrayList<ProviderFactory>();
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();
}
}

View file

@ -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;
/**
* <p></p>
*
* @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<String, String> 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<String, IdentityProviderFactory> availableProviders = new HashMap<String, IdentityProviderFactory>();
List<ProviderFactory> allProviders = new ArrayList<ProviderFactory>();
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();
}
}
}

View file

@ -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);

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -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;
}
}

View file

@ -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);

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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<OAuth2Iden
return AuthenticationResponse.temporaryRedirect(authenticationUrl);
} catch (Exception e) {
throw new RuntimeException(e);
throw new IdentityBrokerException("Could send authentication request to twitter.", e);
}
}
@ -83,7 +84,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
MultivaluedMap<String, String> 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<OAuth2Iden
return AuthenticationResponse.end(identity);
} catch (Exception e) {
throw new RuntimeException(e);
throw new IdentityBrokerException("Could get user profile from twitter.", e);
}
}

View file

@ -30,6 +30,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.IDToken;
import org.keycloak.services.resources.flows.Urls;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus;
@ -53,8 +54,10 @@ import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Set;
@ -69,6 +72,8 @@ import static org.junit.Assert.assertTrue;
*/
public abstract class AbstractIdentityProviderTest {
private static final URI BASE_URI = UriBuilder.fromUri("http://localhost:8081/auth").build();
@ClassRule
public static BrokerKeyCloakRule brokerServerRule = new BrokerKeyCloakRule();
@ -308,7 +313,7 @@ public abstract class AbstractIdentityProviderTest {
UserSessionStatus userSessionStatus = retrieveSessionStatus();
String accessToken = userSessionStatus.getAccessTokenString();
String tokenEndpointUrl = "http://localhost:8081/auth/broker/realm-with-broker/" + getProviderId() + "/token";
URI tokenEndpointUrl = Urls.identityProviderRetrieveToken(BASE_URI, getProviderId(), realm.getName());
final String authHeader = "Bearer " + accessToken;
ClientRequestFilter authFilter = new ClientRequestFilter() {
@Override
@ -317,11 +322,10 @@ public abstract class AbstractIdentityProviderTest {
}
};
Client client = ClientBuilder.newBuilder().register(authFilter).build();
UriBuilder authBase = UriBuilder.fromUri(tokenEndpointUrl);
WebTarget tokenEndpoint = client.target(authBase);
WebTarget tokenEndpoint = client.target(tokenEndpointUrl);
Response response = tokenEndpoint.request().get();
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
assertEquals(Status.OK.getStatusCode(), response.getStatus());
assertNotNull(response.readEntity(String.class));
driver.navigate().to("http://localhost:8081/test-app/logout");
@ -364,14 +368,15 @@ public abstract class AbstractIdentityProviderTest {
grantPage.accept();
assertTrue(oauth.getCurrentQuery().containsKey(OAuth2Constants.CODE));
AccessTokenResponse accessToken = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get(OAuth2Constants.CODE), "password");
String tokenEndpointUrl = "http://localhost:8081/auth/broker/realm-with-broker/" + getProviderId() + "/token";
URI tokenEndpointUrl = Urls.identityProviderRetrieveToken(BASE_URI, getProviderId(), getRealm().getName());
String authHeader = "Bearer " + accessToken.getAccessToken();
HtmlUnitDriver htmlUnitDriver = (WebRule.HtmlUnitDriver) this.driver;
htmlUnitDriver.getWebClient().addRequestHeader(HttpHeaders.AUTHORIZATION, authHeader);
htmlUnitDriver.navigate().to(tokenEndpointUrl);
htmlUnitDriver.navigate().to(tokenEndpointUrl.toString());
grantPage.assertCurrent();
grantPage.accept();

View file

@ -12,7 +12,7 @@
"enabled": true,
"secret": "secret",
"redirectUris": [
"http://localhost:8081/auth/broker/realm-with-broker/kc-oidc-idp"
"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-oidc-idp"
],
"claims": {
"name" : true,

View file

@ -11,7 +11,7 @@
"name": "http://localhost:8081/auth/",
"enabled": true,
"redirectUris": [
"http://localhost:8081/auth/broker/realm-with-broker/kc-saml-signed-idp"
"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-signed-idp"
],
"attributes": {
"saml.assertion.signature": "true",

View file

@ -11,7 +11,7 @@
"name": "http://localhost:8081/auth/",
"enabled": true,
"redirectUris": [
"http://localhost:8081/auth/broker/realm-with-broker/kc-saml-idp-basic"
"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic"
],
"attributes": {
"saml.authnstatement": "true"