KEYCLOAK-1046 - added modules definition, tests and documentation

This commit is contained in:
Vlastimil Elias 2015-03-24 08:49:31 +01:00
parent c2fe63db63
commit ee742be811
13 changed files with 261 additions and 218 deletions

View file

@ -1,5 +1,8 @@
package org.keycloak.broker.oidc.util;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -12,9 +15,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
* @author Vlastimil Elias (velias at redhat dot com)

View file

@ -240,6 +240,10 @@
<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"/>
</module-def>

View file

@ -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"/>

View file

@ -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"/>

View file

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

View file

@ -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"/>

View file

@ -808,6 +808,78 @@
</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>
<section>

View file

@ -238,7 +238,7 @@ ol#kc-totp-settings li:first-of-type {
}
.zocial {
width: 125px;
width: 150px;
}
.zocial:hover {

View file

@ -21,6 +21,7 @@
<module>twitter</module>
<module>facebook</module>
<module>linkedin</module>
<module>stackoverflow</module>
</modules>
</project>

View file

@ -32,7 +32,7 @@ import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.social.SocialIdentityProvider;
/**
* Stackoverflow social provider. See https://developer.linkedin.com/docs/oauth2
* Stackoverflow social provider. See https://api.stackexchange.com/docs/authentication
*
* @author Vlastimil Elias (velias at redhat dot com)
*/
@ -67,14 +67,13 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
FederatedIdentity user = new FederatedIdentity(getJsonProperty(profile, "user_id"));
user.setUsername(extractUsernameFromProfileURL(getJsonProperty(profile, "link")));
// TODO username contains html encoding of national chracters sometimes
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);
throw new IdentityBrokerException("Could not obtain user profile from Stackoverflow: " + e.getMessage(), e);
}
}
@ -93,7 +92,7 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
if (pe.length >= 3) {
return URLDecoder.decode(pe[2], "UTF-8");
} else {
log.warn("Stackoverflow profile URL path is without second part: " + profileURL);
log.warn("Stackoverflow profile URL path is without third part: " + profileURL);
}
} else {
log.warn("Stackoverflow profile URL is without path part: " + profileURL);
@ -201,104 +200,6 @@ public class StackoverflowIdentityProvider extends AbstractOAuth2IdentityProvide
{ "&", "amp" }, // & - ampersand
{ "<", "lt" }, // < - less-than
{ ">", "gt" }, // > - greater-than
// Mapping to escape ISO-8859-1 characters to their named HTML 3.x equivalents.
{ "\u00A0", "nbsp" }, // non-breaking space
{ "\u00A1", "iexcl" }, // inverted exclamation mark
{ "\u00A2", "cent" }, // cent sign
{ "\u00A3", "pound" }, // pound sign
{ "\u00A4", "curren" }, // currency sign
{ "\u00A5", "yen" }, // yen sign = yuan sign
{ "\u00A6", "brvbar" }, // broken bar = broken vertical bar
{ "\u00A7", "sect" }, // section sign
{ "\u00A8", "uml" }, // diaeresis = spacing diaeresis
{ "\u00A9", "copy" }, // © - copyright sign
{ "\u00AA", "ordf" }, // feminine ordinal indicator
{ "\u00AB", "laquo" }, // left-pointing double angle quotation mark = left pointing guillemet
{ "\u00AC", "not" }, // not sign
{ "\u00AD", "shy" }, // soft hyphen = discretionary hyphen
{ "\u00AE", "reg" }, // ® - registered trademark sign
{ "\u00AF", "macr" }, // macron = spacing macron = overline = APL overbar
{ "\u00B0", "deg" }, // degree sign
{ "\u00B1", "plusmn" }, // plus-minus sign = plus-or-minus sign
{ "\u00B2", "sup2" }, // superscript two = superscript digit two = squared
{ "\u00B3", "sup3" }, // superscript three = superscript digit three = cubed
{ "\u00B4", "acute" }, // acute accent = spacing acute
{ "\u00B5", "micro" }, // micro sign
{ "\u00B6", "para" }, // pilcrow sign = paragraph sign
{ "\u00B7", "middot" }, // middle dot = Georgian comma = Greek middle dot
{ "\u00B8", "cedil" }, // cedilla = spacing cedilla
{ "\u00B9", "sup1" }, // superscript one = superscript digit one
{ "\u00BA", "ordm" }, // masculine ordinal indicator
{ "\u00BB", "raquo" }, // right-pointing double angle quotation mark = right pointing guillemet
{ "\u00BC", "frac14" }, // vulgar fraction one quarter = fraction one quarter
{ "\u00BD", "frac12" }, // vulgar fraction one half = fraction one half
{ "\u00BE", "frac34" }, // vulgar fraction three quarters = fraction three quarters
{ "\u00BF", "iquest" }, // inverted question mark = turned question mark
{ "\u00C0", "Agrave" }, // А - uppercase A, grave accent
{ "\u00C1", "Aacute" }, // Б - uppercase A, acute accent
{ "\u00C2", "Acirc" }, // В - uppercase A, circumflex accent
{ "\u00C3", "Atilde" }, // Г - uppercase A, tilde
{ "\u00C4", "Auml" }, // Д - uppercase A, umlaut
{ "\u00C5", "Aring" }, // Е - uppercase A, ring
{ "\u00C6", "AElig" }, // Ж - uppercase AE
{ "\u00C7", "Ccedil" }, // З - uppercase C, cedilla
{ "\u00C8", "Egrave" }, // И - uppercase E, grave accent
{ "\u00C9", "Eacute" }, // Й - uppercase E, acute accent
{ "\u00CA", "Ecirc" }, // К - uppercase E, circumflex accent
{ "\u00CB", "Euml" }, // Л - uppercase E, umlaut
{ "\u00CC", "Igrave" }, // М - uppercase I, grave accent
{ "\u00CD", "Iacute" }, // Н - uppercase I, acute accent
{ "\u00CE", "Icirc" }, // О - uppercase I, circumflex accent
{ "\u00CF", "Iuml" }, // П - uppercase I, umlaut
{ "\u00D0", "ETH" }, // Р - uppercase Eth, Icelandic
{ "\u00D1", "Ntilde" }, // С - uppercase N, tilde
{ "\u00D2", "Ograve" }, // Т - uppercase O, grave accent
{ "\u00D3", "Oacute" }, // У - uppercase O, acute accent
{ "\u00D4", "Ocirc" }, // Ф - uppercase O, circumflex accent
{ "\u00D5", "Otilde" }, // Х - uppercase O, tilde
{ "\u00D6", "Ouml" }, // Ц - uppercase O, umlaut
{ "\u00D7", "times" }, // multiplication sign
{ "\u00D8", "Oslash" }, // Ш - uppercase O, slash
{ "\u00D9", "Ugrave" }, // Щ - uppercase U, grave accent
{ "\u00DA", "Uacute" }, // Ъ - uppercase U, acute accent
{ "\u00DB", "Ucirc" }, // Ы - uppercase U, circumflex accent
{ "\u00DC", "Uuml" }, // Ь - uppercase U, umlaut
{ "\u00DD", "Yacute" }, // Э - uppercase Y, acute accent
{ "\u00DE", "THORN" }, // Ю - uppercase THORN, Icelandic
{ "\u00DF", "szlig" }, // Я - lowercase sharps, German
{ "\u00E0", "agrave" }, // а - lowercase a, grave accent
{ "\u00E1", "aacute" }, // б - lowercase a, acute accent
{ "\u00E2", "acirc" }, // в - lowercase a, circumflex accent
{ "\u00E3", "atilde" }, // г - lowercase a, tilde
{ "\u00E4", "auml" }, // д - lowercase a, umlaut
{ "\u00E5", "aring" }, // е - lowercase a, ring
{ "\u00E6", "aelig" }, // ж - lowercase ae
{ "\u00E7", "ccedil" }, // з - lowercase c, cedilla
{ "\u00E8", "egrave" }, // и - lowercase e, grave accent
{ "\u00E9", "eacute" }, // й - lowercase e, acute accent
{ "\u00EA", "ecirc" }, // к - lowercase e, circumflex accent
{ "\u00EB", "euml" }, // л - lowercase e, umlaut
{ "\u00EC", "igrave" }, // м - lowercase i, grave accent
{ "\u00ED", "iacute" }, // н - lowercase i, acute accent
{ "\u00EE", "icirc" }, // о - lowercase i, circumflex accent
{ "\u00EF", "iuml" }, // п - lowercase i, umlaut
{ "\u00F0", "eth" }, // р - lowercase eth, Icelandic
{ "\u00F1", "ntilde" }, // с - lowercase n, tilde
{ "\u00F2", "ograve" }, // т - lowercase o, grave accent
{ "\u00F3", "oacute" }, // у - lowercase o, acute accent
{ "\u00F4", "ocirc" }, // ф - lowercase o, circumflex accent
{ "\u00F5", "otilde" }, // х - lowercase o, tilde
{ "\u00F6", "ouml" }, // ц - lowercase o, umlaut
{ "\u00F7", "divide" }, // division sign
{ "\u00F8", "oslash" }, // ш - lowercase o, slash
{ "\u00F9", "ugrave" }, // щ - lowercase u, grave accent
{ "\u00FA", "uacute" }, // ъ - lowercase u, acute accent
{ "\u00FB", "ucirc" }, // ы - lowercase u, circumflex accent
{ "\u00FC", "uuml" }, // ь - lowercase u, umlaut
{ "\u00FD", "yacute" }, // э - lowercase y, acute accent
{ "\u00FE", "thorn" }, // ю - lowercase thorn, Icelandic
{ "\u00FF", "yuml" }, // я - lowercase y, umlaut
};
private static final int MIN_ESCAPE = 2;

View file

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

View file

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

View file

@ -75,6 +75,21 @@
"clientSecret": "clientSecret"
}
},
{
"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",