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) { protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
return UriBuilder.fromPath(getConfig().getAuthorizationUrl()) return UriBuilder.fromUri(getConfig().getAuthorizationUrl())
.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope()) .queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
.queryParam(OAUTH2_PARAMETER_STATE, request.getState()) .queryParam(OAUTH2_PARAMETER_STATE, request.getState())
.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code") .queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")

View file

@ -168,11 +168,5 @@
<column name="VALUE" type="VARCHAR(255)"/> <column name="VALUE" type="VARCHAR(255)"/>
</createTable> </createTable>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_ENABLED_EVENT_TYPES" constraintName="FK_H846O4H0W8EPX5NWEDRF5Y69J" referencedColumnNames="ID" referencedTableName="REALM"/> <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> </changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -35,6 +35,9 @@ public class RSATokenVerifier {
if (user == null) { if (user == null) {
throw new VerificationException("Token user was 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())) { if (!realmUrl.equals(token.getIssuer())) {
throw new VerificationException("Token audience doesn't match domain."); 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 if (isPublic()) { // if client is public access type
formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal")); formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, "customer-portal"));
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "password"));
} else { } else {
String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret"); String authorization = BasicAuthHelper.createHeader("customer-portal", "secret-secret-secret");
post.setHeader("Authorization", authorization); 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>. 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: For example to add the event listener sysout example provider using the jboss-cli script execute:
<programlisting><![CDATA[{ <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> }]]></programlisting>
Or to manually create it start by creating the folder <literal>KEYCLOAK_HOME/modules/org/keycloak/examples/event-sysout/main</literal>. 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> 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> </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"> <subsystem xmlns="urn:jboss:domain:keycloak:1.0">
<auth-server name="keycloak-1"> <auth-server name="main-auth-server">
<enabled>true</enabled> <enabled>true</enabled>
<web-context>auth</web-context> <web-context>auth</web-context>
</auth-server> </auth-server>
<auth-server name="keyclaok-2">
<enabled>false</enabled>
<web-context>auth2</web-context>
</auth-server>
</subsystem> </subsystem>
</profile> ]]></programlisting>
]]> </para>
</programlisting> <para>
</para> To configure the server copy <literal>standalone/configuration/keycloak-server.json</literal> to
<warning> <literal>domain/servers/&lt;SERVER NAME&gt;/configuration</literal>. The configuration should be identical
<para> for all servers in a group.
If you create more than one Keycloak server, you will need to use CLI to fully configure each instance. At the least, </para>
you will need to run the <link linkend="uploading-extra-config">update-server-config</link> operation. <para>
</para> Follow the <link linkend='clustering'>Clustering</link> section of the documentation to configure Keycloak
</warning> for clustering. In domain mode it doesn't make much sense to not configure Keycloak in cluster mode.
</section> </para>
<section> <para>
<title>Using CLI and CLI GUI with the Keycloak Subsystem</title> To deploy custom providers and themes you should deploys these as modules and make sure the modules are
<para> available to all servers in the group. See <link linkend='providers'>Providers</link> and
Servers can also be added/removed or enabled/disabled at runtime using the <ulink url="https://developer.jboss.org/wiki/CommandLineInterface">CLI</ulink> or <link linkend='themes'>Themes</link> sections for more information on how to do this.
<ulink url="https://developer.jboss.org/wiki/AGUIForTheCommandLineInterface">CLI GUI</ulink> tool. These are tools that ship with WildFly/EAP and also with </para>
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>
</section> </section>
</chapter> </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 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 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). 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 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": [ "identityProviders": [
{ {
"id" : "facebook", "alias" : "facebook",
"providerId" : "facebook", "providerId" : "facebook",
"name" : "Facebook",
"enabled": true, "enabled": true,
"updateProfileFirstLogin" : "true", "updateProfileFirstLogin" : "true",
"storeToken" : "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**. 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 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). 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 Make sure you've set up the Keycloak Server

View file

@ -53,9 +53,8 @@
], ],
"identityProviders": [ "identityProviders": [
{ {
"id" : "google", "alias" : "google",
"providerId" : "google", "providerId" : "google",
"name" : "Google",
"enabled": true, "enabled": true,
"updateProfileFirstLogin" : "true", "updateProfileFirstLogin" : "true",
"storeToken" : "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 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 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: There are two main realms in this example:

View file

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

View file

@ -28,10 +28,11 @@
}, },
"applications": [ "applications": [
{ {
"name": "http://localhost:8080/auth/", "name": "http://localhost:8080/auth/realms/saml-broker-authentication-realm",
"protocol": "saml",
"enabled": true, "enabled": true,
"redirectUris": [ "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": { "attributes": {
"saml.assertion.signature": "true", "saml.assertion.signature": "true",

View file

@ -119,7 +119,7 @@ public class TwitterShowUserServlet extends HttpServlet {
} }
private String getIdentityProviderTokenUrl() { 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) { private void initKeyCloakClient(ServletConfig config) {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,17 @@
Example Event Listener that prints events to System.out 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, 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 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. login again to see events printed to System.out.

View file

@ -1,7 +1,16 @@
Example Event Store that stores events in memory 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: Then edit standalone/configuration/keycloak-server.json, change:

View file

@ -59,7 +59,7 @@ public class MemEventStoreProvider implements EventStoreProvider {
@Override @Override
public void onEvent(Event event) { public void onEvent(Event event) {
if (!excludedEvents.contains(event.getType())) { if (excludedEvents == null || !excludedEvents.contains(event.getType())) {
events.add(0, event); 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 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. 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 The ClasspathPropertiesFederationProvider is an example of a readonly provider. If you go to the Users/Federation
@ -13,4 +24,4 @@ a "test-users.properties" within the JAR that you can use as the variable.
The FilePropertiesFederationProvider is an example of a writable provider. It synchronizes changes made to The FilePropertiesFederationProvider is an example of a writable provider. It synchronizes changes made to
username and password with the properties file. If you go to the Users/Federation page of the admin console you will username and password with the properties file. If you go to the Users/Federation page of the admin console you will
see this provider listed under "file-properties". To configure this provider you specify a fully qualified file path to see this provider listed under "file-properties". To configure this provider you specify a fully qualified file path to
a properties file in the "path" field of the admin page for this plugin. a properties file in the "path" field of the admin page for this plugin.

View file

@ -1,17 +1,18 @@
package org.keycloak.account; package org.keycloak.account;
import org.apache.http.client.methods.HttpHead; 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.provider.Provider;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; 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> * @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 setError(String message, Object ... parameters);
AccountProvider setErrors(List<FormMessage> messages);
AccountProvider setSuccess(String message, Object ... parameters); AccountProvider setSuccess(String message, Object ... parameters);
AccountProvider setWarning(String message, Object ... parameters); AccountProvider setWarning(String message, Object ... parameters);

View file

@ -1,25 +1,54 @@
package org.keycloak.account.freemarker; 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.jboss.logging.Logger;
import org.keycloak.account.AccountPages; import org.keycloak.account.AccountPages;
import org.keycloak.account.AccountProvider; 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.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.MessageFormatterMethod;
import org.keycloak.freemarker.beans.MessageType;
import org.keycloak.freemarker.beans.MessagesPerFieldBean;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.resources.flows.Urls; 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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -43,13 +72,10 @@ public class FreeMarkerAccountProvider implements AccountProvider {
private FreeMarkerUtil freeMarker; private FreeMarkerUtil freeMarker;
private HttpHeaders headers; private HttpHeaders headers;
public static enum MessageType {SUCCESS, WARNING, ERROR}
private UriInfo uriInfo; private UriInfo uriInfo;
private String message; private List<FormMessage> messages = null;
private Object[] parameters; private MessageType messageType = MessageType.ERROR;
private MessageType messageType;
public FreeMarkerAccountProvider(KeycloakSession session, FreeMarkerUtil freeMarker) { public FreeMarkerAccountProvider(KeycloakSession session, FreeMarkerUtil freeMarker) {
this.session = session; this.session = session;
@ -87,13 +113,13 @@ public class FreeMarkerAccountProvider implements AccountProvider {
} }
Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, headers); Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, headers);
Properties messages; Properties messagesBundle;
try { try {
messages = theme.getMessages(locale); messagesBundle = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messages)); attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to load messages", e); logger.warn("Failed to load messages", e);
messages = new Properties(); messagesBundle = new Properties();
} }
URI baseUri = uriInfo.getBaseUri(); URI baseUri = uriInfo.getBaseUri();
@ -107,15 +133,19 @@ public class FreeMarkerAccountProvider implements AccountProvider {
attributes.put("stateChecker", stateChecker); attributes.put("stateChecker", stateChecker);
} }
if (message != null) { MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean();
String formattedMessage; if (messages != null) {
if(messages.containsKey(message)){ MessageBean wholeMessage = new MessageBean(null, messageType);
formattedMessage = new MessageFormat(messages.getProperty(message),locale).format(parameters); for (FormMessage message : this.messages) {
}else{ String formattedMessageText = formatMessage(message, messagesBundle, locale);
formattedMessage = message; 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) { if (referrer != null) {
attributes.put("referrer", new ReferrerBean(referrer)); attributes.put("referrer", new ReferrerBean(referrer));
@ -134,7 +164,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
b = UriBuilder.fromUri(baseQueryUri).path(uriInfo.getPath()); b = UriBuilder.fromUri(baseQueryUri).path(uriInfo.getPath());
break; 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)); attributes.put("features", new FeaturesBean(identityProviderEnabled, eventsEnabled, passwordUpdateSupported));
@ -173,28 +203,46 @@ public class FreeMarkerAccountProvider implements AccountProvider {
this.passwordSet = passwordSet; this.passwordSet = passwordSet;
return this; 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 @Override
public AccountProvider setError(String message, Object ... parameters) { public AccountProvider setError(String message, Object ... parameters) {
this.message = message; setMessage(MessageType.ERROR, message, parameters);
this.parameters = parameters;
this.messageType = MessageType.ERROR;
return this; return this;
} }
@Override @Override
public AccountProvider setSuccess(String message, Object ... parameters) { public AccountProvider setSuccess(String message, Object ... parameters) {
this.message = message; setMessage(MessageType.SUCCESS, message, parameters);
this.parameters = parameters;
this.messageType = MessageType.SUCCESS;
return this; return this;
} }
@Override @Override
public AccountProvider setWarning(String message, Object ... parameters) { public AccountProvider setWarning(String message, Object ... parameters) {
this.message = message; setMessage(MessageType.WARNING, message, parameters);
this.parameters = parameters;
this.messageType = MessageType.WARNING;
return this; return this;
} }

View file

@ -19,9 +19,7 @@
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org. * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/ */
package org.keycloak.account.freemarker.model; package org.keycloak.freemarker.beans;
import org.keycloak.account.freemarker.FreeMarkerAccountProvider;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -30,9 +28,9 @@ public class MessageBean {
private String summary; 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.summary = message;
this.type = type; this.type = type;
} }
@ -41,20 +39,29 @@ public class MessageBean {
return summary; return summary;
} }
public void appendSummaryLine(String newLine) {
if (newLine == null)
return;
if (summary == null)
summary = newLine;
else
summary = summary + "<br>" + newLine;
}
public String getType() { public String getType() {
return this.type.toString().toLowerCase(); return this.type.toString().toLowerCase();
} }
public boolean isSuccess() { public boolean isSuccess() {
return FreeMarkerAccountProvider.MessageType.SUCCESS.equals(this.type); return MessageType.SUCCESS.equals(this.type);
} }
public boolean isWarning() { public boolean isWarning() {
return FreeMarkerAccountProvider.MessageType.WARNING.equals(this.type); return MessageType.WARNING.equals(this.type);
} }
public boolean isError() { 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}"> <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"> <div class="col-sm-2 col-md-2">
<label for="username" class="control-label">${msg("username")}</label> <label for="username" class="control-label">${msg("username")}</label>
</div> </div>
@ -24,7 +24,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group ${messagesPerField.printIfExists('email','has-error')}">
<div class="col-sm-2 col-md-2"> <div class="col-sm-2 col-md-2">
<label for="email" class="control-label">${msg("email")}</label> <span class="required">*</span> <label for="email" class="control-label">${msg("email")}</label> <span class="required">*</span>
</div> </div>
@ -34,7 +34,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group ${messagesPerField.printIfExists('firstName','has-error')}">
<div class="col-sm-2 col-md-2"> <div class="col-sm-2 col-md-2">
<label for="firstName" class="control-label">${msg("firstName")}</label> <span class="required">*</span> <label for="firstName" class="control-label">${msg("firstName")}</label> <span class="required">*</span>
</div> </div>
@ -44,7 +44,7 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group ${messagesPerField.printIfExists('lastName','has-error')}">
<div class="col-sm-2 col-md-2"> <div class="col-sm-2 col-md-2">
<label for="lastName" class="control-label">${msg("lastName")}</label> <span class="required">*</span> <label for="lastName" class="control-label">${msg("lastName")}</label> <span class="required">*</span>
</div> </div>

View file

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

View file

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

View file

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

View file

@ -1,19 +1,21 @@
package org.keycloak.login; 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.ClientModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.provider.Provider; 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> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
@ -48,7 +50,20 @@ public interface LoginFormsProvider extends Provider {
public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested); public LoginFormsProvider setAccessRequest(List<RoleModel> realmRolesRequested, MultivaluedMap<String,RoleModel> resourceRolesRequested);
public LoginFormsProvider setAccessRequest(String message); 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); 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 setSuccess(String message, Object ... parameters);

View file

@ -1,25 +1,50 @@
package org.keycloak.login.freemarker; 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.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.email.EmailException; import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider; 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.AdvancedMessageFormatterMethod;
import org.keycloak.freemarker.beans.LocaleBean;
import org.keycloak.freemarker.beans.MessageBean;
import org.keycloak.freemarker.beans.MessageFormatterMethod; 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.LoginFormsPages;
import org.keycloak.login.LoginFormsProvider; import org.keycloak.login.LoginFormsProvider;
import org.keycloak.login.freemarker.model.ClientBean; import org.keycloak.login.freemarker.model.ClientBean;
import org.keycloak.login.freemarker.model.CodeBean; 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.LoginBean;
import org.keycloak.login.freemarker.model.MessageBean;
import org.keycloak.login.freemarker.model.OAuthGrantBean; import org.keycloak.login.freemarker.model.OAuthGrantBean;
import org.keycloak.login.freemarker.model.ProfileBean; import org.keycloak.login.freemarker.model.ProfileBean;
import org.keycloak.login.freemarker.model.RealmBean; import org.keycloak.login.freemarker.model.RealmBean;
import org.keycloak.login.freemarker.model.RegisterBean; 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.TotpBean;
import org.keycloak.login.freemarker.model.UrlBean; import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -28,16 +53,10 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.flows.Urls; 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> * @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); private static final Logger logger = Logger.getLogger(FreeMarkerLoginFormsProvider.class);
public static enum MessageType {SUCCESS, WARNING, ERROR}
private String accessCode; private String accessCode;
private Response.Status status; private Response.Status status;
private List<RoleModel> realmRolesRequested; private List<RoleModel> realmRolesRequested;
@ -55,9 +72,8 @@ import java.util.concurrent.TimeUnit;
private Map<String, String> httpResponseHeaders = new HashMap<String, String>(); private Map<String, String> httpResponseHeaders = new HashMap<String, String>();
private String accessRequestMessage; private String accessRequestMessage;
private URI actionUri; private URI actionUri;
private Object[] parameters;
private String message; private List<FormMessage> messages = null;
private MessageType messageType = MessageType.ERROR; private MessageType messageType = MessageType.ERROR;
private MultivaluedMap<String, String> formData; private MultivaluedMap<String, String> formData;
@ -134,7 +150,7 @@ import java.util.concurrent.TimeUnit;
return Response.serverError().build(); return Response.serverError().build();
} }
if (message == null) { if (messages == null) {
setWarning(actionMessage); setWarning(actionMessage);
} }
@ -175,25 +191,30 @@ import java.util.concurrent.TimeUnit;
logger.warn("Failed to load properties", e); logger.warn("Failed to load properties", e);
} }
Properties messages; Properties messagesBundle;
Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, httpHeaders); Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, httpHeaders);
try { try {
messages = theme.getMessages(locale); messagesBundle = theme.getMessages(locale);
attributes.put("msg", new MessageFormatterMethod(locale, messages)); attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle));
} catch (IOException e) { } catch (IOException e) {
logger.warn("Failed to load messages", e); logger.warn("Failed to load messages", e);
messages = new Properties(); messagesBundle = new Properties();
} }
if (message != null) { MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean();
String formattedMessage; if (messages != null) {
if(messages.containsKey(message)){ MessageBean wholeMessage = new MessageBean(null, messageType);
formattedMessage = new MessageFormat(messages.getProperty(message),locale).format(parameters); for (FormMessage message : this.messages) {
}else{ String formattedMessageText = formatMessage(message, messagesBundle, locale);
formattedMessage = message; 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) { 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 // 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); uriBuilder.replaceQuery(null);
@ -218,7 +239,7 @@ import java.util.concurrent.TimeUnit;
b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath()); b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath());
break; 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; break;
case OAUTH_GRANT: case OAUTH_GRANT:
attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage)); 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; break;
case CODE: 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; break;
} }
@ -303,24 +324,51 @@ import java.util.concurrent.TimeUnit;
return createResponse(LoginFormsPages.CODE); return createResponse(LoginFormsPages.CODE);
} }
public FreeMarkerLoginFormsProvider setError(String message, Object ... parameters) { protected void setMessage(MessageType type, String message, Object... parameters) {
this.message = message; 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.messageType = MessageType.ERROR;
this.parameters = parameters; this.messages = new ArrayList<>(messages);
return this; return this;
} }
@Override
public FreeMarkerLoginFormsProvider setSuccess(String message, Object ... parameters) { public FreeMarkerLoginFormsProvider setSuccess(String message, Object ... parameters) {
this.message = message; setMessage(MessageType.SUCCESS, message, parameters);
this.messageType = MessageType.SUCCESS;
this.parameters = parameters;
return this; return this;
} }
@Override
public FreeMarkerLoginFormsProvider setWarning(String message, Object ... parameters) { public FreeMarkerLoginFormsProvider setWarning(String message, Object ... parameters) {
this.message = message; setMessage(MessageType.WARNING, message, parameters);
this.messageType = MessageType.WARNING;
this.parameters = parameters;
return this; 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(); String state = getStateCode();
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(getUrl(request, authUrl, true)) KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(getUrl(request, authUrl, true))
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, clientId) .queryParam(OAuth2Constants.CLIENT_ID, clientId)
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
.queryParam(OAuth2Constants.STATE, state); .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> <id>generate-service-docs</id>
<phase>generate-resources</phase> <phase>generate-resources</phase>
<configuration> <configuration>
<subpackages>org.keycloak.services.resources.admin:org.keycloak.protocol.oidc</subpackages>
<doclet>com.lunatech.doclets.jax.jaxrs.JAXRSDoclet</doclet> <doclet>com.lunatech.doclets.jax.jaxrs.JAXRSDoclet</doclet>
<docletArtifacts> <docletArtifacts>
<docletArtifact> <docletArtifact>
@ -225,6 +226,7 @@
</offlineLink> </offlineLink>
</offlineLinks> </offlineLinks>
<additionalparam>-disablejavascriptexample</additionalparam> <additionalparam>-disablejavascriptexample</additionalparam>
<additionalparam>-pathexcludefilter '/admin/.*index.*' -pathexcludefilter '/admin' -pathexcludefilter '/admin/\\{realm\\}/console.*'</additionalparam>
</configuration> </configuration>
<goals> <goals>
<goal>javadoc</goal> <goal>javadoc</goal>

View file

@ -132,7 +132,7 @@ public class AuthorizationEndpoint {
state = params.getFirst(OIDCLoginProtocol.STATE_PARAM); state = params.getFirst(OIDCLoginProtocol.STATE_PARAM);
scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM); scope = params.getFirst(OIDCLoginProtocol.SCOPE_PARAM);
loginHint = params.getFirst(OIDCLoginProtocol.LOGIN_HINT_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); idpHint = params.getFirst(AdapterConstants.KC_IDP_HINT);
checkSsl(); checkSsl();

View file

@ -33,6 +33,7 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventStoreProvider;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.*; import org.keycloak.models.*;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; 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.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import javax.ws.rs.core.Variant; import javax.ws.rs.core.Variant;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.URI; import java.net.URI;
import java.util.HashSet; import java.util.HashSet;
@ -403,10 +405,10 @@ public class AccountService {
UserModel user = auth.getUser(); UserModel user = auth.getUser();
String error = Validation.validateUpdateProfileForm(formData); List<FormMessage> errors = Validation.validateUpdateProfileForm(formData);
if (error != null) { if (errors != null && !errors.isEmpty()) {
setReferrerOnPage(); setReferrerOnPage();
return account.setError(error).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT); return account.setErrors(errors).setProfileFormData(formData).createResponse(AccountPages.ACCOUNT);
} }
try { 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.resources.flows.Urls;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.social.SocialIdentityProvider; import org.keycloak.social.SocialIdentityProvider;
import org.keycloak.util.ObjectUtil;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.GET; import javax.ws.rs.GET;
@ -343,12 +344,15 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
private void updateFederatedIdentity(FederatedIdentity updatedIdentity, UserModel federatedUser) { private void updateFederatedIdentity(FederatedIdentity updatedIdentity, UserModel federatedUser) {
FederatedIdentityModel federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, updatedIdentity.getIdentityProviderId(), this.realmModel); 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()) { if (isDebugEnabled()) {
LOGGER.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, updatedIdentity.getIdentityProviderId()); LOGGER.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, updatedIdentity.getIdentityProviderId());
}
} }
} }

View file

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

View file

@ -1,75 +1,90 @@
package org.keycloak.services.validation; package org.keycloak.services.validation;
import org.keycloak.models.PasswordPolicy; import java.util.ArrayList;
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.List; import java.util.List;
import java.util.regex.Pattern; 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 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() // 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-]+)*"); 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) { public static List<FormMessage> validateRegistrationForm(RealmModel realm, MultivaluedMap<String, String> formData, List<String> requiredCredentialTypes, PasswordPolicy policy) {
if (isEmpty(formData.getFirst("firstName"))) { List<FormMessage> errors = new ArrayList<>();
return Messages.MISSING_FIRST_NAME;
if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst(FIELD_USERNAME))) {
addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME);
} }
if (isEmpty(formData.getFirst("lastName"))) { if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) {
return Messages.MISSING_LAST_NAME; addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
} }
if (isEmpty(formData.getFirst("email"))) { if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) {
return Messages.MISSING_EMAIL; addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME);
} }
if (!isEmailValid(formData.getFirst("email"))) { if (isEmpty(formData.getFirst(FIELD_EMAIL))) {
return Messages.INVALID_EMAIL; addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL);
} } else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) {
addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL);
if (!realm.isRegistrationEmailAsUsername() && isEmpty(formData.getFirst("username"))) {
return Messages.MISSING_USERNAME;
} }
if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) { if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) {
if (isEmpty(formData.getFirst(CredentialRepresentation.PASSWORD))) { if (isEmpty(formData.getFirst(FIELD_PASSWORD))) {
return Messages.MISSING_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);
if (!formData.getFirst("password").equals(formData.getFirst("password-confirm"))) {
return 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;
}
private static void addError(List<FormMessage> errors, String field, String message){
errors.add(new FormMessage(field, message));
} }
public static PasswordPolicy.Error validatePassword(MultivaluedMap<String, String> formData, PasswordPolicy policy) {
return policy.validate(formData.getFirst("username"), formData.getFirst("password"));
}
public static String validateUpdateProfileForm(MultivaluedMap<String, String> formData) { public static List<FormMessage> validateUpdateProfileForm(MultivaluedMap<String, String> formData) {
if (isEmpty(formData.getFirst("firstName"))) { List<FormMessage> errors = new ArrayList<>();
return Messages.MISSING_FIRST_NAME;
if (isEmpty(formData.getFirst(FIELD_FIRST_NAME))) {
addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME);
} }
if (isEmpty(formData.getFirst("lastName"))) { if (isEmpty(formData.getFirst(FIELD_LAST_NAME))) {
return Messages.MISSING_LAST_NAME; addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME);
} }
if (isEmpty(formData.getFirst("email"))) { if (isEmpty(formData.getFirst(FIELD_EMAIL))) {
return Messages.MISSING_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 errors;
return Messages.INVALID_EMAIL;
}
return null;
} }
public static boolean isEmpty(String s) { 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 javax.ws.rs.core.Response;
import java.util.Map; 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> * @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(); 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 @Test
public void loginNoTimeoutWithLongWait() { public void loginNoTimeoutWithLongWait() {
try { try {
@ -264,9 +286,9 @@ public class LoginTest {
try { try {
loginPage.open(); loginPage.open();
Assert.assertFalse(loginPage.isRememberMeChecked()); assertFalse(loginPage.isRememberMeChecked());
loginPage.setRememberMe(true); loginPage.setRememberMe(true);
Assert.assertTrue(loginPage.isRememberMeChecked()); assertTrue(loginPage.isRememberMeChecked());
loginPage.login("login-test", "password"); loginPage.login("login-test", "password");
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@ -282,7 +304,7 @@ public class LoginTest {
// Assert rememberMe checked and username/email prefilled // Assert rememberMe checked and username/email prefilled
loginPage.open(); loginPage.open();
Assert.assertTrue(loginPage.isRememberMeChecked()); assertTrue(loginPage.isRememberMeChecked());
Assert.assertEquals("login-test", loginPage.getUsername()); Assert.assertEquals("login-test", loginPage.getUsername());
loginPage.setRememberMe(false); loginPage.setRememberMe(false);