Merge branch 'upstream/master' into fork/master

* upstream_master: (115 commits)
  KEYCLOAK-1964 Database field for passwordPolicy is too small
  KEYCLOAK-1971 REST end point does't validate password policies properly
  KEYCLOAK-2104 Add caching of message bundles and properties to ExtendingThemeManager
  KEYCLOAK-2116 - Fix Label of Regenerate Secret button.
  Tweak Travis again
  KEYCLOAK-1937 OpenID Connect Dynamic Client Registration
  fix null pointer
  remove empty password check in direct grant flow
  Updated Travis configuration
  Exclude .lastUpdated files from Travis cache
  KEYCLOAK-2119 OTP Policy form validates both TOTP and HOTP at the same time
  [KEYCLOAK-2119] OTP Policy form validates both TOTP and HOTP at the same time
  KEYCLOAK-2010
  remove empty password check
  default groups ui
  KEYCLOAK-1749 Add documentation and fixed clean-up of expired initial access tokens
  Remove unused import
  Fix missing group in angular $scope in GroupMembersCtrl.
  arquillian-testsuite: Customizable waiting timeouts in class WaitUtils. Changed time unit to MILLISECONDS.
  default groups
  ...
This commit is contained in:
Ramiro Sánchez 2015-11-24 23:52:24 +01:00
commit 24f765ceef
528 changed files with 21078 additions and 9352 deletions

View file

@ -3,14 +3,8 @@ language: java
jdk: jdk:
- oraclejdk8 - oraclejdk8
cache: install:
directories: - travis_wait mvn install -Pdistribution -DskipTests=true -B -V -q
- $HOME/.m2
before_cache:
- rm -rf $HOME/.m2/repository/org/keycloak
install: mvn install -Pdistribution -DskipTests=true -B -V
script: script:
- mvn test -B - mvn test -B

View file

@ -92,4 +92,9 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) { public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context) {
} }
@Override
public IdentityProviderDataMarshaller getMarshaller() {
return new DefaultDataMarshaller();
}
} }

View file

@ -0,0 +1,40 @@
package org.keycloak.broker.provider;
import java.io.IOException;
import org.keycloak.common.util.Base64Url;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class DefaultDataMarshaller implements IdentityProviderDataMarshaller {
@Override
public String serialize(Object value) {
if (value instanceof String) {
return (String) value;
} else {
try {
byte[] bytes = JsonSerialization.writeValueAsBytes(value);
return Base64Url.encode(bytes);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}
@Override
public <T> T deserialize(String serialized, Class<T> clazz) {
if (clazz.equals(String.class)) {
return clazz.cast(serialized);
} else {
byte[] bytes = Base64Url.decode(serialized);
try {
return JsonSerialization.readValue(bytes, clazz);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}
}

View file

@ -103,4 +103,10 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
*/ */
Response export(UriInfo uriInfo, RealmModel realm, String format); Response export(UriInfo uriInfo, RealmModel realm, String format);
/**
* Implementation of marshaller to serialize/deserialize attached data to Strings, which can be saved in clientSession
* @return
*/
IdentityProviderDataMarshaller getMarshaller();
} }

View file

@ -0,0 +1,12 @@
package org.keycloak.broker.provider;
/**
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface IdentityProviderDataMarshaller {
String serialize(Object obj);
<T> T deserialize(String serialized, Class<T> clazz);
}

View file

@ -45,6 +45,11 @@
<artifactId>jboss-logging</artifactId> <artifactId>jboss-logging</artifactId>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View file

@ -0,0 +1,88 @@
package org.keycloak.broker.saml;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import javax.xml.stream.XMLEventReader;
import org.keycloak.broker.provider.DefaultDataMarshaller;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
import org.keycloak.saml.common.exceptions.ParsingException;
import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.StaxUtil;
import org.keycloak.saml.processing.core.parsers.saml.SAMLParser;
import org.keycloak.saml.processing.core.parsers.util.SAMLParserUtil;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLAssertionWriter;
import org.keycloak.saml.processing.core.saml.v2.writers.SAMLResponseWriter;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SAMLDataMarshaller extends DefaultDataMarshaller {
@Override
public String serialize(Object obj) {
// Lame impl, but hopefully sufficient for now. See if something better is needed...
if (obj.getClass().getName().startsWith("org.keycloak.dom.saml")) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
if (obj instanceof ResponseType) {
ResponseType responseType = (ResponseType) obj;
SAMLResponseWriter samlWriter = new SAMLResponseWriter(StaxUtil.getXMLStreamWriter(bos));
samlWriter.write(responseType);
} else if (obj instanceof AssertionType) {
AssertionType assertion = (AssertionType) obj;
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
samlWriter.write(assertion);
} else if (obj instanceof AuthnStatementType) {
AuthnStatementType authnStatement = (AuthnStatementType) obj;
SAMLAssertionWriter samlWriter = new SAMLAssertionWriter(StaxUtil.getXMLStreamWriter(bos));
samlWriter.write(authnStatement, true);
} else {
throw new IllegalArgumentException("Don't know how to serialize object of type " + obj.getClass().getName());
}
} catch (ProcessingException pe) {
throw new RuntimeException(pe);
}
return new String(bos.toByteArray());
} else {
return super.serialize(obj);
}
}
@Override
public <T> T deserialize(String serialized, Class<T> clazz) {
if (clazz.getName().startsWith("org.keycloak.dom.saml")) {
String xmlString = serialized;
try {
if (clazz.equals(ResponseType.class) || clazz.equals(AssertionType.class)) {
byte[] bytes = xmlString.getBytes();
InputStream is = new ByteArrayInputStream(bytes);
Object respType = new SAMLParser().parse(is);
return clazz.cast(respType);
} else if (clazz.equals(AuthnStatementType.class)) {
byte[] bytes = xmlString.getBytes();
InputStream is = new ByteArrayInputStream(bytes);
XMLEventReader xmlEventReader = new SAMLParser().createEventReader(is);
AuthnStatementType authnStatement = SAMLParserUtil.parseAuthnStatement(xmlEventReader);
return clazz.cast(authnStatement);
} else {
throw new IllegalArgumentException("Don't know how to deserialize object of type " + clazz.getName());
}
} catch (ParsingException pe) {
throw new RuntimeException(pe);
}
} else {
return super.deserialize(serialized, clazz);
}
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.provider.IdentityProviderDataMarshaller;
import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType; import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
@ -263,4 +264,8 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
return SignatureAlgorithm.RSA_SHA256; return SignatureAlgorithm.RSA_SHA256;
} }
@Override
public IdentityProviderDataMarshaller getMarshaller() {
return new SAMLDataMarshaller();
}
} }

View file

@ -0,0 +1,64 @@
package org.keycloak.broker.saml;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AuthnStatementType;
import org.keycloak.dom.saml.v2.assertion.NameIDType;
import org.keycloak.dom.saml.v2.protocol.ResponseType;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SAMLDataMarshallerTest {
private static final String TEST_RESPONSE = "<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_4804cf50-cd96-4b92-823e-89adaa0c78ba\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.920Z\" Destination=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\" InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"></samlp:StatusCode></samlp:Status><saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion></samlp:Response>";
private static final String TEST_ASSERTION = "<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9\" Version=\"2.0\" IssueInstant=\"2015-11-06T11:00:33.911Z\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://localhost:8082/auth/realms/realm-with-saml-idp-basic</saml:Issuer><saml:Subject><saml:NameID xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified\">test-user</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\"><saml:SubjectConfirmationData InResponseTo=\"ID_c6b90123-f0bb-4c5c-bf9d-388d5bbe467a\" NotOnOrAfter=\"2015-11-06T11:05:31.911Z\" Recipient=\"http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint\"></saml:SubjectConfirmationData></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore=\"2015-11-06T11:00:31.911Z\" NotOnOrAfter=\"2015-11-06T11:01:31.911Z\"><saml:AudienceRestriction><saml:Audience>http://localhost:8081/auth/realms/realm-with-broker</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name=\"mobile\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">617-666-7777</saml:AttributeValue></saml:Attribute><saml:Attribute Name=\"urn:oid:1.2.840.113549.1.9.1\" FriendlyName=\"email\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">test-user@localhost</saml:AttributeValue></saml:Attribute></saml:AttributeStatement><saml:AttributeStatement><saml:Attribute Name=\"Role\" NameFormat=\"urn:oasis:names:tc:SAML:2.0:attrname-format:basic\"><saml:AttributeValue xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xsi:type=\"xs:string\">manager</saml:AttributeValue></saml:Attribute></saml:AttributeStatement></saml:Assertion>";
private static final String TEST_AUTHN_TYPE = "<saml:AuthnStatement xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\" AuthnInstant=\"2015-11-06T11:00:33.923Z\" SessionIndex=\"fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>";
@Test
public void testParseResponse() throws Exception {
SAMLDataMarshaller serializer = new SAMLDataMarshaller();
ResponseType responseType = serializer.deserialize(TEST_RESPONSE, ResponseType.class);
// test ResponseType
Assert.assertEquals(responseType.getID(), "ID_4804cf50-cd96-4b92-823e-89adaa0c78ba");
Assert.assertEquals(responseType.getDestination(), "http://localhost:8081/auth/realms/realm-with-broker/broker/kc-saml-idp-basic/endpoint");
Assert.assertEquals(responseType.getIssuer().getValue(), "http://localhost:8082/auth/realms/realm-with-saml-idp-basic");
Assert.assertEquals(responseType.getAssertions().get(0).getID(), "ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9");
// back to String
String serialized = serializer.serialize(responseType);
Assert.assertEquals(TEST_RESPONSE, serialized);
}
@Test
public void testParseAssertion() throws Exception {
SAMLDataMarshaller serializer = new SAMLDataMarshaller();
AssertionType assertion = serializer.deserialize(TEST_ASSERTION, AssertionType.class);
// test assertion
Assert.assertEquals(assertion.getID(), "ID_29b196c2-d641-45c8-a423-8ed8e54d4cf9");
Assert.assertEquals(((NameIDType) assertion.getSubject().getSubType().getBaseID()).getValue(), "test-user");
// back to String
String serialized = serializer.serialize(assertion);
Assert.assertEquals(TEST_ASSERTION, serialized);
}
@Test
public void testParseAuthnType() throws Exception {
SAMLDataMarshaller serializer = new SAMLDataMarshaller();
AuthnStatementType authnStatement = serializer.deserialize(TEST_AUTHN_TYPE, AuthnStatementType.class);
// test authnStatement
Assert.assertEquals(authnStatement.getSessionIndex(), "fa0f4fd3-8a11-44f4-9acb-ee30c5bb8fe5");
// back to String
String serialized = serializer.serialize(authnStatement);
Assert.assertEquals(TEST_AUTHN_TYPE, serialized);
}
}

View file

@ -1,275 +0,0 @@
package org.keycloak.client.registration;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.common.util.Base64;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.io.InputStream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistration {
private String clientRegistrationUrl;
private HttpClient httpClient;
private Auth auth;
public static ClientRegistrationBuilder create() {
return new ClientRegistrationBuilder();
}
private ClientRegistration() {
}
public ClientRepresentation create(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = doPost(content);
return deserialize(resultStream, ClientRepresentation.class);
}
public ClientRepresentation get() throws ClientRegistrationException {
if (auth instanceof ClientIdSecretAuth) {
String clientId = ((ClientIdSecretAuth) auth).clientId;
return get(clientId);
} else {
throw new ClientRegistrationException("Requires client authentication");
}
}
public ClientRepresentation get(String clientId) throws ClientRegistrationException {
InputStream resultStream = doGet(clientId);
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
}
public void update(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
doPut(content, client.getClientId());
}
public void delete() throws ClientRegistrationException {
if (auth instanceof ClientIdSecretAuth) {
String clientId = ((ClientIdSecretAuth) auth).clientId;
delete(clientId);
} else {
throw new ClientRegistrationException("Requires client authentication");
}
}
public void delete(String clientId) throws ClientRegistrationException {
doDelete(clientId);
}
public void close() throws ClientRegistrationException {
if (httpClient instanceof CloseableHttpClient) {
try {
((CloseableHttpClient) httpClient).close();
} catch (IOException e) {
throw new ClientRegistrationException("Failed to close http client", e);
}
}
}
private InputStream doPost(String content) throws ClientRegistrationException {
try {
HttpPost request = new HttpPost(clientRegistrationUrl);
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setHeader(HttpHeaders.ACCEPT, "application/json");
request.setEntity(new StringEntity(content));
auth.addAuth(request);
HttpResponse response = httpClient.execute(request);
InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}
if (response.getStatusLine().getStatusCode() == 201) {
return responseStream;
} else {
responseStream.close();
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private InputStream doGet(String endpoint) throws ClientRegistrationException {
try {
HttpGet request = new HttpGet(clientRegistrationUrl + "/" + endpoint);
request.setHeader(HttpHeaders.ACCEPT, "application/json");
auth.addAuth(request);
HttpResponse response = httpClient.execute(request);
InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}
if (response.getStatusLine().getStatusCode() == 200) {
return responseStream;
} else if (response.getStatusLine().getStatusCode() == 404) {
responseStream.close();
return null;
} else {
responseStream.close();
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private void doPut(String content, String endpoint) throws ClientRegistrationException {
try {
HttpPut request = new HttpPut(clientRegistrationUrl + "/" + endpoint);
request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");
request.setHeader(HttpHeaders.ACCEPT, "application/json");
request.setEntity(new StringEntity(content));
auth.addAuth(request);
HttpResponse response = httpClient.execute(request);
if (response.getEntity() != null) {
response.getEntity().getContent().close();
}
if (response.getStatusLine().getStatusCode() != 200) {
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private void doDelete(String endpoint) throws ClientRegistrationException {
try {
HttpDelete request = new HttpDelete(clientRegistrationUrl + "/" + endpoint);
auth.addAuth(request);
HttpResponse response = httpClient.execute(request);
if (response.getEntity() != null) {
response.getEntity().getContent().close();
}
if (response.getStatusLine().getStatusCode() != 200) {
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
private String serialize(ClientRepresentation client) throws ClientRegistrationException {
try {
return JsonSerialization.writeValueAsString(client);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to write json object", e);
}
}
private <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
try {
return JsonSerialization.readValue(inputStream, clazz);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to read json object", e);
}
}
public static class ClientRegistrationBuilder {
private String realm;
private String authServerUrl;
private Auth auth;
private HttpClient httpClient;
public ClientRegistrationBuilder realm(String realm) {
this.realm = realm;
return this;
}
public ClientRegistrationBuilder authServerUrl(String authServerUrl) {
this.authServerUrl = authServerUrl;
return this;
}
public ClientRegistrationBuilder auth(String token) {
this.auth = new TokenAuth(token);
return this;
}
public ClientRegistrationBuilder auth(String clientId, String clientSecret) {
this.auth = new ClientIdSecretAuth(clientId, clientSecret);
return this;
}
public ClientRegistrationBuilder httpClient(HttpClient httpClient) {
this.httpClient = httpClient;
return this;
}
public ClientRegistration build() {
ClientRegistration clientRegistration = new ClientRegistration();
clientRegistration.clientRegistrationUrl = authServerUrl + "/realms/" + realm + "/client-registration/default";
clientRegistration.httpClient = httpClient != null ? httpClient : HttpClients.createDefault();
clientRegistration.auth = auth;
return clientRegistration;
}
}
public interface Auth {
void addAuth(HttpRequest httpRequest);
}
public static class AuthorizationHeaderAuth implements Auth {
private String credentials;
public AuthorizationHeaderAuth(String credentials) {
this.credentials = credentials;
}
public void addAuth(HttpRequest httpRequest) {
httpRequest.setHeader(HttpHeaders.AUTHORIZATION, credentials);
}
}
public static class TokenAuth extends AuthorizationHeaderAuth {
public TokenAuth(String token) {
super("Bearer " + token);
}
}
public static class ClientIdSecretAuth extends AuthorizationHeaderAuth {
private String clientId;
public ClientIdSecretAuth(String clientId, String clientSecret) {
super("Basic " + Base64.encodeBytes((clientId + ":" + clientSecret).getBytes()));
this.clientId = clientId;
}
}
}

View file

@ -2,14 +2,14 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent> <parent>
<artifactId>keycloak-parent</artifactId> <artifactId>keycloak-client-registration-parent</artifactId>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<version>1.7.0.Final-SNAPSHOT</version> <version>1.7.0.Final-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-client-api</artifactId> <artifactId>keycloak-client-registration-api</artifactId>
<name>Keycloak Client API</name> <name>Keycloak Client Registration API</name>
<description/> <description/>
<dependencies> <dependencies>
@ -21,11 +21,6 @@
<groupId>org.apache.httpcomponents</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View file

@ -0,0 +1,68 @@
package org.keycloak.client.registration;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.keycloak.common.util.Base64;
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public abstract class Auth {
public abstract void addAuth(HttpRequest request);
public static Auth token(String token) {
return new BearerTokenAuth(token);
}
public static Auth token(ClientInitialAccessPresentation initialAccess) {
return new BearerTokenAuth(initialAccess.getToken());
}
public static Auth token(ClientRepresentation client) {
return new BearerTokenAuth(client.getRegistrationAccessToken());
}
public static Auth token(OIDCClientRepresentation client) {
return new BearerTokenAuth(client.getRegistrationAccessToken());
}
public static Auth client(String clientId, String clientSecret) {
return new BasicAuth(clientId, clientSecret);
}
private static class BearerTokenAuth extends Auth {
private String token;
public BearerTokenAuth(String token) {
this.token = token;
}
@Override
public void addAuth(HttpRequest request) {
request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
}
}
private static class BasicAuth extends Auth {
private String username;
private String password;
public BasicAuth(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public void addAuth(HttpRequest request) {
String val = Base64.encodeBytes((username + ":" + password).getBytes());
request.setHeader(HttpHeaders.AUTHORIZATION, "Basic " + val);
}
}
}

View file

@ -0,0 +1,186 @@
package org.keycloak.client.registration;
import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClients;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.keycloak.representations.adapters.config.AdapterConfig;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.oidc.OIDCClientRepresentation;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.io.InputStream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistration {
public static final ObjectMapper outputMapper = new ObjectMapper();
static {
outputMapper.getSerializationConfig().addMixInAnnotations(ClientRepresentation.class, ClientRepresentationMixIn.class);
outputMapper.getSerializationConfig().addMixInAnnotations(OIDCClientRepresentation.class, OIDCClientRepresentationMixIn.class);
outputMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
}
private final String JSON = "application/json";
private final String XML = "application/xml";
private final String DEFAULT = "default";
private final String INSTALLATION = "install";
private final String OIDC = "openid-connect";
private final String SAML = "saml2-entity-descriptor";
private HttpUtil httpUtil;
public static ClientRegistrationBuilder create() {
return new ClientRegistrationBuilder();
}
ClientRegistration(HttpUtil httpUtil) {
this.httpUtil = httpUtil;
}
public void close() throws ClientRegistrationException {
if (httpUtil != null) {
httpUtil.close();
}
httpUtil = null;
}
public ClientRegistration auth(Auth auth) {
httpUtil.setAuth(auth);
return this;
}
public ClientRepresentation create(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = httpUtil.doPost(content, JSON, JSON, DEFAULT);
return deserialize(resultStream, ClientRepresentation.class);
}
public ClientRepresentation get(String clientId) throws ClientRegistrationException {
InputStream resultStream = httpUtil.doGet(JSON, DEFAULT, clientId);
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
}
public AdapterConfig getAdapterConfig(String clientId) throws ClientRegistrationException {
InputStream resultStream = httpUtil.doGet(JSON, INSTALLATION, clientId);
return resultStream != null ? deserialize(resultStream, AdapterConfig.class) : null;
}
public ClientRepresentation update(ClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = httpUtil.doPut(content, JSON, JSON, DEFAULT, client.getClientId());
return resultStream != null ? deserialize(resultStream, ClientRepresentation.class) : null;
}
public void delete(ClientRepresentation client) throws ClientRegistrationException {
delete(client.getClientId());
}
public void delete(String clientId) throws ClientRegistrationException {
httpUtil.doDelete(DEFAULT, clientId);
}
public OIDCClientRegistration oidc() {
return new OIDCClientRegistration();
}
public SAMLClientRegistration saml() {
return new SAMLClientRegistration();
}
public static String serialize(Object obj) throws ClientRegistrationException {
try {
return outputMapper.writeValueAsString(obj);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to write json object", e);
}
}
private static <T> T deserialize(InputStream inputStream, Class<T> clazz) throws ClientRegistrationException {
try {
return JsonSerialization.readValue(inputStream, clazz);
} catch (IOException e) {
throw new ClientRegistrationException("Failed to read json object", e);
}
}
public class OIDCClientRegistration {
public OIDCClientRepresentation create(OIDCClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = httpUtil.doPost(content, JSON, JSON, OIDC);
return deserialize(resultStream, OIDCClientRepresentation.class);
}
public OIDCClientRepresentation get(String clientId) throws ClientRegistrationException {
InputStream resultStream = httpUtil.doGet(JSON, OIDC, clientId);
return resultStream != null ? deserialize(resultStream, OIDCClientRepresentation.class) : null;
}
public OIDCClientRepresentation update(OIDCClientRepresentation client) throws ClientRegistrationException {
String content = serialize(client);
InputStream resultStream = httpUtil.doPut(content, JSON, JSON, OIDC, client.getClientId());
return resultStream != null ? deserialize(resultStream, OIDCClientRepresentation.class) : null;
}
public void delete(OIDCClientRepresentation client) throws ClientRegistrationException {
delete(client.getClientId());
}
public void delete(String clientId) throws ClientRegistrationException {
httpUtil.doDelete(OIDC, clientId);
}
}
public class SAMLClientRegistration {
public ClientRepresentation create(String entityDescriptor) throws ClientRegistrationException {
InputStream resultStream = httpUtil.doPost(entityDescriptor, XML, JSON, SAML);
return deserialize(resultStream, ClientRepresentation.class);
}
}
public static class ClientRegistrationBuilder {
private String url;
private HttpClient httpClient;
ClientRegistrationBuilder() {
}
public ClientRegistrationBuilder url(String realmUrl) {
url = realmUrl;
return this;
}
public ClientRegistrationBuilder url(String authUrl, String realm) {
url = HttpUtil.getUrl(authUrl, "realms", realm, "clients");
return this;
}
public ClientRegistrationBuilder httpClient(HttpClient httpClient) {
this.httpClient = httpClient;
return this;
}
public ClientRegistration build() {
if (url == null) {
throw new IllegalStateException("url not configured");
}
if (httpClient == null) {
httpClient = HttpClients.createDefault();
}
return new ClientRegistration(new HttpUtil(httpClient, url));
}
}
}

View file

@ -0,0 +1,13 @@
package org.keycloak.client.registration;
import org.codehaus.jackson.annotate.JsonIgnore;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
abstract class ClientRepresentationMixIn {
@JsonIgnore
String registrationAccessToken;
}

View file

@ -0,0 +1,171 @@
package org.keycloak.client.registration;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.HttpErrorException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
class HttpUtil {
private HttpClient httpClient;
private String baseUri;
private Auth auth;
HttpUtil(HttpClient httpClient, String baseUri) {
this.httpClient = httpClient;
this.baseUri = baseUri;
}
void setAuth(Auth auth) {
this.auth = auth;
}
InputStream doPost(String content, String contentType, String acceptType, String... path) throws ClientRegistrationException {
try {
HttpPost request = new HttpPost(getUrl(baseUri, path));
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
request.setHeader(HttpHeaders.ACCEPT, acceptType);
request.setEntity(new StringEntity(content));
addAuth(request);
HttpResponse response = httpClient.execute(request);
InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}
if (response.getStatusLine().getStatusCode() == 201) {
return responseStream;
} else {
responseStream.close();
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
InputStream doGet(String acceptType, String... path) throws ClientRegistrationException {
try {
HttpGet request = new HttpGet(getUrl(baseUri, path));
request.setHeader(HttpHeaders.ACCEPT, acceptType);
addAuth(request);
HttpResponse response = httpClient.execute(request);
InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}
if (response.getStatusLine().getStatusCode() == 200) {
return responseStream;
} else if (response.getStatusLine().getStatusCode() == 404) {
responseStream.close();
return null;
} else {
if (responseStream != null) {
responseStream.close();
}
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
InputStream doPut(String content, String contentType, String acceptType, String... path) throws ClientRegistrationException {
try {
HttpPut request = new HttpPut(getUrl(baseUri, path));
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
request.setHeader(HttpHeaders.ACCEPT, acceptType);
request.setEntity(new StringEntity(content));
addAuth(request);
HttpResponse response = httpClient.execute(request);
if (response.getEntity() != null) {
response.getEntity().getContent();
}
InputStream responseStream = null;
if (response.getEntity() != null) {
responseStream = response.getEntity().getContent();
}
if (response.getStatusLine().getStatusCode() == 200) {
return responseStream;
} else {
if (responseStream != null) {
responseStream.close();
}
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
void doDelete(String... path) throws ClientRegistrationException {
try {
HttpDelete request = new HttpDelete(getUrl(baseUri, path));
addAuth(request);
HttpResponse response = httpClient.execute(request);
if (response.getEntity() != null) {
response.getEntity().getContent().close();
}
if (response.getStatusLine().getStatusCode() != 204) {
throw new HttpErrorException(response.getStatusLine());
}
} catch (IOException e) {
throw new ClientRegistrationException("Failed to send request", e);
}
}
void close() throws ClientRegistrationException {
if (httpClient instanceof CloseableHttpClient) {
try {
((CloseableHttpClient) httpClient).close();
} catch (IOException e) {
throw new ClientRegistrationException("Failed to close http client", e);
}
}
}
static String getUrl(String baseUri, String... path) {
StringBuilder s = new StringBuilder();
s.append(baseUri);
for (String p : path) {
s.append('/');
s.append(p);
}
return s.toString();
}
private void addAuth(HttpRequestBase request) {
if (auth != null) {
auth.addAuth(request);
}
}
}

View file

@ -0,0 +1,22 @@
package org.keycloak.client.registration;
import org.codehaus.jackson.annotate.JsonIgnore;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
abstract class OIDCClientRepresentationMixIn {
@JsonIgnore
private Integer client_id_issued_at;
@JsonIgnore
private Integer client_secret_expires_at;
@JsonIgnore
private String registration_client_uri;
@JsonIgnore
private String registration_access_token;
}

34
client-registration/cli/pom.xml Executable file
View file

@ -0,0 +1,34 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-client-registration-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.7.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-client-registration-cli</artifactId>
<name>Keycloak Client Registration CLI</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-client-registration-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.aesh</groupId>
<artifactId>aesh</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,70 @@
package org.keycloak.client.registration.cli;
import org.jboss.aesh.cl.parser.CommandLineParserException;
import org.jboss.aesh.console.AeshConsole;
import org.jboss.aesh.console.AeshConsoleBuilder;
import org.jboss.aesh.console.Prompt;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandNotFoundException;
import org.jboss.aesh.console.command.registry.AeshCommandRegistryBuilder;
import org.jboss.aesh.console.settings.Settings;
import org.jboss.aesh.console.settings.SettingsBuilder;
import org.jboss.aesh.terminal.Color;
import org.jboss.aesh.terminal.TerminalColor;
import org.jboss.aesh.terminal.TerminalString;
import org.keycloak.client.registration.Auth;
import org.keycloak.client.registration.ClientRegistration;
import org.keycloak.client.registration.cli.commands.CreateCommand;
import org.keycloak.client.registration.cli.commands.ExitCommand;
import org.keycloak.client.registration.cli.commands.SetupCommand;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientRegistrationCLI {
private static ClientRegistration reg;
public static void main(String[] args) throws CommandLineParserException, CommandNotFoundException {
reg = ClientRegistration.create().url("http://localhost:8080/auth/realms/master").build();
reg.auth(Auth.token("..."));
Context context = new Context();
List<Command> commands = new LinkedList<>();
commands.add(new SetupCommand(context));
commands.add(new CreateCommand(context));
commands.add(new ExitCommand(context));
SettingsBuilder builder = new SettingsBuilder().logging(true);
builder.enableMan(true).readInputrc(false);
Settings settings = builder.create();
AeshCommandRegistryBuilder commandRegistryBuilder = new AeshCommandRegistryBuilder();
for (Command c : commands) {
commandRegistryBuilder.command(c);
}
AeshConsole aeshConsole = new AeshConsoleBuilder()
.commandRegistry(commandRegistryBuilder.create())
.settings(settings)
.prompt(new Prompt(new TerminalString("[clientreg]$ ",
new TerminalColor(Color.GREEN, Color.DEFAULT, Color.Intensity.BRIGHT))))
.create();
aeshConsole.start();
/*
if (args.length > 0) {
CommandContainer command = registry.getCommand(args[0], null);
ParserGenerator.parseAndPopulate(command, args[0], Arrays.copyOfRange(args, 1, args.length));
}*/
//commandInvocation.getCommandRegistry().getAllCommandNames()
}
}

View file

@ -0,0 +1,37 @@
package org.keycloak.client.registration.cli;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.keycloak.client.registration.ClientRegistration;
import org.keycloak.util.SystemPropertiesJsonParserFactory;
import java.io.IOException;
import java.io.InputStream;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Context {
private static final ObjectMapper mapper = new ObjectMapper(new SystemPropertiesJsonParserFactory());
static {
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
mapper.enable(SerializationConfig.Feature.INDENT_OUTPUT);
}
private ClientRegistration reg;
public ClientRegistration getReg() {
return reg;
}
public void setReg(ClientRegistration reg) {
this.reg = reg;
}
public static <T> T readJson(InputStream bytes, Class<T> type) throws IOException {
return mapper.readValue(bytes, type);
}
}

View file

@ -0,0 +1,64 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.Arguments;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.jboss.aesh.io.Resource;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.cli.Context;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@CommandDefinition(name="create", description = "[OPTIONS] FILE")
public class CreateCommand implements Command {
@Option(shortName = 'h', hasValue = false, description = "display this help and exit")
private boolean help;
@Arguments(description = "files or directories thats listed")
private List<Resource> arguments;
private Context context;
public CreateCommand(Context context) {
this.context = context;
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
System.out.println(help);
if(help) {
commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
}
else {
if(arguments != null) {
for(Resource f : arguments) {
System.out.println(f.getAbsolutePath());
ClientRepresentation rep = JsonSerialization.readValue(f.read(), ClientRepresentation.class);
try {
context.getReg().create(rep);
} catch (ClientRegistrationException e) {
e.printStackTrace();
}
}
}
}
// reg.create();
return CommandResult.SUCCESS;
}
}

View file

@ -0,0 +1,29 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.keycloak.client.registration.cli.Context;
import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@CommandDefinition(name="exit", description = "Exit the program")
public class ExitCommand implements Command {
private Context context;
public ExitCommand(Context context) {
this.context = context;
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
commandInvocation.stop();
return CommandResult.SUCCESS;
}
}

View file

@ -0,0 +1,48 @@
package org.keycloak.client.registration.cli.commands;
import org.jboss.aesh.cl.CommandDefinition;
import org.jboss.aesh.cl.Option;
import org.jboss.aesh.console.command.Command;
import org.jboss.aesh.console.command.CommandResult;
import org.jboss.aesh.console.command.invocation.CommandInvocation;
import org.jboss.aesh.io.Resource;
import org.keycloak.client.registration.ClientRegistrationException;
import org.keycloak.client.registration.cli.Context;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@CommandDefinition(name="setup", description = "")
public class SetupCommand implements Command {
@Option(shortName = 'h', hasValue = false, description = "display this help and exit")
private boolean help;
private Context context;
public SetupCommand(Context context) {
this.context = context;
}
@Override
public CommandResult execute(CommandInvocation commandInvocation) throws IOException, InterruptedException {
System.out.println(help);
if(help) {
commandInvocation.getShell().out().println(commandInvocation.getHelpInfo("create"));
}
return CommandResult.SUCCESS;
}
private String promptForUsername(CommandInvocation invocation) throws InterruptedException {
invocation.print("username: ");
return invocation.getInputLine();
}
}

19
client-registration/pom.xml Executable file
View file

@ -0,0 +1,19 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.7.0.Final-SNAPSHOT</version>
</parent>
<name>Keycloak Client Registration Parent</name>
<description/>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-client-registration-parent</artifactId>
<packaging>pom</packaging>
<modules>
<module>api</module>
<!--<module>cli</module>-->
</modules>
</project>

View file

@ -13,7 +13,7 @@ public class ObjectUtil {
* @param str2 * @param str2
* @return true if both strings are null or equal * @return true if both strings are null or equal
*/ */
public static boolean isEqualOrNull(Object str1, Object str2) { public static boolean isEqualOrBothNull(Object str1, Object str2) {
if (str1 == null && str2 == null) { if (str1 == null && str2 == null) {
return true; return true;
} }
@ -24,4 +24,8 @@ public class ObjectUtil {
return str1.equals(str2); return str1.equals(str2);
} }
public static String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
} }

View file

@ -1,43 +0,0 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.7.0.Final-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-connections-file</artifactId>
<name>Keycloak Connections File</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-export-import-api</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-export-import-single-file</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-mapper-asl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -1,86 +0,0 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.keycloak.connections.file;
import org.keycloak.models.KeycloakSession;
/**
* Provides the InMemoryModel and notifies the factory to save it when
* the session is done.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class DefaultFileConnectionProvider implements FileConnectionProvider {
private final DefaultFileConnectionProviderFactory factory;
private final KeycloakSession session;
private final InMemoryModel inMemoryModel;
private boolean isRollbackOnly = false;
public DefaultFileConnectionProvider(DefaultFileConnectionProviderFactory factory,
KeycloakSession session,
InMemoryModel inMemoryModel) {
this.factory = factory;
this.session = session;
this.inMemoryModel = inMemoryModel;
}
@Override
public InMemoryModel getModel() {
return inMemoryModel;
}
@Override
public void sessionClosed(KeycloakSession session) {
factory.sessionClosed(session);
}
@Override
public void close() {
}
@Override
public void begin() {
}
@Override
public void commit() {
factory.commit(session);
}
@Override
public void rollback() {
factory.rollback(session);
}
@Override
public void setRollbackOnly() {
isRollbackOnly = true;
}
@Override
public boolean getRollbackOnly() {
return isRollbackOnly;
}
@Override
public boolean isActive() {
return factory.isActive(session);
}
}

View file

@ -1,210 +0,0 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.keycloak.connections.file;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.codehaus.jackson.JsonToken;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.exportimport.Strategy;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.exportimport.util.ImportUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.util.JsonSerialization;
/**
* This class dispenses a FileConnectionProvider to Keycloak sessions. It
* makes sure that only one InMemoryModel is provided for each session and it
* handles thread contention for the file where the model is read or saved.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class DefaultFileConnectionProviderFactory implements FileConnectionProviderFactory {
protected static final Logger logger = Logger.getLogger(DefaultFileConnectionProviderFactory.class);
private File kcdata;
private final Map<KeycloakSession, FileConnectionProvider> allProviders = new HashMap<KeycloakSession, FileConnectionProvider>();
@Override
public void init(Config.Scope config) {
String fileName = config.get("fileName");
if (fileName == null) {
fileName = "keycloak-model.json";
}
String directory = config.get("directory");
if (directory == null) {
directory = System.getProperty("jboss.server.data.dir");
}
if (directory == null) {
directory = ".";
}
kcdata = new File(directory, fileName);
}
public void sessionClosed(KeycloakSession session) {
synchronized(allProviders) {
allProviders.remove(session);
//logger.info("Removed session " + session.hashCode());
//logger.info("sessionClosed: Session count=" + allModels.size());
}
}
void readModelFile(KeycloakSession session) {
synchronized(allProviders) {
if (!kcdata.exists()) {
return;
}
FileInputStream fis = null;
try {
fis = new FileInputStream(kcdata);
Model model = JsonSerialization.readValue(fis, Model.class);
ImportUtils.importFromStream(session, JsonSerialization.mapper, fis, Strategy.IGNORE_EXISTING);
session.realms().getMigrationModel().setStoredVersion(model.getModelVersion());
ImportUtils.importRealms(session, model.getRealms(), Strategy.IGNORE_EXISTING);
} catch (IOException ioe) {
logger.error("Unable to read model file " + kcdata.getAbsolutePath(), ioe);
} finally {
//logger.info("Read model file for session=" + session.hashCode());
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
logger.error("Failed to close output stream.", e);
}
}
}
}
void writeModelFile(KeycloakSession session) {
synchronized(allProviders) {
FileOutputStream outStream = null;
try {
outStream = new FileOutputStream(kcdata);
exportModel(session, outStream);
} catch (IOException e) {
logger.error("Unable to write model file " + kcdata.getAbsolutePath(), e);
} finally {
//logger.info("Wrote model file for session=" + session.hashCode());
try {
if (outStream != null) {
outStream.close();
}
} catch (IOException e) {
logger.error("Failed to close output stream.", e);
}
}
}
}
private void exportModel(KeycloakSession session, FileOutputStream outStream) throws IOException {
List<RealmModel> realms = session.realms().getRealms();
List<RealmRepresentation> reps = new ArrayList<RealmRepresentation>();
for (RealmModel realm : realms) {
reps.add(ExportUtils.exportRealm(session, realm, true));
}
Model model = new Model();
model.setRealms(reps);
model.setModelVersion(session.realms().getMigrationModel().getStoredVersion());
JsonSerialization.prettyMapper.writeValue(outStream, model);
}
@Override
public FileConnectionProvider create(KeycloakSession session) {
synchronized (allProviders) {
FileConnectionProvider fcProvider = allProviders.get(session);
if (fcProvider == null) {
InMemoryModel model = new InMemoryModel();
fcProvider = new DefaultFileConnectionProvider(this, session, model);
allProviders.put(session, fcProvider);
session.getTransaction().enlist(fcProvider);
readModelFile(session);
//logger.info("Added session " + session.hashCode() + " total sessions=" + allModels.size());
}
return fcProvider;
}
}
// commitCount is used for debugging. This allows you to easily run a test
// to a particular point and then examine the JSON file.
//private static int commitCount = 0;
void commit(KeycloakSession session) {
//commitCount++;
synchronized (allProviders) {
// in case commit was somehow called twice on the same session
if (!allProviders.containsKey(session)) return;
try {
writeModelFile(session);
} finally {
allProviders.remove(session);
//logger.info("Removed session " + session.hashCode());
//logger.info("*** commitCount=" + commitCount);
//logger.info("commit(): Session count=" + allModels.size());
}
// if (commitCount == 16) {Thread.dumpStack();System.exit(0);}
}
}
void rollback(KeycloakSession session) {
synchronized (allProviders) {
allProviders.remove(session);
//logger.info("rollback(): Session count=" + allModels.size());
}
}
boolean isActive(KeycloakSession session) {
synchronized (allProviders) {
return allProviders.containsKey(session);
}
}
@Override
public void close() {
}
@Override
public String getId() {
return "default";
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
}

View file

@ -1,31 +0,0 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.keycloak.connections.file;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakTransaction;
import org.keycloak.provider.Provider;
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public interface FileConnectionProvider extends Provider, KeycloakTransaction {
InMemoryModel getModel();
void sessionClosed(KeycloakSession session);
}

View file

@ -1,25 +0,0 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.keycloak.connections.file;
import org.keycloak.provider.ProviderFactory;
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public interface FileConnectionProviderFactory extends ProviderFactory<FileConnectionProvider> {
}

View file

@ -1,32 +0,0 @@
package org.keycloak.connections.file;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class FileConnectionSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "connectionsFile";
}
@Override
public Class<? extends Provider> getProviderClass() {
return FileConnectionProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return FileConnectionProviderFactory.class;
}
}

View file

@ -1,109 +0,0 @@
/*
* Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @author tags. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.keycloak.connections.file;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
/**
* This class provides an in-memory copy of the entire model for each
* Keycloak session. At the start of the session, the model is read
* from JSON. When the session's transaction ends, the model is written back
* out.
*
* @author Stan Silvert ssilvert@redhat.com (C) 2015 Red Hat Inc.
*/
public class InMemoryModel {
private final Map<String, RealmModel> allRealms = new HashMap<String, RealmModel>();
// realmId, userId, userModel
private final Map<String, Map<String,UserModel>> allUsers = new HashMap<String, Map<String,UserModel>>();
private String modelVersion;
public InMemoryModel() {
}
public void putRealm(String id, RealmModel realm) {
allRealms.put(id, realm);
allUsers.put(id, new HashMap<String, UserModel>());
}
public String getModelVersion() {
return modelVersion;
}
public void setModelVersion(String modelVersion) {
this.modelVersion = modelVersion;
}
public RealmModel getRealm(String id) {
return allRealms.get(id);
}
public Collection<RealmModel> getRealms() {
return allRealms.values();
}
public RealmModel getRealmByName(String name) {
for (RealmModel realm : getRealms()) {
if (realm.getName().equals(name)) return realm;
}
return null;
}
public boolean removeRealm(String id) {
allUsers.remove(id);
return (allRealms.remove(id) != null);
}
protected Map<String, UserModel> realmUsers(String realmId) {
Map<String, UserModel> realmUsers = allUsers.get(realmId);
if (realmUsers == null) throw new NullPointerException("Realm users not found for id=" + realmId);
return realmUsers;
}
public void putUser(String realmId, String userId, UserModel user) {
realmUsers(realmId).put(userId, user);
}
public UserModel getUser(String realmId, String userId) {
return realmUsers(realmId).get(userId);
}
public boolean hasUserWithUsername(String realmId, String username) {
for (UserModel user : getUsers(realmId)) {
if (user.getUsername().equals(username)) return true;
}
return false;
}
public Collection<UserModel> getUsers(String realmId) {
return realmUsers(realmId).values();
}
public boolean removeUser(String realmId, String userId) {
return (realmUsers(realmId).remove(userId) != null);
}
}

View file

@ -1,30 +0,0 @@
package org.keycloak.connections.file;
import org.keycloak.representations.idm.RealmRepresentation;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Model {
private String modelVersion;
private List<RealmRepresentation> realms;
public String getModelVersion() {
return modelVersion;
}
public void setModelVersion(String modelVersion) {
this.modelVersion = modelVersion;
}
public List<RealmRepresentation> getRealms() {
return realms;
}
public void setRealms(List<RealmRepresentation> realms) {
this.realms = realms;
}
}

View file

@ -1 +0,0 @@
org.keycloak.connections.file.DefaultFileConnectionProviderFactory

View file

@ -1 +0,0 @@
org.keycloak.connections.file.FileConnectionSpi

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="bburke@redhat.com" id="1.7.0">
<createTable tableName="KEYCLOAK_GROUP">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)"/>
<column name="PARENT_GROUP" type="VARCHAR(36)"/>
<column name="REALM_ID" type="VARCHAR(36)"/>
</createTable>
<createTable tableName="GROUP_ROLE_MAPPING">
<column name="ROLE_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="GROUP_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="GROUP_ATTRIBUTE">
<column name="ID" type="VARCHAR(36)" defaultValue="sybase-needs-something-here">
<constraints nullable="false"/>
</column>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="GROUP_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="USER_GROUP_MEMBERSHIP">
<column name="GROUP_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="REALM_DEFAULT_GROUPS">
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="GROUP_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<addColumn tableName="IDENTITY_PROVIDER">
<column name="FIRST_BROKER_LOGIN_FLOW_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</addColumn>
<dropColumn tableName="IDENTITY_PROVIDER" columnName="UPDATE_PROFILE_FIRST_LGN_MD"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GROUP" tableName="KEYCLOAK_GROUP"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="KEYCLOAK_GROUP" constraintName="FK_GROUP_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GROUP_ATTRIBUTE_PK" tableName="GROUP_ATTRIBUTE"/>
<addForeignKeyConstraint baseColumnNames="GROUP_ID" baseTableName="GROUP_ATTRIBUTE" constraintName="FK_GROUP_ATTRIBUTE_GROUP" referencedColumnNames="ID" referencedTableName="KEYCLOAK_GROUP"/>
<addPrimaryKey columnNames="GROUP_ID, USER_ID" constraintName="CONSTRAINT_USER_GROUP" tableName="USER_GROUP_MEMBERSHIP"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="USER_GROUP_MEMBERSHIP" constraintName="FK_USER_GROUP_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addPrimaryKey columnNames="ROLE_ID, GROUP_ID" constraintName="CONSTRAINT_GROUP_ROLE" tableName="GROUP_ROLE_MAPPING"/>
<addForeignKeyConstraint baseColumnNames="GROUP_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_GROUP" referencedColumnNames="ID" referencedTableName="KEYCLOAK_GROUP"/>
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="GROUP_ROLE_MAPPING" constraintName="FK_GROUP_ROLE_ROLE" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addUniqueConstraint columnNames="GROUP_ID" constraintName="CON_GROUP_ID_DEF_GROUPS" tableName="REALM_DEFAULT_GROUPS"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_DEFAULT_GROUPS" constraintName="FK_DEF_GROUPS_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="GROUP_ID" baseTableName="REALM_DEFAULT_GROUPS" constraintName="FK_DEF_GROUPS_GROUP" referencedColumnNames="ID" referencedTableName="KEYCLOAK_GROUP"/>
<addColumn tableName="CLIENT">
<column name="REGISTRATION_TOKEN" type="VARCHAR(255)"/>
</addColumn>
<modifyDataType tableName="REALM" columnName="PASSWORD_POLICY" newDataType="VARCHAR(2550)"/>
</changeSet>
</databaseChangeLog>

View file

@ -10,4 +10,5 @@
<include file="META-INF/jpa-changelog-1.4.0.xml"/> <include file="META-INF/jpa-changelog-1.4.0.xml"/>
<include file="META-INF/jpa-changelog-1.5.0.xml"/> <include file="META-INF/jpa-changelog-1.5.0.xml"/>
<include file="META-INF/jpa-changelog-1.6.1.xml"/> <include file="META-INF/jpa-changelog-1.6.1.xml"/>
<include file="META-INF/jpa-changelog-1.7.0.xml"/>
</databaseChangeLog> </databaseChangeLog>

View file

@ -1,18 +1,5 @@
package org.keycloak.connections.jpa; package org.keycloak.connections.jpa;
import org.hibernate.ejb.AvailableSettings;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import javax.naming.InitialContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.sql.DataSource;
import java.sql.Connection; import java.sql.Connection;
import java.sql.DatabaseMetaData; import java.sql.DatabaseMetaData;
import java.sql.DriverManager; import java.sql.DriverManager;
@ -22,10 +9,25 @@ import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import javax.naming.InitialContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.sql.DataSource;
import org.hibernate.ejb.AvailableSettings;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class DefaultJpaConnectionProviderFactory implements JpaConnectionProviderFactory { public class DefaultJpaConnectionProviderFactory implements JpaConnectionProviderFactory, ServerInfoAwareProviderFactory {
private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class); private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class);

View file

@ -1,10 +1,10 @@
package org.keycloak.connections.jpa; package org.keycloak.connections.jpa;
import org.keycloak.provider.ServerInfoAwareProviderFactory; import org.keycloak.provider.ProviderFactory;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public interface JpaConnectionProviderFactory extends ServerInfoAwareProviderFactory<JpaConnectionProvider> { public interface JpaConnectionProviderFactory extends ProviderFactory<JpaConnectionProvider> {
} }

View file

@ -12,7 +12,7 @@ public interface JpaUpdaterProvider extends Provider {
public String FIRST_VERSION = "1.0.0.Final"; public String FIRST_VERSION = "1.0.0.Final";
public String LAST_VERSION = "1.6.1"; public String LAST_VERSION = "1.7.0";
public String getCurrentVersionSql(String defaultSchema); public String getCurrentVersionSql(String defaultSchema);

View file

@ -31,6 +31,10 @@
<class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class> <class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
<class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class> <class>org.keycloak.models.jpa.session.PersistentUserSessionEntity</class>
<class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class> <class>org.keycloak.models.jpa.session.PersistentClientSessionEntity</class>
<class>org.keycloak.models.jpa.entities.GroupEntity</class>
<class>org.keycloak.models.jpa.entities.GroupAttributeEntity</class>
<class>org.keycloak.models.jpa.entities.GroupRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.UserGroupMembershipEntity</class>
<!-- JpaAuditProviders --> <!-- JpaAuditProviders -->
<class>org.keycloak.events.jpa.EventEntity</class> <class>org.keycloak.events.jpa.EventEntity</class>

View file

@ -1,11 +1,12 @@
package org.keycloak.connections.mongo; package org.keycloak.connections.mongo;
import com.mongodb.DB; import java.lang.reflect.Method;
import com.mongodb.MongoClient; import java.net.UnknownHostException;
import com.mongodb.MongoClientOptions; import java.util.Collections;
import com.mongodb.MongoClientURI; import java.util.LinkedHashMap;
import com.mongodb.MongoCredential; import java.util.Map;
import com.mongodb.ServerAddress;
import javax.net.ssl.SSLSocketFactory;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
@ -15,24 +16,26 @@ import org.keycloak.connections.mongo.impl.context.TransactionMongoStoreInvocati
import org.keycloak.connections.mongo.updater.MongoUpdaterProvider; import org.keycloak.connections.mongo.updater.MongoUpdaterProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ServerInfoAwareProviderFactory;
import javax.net.ssl.SSLSocketFactory; import com.mongodb.DB;
import java.lang.reflect.Method; import com.mongodb.MongoClient;
import java.net.UnknownHostException; import com.mongodb.MongoClientOptions;
import java.util.Collections; import com.mongodb.MongoClientURI;
import java.util.LinkedHashMap; import com.mongodb.MongoCredential;
import java.util.Map; import com.mongodb.ServerAddress;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory { public class DefaultMongoConnectionFactoryProvider implements MongoConnectionProviderFactory, ServerInfoAwareProviderFactory {
// TODO Make it dynamic // TODO Make it dynamic
private String[] entities = new String[]{ private String[] entities = new String[]{
"org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity",
"org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity",

View file

@ -1,9 +1,9 @@
package org.keycloak.connections.mongo; package org.keycloak.connections.mongo;
import org.keycloak.provider.ServerInfoAwareProviderFactory; import org.keycloak.provider.ProviderFactory;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
public interface MongoConnectionProviderFactory extends ServerInfoAwareProviderFactory<MongoConnectionProvider> { public interface MongoConnectionProviderFactory extends ProviderFactory<MongoConnectionProvider> {
} }

View file

@ -17,7 +17,6 @@
<module>jpa-liquibase</module> <module>jpa-liquibase</module>
<module>infinispan</module> <module>infinispan</module>
<module>mongo</module> <module>mongo</module>
<module>file</module>
<module>mongo-update</module> <module>mongo-update</module>
<module>http-client</module> <module>http-client</module>
</modules> </modules>

View file

@ -0,0 +1,36 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessCreatePresentation {
private Integer expiration;
private Integer count;
public ClientInitialAccessCreatePresentation() {
}
public ClientInitialAccessCreatePresentation(Integer expiration, Integer count) {
this.expiration = expiration;
this.count = count;
}
public Integer getExpiration() {
return expiration;
}
public void setExpiration(Integer expiration) {
this.expiration = expiration;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
}

View file

@ -0,0 +1,67 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ClientInitialAccessPresentation {
private String id;
private String token;
private Integer timestamp;
private Integer expiration;
private Integer count;
private Integer remainingCount;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Integer getTimestamp() {
return timestamp;
}
public void setTimestamp(Integer timestamp) {
this.timestamp = timestamp;
}
public Integer getExpiration() {
return expiration;
}
public void setExpiration(Integer expiration) {
this.expiration = expiration;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getRemainingCount() {
return remainingCount;
}
public void setRemainingCount(Integer remainingCount) {
this.remainingCount = remainingCount;
}
}

View file

@ -19,6 +19,7 @@ public class ClientRepresentation {
protected Boolean enabled; protected Boolean enabled;
protected String clientAuthenticatorType; protected String clientAuthenticatorType;
protected String secret; protected String secret;
protected String registrationAccessToken;
protected String[] defaultRoles; protected String[] defaultRoles;
protected List<String> redirectUris; protected List<String> redirectUris;
protected List<String> webOrigins; protected List<String> webOrigins;
@ -124,6 +125,14 @@ public class ClientRepresentation {
this.secret = secret; this.secret = secret;
} }
public String getRegistrationAccessToken() {
return registrationAccessToken;
}
public void setRegistrationAccessToken(String registrationAccessToken) {
this.registrationAccessToken = registrationAccessToken;
}
public List<String> getRedirectUris() { public List<String> getRedirectUris() {
return redirectUris; return redirectUris;
} }
@ -251,4 +260,5 @@ public class ClientRepresentation {
public void setProtocolMappers(List<ProtocolMapperRepresentation> protocolMappers) { public void setProtocolMappers(List<ProtocolMapperRepresentation> protocolMappers) {
this.protocolMappers = protocolMappers; this.protocolMappers = protocolMappers;
} }
} }

View file

@ -0,0 +1,85 @@
package org.keycloak.representations.idm;
import org.codehaus.jackson.annotate.JsonIgnore;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupRepresentation {
protected String id;
protected String name;
protected String path;
protected Map<String, List<String>> attributes;
protected List<String> realmRoles;
protected Map<String, List<String>> clientRoles;
protected List<GroupRepresentation> subGroups;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public List<String> getRealmRoles() {
return realmRoles;
}
public void setRealmRoles(List<String> realmRoles) {
this.realmRoles = realmRoles;
}
public Map<String, List<String>> getClientRoles() {
return clientRoles;
}
public void setClientRoles(Map<String, List<String>> clientRoles) {
this.clientRoles = clientRoles;
}
public Map<String, List<String>> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
public GroupRepresentation singleAttribute(String name, String value) {
if (this.attributes == null) attributes = new HashMap<>();
attributes.put(name, Arrays.asList(value));
return this;
}
public List<GroupRepresentation> getSubGroups() {
return subGroups;
}
public void setSubGroups(List<GroupRepresentation> subGroups) {
this.subGroups = subGroups;
}
}

View file

@ -46,12 +46,14 @@ public class IdentityProviderRepresentation {
* @see #UPFLM_MISSING * @see #UPFLM_MISSING
* @see #UPFLM_OFF * @see #UPFLM_OFF
*/ */
@Deprecated
protected String updateProfileFirstLoginMode = UPFLM_ON; protected String updateProfileFirstLoginMode = UPFLM_ON;
protected boolean trustEmail; protected boolean trustEmail;
protected boolean storeToken; protected boolean storeToken;
protected boolean addReadTokenRoleOnCreate; protected boolean addReadTokenRoleOnCreate;
protected boolean authenticateByDefault; protected boolean authenticateByDefault;
protected String firstBrokerLoginFlowAlias;
protected Map<String, String> config = new HashMap<String, String>(); protected Map<String, String> config = new HashMap<String, String>();
public String getInternalId() { public String getInternalId() {
@ -106,15 +108,17 @@ public class IdentityProviderRepresentation {
} }
/** /**
* @return see {@link #updateProfileFirstLoginMode} * @deprecated deprecated and replaced by configuration on IdpReviewProfileAuthenticator
*/ */
@Deprecated
public String getUpdateProfileFirstLoginMode() { public String getUpdateProfileFirstLoginMode() {
return updateProfileFirstLoginMode; return updateProfileFirstLoginMode;
} }
/** /**
* @param updateProfileFirstLoginMode see {@link #updateProfileFirstLoginMode} * @deprecated deprecated and replaced by configuration on IdpReviewProfileAuthenticator
*/ */
@Deprecated
public void setUpdateProfileFirstLoginMode(String updateProfileFirstLoginMode) { public void setUpdateProfileFirstLoginMode(String updateProfileFirstLoginMode) {
this.updateProfileFirstLoginMode = updateProfileFirstLoginMode; this.updateProfileFirstLoginMode = updateProfileFirstLoginMode;
} }
@ -127,6 +131,14 @@ public class IdentityProviderRepresentation {
this.authenticateByDefault = authenticateByDefault; this.authenticateByDefault = authenticateByDefault;
} }
public String getFirstBrokerLoginFlowAlias() {
return firstBrokerLoginFlowAlias;
}
public void setFirstBrokerLoginFlowAlias(String firstBrokerLoginFlowAlias) {
this.firstBrokerLoginFlowAlias = firstBrokerLoginFlowAlias;
}
public boolean isStoreToken() { public boolean isStoreToken() {
return this.storeToken; return this.storeToken;
} }

View file

@ -47,7 +47,9 @@ public class RealmRepresentation {
protected String certificate; protected String certificate;
protected String codeSecret; protected String codeSecret;
protected RolesRepresentation roles; protected RolesRepresentation roles;
protected List<GroupRepresentation> groups;
protected List<String> defaultRoles; protected List<String> defaultRoles;
protected List<String> defaultGroups;
@Deprecated @Deprecated
protected Set<String> requiredCredentials; protected Set<String> requiredCredentials;
protected String passwordPolicy; protected String passwordPolicy;
@ -268,6 +270,14 @@ public class RealmRepresentation {
this.defaultRoles = defaultRoles; this.defaultRoles = defaultRoles;
} }
public List<String> getDefaultGroups() {
return defaultGroups;
}
public void setDefaultGroups(List<String> defaultGroups) {
this.defaultGroups = defaultGroups;
}
public String getPrivateKey() { public String getPrivateKey() {
return privateKey; return privateKey;
} }
@ -775,4 +785,12 @@ public class RealmRepresentation {
public void setClientAuthenticationFlow(String clientAuthenticationFlow) { public void setClientAuthenticationFlow(String clientAuthenticationFlow) {
this.clientAuthenticationFlow = clientAuthenticationFlow; this.clientAuthenticationFlow = clientAuthenticationFlow;
} }
public List<GroupRepresentation> getGroups() {
return groups;
}
public void setGroups(List<GroupRepresentation> groups) {
this.groups = groups;
}
} }

View file

@ -40,6 +40,8 @@ public class UserRepresentation {
@Deprecated @Deprecated
protected List<SocialLinkRepresentation> socialLinks; protected List<SocialLinkRepresentation> socialLinks;
protected List<String> groups;
public String getSelf() { public String getSelf() {
return self; return self;
} }
@ -216,4 +218,12 @@ public class UserRepresentation {
public void setServiceAccountClientId(String serviceAccountClientId) { public void setServiceAccountClientId(String serviceAccountClientId) {
this.serviceAccountClientId = serviceAccountClientId; this.serviceAccountClientId = serviceAccountClientId;
} }
public List<String> getGroups() {
return groups;
}
public void setGroups(List<String> groups) {
this.groups = groups;
}
} }

View file

@ -1,45 +0,0 @@
package org.keycloak.representations.idm;
import java.util.HashSet;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UserRoleMappingRepresentation {
protected String self; // link
protected String username;
protected Set<String> roles;
public String getSelf() {
return self;
}
public void setSelf(String self) {
this.self = self;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Set<String> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
public UserRoleMappingRepresentation role(String role) {
if (this.roles == null) this.roles = new HashSet<String>();
this.roles.add(role);
return this;
}
}

View file

@ -0,0 +1,223 @@
package org.keycloak.representations.oidc;
import org.codehaus.jackson.annotate.JsonAutoDetect;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
public class OIDCClientRepresentation {
private List<String> redirect_uris;
private String token_endpoint_auth_method;
private String grant_types;
private String response_types;
private String client_id;
private String client_secret;
private String client_name;
private String client_uri;
private String logo_uri;
private String scope;
private String contacts;
private String tos_uri;
private String policy_uri;
private String jwks_uri;
private String jwks;
private String software_id;
private String software_version;
private Integer client_id_issued_at;
private Integer client_secret_expires_at;
private String registration_client_uri;
private String registration_access_token;
public List<String> getRedirectUris() {
return redirect_uris;
}
public void setRedirectUris(List<String> redirectUris) {
this.redirect_uris = redirectUris;
}
public String getTokenEndpointAuthMethod() {
return token_endpoint_auth_method;
}
public void setTokenEndpointAuthMethod(String token_endpoint_auth_method) {
this.token_endpoint_auth_method = token_endpoint_auth_method;
}
public String getGrantTypes() {
return grant_types;
}
public void setGrantTypes(String grantTypes) {
this.grant_types = grantTypes;
}
public String getResponseTypes() {
return response_types;
}
public void setResponseTypes(String responseTypes) {
this.response_types = responseTypes;
}
public String getClientId() {
return client_id;
}
public void setClientId(String clientId) {
this.client_id = clientId;
}
public String getClientSecret() {
return client_secret;
}
public void setClientSecret(String clientSecret) {
this.client_secret = clientSecret;
}
public String getClientName() {
return client_name;
}
public void setClientName(String client_name) {
this.client_name = client_name;
}
public String getClientUri() {
return client_uri;
}
public void setClientUri(String client_uri) {
this.client_uri = client_uri;
}
public String getLogoUri() {
return logo_uri;
}
public void setLogoUri(String logo_uri) {
this.logo_uri = logo_uri;
}
public String getScope() {
return scope;
}
public void setScope(String scope) {
this.scope = scope;
}
public String getContacts() {
return contacts;
}
public void setContacts(String contacts) {
this.contacts = contacts;
}
public String getTosUri() {
return tos_uri;
}
public void setTosUri(String tos_uri) {
this.tos_uri = tos_uri;
}
public String getPolicyUri() {
return policy_uri;
}
public void setPolicyUri(String policy_uri) {
this.policy_uri = policy_uri;
}
public String getJwksUri() {
return jwks_uri;
}
public void setJwksUri(String jwks_uri) {
this.jwks_uri = jwks_uri;
}
public String getJwks() {
return jwks;
}
public void setJwks(String jwks) {
this.jwks = jwks;
}
public String getSoftwareId() {
return software_id;
}
public void setSoftwareId(String softwareId) {
this.software_id = softwareId;
}
public String getSoftwareVersion() {
return software_version;
}
public void setSoftwareVersion(String softwareVersion) {
this.software_version = softwareVersion;
}
public Integer getClientIdIssuedAt() {
return client_id_issued_at;
}
public void setClientIdIssuedAt(Integer clientIdIssuedAt) {
this.client_id_issued_at = clientIdIssuedAt;
}
public Integer getClientSecretExpiresAt() {
return client_secret_expires_at;
}
public void setClientSecretExpiresAt(Integer client_secret_expires_at) {
this.client_secret_expires_at = client_secret_expires_at;
}
public String getRegistrationClientUri() {
return registration_client_uri;
}
public void setRegistrationClientUri(String registrationClientUri) {
this.registration_client_uri = registrationClientUri;
}
public String getRegistrationAccessToken() {
return registration_access_token;
}
public void setRegistrationAccessToken(String registrationAccessToken) {
this.registration_access_token = registrationAccessToken;
}
}

View file

@ -19,6 +19,7 @@ public class TokenUtil {
public static final String TOKEN_TYPE_OFFLINE = "Offline"; public static final String TOKEN_TYPE_OFFLINE = "Offline";
public static boolean isOfflineTokenRequested(String scopeParam) { public static boolean isOfflineTokenRequested(String scopeParam) {
if (scopeParam == null) { if (scopeParam == null) {
return false; return false;

View file

@ -36,10 +36,6 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId> <artifactId>keycloak-model-jpa</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-file</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-model-sessions-infinispan</artifactId> <artifactId>keycloak-model-sessions-infinispan</artifactId>

View file

@ -9,7 +9,7 @@
<fileSets> <fileSets>
<fileSet> <fileSet>
<directory>../../target/site/apidocs</directory> <directory>target/site/apidocs</directory>
<outputDirectory>javadocs</outputDirectory> <outputDirectory>javadocs</outputDirectory>
</fileSet> </fileSet>
<fileSet> <fileSet>

View file

@ -1,11 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<style>
td { padding: 5px; }
</style>
</head>
<body>
<h1>Keyloak Documentation</h1> <h1>Keyloak Documentation</h1>
<ul> <table>
<li><a href="userguide/keycloak-server/html/index.html">Server and Keycloak Adapter Userguide HTML</a></li> <tr>
<li><a href="userguide/keycloak-server/html_single/index.html">Server and Keycloak Adapter Userguide HTML Single Page</a></li> <td>Server and Keycloak Adapter Userguide</td>
<li><a href="userguide/keycloak-server/pdf/keycloak-reference-guide-en-US.pdf">Server and Keycloak Adapter Userguide PDF</a></li> <td><a href="userguide/keycloak-server/html/index.html">HTML</a></td>
<li><a href="userguide/saml-client-adapter/html/index.html">SAML Client Adapter Userguide HTML</a></li> <td><a href="userguide/keycloak-server/html_single/index.html">HTML Single Page</a></td>
<li><a href="userguide/saml-client-adapter/html_single/index.html">>SAML Client Adapter Userguide HTML Single Page</a></li> <td><a href="userguide/keycloak-server/pdf/keycloak-reference-guide-en-US.pdf">PDF</a></td>
<li><a href="userguide/saml-client-adapter/pdf/keycloak-reference-guide-en-US.pdf">SAML Client Adapter Userguide PDF</a></li> </tr>
<li><a href="rest-api/overview-index.html">Admin REST API</a></li> <tr>
<li><a href="javadocs/index.html">Javadocs</a></li> <td>SAML Client Adapter Userguide</td>
</ul> <td><a href="userguide/saml-client-adapter/html/index.html">HTML</a></td>
<td><a href="userguide/saml-client-adapter/html_single/index.html">HTML Single Page</a></td>
<td><a href="userguide/saml-client-adapter/pdf/keycloak-saml-adapter-reference-guide-en-US.pdf">PDF</a></td>
</tr>
<tr>
<td>Admin REST API</td>
<td colspan="3"><a href="rest-api/index.html">HTML</a></td>
</tr>
<tr>
<td>Javadocs</td>
<td colspan="3"><a href="javadocs/index.html">HTML</a></td>
</tr>
</body>
</html>

View file

@ -66,4 +66,15 @@
</includes> </includes>
</fileSet> </fileSet>
</fileSets> </fileSets>
<files>
<file>
<source>src/main/resources/content/standalone/configuration/keycloak-server.json</source>
<outputDirectory>content/domain/servers/server-one/configuration</outputDirectory>
</file>
<file>
<source>src/main/resources/content/standalone/configuration/keycloak-server.json</source>
<outputDirectory>content/domain/servers/server-two/configuration</outputDirectory>
</file>
</files>
</assembly> </assembly>

View file

@ -10,7 +10,7 @@
<subsystem>ee.xml</subsystem> <subsystem>ee.xml</subsystem>
<subsystem>ejb3.xml</subsystem> <subsystem>ejb3.xml</subsystem>
<subsystem>io.xml</subsystem> <subsystem>io.xml</subsystem>
<subsystem>infinispan.xml</subsystem> <subsystem>keycloak-infinispan.xml</subsystem>
<subsystem>jaxrs.xml</subsystem> <subsystem>jaxrs.xml</subsystem>
<subsystem>jca.xml</subsystem> <subsystem>jca.xml</subsystem>
<subsystem>jdr.xml</subsystem> <subsystem>jdr.xml</subsystem>
@ -41,7 +41,7 @@
<subsystem>ee.xml</subsystem> <subsystem>ee.xml</subsystem>
<subsystem supplement="ha">ejb3.xml</subsystem> <subsystem supplement="ha">ejb3.xml</subsystem>
<subsystem>io.xml</subsystem> <subsystem>io.xml</subsystem>
<subsystem supplement="ha">infinispan.xml</subsystem> <subsystem supplement="ha">keycloak-infinispan.xml</subsystem>
<subsystem>jaxrs.xml</subsystem> <subsystem>jaxrs.xml</subsystem>
<subsystem>jca.xml</subsystem> <subsystem>jca.xml</subsystem>
<subsystem>jdr.xml</subsystem> <subsystem>jdr.xml</subsystem>
@ -74,7 +74,7 @@
<subsystem supplement="full">ee.xml</subsystem> <subsystem supplement="full">ee.xml</subsystem>
<subsystem supplement="full">ejb3.xml</subsystem> <subsystem supplement="full">ejb3.xml</subsystem>
<subsystem>io.xml</subsystem> <subsystem>io.xml</subsystem>
<subsystem>infinispan.xml</subsystem> <subsystem>keycloak-infinispan.xml</subsystem>
<subsystem>iiop-openjdk.xml</subsystem> <subsystem>iiop-openjdk.xml</subsystem>
<subsystem>jaxrs.xml</subsystem> <subsystem>jaxrs.xml</subsystem>
<subsystem>jca.xml</subsystem> <subsystem>jca.xml</subsystem>
@ -108,7 +108,7 @@
<subsystem supplement="full">ee.xml</subsystem> <subsystem supplement="full">ee.xml</subsystem>
<subsystem supplement="full-ha">ejb3.xml</subsystem> <subsystem supplement="full-ha">ejb3.xml</subsystem>
<subsystem>io.xml</subsystem> <subsystem>io.xml</subsystem>
<subsystem supplement="ha">infinispan.xml</subsystem> <subsystem supplement="ha">keycloak-infinispan.xml</subsystem>
<subsystem>iiop-openjdk.xml</subsystem> <subsystem>iiop-openjdk.xml</subsystem>
<subsystem>jaxrs.xml</subsystem> <subsystem>jaxrs.xml</subsystem>
<subsystem>jca.xml</subsystem> <subsystem>jca.xml</subsystem>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-connections-file">
<resources>
<artifact name="${org.keycloak:keycloak-connections-file}"/>
</resources>
<dependencies>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-export-import-api"/>
<module name="org.keycloak.keycloak-export-import-single-file"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>
</module>

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-model-file">
<resources>
<artifact name="${org.keycloak:keycloak-model-file}"/>
</resources>
<dependencies>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-connections-file"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>
</module>

View file

@ -8,7 +8,6 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-file" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/> <module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>
@ -33,7 +32,6 @@
<module name="org.keycloak.keycloak-model-api" services="import"/> <module name="org.keycloak.keycloak-model-api" services="import"/>
<module name="org.keycloak.keycloak-model-jpa" services="import"/> <module name="org.keycloak.keycloak-model-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-mongo" services="import"/> <module name="org.keycloak.keycloak-model-mongo" services="import"/>
<module name="org.keycloak.keycloak-model-file" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-infinispan" services="import"/> <module name="org.keycloak.keycloak-model-sessions-infinispan" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/> <module name="org.keycloak.keycloak-saml-protocol" services="import"/>
<module name="org.keycloak.keycloak-services" export="true" services="import"/> <module name="org.keycloak.keycloak-services" export="true" services="import"/>

View file

@ -18,7 +18,6 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-file" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/> <module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>
@ -43,7 +42,6 @@
<module name="org.keycloak.keycloak-model-api" services="import"/> <module name="org.keycloak.keycloak-model-api" services="import"/>
<module name="org.keycloak.keycloak-model-jpa" services="import"/> <module name="org.keycloak.keycloak-model-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-mongo" services="import"/> <module name="org.keycloak.keycloak-model-mongo" services="import"/>
<module name="org.keycloak.keycloak-model-file" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-infinispan" services="import"/> <module name="org.keycloak.keycloak-model-sessions-infinispan" services="import"/>
<module name="org.keycloak.keycloak-saml-core" services="import"/> <module name="org.keycloak.keycloak-saml-core" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/> <module name="org.keycloak.keycloak-saml-protocol" services="import"/>

View file

@ -173,10 +173,6 @@
<maven-resource group="org.keycloak" artifact="keycloak-connections-jpa-liquibase"/> <maven-resource group="org.keycloak" artifact="keycloak-connections-jpa-liquibase"/>
</module-def> </module-def>
<module-def name="org.keycloak.keycloak-connections-file">
<maven-resource group="org.keycloak" artifact="keycloak-connections-file"/>
</module-def>
<module-def name="org.keycloak.keycloak-connections-infinispan"> <module-def name="org.keycloak.keycloak-connections-infinispan">
<maven-resource group="org.keycloak" artifact="keycloak-connections-infinispan"/> <maven-resource group="org.keycloak" artifact="keycloak-connections-infinispan"/>
</module-def> </module-def>
@ -250,12 +246,6 @@
<maven-resource group="org.keycloak" artifact="keycloak-saml-protocol"/> <maven-resource group="org.keycloak" artifact="keycloak-saml-protocol"/>
</module-def> </module-def>
<!-- file -->
<module-def name="org.keycloak.keycloak-model-file">
<maven-resource group="org.keycloak" artifact="keycloak-model-file"/>
</module-def>
<!-- mongo --> <!-- mongo -->
<module-def name="org.keycloak.keycloak-connections-mongo"> <module-def name="org.keycloak.keycloak-connections-mongo">

View file

@ -8,7 +8,6 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-file" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/> <module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>
@ -33,7 +32,6 @@
<module name="org.keycloak.keycloak-model-api" services="import"/> <module name="org.keycloak.keycloak-model-api" services="import"/>
<module name="org.keycloak.keycloak-model-jpa" services="import"/> <module name="org.keycloak.keycloak-model-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-mongo" services="import"/> <module name="org.keycloak.keycloak-model-mongo" services="import"/>
<module name="org.keycloak.keycloak-model-file" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-infinispan" services="import"/> <module name="org.keycloak.keycloak-model-sessions-infinispan" services="import"/>
<module name="org.keycloak.keycloak-saml-protocol" services="import"/> <module name="org.keycloak.keycloak-saml-protocol" services="import"/>
<module name="org.keycloak.keycloak-services" export="true" services="import"/> <module name="org.keycloak.keycloak-services" export="true" services="import"/>

View file

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-connections-file">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-export-import-api"/>
<module name="org.keycloak.keycloak-export-import-single-file"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>
</module>

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-model-file">
<resources>
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-connections-file"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>
</module>

View file

@ -18,7 +18,6 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/> <module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/> <module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-file" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/> <module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>
@ -43,7 +42,6 @@
<module name="org.keycloak.keycloak-model-api" services="import"/> <module name="org.keycloak.keycloak-model-api" services="import"/>
<module name="org.keycloak.keycloak-model-jpa" services="import"/> <module name="org.keycloak.keycloak-model-jpa" services="import"/>
<module name="org.keycloak.keycloak-model-mongo" services="import"/> <module name="org.keycloak.keycloak-model-mongo" services="import"/>
<module name="org.keycloak.keycloak-model-file" services="import"/>
<module name="org.keycloak.keycloak-model-sessions-infinispan" services="import"/> <module name="org.keycloak.keycloak-model-sessions-infinispan" services="import"/>
<module name="org.keycloak.keycloak-saml-core" services="import"/> <module name="org.keycloak.keycloak-saml-core" services="import"/>

View file

@ -114,6 +114,10 @@
<name>picketlink.version</name> <name>picketlink.version</name>
<value>${picketlink.version}</value> <value>${picketlink.version}</value>
</injection> </injection>
<injection>
<name>wildfly.version</name>
<value>${wildfly.version}</value>
</injection>
</injections> </injections>
<options> <options>
<xmlTransformerType>saxon</xmlTransformerType> <xmlTransformerType>saxon</xmlTransformerType>

View file

@ -27,6 +27,7 @@
<!ENTITY Migration SYSTEM "modules/MigrationFromOlderVersions.xml"> <!ENTITY Migration SYSTEM "modules/MigrationFromOlderVersions.xml">
<!ENTITY Email SYSTEM "modules/email.xml"> <!ENTITY Email SYSTEM "modules/email.xml">
<!ENTITY Roles SYSTEM "modules/roles.xml"> <!ENTITY Roles SYSTEM "modules/roles.xml">
<!ENTITY Groups SYSTEM "modules/groups.xml">
<!ENTITY DirectAccess SYSTEM "modules/direct-access.xml"> <!ENTITY DirectAccess SYSTEM "modules/direct-access.xml">
<!ENTITY ServiceAccounts SYSTEM "modules/service-accounts.xml"> <!ENTITY ServiceAccounts SYSTEM "modules/service-accounts.xml">
<!ENTITY CORS SYSTEM "modules/cors.xml"> <!ENTITY CORS SYSTEM "modules/cors.xml">
@ -48,6 +49,7 @@
<!ENTITY Recaptcha SYSTEM "modules/recaptcha.xml"> <!ENTITY Recaptcha SYSTEM "modules/recaptcha.xml">
<!ENTITY AuthSPI SYSTEM "modules/auth-spi.xml"> <!ENTITY AuthSPI SYSTEM "modules/auth-spi.xml">
<!ENTITY FilterAdapter SYSTEM "modules/servlet-filter-adapter.xml"> <!ENTITY FilterAdapter SYSTEM "modules/servlet-filter-adapter.xml">
<!ENTITY ClientRegistration SYSTEM "modules/client-registration.xml">
]> ]>
<book> <book>
@ -116,6 +118,7 @@ This one is short
&MultiTenancy; &MultiTenancy;
&JAAS; &JAAS;
</chapter> </chapter>
&ClientRegistration;
&IdentityBroker; &IdentityBroker;
&Themes; &Themes;
@ -131,6 +134,7 @@ This one is short
</chapter> </chapter>
&AccessTypes; &AccessTypes;
&Roles; &Roles;
&Groups;
&DirectAccess; &DirectAccess;
&ServiceAccounts; &ServiceAccounts;
&CORS; &CORS;

View file

@ -79,16 +79,34 @@
<section> <section>
<title>Version specific migration</title> <title>Version specific migration</title>
<section>
<title>Migrating to 1.7.0.CR1</title>
<simplesect>
<title>Option 'Update Profile On First Login' moved from Identity provider to Review Profile authenticator</title>
<para>
In this version, we added <literal>First Broker Login</literal>, which allows you to specify what exactly should be done
when new user is logged through Identity provider (or Social provider), but there is no existing Keycloak user
yet linked to the social account. As part of this work, we added option <literal>First Login Flow</literal> to identity providers where
you can specify the flow and then you can configure this flow under <literal>Authentication</literal> tab in admin console.
</para>
<para>
We also removed the option <literal>Update Profile On First Login</literal> from the Identity provider settings and moved it
to the configuration of <literal>Review Profile</literal> authenticator. So once you specify which flow should be used for your
Identity provider (by default it's <literal>First Broker Login</literal> flow), you go to <literal>Authentication</literal> tab, select the flow
and then you configure the option under <literal>Review Profile</literal> authenticator.
</para>
</simplesect>
</section>
<section> <section>
<title>Migrating to 1.6.0.Final</title> <title>Migrating to 1.6.0.Final</title>
<simplesect> <simplesect>
<title>Refresh tokens are not reusable anymore</title> <title>Option that refresh tokens are not reusable anymore</title>
<para> <para>
Old versions of Keycloak allowed reusing refresh tokens multiple times. Keycloak no longer permits Old versions of Keycloak allowed reusing refresh tokens multiple times. Keycloak still permits this,
this by default. When a refresh token is used to obtain a new access token a new refresh token is also but also have an option <literal>Revoke refresh token</literal> to disallow it. Option is in in admin console under token settings.
included. This new refresh token should be used next time the access token is refreshed. If this is When a refresh token is used to obtain a new access token a new refresh token is also
a problem for you it's possible to enable reuse of refresh tokens in the admin console under token included. When option is enabled, then this new refresh token should be used next time the access token is refreshed.
settings. It won't be possible to reuse old refresh token multiple times.
</para> </para>
</simplesect> </simplesect>
<simplesect> <simplesect>

View file

@ -63,6 +63,9 @@
<listitem> <listitem>
<literal>manage-applications</literal> - Create, modify and delete applications in the realm <literal>manage-applications</literal> - Create, modify and delete applications in the realm
</listitem> </listitem>
<listitem>
<literal>create-clients</literal> - Create clients in the realm
</listitem>
<listitem> <listitem>
<literal>manage-clients</literal> - Create, modify and delete clients in the realm <literal>manage-clients</literal> - Create, modify and delete clients in the realm
</listitem> </listitem>

View file

@ -866,6 +866,14 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
</para> </para>
</section> </section>
<section>
<title>Modifying First Broker Login Flow</title>
<para>
First Broker Login flow is used during first login with some identity provider. Term <literal>First Login</literal> means that there is not yet existing Keycloak account
linked with the particular authenticated identity provider account. More details about this flow are in the <link linkend="identity-broker-first-login">Identity provider chapter</link>.
</para>
</section>
<section id="client_authentication"> <section id="client_authentication">
<title>Authentication of clients</title> <title>Authentication of clients</title>
<para>Keycloak actually supports pluggable authentication for <ulink url="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect</ulink> <para>Keycloak actually supports pluggable authentication for <ulink url="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect</ulink>

View file

@ -0,0 +1,215 @@
<chapter id="client-registration">
<title>Client Registration</title>
<para>
In order for an application or service to utilize Keycloak it has to register a client in Keycloak. An
admin can do this through the admin console (or admin REST endpoints), but clients can also register themselves
through Keycloak's client registration service.
</para>
<para>
The Client Registration Service provides built-in support for Keycloak Client Representations, OpenID Connect
Client Meta Data and SAML Entity Descriptors. It's also possible to plugin custom client registration providers
if required. The Client Registration Service endpoint is <literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/&lt;provider&gt;</literal>.
</para>
<para>
The built-in supported <literal>providers</literal> are:
<itemizedlist>
<listitem><literal>default</literal> Keycloak Representations</listitem>
<listitem><literal>install</literal> Keycloak Adapter Configuration</listitem>
<listitem><literal>openid-connect</literal> OpenID Connect Dynamic Client Registration</listitem>
<listitem><literal>saml2-entity-descriptor</literal> SAML Entity Descriptors</listitem>
</itemizedlist>
The following sections will describe how to use the different providers.
</para>
<section>
<title>Authentication</title>
<para>
To invoke the Client Registration Services you need a token. The token can be a standard bearer token, a
initial access token or a registration access token.
</para>
<section>
<title>Bearer Token</title>
<para>
The bearertoken can be issued on behalf of a user or a Service Account. The following permissions are required
to invoke the endpoints (see <link linkend='admin-permissions'>Admin Permissions</link> for more details):
<itemizedlist>
<listitem>
<literal>create-client</literal> or <literal>manage-client</literal> - To create clients
</listitem>
<listitem>
<literal>view-client</literal> or <literal>manage-client</literal> - To view clients
</listitem>
<listitem>
<literal>manage-client</literal> - To update or delete clients
</listitem>
</itemizedlist>
If you are using a regular bearer token to create clients we recommend using a token from on behalf of a
Service Account with only the <literal>create-client</literal> role. See the
<link linkend="service-accounts">Service Account</link> section for more details.
</para>
</section>
<section>
<title>Initial Access Token</title>
<para>
The best approach to create new clients is by using initial access tokens. An initial access token can
only be used to create clients and has a configurable expiration as well as a configurable limit on
how many clients can be created.
</para>
<para>
An initial access token can be created through the admin console. To create a new initial access token
first select the realm in the admin console, then click on <literal>Realm Settings</literal> in the menu
on the left, followed by <literal>Initial Access Tokens</literal> in the tabs displayed in the page.
</para>
<para>
You will now be able to see any existing initial access tokens. If you have access you can delete tokens
that are no longer required. You can only retrieve the value of the token when you are creating it. To
create a new token click on <literal>Create</literal>. You can now optionally add how long the token
should be valid, also how many clients can be created using the token. After you click on <literal>Save</literal>
the token value is displayed. It is important that you copy/paste this token now as you won't be able
to retrieve it later. If you forget to copy/paste it, then delete the token and create another one.
The token value is used as a standard bearer token when invoking the Client Registration Services, by
adding it to the Authorization header in the request. For example:
<programlisting><![CDATA[
Authorization: bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M....
]]></programlisting>
</para>
</section>
<section>
<title>Registration Access Token</title>
<para>
When you create a client through the Client Registration Service the response will include a registration
access token. The registration access token provides access to retrieve the client configuration later, but
also to update or delete the client. The registration access token is included with the request in the
same way as a bearer token or initial access token. Registration access tokens are only valid once
when it's used the response will include a new token.
</para>
<para>
If a client was created outside of the Client Registration Service it won't have a registration access
token associated with it. You can create one through the admin console. This can also be useful if
you loose the token for a particular client. To create a new token find the client in the admin console
and click on <literal>Credentials</literal>. Then click on <literal>Generate registration access token</literal>.
</para>
</section>
</section>
<section>
<title>Keycloak Representations</title>
<para>
The <literal>default</literal> client registration provider can be used to create, retrieve, update and delete a client. It uses
Keycloaks Client Representation format which provides support for configuring clients exactly as they can
be configured through the admin console, including for example configuring protocol mappers.
</para>
<para>
To create a client create a Client Representation (JSON) then do a HTTP POST to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/&lt;provider&gt;/default</literal>. It will return a Client Representation
that also includes the registration access token. You should save the registration access token somewhere
if you want to retrieve the config, update or delete the client later.
</para>
<para>
To retrieve the Client Representation then do a HTTP GET to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;clients/&lt;provider&gt;/default/&lt;client id&gt;</literal>. It will also
return a new registration access token.
</para>
<para>
To update the Client Representation then do a HTTP PUT to with the updated Client Representation to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/&lt;provider&gt;/default/&lt;client id&gt;</literal>. It will also
return a new registration access token.
</para>
<para>
To delete the Client Representation then do a HTTP DELETE to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/&lt;provider&gt;/default/&lt;client id&gt;</literal>
</para>
</section>
<section>
<title>Keycloak Adapter Configuration</title>
<para>
The <literal>installation</literal> client registration provider can be used to retrieve the adapter configuration
for a client. In addition to token authentication you can also authenticate with client credentials using
HTTP basic authentication. To do this include the following header in the request:
<programlisting><![CDATA[
Authorization: basic BASE64(client-id + ':' + client-secret)
]]></programlisting>
</para>
<para>
To retrieve the Adapter Configuration then do a HTTP GET to:
<literal>&lt;KEYCLOAK URL&gt;//realms/&lt;realm&gt;clients/&lt;provider&gt;/installation/&lt;client id&gt;</literal>
</para>
<para>
No authentication is required for public clients. This means that for the JavaScript adapter you can
load the client configuration directly from Keycloak using the above URL.
</para>
</section>
<section>
<title>OpenID Connect Dynamic Client Registration</title>
<para>
Keycloak implements <ulink url="https://openid.net/specs/openid-connect-registration-1_0.html">OpenID Connect Dynamic Client Registration</ulink>,
which extends <ulink url="https://tools.ietf.org/html/rfc7591">OAuth 2.0 Dynamic Client Registration Protocol</ulink> and
<ulink url="https://tools.ietf.org/html/rfc7592">OAuth 2.0 Dynamic Client Registration Management Protocol</ulink>.
</para>
<para>
The endpoint to use these specifications to register clients in Keycloak is:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/&lt;provider&gt;/oidc[/&lt;client id&gt;]</literal>.
</para>
<para>
This endpoints can also be found in the OpenID Connect Discovery endpoint for the realm:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/.well-known/openid-configuration</literal>.
</para>
</section>
<section>
<title>SAML Entity Descriptors</title>
<para>
The SAML Entity Descriptor endpoint only supports using SAML v2 Entity Descriptors to create clients. It
doesn't support retrieving, updating or deleting clients. For those operations the Keycloak representation
endpoints should be used. When creating a client a Keycloak Client Representation is returned with details
about the created client, including a registration access token.
</para>
<para>
To create a client do a HTTP POST with the SAML Entity Descriptor to:
<literal>&lt;KEYCLOAK URL&gt;/realms/&lt;realm&gt;/clients/&lt;provider&gt;/saml2-entity-descriptor</literal>.
</para>
</section>
<section>
<title>Client Registration Java API</title>
<para>
The Client Registration Java API makes it easy to use the Client Registration Service using Java. To use
include the dependency <literal>org.keycloak:keycloak-client-registration-api:&gt;VERSION&lt;</literal> from
Maven.
</para>
<para>
For full instructions on using the Client Registration refer to the JavaDocs. Below is an example of creating
a client:
<programlisting><![CDATA[
String initialAccessToken = "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJmMjJmNzQyYy04ZjNlLTQ2M....";
ClientRepresentation client = new ClientRepresentation();
client.setClientId(CLIENT_ID);
ClientRegistration reg = ClientRegistration.create().url("http://keycloak/auth/realms/myrealm").build();
reg.auth(initialAccessToken);
client = reg.create(client);
String registrationAccessToken = client.getRegistrationAccessToken();
]]></programlisting>
</para>
</section>
<!--
<section>
<title>Client Registration CLI</title>
<para>
TODO
</para>
</section>
-->
</chapter>

View file

@ -8,7 +8,7 @@
<para> <para>
<orderedlist> <orderedlist>
<listitem> <listitem>
Create a new theme within the <literal>themes/admin/mytheme</literal> directory in your distribution. Create a new theme within the <literal>themes/mytheme/admin</literal> directory in your distribution.
Where <literal>mytheme</literal> is whatever you want to name your theme. Where <literal>mytheme</literal> is whatever you want to name your theme.
</listitem> </listitem>
<listitem> <listitem>
@ -19,15 +19,15 @@ import=common/keycloak
]]></programlisting> ]]></programlisting>
</listitem> </listitem>
<listitem> <listitem>
Copy the file <literal>themes/admin/base/resources/partials/user-attribute-entry.html</literal> into the Copy the file <literal>themes/base/admin/resources/partials/user-attributes.html</literal> into the
a mirror directory in your theme: <literal>themes/admin/mytheme/resources/partials/user-attribute-entry.html</literal>. a mirror directory in your theme: <literal>themes/mytheme/admin/resources/partials/user-attributes.html</literal>.
What you are doing here is overriding the user attribute entry page in the admin console and putting in What you are doing here is overriding the user attribute entry page in the admin console and putting in
what attributes you want. This file already contains an example of entering address data. You can remove what attributes you want. This file already contains an example of entering address data. You can remove
this if you want and replace it with something else. Also, if you want to edit this file directly instead this if you want and replace it with something else. Also, if you want to edit this file directly instead
of creating a new theme, you can. of creating a new theme, you can.
</listitem> </listitem>
<listitem> <listitem>
In the <literal>user-attribute-entry.html</literal> file add your custom user attribute entry form item. For example In the <literal>user-attributes.html</literal> file add your custom user attribute entry form item. For example
<programlisting><![CDATA[ <div class="form-group clearfix block"> <programlisting><![CDATA[ <div class="form-group clearfix block">
<label class="col-sm-2 control-label" for="mobile">Mobile</label> <label class="col-sm-2 control-label" for="mobile">Mobile</label>
<div class="col-sm-6"> <div class="col-sm-6">
@ -52,7 +52,7 @@ import=common/keycloak
<para> <para>
<orderedlist> <orderedlist>
<listitem> <listitem>
Create a new theme within the <literal>themes/login/mytheme</literal> directory in your distribution. Create a new theme within the <literal>themes/mytheme/login</literal> directory in your distribution.
Where <literal>mytheme</literal> is whatever you want to name your theme. Where <literal>mytheme</literal> is whatever you want to name your theme.
</listitem> </listitem>
<listitem> <listitem>
@ -63,8 +63,8 @@ import=common/keycloak
styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/login.css ../patternfly/lib/zocial/zocial.css css/login.css]]></programlisting> styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/login.css ../patternfly/lib/zocial/zocial.css css/login.css]]></programlisting>
</listitem> </listitem>
<listitem> <listitem>
Copy the file <literal>themes/login/base/register.ftl</literal> into the Copy the file <literal>themes/base/login/register.ftl</literal> into the
a mirror directory in your theme: <literal>themes/login/mytheme/register.ftl</literal>. a mirror directory in your theme: <literal>themes/mytheme/login/register.ftl</literal>.
What you are doing here is overriding the registration page and adding What you are doing here is overriding the registration page and adding
what attributes you want. This file already contains an example of entering address data. You can remove what attributes you want. This file already contains an example of entering address data. You can remove
this if you want and replace it with something else. Also, if you want to edit this file directly instead this if you want and replace it with something else. Also, if you want to edit this file directly instead
@ -101,7 +101,7 @@ styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/login.
<para> <para>
<orderedlist> <orderedlist>
<listitem> <listitem>
Create a new theme within the <literal>themes/account/mytheme</literal> directory in your distribution. Create a new theme within the <literal>themes/mytheme/account</literal> directory in your distribution.
Where <literal>mytheme</literal> is whatever you want to name your theme. Where <literal>mytheme</literal> is whatever you want to name your theme.
</listitem> </listitem>
<listitem> <listitem>
@ -113,8 +113,8 @@ import=common/keycloak
styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/account.css css/account.css]]></programlisting> styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/account.css css/account.css]]></programlisting>
</listitem> </listitem>
<listitem> <listitem>
Copy the file <literal>themes/account/base/account.ftl</literal> into the Copy the file <literal>themes/base/account/account.ftl</literal> into the
a mirror directory in your theme: <literal>themes/account/mytheme/account.ftl</literal>. a mirror directory in your theme: <literal>themes/mytheme/account/account.ftl</literal>.
What you are doing here is overriding the profile page and adding What you are doing here is overriding the profile page and adding
what attributes you want to manage. This file already contains an example of entering address data. You can remove what attributes you want to manage. This file already contains an example of entering address data. You can remove
this if you want and replace it with something else. Also, if you want to edit this file directly instead this if you want and replace it with something else. Also, if you want to edit this file directly instead

View file

@ -0,0 +1,31 @@
<chapter id="groups">
<title>Groups</title>
<para>
Groups in Keycloak allow you to manage a common set of attributes and role mappings for a large set of users.
Users can be members of zero or more groups. Users inherit the attributes and role mappings assign to each group.
As an admin this makes it easy for you to manage permissions for a user in one place.
</para>
<para>
Groups are hierarchical. A group can have many subgroups, but a group can only have one parent. Subgroups inherit
the attributes and role mappings from the parent. This applies to user as well. So, if you have a parent group and a child group
and a user that only belongs to the child group, the user inherits the attributes and role mappings of both the
parent and child.
</para>
<section>
<title>Groups vs. Roles</title>
<para>
In the IT world the concepts of Group and Role are often blurred and interchangeable. In Keycloak, Groups are just
a collection of users that you can apply roles and attributes to in one place. Roles are used to assign permissions
and access control.
</para>
<para>
Keycloak Roles have the concept of a Composite Role. A role can be associated with one or more additional roles.
This is called a Composite Role. If a user has a role mapping to the Composite Role, they inherit all the roles associated
with the composite. So what's the difference from a Keycloak Group and a Composite Role? Logically they could be
used for the same exact thing. The difference is conceptual. Composite roles should be used to compose the
permission model of your set of services and applications. So, roles become a set of permissions. Groups on the
other hand, would be a set of users that have a set of permissions. Use Groups to manage users, composite roles to
manage applications and services.
</para>
</section>
</chapter>

View file

@ -66,7 +66,7 @@
</itemizedlist> </itemizedlist>
<section> <section id="identity-broker-overview">
<title>Overview</title> <title>Overview</title>
<para> <para>
@ -127,10 +127,11 @@
<listitem> <listitem>
<para> <para>
Now Keycloak is going to check if the response from the identity provider is valid. If valid, it will create an user Now Keycloak is going to check if the response from the identity provider is valid. If valid, it will create an user
or just skip that if the user already exists. If it is a new user, Keycloak will ask informations about the user to the identity provider or just skip that if the user already exists. If it is a new user, Keycloak may ask informations about the user to the identity provider
(or just read that from a security token) and create the user locally. This is what we call <emphasis>identity federation</emphasis>. (or just read that from a security token) and create the user locally. This is what we call <emphasis>identity federation</emphasis>.
If the user already exists Keycloak will ask him to link the identity returned from the identity provider If the user already exists Keycloak may ask him to link the identity returned from the identity provider
with his existing account. A process that we call <emphasis>account linking</emphasis>. with his existing account. A process that we call <emphasis>account linking</emphasis>. What exactly is done is configurable
and can be specified by setup of <link linkend="identity-broker-first-login">First Login Flow</link> .
At the end of this step, Keycloak authenticates the user and issues its own token in order to access At the end of this step, Keycloak authenticates the user and issues its own token in order to access
the requested resource in the service provider. the requested resource in the service provider.
</para> </para>
@ -210,7 +211,7 @@
<para> <para>
Social providers allows you to enable social authentication to your realm. Social providers allows you to enable social authentication to your realm.
Keycloak makes it easy to let users log in to your application using an existing account with a social network. Keycloak makes it easy to let users log in to your application using an existing account with a social network.
Currently Facebook, Google and Twitter are supported with more planned for the future. Currently Facebook, Google, Twitter, GitHub, LinkedIn and StackOverflow are supported with more planned for the future.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
@ -274,6 +275,15 @@
be used by any other means. be used by any other means.
</entry> </entry>
</row> </row>
<row>
<entry>
<literal>Authenticate By Default</literal>
</entry>
<entry>
If enabled, Keycloak will automatically redirect to this identity provider even before displaying login screen.
In other words, steps 3 and 4 from <link linkend="identity-broker-overview">the base flow</link> are skipped.
</entry>
</row>
<row> <row>
<entry> <entry>
<literal>Store Tokens</literal> <literal>Store Tokens</literal>
@ -293,20 +303,6 @@
to access any stored external tokens via the broker service. to access any stored external tokens via the broker service.
</entry> </entry>
</row> </row>
<row>
<entry>
<literal>Update Profile on First Login</literal>
</entry>
<entry>
Allows you to force users to update their profile right after the authentication finishes and
before the account is actually created in Keycloak. When "On", users will be always presented with the
<emphasis>update profile page</emphasis> asking for additional information in order to federate their identities.
When "On missing info", users will be presented with the <emphasis>update profile page</emphasis> only if some
mandatory information (email, first name, last name) is not provided by identity provider.
If "Off", the account will be created with the minimal information obtained from the identity provider
during the authentication process.
</entry>
</row>
<row> <row>
<entry> <entry>
<literal>Trust email</literal> <literal>Trust email</literal>
@ -326,6 +322,16 @@
You can put number into this field, providers with lower numbers are shown first. You can put number into this field, providers with lower numbers are shown first.
</entry> </entry>
</row> </row>
<row>
<entry>
<literal>First Login Flow</literal>
</entry>
<entry>
Alias of authentication flow, which is triggered during first login with this identity provider. Term <literal>First Login</literal>
means that there is not yet existing Keycloak account linked with the authenticated identity provider account.
More details in <link linkend="identity-broker-first-login">First Login section</link>.
</entry>
</row>
</tbody> </tbody>
</tgroup> </tgroup>
</table> </table>
@ -340,8 +346,8 @@
Forcing users to register to your realm when they want to access applications is hard. Forcing users to register to your realm when they want to access applications is hard.
So is trying to remember yet another username and password combination. So is trying to remember yet another username and password combination.
Social identity providers makes it easy for users to register on your realm and quickly sign in using a social network. Social identity providers makes it easy for users to register on your realm and quickly sign in using a social network.
Keycloak provides built-in support for the most common social networks out there, such as Google, Facebook, Twitter and Keycloak provides built-in support for the most common social networks out there, such as Google, Facebook, Twitter,
even Github. Github, LinkedId and StackOverflow.
</para> </para>
<section> <section>
@ -1211,7 +1217,13 @@ Authorization: Bearer {keycloak_access_token}]]></programlisting>
<section> <section>
<title>Automatically Select and Identity Provider</title> <title>Automatically Select and Identity Provider</title>
<para> <para>
Applications can automatically select an identity provider in order to authenticate an user. In this case, the user will not be presented to the login page but automatically redirected to the identity provider. Each Identity provider has option <literal>Authenticate By Default</literal>, which allows that Identity provider is automatically
selected during authentication. User won't even see Keycloak login page, but is automatically redirected to the identity provider.
</para>
<para>
Applications can also automatically select an identity provider in order to authenticate an user.
Selection per application is preferred over <literal>Authenticate By Default</literal> option if you need more control
on when exactly is Identity provider automatically selected.
</para> </para>
<para> <para>
Keycloak supports a specific HTTP query parameter that you can use as a hint to tell the server which identity provider should be used to authenticate the user. Keycloak supports a specific HTTP query parameter that you can use as a hint to tell the server which identity provider should be used to authenticate the user.
@ -1283,6 +1295,122 @@ keycloak.createLoginUrl({
</para> </para>
</section> </section>
<section id="identity-broker-first-login">
<title>First Login Flow</title>
<para>
When Keycloak successfully authenticates user through identity provider (step 8 in <link linkend="identity-broker-overview">Overview</link> chapter),
there can be two situations:
<orderedlist>
<listitem>
<para>
There is already Keycloak user account linked with the authenticated identity provider account. In this case,
Keycloak will just authenticate as the existing user and redirect back to application (step 9 in <link linkend="identity-broker-overview">Overview</link> chapter).
</para>
</listitem>
<listitem>
<para>
There is not yet existing Keycloak user account linked with the identity provider account. This situation is more tricky.
Usually you just want to register new account into Keycloak database, but what if there is existing Keycloak account with same email like the identity provider account?
Automatically link identity provider account with existing Keycloak account is not very good option as there are possible security flaws related to that...
</para>
</listitem>
</orderedlist>
</para>
<para>
Because we had various requirements what to do in second case, we changed the behaviour to be flexible and configurable
through <link linkend="auth_spi">Authentication Flows SPI</link>. In admin console in Identity provider settings, there is option
<literal>First Login Flow</literal>, which allows you to choose, which workflow will be used after "first login" with this identity provider account.
By default it points to <literal>first broker login</literal> flow, but you can configure and use your own flow and use different flows for different identity providers etc.
</para>
<para>
The flow itself is configured in admin console under <literal>Authentication</literal> tab. When you choose <literal>First Broker Login</literal> flow,
you will see what authenticators are used by default. You can either re-configure existing flow (For example disable some authenticators,
mark some of them as <literal>required</literal>, configure some authenticators etc). Or you can even create new authentication flow and/or
write your own Authenticator implementations and use it in your flow. See <link linkend="auth_spi">Authentication Flows SPI</link> for more details on how to do it.
</para>
<para>
For <literal>First Broker Login</literal> case, it might be useful if your Authenticator is subclass of <literal>org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator</literal>
so you have access to all details about authenticated Identity provider account. But it's not a requirement.
</para>
<section>
<title>Default First Login Flow</title>
<para>
Let's describe the default behaviour provided by <literal>First Broker Login</literal> flow. There are those authenticators:
<variablelist>
<varlistentry>
<term>Review Profile</term>
<listitem>
<para>
This authenticator might display the profile info page, where user can review his profile retrieved from identity provider.
The authenticator is configurable. You can set <literal>Update Profile On First Login</literal> option.
When <literal>On</literal>, users will be always presented with the profile page asking for additional information
in order to federate their identities. When <literal>missing</literal>, users will be presented with
the profile page only if some mandatory information (email, first name, last name) is not provided by identity provider.
If <literal>Off</literal>, the profile page won't be displayed, unless user clicks in later phase on <literal>Review profile info</literal>
link (page displayed in later phase by <literal>Confirm Link Existing Account</literal> authenticator)
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Create User If Unique</term>
<listitem>
<para>
This authenticator checks if there is already existing Keycloak account with same email or username like
the account from identity provider. If it's not, then authenticator just creates new Keyclok account and
link it with identity provider and whole flow is finished. Otherwise it goes to the next <literal>Handle Existing Account</literal> subflow.
If you always want to ensure that there is no duplicated account, you can mark this authenticator as <literal>REQUIRED</literal> .
In this case, the user will see the error page if there is existing Keycloak account and user needs
to link his identity provider account through Account management.
</para>
<para>
This authenticator also has config option <literal>Require Password Update After Registration</literal> .
When enabled, user is required to update password after account is created.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Confirm Link Existing Account</term>
<listitem>
<para>
User will see the info page, that there is existing Keycloak account with same email. He can either
review his profile again and use different email or username (flow is restarted and goes back to <literal>Review Profile</literal> authenticator).
Or he can confirm that he wants to link identity provider account with his existing Keycloak account.
Disable this authenticator if you don't want users to see this confirmation page, but go straight
to linking identity provider account by email verification or re-authentication.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Verify Existing Account By Email</term>
<listitem>
<para>
This authenticator is <literal>ALTERNATIVE</literal> by default, so it's used only if realm has SMTP setup configured.
It will send mail to user, where he can confirm that he wants to link identity provider with his Keycloak account.
Disable this if you don't want to confirm linking by email, but instead you always want users to reauthenticate with their password (and alternatively OTP).
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>Verify Existing Account By Re-authentication</term>
<listitem>
<para>
This authenticator is used if email authenticator is disabled or non-available (SMTP not configured for realm). It
will display login screen where user needs to authenticate with his password to link his Keycloak account with Identity provider.
User can also re-authenticate with some different identity provider, which is already linked to his keycloak account.
You can also force users to use OTP, otherwise it's optional and used only if OTP is already set for user account.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
</section>
<section> <section>
<title>Examples</title> <title>Examples</title>
<para> <para>

View file

@ -16,7 +16,7 @@
</para> </para>
<para> <para>
For example to implement the Event Listener SPI you start by implementing EventListenerProviderFactory: For example to implement the Event Listener SPI you start by implementing EventListenerProviderFactory:
<programlisting><![CDATA[{ <programlisting><![CDATA[
package org.acme.provider; package org.acme.provider;
import ... import ...
@ -43,7 +43,7 @@ public class MyEventListenerProviderFactory implements EventListenerProviderFact
} }
} }
}]]></programlisting> ]]></programlisting>
The example uses an imagined MaxList which has a maximum size and is concurrency safe. When the maximum size is reached The example uses an imagined MaxList which has a maximum size and is concurrency safe. When the maximum size is reached
and new entries are added the oldest entry is removed. Keycloak creates a single instance of and new entries are added the oldest entry is removed. Keycloak creates a single instance of
EventListenerProviderFactory which makes it possible to store state for multiple requests. EventListenerProvider EventListenerProviderFactory which makes it possible to store state for multiple requests. EventListenerProvider
@ -51,7 +51,7 @@ public class MyEventListenerProviderFactory implements EventListenerProviderFact
</para> </para>
<para> <para>
Next you would implement EventListenerProvider: Next you would implement EventListenerProvider:
<programlisting><![CDATA[{ <programlisting><![CDATA[
package org.acme.provider; package org.acme.provider;
import ... import ...
@ -75,13 +75,61 @@ public class MyEventListenerProvider implements EventListenerProvider {
} }
} }
}]]></programlisting> ]]></programlisting>
</para> </para>
<para> <para>
The file <literal>META-INF/services/org.keycloak.events.EventListenerProviderFactory</literal> should The file <literal>META-INF/services/org.keycloak.events.EventListenerProviderFactory</literal> should
contain the full name of your ProviderFactory implementation: contain the full name of your ProviderFactory implementation:
<programlisting><![CDATA[org.acme.provider.MyEventListenerProviderFactory]]></programlisting> <programlisting><![CDATA[org.acme.provider.MyEventListenerProviderFactory]]></programlisting>
</para> </para>
<section>
<title>Show info from you SPI implementation in Keycloak admin console</title>
<para>
Sometimes it is useful to show additional info about your Provider to a Keycloak administrator.
You can show provider build time informations (eg. version of custom provider currently installed),
current configuration of the provider (eg. url of remote system your provider talks to) or some operational
info (average time of response from remote system your provider talks to).
Keycloak admin console provides Server Info page to show this kind of information.
</para>
<para>
To show info from your provider it is enough to implement
<literal>org.keycloak.provider.ServerInfoAwareProviderFactory</literal> interface in your ProviderFactory.
Example implementation for MyEventListenerProviderFactory from previous example:
<programlisting><![CDATA[
package org.acme.provider;
import ...
public class MyEventListenerProviderFactory implements EventListenerProviderFactory, ServerInfoAwareProviderFactory {
private List<Event> events;
private int max;
...
@Override
public void init(Config.Scope config) {
max = config.getInt("max");
events = new MaxList(max);
}
...
@Override
public Map<String, String> getOperationalInfo() {
Map<String, String> ret = new LinkedHashMap<>();
ret.put("version", "1.0");
ret.put("listSizeMax", max + "");
ret.put("listSizeCurrent", events.size() + "");
return ret;
}
}
]]></programlisting>
</para>
</section>
</section> </section>
<section> <section>
@ -98,13 +146,13 @@ public class MyEventListenerProvider implements EventListenerProvider {
To register a provider using Modules first create a module. To do this you can either use the jboss-cli To register a provider using Modules first create a module. To do this you can either use the jboss-cli
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=target/event-listener-sysout-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-model-api,org.keycloak.keycloak-events-api" 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>
with the following content: with the following content:
<programlisting><![CDATA[{ <programlisting><![CDATA[
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.keycloak.examples.event-sysout"> <module xmlns="urn:jboss:module:1.1" name="org.keycloak.examples.event-sysout">
<resources> <resources>
@ -116,7 +164,7 @@ public class MyEventListenerProvider implements EventListenerProvider {
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
</dependencies> </dependencies>
</module> </module>
}]]></programlisting> ]]></programlisting>
</para> </para>
<para> <para>
Once you've created the module you need to register this module with Keycloak. This is done by editing Once you've created the module you need to register this module with Keycloak. This is done by editing

View file

@ -1,7 +1,7 @@
<chapter id="roles"> <chapter id="roles">
<title>Roles</title> <title>Roles</title>
<para> <para>
In Keycloak, roles (or permissions) can be defined globally at the realm level, or individually per application. In Keycloak, roles can be defined globally at the realm level, or individually per application.
Each role has a name which must be unique at the level it is defined in, i.e. you can have only one "admin" role at Each role has a name which must be unique at the level it is defined in, i.e. you can have only one "admin" role at
the realm level. You may have that a role named "admin" within an Application too, but "admin" must be unique the realm level. You may have that a role named "admin" within an Application too, but "admin" must be unique
for that application. for that application.

View file

@ -43,9 +43,9 @@
<section id="overlay_install"> <section id="overlay_install">
<title>Install on existing WildFly 9.0.1.Final</title> <title>Install on existing WildFly &wildfly.version;</title>
<para> <para>
Keycloak can be installed into an existing WildFly 9.0.0.Final server. To do this download Keycloak can be installed into an existing WildFly &wildfly.version; server. To do this download
<literal>keycloak-overlay-&project.version;.zip</literal> or <literal>keycloak-overlay-&project.version;.tar.gz</literal>. <literal>keycloak-overlay-&project.version;.zip</literal> or <literal>keycloak-overlay-&project.version;.tar.gz</literal>.
Once downloaded extract into the root directory of your WildFly installation. To start WildFly with Keycloak Once downloaded extract into the root directory of your WildFly installation. To start WildFly with Keycloak
run: run:
@ -62,11 +62,15 @@
<para> <para>
To add Keycloak to other sever configurations (standalone.xml, standalone-ha.xml, etc.) start the server with To add Keycloak to other sever configurations (standalone.xml, standalone-ha.xml, etc.) start the server with
the desired server-config. If you are running the server in standalone mode run: the desired server-config. If you are running the server in standalone mode run:
<programlisting>cd &lt;WILDFLY_HOME&gt;/bin <programlisting>
./jboss-cli.sh -c --file=keycloak-install.cli</programlisting> cd &lt;WILDFLY_HOME&gt;/bin
./jboss-cli.sh -c --file=keycloak-install.cli
</programlisting>
Or if you are running in clustering (HA) mode (by having used -c standalone-ha.xml) then run: Or if you are running in clustering (HA) mode (by having used -c standalone-ha.xml) then run:
<programlisting>cd &lt;WILDFLY_HOME&gt;/bin <programlisting>
./jboss-cli.sh -c --file=keycloak-install-ha.cli</programlisting> cd &lt;WILDFLY_HOME&gt;/bin
./jboss-cli.sh -c --file=keycloak-install-ha.cli
</programlisting>
You may see exceptions in the server log, but after restarting the server they should be gone. You may see exceptions in the server log, but after restarting the server they should be gone.
You can restart the server with: You can restart the server with:
<programlisting>&lt;WILDFLY_HOME&gt;/bin/jboss-cli.sh -c :reload</programlisting> <programlisting>&lt;WILDFLY_HOME&gt;/bin/jboss-cli.sh -c :reload</programlisting>
@ -75,7 +79,7 @@
<section> <section>
<title>Install on existing JBoss EAP 6.4.0.GA</title> <title>Install on existing JBoss EAP 6.4.0.GA</title>
<para> <para>
Same procedure as WildFly 9.0.1.Final, but download <literal>keycloak-overlay-eap6-&project.version;.zip</literal> or <literal>keycloak-overlay-eap6-&project.version;.tar.gz</literal>. Same procedure as WildFly &wildfly.version;, but download <literal>keycloak-overlay-eap6-&project.version;.zip</literal> or <literal>keycloak-overlay-eap6-&project.version;.tar.gz</literal>.
</para> </para>
</section> </section>
<section> <section>
@ -85,7 +89,7 @@
To install it first download <literal>keycloak-demo-&project.version;.zip</literal> or To install it first download <literal>keycloak-demo-&project.version;.zip</literal> or
<literal>keycloak-demo-&project.version;.tar.gz</literal>. Once downloaded extract it inside <literal>keycloak-demo-&project.version;.tar.gz</literal>. Once downloaded extract it inside
<literal>keycloak-demo-&project.version;</literal> you'll find <literal>keycloak</literal> which contains <literal>keycloak-demo-&project.version;</literal> you'll find <literal>keycloak</literal> which contains
a full WildFly 9.0.0.Final server with Keycloak Server and Adapters included. You'll also find <literal>docs</literal> a full WildFly &wildfly.version; server with Keycloak Server and Adapters included. You'll also find <literal>docs</literal>
and <literal>examples</literal> which contains everything you need to get started developing applications that use Keycloak. and <literal>examples</literal> which contains everything you need to get started developing applications that use Keycloak.
</para> </para>
<para> <para>
@ -437,12 +441,12 @@ All configuration options are optional. Default value for directory is <literal>
settings you can specify before boot time. This is configured in the settings you can specify before boot time. This is configured in the
<literal>standalone/configuration/keycloak-server.json</literal>. <literal>standalone/configuration/keycloak-server.json</literal>.
By default the setting is like this: By default the setting is like this:
<programlisting><![CDATA[ <programlisting><![CDATA[
"connectionsHttpClient": { "connectionsHttpClient": {
"default": { "default": {
"disable-trust-manager": true "disable-trust-manager": true
} }
}, },
]]></programlisting> ]]></programlisting>
Possible configuration options are: Possible configuration options are:
<variablelist> <variablelist>
@ -659,25 +663,25 @@ All configuration options are optional. Default value for directory is <literal>
to do with the <literal>keytool</literal> utility that comes with the Java jdk. to do with the <literal>keytool</literal> utility that comes with the Java jdk.
</para> </para>
<para> <para>
<programlisting> <programlisting>
$ keytool -genkey -alias localhost -keyalg RSA -keystore keycloak.jks -validity 10950 $ keytool -genkey -alias localhost -keyalg RSA -keystore keycloak.jks -validity 10950
Enter keystore password: secret Enter keystore password: secret
Re-enter new password: secret Re-enter new password: secret
What is your first and last name? What is your first and last name?
[Unknown]: localhost [Unknown]: localhost
What is the name of your organizational unit? What is the name of your organizational unit?
[Unknown]: Keycloak [Unknown]: Keycloak
What is the name of your organization? What is the name of your organization?
[Unknown]: Red Hat [Unknown]: Red Hat
What is the name of your City or Locality? What is the name of your City or Locality?
[Unknown]: Westford [Unknown]: Westford
What is the name of your State or Province? What is the name of your State or Province?
[Unknown]: MA [Unknown]: MA
What is the two-letter country code for this unit? What is the two-letter country code for this unit?
[Unknown]: US [Unknown]: US
Is CN=localhost, OU=Keycloak, O=Test, L=Westford, ST=MA, C=US correct? Is CN=localhost, OU=Keycloak, O=Test, L=Westford, ST=MA, C=US correct?
[no]: yes [no]: yes
</programlisting> </programlisting>
</para> </para>
<para> <para>
You should answer <literal>What is your first and last name ?</literal> question with You should answer <literal>What is your first and last name ?</literal> question with
@ -693,44 +697,44 @@ All configuration options are optional. Default value for directory is <literal>
</para> </para>
<para> <para>
The first thing to do is generate a Certificate Request: The first thing to do is generate a Certificate Request:
<programlisting> <programlisting>
$ keytool -certreq -alias yourdomain -keystore keycloak.jks > keycloak.careq $ keytool -certreq -alias yourdomain -keystore keycloak.jks > keycloak.careq
</programlisting> </programlisting>
</para> </para>
<para> <para>
Where <literal>yourdomain</literal> is a DNS name for which this certificate is generated for. Where <literal>yourdomain</literal> is a DNS name for which this certificate is generated for.
Keytool generates the request: Keytool generates the request:
<programlisting> <programlisting>
-----BEGIN NEW CERTIFICATE REQUEST----- -----BEGIN NEW CERTIFICATE REQUEST-----
MIIC2jCCAcICAQAwZTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1BMREwDwYDVQQHEwhXZXN0Zm9y MIIC2jCCAcICAQAwZTELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1BMREwDwYDVQQHEwhXZXN0Zm9y
ZDEQMA4GA1UEChMHUmVkIEhhdDEQMA4GA1UECxMHUmVkIEhhdDESMBAGA1UEAxMJbG9jYWxob3N0 ZDEQMA4GA1UEChMHUmVkIEhhdDEQMA4GA1UECxMHUmVkIEhhdDESMBAGA1UEAxMJbG9jYWxob3N0
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr7kck2TaavlEOGbcpi9c0rncY4HhdzmY MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr7kck2TaavlEOGbcpi9c0rncY4HhdzmY
Ax2nZfq1eZEaIPqI5aTxwQZzzLDK9qbeAd8Ji79HzSqnRDxNYaZu7mAYhFKHgixsolE3o5Yfzbw1 Ax2nZfq1eZEaIPqI5aTxwQZzzLDK9qbeAd8Ji79HzSqnRDxNYaZu7mAYhFKHgixsolE3o5Yfzbw1
29Rvy+eUVe+WZxv5oo9wolVVpdSINIMEL2LaFhtX/c1dqiqYVpfnvFshZQaIg2nL8juzZcBjj4as 29Rvy+eUVe+WZxv5oo9wolVVpdSINIMEL2LaFhtX/c1dqiqYVpfnvFshZQaIg2nL8juzZcBjj4as
H98gIS7khql/dkZKsw9NLvyxgJvp7PaXurX29fNf3ihG+oFrL22oFyV54BWWxXCKU/GPn61EGZGw H98gIS7khql/dkZKsw9NLvyxgJvp7PaXurX29fNf3ihG+oFrL22oFyV54BWWxXCKU/GPn61EGZGw
Ft2qSIGLdctpMD1aJR2bcnlhEjZKDksjQZoQ5YMXaAGkcYkG6QkgrocDE2YXDbi7GIdf9MegVJ35 Ft2qSIGLdctpMD1aJR2bcnlhEjZKDksjQZoQ5YMXaAGkcYkG6QkgrocDE2YXDbi7GIdf9MegVJ35
2DQMpwIDAQABoDAwLgYJKoZIhvcNAQkOMSEwHzAdBgNVHQ4EFgQUQwlZJBA+fjiDdiVzaO9vrE/i 2DQMpwIDAQABoDAwLgYJKoZIhvcNAQkOMSEwHzAdBgNVHQ4EFgQUQwlZJBA+fjiDdiVzaO9vrE/i
n2swDQYJKoZIhvcNAQELBQADggEBAC5FRvMkhal3q86tHPBYWBuTtmcSjs4qUm6V6f63frhveWHf n2swDQYJKoZIhvcNAQELBQADggEBAC5FRvMkhal3q86tHPBYWBuTtmcSjs4qUm6V6f63frhveWHf
PzRrI1xH272XUIeBk0gtzWo0nNZnf0mMCtUBbHhhDcG82xolikfqibZijoQZCiGiedVjHJFtniDQ PzRrI1xH272XUIeBk0gtzWo0nNZnf0mMCtUBbHhhDcG82xolikfqibZijoQZCiGiedVjHJFtniDQ
9bMDUOXEMQ7gHZg5q6mJfNG9MbMpQaUVEEFvfGEQQxbiFK7hRWU8S23/d80e8nExgQxdJWJ6vd0X 9bMDUOXEMQ7gHZg5q6mJfNG9MbMpQaUVEEFvfGEQQxbiFK7hRWU8S23/d80e8nExgQxdJWJ6vd0X
MzzFK6j4Dj55bJVuM7GFmfdNC52pNOD5vYe47Aqh8oajHX9XTycVtPXl45rrWAH33ftbrS8SrZ2S MzzFK6j4Dj55bJVuM7GFmfdNC52pNOD5vYe47Aqh8oajHX9XTycVtPXl45rrWAH33ftbrS8SrZ2S
vqIFQeuLL3BaHwpl3t7j2lMWcK1p80laAxEASib/fAwrRHpLHBXRcq6uALUOZl4Alt8= vqIFQeuLL3BaHwpl3t7j2lMWcK1p80laAxEASib/fAwrRHpLHBXRcq6uALUOZl4Alt8=
-----END NEW CERTIFICATE REQUEST----- -----END NEW CERTIFICATE REQUEST-----
</programlisting> </programlisting>
</para> </para>
<para> <para>
Send this ca request to your CA. The CA will issue you a signed certificate and send it to you. Send this ca request to your CA. The CA will issue you a signed certificate and send it to you.
Before you import your new cert, you must obtain and import the root certificate of the CA. Before you import your new cert, you must obtain and import the root certificate of the CA.
You can download the cert from CA (ie.: root.crt) and import as follows: You can download the cert from CA (ie.: root.crt) and import as follows:
<programlisting> <programlisting>
$ keytool -import -keystore keycloak.jks -file root.crt -alias root $ keytool -import -keystore keycloak.jks -file root.crt -alias root
</programlisting> </programlisting>
</para> </para>
<para> <para>
Last step is import your new CA generated certificate to your keystore: Last step is import your new CA generated certificate to your keystore:
<programlisting> <programlisting>
$ keytool -import -alias yourdomain -keystore keycloak.jks -file your-certificate.cer $ keytool -import -alias yourdomain -keystore keycloak.jks -file your-certificate.cer
</programlisting> </programlisting>
</para> </para>
</section> </section>
</section> </section>
@ -744,18 +748,19 @@ All configuration options are optional. Default value for directory is <literal>
</para> </para>
<para> <para>
To the <literal>security-realms</literal> element add: To the <literal>security-realms</literal> element add:
<programlisting><![CDATA[<security-realm name="UndertowRealm"> <programlisting><![CDATA[
<server-identities> <security-realm name="UndertowRealm">
<ssl> <server-identities>
<keystore path="keycloak.jks" relative-to="jboss.server.config.dir" keystore-password="secret" /> <ssl>
</ssl> <keystore path="keycloak.jks" relative-to="jboss.server.config.dir" keystore-password="secret" />
</server-identities> </ssl>
</security-realm>]]></programlisting> </server-identities>
</security-realm>
]]></programlisting>
</para> </para>
<para> <para>
Find the element <literal>&lt;server name="default-server"&gt;</literal> (it's a child element of <literal>&lt;subsystem xmlns="urn:jboss:domain:undertow:1.0"&gt;</literal>) and add: Find the element <literal>&lt;server name="default-server"&gt;</literal> (it's a child element of <literal>&lt;subsystem xmlns="urn:jboss:domain:undertow:1.0"&gt;</literal>) and add:
<programlisting><![CDATA[<https-listener name="https" socket-binding="https" security-realm="UndertowRealm"/> <programlisting><![CDATA[<https-listener name="https" socket-binding="https" security-realm="UndertowRealm"/>]]></programlisting>
]]></programlisting>
</para> </para>
<para> <para>
Check the <ulink url="https://docs.jboss.org/author/display/WFLY8/Undertow+(web)+subsystem+configuration">Wildfly Undertow</ulink> documentation for more information on fine tuning the socket connections. Check the <ulink url="https://docs.jboss.org/author/display/WFLY8/Undertow+(web)+subsystem+configuration">Wildfly Undertow</ulink> documentation for more information on fine tuning the socket connections.
@ -813,44 +818,20 @@ All configuration options are optional. Default value for directory is <literal>
</section> </section>
<section> <section>
<title>Adding Keycloak server in Domain Mode</title> <title>Keycloak server in Domain Mode</title>
<para> <para>
In domain mode, you start the server with the "domain" command instead of the "standalone" command. In this case, the Keycloak subsystem is 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 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. profile. The Keycloak subsystem is defined for all initial profiles.
</para> </para>
<para> <para>
To enable Keycloak for a server profile edit domain/configuration/domain.xml. To the <literal>extensions</literal> THe server is also added to server profiles. By default two servers are started
element add the Keycloak extension: in the main-server-group which uses the full profile.
<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="main-auth-server">
<enabled>true</enabled>
<web-context>auth</web-context>
</auth-server>
</subsystem>
]]></programlisting>
</para> </para>
<para> <para>
To configure the server copy <literal>standalone/configuration/keycloak-server.json</literal> to You need to make sure <literal>domain/servers/&lt;SERVER NAME&gt;/configuration</literal> is identical
<literal>domain/servers/&lt;SERVER NAME&gt;/configuration</literal>. The configuration should be identical
for all servers in a group. for all servers in a group.
</para> </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> <para>
To deploy custom providers and themes you should deploys these as modules and make sure the modules are 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 available to all servers in the group. See <link linkend='providers'>Providers</link> and
@ -865,12 +846,12 @@ All configuration options are optional. Default value for directory is <literal>
</para> </para>
<para> <para>
To do this, add the <literal>default-web-module</literal> attribute in the Undertow subystem in standalone.xml. To do this, add the <literal>default-web-module</literal> attribute in the Undertow subystem in standalone.xml.
<programlisting><![CDATA[ <programlisting><![CDATA[
<subsystem xmlns="urn:jboss:domain:undertow:2.0"> <subsystem xmlns="urn:jboss:domain:undertow:2.0">
<server name="default-server"> <server name="default-server">
<host name="default-host" alias="localhost" default-web-module="keycloak-server.war"> <host name="default-host" alias="localhost" default-web-module="keycloak-server.war">
<location name="/" handler="welcome-content"/> <location name="/" handler="welcome-content"/>
</host> </host>
]]></programlisting> ]]></programlisting>
</para> </para>
<para> <para>

View file

@ -44,8 +44,7 @@ public interface Errors {
String NOT_ALLOWED = "not_allowed"; String NOT_ALLOWED = "not_allowed";
String FEDERATED_IDENTITY_EMAIL_EXISTS = "federated_identity_email_exists"; String FEDERATED_IDENTITY_EXISTS = "federated_identity_account_exists";
String FEDERATED_IDENTITY_USERNAME_EXISTS = "federated_identity_username_exists";
String SSL_REQUIRED = "ssl_required"; String SSL_REQUIRED = "ssl_required";
String USER_SESSION_NOT_FOUND = "user_session_not_found"; String USER_SESSION_NOT_FOUND = "user_session_not_found";
@ -53,4 +52,5 @@ public interface Errors {
String EMAIL_SEND_FAILED = "email_send_failed"; String EMAIL_SEND_FAILED = "email_send_failed";
String INVALID_EMAIL = "invalid_email"; String INVALID_EMAIL = "invalid_email";
String IDENTITY_PROVIDER_LOGIN_FAILURE = "identity_provider_login_failure"; String IDENTITY_PROVIDER_LOGIN_FAILURE = "identity_provider_login_failure";
String IDENTITY_PROVIDER_ERROR = "identity_provider_error";
} }

View file

@ -48,6 +48,8 @@ public enum EventType {
SEND_VERIFY_EMAIL_ERROR(true), SEND_VERIFY_EMAIL_ERROR(true),
SEND_RESET_PASSWORD(true), SEND_RESET_PASSWORD(true),
SEND_RESET_PASSWORD_ERROR(true), SEND_RESET_PASSWORD_ERROR(true),
SEND_IDENTITY_PROVIDER_LINK(true),
SEND_IDENTITY_PROVIDER_LINK_ERROR(true),
RESET_PASSWORD(true), RESET_PASSWORD(true),
RESET_PASSWORD_ERROR(true), RESET_PASSWORD_ERROR(true),
@ -60,12 +62,12 @@ public enum EventType {
IDENTITY_PROVIDER_LOGIN(false), IDENTITY_PROVIDER_LOGIN(false),
IDENTITY_PROVIDER_LOGIN_ERROR(false), IDENTITY_PROVIDER_LOGIN_ERROR(false),
IDENTITY_PROVIDER_FIRST_LOGIN(true),
IDENTITY_PROVIDER_FIRST_LOGIN_ERROR(true),
IDENTITY_PROVIDER_RESPONSE(false), IDENTITY_PROVIDER_RESPONSE(false),
IDENTITY_PROVIDER_RESPONSE_ERROR(false), IDENTITY_PROVIDER_RESPONSE_ERROR(false),
IDENTITY_PROVIDER_RETRIEVE_TOKEN(false), IDENTITY_PROVIDER_RETRIEVE_TOKEN(false),
IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR(false), IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR(false),
IDENTITY_PROVIDER_ACCCOUNT_LINKING(false),
IDENTITY_PROVIDER_ACCCOUNT_LINKING_ERROR(false),
IMPERSONATE(true), IMPERSONATE(true),
CUSTOM_REQUIRED_ACTION(true), CUSTOM_REQUIRED_ACTION(true),
CUSTOM_REQUIRED_ACTION_ERROR(true), CUSTOM_REQUIRED_ACTION_ERROR(true),

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>js-console</module-name>
</web-app>

View file

@ -1,6 +1,7 @@
package org.keycloak.examples.federation.properties; package org.keycloak.examples.federation.properties;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; 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;
@ -106,6 +107,12 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
// complete we dont'care if a role is removed
}
/** /**
* See if the user is still in the properties file * See if the user is still in the properties file
* *

View file

@ -63,4 +63,6 @@ public class ClasspathPropertiesFederationProvider extends BasePropertiesFederat
throw new IllegalStateException("Remove not supported"); throw new IllegalStateException("Remove not supported");
} }
} }

View file

@ -7,6 +7,7 @@ import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig; import org.codehaus.jackson.map.SerializationConfig;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleContainerModel;
@ -15,6 +16,7 @@ import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
@ -294,6 +296,12 @@ public class ExportUtils {
} }
} }
List<String> groups = new LinkedList<>();
for (GroupModel group : user.getGroups()) {
groups.add(ModelToRepresentation.buildGroupPath(group));
}
userRep.setGroups(groups);
return userRep; return userRep;
} }

View file

@ -12,6 +12,7 @@ import org.jboss.logging.Logger;
import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator;
import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; 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;
@ -105,12 +106,17 @@ public class KerberosFederationProvider implements UserFederationProvider {
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
}
@Override @Override
public boolean isValid(RealmModel realm, UserModel local) { public boolean isValid(RealmModel realm, UserModel local) {
// KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now // KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now
String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm(); String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm();
return kerberosPrincipal.equals(local.getFirstAttribute(KERBEROS_PRINCIPAL)); return kerberosPrincipal.equalsIgnoreCase(local.getFirstAttribute(KERBEROS_PRINCIPAL));
} }
@Override @Override

View file

@ -17,6 +17,7 @@ import javax.security.auth.login.LoginException;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.CommonKerberosConfig;
import org.keycloak.models.ModelException;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -54,6 +55,8 @@ public class KerberosUsernamePasswordAuthenticator {
String message = le.getMessage(); String message = le.getMessage();
logger.debug("Message from kerberos: " + message); logger.debug("Message from kerberos: " + message);
checkKerberosServerAvailable(le);
// Bit cumbersome, but seems to work with tested kerberos servers // Bit cumbersome, but seems to work with tested kerberos servers
boolean exists = (!message.contains("Client not found")); boolean exists = (!message.contains("Client not found"));
return exists; return exists;
@ -74,11 +77,19 @@ public class KerberosUsernamePasswordAuthenticator {
logoutSubject(); logoutSubject();
return true; return true;
} catch (LoginException le) { } catch (LoginException le) {
checkKerberosServerAvailable(le);
logger.debug("Failed to authenticate user " + username, le); logger.debug("Failed to authenticate user " + username, le);
return false; return false;
} }
} }
protected void checkKerberosServerAvailable(LoginException le) {
if (le.getMessage().contains("Port Unreachable")) {
throw new ModelException("Kerberos unreachable", le);
}
}
/** /**
* Returns true if user was successfully authenticated against Kerberos * Returns true if user was successfully authenticated against Kerberos

View file

@ -12,6 +12,7 @@ import org.keycloak.federation.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.federation.ldap.kerberos.LDAPProviderKerberosConfig;
import org.keycloak.federation.ldap.mappers.LDAPFederationMapper; import org.keycloak.federation.ldap.mappers.LDAPFederationMapper;
import org.keycloak.models.CredentialValidationOutput; import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants; import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
@ -319,6 +320,11 @@ public class LDAPFederationProvider implements UserFederationProvider {
// TODO: Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper? // TODO: Maybe mappers callback to ensure role deletion propagated to LDAP by RoleLDAPFederationMapper?
} }
@Override
public void preRemove(RealmModel realm, GroupModel group) {
}
public boolean validPassword(RealmModel realm, UserModel user, String password) { public boolean validPassword(RealmModel realm, UserModel user, String password) {
if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) { if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) {
// Use Kerberos JAAS (Krb5LoginModule) // Use Kerberos JAAS (Krb5LoginModule)

View file

@ -153,6 +153,10 @@ public class ExtendingThemeManager implements ThemeProvider {
private List<Theme> themes; private List<Theme> themes;
private Properties properties;
private ConcurrentHashMap<String, ConcurrentHashMap<Locale, Properties>> messages = new ConcurrentHashMap<>();
public ExtendingTheme(List<Theme> themes) { public ExtendingTheme(List<Theme> themes) {
this.themes = themes; this.themes = themes;
} }
@ -229,28 +233,41 @@ public class ExtendingThemeManager implements ThemeProvider {
@Override @Override
public Properties getMessages(String baseBundlename, Locale locale) throws IOException { public Properties getMessages(String baseBundlename, Locale locale) throws IOException {
Properties messages = new Properties(); if (messages.get(baseBundlename) == null || messages.get(baseBundlename).get(locale) == null) {
ListIterator<Theme> itr = themes.listIterator(themes.size()); Properties messages = new Properties();
while (itr.hasPrevious()) { ListIterator<Theme> itr = themes.listIterator(themes.size());
Properties m = itr.previous().getMessages(baseBundlename, locale); while (itr.hasPrevious()) {
if (m != null) { Properties m = itr.previous().getMessages(baseBundlename, locale);
messages.putAll(m); if (m != null) {
messages.putAll(m);
}
} }
this.messages.putIfAbsent(baseBundlename, new ConcurrentHashMap<Locale, Properties>());
this.messages.get(baseBundlename).putIfAbsent(locale, messages);
return messages;
} else {
return messages.get(baseBundlename).get(locale);
} }
return messages;
} }
@Override @Override
public Properties getProperties() throws IOException { public Properties getProperties() throws IOException {
Properties properties = new Properties(); if (properties == null) {
ListIterator<Theme> itr = themes.listIterator(themes.size()); Properties properties = new Properties();
while (itr.hasPrevious()) { ListIterator<Theme> itr = themes.listIterator(themes.size());
Properties p = itr.previous().getProperties(); while (itr.hasPrevious()) {
if (p != null) { Properties p = itr.previous().getProperties();
properties.putAll(p); if (p != null) {
properties.putAll(p);
}
} }
this.properties = properties;
return properties;
} else {
return properties;
} }
return properties;
} }
} }

View file

@ -126,4 +126,4 @@ locale_de=Deutsch
locale_en=English locale_en=English
locale_it=Italian locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil) locale_pt-BR=Portugu\u00EAs (Brasil)
locale_fr=Français locale_fr=Fran\u00e7ais

View file

@ -133,6 +133,7 @@ federatedIdentityLinkNotActiveMessage=This identity is not active anymore.
federatedIdentityRemovingLastProviderMessage=You can''t remove last federated identity as you don''t have password. federatedIdentityRemovingLastProviderMessage=You can''t remove last federated identity as you don''t have password.
identityProviderRedirectErrorMessage=Failed to redirect to identity provider. identityProviderRedirectErrorMessage=Failed to redirect to identity provider.
identityProviderRemovedMessage=Identity provider removed successfully. identityProviderRemovedMessage=Identity provider removed successfully.
identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user.
accountDisabledMessage=Account is disabled, contact admin. accountDisabledMessage=Account is disabled, contact admin.
@ -150,4 +151,4 @@ locale_de=German
locale_en=English locale_en=English
locale_it=Italian locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil) locale_pt-BR=Portugu\u00EAs (Brasil)
locale_fr=Français locale_fr=Fran\u00e7ais

View file

@ -0,0 +1,154 @@
doSave=Guardar
doCancel=Cancelar
doLogOutAllSessions=Desconectar de todas las sesiones
doRemove=Eliminar
doAdd=A\u00F1adir
doSignOut=Desconectar
editAccountHtmlTtile=Editar cuenta
federatedIdentitiesHtmlTitle=Identidades federadas
accountLogHtmlTitle=Registro de la cuenta
changePasswordHtmlTitle=Cambiar contrase\u00F1a
sessionsHtmlTitle=Sesiones
accountManagementTitle=Gesti\u00F3n de Cuenta Keycloak
authenticatorTitle=Autenticador
applicationsHtmlTitle=Aplicaciones
authenticatorCode=C\u00F3digo de un solo uso
email=Email
firstName=Nombre
givenName=Nombre de pila
fullName=Nombre completo
lastName=Apellidos
familyName=Apellido
password=Contrase\u00F1a
passwordConfirm=Confirma la contrase\u00F1a
passwordNew=Nueva contrase\u00F1a
username=Usuario
address=Direcci\u00F3n
street=Calle
locality=Ciudad o Municipio
region=Estado, Provincia, o Regi\u00F3n
postal_code=C\u00F3digo Postal
country=Pa\u00EDs
emailVerified=Email verificado
gssDelegationCredential=GSS Delegation Credential
role_admin=Administrador
role_realm-admin=Administrador del dominio
role_create-realm=Crear dominio
role_view-realm=Ver dominio
role_view-users=Ver usuarios
role_view-applications=Ver aplicaciones
role_view-clients=Ver clientes
role_view-events=Ver eventos
role_view-identity-providers=Ver proveedores de identidad
role_manage-realm=Gestionar dominio
role_manage-users=Gestionar usuarios
role_manage-applications=Gestionar aplicaciones
role_manage-identity-providers=Gestionar proveedores de identidad
role_manage-clients=Gestionar clientes
role_manage-events=Gestionar eventos
role_view-profile=Ver perfil
role_manage-account=Gestionar cuenta
role_read-token=Leer token
role_offline-access=Acceso sin conexi\u00F3n
client_account=Cuenta
client_security-admin-console=Consola de Administraci\u00F3n de Seguridad
client_realm-management=Gesti\u00F3n de dominio
client_broker=Broker
requiredFields=Campos obligatorios
allFieldsRequired=Todos los campos obligatorios
backToApplication=&laquo; Volver a la aplicaci\u00F3n
backTo=Volver a {0}
date=Fecha
event=Evento
ip=IP
client=Cliente
clients=Clientes
details=Detalles
started=Iniciado
lastAccess=\u00DAltimo acceso
expires=Expira
applications=Aplicaciones
account=Cuenta
federatedIdentity=Identidad federada
authenticator=Autenticador
sessions=Sesiones
log=Regisro
application=Aplicaci\u00F3n
availablePermissions=Permisos disponibles
grantedPermissions=Permisos concedidos
grantedPersonalInfo=Informaci\u00F3n personal concedida
additionalGrants=Permisos adicionales
action=Acci\u00F3n
inResource=en
fullAccess=Acceso total
offlineToken=C\u00F3digo de autorizaci\u00F3n offline
revoke=Revocar permiso
configureAuthenticators=Autenticadores configurados
mobile=M\u00F3vil
totpStep1=Instala <a href=\"https://fedorahosted.org/freeotp/\" target=\"_blank\">FreeOTP</a> o Google Authenticator en tu tel\u00E9fono m\u00F3vil. Ambas aplicaciones est\u00E1n disponibles en <a href=\"https://play.google.com\">Google Play</a> y en la App Store de Apple.
totpStep2=Abre la aplicacvi\u00F3n y escanea el c\u00F3digo o introduce la clave.
totpStep3=Introduce el c\u00F3digo \u00FAnico que te muestra la aplicaci\u00F3n de autenticaci\u00F3n y haz clic en Enviar para finalizar la configuraci\u00F3n
missingUsernameMessage=Por favor indica tu usuario.
missingFirstNameMessage=Por favor indica el nombre.
invalidEmailMessage=Email no v\u00E1lido
missingLastNameMessage=Por favor indica tus apellidos.
missingEmailMessage=Por favor indica el email.
missingPasswordMessage=Por favor indica tu contrase\u00F1a.
notMatchPasswordMessage=Las contrase\u00F1as no coinciden.
missingTotpMessage=Por favor indica tu c\u00F3digo de autenticaci\u00F3n
invalidPasswordExistingMessage=La contrase\u00F1a actual no es correcta.
invalidPasswordConfirmMessage=La confirmaci\u00F3n de contrase\u00F1a no coincide.
invalidTotpMessage=El c\u00F3digo de autenticaci\u00F3n no es v\u00E1lido.
usernameExistsMessage=El usuario ya existe
emailExistsMessage=El email ya existe
readOnlyUserMessage=No puedes actualizar tu usuario porque tu cuenta es de solo lectura.
readOnlyPasswordMessage=No puedes actualizar tu contrase\u00F1a porque tu cuenta es de solo lectura.
successTotpMessage=Aplicaci\u00F3n de autenticaci\u00F3n m\u00F3vil configurada.
successTotpRemovedMessage=Aplicaci\u00F3n de autenticaci\u00F3n m\u00F3vil eliminada.
successGrantRevokedMessage=Permiso revocado correctamente
accountUpdatedMessage=Tu cuenta se ha actualizado.
accountPasswordUpdatedMessage=Tu contrase\u00F1a se ha actualizado.
missingIdentityProviderMessage=Proveedor de identidad no indicado.
invalidFederatedIdentityActionMessage=Acci\u00F3n no v\u00E1lida o no indicada.
identityProviderNotFoundMessage=No se encontr\u00F3 un proveedor de identidad.
federatedIdentityLinkNotActiveMessage=Esta identidad ya no est\u00E1 activa
federatedIdentityRemovingLastProviderMessage=No puedes eliminar la \u00FAltima identidad federada porque no tienes fijada una contrase\u00F1a.
identityProviderRedirectErrorMessage=Error en la redirecci\u00F3n al proveedor de identidad
identityProviderRemovedMessage=Proveedor de identidad borrado correctamente.
accountDisabledMessage=La cuenta est\u00E1 desactivada, contacta con el administrador.
accountTemporarilyDisabledMessage=La cuenta est\u00E1 temporalmente desactivada, contacta con el administrador o int\u00E9ntalo de nuevo m\u00E1s tarde.
invalidPasswordMinLengthMessage=Contrase\u00F1a incorrecta: longitud m\u00EDnima {0}.
invalidPasswordMinLowerCaseCharsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} letras min\u00FAsculas.
invalidPasswordMinDigitsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} caracteres num\u00E9ricos.
invalidPasswordMinUpperCaseCharsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} letras may\u00FAsculas.
invalidPasswordMinSpecialCharsMessage=Contrase\u00F1a incorrecta: debe contener al menos {0} caracteres especiales.
invalidPasswordNotUsernameMessage=Contrase\u00F1a incorrecta: no puede ser igual al nombre de usuario.
invalidPasswordRegexPatternMessage=Contrase\u00F1a incorrecta: no cumple la expresi\u00F3n regular.
invalidPasswordHistoryMessage=Contrase\u00F1a incorrecta: no puede ser igual a ninguna de las \u00FAltimas {0} contrase\u00F1as.
locale_de=German
locale_en=English
locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil)
locale_fr=Fran\u00E7ais
locale_es=Espa\u00F1ol

View file

@ -10,7 +10,7 @@ doSignOut=D\u00e9connexion
editAccountHtmlTtile=Edition du compte editAccountHtmlTtile=Edition du compte
federatedIdentitiesHtmlTitle=Identit\u00e9s f\u00e9d\u00e9r\u00e9es federatedIdentitiesHtmlTitle=Identit\u00e9s f\u00e9d\u00e9r\u00e9es
accountLogHtmlTitle=Acces au compte accountLogHtmlTitle=Acc\u00e8s au compte
changePasswordHtmlTitle=Changer de mot de passe changePasswordHtmlTitle=Changer de mot de passe
sessionsHtmlTitle=Sessions sessionsHtmlTitle=Sessions
accountManagementTitle=Gestion de Compte Keycloak accountManagementTitle=Gestion de Compte Keycloak
@ -22,7 +22,7 @@ email=Courriel
firstName=Nom firstName=Nom
givenName=Pr\u00e9nom givenName=Pr\u00e9nom
fullName=Nom Complet fullName=Nom Complet
lastName=Last name lastName=Nom
familyName=Nom de Famille familyName=Nom de Famille
password=Mot de passe password=Mot de passe
passwordConfirm=Confirmation passwordConfirm=Confirmation
@ -31,7 +31,7 @@ username=Compte
address=Adresse address=Adresse
street=Rue street=Rue
locality=Ville ou Localit\u00e9 locality=Ville ou Localit\u00e9
region=State, Province, or R\u00e9gion region=\u00c9tat, Province ou R\u00e9gion
postal_code=Code Postal postal_code=Code Postal
country=Pays country=Pays
emailVerified=Courriel v\u00e9rifi\u00e9 emailVerified=Courriel v\u00e9rifi\u00e9
@ -50,7 +50,7 @@ role_manage-realm=G\u00e9rer le domaine
role_manage-users=G\u00e9rer les utilisateurs role_manage-users=G\u00e9rer les utilisateurs
role_manage-applications=G\u00e9rer les applications role_manage-applications=G\u00e9rer les applications
role_manage-identity-providers=G\u00e9rer les fournisseurs d''identit\u00e9s role_manage-identity-providers=G\u00e9rer les fournisseurs d''identit\u00e9s
role_manage-clients=G\u00e9rer les clients role_manage-clients=G\u00e9rer les clients
role_manage-events=G\u00e9rer les \u00e9v\u00e9nements role_manage-events=G\u00e9rer les \u00e9v\u00e9nements
role_view-profile=Voir le profile role_view-profile=Voir le profile
role_manage-account=G\u00e9rer le compte role_manage-account=G\u00e9rer le compte
@ -63,7 +63,7 @@ client_broker=Broker
requiredFields=Champs obligatoires requiredFields=Champs obligatoires
allFieldsRequired=Tous les champs obligatoires allFieldsRequired=Tous les champs sont obligatoires
backToApplication=&laquo; Revenir \u00e0 l''application backToApplication=&laquo; Revenir \u00e0 l''application
backTo=Revenir \u00e0 {0} backTo=Revenir \u00e0 {0}
@ -74,9 +74,9 @@ ip=IP
client=Client client=Client
clients=Clients clients=Clients
details=D\u00e9tails details=D\u00e9tails
started=S\u00e9lectionn\u00e9 started=D\u00e9but
lastAccess=Dernier acc\u00e8s lastAccess=Dernier acc\u00e8s
expires=Expires expires=Expiration
applications=Applications applications=Applications
account=Compte account=Compte
@ -88,7 +88,7 @@ log=Connexion
application=Application application=Application
availablePermissions=Permissions Disponibles availablePermissions=Permissions Disponibles
grantedPermissions=Permissions accord\u00e9es grantedPermissions=Permissions accord\u00e9es
grantedPersonalInfo=Informations personnels accord\u00e9es grantedPersonalInfo=Informations personnelles accord\u00e9es
additionalGrants=Droits additionnels additionalGrants=Droits additionnels
action=Action action=Action
inResource=dans inResource=dans
@ -99,7 +99,7 @@ revoke=R\u00e9voquer un droit
configureAuthenticators=Authentifications configur\u00e9es. configureAuthenticators=Authentifications configur\u00e9es.
mobile=T\u00e9l\u00e9phone mobile mobile=T\u00e9l\u00e9phone mobile
totpStep1=Installez <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> ou bien Google Authenticator sur votre mobile. Ces deux applications sont disponibles sur <a href="https://play.google.com">Google Play</a> et Apple App Store. totpStep1=Installez <a href="https://fedorahosted.org/freeotp/" target="_blank">FreeOTP</a> ou bien Google Authenticator sur votre mobile. Ces deux applications sont disponibles sur <a href="https://play.google.com">Google Play</a> et Apple App Store.
totpStep2=Ouvrez l''application et scanner le code bar ou entrez la clef. totpStep2=Ouvrez l''application et scanner le code barre ou entrez la cl\u00e9.
totpStep3=Entrez le code \u00e0 usage unique fourni par l''application et cliquez sur Sauvegarder pour terminer. totpStep3=Entrez le code \u00e0 usage unique fourni par l''application et cliquez sur Sauvegarder pour terminer.
missingUsernameMessage=Veuillez entrer votre nom d''utilisateur. missingUsernameMessage=Veuillez entrer votre nom d''utilisateur.
@ -140,17 +140,18 @@ identityProviderRemovedMessage=Le fournisseur d''identit\u00e9 a \u00e9t\u00e9 s
accountDisabledMessage=Ce compte est d\u00e9sactiv\u00e9, veuillez contacter votre administrateur. accountDisabledMessage=Ce compte est d\u00e9sactiv\u00e9, veuillez contacter votre administrateur.
accountTemporarilyDisabledMessage=Ce compte est temporairement d\u00e9sactiv\u00e9, veuillez contacter votre administrateur ou r\u00e9essayez plus tard.. accountTemporarilyDisabledMessage=Ce compte est temporairement d\u00e9sactiv\u00e9, veuillez contacter votre administrateur ou r\u00e9essayez plus tard..
invalidPasswordMinLengthMessage=Mot de passe invalide : longueur minimale {0}. invalidPasswordMinLengthMessage=Mot de passe invalide: longueur minimale {0}.
invalidPasswordMinLowerCaseCharsMessage=Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule. invalidPasswordMinLowerCaseCharsMessage=Mot de passe invalide: doit contenir au moins {0} lettre(s) en minuscule.
invalidPasswordMinDigitsMessage=Mot de passe invalide : doit contenir au moins {0} chiffre(s). invalidPasswordMinDigitsMessage=Mot de passe invalide: doit contenir au moins {0} chiffre(s).
invalidPasswordMinUpperCaseCharsMessage=Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule. invalidPasswordMinUpperCaseCharsMessage=Mot de passe invalide: doit contenir au moins {0} lettre(s) en majuscule.
invalidPasswordMinSpecialCharsMessage=Mot de passe invalide : doit contenir au moins {0} caract\u00e8re(s) sp\u00e9ciaux. invalidPasswordMinSpecialCharsMessage=Mot de passe invalide: doit contenir au moins {0} caract\u00e8re(s) sp\u00e9ciaux.
invalidPasswordNotUsernameMessage=Mot de passe invalide : ne doit pas etre identique au nom d''utilisateur. invalidPasswordNotUsernameMessage=Mot de passe invalide: ne doit pas \u00eatre identique au nom d''utilisateur.
invalidPasswordRegexPatternMessage=Mot de passe invalide : ne valide pas l''expression rationnelle. invalidPasswordRegexPatternMessage=Mot de passe invalide: ne valide pas l''expression rationnelle.
invalidPasswordHistoryMessage=Mot de passe invalide : ne doit pas \u00eatre \u00e9gal aux {0} derniers mot de passe. invalidPasswordHistoryMessage=Mot de passe invalide: ne doit pas \u00eatre \u00e9gal aux {0} derniers mot de passe.
locale_de=German locale_de=German
locale_en=English locale_en=English
locale_it=Italian locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil) locale_pt-BR=Portugu\u00EAs (Brasil)
locale_fr=Fran\u00e7ais locale_fr=Fran\u00e7ais
locale_es=Espa\u00F1ol

View file

@ -125,4 +125,4 @@ locale_de=German
locale_en=English locale_en=English
locale_it=Italian locale_it=Italian
locale_pt-BR=Portugu\u00EAs (Brasil) locale_pt-BR=Portugu\u00EAs (Brasil)
locale_fr=Français locale_fr=Fran\u00e7ais

View file

@ -147,4 +147,4 @@ locale_de=Deutsch
locale_en=English locale_en=English
locale_it=Italian locale_it=Italian
locale_pt-BR=Portugu\u00EAs (BR) locale_pt-BR=Portugu\u00EAs (BR)
locale_fr=Français locale_fr=Fran\u00e7ais

View file

@ -25,6 +25,7 @@
<script src="${resourceUrl}/lib/angular/angular-sanitize.js"></script> <script src="${resourceUrl}/lib/angular/angular-sanitize.js"></script>
<script src="${resourceUrl}/lib/angular/angular-translate.js"></script> <script src="${resourceUrl}/lib/angular/angular-translate.js"></script>
<script src="${resourceUrl}/lib/angular/angular-translate-loader-url.js"></script> <script src="${resourceUrl}/lib/angular/angular-translate-loader-url.js"></script>
<script src="${resourceUrl}/lib/angular/treeview/angular.treeview.js"></script>
<script src="${resourceUrl}/lib/angular/ui-bootstrap-tpls-0.11.0.js"></script> <script src="${resourceUrl}/lib/angular/ui-bootstrap-tpls-0.11.0.js"></script>
<script src="${resourceUrl}/lib/angular/select2.js" type="text/javascript"></script> <script src="${resourceUrl}/lib/angular/select2.js" type="text/javascript"></script>
@ -37,6 +38,7 @@
<script src="${resourceUrl}/js/controllers/realm.js" type="text/javascript"></script> <script src="${resourceUrl}/js/controllers/realm.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/controllers/clients.js" type="text/javascript"></script> <script src="${resourceUrl}/js/controllers/clients.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/controllers/users.js" type="text/javascript"></script> <script src="${resourceUrl}/js/controllers/users.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/controllers/groups.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/loaders.js" type="text/javascript"></script> <script src="${resourceUrl}/js/loaders.js" type="text/javascript"></script>
<script src="${resourceUrl}/js/services.js" type="text/javascript"></script> <script src="${resourceUrl}/js/services.js" type="text/javascript"></script>
</head> </head>

View file

@ -22,7 +22,7 @@ registrationEmailAsUsername=de Email as username
registrationEmailAsUsername.tooltip=de If enabled then username field is hidden from registration form and email is used as username for new user. registrationEmailAsUsername.tooltip=de If enabled then username field is hidden from registration form and email is used as username for new user.
editUsernameAllowed=de Edit username editUsernameAllowed=de Edit username
editUsernameAllowed.tooltip=de If enabled, the username field is editable, readonly otherwise. editUsernameAllowed.tooltip=de If enabled, the username field is editable, readonly otherwise.
resetPasswordAllowed=de Forget password resetPasswordAllowed=de Forgot password
resetPasswordAllowed.tooltip=de Show a link on login page for user to click on when they have forgotten their credentials. resetPasswordAllowed.tooltip=de Show a link on login page for user to click on when they have forgotten their credentials.
rememberMe=de Remember Me rememberMe=de Remember Me
rememberMe.tooltip=de Show checkbox on login page to allow user to remain logged in between browser restarts until session expires. rememberMe.tooltip=de Show checkbox on login page to allow user to remain logged in between browser restarts until session expires.

View file

@ -22,7 +22,7 @@ registrationEmailAsUsername=Email as username
registrationEmailAsUsername.tooltip=If enabled then username field is hidden from registration form and email is used as username for new user. registrationEmailAsUsername.tooltip=If enabled then username field is hidden from registration form and email is used as username for new user.
editUsernameAllowed=Edit username editUsernameAllowed=Edit username
editUsernameAllowed.tooltip=If enabled, the username field is editable, readonly otherwise. editUsernameAllowed.tooltip=If enabled, the username field is editable, readonly otherwise.
resetPasswordAllowed=Forget password resetPasswordAllowed=Forgot password
resetPasswordAllowed.tooltip=Show a link on login page for user to click on when they have forgotten their credentials. resetPasswordAllowed.tooltip=Show a link on login page for user to click on when they have forgotten their credentials.
rememberMe=Remember Me rememberMe=Remember Me
rememberMe.tooltip=Show checkbox on login page to allow user to remain logged in between browser restarts until session expires. rememberMe.tooltip=Show checkbox on login page to allow user to remain logged in between browser restarts until session expires.
@ -109,6 +109,7 @@ realm-tab-email=Email
realm-tab-themes=Themes realm-tab-themes=Themes
realm-tab-cache=Cache realm-tab-cache=Cache
realm-tab-tokens=Tokens realm-tab-tokens=Tokens
realm-tab-client-initial-access=Initial Access Tokens
realm-tab-security-defenses=Security Defenses realm-tab-security-defenses=Security Defenses
realm-tab-general=General realm-tab-general=General
add-realm=Add Realm add-realm=Add Realm
@ -271,6 +272,9 @@ import-client-certificate=Import Client Certificate
jwt-import.key-alias.tooltip=Archive alias for your certificate. jwt-import.key-alias.tooltip=Archive alias for your certificate.
secret=Secret secret=Secret
regenerate-secret=Regenerate Secret regenerate-secret=Regenerate Secret
registrationAccessToken=Registration access token
registrationAccessToken.regenerate=Regenerate registration access token
registrationAccessToken.tooltip=The registration access token provides access for clients to the client registration service.
add-role=Add Role add-role=Add Role
role-name=Role Name role-name=Role Name
composite=Composite composite=Composite
@ -374,6 +378,7 @@ table-of-identity-providers=Table of identity providers
add-provider.placeholder=Add provider... add-provider.placeholder=Add provider...
provider=Provider provider=Provider
gui-order=GUI order gui-order=GUI order
first-broker-login-flow=First Login Flow
redirect-uri=Redirect URI redirect-uri=Redirect URI
redirect-uri.tooltip=The redirect uri to use when configuring the identity provider. redirect-uri.tooltip=The redirect uri to use when configuring the identity provider.
alias=Alias alias=Alias
@ -393,6 +398,7 @@ update-profile-on-first-login.tooltip=Define conditions under which a user has t
trust-email=Trust Email trust-email=Trust Email
trust-email.tooltip=If enabled then email provided by this provider is not verified even if verification is enabled for the realm. trust-email.tooltip=If enabled then email provided by this provider is not verified even if verification is enabled for the realm.
gui-order.tooltip=Number defining order of the provider in GUI (eg. on Login page). gui-order.tooltip=Number defining order of the provider in GUI (eg. on Login page).
first-broker-login-flow.tooltip=Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.
openid-connect-config=OpenID Connect Config openid-connect-config=OpenID Connect Config
openid-connect-config.tooltip=OIDC SP and external IDP configuration. openid-connect-config.tooltip=OIDC SP and external IDP configuration.
authorization-url=Authorization URL authorization-url=Authorization URL
@ -465,3 +471,17 @@ identity-provider-mappers=Identity Provider Mappers
create-identity-provider-mapper=Create Identity Provider Mapper create-identity-provider-mapper=Create Identity Provider Mapper
add-identity-provider-mapper=Add Identity Provider Mapper add-identity-provider-mapper=Add Identity Provider Mapper
client.description.tooltip=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description} client.description.tooltip=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description}
expires=Expires
expiration=Expiration
count=Count
remainingCount=Remaining count
created=Created
back=Back
initial-access-tokens=Initial Access Tokens
add-initial-access-tokens=Add Initial Access Token
initial-access-token=Initial Access Token
initial-access.copyPaste.tooltip=Copy/paste the initial access token before navigating away from this page as it's not posible to retrieve later
continue=Continue
initial-access-token.confirm.title=Copy Initial Access Token
initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later

Some files were not shown because too many files have changed in this diff Show more