diff --git a/broker/core/pom.xml b/broker/core/pom.xml new file mode 100755 index 0000000000..ab4389d2da --- /dev/null +++ b/broker/core/pom.xml @@ -0,0 +1,48 @@ + + + + keycloak-broker-parent + org.keycloak + 1.2.0.Beta1-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-broker-core + Keycloak Broker Core + + jar + + + + org.keycloak + keycloak-core + ${project.version} + + + org.keycloak + keycloak-model-api + ${project.version} + + + org.jboss.resteasy + resteasy-jaxrs + + + log4j + log4j + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-simple + + + + + + diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java new file mode 100644 index 0000000000..f38d840b56 --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProvider.java @@ -0,0 +1,42 @@ +/* + * 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; + +import org.keycloak.models.IdentityProviderModel; + +/** + * @author Pedro Igor + */ +public abstract class AbstractIdentityProvider implements IdentityProvider { + + private final C config; + + public AbstractIdentityProvider(C config) { + this.config = config; + } + + public C getConfig() { + return this.config; + } + + @Override + public void close() { + // no-op + } + +} diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java new file mode 100644 index 0000000000..b615f3858c --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderFactory.java @@ -0,0 +1,51 @@ +/* + * 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; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Pedro Igor + */ +public abstract class AbstractIdentityProviderFactory implements IdentityProviderFactory { + + @Override + public void close() { + + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public T create(KeycloakSession session) { + return null; + } + + @Override + public Map parseConfig(InputStream inputStream) { + return new HashMap(); + } +} diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java b/broker/core/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java new file mode 100644 index 0000000000..bd0898fa4a --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/AuthenticationRequest.java @@ -0,0 +1,76 @@ +/* + * 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; + +import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.RealmModel; + +import javax.ws.rs.core.UriInfo; + +/** + * @author Pedro Igor + */ +public class AuthenticationRequest { + + private final UriInfo uriInfo; + private final String state; + private final HttpRequest httpRequest; + private final RealmModel realm; + private final String redirectUri; + private final ClientSessionModel clientSession; + + public AuthenticationRequest(RealmModel realm, ClientSessionModel clientSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) { + this.realm = realm; + this.httpRequest = httpRequest; + this.uriInfo = uriInfo; + this.state = state; + this.redirectUri = redirectUri; + this.clientSession = clientSession; + } + + public UriInfo getUriInfo() { + return this.uriInfo; + } + + public String getState() { + return this.state; + } + + public HttpRequest getHttpRequest() { + return this.httpRequest; + } + + public RealmModel getRealm() { + return this.realm; + } + + /** + *

Returns the redirect url that must be included in an authentication request in order to process responses from an + * identity provider.

+ * + * @return + */ + public String getRedirectUri() { + return this.redirectUri; + } + + public ClientSessionModel getClientSession() { + return this.clientSession; + } +} diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/AuthenticationResponse.java b/broker/core/src/main/java/org/keycloak/broker/provider/AuthenticationResponse.java new file mode 100644 index 0000000000..641c7dacea --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/AuthenticationResponse.java @@ -0,0 +1,57 @@ +/* + * 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; + +import javax.ws.rs.core.Response; +import java.net.URI; + +/** + * @author Pedro Igor + */ +public class AuthenticationResponse { + + private final Response response; + private final FederatedIdentity user; + + private AuthenticationResponse(FederatedIdentity user) { + this.user = user; + this.response = null; + } + + private AuthenticationResponse(Response response) { + this.user = null; + this.response = response; + } + + public Response getResponse() { + return this.response; + } + + public FederatedIdentity getUser() { + return this.user; + } + + public static AuthenticationResponse end(FederatedIdentity identity) { + return new AuthenticationResponse(identity); + } + + public static AuthenticationResponse temporaryRedirect(URI url) { + return new AuthenticationResponse(Response.temporaryRedirect(url).build()); + } + +} 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 new file mode 100644 index 0000000000..ac853f7e3e --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/FederatedIdentity.java @@ -0,0 +1,87 @@ +/* + * 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; + +/** + *

Represents all identity information obtained from an {@link IdentityProvider} after a + * successful authentication.

+ * + * @author Pedro Igor + */ +public class FederatedIdentity { + + private String id; + private String username; + private String firstName; + private String lastName; + private String email; + + public FederatedIdentity(String id) { + if (id == null) { + throw new RuntimeException("No identifier provider for identity."); + } + + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFirstName() { + return firstName; + } + + public void setName(String name) { + if (name != null) { + int i = name.lastIndexOf(' '); + if (i != -1) { + firstName = name.substring(0, i); + lastName = name.substring(i + 1); + } else { + firstName = name; + } + } + } + + public String getLastName() { + return lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + +} 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 new file mode 100644 index 0000000000..cfe6f41dc7 --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProvider.java @@ -0,0 +1,67 @@ +/* + * 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; + +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.provider.Provider; + +/** + * @author Pedro Igor + */ +public interface IdentityProvider extends Provider { + + /** + *

Initiates the authentication process by sending an authentication request to an identity provider. This method is called + * only once during the authentication.

+ * + *

Depending on how the authentication is performed, this method may redirect the user to the identity provider for authentication. + * In this case, the response would contain a {@link javax.ws.rs.core.Response} that will be used to redirect the user.

+ * + *

However, if the authentication flow does not require a redirect to the identity provider (eg.: simple challenge/response mechanism), this method may return a response containing + * a {@link FederatedIdentity} representing the identity information for an user. In this case, the authentication flow stops.

+ * + * @param request The initial authentication request. Contains all the contextual information in order to build an authentication request to the + * identity provider. + * @return + */ + AuthenticationResponse handleRequest(AuthenticationRequest request); + + /** + *

Obtains state information sent to the identity provider during the authentication request. Implementations must always + * return the same state in order to check the validity of a response from the identity provider.

+ * + *

This method is invoked on each response from the identity provider.

+ * + * @param request The request sent by the identity provider in a response to an authentication request. + * @return + */ + String getRelayState(AuthenticationRequest request); + + /** + *

Handles a response from the identity provider after a successful authentication request is made. Usually, the response will + * contain all the necessary information in order to trust the authentication performed by the identity provider and resolve + * the identity information for the authenticating user.

+ * + *

If the response is trusted and proves user's authenticity, this method may return a + * {@link FederatedIdentity} in the response. In this case, the authentication flow stops.

+ * + * @param request + * @return + */ + AuthenticationResponse handleResponse(AuthenticationRequest request); +} diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java new file mode 100644 index 0000000000..1f1bdccf05 --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderFactory.java @@ -0,0 +1,56 @@ +/* + * 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; + +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.provider.ProviderFactory; + +import java.io.InputStream; +import java.util.Map; + +/** + * @author Pedro Igor + */ +public interface IdentityProviderFactory extends ProviderFactory { + + /** + *

A friendly name for this factory.

+ * + * @return + */ + String getName(); + + /** + *

Creates an {@link IdentityProvider} based on the configuration contained in + * model.

+ * + * @param model The configuration to be used to create the identity provider. + * @return + */ + T create(IdentityProviderModel model); + + /** + *

Creates an {@link IdentityProvider} based on the configuration from + * inputStream.

+ * + * @param model The model containing the common abd basic configuration for an identity provider. + * @param inputStream The input stream from where configuration will be loaded from.. + * @return + */ + Map parseConfig(InputStream inputStream); +} \ No newline at end of file diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderSpi.java b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderSpi.java new file mode 100644 index 0000000000..de9872aea8 --- /dev/null +++ b/broker/core/src/main/java/org/keycloak/broker/provider/IdentityProviderSpi.java @@ -0,0 +1,45 @@ +/* + * 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; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Pedro Igor + */ +public class IdentityProviderSpi implements Spi { + + public static final String IDENTITY_PROVIDER_SPI_NAME = "identity_provider"; + + @Override + public String getName() { + return IDENTITY_PROVIDER_SPI_NAME; + } + + @Override + public Class getProviderClass() { + return IdentityProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return IdentityProviderFactory.class; + } +} diff --git a/broker/core/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/broker/core/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100755 index 0000000000..d4ef41b616 --- /dev/null +++ b/broker/core/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1 @@ +org.keycloak.broker.provider.IdentityProviderSpi \ No newline at end of file diff --git a/broker/oidc/pom.xml b/broker/oidc/pom.xml new file mode 100755 index 0000000000..313db2d180 --- /dev/null +++ b/broker/oidc/pom.xml @@ -0,0 +1,35 @@ + + + + keycloak-broker-parent + org.keycloak + 1.2.0.Beta1-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-broker-oidc + Keycloak Broker - OpenID Connect Identity Provider + + jar + + + + org.keycloak + keycloak-broker-core + ${project.version} + + + org.codehaus.jackson + jackson-core-asl + provided + + + org.codehaus.jackson + jackson-mapper-asl + provided + + + + 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 new file mode 100644 index 0000000000..a34438ebff --- /dev/null +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -0,0 +1,156 @@ +/* + * 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.oidc; + +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.map.ObjectMapper; +import org.keycloak.OAuth2Constants; +import org.keycloak.broker.oidc.util.SimpleHttp; +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 javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Pedro Igor + */ +public abstract class AbstractOAuth2IdentityProvider extends AbstractIdentityProvider { + + public static final String OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"; + protected static ObjectMapper mapper = new ObjectMapper(); + + public static final String OAUTH2_PARAMETER_ACCESS_TOKEN = "access_token"; + public static final String OAUTH2_PARAMETER_SCOPE = "scope"; + public static final String OAUTH2_PARAMETER_STATE = "state"; + public static final String OAUTH2_PARAMETER_RESPONSE_TYPE = "response_type"; + public static final String OAUTH2_PARAMETER_REDIRECT_URI = "redirect_uri"; + public static final String OAUTH2_PARAMETER_CODE = "code"; + public static final String OAUTH2_PARAMETER_CLIENT_ID = "client_id"; + public static final String OAUTH2_PARAMETER_CLIENT_SECRET = "client_secret"; + public static final String OAUTH2_PARAMETER_GRANT_TYPE = "grant_type"; + + public AbstractOAuth2IdentityProvider(C config) { + super(config); + } + + @Override + public AuthenticationResponse handleRequest(AuthenticationRequest request) { + try { + URI authorizationUrl = createAuthorizationUrl(request).build(); + + return AuthenticationResponse.temporaryRedirect(authorizationUrl); + } catch (Exception e) { + throw new RuntimeException("Could not create authentication request.", e); + } + } + + @Override + public String getRelayState(AuthenticationRequest request) { + UriInfo uriInfo = request.getUriInfo(); + return uriInfo.getQueryParameters().getFirst(OAUTH2_PARAMETER_STATE); + } + + @Override + public AuthenticationResponse handleResponse(AuthenticationRequest request) { + UriInfo uriInfo = request.getUriInfo(); + String error = uriInfo.getQueryParameters().getFirst(OAuth2Constants.ERROR); + + if (error != null) { + if (error.equals("access_denied")) { + throw new RuntimeException("Access denied."); + } else { + throw new RuntimeException(error); + } + } + + try { + String authorizationCode = uriInfo.getQueryParameters().getFirst(OAUTH2_PARAMETER_CODE); + + if (authorizationCode != null) { + String response = SimpleHttp.doPost(getConfig().getTokenUrl()) + .param(OAUTH2_PARAMETER_CODE, authorizationCode) + .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId()) + .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()) + .param(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri()) + .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE).asString(); + + return doHandleResponse(response); + } + + throw new RuntimeException("No authorization code from identity provider."); + } catch (Exception e) { + throw new RuntimeException("Could not process response from identity provider.", e); + } + } + + protected AuthenticationResponse doHandleResponse(String response) throws IOException { + String token = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN); + + if (token == null) { + throw new RuntimeException("No access token from server."); + } + + return AuthenticationResponse.end(getFederatedIdentity(token)); + } + + protected String extractTokenFromResponse(String response, String tokenName) throws IOException { + if (response.startsWith("{")) { + return mapper.readTree(response).get(tokenName).getTextValue(); + } else { + Matcher matcher = Pattern.compile(tokenName + "=([^&]+)").matcher(response); + + if (matcher.find()) { + return matcher.group(1); + } + } + + return null; + } + + protected FederatedIdentity getFederatedIdentity(String accessToken) { + throw new RuntimeException("Not implemented."); + }; + + protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) { + return UriBuilder.fromPath(getConfig().getAuthorizationUrl()) + .queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope()) + .queryParam(OAUTH2_PARAMETER_STATE, request.getState()) + .queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code") + .queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId()) + .queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri()); + } + + protected String getJsonProperty(JsonNode jsonNode, String name) { + if (jsonNode.has(name)) { + return jsonNode.get(name).asText(); + } + + return null; + } + + protected JsonNode asJsonNode(String json) throws IOException { + return mapper.readTree(json); + } +} diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java new file mode 100644 index 0000000000..52be1cdaee --- /dev/null +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java @@ -0,0 +1,80 @@ +/* + * 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.oidc; + +import org.keycloak.models.IdentityProviderModel; + +import java.util.Map; + +/** + * @author Pedro Igor + */ +public class OAuth2IdentityProviderConfig extends IdentityProviderModel { + + public OAuth2IdentityProviderConfig(String providerId, String id, String name, Map config) { + super(providerId, id, name, config); + } + + public String getAuthorizationUrl() { + return getConfig().get("authorizationUrl"); + } + + public void setAuthorizationUrl(String authorizationUrl) { + getConfig().put("authorizationUrl", authorizationUrl); + } + + public String getTokenUrl() { + return getConfig().get("tokenUrl"); + } + + public void setTokenUrl(String tokenUrl) { + getConfig().put("tokenUrl", tokenUrl); + } + + public String getUserInfoUrl() { + return getConfig().get("userInfoUrl"); + } + + public void setUserInfoUrl(String userInfoUrl) { + getConfig().put("userInfoUrl", userInfoUrl); + } + + public String getClientId() { + return getConfig().get("clientId"); + } + + public void setClientId(String clientId) { + getConfig().put("clientId", clientId); + } + + public String getClientSecret() { + return getConfig().get("clientSecret"); + } + + public void setClientSecret(String clientSecret) { + getConfig().put("clientSecret", clientSecret); + } + + public String getDefaultScope() { + return getConfig().get("defaultScope"); + } + + public void setDefaultScope(String defaultScope) { + getConfig().put("defaultScope", defaultScope); + } +} \ No newline at end of file diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java new file mode 100644 index 0000000000..f82ccc6d78 --- /dev/null +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java @@ -0,0 +1,128 @@ +/* + * 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.oidc; + +import org.codehaus.jackson.JsonNode; +import org.keycloak.broker.oidc.util.SimpleHttp; +import org.keycloak.broker.provider.AuthenticationRequest; +import org.keycloak.broker.provider.AuthenticationResponse; +import org.keycloak.broker.provider.FederatedIdentity; +import org.keycloak.jose.jws.JWSInput; + +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; + +/** + * @author Pedro Igor + */ +public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider { + + public static final String OAUTH2_PARAMETER_PROMPT = "prompt"; + public static final String OIDC_PARAMETER_ID_TOKEN = "id_token"; + + public OIDCIdentityProvider(OIDCIdentityProviderConfig config) { + super(config); + } + + @Override + protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) { + return super.createAuthorizationUrl(request) + .queryParam(OAUTH2_PARAMETER_PROMPT, getConfig().getPrompt()); + } + + @Override + protected AuthenticationResponse doHandleResponse(String response) throws IOException { + String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN); + + if (accessToken == null) { + throw new RuntimeException("No access_token from server."); + } + + String idToken = extractTokenFromResponse(response, OIDC_PARAMETER_ID_TOKEN); + + validateIdToken(idToken); + + try { + JsonNode userInfo = SimpleHttp.doGet(getConfig().getUserInfoUrl()) + .header("Authorization", "Bearer " + accessToken) + .asJson(); + + String id = getJsonProperty(userInfo, "sub"); + String name = getJsonProperty(userInfo, "name"); + String preferredUsername = getJsonProperty(userInfo, "preferred_username"); + String email = getJsonProperty(userInfo, "email"); + + FederatedIdentity identity = new FederatedIdentity(id); + + identity.setId(id); + identity.setName(name); + identity.setEmail(email); + + if (preferredUsername == null) { + preferredUsername = email; + } + + if (preferredUsername == null) { + preferredUsername = id; + } + + identity.setUsername(preferredUsername); + + return AuthenticationResponse.end(identity); + } catch (Exception e) { + throw new RuntimeException("Could not fetch attributes from userinfo endpoint.", e); + } + } + + private void validateIdToken(String idToken) { + if (idToken == null) { + throw new RuntimeException("No id_token from server."); + } + + try { + JsonNode idTokenInfo = asJsonNode(decodeJWS(idToken)); + + String aud = getJsonProperty(idTokenInfo, "aud"); + String iss = getJsonProperty(idTokenInfo, "iss"); + + if (aud != null && !aud.equals(getConfig().getClientId())) { + throw new RuntimeException("Wrong audience from id_token.."); + } + + String trustedIssuers = getConfig().getIssuer(); + + if (trustedIssuers != null) { + String[] issuers = trustedIssuers.split(","); + + for (String trustedIssuer : issuers) { + if (iss != null && iss.equals(trustedIssuer.trim())) { + return; + } + } + + throw new RuntimeException("Wrong issuer from id_token.."); + } + } catch (IOException e) { + throw new RuntimeException("Could not decode id token.", e); + } + } + + private String decodeJWS(String token) { + return new JWSInput(token).readContentAsString(); + } +} diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java new file mode 100644 index 0000000000..302221a759 --- /dev/null +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java @@ -0,0 +1,56 @@ +/* + * 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.oidc; + +import java.util.Map; + +/** + * @author Pedro Igor + */ +public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig { + + public OIDCIdentityProviderConfig(String providerId, String id, String name, Map config) { + super(providerId, id, name, config); + } + + public String getPrompt() { + String prompt = getConfig().get("prompt"); + + if (prompt == null || "".equals(prompt)) { + return "none"; + } + + return prompt; + } + + @Override + public String getDefaultScope() { + String scope = super.getDefaultScope(); + + if (scope == null || "".equals(scope)) { + scope = "openid"; + } + + return scope; + } + + public String getIssuer() { + return getConfig().get("issuer"); + } + +} diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java new file mode 100644 index 0000000000..10316d9799 --- /dev/null +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java @@ -0,0 +1,42 @@ +/* + * 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.oidc; + +import org.keycloak.broker.provider.AbstractIdentityProviderFactory; +import org.keycloak.models.IdentityProviderModel; + +/** + * @author Pedro Igor + */ +public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory { + + @Override + public String getName() { + return "OpenID Connect v1.0"; + } + + @Override + public OIDCIdentityProvider create(IdentityProviderModel model) { + return new OIDCIdentityProvider(new OIDCIdentityProviderConfig(getId(), model.getId(), model.getName(), model.getConfig())); + } + + @Override + public String getId() { + return "oidc"; + } +} diff --git a/social/core/src/main/java/org/keycloak/social/utils/SimpleHttp.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java similarity index 99% rename from social/core/src/main/java/org/keycloak/social/utils/SimpleHttp.java rename to broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java index 757d58f3ae..870e951a96 100644 --- a/social/core/src/main/java/org/keycloak/social/utils/SimpleHttp.java +++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java @@ -1,4 +1,4 @@ -package org.keycloak.social.utils; +package org.keycloak.broker.oidc.util; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; diff --git a/broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory b/broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory new file mode 100644 index 0000000000..50071ed65f --- /dev/null +++ b/broker/oidc/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory @@ -0,0 +1 @@ +org.keycloak.broker.oidc.OIDCIdentityProviderFactory \ No newline at end of file diff --git a/broker/pom.xml b/broker/pom.xml new file mode 100755 index 0000000000..7121b2b031 --- /dev/null +++ b/broker/pom.xml @@ -0,0 +1,23 @@ + + + + keycloak-parent + org.keycloak + 1.2.0.Beta1-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-broker-parent + Keycloak Broker Parent + + pom + + + core + oidc + saml + + + diff --git a/broker/saml/pom.xml b/broker/saml/pom.xml new file mode 100755 index 0000000000..a0e6706f1d --- /dev/null +++ b/broker/saml/pom.xml @@ -0,0 +1,30 @@ + + + + keycloak-broker-parent + org.keycloak + 1.2.0.Beta1-SNAPSHOT + ../pom.xml + + 4.0.0 + + keycloak-broker-saml + Keycloak Broker - SAML Identity Provider + + jar + + + + org.keycloak + keycloak-broker-core + ${project.version} + provided + + + org.picketlink + picketlink-federation + + + + diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java new file mode 100644 index 0000000000..59863f8d35 --- /dev/null +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java @@ -0,0 +1,255 @@ +/* + * 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.saml; + +import org.jboss.resteasy.spi.HttpRequest; +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.picketlink.common.constants.JBossSAMLConstants; +import org.picketlink.common.constants.JBossSAMLURIConstants; +import org.picketlink.common.exceptions.ProcessingException; +import org.picketlink.common.util.DocumentUtil; +import org.picketlink.common.util.StaxParserUtil; +import org.picketlink.identity.federation.api.saml.v2.request.SAML2Request; +import org.picketlink.identity.federation.api.saml.v2.response.SAML2Response; +import org.picketlink.identity.federation.api.saml.v2.sig.SAML2Signature; +import org.picketlink.identity.federation.core.parsers.saml.SAMLParser; +import org.picketlink.identity.federation.core.saml.v2.common.IDGenerator; +import org.picketlink.identity.federation.core.saml.v2.common.SAMLDocumentHolder; +import org.picketlink.identity.federation.core.util.JAXPValidationUtil; +import org.picketlink.identity.federation.core.util.XMLEncryptionUtil; +import org.picketlink.identity.federation.core.util.XMLSignatureUtil; +import org.picketlink.identity.federation.saml.v2.assertion.AssertionType; +import org.picketlink.identity.federation.saml.v2.assertion.EncryptedAssertionType; +import org.picketlink.identity.federation.saml.v2.assertion.NameIDType; +import org.picketlink.identity.federation.saml.v2.assertion.SubjectType; +import org.picketlink.identity.federation.saml.v2.assertion.SubjectType.STSubType; +import org.picketlink.identity.federation.saml.v2.protocol.AuthnRequestType; +import org.picketlink.identity.federation.saml.v2.protocol.ResponseType; +import org.picketlink.identity.federation.saml.v2.protocol.ResponseType.RTChoiceType; +import org.picketlink.identity.federation.saml.v2.protocol.StatusCodeType; +import org.picketlink.identity.federation.saml.v2.protocol.StatusDetailType; +import org.picketlink.identity.federation.saml.v2.protocol.StatusType; +import org.picketlink.identity.federation.web.util.PostBindingUtil; +import org.picketlink.identity.federation.web.util.RedirectBindingUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import javax.xml.namespace.QName; +import java.net.URI; +import java.net.URLDecoder; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * @author Pedro Igor + */ +public class SAMLIdentityProvider extends AbstractIdentityProvider { + + private static final String SAML_REQUEST_PARAMETER = "SAMLRequest"; + private static final String SAML_RESPONSE_PARAMETER = "SAMLResponse"; + private static final String RELAY_STATE_PARAMETER = "RelayState"; + + private SAML2Signature saml2Signature = new SAML2Signature(); + + public SAMLIdentityProvider(SAMLIdentityProviderConfig config) { + super(config); + } + + @Override + public AuthenticationResponse handleRequest(AuthenticationRequest request) { + try { + UriInfo uriInfo = request.getUriInfo(); + String issuerURL = UriBuilder.fromUri(uriInfo.getBaseUri()).build().toString(); + String destinationUrl = getConfig().getSingleSignOnServiceUrl(); + SAML2Request samlRequest = new SAML2Request(); + String nameIDPolicyFormat = getConfig().getNameIDPolicyFormat(); + + if (nameIDPolicyFormat == null) { + nameIDPolicyFormat = JBossSAMLURIConstants.NAMEID_FORMAT_PERSISTENT.get(); + } + + samlRequest.setNameIDFormat(nameIDPolicyFormat); + + AuthnRequestType authn = samlRequest + .createAuthnRequestType(IDGenerator.create("ID_"), request.getRedirectUri(), destinationUrl, issuerURL); + + authn.setProtocolBinding(URI.create(JBossSAMLURIConstants.SAML_HTTP_POST_BINDING.get())); + authn.setForceAuthn(getConfig().isForceAuthn()); + + Document authnDoc = samlRequest.convert(authn); + + if (getConfig().isWantAuthnRequestsSigned()) { + PrivateKey privateKey = request.getRealm().getPrivateKey(); + 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."); + } + + 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."); + } + + KeyPair keypair = new KeyPair(publicKey, privateKey); + + this.saml2Signature.signSAMLDocument(authnDoc, keypair); + } + + byte[] responseBytes = DocumentUtil.getDocumentAsString(authnDoc).getBytes("UTF-8"); + String urlEncodedResponse = RedirectBindingUtil.deflateBase64URLEncode(responseBytes); + URI redirectUri = UriBuilder.fromPath(destinationUrl) + .queryParam(SAML_REQUEST_PARAMETER, urlEncodedResponse) + .queryParam(RELAY_STATE_PARAMETER, request.getState()).build(); + + return AuthenticationResponse.temporaryRedirect(redirectUri); + } catch (Exception e) { + throw new RuntimeException("Could not create authentication request.", e); + } + } + + @Override + public String getRelayState(AuthenticationRequest request) { + HttpRequest httpRequest = request.getHttpRequest(); + return httpRequest.getFormParameters().getFirst(RELAY_STATE_PARAMETER); + } + + @Override + public AuthenticationResponse handleResponse(AuthenticationRequest request) { + HttpRequest httpRequest = request.getHttpRequest(); + String samlResponse = httpRequest.getFormParameters().getFirst(SAML_RESPONSE_PARAMETER); + + if (samlResponse == null) { + throw new RuntimeException("No response from SAML identity provider."); + } + + try { + SAML2Request saml2Request = new SAML2Request(); + ResponseType responseType = (ResponseType) saml2Request + .getSAML2ObjectFromStream(PostBindingUtil.base64DecodeAsStream(URLDecoder.decode(samlResponse, "UTF-8"))); + AssertionType assertion = getAssertion(request, saml2Request, responseType); + + SubjectType subject = assertion.getSubject(); + STSubType subType = subject.getSubType(); + NameIDType subjectNameID = (NameIDType) subType.getBaseID(); + + FederatedIdentity user = new FederatedIdentity(subjectNameID.getValue()); + + user.setUsername(subjectNameID.getValue()); + + if (subjectNameID.getFormat().toString().equals(JBossSAMLURIConstants.NAMEID_FORMAT_EMAIL.get())) { + user.setEmail(subjectNameID.getValue()); + } + + return AuthenticationResponse.end(user); + } catch (Exception e) { + throw new RuntimeException("Could not process response from SAML identity provider.", e); + } + } + + private AssertionType getAssertion(AuthenticationRequest request, SAML2Request saml2Request, ResponseType responseType) throws ProcessingException { + validateStatusResponse(responseType); + validateSignature(saml2Request); + + List assertions = responseType.getAssertions(); + + if (assertions.isEmpty()) { + throw new RuntimeException("No assertion from response."); + } + + RTChoiceType rtChoiceType = assertions.get(0); + EncryptedAssertionType encryptedAssertion = rtChoiceType.getEncryptedAssertion(); + + if (encryptedAssertion != null) { + decryptAssertion(responseType, request.getRealm().getPrivateKey()); + + } + + return responseType.getAssertions().get(0).getAssertion(); + } + + private void validateSignature(SAML2Request saml2Request) throws ProcessingException { + if (getConfig().isValidateSignature()) { + X509Certificate certificate = XMLSignatureUtil.getX509CertificateFromKeyInfoString(getConfig().getSigningPublicKey().replaceAll("\\s", "")); + SAMLDocumentHolder samlDocumentHolder = saml2Request.getSamlDocumentHolder(); + Document samlDocument = samlDocumentHolder.getSamlDocument(); + + this.saml2Signature.validate(samlDocument, certificate.getPublicKey()); + } + } + + private void validateStatusResponse(ResponseType responseType) { + StatusType status = responseType.getStatus(); + StatusCodeType statusCode = status.getStatusCode(); + + if (!JBossSAMLURIConstants.STATUS_SUCCESS.get().equals(statusCode.getValue().toString())) { + StatusDetailType statusDetailType = status.getStatusDetail(); + StringBuilder detailMessage = new StringBuilder(); + + if (statusDetailType != null) { + for (Object statusDetail : statusDetailType.getAny()) { + detailMessage.append(statusDetail); + } + } else { + detailMessage.append("none"); + } + + throw new RuntimeException("Authentication failed with code [" + statusCode.getValue() + " and detail [" + detailMessage.toString() + "."); + } + } + + private ResponseType decryptAssertion(ResponseType responseType, PrivateKey privateKey) throws ProcessingException { + SAML2Response saml2Response = new SAML2Response(); + + try { + Document doc = saml2Response.convert(responseType); + Element enc = DocumentUtil.getElement(doc, new QName(JBossSAMLConstants.ENCRYPTED_ASSERTION.get())); + + if (enc == null) { + throw new RuntimeException("No encrypted assertion found."); + } + + String oldID = enc.getAttribute(JBossSAMLConstants.ID.get()); + Document newDoc = DocumentUtil.createDocument(); + Node importedNode = newDoc.importNode(enc, true); + newDoc.appendChild(importedNode); + + Element decryptedDocumentElement = XMLEncryptionUtil.decryptElementInDocument(newDoc, privateKey); + SAMLParser parser = new SAMLParser(); + + JAXPValidationUtil.checkSchemaValidation(decryptedDocumentElement); + AssertionType assertion = (AssertionType) parser.parse(StaxParserUtil.getXMLEventReader(DocumentUtil + .getNodeAsStream(decryptedDocumentElement))); + + responseType.replaceAssertion(oldID, new RTChoiceType(assertion)); + + return responseType; + } catch (Exception e) { + throw new RuntimeException("Could not decrypt assertion.", e); + } + } + +} diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java new file mode 100644 index 0000000000..b0b87247b5 --- /dev/null +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderConfig.java @@ -0,0 +1,92 @@ +/* + * 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.saml; + +import org.keycloak.models.IdentityProviderModel; + +import java.util.Map; + +/** + * @author Pedro Igor + */ +public class SAMLIdentityProviderConfig extends IdentityProviderModel { + + public SAMLIdentityProviderConfig() { + super(); + } + + public SAMLIdentityProviderConfig(String providerId, String id, String name, Map config) { + super(providerId, id, name, config); + } + + public String getSingleSignOnServiceUrl() { + return getConfig().get("singleSignOnServiceUrl"); + } + + public void setSingleSignOnServiceUrl(String singleSignOnServiceUrl) { + getConfig().put("singleSignOnServiceUrl", singleSignOnServiceUrl); + } + + public boolean isValidateSignature() { + return Boolean.valueOf(getConfig().get("validateSignature")); + } + + public void setValidateSignature(boolean validateSignature) { + getConfig().put("validateSignature", String.valueOf(validateSignature)); + } + + public boolean isForceAuthn() { + return Boolean.valueOf(getConfig().get("forceAuthn")); + } + + public void setForceAuthn(boolean forceAuthn) { + getConfig().put("forceAuthn", String.valueOf(forceAuthn)); + } + + public String getSigningPublicKey() { + return getConfig().get("signingPublicKey"); + } + + public void setSigningPublicKey(String signingPublicKey) { + getConfig().put("signingPublicKey", signingPublicKey); + } + + public String getNameIDPolicyFormat() { + return getConfig().get("nameIDPolicyFormat"); + } + + public void setNameIDPolicyFormat(String signingPublicKey) { + getConfig().put("nameIDPolicyFormat", signingPublicKey); + } + + public boolean isWantAuthnRequestsSigned() { + return Boolean.valueOf(getConfig().get("wantAuthnRequestsSigned")); + } + + public void setWantAuthnRequestsSigned(boolean wantAuthnRequestsSigned) { + getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned)); + } + + public String getEncryptionPublicKey() { + return getConfig().get("encryptionPublicKey"); + } + + public void setEncryptionPublicKey(String encryptionPublicKey) { + getConfig().put("encryptionPublicKey", encryptionPublicKey); + } +} diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java new file mode 100644 index 0000000000..9cbd107abd --- /dev/null +++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProviderFactory.java @@ -0,0 +1,125 @@ +/* + * 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.saml; + +import org.keycloak.broker.provider.AbstractIdentityProviderFactory; +import org.keycloak.models.IdentityProviderModel; +import org.picketlink.common.exceptions.ParsingException; +import org.picketlink.common.util.DocumentUtil; +import org.picketlink.identity.federation.core.parsers.saml.SAMLParser; +import org.picketlink.identity.federation.saml.v2.metadata.EntitiesDescriptorType; +import org.picketlink.identity.federation.saml.v2.metadata.EntityDescriptorType; +import org.picketlink.identity.federation.saml.v2.metadata.IDPSSODescriptorType; +import org.picketlink.identity.federation.saml.v2.metadata.KeyDescriptorType; +import org.picketlink.identity.federation.saml.v2.metadata.KeyTypes; +import org.w3c.dom.Element; + +import javax.xml.namespace.QName; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Pedro Igor + */ +public class SAMLIdentityProviderFactory extends AbstractIdentityProviderFactory { + + @Override + public String getName() { + return "SAML v2.0"; + } + + @Override + public SAMLIdentityProvider create(IdentityProviderModel model) { + return new SAMLIdentityProvider(new SAMLIdentityProviderConfig(getId(), model.getId(), model.getName(), model.getConfig())); + } + + @Override + public Map parseConfig(InputStream inputStream) { + try { + Object parsedObject = new SAMLParser().parse(inputStream); + EntityDescriptorType entityType; + + if (EntitiesDescriptorType.class.isInstance(parsedObject)) { + entityType = (EntityDescriptorType) ((EntitiesDescriptorType) parsedObject).getEntityDescriptor().get(0); + } else { + entityType = (EntityDescriptorType) parsedObject; + } + + List choiceType = entityType.getChoiceType(); + + if (!choiceType.isEmpty()) { + EntityDescriptorType.EDTChoiceType edtChoiceType = choiceType.get(0); + List descriptors = edtChoiceType.getDescriptors(); + + if (!descriptors.isEmpty()) { + EntityDescriptorType.EDTDescriptorChoiceType edtDescriptorChoiceType = descriptors.get(0); + IDPSSODescriptorType idpDescriptor = edtDescriptorChoiceType.getIdpDescriptor(); + + if (idpDescriptor != null) { + SAMLIdentityProviderConfig samlIdentityProviderConfig = new SAMLIdentityProviderConfig(); + + samlIdentityProviderConfig.setSingleSignOnServiceUrl(idpDescriptor.getSingleSignOnService().get(0).getLocation().toString()); + samlIdentityProviderConfig.setWantAuthnRequestsSigned(idpDescriptor.isWantAuthnRequestsSigned()); + samlIdentityProviderConfig.setValidateSignature(idpDescriptor.isWantAuthnRequestsSigned()); + + List keyDescriptor = idpDescriptor.getKeyDescriptor(); + String defaultPublicKey = null; + + if (keyDescriptor != null) { + for (KeyDescriptorType keyDescriptorType : keyDescriptor) { + Element keyInfo = keyDescriptorType.getKeyInfo(); + Element x509KeyInfo = DocumentUtil.getChildElement(keyInfo, new QName("dsig", "X509Certificate")); + + if (KeyTypes.SIGNING.equals(keyDescriptorType.getUse())) { + samlIdentityProviderConfig.setSigningPublicKey(x509KeyInfo.getTextContent()); + } else if (KeyTypes.ENCRYPTION.equals(keyDescriptorType.getUse())) { + samlIdentityProviderConfig.setEncryptionPublicKey(x509KeyInfo.getTextContent()); + } else if (keyDescriptorType.getUse() == null) { + defaultPublicKey = x509KeyInfo.getTextContent(); + } + } + } + + if (defaultPublicKey != null) { + if (samlIdentityProviderConfig.getSigningPublicKey() == null) { + samlIdentityProviderConfig.setSigningPublicKey(defaultPublicKey); + } + + if (samlIdentityProviderConfig.getEncryptionPublicKey() == null) { + samlIdentityProviderConfig.setEncryptionPublicKey(defaultPublicKey); + } + } + + return samlIdentityProviderConfig.getConfig(); + } + } + } + } catch (ParsingException pe) { + throw new RuntimeException("Could not parse IdP SAML Metadata", pe); + } + + return new HashMap(); + } + + @Override + public String getId() { + return "saml"; + } +} diff --git a/broker/saml/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory b/broker/saml/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory new file mode 100644 index 0000000000..afd7cd062a --- /dev/null +++ b/broker/saml/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderFactory @@ -0,0 +1 @@ +org.keycloak.broker.saml.SAMLIdentityProviderFactory \ No newline at end of file diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml new file mode 100644 index 0000000000..e8acd713d0 --- /dev/null +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml index 1a1e0a7736..3d8b67189f 100644 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml @@ -3,4 +3,5 @@ + diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index a5069535d0..ddf4396d10 100755 --- a/connections/jpa/src/main/resources/META-INF/persistence.xml +++ b/connections/jpa/src/main/resources/META-INF/persistence.xml @@ -11,12 +11,13 @@ org.keycloak.models.jpa.entities.RequiredCredentialEntity org.keycloak.models.jpa.entities.UserFederationProviderEntity org.keycloak.models.jpa.entities.RoleEntity - org.keycloak.models.jpa.entities.SocialLinkEntity + org.keycloak.models.jpa.entities.FederatedIdentityEntity org.keycloak.models.jpa.entities.UserEntity org.keycloak.models.jpa.entities.UserRequiredActionEntity org.keycloak.models.jpa.entities.UserAttributeEntity org.keycloak.models.jpa.entities.UserRoleMappingEntity org.keycloak.models.jpa.entities.ScopeMappingEntity + org.keycloak.models.jpa.entities.IdentityProviderEntity org.keycloak.models.sessions.jpa.entities.ClientSessionEntity @@ -29,7 +30,7 @@ org.keycloak.events.jpa.EventEntity true - + diff --git a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java index 30fcb5d609..1d0f8ea5d5 100755 --- a/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java +++ b/core/src/main/java/org/keycloak/jose/jws/JWSHeader.java @@ -21,6 +21,9 @@ public class JWSHeader implements Serializable { @JsonProperty("cty") private String contentType; + @JsonProperty("kid") + private String keyId; + public JWSHeader() { } @@ -42,6 +45,9 @@ public class JWSHeader implements Serializable { return contentType; } + public String getKeyId() { + return keyId; + } private static final ObjectMapper mapper = new ObjectMapper(); diff --git a/core/src/main/java/org/keycloak/representations/idm/FederatedIdentityRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/FederatedIdentityRepresentation.java new file mode 100644 index 0000000000..0a316489ef --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/FederatedIdentityRepresentation.java @@ -0,0 +1,35 @@ +package org.keycloak.representations.idm; + +/** + * @author Marek Posolda + */ +public class FederatedIdentityRepresentation { + + protected String identityProvider; + protected String userId; + protected String userName; + + public String getIdentityProvider() { + return identityProvider; + } + + public void setIdentityProvider(String identityProvider) { + this.identityProvider = identityProvider; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java new file mode 100644 index 0000000000..023bd0114f --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/IdentityProviderRepresentation.java @@ -0,0 +1,91 @@ +/* + * 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.representations.idm; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Pedro Igor + */ +public class IdentityProviderRepresentation { + + protected String id; + protected String providerId; + protected String name; + protected boolean enabled = true; + protected boolean updateProfileFirstLogin = true; + protected String groupName; + protected Map config = new HashMap(); + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getProviderId() { + return this.providerId; + } + + public void setProviderId(String providerId) { + this.providerId = providerId; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getConfig() { + return this.config; + } + + public void setConfig(Map config) { + this.config = config; + } + + public String getGroupName() { + return this.groupName; + } + + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isUpdateProfileFirstLogin() { + return this.updateProfileFirstLogin; + } + + public void setUpdateProfileFirstLogin(boolean updateProfileFirstLogin) { + this.updateProfileFirstLogin = updateProfileFirstLogin; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java index b0a0a21114..e5a1173aba 100755 --- a/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/RealmRepresentation.java @@ -25,8 +25,6 @@ public class RealmRepresentation { protected Boolean rememberMe; protected Boolean verifyEmail; protected Boolean resetPasswordAllowed; - protected Boolean social; - protected Boolean updateProfileOnInitialSocialLogin; protected Boolean userCacheEnabled; protected Boolean realmCacheEnabled; @@ -55,7 +53,6 @@ public class RealmRepresentation { protected List applications; protected List oauthClients; protected Map browserSecurityHeaders; - protected Map socialProviders; protected Map smtpServer; protected List userFederationProviders; protected String loginTheme; @@ -65,6 +62,8 @@ public class RealmRepresentation { protected Boolean eventsEnabled; protected Long eventsExpiration; protected List eventsListeners; + private List identityProviders; + private boolean identityFederationEnabled; public String getId() { return id; @@ -294,22 +293,6 @@ public class RealmRepresentation { this.resetPasswordAllowed = resetPassword; } - public Boolean isSocial() { - return social; - } - - public void setSocial(Boolean social) { - this.social = social; - } - - public Boolean isUpdateProfileOnInitialSocialLogin() { - return updateProfileOnInitialSocialLogin; - } - - public void setUpdateProfileOnInitialSocialLogin(Boolean updateProfileOnInitialSocialLogin) { - this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin; - } - public Map getBrowserSecurityHeaders() { return browserSecurityHeaders; } @@ -318,14 +301,6 @@ public class RealmRepresentation { this.browserSecurityHeaders = browserSecurityHeaders; } - public Map getSocialProviders() { - return socialProviders; - } - - public void setSocialProviders(Map socialProviders) { - this.socialProviders = socialProviders; - } - public Map getSmtpServer() { return smtpServer; } @@ -485,4 +460,24 @@ public class RealmRepresentation { public void setUserFederationProviders(List userFederationProviders) { this.userFederationProviders = userFederationProviders; } + + public List getIdentityProviders() { + if (this.identityProviders == null) { + this.identityProviders = new ArrayList(); + } + + return identityProviders; + } + + public void setIdentityProviders(List identityProviders) { + this.identityProviders = identityProviders; + } + + public void addIdentityProvider(IdentityProviderRepresentation identityProviderRepresentation) { + getIdentityProviders().add(identityProviderRepresentation); + } + + public boolean isIdentityFederationEnabled() { + return !getIdentityProviders().isEmpty(); + } } diff --git a/core/src/main/java/org/keycloak/representations/idm/SocialLinkRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/SocialLinkRepresentation.java deleted file mode 100644 index 8203261fed..0000000000 --- a/core/src/main/java/org/keycloak/representations/idm/SocialLinkRepresentation.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.keycloak.representations.idm; - -/** - * @author Marek Posolda - */ -public class SocialLinkRepresentation { - - protected String socialProvider; - protected String socialUserId; - protected String socialUsername; - - public String getSocialProvider() { - return socialProvider; - } - - public void setSocialProvider(String socialProvider) { - this.socialProvider = socialProvider; - } - - public String getSocialUserId() { - return socialUserId; - } - - public void setSocialUserId(String socialUserId) { - this.socialUserId = socialUserId; - } - - public String getSocialUsername() { - return socialUsername; - } - - public void setSocialUsername(String socialUsername) { - this.socialUsername = socialUsername; - } -} diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 45cac16445..8fa1d6aadb 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -24,7 +24,7 @@ public class UserRepresentation { protected Map attributes; protected List credentials; protected List requiredActions; - protected List socialLinks; + protected List federatedIdentities; protected List realmRoles; protected Map> applicationRoles; @@ -139,12 +139,12 @@ public class UserRepresentation { this.requiredActions = requiredActions; } - public List getSocialLinks() { - return socialLinks; + public List getFederatedIdentities() { + return federatedIdentities; } - public void setSocialLinks(List socialLinks) { - this.socialLinks = socialLinks; + public void setFederatedIdentities(List federatedIdentities) { + this.federatedIdentities = federatedIdentities; } public List getRealmRoles() { diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index 0a65c8ffb5..eb1fe4675c 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -82,7 +82,17 @@ ${project.version} - + + + org.keycloak + keycloak-broker-oidc + ${project.version} + + + org.keycloak + keycloak-broker-saml + ${project.version} + org.keycloak keycloak-social-github diff --git a/events/api/src/main/java/org/keycloak/events/Errors.java b/events/api/src/main/java/org/keycloak/events/Errors.java index 3e8c07ceb0..2e7588056f 100755 --- a/events/api/src/main/java/org/keycloak/events/Errors.java +++ b/events/api/src/main/java/org/keycloak/events/Errors.java @@ -34,7 +34,7 @@ public interface Errors { String NOT_ALLOWED = "not_allowed"; - String SOCIAL_PROVIDER_NOT_FOUND = "social_provider_not_found"; + String IDENTITY_PROVIDER_NOT_FOUND = "identity_provider_not_found"; String SOCIAL_ID_IN_USE = "social_id_in_use"; String SSL_REQUIRED = "ssl_required"; diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java index cd624b488f..681c87d4f9 100755 --- a/events/api/src/main/java/org/keycloak/events/EventType.java +++ b/events/api/src/main/java/org/keycloak/events/EventType.java @@ -19,7 +19,7 @@ public enum EventType { REFRESH_TOKEN_ERROR, SOCIAL_LINK, SOCIAL_LINK_ERROR, - REMOVE_SOCIAL_LINK, + REMOVE_FEDERATED_IDENTITY, REMOVE_SOCIAL_LINK_ERROR, UPDATE_EMAIL, diff --git a/examples/basic-auth/basicauthrealm.json b/examples/basic-auth/basicauthrealm.json index d738fd2ca2..f6bac20a5b 100644 --- a/examples/basic-auth/basicauthrealm.json +++ b/examples/basic-auth/basicauthrealm.json @@ -9,8 +9,6 @@ "passwordCredentialGrantAllowed": true, "sslRequired": "external", "registrationAllowed": false, - "social": false, - "updateProfileOnInitialSocialLogin": false, "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password" ], diff --git a/examples/cors/cors-realm.json b/examples/cors/cors-realm.json index cafb7ffe19..ab08ee39e7 100755 --- a/examples/cors/cors-realm.json +++ b/examples/cors/cors-realm.json @@ -8,8 +8,6 @@ "ssoSessionMaxLifespan": 36000, "sslRequired": "external", "registrationAllowed": false, - "social": false, - "updateProfileOnInitialSocialLogin": false, "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password" ], diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json index cc8e02f93c..7cf597a383 100755 --- a/examples/demo-template/testrealm.json +++ b/examples/demo-template/testrealm.json @@ -9,8 +9,6 @@ "passwordCredentialGrantAllowed": true, "sslRequired": "external", "registrationAllowed": false, - "social": false, - "updateProfileOnInitialSocialLogin": false, "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password" ], diff --git a/examples/multi-tenant/tenant1-realm.json b/examples/multi-tenant/tenant1-realm.json index dc41441445..e7a9cc9dfa 100644 --- a/examples/multi-tenant/tenant1-realm.json +++ b/examples/multi-tenant/tenant1-realm.json @@ -7,9 +7,7 @@ "accessCodeLifespanUserAction": 6000, "sslRequired": "external", "registrationAllowed": false, - "social": false, "passwordCredentialGrantAllowed": true, - "updateProfileOnInitialSocialLogin": false, "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "requiredCredentials": [ "password" ], diff --git a/examples/multi-tenant/tenant2-realm.json b/examples/multi-tenant/tenant2-realm.json index e19509d954..d628b731f1 100644 --- a/examples/multi-tenant/tenant2-realm.json +++ b/examples/multi-tenant/tenant2-realm.json @@ -7,7 +7,6 @@ "accessCodeLifespanUserAction": 6000, "sslRequired": "external", "registrationAllowed": false, - "social": false, "passwordCredentialGrantAllowed": true, "updateProfileOnInitialSocialLogin": false, "privateKey": "MIICXQIBAAKBgQDA0oJjgPQJhnVhOo51KauQGfLLreMFu64OJdKXRnfvAQJQTuKNwc5JrR63l/byyW1B6FgclABF818TtLvMCAkn4EuFwQZCZhg3x3+lFGiB/IzC6UAt4Bi0JQrTbdh83/U97GIPegvaDqiqEiQESEkbCZWxM6sh/34hQaAhCaFpMwIDAQABAoGADwFSvEOQuh0IjWRtKZjwjOo4BrmlbRDJ3rf6x2LoemTttSouXzGxx/H87fSZdxNNuU9HbBHoY4ko4POzmZEWhS0gV6UjM7VArc4YjID6Hh2tfU9vCbuuKZrRs7RjxL70b51WxycKc49PQ4JiR3g04punrpq2UzToPrm66zI+ICECQQD2Jauo6cXXoxHR0QychQf4dityZwFXUoR/8oI/YFiu9XwcWgSMwrFKUdWWNKYmrIRNqCBzrGyeiGdaAjsw41T3AkEAyIpn+XL7bek/uLno5/7ULauf2dFI6MEaHJixQJD7S6Tfo/CGuDK93H4K0GAdjgR0LA0tCnB09yyPCd5NmAYKpQJBAO7+BH4s/PsyScr+vs/6GpMTqXuap6KxbBUO0YfXdEPr9mVQwboqDxmp+0esNua1+n+sDlZBw/TpW+/42p/NGmECQF0sOQyjyH+TfGCmN7j6I7ioYZeA7h/9/9TDeK8n7SmDC8kOanlQUfgMs5eG4JRoK1WANaoA/8cLc9XA7EoynGUCQQDx/Gjg6qyWheVujxjKufH1XkqDNiQHClDRM1ntChCmGq/RmpVmce+mYeOYZ9eofv7UJUCBdamllRlB+056Ld2h", diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index c9ba3dd806..7c63a62269 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -13,7 +13,7 @@ import org.keycloak.models.OAuthClientModel; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; -import org.keycloak.models.SocialLinkModel; +import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.ModelToRepresentation; @@ -25,7 +25,7 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.RolesRepresentation; import org.keycloak.representations.idm.ScopeMappingRepresentation; -import org.keycloak.representations.idm.SocialLinkRepresentation; +import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.UserRepresentation; import java.io.IOException; @@ -253,14 +253,14 @@ public class ExportUtils { UserRepresentation userRep = ModelToRepresentation.toRepresentation(user); // Social links - Set socialLinks = session.users().getSocialLinks(user, realm); - List socialLinkReps = new ArrayList(); - for (SocialLinkModel socialLink : socialLinks) { - SocialLinkRepresentation socialLinkRep = exportSocialLink(socialLink); + Set socialLinks = session.users().getFederatedIdentities(user, realm); + List socialLinkReps = new ArrayList(); + for (FederatedIdentityModel socialLink : socialLinks) { + FederatedIdentityRepresentation socialLinkRep = exportSocialLink(socialLink); socialLinkReps.add(socialLinkRep); } if (socialLinkReps.size() > 0) { - userRep.setSocialLinks(socialLinkReps); + userRep.setFederatedIdentities(socialLinkReps); } // Role mappings @@ -303,11 +303,11 @@ public class ExportUtils { return userRep; } - public static SocialLinkRepresentation exportSocialLink(SocialLinkModel socialLink) { - SocialLinkRepresentation socialLinkRep = new SocialLinkRepresentation(); - socialLinkRep.setSocialProvider(socialLink.getSocialProvider()); - socialLinkRep.setSocialUserId(socialLink.getSocialUserId()); - socialLinkRep.setSocialUsername(socialLink.getSocialUsername()); + public static FederatedIdentityRepresentation exportSocialLink(FederatedIdentityModel socialLink) { + FederatedIdentityRepresentation socialLinkRep = new FederatedIdentityRepresentation(); + socialLinkRep.setIdentityProvider(socialLink.getIdentityProvider()); + socialLinkRep.setUserId(socialLink.getUserId()); + socialLinkRep.setUserName(socialLink.getUserName()); return socialLinkRep; } diff --git a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java index dc7e3c0f06..d316574205 100644 --- a/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java +++ b/forms/account-api/src/main/java/org/keycloak/account/AccountPages.java @@ -5,6 +5,6 @@ package org.keycloak.account; */ public enum AccountPages { - ACCOUNT, PASSWORD, TOTP, SOCIAL, LOG, SESSIONS; + ACCOUNT, PASSWORD, TOTP, FEDERATED_IDENTITY, LOG, SESSIONS; } diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java index 6d777478dd..368b4591e3 100755 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java @@ -4,7 +4,7 @@ import org.jboss.logging.Logger; import org.keycloak.account.AccountPages; import org.keycloak.account.AccountProvider; import org.keycloak.account.freemarker.model.AccountBean; -import org.keycloak.account.freemarker.model.AccountSocialBean; +import org.keycloak.account.freemarker.model.AccountFederatedIdentityBean; import org.keycloak.account.freemarker.model.FeaturesBean; import org.keycloak.account.freemarker.model.LogBean; import org.keycloak.account.freemarker.model.MessageBean; @@ -51,7 +51,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { private List events; private String stateChecker; private List sessions; - private boolean socialEnabled; + private boolean identityProviderEnabled; private boolean eventsEnabled; private boolean passwordUpdateSupported; private boolean passwordSet; @@ -124,7 +124,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { attributes.put("url", new UrlBean(realm, theme, baseUri, baseQueryUri, uriInfo.getRequestUri(), stateChecker)); - attributes.put("features", new FeaturesBean(socialEnabled, eventsEnabled, passwordUpdateSupported)); + attributes.put("features", new FeaturesBean(identityProviderEnabled, eventsEnabled, passwordUpdateSupported)); switch (page) { case ACCOUNT: @@ -133,8 +133,8 @@ public class FreeMarkerAccountProvider implements AccountProvider { case TOTP: attributes.put("totp", new TotpBean(realm, user, baseUri)); break; - case SOCIAL: - attributes.put("social", new AccountSocialBean(session, realm, user, uriInfo.getBaseUri(), stateChecker)); + case FEDERATED_IDENTITY: + attributes.put("federatedIdentity", new AccountFederatedIdentityBean(session, realm, user, uriInfo.getBaseUri(), stateChecker)); break; case LOG: attributes.put("log", new LogBean(events)); @@ -232,8 +232,8 @@ public class FreeMarkerAccountProvider implements AccountProvider { } @Override - public AccountProvider setFeatures(boolean socialEnabled, boolean eventsEnabled, boolean passwordUpdateSupported) { - this.socialEnabled = socialEnabled; + public AccountProvider setFeatures(boolean identityProviderEnabled, boolean eventsEnabled, boolean passwordUpdateSupported) { + this.identityProviderEnabled = identityProviderEnabled; this.eventsEnabled = eventsEnabled; this.passwordUpdateSupported = passwordUpdateSupported; return this; diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java index 80fc179d7d..1d33f8aa8e 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/Templates.java @@ -15,8 +15,8 @@ public class Templates { return "password.ftl"; case TOTP: return "totp.ftl"; - case SOCIAL: - return "social.ftl"; + case FEDERATED_IDENTITY: + return "federatedIdentity.ftl"; case LOG: return "log.ftl"; case SESSIONS: diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountFederatedIdentityBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountFederatedIdentityBean.java new file mode 100755 index 0000000000..66f2d8d22a --- /dev/null +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountFederatedIdentityBean.java @@ -0,0 +1,114 @@ +package org.keycloak.account.freemarker.model; + +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.resources.AccountService; +import org.keycloak.services.resources.flows.Urls; + +import javax.ws.rs.core.UriBuilder; +import java.net.URI; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +/** + * @author Marek Posolda + */ +public class AccountFederatedIdentityBean { + + private final List identities; + private final boolean removeLinkPossible; + private final KeycloakSession session; + + public AccountFederatedIdentityBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri, String stateChecker) { + this.session = session; + URI accountIdentityUpdateUri = Urls.accountFederatedIdentityUpdate(baseUri, realm.getName()); + this.identities = new LinkedList(); + + List identityProviders = realm.getIdentityProviders(); + Set identities = session.users().getFederatedIdentities(user, realm); + + int availableIdentities = 0; + if (identityProviders != null && !identityProviders.isEmpty()) { + for (IdentityProviderModel provider : identityProviders) { + String providerId = provider.getId(); + + FederatedIdentityModel identity = getIdentity(identities, providerId); + + if (identity != null) { + availableIdentities++; + } + + String action = identity != null ? "remove" : "add"; + String actionUrl = UriBuilder.fromUri(accountIdentityUpdateUri) + .queryParam("action", action) + .queryParam("provider_id", providerId) + .queryParam("stateChecker", stateChecker) + .build().toString(); + + FederatedIdentityEntry entry = new FederatedIdentityEntry(identity, provider.getName(), actionUrl); + this.identities.add(entry); + } + } + + // Removing last social provider is not possible if you don't have other possibility to authenticate + this.removeLinkPossible = availableIdentities > 1 || user.getFederationLink() != null || AccountService.isPasswordSet(user); + } + + private FederatedIdentityModel getIdentity(Set identities, String providerId) { + for (FederatedIdentityModel link : identities) { + if (providerId.equals(link.getIdentityProvider())) { + return link; + } + } + return null; + } + + public List getIdentities() { + return identities; + } + + public boolean isRemoveLinkPossible() { + return removeLinkPossible; + } + + public class FederatedIdentityEntry { + + private FederatedIdentityModel federatedIdentityModel; + private final String providerName; + private final String actionUrl; + + public FederatedIdentityEntry(FederatedIdentityModel federatedIdentityModel, String providerName, String actionUrl) { + this.federatedIdentityModel = federatedIdentityModel; + this.providerName = providerName; + this.actionUrl = actionUrl; + } + + public String getProviderId() { + return federatedIdentityModel != null ? federatedIdentityModel.getIdentityProvider() : null; + } + + public String getProviderName() { + return providerName; + } + + public String getUserId() { + return federatedIdentityModel != null ? federatedIdentityModel.getUserId() : null; + } + + public String getUserName() { + return federatedIdentityModel != null ? federatedIdentityModel.getUserName() : null; + } + + public boolean isConnected() { + return federatedIdentityModel != null; + } + + public String getActionUrl() { + return actionUrl; + } + } +} diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java deleted file mode 100755 index 3a7650c549..0000000000 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/AccountSocialBean.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.keycloak.account.freemarker.model; - -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.RealmModel; -import org.keycloak.models.SocialLinkModel; -import org.keycloak.models.UserModel; -import org.keycloak.services.resources.AccountService; -import org.keycloak.services.resources.flows.Urls; -import org.keycloak.social.SocialLoader; -import org.keycloak.social.SocialProvider; - -import javax.ws.rs.core.UriBuilder; -import java.net.URI; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * @author Marek Posolda - */ -public class AccountSocialBean { - - private final List socialLinks; - private final boolean removeLinkPossible; - private final KeycloakSession session; - - public AccountSocialBean(KeycloakSession session, RealmModel realm, UserModel user, URI baseUri, String stateChecker) { - this.session = session; - URI accountSocialUpdateUri = Urls.accountSocialUpdate(baseUri, realm.getName()); - this.socialLinks = new LinkedList(); - - Map socialConfig = realm.getSocialConfig(); - Set userSocialLinks = session.users().getSocialLinks(user, realm); - - int availableLinks = 0; - if (socialConfig != null && !socialConfig.isEmpty()) { - for (SocialProvider provider : SocialLoader.load()) { - String socialProviderId = provider.getId(); - if (socialConfig.containsKey(socialProviderId + ".key")) { - SocialLinkModel socialLink = getSocialLink(userSocialLinks, socialProviderId); - - if (socialLink != null) { - availableLinks++; - } - String action = socialLink != null ? "remove" : "add"; - String actionUrl = UriBuilder.fromUri(accountSocialUpdateUri) - .queryParam("action", action) - .queryParam("provider_id", socialProviderId) - .queryParam("stateChecker", stateChecker) - .build().toString(); - - SocialLinkEntry entry = new SocialLinkEntry(socialLink, provider.getName(), actionUrl); - this.socialLinks.add(entry); - } - } - } - - // Removing last social provider is not possible if you don't have other possibility to authenticate - this.removeLinkPossible = availableLinks > 1 || user.getFederationLink() != null || AccountService.isPasswordSet(user); - } - - private SocialLinkModel getSocialLink(Set userSocialLinks, String socialProviderId) { - for (SocialLinkModel link : userSocialLinks) { - if (socialProviderId.equals(link.getSocialProvider())) { - return link; - } - } - return null; - } - - public List getLinks() { - return socialLinks; - } - - public boolean isRemoveLinkPossible() { - return removeLinkPossible; - } - - public class SocialLinkEntry { - - private SocialLinkModel link; - private final String providerName; - private final String actionUrl; - - public SocialLinkEntry(SocialLinkModel link, String providerName, String actionUrl) { - this.link = link; - this.providerName = providerName; - this.actionUrl = actionUrl; - } - - public String getProviderId() { - return link != null ? link.getSocialProvider() : null; - } - - public String getProviderName() { - return providerName; - } - - public String getSocialUserId() { - return link != null ? link.getSocialUserId() : null; - } - - public String getSocialUsername() { - return link != null ? link.getSocialUsername() : null; - } - - public boolean isConnected() { - return link != null; - } - - public String getActionUrl() { - return actionUrl; - } - } -} diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java index 06e99eb001..b3b96d6388 100644 --- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java +++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/FeaturesBean.java @@ -5,18 +5,18 @@ package org.keycloak.account.freemarker.model; */ public class FeaturesBean { - private final boolean social; + private final boolean identityFederation; private final boolean log; private final boolean passwordUpdateSupported; - public FeaturesBean(boolean social, boolean log, boolean passwordUpdateSupported) { - this.social = social; + public FeaturesBean(boolean identityFederation, boolean log, boolean passwordUpdateSupported) { + this.identityFederation = identityFederation; this.log = log; this.passwordUpdateSupported = passwordUpdateSupported; } - public boolean isSocial() { - return social; + public boolean isIdentityFederation() { + return identityFederation; } public boolean isLog() { diff --git a/forms/common-themes/src/main/resources/theme/account/base/social.ftl b/forms/common-themes/src/main/resources/theme/account/base/federatedIdentity.ftl similarity index 53% rename from forms/common-themes/src/main/resources/theme/account/base/social.ftl rename to forms/common-themes/src/main/resources/theme/account/base/federatedIdentity.ftl index 2bba80588e..a4812a1810 100755 --- a/forms/common-themes/src/main/resources/theme/account/base/social.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/federatedIdentity.ftl @@ -3,26 +3,26 @@
-

Social Accounts

+

Federated Identities

- <#list social.links as socialLink> + <#list federatedIdentity.identities as identity>
- +
- +
- <#if socialLink.connected> - <#if social.removeLinkPossible> - Remove ${socialLink.providerName!} + <#if identity.connected> + <#if federatedIdentity.removeLinkPossible> + Remove ${identity.providerName!} <#else> - Add ${socialLink.providerName!} + Add ${identity.providerName!}
diff --git a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties index 6feedf537d..e49a938d89 100755 --- a/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties +++ b/forms/common-themes/src/main/resources/theme/account/base/messages/messages.properties @@ -29,13 +29,13 @@ successTotpRemoved=Mobile authenticator removed. accountUpdated=Your account has been updated accountPasswordUpdated=Your password has been updated -missingSocialProvider=Social provider not specified -invalidSocialAction=Invalid or missing action -socialProviderNotFound=Specified social provider not found -socialLinkNotActive=This social link is not active anymore -socialRemovingLastProvider=You can't remove last social provider as you don't have password -socialRedirectError=Failed to redirect to social provider -socialProviderRemoved=Social provider removed successfully +missingIdentityProvider=Identity provider not specified +invalidFederatedIdentityAction=Invalid or missing action +identityProviderNotFound=Specified identity provider not found +federatedIdentityLinkNotActive=This identity is not active anymore +federatedIdentityRemovingLastProvider=You can't remove last federated identity as you don't have password +identityProviderRedirectError=Failed to redirect to identity provider +identityProviderRemoved=Identity provider removed successfully accountDisabled=Account is disabled, contact admin\ accountTemporarilyDisabled=Account is temporarily disabled, contact admin or try again later diff --git a/forms/common-themes/src/main/resources/theme/account/base/template.ftl b/forms/common-themes/src/main/resources/theme/account/base/template.ftl index 49040df65b..11dc877bf4 100644 --- a/forms/common-themes/src/main/resources/theme/account/base/template.ftl +++ b/forms/common-themes/src/main/resources/theme/account/base/template.ftl @@ -42,7 +42,7 @@
  • Account
  • <#if features.passwordUpdateSupported>
  • Password
  • Authenticator
  • - <#if features.social>
  • Social
  • + <#if features.identityFederation>
  • Federated Identity
  • Sessions
  • <#if features.log>
  • Log
  • diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js index 3198abf4b1..d3cfbaf833 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/app.js @@ -147,17 +147,41 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'RealmKeysDetailCtrl' }) - .when('/realms/:realm/social-settings', { - templateUrl : 'partials/realm-social.html', + .when('/realms/:realm/identity-provider-settings', { + templateUrl : 'partials/realm-identity-provider.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); }, serverInfo : function(ServerInfoLoader) { return ServerInfoLoader(); + }, + instance : function(IdentityProviderLoader) { + return {}; + }, + providerFactory : function(IdentityProviderFactoryLoader) { + return IdentityProviderFactoryLoader(); } }, - controller : 'RealmSocialCtrl' + controller : 'RealmIdentityProviderCtrl' + }) + .when('/realms/:realm/identity-provider-settings/provider/:provider_id/:id', { + templateUrl : function(params){ return 'partials/realm-identity-provider-' + params.provider_id + '.html'; }, + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + }, + serverInfo : function(ServerInfoLoader) { + return ServerInfoLoader(); + }, + instance : function(IdentityProviderLoader) { + return IdentityProviderLoader(); + }, + providerFactory : function(IdentityProviderFactoryLoader) { + return IdentityProviderFactoryLoader(); + } + }, + controller : 'RealmIdentityProviderCtrl' }) .when('/realms/:realm/default-roles', { templateUrl : 'partials/realm-default-roles.html', @@ -282,8 +306,8 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'UserSessionsCtrl' }) - .when('/realms/:realm/users/:user/social-links', { - templateUrl : 'partials/user-social-links.html', + .when('/realms/:realm/users/:user/federated-identity', { + templateUrl : 'partials/user-federated-identity.html', resolve : { realm : function(RealmLoader) { return RealmLoader(); @@ -291,11 +315,11 @@ module.config([ '$routeProvider', function($routeProvider) { user : function(UserLoader) { return UserLoader(); }, - socialLinks : function(UserSocialLinksLoader) { - return UserSocialLinksLoader(); + federatedIdentities : function(UserFederatedIdentityLoader) { + return UserFederatedIdentityLoader(); } }, - controller : 'UserSocialCtrl' + controller : 'UserFederatedIdentityCtrl' }) .when('/realms/:realm/users', { templateUrl : 'partials/user-list.html', diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js index 5a1c85d35c..8e9573d81f 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/realm.js @@ -258,7 +258,6 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser $scope.realm = angular.copy(realm); } - $scope.social = $scope.realm.social; $scope.registrationAllowed = $scope.realm.registrationAllowed; var oldCopy = angular.copy($scope.realm); @@ -287,7 +286,6 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser } $location.url("/realms/" + realmCopy.realm); Notifications.success("The realm has been created."); - $scope.social = $scope.realm.social; $scope.registrationAllowed = $scope.realm.registrationAllowed; }); }); @@ -307,7 +305,6 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser }); $location.url("/realms/" + realmCopy.realm); Notifications.success("Your changes have been saved to the realm."); - $scope.social = $scope.realm.social; $scope.registrationAllowed = $scope.realm.registrationAllowed; }); } @@ -337,7 +334,6 @@ module.controller('RealmDetailCtrl', function($scope, Current, Realm, realm, ser function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, url) { $scope.realm = angular.copy(realm); $scope.serverInfo = serverInfo; - $scope.social = $scope.realm.social; $scope.registrationAllowed = $scope.realm.registrationAllowed; var oldCopy = angular.copy($scope.realm); @@ -367,7 +363,6 @@ function genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $l }); $location.url(url); Notifications.success("Your changes have been saved to the realm."); - $scope.social = $scope.realm.social; $scope.registrationAllowed = $scope.realm.registrationAllowed; }); }; @@ -617,65 +612,156 @@ module.controller('RealmDefaultRolesCtrl', function ($scope, Realm, realm, appli }); -module.controller('RealmSocialCtrl', function($scope, realm, Realm, serverInfo, $location, Notifications) { - console.log('RealmSocialCtrl'); +module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload, realm, instance, providerFactory, IdentityProvider, serverInfo, $location, Notifications) { + console.log('RealmIdentityProviderCtrl'); $scope.realm = angular.copy(realm); + + $scope.hidePassword = true; + + $scope.getBoolean = function(value) { + if (value == 'true') { + return true; + } else if (value == 'false') { + return false; + } else { + return value; + } + } + + if (instance && instance.id) { + $scope.identityProvider = angular.copy(instance); + + // fixme: this is a hack to make onofswith work and recognize string representation of boolean values + $scope.identityProvider.config.validateSignature = $scope.getBoolean($scope.identityProvider.config.validateSignature); + $scope.identityProvider.config.forceAuthn = $scope.getBoolean($scope.identityProvider.config.forceAuthn); + $scope.newIdentityProvider = false; + } else { + $scope.identityProvider = {}; + $scope.identityProvider.id = providerFactory.id; + $scope.identityProvider.providerId = providerFactory.id; + $scope.identityProvider.name = providerFactory.name; + $scope.identityProvider.enabled = true; + $scope.identityProvider.updateProfileFirstLogin = true; + $scope.newIdentityProvider = true; + } + $scope.serverInfo = serverInfo; - $scope.allProviders = serverInfo.socialProviders; - $scope.configuredProviders = []; + $scope.allProviders = angular.copy(serverInfo.identityProviders); - $scope.$watch('realm.socialProviders', function(socialProviders) { - $scope.configuredProviders = []; - for (var providerConfig in socialProviders) { - var i = providerConfig.split('.'); - if (i.length == 2 && i[1] == 'key') { - $scope.configuredProviders.push(i[0]); - } - } - }, true); + $scope.configuredProviders = angular.copy(realm.identityProviders); - var oldCopy = angular.copy($scope.realm); - $scope.changed = false; - $scope.callbackUrl = $location.absUrl().replace(/\/admin.*/, "/social/callback"); + $scope.files = []; + $scope.importFile = false; - $scope.addProvider = function(pId) { - if (!$scope.realm.socialProviders) { - $scope.realm.socialProviders = {}; + $scope.onFileSelect = function($files) { + $scope.importFile = true; + $scope.files = $files; + }; + + $scope.clearFileSelect = function() { + $scope.importFile = false; + $scope.files = null; + } + + $scope.uploadFile = function() { + //$files: an array of files selected, each file has name, size, and type. + for (var i = 0; i < $scope.files.length; i++) { + var $file = $scope.files[i]; + $scope.upload = $upload.upload({ + url: authUrl + '/admin/realms/' + realm.realm + '/identity-provider/', + // method: POST or PUT, + // headers: {'headerKey': 'headerValue'}, withCredential: true, + data: $scope.identityProvider, + file: $file + /* set file formData name for 'Content-Desposition' header. Default: 'file' */ + //fileFormDataName: myFile, + /* customize how data is added to formData. See #40#issuecomment-28612000 for example */ + //formDataAppender: function(formData, key, val){} + }).progress(function(evt) { + console.log('percent: ' + parseInt(100.0 * evt.loaded / evt.total)); + }).success(function(data, status, headers) { + $location.url("/realms/" + realm.realm + "/identity-provider-settings"); + Notifications.success("The " + $scope.identityProvider.name + " provider has been created."); + }).error(function() { + Notifications.error("The file can not be uploaded. Please verify the file."); + }); } - - $scope.realm.socialProviders[pId + ".key"] = ""; - $scope.realm.socialProviders[pId + ".secret"] = ""; }; - $scope.removeProvider = function(pId) { - delete $scope.realm.socialProviders[pId+".key"]; - delete $scope.realm.socialProviders[pId+".secret"]; - }; + $scope.$watch('configuredProviders', function(configuredProviders) { + if (configuredProviders) { + $scope.configuredProviders = angular.copy(configuredProviders); - $scope.$watch('realm', function() { - if (!angular.equals($scope.realm, oldCopy)) { - $scope.changed = true; + for (var j = 0; j < configuredProviders.length; j++) { + var configProvidedId = configuredProviders[j].providerId; + + for (var i in $scope.allProviders) { + var provider = $scope.allProviders[i]; + + if (provider.groupName == 'Social' && (provider.id == configProvidedId)) { + $scope.allProviders.splice(i, 1); + break; + } + } + } } }, true); - $scope.save = function() { - var realmCopy = angular.copy($scope.realm); - realmCopy.social = true; - $scope.changed = false; - Realm.update(realmCopy, function () { - $location.url("/realms/" + realm.realm + "/social-settings"); - Notifications.success("The changes have been saved to the realm."); - oldCopy = realmCopy; + $scope.callbackUrl = $location.absUrl().replace(/\/admin.*/, "/broker/") + realm.realm + "/" ; + + $scope.addProvider = function(provider) { + $location.url("/realms/" + realm.realm + "/identity-provider-settings/provider/" + provider.id + "/" + provider.id); + }; + + $scope.remove = function() { + IdentityProvider.delete({ + realm: $scope.realm.realm, + id: $scope.identityProvider.id + }, $scope.identityProvider, function () { + $scope.changed = false; + $location.url("/realms/" + realm.realm + "/identity-provider-settings"); + Notifications.success("The " + $scope.identityProvider.name + " provider has been deleted."); }); }; - $scope.reset = function() { - $scope.realm = angular.copy(oldCopy); - $scope.changed = false; + $scope.save = function() { + if ($scope.newIdentityProvider) { + IdentityProvider.create({ + realm: $scope.realm.realm + }, $scope.identityProvider, function () { + $location.url("/realms/" + realm.realm + "/identity-provider-settings"); + Notifications.success("The " + $scope.identityProvider.name + " provider has been created."); + }); + } else { + IdentityProvider.update({ + realm: $scope.realm.realm + }, $scope.identityProvider, function () { + $location.url("/realms/" + realm.realm + "/identity-provider-settings"); + Notifications.success("The " + $scope.identityProvider.name + " provider has been update."); + }); + } }; + $scope.reset = function() { + $scope.identityProvider = {}; + $scope.configuredProviders = angular.copy($scope.realm.identityProviders); + }; + + $scope.showPassword = function(flag) { + $scope.hidePassword = flag; + }; + + $scope.getBoolean = function(value) { + if (value == 'true') { + return true; + } else if (value == 'false') { + return false; + } else { + return value; + } + } }); module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http, $location, $route, Dialog, Notifications, TimeUnit) { diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js index 8ba2d5a179..a7f8a5d517 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/controllers/users.js @@ -129,11 +129,11 @@ module.controller('UserSessionsCtrl', function($scope, realm, user, sessions, Us } }); -module.controller('UserSocialCtrl', function($scope, realm, user, socialLinks) { +module.controller('UserFederatedIdentityCtrl', function($scope, realm, user, federatedIdentities) { $scope.realm = realm; $scope.user = user; - $scope.socialLinks = socialLinks; - console.log('showing social links of user'); + $scope.federatedIdentities = federatedIdentities; + console.log('showing federated identities of user'); }); diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js index 455161585b..cb467f4164 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/loaders.js @@ -125,8 +125,8 @@ module.factory('UserSessionsLoader', function(Loader, UserSessions, $route, $q) }); }); -module.factory('UserSocialLinksLoader', function(Loader, UserSocialLinks, $route, $q) { - return Loader.query(UserSocialLinks, function() { +module.factory('UserFederatedIdentityLoader', function(Loader, UserFederatedIdentity, $route, $q) { + return Loader.query(UserFederatedIdentity, function() { return { realm : $route.current.params.realm, user : $route.current.params.user @@ -277,3 +277,21 @@ module.factory('OAuthClientInstallationLoader', function(Loader, OAuthClientInst } }); }); + +module.factory('IdentityProviderLoader', function(Loader, IdentityProvider, $route, $q) { + return Loader.get(IdentityProvider, function () { + return { + realm: $route.current.params.realm, + id: $route.current.params.id + } + }); +}); + +module.factory('IdentityProviderFactoryLoader', function(Loader, IdentityProviderFactory, $route, $q) { + return Loader.get(IdentityProviderFactory, function () { + return { + realm: $route.current.params.realm, + provider_id: $route.current.params.provider_id + } + }); +}); \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js index d47e3abbce..47af934c03 100755 --- a/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/js/services.js @@ -248,8 +248,8 @@ module.factory('UserLogout', function($resource) { user : '@user' }); }); -module.factory('UserSocialLinks', function($resource) { - return $resource(authUrl + '/admin/realms/:realm/users/:user/social-links', { +module.factory('UserFederatedIdentity', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/users/:user/federated-identity', { realm : '@realm', user : '@user' }); @@ -1052,4 +1052,27 @@ module.factory('PasswordPolicy', function() { }; return p; +}); + +module.factory('IdentityProvider', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/identity-provider/:id', { + realm : '@realm' + }, { + create : { + method : 'POST' + }, + delete : { + method : 'DELETE' + }, + update: { + method : 'PUT' + } + }); +}); + +module.factory('IdentityProviderFactory', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/identity-provider/providers/:provider_id', { + realm : '@realm', + provider_id : '@provider_id' + }); }); \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-facebook.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-facebook.html new file mode 100755 index 0000000000..d565cfb7eb --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-facebook.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-github.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-github.html new file mode 100755 index 0000000000..d565cfb7eb --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-github.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-google.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-google.html new file mode 100755 index 0000000000..d565cfb7eb --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-google.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-oidc.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-oidc.html new file mode 100755 index 0000000000..544ea0aa61 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-oidc.html @@ -0,0 +1,117 @@ +
    +
    + +

    +
    + +

    {{identityProvider.name}} Provider Settings

    +

    * Required fields

    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + + + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + + +
    + +
    +
    + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html new file mode 100755 index 0000000000..6d4ea91008 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/admin/base/resources/partials/realm-identity-provider-saml.html @@ -0,0 +1,100 @@ +
    +
    + +

    +
    + +

    {{identityProvider.name}} Provider Settings

    +

    * Required fields

    +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + + + {{files[0].name}} + +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +