keycloak-aplcache
Changes
dependencies/server-all/pom.xml 5(+5 -0)
distribution/modules/build.xml 4(+4 -0)
distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml 1(+1 -0)
distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml 1(+1 -0)
distribution/modules/src/main/resources/modules/org/keycloak/keycloak-social-stackoverflow/main/module.xml 22(+22 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-facebook-ext.html 0(+0 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-github-ext.html 0(+0 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-google-ext.html 0(+0 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-linkedin.html 0(+0 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-linkedin-ext.html 0(+0 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html 3(+2 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-stackoverflow.html 1(+1 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-stackoverflow-ext.html 7(+7 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-twitter-ext.html 0(+0 -0)
social/pom.xml 1(+1 -0)
social/stackoverflow/.gitignore 1(+1 -0)
social/stackoverflow/pom.xml 40(+40 -0)
social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java 214(+214 -0)
social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackOverflowIdentityProviderConfig.java 40(+40 -0)
social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java 47(+47 -0)
social/stackoverflow/src/main/resources/META-INF/services/org.keycloak.social.SocialIdentityProviderFactory 1(+1 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java 2(+2 -0)
Details
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java
index 870e951..c9050b8 100644
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/util/SimpleHttp.java
@@ -13,9 +13,11 @@ import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
+import java.util.zip.GZIPInputStream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ * @author Vlastimil Elias (velias at redhat dot com)
*/
public class SimpleHttp {
@@ -116,7 +118,11 @@ public class SimpleHttp {
connection.setDoOutput(false);
}
+ String ce = connection.getHeaderField("Content-Encoding");
is = connection.getInputStream();
+ if ("gzip".equals(ce)) {
+ is = new GZIPInputStream(is);
+ }
return toString(is);
} finally {
if (os != null) {
dependencies/server-all/pom.xml 5(+5 -0)
diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index a7bd522..2c1f6f3 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -127,6 +127,11 @@
<artifactId>keycloak-social-linkedin</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-social-stackoverflow</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<!-- ldap federation api -->
<dependency>
distribution/modules/build.xml 4(+4 -0)
diff --git a/distribution/modules/build.xml b/distribution/modules/build.xml
index ad0f9e7..80ff507 100755
--- a/distribution/modules/build.xml
+++ b/distribution/modules/build.xml
@@ -239,6 +239,10 @@
<module-def name="org.keycloak.keycloak-social-linkedin">
<maven-resource group="org.keycloak" artifact="keycloak-social-linkedin"/>
</module-def>
+
+ <module-def name="org.keycloak.keycloak-social-stackoverflow">
+ <maven-resource group="org.keycloak" artifact="keycloak-social-stackoverflow"/>
+ </module-def>
<module-def name="org.keycloak.keycloak-kerberos-federation">
<maven-resource group="org.keycloak" artifact="keycloak-kerberos-federation"/>
diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml
index f8a251d..f553d24 100755
--- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml
+++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-server/main/module.xml
@@ -58,6 +58,7 @@
<module name="org.keycloak.keycloak-social-google" services="import"/>
<module name="org.keycloak.keycloak-social-twitter" services="import"/>
<module name="org.keycloak.keycloak-social-linkedin" services="import"/>
+ <module name="org.keycloak.keycloak-social-stackoverflow" services="import"/>
<module name="org.keycloak.keycloak-subsystem" services="import"/>
<module name="org.keycloak.keycloak-timer-api" services="import"/>
<module name="org.keycloak.keycloak-timer-basic" services="import"/>
diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
index 6166b9b..86e86f4 100755
--- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
+++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
@@ -61,6 +61,7 @@
<module name="org.keycloak.keycloak-social-google" services="import"/>
<module name="org.keycloak.keycloak-social-twitter" services="import"/>
<module name="org.keycloak.keycloak-social-linkedin" services="import"/>
+ <module name="org.keycloak.keycloak-social-stackoverflow" services="import"/>
<module name="org.keycloak.keycloak-timer-api" services="import"/>
<module name="org.keycloak.keycloak-timer-basic" services="import"/>
diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-social-stackoverflow/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-social-stackoverflow/main/module.xml
new file mode 100755
index 0000000..6ddd2a4
--- /dev/null
+++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-social-stackoverflow/main/module.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+
+<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-social-stackoverflow">
+ <resources>
+ <!-- Insert resources here -->
+ </resources>
+ <dependencies>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-social-core"/>
+ <module name="org.keycloak.keycloak-broker-core"/>
+ <module name="org.keycloak.keycloak-broker-oidc"/>
+ <module name="org.keycloak.keycloak-model-api"/>
+ <module name="org.jboss.logging"/>
+ <module name="javax.api"/>
+ <module name="org.codehaus.jackson.jackson-core-asl"/>
+ <module name="org.codehaus.jackson.jackson-mapper-asl"/>
+ <module name="org.codehaus.jackson.jackson-xc"/>
+ </dependencies>
+
+</module>
diff --git a/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
index 6a97ca5..6caa2c8 100755
--- a/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
+++ b/distribution/subsystem-war/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
@@ -51,6 +51,7 @@
<module name="org.keycloak.keycloak-social-google" services="import"/>
<module name="org.keycloak.keycloak-social-twitter" services="import"/>
<module name="org.keycloak.keycloak-social-linkedin" services="import"/>
+ <module name="org.keycloak.keycloak-social-stackoverflow" services="import"/>
<module name="org.keycloak.keycloak-timer-api" services="import"/>
<module name="org.keycloak.keycloak-timer-basic" services="import"/>
<module name="org.hibernate" services="import"/>
diff --git a/docbook/reference/en/en-US/modules/identity-broker.xml b/docbook/reference/en/en-US/modules/identity-broker.xml
index 3a74b73..5ebbbe2 100755
--- a/docbook/reference/en/en-US/modules/identity-broker.xml
+++ b/docbook/reference/en/en-US/modules/identity-broker.xml
@@ -807,6 +807,78 @@
</tbody>
</tgroup>
</table>
+ </section>
+ <section>
+ <title>StackOverflow</title>
+ <para>
+ To enable login with StackOverflow you first have to register an OAuth application on
+ <ulink url="https://stackapps.com/">StackApps</ulink>. Then you need to copy the client id, secret and key into the Keycloak Admin Console.
+ </para>
+ <para>
+ Let's see first how to create an application with StackOverflow.
+ </para>
+ <orderedlist>
+ <listitem>
+ <para>
+ Go to <ulink url="http://stackapps.com/apps/oauth/register">registering your application on Stack Apps</ulink> url and login here.
+ Use any value for <literal>Application Name</literal>, <literal>Application Website</literal> and <literal>Description</literal> you want.
+ Set <literal>OAuth Domain</literal> to the domain where your Keycloak instance runs.
+ Click the <literal>Register Your Application</literal> button.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Copy <literal>Client Id</literal>, <literal>Client Secret</literal> and <literal>Key</literal> from the shown page.
+ </para>
+ </listitem>
+ </orderedlist>
+ <para>
+ Now that you have the client id, secret and key, you can proceed with the creation of a StackOverflow Identity Provider in Keycloak. As follows:
+ </para>
+ <orderedlist>
+ <listitem>
+ <para>
+ Select the <literal>StackOverflow</literal> identity provider from the drop-down box on the top right corner of the identity providers table in Keycloak's Admin Console. You should be presented with a specific page to configure the selected provided.
+ </para>
+ </listitem>
+ <listitem>
+ <para>
+ Copy the client id, client secret and key to their corresponding fields in the Keycloak Admin Console. Click <literal>Save</literal>.
+ </para>
+ </listitem>
+ </orderedlist>
+ <para>
+ That is it! This pretty much what you need to do in order to setup this identity provider.
+ </para>
+ <para>
+ The table below lists some additional configuration options you may use when configuring this provider.
+ </para>
+ <table>
+ <title>Configuration Options</title>
+ <tgroup align="left" cols="2">
+ <thead>
+ <row>
+ <entry>
+ Configuration
+ </entry>
+ <entry>
+ Description
+ </entry>
+ </row>
+ </thead>
+ <tbody valign="top">
+ <row>
+ <entry>
+ <literal>Default Scopes</literal>
+ </entry>
+ <entry>
+ Allows you to manually specify the scopes that users must authorize when authenticating with this provider.
+ For a complete list of scopes, please take a look at application configuration in <ulink url="https://api.stackexchange.com/docs/authentication#scope">StackExchange API Authentication</ulink> documentation. Keycloak uses the empty scope by default.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
</section>
</section>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-facebook-ext.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-facebook-ext.html
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-facebook-ext.html
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-github-ext.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-github-ext.html
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-github-ext.html
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-google-ext.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-google-ext.html
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-google-ext.html
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-linkedin-ext.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-linkedin-ext.html
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-linkedin-ext.html
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 aa6a5eb..4a23349 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
@@ -28,12 +28,13 @@
</div>
<span tooltip-placement="right" tooltip="The client or application secret registered withing the identity provider." class="fa fa-info-circle"></span>
</div>
+ <div data-ng-include data-src="resourceUrl + '/partials/realm-identity-provider-' + identityProvider.providerId + '-ext.html'"></div>
<div class="form-group clearfix">
<label class="col-sm-2 control-label" for="defaultScope">Default Scopes </label>
<div class="col-sm-4">
<input class="form-control" id="defaultScope" type="text" ng-model="identityProvider.config.defaultScope">
</div>
- <span tooltip-placement="right" tooltip="The scopes to be sent when asking for authorization. It can be a space-separated list of scopes. Defaults to 'openid'." class="fa fa-info-circle"></span>
+ <span tooltip-placement="right" tooltip="The scopes to be sent when asking for authorization. See documentation for possible values, separator and default value'." class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="enabled">Store Tokens</label>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-stackoverflow.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-stackoverflow.html
new file mode 100755
index 0000000..a4630ac
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-stackoverflow.html
@@ -0,0 +1 @@
+<div data-ng-include data-src="resourceUrl + '/partials/realm-identity-provider-social.html'"></div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-stackoverflow-ext.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-stackoverflow-ext.html
new file mode 100755
index 0000000..86516df
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-stackoverflow-ext.html
@@ -0,0 +1,7 @@
+ <div class="form-group clearfix">
+ <label class="col-sm-2 control-label" for="clientId">Key <span class="required">*</span></label>
+ <div class="col-sm-4">
+ <input class="form-control" id="clientId" type="text" ng-model="identityProvider.config.key" required>
+ </div>
+ <span tooltip-placement="right" tooltip="The Key obtained from Stack Overflow application registration." class="fa fa-info-circle"></span>
+ </div>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-twitter-ext.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-twitter-ext.html
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-twitter-ext.html
diff --git a/forms/common-themes/src/main/resources/theme/patternfly/login/resources/css/login.css b/forms/common-themes/src/main/resources/theme/patternfly/login/resources/css/login.css
index 345f594..2bc07bb 100644
--- a/forms/common-themes/src/main/resources/theme/patternfly/login/resources/css/login.css
+++ b/forms/common-themes/src/main/resources/theme/patternfly/login/resources/css/login.css
@@ -238,7 +238,7 @@ ol#kc-totp-settings li:first-of-type {
}
.zocial {
- width: 125px;
+ width: 150px;
}
.zocial:hover {
social/pom.xml 1(+1 -0)
diff --git a/social/pom.xml b/social/pom.xml
index ded7c60..ec08ca2 100755
--- a/social/pom.xml
+++ b/social/pom.xml
@@ -21,6 +21,7 @@
<module>twitter</module>
<module>facebook</module>
<module>linkedin</module>
+ <module>stackoverflow</module>
</modules>
</project>
social/stackoverflow/.gitignore 1(+1 -0)
diff --git a/social/stackoverflow/.gitignore b/social/stackoverflow/.gitignore
new file mode 100644
index 0000000..b83d222
--- /dev/null
+++ b/social/stackoverflow/.gitignore
@@ -0,0 +1 @@
+/target/
social/stackoverflow/pom.xml 40(+40 -0)
diff --git a/social/stackoverflow/pom.xml b/social/stackoverflow/pom.xml
new file mode 100755
index 0000000..f05a77b
--- /dev/null
+++ b/social/stackoverflow/pom.xml
@@ -0,0 +1,40 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>keycloak-social-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.2.0.Beta1-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+ <packaging>jar</packaging>
+
+ <artifactId>keycloak-social-stackoverflow</artifactId>
+ <name>Keycloak Social StackOverflow</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-social-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-broker-oidc</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.jackson</groupId>
+ <artifactId>jackson-mapper-asl</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
new file mode 100755
index 0000000..40fb32f
--- /dev/null
+++ b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java
@@ -0,0 +1,214 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 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.social.stackoverflow;
+
+import java.io.StringWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.util.HashMap;
+
+import org.codehaus.jackson.JsonNode;
+import org.jboss.logging.Logger;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
+import org.keycloak.broker.oidc.util.SimpleHttp;
+import org.keycloak.broker.provider.FederatedIdentity;
+import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.social.SocialIdentityProvider;
+
+/**
+ * Stackoverflow social provider. See https://api.stackexchange.com/docs/authentication
+ *
+ * @author Vlastimil Elias (velias at redhat dot com)
+ */
+public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvider<StackOverflowIdentityProviderConfig>
+ implements SocialIdentityProvider<StackOverflowIdentityProviderConfig> {
+
+ private static final Logger log = Logger.getLogger(StackoverflowIdentityProvider.class);
+
+ public static final String AUTH_URL = "https://stackexchange.com/oauth";
+ public static final String TOKEN_URL = "https://stackexchange.com/oauth/access_token";
+ public static final String PROFILE_URL = "https://api.stackexchange.com/2.2/me?order=desc&sort=name&site=stackoverflow";
+ public static final String DEFAULT_SCOPE = "";
+
+ public StackoverflowIdentityProvider(StackOverflowIdentityProviderConfig config) {
+ super(config);
+ config.setAuthorizationUrl(AUTH_URL);
+ config.setTokenUrl(TOKEN_URL);
+ config.setUserInfoUrl(PROFILE_URL);
+ }
+
+ @Override
+ protected FederatedIdentity doGetFederatedIdentity(String accessToken) {
+ log.debug("doGetFederatedIdentity()");
+ try {
+
+ String URL = PROFILE_URL + "&access_token=" + accessToken + "&key=" + getConfig().getKey();
+ if (log.isDebugEnabled()) {
+ log.debug("StackOverflow profile request to: " + URL);
+ }
+ JsonNode profile = SimpleHttp.doGet(URL).asJson().get("items").get(0);
+
+ FederatedIdentity user = new FederatedIdentity(getJsonProperty(profile, "user_id"));
+
+ user.setUsername(extractUsernameFromProfileURL(getJsonProperty(profile, "link")));
+ user.setName(unescapeHtml3(getJsonProperty(profile, "display_name")));
+ // email is not provided
+ // user.setEmail(getJsonProperty(profile, "email"));
+
+ return user;
+ } catch (Exception e) {
+ throw new IdentityBrokerException("Could not obtain user profile from Stackoverflow: " + e.getMessage(), e);
+ }
+ }
+
+ protected static String extractUsernameFromProfileURL(String profileURL) {
+ if (isNotBlank(profileURL)) {
+
+ try {
+ log.debug("go to extract username from profile URL " + profileURL);
+ URL u = new URL(profileURL);
+ String path = u.getPath();
+ if (isNotBlank(path) && path.length() > 1) {
+ if (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ String[] pe = path.split("/");
+ if (pe.length >= 3) {
+ return URLDecoder.decode(pe[2], "UTF-8");
+ } else {
+ log.warn("Stackoverflow profile URL path is without third part: " + profileURL);
+ }
+ } else {
+ log.warn("Stackoverflow profile URL is without path part: " + profileURL);
+ }
+ } catch (MalformedURLException e) {
+ log.warn("Stackoverflow profile URL is malformed: " + profileURL);
+ } catch (Exception e) {
+ log.warn("Stackoverflow profile URL " + profileURL + " username extraction failed due: " + e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ private static boolean isNotBlank(String s) {
+ return s != null && s.trim().length() > 0;
+ }
+
+ @Override
+ protected String getDefaultScopes() {
+ return DEFAULT_SCOPE;
+ }
+
+ public static final String unescapeHtml3(final String input) {
+ if (input == null)
+ return null;
+ StringWriter writer = null;
+ int len = input.length();
+ int i = 1;
+ int st = 0;
+ while (true) {
+ // look for '&'
+ while (i < len && input.charAt(i - 1) != '&')
+ i++;
+ if (i >= len)
+ break;
+
+ // found '&', look for ';'
+ int j = i;
+ while (j < len && j < i + MAX_ESCAPE + 1 && input.charAt(j) != ';')
+ j++;
+ if (j == len || j < i + MIN_ESCAPE || j == i + MAX_ESCAPE + 1) {
+ i++;
+ continue;
+ }
+
+ // found escape
+ if (input.charAt(i) == '#') {
+ // numeric escape
+ int k = i + 1;
+ int radix = 10;
+
+ final char firstChar = input.charAt(k);
+ if (firstChar == 'x' || firstChar == 'X') {
+ k++;
+ radix = 16;
+ }
+
+ try {
+ int entityValue = Integer.parseInt(input.substring(k, j), radix);
+
+ if (writer == null)
+ writer = new StringWriter(input.length());
+ writer.append(input.substring(st, i - 1));
+
+ if (entityValue > 0xFFFF) {
+ final char[] chrs = Character.toChars(entityValue);
+ writer.write(chrs[0]);
+ writer.write(chrs[1]);
+ } else {
+ writer.write(entityValue);
+ }
+
+ } catch (NumberFormatException ex) {
+ i++;
+ continue;
+ }
+ } else {
+ // named escape
+ CharSequence value = lookupMap.get(input.substring(i, j));
+ if (value == null) {
+ i++;
+ continue;
+ }
+
+ if (writer == null)
+ writer = new StringWriter(input.length());
+ writer.append(input.substring(st, i - 1));
+
+ writer.append(value);
+ }
+
+ // skip escape
+ st = j + 1;
+ i = st;
+ }
+
+ if (writer != null) {
+ writer.append(input.substring(st, len));
+ return writer.toString();
+ }
+ return input;
+ }
+
+ private static final String[][] ESCAPES = { { "\"", "quot" }, // " - double-quote
+ { "&", "amp" }, // & - ampersand
+ { "<", "lt" }, // < - less-than
+ { ">", "gt" }, // > - greater-than
+ };
+
+ private static final int MIN_ESCAPE = 2;
+ private static final int MAX_ESCAPE = 6;
+
+ private static final HashMap<String, CharSequence> lookupMap;
+ static {
+ lookupMap = new HashMap<String, CharSequence>();
+ for (final CharSequence[] seq : ESCAPES)
+ lookupMap.put(seq[1].toString(), seq[0]);
+ }
+}
diff --git a/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackOverflowIdentityProviderConfig.java b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackOverflowIdentityProviderConfig.java
new file mode 100644
index 0000000..f531d7c
--- /dev/null
+++ b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackOverflowIdentityProviderConfig.java
@@ -0,0 +1,40 @@
+/*
+ * 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.social.stackoverflow;
+
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.models.IdentityProviderModel;
+
+/**
+ * @author Vlastimil Elias (velias at redhat dot com)
+ */
+public class StackOverflowIdentityProviderConfig extends OAuth2IdentityProviderConfig {
+
+ public StackOverflowIdentityProviderConfig(IdentityProviderModel model) {
+ super(model);
+ }
+
+ public String getKey() {
+ return getConfig().get("key");
+ }
+
+ public void setKey(String key) {
+ getConfig().put("key", key);
+ }
+
+}
\ No newline at end of file
diff --git a/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java
new file mode 100644
index 0000000..d02c2d7
--- /dev/null
+++ b/social/stackoverflow/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProviderFactory.java
@@ -0,0 +1,47 @@
+/*
+ * JBoss, Home of Professional Open Source
+ *
+ * Copyright 2015 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.social.stackoverflow;
+
+import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.social.SocialIdentityProviderFactory;
+
+/**
+ * @author Vlastimil Elias (velias at redhat dot com)
+ */
+public class StackoverflowIdentityProviderFactory extends
+ AbstractIdentityProviderFactory<StackoverflowIdentityProvider> implements
+ SocialIdentityProviderFactory<StackoverflowIdentityProvider> {
+
+ public static final String PROVIDER_ID = "stackoverflow";
+
+ @Override
+ public String getName() {
+ return "StackOverflow";
+ }
+
+ @Override
+ public StackoverflowIdentityProvider create(IdentityProviderModel model) {
+ return new StackoverflowIdentityProvider(new StackOverflowIdentityProviderConfig(model));
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+}
diff --git a/social/stackoverflow/src/main/resources/META-INF/services/org.keycloak.social.SocialIdentityProviderFactory b/social/stackoverflow/src/main/resources/META-INF/services/org.keycloak.social.SocialIdentityProviderFactory
new file mode 100644
index 0000000..4bbbe9c
--- /dev/null
+++ b/social/stackoverflow/src/main/resources/META-INF/services/org.keycloak.social.SocialIdentityProviderFactory
@@ -0,0 +1 @@
+org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory
\ No newline at end of file
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java
index 84bbc9c..57886e7 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderModelTest.java
@@ -25,6 +25,7 @@ import org.keycloak.social.github.GitHubIdentityProviderFactory;
import org.keycloak.social.google.GoogleIdentityProviderFactory;
import org.keycloak.social.twitter.TwitterIdentityProviderFactory;
import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
+import org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory;
import org.keycloak.testsuite.model.AbstractModelTest;
import java.util.Collections;
@@ -49,6 +50,7 @@ public abstract class AbstractIdentityProviderModelTest extends AbstractModelTes
this.expectedProviders.add(GitHubIdentityProviderFactory.PROVIDER_ID);
this.expectedProviders.add(TwitterIdentityProviderFactory.PROVIDER_ID);
this.expectedProviders.add(LinkedInIdentityProviderFactory.PROVIDER_ID);
+ this.expectedProviders.add(StackoverflowIdentityProviderFactory.PROVIDER_ID);
this.expectedProviders = Collections.unmodifiableSet(this.expectedProviders);
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
index f596f1b..eda8b69 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
@@ -17,6 +17,11 @@
*/
package org.keycloak.testsuite.broker;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
import org.junit.Test;
import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
import org.keycloak.broker.oidc.OIDCIdentityProvider;
@@ -36,15 +41,13 @@ import org.keycloak.social.github.GitHubIdentityProvider;
import org.keycloak.social.github.GitHubIdentityProviderFactory;
import org.keycloak.social.google.GoogleIdentityProvider;
import org.keycloak.social.google.GoogleIdentityProviderFactory;
-import org.keycloak.social.twitter.TwitterIdentityProvider;
-import org.keycloak.social.twitter.TwitterIdentityProviderFactory;
import org.keycloak.social.linkedin.LinkedInIdentityProvider;
import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import org.keycloak.social.stackoverflow.StackOverflowIdentityProviderConfig;
+import org.keycloak.social.stackoverflow.StackoverflowIdentityProvider;
+import org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory;
+import org.keycloak.social.twitter.TwitterIdentityProvider;
+import org.keycloak.social.twitter.TwitterIdentityProviderFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -164,6 +167,8 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
assertTwitterIdentityProviderConfig(identityProvider);
} else if (LinkedInIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
assertLinkedInIdentityProviderConfig(identityProvider);
+ } else if (StackoverflowIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
+ assertStackoverflowIdentityProviderConfig(identityProvider);
} else {
continue;
}
@@ -262,8 +267,8 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
}
private void assertLinkedInIdentityProviderConfig(IdentityProviderModel identityProvider) {
- LinkedInIdentityProvider gitHubIdentityProvider = new LinkedInIdentityProviderFactory().create(identityProvider);
- OAuth2IdentityProviderConfig config = gitHubIdentityProvider.getConfig();
+ LinkedInIdentityProvider liIdentityProvider = new LinkedInIdentityProviderFactory().create(identityProvider);
+ OAuth2IdentityProviderConfig config = liIdentityProvider.getConfig();
assertEquals("model-linkedin", config.getAlias());
assertEquals(LinkedInIdentityProviderFactory.PROVIDER_ID, config.getProviderId());
@@ -278,6 +283,24 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
assertEquals(LinkedInIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
}
+ private void assertStackoverflowIdentityProviderConfig(IdentityProviderModel identityProvider) {
+ StackoverflowIdentityProvider soIdentityProvider = new StackoverflowIdentityProviderFactory().create(identityProvider);
+ StackOverflowIdentityProviderConfig config = soIdentityProvider.getConfig();
+
+ assertEquals("model-stackoverflow", config.getAlias());
+ assertEquals(StackoverflowIdentityProviderFactory.PROVIDER_ID, config.getProviderId());
+ assertEquals(true, config.isEnabled());
+ assertEquals(true, config.isUpdateProfileFirstLogin());
+ assertEquals(false, config.isAuthenticateByDefault());
+ assertEquals(false, config.isStoreToken());
+ assertEquals("clientId", config.getClientId());
+ assertEquals("clientSecret", config.getClientSecret());
+ assertEquals("keyValue", config.getKey());
+ assertEquals(StackoverflowIdentityProvider.AUTH_URL, config.getAuthorizationUrl());
+ assertEquals(StackoverflowIdentityProvider.TOKEN_URL, config.getTokenUrl());
+ assertEquals(StackoverflowIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
+ }
+
private void assertTwitterIdentityProviderConfig(IdentityProviderModel identityProvider) {
TwitterIdentityProvider twitterIdentityProvider = new TwitterIdentityProviderFactory().create(identityProvider);
OAuth2IdentityProviderConfig config = twitterIdentityProvider.getConfig();
diff --git a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
index 96cdc96..7174420 100755
--- a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
+++ b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
@@ -76,6 +76,21 @@
}
},
{
+ "alias" : "model-stackoverflow",
+ "providerId" : "stackoverflow",
+ "enabled": true,
+ "updateProfileFirstLogin" : "true",
+ "storeToken": false,
+ "config": {
+ "key": "keyValue",
+ "authorizationUrl": "authorizationUrl",
+ "tokenUrl": "tokenUrl",
+ "userInfoUrl": "userInfoUrl",
+ "clientId": "clientId",
+ "clientSecret": "clientSecret"
+ }
+ },
+ {
"alias" : "model-saml-signed-idp",
"providerId" : "saml",
"enabled": true,