Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Bill Burke 2015-04-01 10:33:25 -04:00
commit 10ced1e908
45 changed files with 1567 additions and 1380 deletions

View file

@ -143,7 +143,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
;
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
return UriBuilder.fromPath(getConfig().getAuthorizationUrl())
return UriBuilder.fromUri(getConfig().getAuthorizationUrl())
.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
.queryParam(OAUTH2_PARAMETER_STATE, request.getState())
.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")

View file

@ -168,11 +168,5 @@
<column name="VALUE" type="VARCHAR(255)"/>
</createTable>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_ENABLED_EVENT_TYPES" constraintName="FK_H846O4H0W8EPX5NWEDRF5Y69J" referencedColumnNames="ID" referencedTableName="REALM"/>
<addColumn tableName="EVENT_ENTITY">
<column name="EVENT_GROUP" type="VARCHAR(255)"/>
<column name="REPRESENTATION" type="BLOB"/>
</addColumn>
</changeSet>
</databaseChangeLog>

View file

@ -35,6 +35,9 @@ public class RSATokenVerifier {
if (user == null) {
throw new VerificationException("Token user was null.");
}
if (realmUrl == null) {
throw new VerificationException("Realm URL is null. Make sure to add auth-server-url to the configuration of your adapter!");
}
if (!realmUrl.equals(token.getIssuer())) {
throw new VerificationException("Token audience doesn't match domain.");

View file

@ -0,0 +1,27 @@
package org.keycloak.util;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ObjectUtil {
private ObjectUtil() {}
/**
*
* @param str1
* @param str2
* @return true if both strings are null or equal
*/
public static boolean isEqualOrNull(Object str1, Object str2) {
if (str1 == null && str2 == null) {
return true;
}
if ((str1 != null && str2 == null) || (str1 == null && str2 != null)) {
return false;
}
return str1.equals(str2);
}
}

View file

@ -87,6 +87,7 @@ try {
if (isPublic()) { // if client is public access type
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal"));
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "password"));
} else {
String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret");
post.setHeader("Authorization", authorization);

View file

@ -99,7 +99,7 @@ public class MyEventListenerProvider implements EventListenerProvider {
script or manually create a folder inside KEYCLOAK_HOME/modules and add your jar and a <literal>module.xml</literal>.
For example to add the event listener sysout example provider using the jboss-cli script execute:
<programlisting><![CDATA[{
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.event-sysout --resources=event-listener-sysout-example.jar"
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.event-sysout --resources=target/event-listener-sysout-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api,org.keycloak.keycloak-events-api"
}]]></programlisting>
Or to manually create it start by creating the folder <literal>KEYCLOAK_HOME/modules/org/keycloak/examples/event-sysout/main</literal>.
Then copy <literal>event-listener-sysout-example.jar</literal> to this folder and create <literal>module.xml</literal>

View file

@ -780,175 +780,50 @@ All configuration options are optional. Default value for directory is <literal>
</section>
</section>
<section>
<title>Configuring Servers from the Subsystem</title>
<para>
If you are using WildFly or EAP,he Keycloak server is deployed and configured from the Keycloak subsystem. This makes provisioning simpler in a domain environment.
It also allows you to create more than one Keycloak server instance inside a single WildFly instance. And, you can upload providers, themes, and
server configurations without disturbing Keycloak's auth-server.war.
</para>
<section>
<title>Manually Creating A Server</title>
<para>
A Keycloak server can be declared by editing standalone.xml or domain.xml.
</para>
<para>
<programlisting><![CDATA[
<server xmlns="urn:jboss:domain:1.4">
<profile>
<section>
<title>Adding Keycloak server in Domain Mode</title>
<para>
In domain mode, you start the server with the "domain" command instead of the "standalone" command. In this case, the Keycloak subsystem is
defined in domain/configuration/domain.xml instead of standalone/configuration.standalone.xml. Inside domain.xml, you will see more than one
profile. A Keycloak subsystem can be defined in zero or more of those profiles.
</para>
<para>
To enable Keycloak for a server profile edit domain/configuration/domain.xml. To the <literal>extensions</literal>
element add the Keycloak extension:
<programlisting><![CDATA[
<extensions>
...
<extension module="org.keycloak.keycloak-subsystem"/>
</extensions>
]]></programlisting>
Then you need to add the server to the required server profiles. By default WildFly starts two servers
in the main-server-group which uses the full profile. To add Keycloak for this profile add the Keycloak
subsystem to the <literal>profile</literal> element with <literal>name</literal> full:
<programlisting><![CDATA[
<profile name="full">
...
<subsystem xmlns="urn:jboss:domain:keycloak:1.0">
<auth-server name="keycloak-1">
<auth-server name="main-auth-server">
<enabled>true</enabled>
<web-context>auth</web-context>
</auth-server>
<auth-server name="keyclaok-2">
<enabled>false</enabled>
<web-context>auth2</web-context>
</auth-server>
</subsystem>
</profile>
]]>
</programlisting>
</para>
<warning>
<para>
If you create more than one Keycloak server, you will need to use CLI to fully configure each instance. At the least,
you will need to run the <link linkend="uploading-extra-config">update-server-config</link> operation.
</para>
</warning>
</section>
<section>
<title>Using CLI and CLI GUI with the Keycloak Subsystem</title>
<para>
Servers can also be added/removed or enabled/disabled at runtime using the <ulink url="https://developer.jboss.org/wiki/CommandLineInterface">CLI</ulink> or
<ulink url="https://developer.jboss.org/wiki/AGUIForTheCommandLineInterface">CLI GUI</ulink> tool. These are tools that ship with WildFly/EAP and also with
the Keycloak Appliance installation. See <ulink url="https://developer.jboss.org/wiki/CommandLineInterface">CLI</ulink> or
<ulink url="https://developer.jboss.org/wiki/AGUIForTheCommandLineInterface">CLI GUI</ulink> documentation to learn more about how to start the tools,
issue commands, and create CLI scripts.
</para>
<para>
To start CLI with the Keycloak Appliance install:
<programlisting><![CDATA[
cd <APPLIANCE_INSTALL_DIR>/keycloak/bin
./jboss-cli.sh --gui
or
./jboss.cli.bat --gui]]>
</programlisting>
<note>Your server must be running to start in --gui mode.</note>
</para>
<section>
<title>Basic CLI Commands</title>
<para>
Command to add a server in CLI:
<programlisting><![CDATA[
/subsystem=keycloak/auth-server=my-auth-server/:add(web-context=my-auth, enabled=true)]]>
</programlisting>
Because "enabled=true", a new Keycloak server will be immediately deployed. By default "enabled" is set to false.
</para>
<para>
Command to remove a server in CLI:
<programlisting><![CDATA[
/subsystem=keycloak/auth-server=my-auth-server/:remove]]>
</programlisting>
The Keycloak server will be immediately deleted and undeployed.
</para>
<para>
Command to enable or disable a server in CLI:
<programlisting><![CDATA[
/subsystem=keycloak/auth-server=foo/:write-attribute(name=enabled,value=true)]]>
</programlisting>
The Keycloak server will be immediately deployed or undeployed, but not deleted.
</para>
</section>
<section id="uploading-extra-config">
<title>Uploading extra configuration using CLI</title>
<para>
The Keycloak subsystem allows you to upload keycloak-server.json, provider jars, and theme jars to a Keycloak server instance. The
CLI operations for this are "update-server-config" and "add-provider". You may use CLI, CLI GUI, or CLI scripts for these operations. The following
examples are shown using <ulink url="https://developer.jboss.org/wiki/AGUIForTheCommandLineInterface">CLI GUI</ulink> for clarity.
</para>
<para>
To use a new keycloak-server.json file for your server, find your server under the Keycloak subsystem. Then right-click the server,
select "update-server-config", and upload your file.
</para>
<para>
<imagedata fileref="images/update-server-config-select.png"/>
</para>
<para>
<imagedata fileref="images/update-server-config-dialog.png"/>
</para>
<warning>
<para>
If you use the update-server-config operation, you should delete or rename &lt;WILDFLY_HOME&gt;/standalone/configuration/keycloak-server.json.
Otherwise, all Keycloak server instances will use this file instead of your uploaded file.
</para>
</warning>
<para>
To upload a new provider jar or theme jar to your server, find your server under the Keycloak subsystem. Then right-click the server,
select "add-provider", and upload your file.
</para>
<para>
<imagedata fileref="images/add-provider-select.png"/>
</para>
<para>
<imagedata fileref="images/add-provider-dialog.png"/>
</para>
</section>
<section>
<title>Working with overlays</title>
<para>
When you upload a provider jar, theme jar, or keycloak-server.json file, you are creating an overlay. That is, the file is "overlayed"
onto the Keycloak server at deploy time. There are two additional operations that help you manage these overlays. They are "list-overlays" and
"remove-overlay". Here are CLI examples of these operations.
</para>
<para>
<programlisting>
/subsystem=keycloak/auth-server=my-auth-server/:list-overlays
{
"outcome" => "success",
"result" => [
"/WEB-INF/classes/META-INF/keycloak-server.json",
"/WEB-INF/lib/federation-properties-example.jar"
],
}</programlisting>
<programlisting>
/subsystem=keycloak/auth-server=my-auth-server/:remove-overlay(overlay-file-path=/WEB-INF/lib/federation-properties-example.jar,redeploy=true)
{
"outcome" => "success",
}</programlisting>
</para>
<para>
<note>
Notice in the "list-overlays" operation, the full path to the server config is
/WEB-INF/classes/META-INF/keycloak-server.json. This is always the uploaded path for an "update-server-config" operation.
If you remove this overlay, the Keycloak server will revert to its default keycloak-server.json. If you have a
keycloak-server.json file in your &lt;WILDFLY_HOME&gt;/standalone/configuration directory, it will always take precedence
over both the default and the overlay.
</note>
</para>
</section>
</section>
<section>
<title>Adding a Keycloak server in Domain Mode</title>
<para>
In domain mode, you start the server with the "domain" command instead of the "standalone" command. In this case, the Keycloak subsystem is
defined in domain/configuration/domain.xml instead of standalone/configuration.standalone.xml. Inside domain.xml, you will see more than one
profile. A Keycloak subsystem can be defined in zero or more of those profiles.
</para>
<para>
In the example below, a Keycloak server named "foo" is defined in the "full" profile. The "full" profile is assigned to the "main-server-group".
Every WildFly instance that belongs to "main-server-group" will get an identically configured deployment of the "foo" Keycloak server.
</para>
<para>
All operations discussed earlier are valid for a Keycloak server in a domain. You can enable/disable, upload new keyclaok-server.json, and add provider jars.
In the following example, any changes that are made to the "foo" server will be automatically propogated to every instance in "main-server-group".
</para>
<para>
<imagedata fileref="images/domain-mode.png"/>
</para>
</section>
]]></programlisting>
</para>
<para>
To configure the server copy <literal>standalone/configuration/keycloak-server.json</literal> to
<literal>domain/servers/&lt;SERVER NAME&gt;/configuration</literal>. The configuration should be identical
for all servers in a group.
</para>
<para>
Follow the <link linkend='clustering'>Clustering</link> section of the documentation to configure Keycloak
for clustering. In domain mode it doesn't make much sense to not configure Keycloak in cluster mode.
</para>
<para>
To deploy custom providers and themes you should deploys these as modules and make sure the modules are
available to all servers in the group. See <link linkend='providers'>Providers</link> and
<link linkend='themes'>Themes</link> sections for more information on how to do this.
</para>
</section>
</chapter>

View file

@ -33,7 +33,7 @@ documentation.
Please take a look on [Facebook Developer Console](https://developers.facebook.com/apps/) for more details. Make sure to use the correct
redirect URI to be used as URL on Facebook. The facebook will redirect to this URI after finish authentication. For this example, it's the URL
[http://localhost:8080/auth/realms/facebook-identity-provider-realm/broker/facebook](http://localhost:8080/auth/realms/facebook-identity-provider-realm/broker/facebook) .
[http://localhost:8080/auth/realms/facebook-identity-provider-realm/broker/facebook/endpoint](http://localhost:8080/auth/realms/facebook-identity-provider-realm/broker/facebook/endpoint) .
You can also determine this redirect URI from Keycloak admin console (It's in Identity provider settings for Facebook provider).
Once you have a Facebook Application configured, you need to obtain both **App ID** and **App Secret** and update the

View file

@ -53,9 +53,8 @@
],
"identityProviders": [
{
"id" : "facebook",
"alias" : "facebook",
"providerId" : "facebook",
"name" : "Facebook",
"enabled": true,
"updateProfileFirstLogin" : "true",
"storeToken" : "true",

View file

@ -54,7 +54,7 @@ Once you have a Google Application configured, you need to obtain both **Client
Please, update both *clientId* and *clientSecret* configuration options with the **Client ID** and **Client Secret**.
Make sure to use the correct redirect URI to be used as URL on Google. The Google will redirect to this URI after finish authentication. For this example, it's the URL
[http://localhost:8080/auth/realms/google-identity-provider-realm/broker/google](http://localhost:8080/auth/realms/google-identity-provider-realm/broker/google) .
[http://localhost:8080/auth/realms/google-identity-provider-realm/broker/google/endpoint](http://localhost:8080/auth/realms/google-identity-provider-realm/broker/google/endpoint) .
You can also determine the redirect URI from Keycloak admin console (It's in Identity provider settings for Google provider).
Make sure you've set up the Keycloak Server

View file

@ -53,9 +53,8 @@
],
"identityProviders": [
{
"id" : "google",
"alias" : "google",
"providerId" : "google",
"name" : "Google",
"enabled": true,
"updateProfileFirstLogin" : "true",
"storeToken" : "true",

View file

@ -5,7 +5,7 @@ What is it?
This example demonstrates how to broker a SAML Identity Provider in KeyCloak. In this case, the SAML Identity Provider
belongs to a different realm than the application and we want to trust users from one realm to authenticate and access the
applications in aonther realm.
applications in another realm.
There are two main realms in this example:

View file

@ -47,9 +47,8 @@
],
"identityProviders": [
{
"id" : "saml-identity-provider",
"alias" : "saml-identity-provider",
"providerId" : "saml",
"name" : "SAML v2 Identity Provider",
"enabled": true,
"updateProfileFirstLogin" : "true",
"storeToken" : "true",

View file

@ -28,10 +28,11 @@
},
"applications": [
{
"name": "http://localhost:8080/auth/",
"name": "http://localhost:8080/auth/realms/saml-broker-authentication-realm",
"protocol": "saml",
"enabled": true,
"redirectUris": [
"http://localhost:8080/auth/realms/saml-broker-authentication-realm/broker/saml-identity-provider"
"http://localhost:8080/auth/realms/saml-broker-authentication-realm/broker/saml-identity-provider/endpoint"
],
"attributes": {
"saml.assertion.signature": "true",

View file

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

View file

@ -64,9 +64,8 @@
],
"identityProviders": [
{
"id" : "twitter",
"alias" : "twitter",
"providerId" : "twitter",
"name" : "Twitter",
"enabled": true,
"updateProfileFirstLogin" : "true",
"storeToken" : "true",

View file

@ -2,6 +2,7 @@
"realm" : "cors",
"resource" : "cors-database-service",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "http://localhost-auth:8080/auth",
"bearer-only" : true,
"ssl-required": "external",
"enable-cors": true

View file

@ -80,6 +80,7 @@ public class AdminClient {
List <NameValuePair> formparams = new ArrayList <NameValuePair>();
formparams.add(new BasicNameValuePair("username", "admin"));
formparams.add(new BasicNameValuePair("password", "password"));
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "password"));
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "admin-client"));
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);

View file

@ -26,11 +26,11 @@ User <b id="subject"></b> made this request.
if (keycloak.idToken) {
document.getElementById('profileType').innerHTML = 'IDToken';
document.getElementById('username').innerHTML = keycloak.idToken.preferred_username;
document.getElementById('email').innerHTML = keycloak.idToken.email;
document.getElementById('name').innerHTML = keycloak.idToken.name;
document.getElementById('givenName').innerHTML = keycloak.idToken.given_name;
document.getElementById('familyName').innerHTML = keycloak.idToken.family_name;
document.getElementById('username').innerHTML = keycloak.idTokenParsed.preferred_username;
document.getElementById('email').innerHTML = keycloak.idTokenParsed.email;
document.getElementById('name').innerHTML = keycloak.idTokenParsed.name;
document.getElementById('givenName').innerHTML = keycloak.idTokenParsed.given_name;
document.getElementById('familyName').innerHTML = keycloak.idTokenParsed.family_name;
} else {
keycloak.loadUserProfile(function() {
document.getElementById('profileType').innerHTML = 'Account Service';

View file

@ -1,7 +1,17 @@
Example Event Listener that prints events to System.out
=======================================================
To deploy copy target/event-listener-sysout-example.jar to standalone/configuration/providers.
To deploy copy target/event-listener-sysout-example.jar to standalone/configuration/providers. Alternatively you can deploy as a module by running:
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.event-sysout --resources=target/event-listener-sysout-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api,org.keycloak.keycloak-events-api"
Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
"providers": [
....
"module:org.keycloak.examples.event-sysout"
],
Then start (or restart) the server. Once started open the admin console, select your realm, then click on Events,
followed by config. Click on Listeners select box, then pick sysout from the dropdown. After this try to logout and
login again to see events printed to System.out.

View file

@ -1,7 +1,16 @@
Example Event Store that stores events in memory
================================================
To deploy copy target/event-store-mem-example.jar to standalone/configuration/providers.
To deploy copy target/event-store-mem-example.jar to standalone/configuration/providers. Alternatively you can deploy as a module by running:
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.event-inmem --resources=target/event-store-mem-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api,org.keycloak.keycloak-events-api"
Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
"providers": [
....
"module:org.keycloak.examples.event-inmem"
],
Then edit standalone/configuration/keycloak-server.json, change:

View file

@ -59,7 +59,7 @@ public class MemEventStoreProvider implements EventStoreProvider {
@Override
public void onEvent(Event event) {
if (!excludedEvents.contains(event.getType())) {
if (excludedEvents == null || !excludedEvents.contains(event.getType())) {
events.add(0, event);
}
}

View file

@ -2,7 +2,18 @@ Example User Federation Provider
===================================================
This is an example of user federation backed by a simple properties file. This properties file only contains username/password
key pairs. To deploy, build this directory then take the jar and copy it to standalone/configuration/providers.
key pairs. To deploy, build this directory then take the jar and copy it to standalone/configuration/providers. Alternatively you can deploy as a module by running:
KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.userprops --resources=target/federation-properties-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api"
Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
"providers": [
....
"module:org.keycloak.examples.userprops"
],
You will then have to restart the authentication server.
The ClasspathPropertiesFederationProvider is an example of a readonly provider. If you go to the Users/Federation

View file

@ -1,17 +1,18 @@
package org.keycloak.account;
import org.apache.http.client.methods.HttpHead;
import org.keycloak.events.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.Provider;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.List;
import org.keycloak.events.Event;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -26,6 +27,8 @@ public interface AccountProvider extends Provider {
AccountProvider setError(String message, Object ... parameters);
AccountProvider setErrors(List<FormMessage> messages);
AccountProvider setSuccess(String message, Object ... parameters);
AccountProvider setWarning(String message, Object ... parameters);

View file

@ -1,25 +1,54 @@
package org.keycloak.account.freemarker;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.account.AccountPages;
import org.keycloak.account.AccountProvider;
import org.keycloak.account.freemarker.model.*;
import org.keycloak.account.freemarker.model.AccountBean;
import org.keycloak.account.freemarker.model.AccountFederatedIdentityBean;
import org.keycloak.account.freemarker.model.FeaturesBean;
import org.keycloak.account.freemarker.model.LogBean;
import org.keycloak.account.freemarker.model.PasswordBean;
import org.keycloak.account.freemarker.model.RealmBean;
import org.keycloak.account.freemarker.model.ReferrerBean;
import org.keycloak.account.freemarker.model.SessionsBean;
import org.keycloak.account.freemarker.model.TotpBean;
import org.keycloak.account.freemarker.model.UrlBean;
import org.keycloak.events.Event;
import org.keycloak.freemarker.*;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.freemarker.beans.LocaleBean;
import org.keycloak.freemarker.beans.MessageBean;
import org.keycloak.freemarker.beans.MessageFormatterMethod;
import org.keycloak.freemarker.beans.MessageType;
import org.keycloak.freemarker.beans.MessagesPerFieldBean;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.resources.flows.Urls;
import javax.ws.rs.core.*;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.*;
import org.keycloak.freemarker.beans.LocaleBean;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -43,13 +72,10 @@ public class FreeMarkerAccountProvider implements AccountProvider {
private FreeMarkerUtil freeMarker;
private HttpHeaders headers;
public static enum MessageType {SUCCESS, WARNING, ERROR}
private UriInfo uriInfo;
private String message;
private Object[] parameters;
private MessageType messageType;
private List<FormMessage> messages = null;
private MessageType messageType = MessageType.ERROR;
public FreeMarkerAccountProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
this.session = session;
@ -87,13 +113,13 @@ public class FreeMarkerAccountProvider implements AccountProvider {
}
Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, headers);
Properties messages;
Properties messagesBundle;
try {
messages = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messages));
messagesBundle = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messages = new Properties();
messagesBundle = new Properties();
}
URI baseUri = uriInfo.getBaseUri();
@ -107,15 +133,19 @@ public class FreeMarkerAccountProvider implements AccountProvider {
attributes.put("stateChecker", stateChecker);
}
if (message != null) {
String formattedMessage;
if(messages.containsKey(message)){
formattedMessage = new MessageFormat(messages.getProperty(message),locale).format(parameters);
}else{
formattedMessage = message;
MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean();
if (messages != null) {
MessageBean wholeMessage = new MessageBean(null, messageType);
for (FormMessage message : this.messages) {
String formattedMessageText = formatMessage(message, messagesBundle, locale);
if (formattedMessageText != null) {
wholeMessage.appendSummaryLine(formattedMessageText);
messagesPerField.addMessage(message.getField(), formattedMessageText, messageType);
}
}
attributes.put("message", new MessageBean(formattedMessage, messageType));
attributes.put("message", wholeMessage);
}
attributes.put("messagesPerField", messagesPerField);
if (referrer != null) {
attributes.put("referrer", new ReferrerBean(referrer));
@ -134,7 +164,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
b = UriBuilder.fromUri(baseQueryUri).path(uriInfo.getPath());
break;
}
attributes.put("locale", new LocaleBean(realm, locale, b, messages));
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
attributes.put("features", new FeaturesBean(identityProviderEnabled, eventsEnabled, passwordUpdateSupported));
@ -174,27 +204,45 @@ public class FreeMarkerAccountProvider implements AccountProvider {
return this;
}
protected void setMessage(MessageType type, String message, Object... parameters) {
messageType = type;
messages = new ArrayList<>();
messages.add(new FormMessage(null, message, parameters));
}
protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) {
if (message == null)
return null;
if (messagesBundle.containsKey(message.getMessage())) {
return new MessageFormat(messagesBundle.getProperty(message.getMessage()), locale).format(message.getParameters());
} else {
return message.getMessage();
}
}
@Override
public AccountProvider setErrors(List<FormMessage> messages) {
this.messageType = MessageType.ERROR;
this.messages = new ArrayList<>(messages);
return this;
}
@Override
public AccountProvider setError(String message, Object ... parameters) {
this.message = message;
this.parameters = parameters;
this.messageType = MessageType.ERROR;
setMessage(MessageType.ERROR, message, parameters);
return this;
}
@Override
public AccountProvider setSuccess(String message, Object ... parameters) {
this.message = message;
this.parameters = parameters;
this.messageType = MessageType.SUCCESS;
setMessage(MessageType.SUCCESS, message, parameters);
return this;
}
@Override
public AccountProvider setWarning(String message, Object ... parameters) {
this.message = message;
this.parameters = parameters;
this.messageType = MessageType.WARNING;
setMessage(MessageType.WARNING, message, parameters);
return this;
}

View file

@ -19,9 +19,7 @@
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.account.freemarker.model;
import org.keycloak.account.freemarker.FreeMarkerAccountProvider;
package org.keycloak.freemarker.beans;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -30,9 +28,9 @@ public class MessageBean {
private String summary;
private FreeMarkerAccountProvider.MessageType type;
private MessageType type;
public MessageBean(String message, FreeMarkerAccountProvider.MessageType type) {
public MessageBean(String message, MessageType type) {
this.summary = message;
this.type = type;
}
@ -41,20 +39,29 @@ public class MessageBean {
return summary;
}
public void appendSummaryLine(String newLine) {
if (newLine == null)
return;
if (summary == null)
summary = newLine;
else
summary = summary + "<br>" + newLine;
}
public String getType() {
return this.type.toString().toLowerCase();
}
public boolean isSuccess() {
return FreeMarkerAccountProvider.MessageType.SUCCESS.equals(this.type);
return MessageType.SUCCESS.equals(this.type);
}
public boolean isWarning() {
return FreeMarkerAccountProvider.MessageType.WARNING.equals(this.type);
return MessageType.WARNING.equals(this.type);
}
public boolean isError() {
return FreeMarkerAccountProvider.MessageType.ERROR.equals(this.type);
return MessageType.ERROR.equals(this.type);
}
}

View file

@ -0,0 +1,17 @@
/*
* JBoss, Home of Professional Open Source
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
*/
package org.keycloak.freemarker.beans;
/**
* Enum with types of messages.
*
* @author Vlastimil Elias (velias at redhat dot com)
*/
public enum MessageType {
SUCCESS, WARNING, ERROR
}

View file

@ -0,0 +1,73 @@
/*
* JBoss, Home of Professional Open Source
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
*/
package org.keycloak.freemarker.beans;
import java.util.HashMap;
import java.util.Map;
/**
* Bean used to hold form messages per field. Stored under <code>messagesPerField</code> key in Freemarker context.
*
* @author Vlastimil Elias (velias at redhat dot com)
*/
public class MessagesPerFieldBean {
private Map<String, MessageBean> messagesPerField = new HashMap<String, MessageBean>();
public void addMessage(String field, String messageText, MessageType messageType) {
if (messageText == null || messageText.trim().isEmpty())
return;
if (field == null)
field = "global";
MessageBean fm = messagesPerField.get(field);
if (fm == null) {
messagesPerField.put(field, new MessageBean(messageText, messageType));
} else {
fm.appendSummaryLine(messageText);
}
}
/**
* Check if message for given field exists
*
* @param field
* @return
*/
public boolean exists(String field) {
return messagesPerField.containsKey(field);
}
/**
* Get message for given field.
*
* @param fieldName
* @return message text or empty string
*/
public String get(String fieldName) {
MessageBean mb = messagesPerField.get(fieldName);
if (mb != null) {
return mb.getSummary();
} else {
return "";
}
}
/**
* Print text if message for given field exists. Useful eg. to add css styles for fields with message.
*
* @param fieldName to check for
* @param text to print
* @return text if message exists for given field, else empty string
*/
public String printIfExists(String fieldName, String text) {
if (exists(fieldName))
return text;
else
return "";
}
}

View file

@ -14,7 +14,7 @@
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}">
<div class="form-group">
<div class="form-group ${messagesPerField.printIfExists('username','has-error')}">
<div class="col-sm-2 col-md-2">
<label for="username" class="control-label">${msg("username")}</label>
</div>
@ -24,7 +24,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group ${messagesPerField.printIfExists('email','has-error')}">
<div class="col-sm-2 col-md-2">
<label for="email" class="control-label">${msg("email")}</label> <span class="required">*</span>
</div>
@ -34,7 +34,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group ${messagesPerField.printIfExists('firstName','has-error')}">
<div class="col-sm-2 col-md-2">
<label for="firstName" class="control-label">${msg("firstName")}</label> <span class="required">*</span>
</div>
@ -44,7 +44,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group ${messagesPerField.printIfExists('lastName','has-error')}">
<div class="col-sm-2 col-md-2">
<label for="lastName" class="control-label">${msg("lastName")}</label> <span class="required">*</span>
</div>

View file

@ -6,7 +6,7 @@
${msg("loginProfileTitle")}
<#elseif section = "form">
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginUpdateProfileUrl}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
</div>
@ -15,7 +15,7 @@
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('firstName',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
</div>
@ -24,7 +24,7 @@
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('lastName',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
</div>

View file

@ -7,7 +7,7 @@
<#elseif section = "form">
<form id="kc-register-form" class="${properties.kcFormClass!}" action="${url.registrationAction}" method="post">
<#if !realm.registrationEmailAsUsername>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
</div>
@ -16,7 +16,7 @@
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('firstName',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="firstName" class="${properties.kcLabelClass!}">${msg("firstName")}</label>
</div>
@ -25,7 +25,7 @@
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('lastName',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="lastName" class="${properties.kcLabelClass!}">${msg("lastName")}</label>
</div>
@ -34,7 +34,7 @@
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>
</div>
@ -43,7 +43,7 @@
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('password',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
</div>
@ -52,7 +52,7 @@
</div>
</div>
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('password-confirm',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="password-confirm" class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label>
</div>

View file

@ -18,6 +18,7 @@ kcFormAreaClass=col-xs-12 col-sm-8 col-md-8 col-lg-6 login
kcFormClass=form-horizontal
kcFormGroupClass=form-group
kcFormGroupErrorClass=has-error
kcLabelClass=control-label
kcLabelWrapperClass=col-xs-12 col-sm-12 col-md-4 col-lg-3
kcInputClass=form-control

View file

@ -1,19 +1,21 @@
package org.keycloak.login;
import java.net.URI;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.Provider;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -48,8 +50,21 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
public LoginFormsProvider setAccessRequest(String message);
/**
* Set one global error message.
*
* @param message key of message
* @param parameters to be formatted into message
*/
public LoginFormsProvider setError(String message, Object ... parameters);
/**
* Set multiple error messages.
*
* @param messages to be set
*/
public LoginFormsProvider setErrors(List<FormMessage> messages);
public LoginFormsProvider setSuccess(String message, Object ... parameters);
public LoginFormsProvider setWarning(String message, Object ... parameters);

View file

@ -1,25 +1,50 @@
package org.keycloak.login.freemarker;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.OAuth2Constants;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
import org.keycloak.freemarker.*;
import org.keycloak.freemarker.BrowserSecurityHeaderSetup;
import org.keycloak.freemarker.FreeMarkerException;
import org.keycloak.freemarker.FreeMarkerUtil;
import org.keycloak.freemarker.LocaleHelper;
import org.keycloak.freemarker.Theme;
import org.keycloak.freemarker.ThemeProvider;
import org.keycloak.freemarker.beans.AdvancedMessageFormatterMethod;
import org.keycloak.freemarker.beans.LocaleBean;
import org.keycloak.freemarker.beans.MessageBean;
import org.keycloak.freemarker.beans.MessageFormatterMethod;
import org.keycloak.freemarker.beans.MessageType;
import org.keycloak.freemarker.beans.MessagesPerFieldBean;
import org.keycloak.login.LoginFormsPages;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.login.freemarker.model.ClientBean;
import org.keycloak.login.freemarker.model.CodeBean;
import org.keycloak.freemarker.beans.LocaleBean;
import org.keycloak.login.freemarker.model.IdentityProviderBean;
import org.keycloak.login.freemarker.model.LoginBean;
import org.keycloak.login.freemarker.model.MessageBean;
import org.keycloak.login.freemarker.model.OAuthGrantBean;
import org.keycloak.login.freemarker.model.ProfileBean;
import org.keycloak.login.freemarker.model.RealmBean;
import org.keycloak.login.freemarker.model.RegisterBean;
import org.keycloak.login.freemarker.model.IdentityProviderBean;
import org.keycloak.login.freemarker.model.TotpBean;
import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel;
@ -28,16 +53,10 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.flows.Urls;
import javax.ws.rs.core.*;
import java.io.IOException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -45,8 +64,6 @@ import java.util.concurrent.TimeUnit;
private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class);
public static enum MessageType {SUCCESS, WARNING, ERROR}
private String accessCode;
private Response.Status status;
private List<RoleModel> realmRolesRequested;
@ -55,9 +72,8 @@ import java.util.concurrent.TimeUnit;
private Map<String, String> httpResponseHeaders = new HashMap<String, String>();
private String accessRequestMessage;
private URI actionUri;
private Object[] parameters;
private String message;
private List<FormMessage> messages = null;
private MessageType messageType = MessageType.ERROR;
private MultivaluedMap<String, String> formData;
@ -134,7 +150,7 @@ import java.util.concurrent.TimeUnit;
return Response.serverError().build();
}
if (message == null) {
if (messages == null) {
setWarning(actionMessage);
}
@ -175,25 +191,30 @@ import java.util.concurrent.TimeUnit;
logger.warn("Failed to load properties", e);
}
Properties messages;
Properties messagesBundle;
Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, httpHeaders);
try {
messages = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messages));
messagesBundle = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
} catch (IOException e) {
logger.warn("Failed to load messages", e);
messages = new Properties();
messagesBundle = new Properties();
}
if (message != null) {
String formattedMessage;
if(messages.containsKey(message)){
formattedMessage = new MessageFormat(messages.getProperty(message),locale).format(parameters);
}else{
formattedMessage = message;
MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean();
if (messages != null) {
MessageBean wholeMessage = new MessageBean(null, messageType);
for (FormMessage message : this.messages) {
String formattedMessageText = formatMessage(message, messagesBundle, locale);
if (formattedMessageText != null) {
wholeMessage.appendSummaryLine(formattedMessageText);
messagesPerField.addMessage(message.getField(), formattedMessageText, messageType);
}
}
attributes.put("message", new MessageBean(formattedMessage, messageType));
attributes.put("message", wholeMessage);
}
attributes.put("messagesPerField", messagesPerField);
if (page == LoginFormsPages.OAUTH_GRANT) {
// for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
uriBuilder.replaceQuery(null);
@ -218,7 +239,7 @@ import java.util.concurrent.TimeUnit;
b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
break;
}
attributes.put("locale", new LocaleBean(realm, locale, b, messages));
attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle));
}
}
@ -240,10 +261,10 @@ import java.util.concurrent.TimeUnit;
break;
case OAUTH_GRANT:
attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage));
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messages));
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
break;
case CODE:
attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null));
attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? getFirstMessageUnformatted() : null));
break;
}
@ -303,24 +324,51 @@ import java.util.concurrent.TimeUnit;
return createResponse(LoginFormsPages.CODE);
}
public FreeMarkerLoginFormsProvider setError(String message, Object ... parameters) {
this.message = message;
protected void setMessage(MessageType type, String message, Object... parameters) {
messageType = type;
messages = new ArrayList<>();
messages.add(new FormMessage(null, message, parameters));
}
protected String getFirstMessageUnformatted() {
if (messages != null && !messages.isEmpty()) {
return messages.get(0).getMessage();
}
return null;
}
protected String formatMessage(FormMessage message, Properties messagesBundle, Locale locale) {
if (message == null)
return null;
if (messagesBundle.containsKey(message.getMessage())) {
return new MessageFormat(messagesBundle.getProperty(message.getMessage()), locale).format(message.getParameters());
} else {
return message.getMessage();
}
}
@Override
public FreeMarkerLoginFormsProvider setError(String message, Object... parameters) {
setMessage(MessageType.ERROR, message, parameters);
return this;
}
@Override
public LoginFormsProvider setErrors(List<FormMessage> messages) {
this.messageType = MessageType.ERROR;
this.parameters = parameters;
this.messages = new ArrayList<>(messages);
return this;
}
@Override
public FreeMarkerLoginFormsProvider setSuccess(String message, Object ... parameters) {
this.message = message;
this.messageType = MessageType.SUCCESS;
this.parameters = parameters;
setMessage(MessageType.SUCCESS, message, parameters);
return this;
}
@Override
public FreeMarkerLoginFormsProvider setWarning(String message, Object ... parameters) {
this.message = message;
this.messageType = MessageType.WARNING;
this.parameters = parameters;
setMessage(MessageType.WARNING, message, parameters);
return this;
}

View file

@ -1,60 +0,0 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.login.freemarker.model;
import org.keycloak.login.freemarker.FreeMarkerLoginFormsProvider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class MessageBean {
private String summary;
private FreeMarkerLoginFormsProvider.MessageType type;
public MessageBean(String message, FreeMarkerLoginFormsProvider.MessageType type) {
this.summary = message;
this.type = type;
}
public String getSummary() {
return summary;
}
public String getType() {
return this.type.toString().toLowerCase();
}
public boolean isSuccess() {
return FreeMarkerLoginFormsProvider.MessageType.SUCCESS.equals(this.type);
}
public boolean isWarning() {
return FreeMarkerLoginFormsProvider.MessageType.WARNING.equals(this.type);
}
public boolean isError() {
return FreeMarkerLoginFormsProvider.MessageType.ERROR.equals(this.type);
}
}

View file

@ -77,6 +77,7 @@ public class ServletOAuthClient extends AbstractOAuthClient {
String state = getStateCode();
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(getUrl(request, authUrl, true))
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, clientId)
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
.queryParam(OAuth2Constants.STATE, state);

View file

@ -0,0 +1,69 @@
/*
* JBoss, Home of Professional Open Source
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
*/
package org.keycloak.models.utils;
import java.util.Arrays;
/**
* Message (eg. error) to be shown in form.
*
* @author Vlastimil Elias (velias at redhat dot com)
*/
public class FormMessage {
/**
* Value used for {@link #field} if message is global (not tied to any specific form field)
*/
public static final String GLOBAL = "global";
private String field;
private String message;
private Object[] parameters;
/**
* Create message.
*
* @param field this message is for. {@link #GLOBAL} is used if null
* @param message key for the message
* @param parameters to be formatted into message
*/
public FormMessage(String field, String message, Object... parameters) {
this(field, message);
this.parameters = parameters;
}
/**
* Create message without parameters.
*
* @param field this message is for. {@link #GLOBAL} is used if null
* @param message key for the message
*/
public FormMessage(String field, String message) {
super();
if (field == null)
field = GLOBAL;
this.field = field;
this.message = message;
}
public String getField() {
return field;
}
public String getMessage() {
return message;
}
public Object[] getParameters() {
return parameters;
}
@Override
public String toString() {
return "FormMessage [field=" + field + ", message=" + message + ", parameters=" + Arrays.toString(parameters) + "]";
}
}

View file

@ -209,6 +209,7 @@
<id>generate-service-docs</id>
<phase>generate-resources</phase>
<configuration>
<subpackages>org.keycloak.services.resources.admin:org.keycloak.protocol.oidc</subpackages>
<doclet>com.lunatech.doclets.jax.jaxrs.JAXRSDoclet</doclet>
<docletArtifacts>
<docletArtifact>
@ -225,6 +226,7 @@
</offlineLink>
</offlineLinks>
<additionalparam>-disablejavascriptexample</additionalparam>
<additionalparam>-pathexcludefilter '/admin/.*index.*' -pathexcludefilter '/admin' -pathexcludefilter '/admin/\\{realm\\}/console.*'</additionalparam>
</configuration>
<goals>
<goal>javadoc</goal>

View file

@ -132,7 +132,7 @@ public class AuthorizationEndpoint {
state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_PARAM);
prompt = params.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM);
prompt = params.getFirst(OIDCLoginProtocol.PROMPT_PARAM);
idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT);
checkSsl();

View file

@ -33,6 +33,7 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventStoreProvider;
import org.keycloak.events.EventType;
import org.keycloak.models.*;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
@ -69,6 +70,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Variant;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.HashSet;
@ -403,10 +405,10 @@ public class AccountService {
UserModel user = auth.getUser();
String error = Validation.validateUpdateProfileForm(formData);
if (error != null) {
List<FormMessage> errors = Validation.validateUpdateProfileForm(formData);
if (errors != null && !errors.isEmpty()) {
setReferrerOnPage();
return account.setError(error).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
}
try {

View file

@ -51,6 +51,7 @@ import org.keycloak.services.resources.flows.Flows;
import org.keycloak.services.resources.flows.Urls;
import org.keycloak.services.validation.Validation;
import org.keycloak.social.SocialIdentityProvider;
import org.keycloak.util.ObjectUtil;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@ -343,12 +344,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
private void updateFederatedIdentity(FederatedIdentity updatedIdentity, UserModel federatedUser) {
FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, updatedIdentity.getIdentityProviderId(), this.realmModel);
federatedIdentityModel.setToken(updatedIdentity.getToken());
// Skip DB write if tokens are null or equal
if (!ObjectUtil.isEqualOrNull(updatedIdentity.getToken(), federatedIdentityModel.getToken())) {
federatedIdentityModel.setToken(updatedIdentity.getToken());
this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel);
this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel);
if (isDebugEnabled()) {
LOGGER.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, updatedIdentity.getIdentityProviderId());
if (isDebugEnabled()) {
LOGGER.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, updatedIdentity.getIdentityProviderId());
}
}
}

View file

@ -43,6 +43,7 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.LoginProtocol;
@ -72,6 +73,8 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -431,8 +434,8 @@ public class LoginActionsService {
return Flows.forwardToSecurityFailurePage(session, realm, uriInfo, headers, Messages.INVALID_CODE);
}
String username = formData.getFirst("username");
String email = formData.getFirst("email");
String username = formData.getFirst(Validation.FIELD_USERNAME);
String email = formData.getFirst(Validation.FIELD_EMAIL);
if (realm.isRegistrationEmailAsUsername()) {
username = email;
formData.putSingle(AuthenticationManager.FORM_USERNAME, username);
@ -467,20 +470,12 @@ public class LoginActionsService {
}
// Validate here, so user is not created if password doesn't validate to passwordPolicy of current realm
String errorMessage = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes);
Object[] parameters = new Object[0];
if (errorMessage == null) {
PasswordPolicy.Error error = Validation.validatePassword(formData, realm.getPasswordPolicy());
if(error != null){
errorMessage = error.getMessage();
parameters = error.getParameters();
}
}
List<FormMessage> errors = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes, realm.getPasswordPolicy());
if (errorMessage != null) {
if (errors != null && !errors.isEmpty()) {
event.error(Errors.INVALID_REGISTRATION);
return Flows.forms(session, realm, client, uriInfo, headers)
.setError(errorMessage, parameters)
.setErrors(errors)
.setFormData(formData)
.setClientSessionCode(clientCode.getCode())
.createRegistration();
@ -497,7 +492,7 @@ public class LoginActionsService {
}
// Validate that user with this email doesn't exist in realm or any federation provider
if (session.users().getUserByEmail(email, realm) != null) {
if (email != null && session.users().getUserByEmail(email, realm) != null) {
event.error(Errors.EMAIL_IN_USE);
return Flows.forms(session, realm, client, uriInfo, headers)
.setError(Messages.EMAIL_EXISTS)
@ -632,9 +627,9 @@ public class LoginActionsService {
initEvent(clientSession);
String error = Validation.validateUpdateProfileForm(formData);
if (error != null) {
return Flows.forms(session, realm, null, uriInfo, headers).setUser(user).setError(error)
List<FormMessage> errors = Validation.validateUpdateProfileForm(formData);
if (errors != null && !errors.isEmpty()) {
return Flows.forms(session, realm, null, uriInfo, headers).setUser(user).setErrors(errors)
.setClientSessionCode(accessCode.getCode())
.createResponse(RequiredAction.UPDATE_PROFILE);
}

View file

@ -113,7 +113,6 @@ public class RealmsResource {
Object endpoint = factory.createProtocolEndpoint(realm, event, authManager);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
//resourceContext.initResource(tokenService);
return endpoint;
}
@ -132,8 +131,6 @@ public class RealmsResource {
AuthenticationManager authManager = new AuthenticationManager(protector);
LoginActionsService service = new LoginActionsService(realm, authManager, event);
ResteasyProviderFactory.getInstance().injectProperties(service);
//resourceContext.initResource(service);
return service;
}
@ -147,7 +144,6 @@ public class RealmsResource {
return service;
}
protected RealmModel locateRealm(String name, RealmManager realmManager) {
RealmModel realm = realmManager.getRealmByName(name);
if (realm == null) {

View file

@ -1,75 +1,90 @@
package org.keycloak.services.validation;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import javax.ws.rs.core.MultivaluedMap;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.messages.Messages;
public class Validation {
public static final String FIELD_PASSWORD_CONFIRM = "password-confirm";
public static final String FIELD_EMAIL = "email";
public static final String FIELD_LAST_NAME = "lastName";
public static final String FIELD_FIRST_NAME = "firstName";
public static final String FIELD_PASSWORD = "password";
public static final String FIELD_USERNAME = "username";
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
public static String validateRegistrationForm(RealmModel realm, MultivaluedMap<String, String> formData, List<String> requiredCredentialTypes) {
if (isEmpty(formData.getFirst("firstName"))) {
return Messages.MISSING_FIRST_NAME;
public static List<FormMessage> validateRegistrationForm(RealmModel realm, MultivaluedMap<String, String> formData, List<String> requiredCredentialTypes, PasswordPolicy policy) {
List<FormMessage> errors = new ArrayList<>();
if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst(FIELD_USERNAME))) {
addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
}
if (isEmpty(formData.getFirst("lastName"))) {
return Messages.MISSING_LAST_NAME;
if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) {
addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
}
if (isEmpty(formData.getFirst("email"))) {
return Messages.MISSING_EMAIL;
if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) {
addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME);
}
if (!isEmailValid(formData.getFirst("email"))) {
return Messages.INVALID_EMAIL;
}
if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst("username"))) {
return Messages.MISSING_USERNAME;
if (isEmpty(formData.getFirst(FIELD_EMAIL))) {
addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL);
} else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) {
addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL);
}
if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
if (isEmpty(formData.getFirst(CredentialRepresentation.PASSWORD))) {
return Messages.MISSING_PASSWORD;
}
if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) {
return Messages.INVALID_PASSWORD_CONFIRM;
if (isEmpty(formData.getFirst(FIELD_PASSWORD))) {
addError(errors, FIELD_PASSWORD, Messages.MISSING_PASSWORD);
} else if (!formData.getFirst(FIELD_PASSWORD).equals(formData.getFirst(FIELD_PASSWORD_CONFIRM))) {
addError(errors, FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM);
}
}
return null;
if (formData.getFirst(FIELD_PASSWORD) != null) {
PasswordPolicy.Error err = policy.validate(realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD));
if (err != null)
errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters()));
}
return errors;
}
public static PasswordPolicy.Error validatePassword(MultivaluedMap<String, String> formData, PasswordPolicy policy) {
return policy.validate(formData.getFirst("username"), formData.getFirst("password"));
private static void addError(List<FormMessage> errors, String field, String message){
errors.add(new FormMessage(field, message));
}
public static String validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
if (isEmpty(formData.getFirst("firstName"))) {
return Messages.MISSING_FIRST_NAME;
public static List<FormMessage> validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
List<FormMessage> errors = new ArrayList<>();
if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) {
addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
}
if (isEmpty(formData.getFirst("lastName"))) {
return Messages.MISSING_LAST_NAME;
if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) {
addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME);
}
if (isEmpty(formData.getFirst("email"))) {
return Messages.MISSING_EMAIL;
if (isEmpty(formData.getFirst(FIELD_EMAIL))) {
addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL);
} else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) {
addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL);
}
if (!isEmailValid(formData.getFirst("email"))) {
return Messages.INVALID_EMAIL;
}
return null;
return errors;
}
public static boolean isEmpty(String s) {

View file

@ -50,6 +50,9 @@ import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.core.Response;
import java.util.Map;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -198,6 +201,25 @@ public class LoginTest {
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
}
@Test
public void loginPromptNone() {
driver.navigate().to(oauth.getLoginFormUrl().toString() + "&prompt=none");
assertFalse(loginPage.isCurrent());
assertTrue(appPage.isCurrent());
loginPage.open();
loginPage.login("login-test", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent();
driver.navigate().to(oauth.getLoginFormUrl().toString() + "&prompt=none");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).removeDetail(Details.USERNAME).detail(Details.AUTH_METHOD, "sso").assertEvent();
}
@Test
public void loginNoTimeoutWithLongWait() {
try {
@ -264,9 +286,9 @@ public class LoginTest {
try {
loginPage.open();
Assert.assertFalse(loginPage.isRememberMeChecked());
assertFalse(loginPage.isRememberMeChecked());
loginPage.setRememberMe(true);
Assert.assertTrue(loginPage.isRememberMeChecked());
assertTrue(loginPage.isRememberMeChecked());
loginPage.login("login-test", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@ -282,7 +304,7 @@ public class LoginTest {
// Assert rememberMe checked and username/email prefilled
loginPage.open();
Assert.assertTrue(loginPage.isRememberMeChecked());
assertTrue(loginPage.isRememberMeChecked());
Assert.assertEquals("login-test", loginPage.getUsername());
loginPage.setRememberMe(false);