keycloak-aplcache

Merge pull request #1082 from patriot1burke/master broker

3/23/2015 10:39:37 PM

Changes

Details

diff --git a/broker/oidc/pom.xml b/broker/oidc/pom.xml
index 478cb47..5bd4d54 100755
--- a/broker/oidc/pom.xml
+++ b/broker/oidc/pom.xml
@@ -17,6 +17,11 @@
     <dependencies>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-broker-core</artifactId>
             <version>${project.version}</version>
         </dependency>
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index f2245e3..274171c 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -33,9 +33,11 @@ import org.keycloak.events.EventType;
 import org.keycloak.models.FederatedIdentityModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
+import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.services.managers.EventsManager;
 import org.keycloak.services.messages.Messages;
 import org.keycloak.services.resources.flows.Flows;
+import org.keycloak.util.JsonSerialization;
 
 import javax.ws.rs.GET;
 import javax.ws.rs.QueryParam;
@@ -47,6 +49,7 @@ import javax.ws.rs.core.UriInfo;
 import java.io.IOException;
 import java.net.URI;
 import java.util.HashMap;
+import java.util.Map;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -57,6 +60,9 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
     protected static final Logger logger = Logger.getLogger(AbstractOAuth2IdentityProvider.class);
 
     public static final String OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";
+    public static final String FEDERATED_ACCESS_TOKEN = "FEDERATED_ACCESS_TOKEN";
+    public static final String FEDERATED_REFRESH_TOKEN = "FEDERATED_REFRESH_TOKEN";
+    public static final String FEDERATED_TOKEN_EXPIRATION = "FEDERATED_TOKEN_EXPIRATION";
     protected static ObjectMapper mapper = new ObjectMapper();
 
     public static final String OAUTH2_PARAMETER_ACCESS_TOKEN = "access_token";
@@ -69,7 +75,6 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
     public static final String OAUTH2_PARAMETER_CLIENT_SECRET = "client_secret";
     public static final String OAUTH2_PARAMETER_GRANT_TYPE = "grant_type";
 
-    protected AuthenticationCallback callback;
 
     public AbstractOAuth2IdentityProvider(C config) {
         super(config);
@@ -81,8 +86,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
 
     @Override
     public Object callback(RealmModel realm, AuthenticationCallback callback) {
-        this.callback = callback;
-        return new Endpoint(realm);
+        return new Endpoint(callback, realm);
     }
 
     @Override
@@ -124,8 +128,17 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         return null;
     }
 
-    protected FederatedIdentity getFederatedIdentity(String response) {
-        String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
+    protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, String response) {
+        AccessTokenResponse tokenResponse = null;
+        try {
+            tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
+        } catch (IOException e) {
+            throw new IdentityBrokerException("Could not decode access token response.", e);
+        }
+        String accessToken = tokenResponse.getToken();
+        notes.put(FEDERATED_ACCESS_TOKEN, accessToken);
+        notes.put(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
+        notes.put(FEDERATED_TOKEN_EXPIRATION, Long.toString(tokenResponse.getExpiresIn()));
 
         if (accessToken == null) {
             throw new IdentityBrokerException("No access token from server.");
@@ -164,6 +177,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
     protected abstract String getDefaultScopes();
 
     protected class Endpoint {
+        protected AuthenticationCallback callback;
         protected RealmModel realm;
 
         @Context
@@ -178,7 +192,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
         @Context
         protected UriInfo uriInfo;
 
-        public Endpoint(RealmModel realm) {
+        public Endpoint(AuthenticationCallback callback, RealmModel realm) {
+            this.callback = callback;
             this.realm = realm;
         }
 
@@ -205,13 +220,14 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
                             .param(OAUTH2_PARAMETER_REDIRECT_URI, uriInfo.getAbsolutePath().toString())
                             .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE).asString();
 
-                    FederatedIdentity federatedIdentity = getFederatedIdentity(response);
+                    HashMap<String, String> userNotes = new HashMap<String, String>();
+                    FederatedIdentity federatedIdentity = getFederatedIdentity(userNotes, response);
 
                     if (getConfig().isStoreToken()) {
                         federatedIdentity.setToken(response);
                     }
 
-                    return callback.authenticated(new HashMap<String, String>(), getConfig(), federatedIdentity, state);
+                    return callback.authenticated(userNotes, getConfig(), federatedIdentity, state);
                 }
             } catch (Exception e) {
                 logger.error("Failed to make identity provider oauth callback", e);
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
old mode 100644
new mode 100755
index 2c43d61..6da5e59
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -22,10 +22,32 @@ import org.keycloak.broker.oidc.util.SimpleHttp;
 import org.keycloak.broker.provider.AuthenticationRequest;
 import org.keycloak.broker.provider.FederatedIdentity;
 import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.broker.provider.IdentityProvider;
+import org.keycloak.events.Errors;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
 import org.keycloak.jose.jws.JWSInput;
-
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.IDToken;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.services.managers.EventsManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.services.resources.IdentityBrokerService;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.services.resources.flows.Flows;
+import org.keycloak.util.JsonSerialization;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
 import java.io.IOException;
+import java.util.Map;
 
 /**
  * @author Pedro Igor
@@ -33,8 +55,8 @@ import java.io.IOException;
 public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIdentityProviderConfig> {
 
     public static final String OAUTH2_PARAMETER_PROMPT = "prompt";
-    public static final String OIDC_PARAMETER_ID_TOKEN = "id_token";
     public static final String SCOPE_OPENID = "openid";
+    public static final String FEDERATED_ID_TOKEN = "FEDERATED_ID_TOKEN";
 
     public OIDCIdentityProvider(OIDCIdentityProviderConfig config) {
         super(config);
@@ -47,6 +69,54 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     }
 
     @Override
+    public Object callback(RealmModel realm, AuthenticationCallback callback) {
+        return new OIDCEndpoint(callback, realm);
+    }
+
+    protected class OIDCEndpoint extends Endpoint {
+        public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm) {
+            super(callback, realm);
+        }
+
+        @GET
+        @Path("logout_response")
+        public Response logoutResponse(@Context UriInfo uriInfo,
+                                       @QueryParam("state") String state) {
+            UserSessionModel userSession = session.sessions().getUserSession(realm, state);
+            if (userSession == null) {
+                logger.error("no valid user session");
+                EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder();
+                event.event(EventType.LOGOUT);
+                event.error(Errors.USER_SESSION_NOT_FOUND);
+                return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.IDENTITY_PROVIDER_UNEXPECTED_ERROR);
+            }
+            if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
+                logger.error("usersession in different state");
+                EventBuilder event = new EventsManager(realm, session, clientConnection).createEventBuilder();
+                event.event(EventType.LOGOUT);
+                event.error(Errors.USER_SESSION_NOT_FOUND);
+                return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.SESSION_NOT_ACTIVE);
+            }
+            return AuthenticationManager.finishBrowserLogout(session, realm, userSession, uriInfo, clientConnection, headers);
+        }
+    }
+
+    @Override
+    public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
+        if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null;
+        UriBuilder logoutUri = UriBuilder.fromUri(getConfig().getLogoutUrl())
+                                         .queryParam("state", userSession.getId());
+        String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
+        if (idToken != null) logoutUri.queryParam("id_token_hint", idToken);
+        String redirect = RealmsResource.brokerUrl(uriInfo)
+                                        .path(IdentityBrokerService.class, "getEndpoint")
+                                        .path(OIDCEndpoint.class, "logoutResponse")
+                                        .build(realm.getName(), getConfig().getAlias()).toString();
+        logoutUri.queryParam("post_logout_redirect_uri", redirect);
+        return Response.status(302).location(logoutUri.build()).build();
+    }
+
+    @Override
     protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
         UriBuilder authorizationUrl = super.createAuthorizationUrl(request);
         String prompt = getConfig().getPrompt();
@@ -59,26 +129,45 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
     }
 
     @Override
-    protected FederatedIdentity getFederatedIdentity(String response) {
-        String accessToken = extractTokenFromResponse(response, OAUTH2_PARAMETER_ACCESS_TOKEN);
+    protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, String response) {
+        AccessTokenResponse tokenResponse = null;
+        try {
+            tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
+        } catch (IOException e) {
+            throw new IdentityBrokerException("Could not decode access token response.", e);
+        }
+        String accessToken = tokenResponse.getToken();
 
         if (accessToken == null) {
             throw new IdentityBrokerException("No access_token from server.");
         }
 
-        String idToken = extractTokenFromResponse(response, OIDC_PARAMETER_ID_TOKEN);
+        String encodedIdToken = tokenResponse.getIdToken();
 
-        validateIdToken(idToken);
+        notes.put(FEDERATED_ACCESS_TOKEN, accessToken);
+        notes.put(FEDERATED_ID_TOKEN, encodedIdToken);
+        notes.put(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
+        notes.put(FEDERATED_TOKEN_EXPIRATION, Long.toString(tokenResponse.getExpiresIn()));
 
-        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");
+        IDToken idToken = validateIdToken(encodedIdToken);
+
+        try {
+            String id = idToken.getSubject();
+            String name = idToken.getName();
+            String preferredUsername = idToken.getPreferredUsername();
+            String email = idToken.getEmail();
+
+            if (id == null || name == null || preferredUsername == null || email == null && getConfig().getUserInfoUrl() != null) {
+                JsonNode userInfo = SimpleHttp.doGet(getConfig().getUserInfoUrl())
+                        .header("Authorization", "Bearer " + accessToken)
+                        .asJson();
+
+                id = getJsonProperty(userInfo, "sub");
+                name = getJsonProperty(userInfo, "name");
+                preferredUsername = getJsonProperty(userInfo, "preferred_username");
+                email = getJsonProperty(userInfo, "email");
+            }
 
             FederatedIdentity identity = new FederatedIdentity(id);
 
@@ -106,16 +195,16 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
         }
     }
 
-    private void validateIdToken(String idToken) {
-        if (idToken == null) {
+    private IDToken validateIdToken(String encodedToken) {
+        if (encodedToken == null) {
             throw new IdentityBrokerException("No id_token from server.");
         }
 
         try {
-            JsonNode idTokenInfo = asJsonNode(decodeJWS(idToken));
+            IDToken idToken = new JWSInput(encodedToken).readJsonContent(IDToken.class);
 
-            String aud = getJsonProperty(idTokenInfo, "aud");
-            String iss = getJsonProperty(idTokenInfo, "iss");
+            String aud = idToken.getAudience();
+            String iss = idToken.getIssuer();
 
             if (aud != null && !aud.equals(getConfig().getClientId())) {
                 throw new RuntimeException("Wrong audience from id_token..");
@@ -128,12 +217,13 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
 
                 for (String trustedIssuer : issuers) {
                     if (iss != null && iss.equals(trustedIssuer.trim())) {
-                        return;
+                        return idToken;
                     }
                 }
 
                 throw new IdentityBrokerException("Wrong issuer from id_token..");
             }
+            return idToken;
         } catch (IOException e) {
             throw new IdentityBrokerException("Could not decode id token.", e);
         }
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
old mode 100644
new mode 100755
index de4043c..0bcdfa3
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
@@ -31,8 +31,21 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
     public String getPrompt() {
         return getConfig().get("prompt");
     }
+    public void setPrompt(String prompt) {
+        getConfig().put("prompt", prompt);
+    }
 
     public String getIssuer() {
         return getConfig().get("issuer");
     }
+    public void setIssuer(String issuer) {
+        getConfig().put("issuer", issuer);
+    }
+    public String getLogoutUrl() {
+        return getConfig().get("logoutUrl");
+    }
+    public void setLogoutUrl(String url) {
+        getConfig().put("logoutUrl", url);
+    }
+
 }
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
old mode 100644
new mode 100755
index 65bfedd..7a21eae
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
@@ -19,6 +19,13 @@ package org.keycloak.broker.oidc;
 
 import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
 import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
 
 /**
  * @author Pedro Igor
@@ -41,4 +48,22 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
     public String getId() {
         return PROVIDER_ID;
     }
+
+    @Override
+    public Map<String, String> parseConfig(InputStream inputStream) {
+        OIDCConfigurationRepresentation rep = null;
+        try {
+            rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
+        } catch (IOException e) {
+            throw new RuntimeException("failed to load openid connect metadata", e);
+        }
+        OIDCIdentityProviderConfig config = new OIDCIdentityProviderConfig(new IdentityProviderModel());
+        config.setIssuer(rep.getIssuer());
+        config.setLogoutUrl(rep.getLogoutEndpoint());
+        config.setAuthorizationUrl(rep.getAuthorizationEndpoint());
+        config.setTokenUrl(rep.getTokenEndpoint());
+        config.setUserInfoUrl(rep.getUserinfoEndpoint());
+        return config.getConfig();
+
+    }
 }
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
index 4ac8177..214b155 100755
--- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
+++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLIdentityProvider.java
@@ -118,7 +118,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
 
     @Override
     public Response keycloakInitiatedBrowserLogout(UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
-        if (getConfig().getSingleLogoutServiceUrl() == null) return null;
+        if (getConfig().getSingleLogoutServiceUrl() == null || getConfig().getSingleLogoutServiceUrl().trim().equals("")) return null;
 
         SAML2LogoutRequestBuilder logoutBuilder = new SAML2LogoutRequestBuilder()
                 .issuer(getEntityId(uriInfo, realm))
diff --git a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java
index fd8ec0f..7baf553 100755
--- a/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java
+++ b/core/src/main/java/org/keycloak/representations/AccessTokenResponse.java
@@ -1,7 +1,12 @@
 package org.keycloak.representations;
 
+import org.codehaus.jackson.annotate.JsonAnyGetter;
+import org.codehaus.jackson.annotate.JsonAnySetter;
 import org.codehaus.jackson.annotate.JsonProperty;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * OAuth 2.0 Access Token Response json
  *
@@ -33,6 +38,10 @@ public class AccessTokenResponse {
     @JsonProperty("session-state")
     protected String sessionState;
 
+    protected Map<String, Object> otherClaims = new HashMap<String, Object>();
+
+
+
     public String getToken() {
         return token;
     }
@@ -96,4 +105,15 @@ public class AccessTokenResponse {
     public void setSessionState(String sessionState) {
         this.sessionState = sessionState;
     }
+
+    @JsonAnyGetter
+    public Map<String, Object> getOtherClaims() {
+        return otherClaims;
+    }
+
+    @JsonAnySetter
+    public void setOtherClaims(String name, Object value) {
+        otherClaims.put(name, value);
+    }
+
 }
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index d39c8e5..fc0ac6d 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -1299,6 +1299,53 @@ module.directive('onoffswitchmodel', function() {
     }
 });
 
+/**
+ * Directive for presenting an ON-OFF switch for checkbox.
+ * This directive provides some additional capabilities to the default onoffswitch such as:
+ *
+ * - Specific scope to specify the value. Instead of just true or false.
+ *
+ * Usage: <input ng-model="mmm" name="nnn" id="iii" onoffswitchvalue [on-text="ooo" off-text="fff"] />
+ */
+module.directive('onoffswitchvalue', function() {
+    return {
+        restrict: "EA",
+        replace: true,
+        scope: {
+            name: '@',
+            id: '@',
+            value: '=',
+            ngModel: '=',
+            ngDisabled: '=',
+            kcOnText: '@onText',
+            kcOffText: '@offText'
+        },
+        // TODO - The same code acts differently when put into the templateURL. Find why and move the code there.
+        //templateUrl: "templates/kc-switch.html",
+        template: "<span><div class='onoffswitch' tabindex='0'><input type='checkbox' ng-true-value='{{value}}' ng-model='ngModel' ng-disabled='ngDisabled' class='onoffswitch-checkbox' name='{{name}}' id='{{id}}'><label for='{{id}}' class='onoffswitch-label'><span class='onoffswitch-inner'><span class='onoffswitch-active'>{{kcOnText}}</span><span class='onoffswitch-inactive'>{{kcOffText}}</span></span><span class='onoffswitch-switch'></span></label></div></span>",
+        compile: function(element, attrs) {
+            /*
+             We don't want to propagate basic attributes to the root element of directive. Id should be passed to the
+             input element only to achieve proper label binding (and validity).
+             */
+            element.removeAttr('name');
+            element.removeAttr('id');
+
+            if (!attrs.onText) { attrs.onText = "ON"; }
+            if (!attrs.offText) { attrs.offText = "OFF"; }
+
+            element.bind('keydown', function(e){
+                var code = e.keyCode || e.which;
+                if (code === 32 || code === 13) {
+                    e.stopImmediatePropagation();
+                    e.preventDefault();
+                    $(e.target).find('input').click();
+                }
+            });
+        }
+    }
+});
+
 module.directive('kcInput', function() {
     var d = {
         scope : true,
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 55b74f0..b030948 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -643,6 +643,15 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
 
     $scope.realm = angular.copy(realm);
 
+    $scope.initProvider = function() {
+        if (instance && instance.alias) {
+
+        } else {
+            $scope.identityProvider.updateProfileFirstLogin = false;
+        }
+
+    };
+
     $scope.initSamlProvider = function() {
         $scope.nameIdFormats = [
             {
@@ -684,6 +693,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
 
         } else {
             $scope.identityProvider.config.nameIDPolicyFormat = $scope.nameIdFormats[0].format;
+            $scope.identityProvider.updateProfileFirstLogin = false;
         }
     }
 
@@ -698,7 +708,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
         $scope.identityProvider.alias = providerFactory.name;
         $scope.identityProvider.providerId = providerFactory.id;
         $scope.identityProvider.enabled = true;
-        $scope.identityProvider.updateProfileFirstLogin = true;
+        $scope.identityProvider.updateProfileFirstLogin = false;
         $scope.identityProvider.authenticateByDefault = false;
         $scope.newIdentityProvider = true;
     }
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
index df99908..e89569c 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
@@ -1,23 +1,42 @@
 <div class="bs-sidebar col-sm-3 " data-ng-include data-src="resourceUrl + '/partials/realm-menu.html'"></div>
-    <div id="content-area" class="col-sm-9" role="main">
+    <div id="content-area" class="col-sm-9" role="main"  data-ng-init="initProvider()">
         <data-kc-navigation data-kc-current="social" data-kc-realm="realm.realm" data-kc-social="realm.social"></data-kc-navigation>
         <h2></h2>
         <div id="content">
             <ol class="breadcrumb">
                 <li><a href="#/realms/{{realm.realm}}/identity-provider-settings">Identity Providers</a></li>
-                <li class="active">{{identityProvider.name}} Provider Settings</li>
+                <li class="active">{{identityProvider.alias}} Provider Settings</li>
             </ol>
-            <h2 class="pull-left">{{identityProvider.name}} Provider Settings</h2>
+            <h2 class="pull-left">{{identityProvider.alias}} Provider Settings</h2>
             <p class="subtitle"><span class="required">*</span> Required fields</p>
             <form class="form-horizontal" name="realmForm" novalidate>
                 <fieldset>
                     <div class="form-group clearfix">
                         <label class="col-sm-2 control-label" for="identifier">Alias <span class="required">*</span></label>
                         <div class="col-sm-4">
-                            <input class="form-control" id="identifier" type="text" ng-model="identityProvider.id" data-ng-readonly="!newIdentityProvider" required>
+                            <input class="form-control" id="identifier" type="text" ng-model="identityProvider.alias" data-ng-readonly="!newIdentityProvider" required>
                         </div>
                         <span tooltip-placement="right" tooltip="The alias unique identifies an identity provider and it is also used to build the redirect uri." class="fa fa-info-circle"></span>
                     </div>
+                    <div class="form-group" data-ng-show="newIdentityProvider && !importFile">
+                        <label class="col-sm-2 control-label" for="fromUrl">Import From Url</label>
+                        <div class="col-sm-4">
+                            <input class="form-control" id="fromUrl" type="text" ng-model="fromUrl">
+                        </div>
+                        <span tooltip-placement="right" tooltip="Import metadata from a remote IDP SAML entity descriptor." class="fa fa-info-circle"></span>
+                    </div>
+                    <div class="form-group" data-ng-show="newIdentityProvider && !importUrl">
+                        <label class="col-sm-2 control-label">Import From File</label>
+                        <div class="col-sm-4">
+                            <div class="controls kc-button-input-file" data-ng-show="!files || files.length == 0">
+                                <a href="#" class="btn btn-default"><span class="kc-icon-upload">Icon: Upload</span>Choose a File...</a>
+                                <input id="import-file" type="file" class="transparent" ng-file-select="onFileSelect($files)">
+                            </div>
+                        <span class="kc-uploaded-file" data-ng-show="files.length > 0">
+                            {{files[0].name}}
+                        </span>
+                        </div>
+                    </div>
                     <div class="form-group clearfix">
                         <label class="col-sm-2 control-label" for="authorizationUrl">Authorization Url <span class="required">*</span></label>
                         <div class="col-sm-4">
@@ -33,11 +52,18 @@
                         <span tooltip-placement="right" tooltip="The Token Url." class="fa fa-info-circle"></span>
                     </div>
                     <div class="form-group clearfix">
-                        <label class="col-sm-2 control-label" for="userInfoUrl">User Info Url <span class="required">*</span></label>
+                        <label class="col-sm-2 control-label" for="userInfoUrl">Logout Url</label>
+                        <div class="col-sm-4">
+                            <input class="form-control" id="logoutUrl" type="text" ng-model="identityProvider.config.logoutUrl">
+                        </div>
+                        <span tooltip-placement="right" tooltip="End session endpoint to use to logout user from external IDP." class="fa fa-info-circle"></span>
+                    </div>
+                    <div class="form-group clearfix">
+                        <label class="col-sm-2 control-label" for="userInfoUrl">User Info Url</label>
                         <div class="col-sm-4">
-                            <input class="form-control" id="userInfoUrl" type="text" ng-model="identityProvider.config.userInfoUrl" required>
+                            <input class="form-control" id="userInfoUrl" type="text" ng-model="identityProvider.config.userInfoUrl">
                         </div>
-                        <span tooltip-placement="right" tooltip="The User Info Url." class="fa fa-info-circle"></span>
+                        <span tooltip-placement="right" tooltip="The User Info Url.  This is optional." class="fa fa-info-circle"></span>
                     </div>
                     <div class="form-group clearfix">
                         <label class="col-sm-2 control-label" for="clientId">Client ID <span class="required">*</span></label>
@@ -116,7 +142,10 @@
                 </fieldset>
 
                 <div class="pull-right form-actions">
-                    <button kc-save>Save</button>
+                    <button kc-save data-ng-show="changed">Save</button>
+                    <button type="submit" data-ng-click="cancel()" data-ng-show="changed" class="btn btn-lg btn-default">Cancel</button>
+                    <button type="submit" data-ng-click="uploadFile()" data-ng-show="importFile" class="btn btn-lg btn-primary">Import</button>
+                    <button type="submit" data-ng-click="importFrom()" data-ng-show="importUrl" class="btn btn-lg btn-primary">Import</button>
                     <button kc-delete data-ng-click="remove()" data-ng-show="!newIdentityProvider">Delete</button>
                 </div>
             </form>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
index 4e1d88c..aa0364b 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
@@ -47,7 +47,7 @@
                     <div class="form-group clearfix" data-ng-show="!importFile && !importUrl">
                         <label class="col-sm-2 control-label" for="singleSignOnServiceUrl">Single Logout Service Url</label>
                         <div class="col-sm-4">
-                            <input class="form-control" id="singleLogoutServiceUrl" type="text" ng-model="identityProvider.config.singleLogoutServiceUrl" required>
+                            <input class="form-control" id="singleLogoutServiceUrl" type="text" ng-model="identityProvider.config.singleLogoutServiceUrl">
                         </div>
                         <span tooltip-placement="right" tooltip="The Url that must be used to send logout requests." class="fa fa-info-circle"></span>
                     </div>
@@ -71,35 +71,35 @@
                     <div class="form-group" data-ng-show="!importFile && !importUrl">
                         <label class="col-sm-2 control-label" for="wantAuthnRequestsSigned">Want AuthnRequests Signed</label>
                         <div class="col-sm-4">
-                            <input ng-model="identityProvider.config.wantAuthnRequestsSigned" id="wantAuthnRequestsSigned" value="'true'" onoffswitchmodel />
+                            <input ng-model="identityProvider.config.wantAuthnRequestsSigned" id="wantAuthnRequestsSigned" name="wantAuthnRequestsSigned" value="'true'" onoffswitchvalue />
                         </div>
                         <span tooltip-placement="right" tooltip=" Indicates whether the identity provider expects signed a AuthnRequest." class="fa fa-info-circle"></span>
                     </div>
                     <div class="form-group" data-ng-show="!importFile && !importUrl">
                         <label class="col-sm-2 control-label" for="forceAuthn">Force Authentication</label>
                         <div class="col-sm-4">
-                            <input ng-model="identityProvider.config.forceAuthn" id="forceAuthn" value="'true'" onoffswitchmodel />
+                            <input ng-model="identityProvider.config.forceAuthn" id="forceAuthn" name="forceAuthn" value="'true'" onoffswitchvalue />
                         </div>
                         <span tooltip-placement="right" tooltip=" Indicates whether the identity provider must authenticate the presenter directly rather than rely on a previous security context." class="fa fa-info-circle"></span>
                     </div>
                     <div class="form-group" data-ng-show="!importFile && !importUrl">
                         <label class="col-sm-2 control-label" for="validateSignature">Validate Signature</label>
                         <div class="col-sm-4">
-                            <input ng-model="identityProvider.config.validateSignature" id="validateSignature" value="'true'" onoffswitchmodel />
+                            <input ng-model="identityProvider.config.validateSignature" id="validateSignature" value="'true'" onoffswitchvalue />
                         </div>
                         <span tooltip-placement="right" tooltip="Enable/disable signature validation of SAML responses." class="fa fa-info-circle"></span>
                     </div>
                     <div class="form-group" data-ng-show="!importFile && !importUrl">
                         <label class="col-sm-2 control-label" for="postBindingResponse">HTTP-POST Binding Response</label>
                         <div class="col-sm-4">
-                            <input ng-model="identityProvider.config.postBindingResponse" id="postBindingResponse" value="'true'" onoffswitchmodel />
+                            <input ng-model="identityProvider.config.postBindingResponse" id="postBindingResponse" value="'true'" onoffswitchvalue />
                         </div>
                         <span tooltip-placement="right" tooltip="Indicates whether the identity provider must respond to the AuthnRequest using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used." class="fa fa-info-circle"></span>
                     </div>
                     <div class="form-group" data-ng-show="!importFile && !importUrl">
                         <label class="col-sm-2 control-label" for="postBindingAuthnRequest">HTTP-POST Binding for AuthnRequest</label>
                         <div class="col-sm-4">
-                            <input ng-model="identityProvider.config.postBindingAuthnRequest" id="postBindingAuthnRequest" value="'true'" onoffswitchmodel />
+                            <input ng-model="identityProvider.config.postBindingAuthnRequest" id="postBindingAuthnRequest" value="'true'" onoffswitchvalue />
                         </div>
                         <span tooltip-placement="right" tooltip="Indicates whether the AuthnRequest must be sent using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used." class="fa fa-info-circle"></span>
                     </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
index d37e6a3..aa6a5eb 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html
@@ -1,5 +1,5 @@
 <div class="bs-sidebar col-sm-3 " data-ng-include data-src="resourceUrl + '/partials/realm-menu.html'"></div>
-    <div id="content-area" class="col-sm-9" role="main">
+    <div id="content-area" class="col-sm-9" role="main" data-ng-init="initProvider()">
         <data-kc-navigation data-kc-current="social" data-kc-realm="realm.realm" data-kc-social="realm.social"></data-kc-navigation>
         <h2></h2>
         <div id="content">
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
index 07b9d00..a61c811 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/LogoutEndpoint.java
@@ -121,7 +121,7 @@ public class LogoutEndpoint {
         AuthenticationManager.AuthResult authResult = authManager.authenticateIdentityCookie(session, realm, uriInfo, clientConnection, headers, false);
         if (authResult != null) {
             userSession = userSession != null ? userSession : authResult.getSession();
-            if (redirectUri != null) userSession.setNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI, redirect);
+            if (redirect != null) userSession.setNote(OIDCLoginProtocol.LOGOUT_REDIRECT_URI, redirect);
             if (state != null) userSession.setNote(OIDCLoginProtocol.LOGOUT_STATE_PARAM, state);
             userSession.setNote(AuthenticationManager.KEYCLOAK_LOGOUT_PROTOCOL, OIDCLoginProtocol.LOGIN_PROTOCOL);
             return AuthenticationManager.browserLogout(session, realm, authResult.getSession(), uriInfo, clientConnection, headers);
@@ -131,7 +131,7 @@ public class LogoutEndpoint {
             event.user(userSession.getUser()).session(userSession).success();
         }
 
-        if (redirectUri != null) {
+        if (redirect != null) {
             UriBuilder uriBuilder = UriBuilder.fromUri(redirect);
             if (state != null) uriBuilder.queryParam(OIDCLoginProtocol.STATE_PARAM, state);
             return Response.status(302).location(uriBuilder.build()).build();
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
index 0e3d4f3..9245e58 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/representations/OIDCConfigurationRepresentation.java
@@ -1,8 +1,12 @@
 package org.keycloak.protocol.oidc.representations;
 
+import org.codehaus.jackson.annotate.JsonAnyGetter;
+import org.codehaus.jackson.annotate.JsonAnySetter;
 import org.codehaus.jackson.annotate.JsonProperty;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -44,6 +48,8 @@ public class OIDCConfigurationRepresentation {
     @JsonProperty("response_modes_supported")
     private List<String> responseModesSupported;
 
+    protected Map<String, Object> otherClaims = new HashMap<String, Object>();
+
     public String getIssuer() {
         return issuer;
     }
@@ -131,4 +137,15 @@ public class OIDCConfigurationRepresentation {
     public void setResponseModesSupported(List<String> responseModesSupported) {
         this.responseModesSupported = responseModesSupported;
     }
+
+    @JsonAnyGetter
+    public Map<String, Object> getOtherClaims() {
+        return otherClaims;
+    }
+
+    @JsonAnySetter
+    public void setOtherClaims(String name, Object value) {
+        otherClaims.put(name, value);
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index a835b66..4d4f41b 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -76,7 +76,6 @@ import static org.keycloak.models.UserModel.RequiredAction.UPDATE_PROFILE;
  *
  * @author Pedro Igor
  */
-@Path("/broker")
 public class IdentityBrokerService implements IdentityProvider.AuthenticationCallback {
 
     private static final Logger LOGGER = Logger.getLogger(IdentityBrokerService.class);
diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
index 0feccb4..1f43e9a 100755
--- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java
@@ -79,6 +79,10 @@ public class RealmsResource {
         return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getProtocol");
     }
 
+    public static UriBuilder brokerUrl(UriInfo uriInfo) {
+        return uriInfo.getBaseUriBuilder().path(RealmsResource.class).path(RealmsResource.class, "getBrokerService");
+    }
+
     @Path("{realm}/login-status-iframe.html")
     @Deprecated
     public Object getLoginStatusIframe(final @PathParam("realm") String name,