Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Vlasta Ramik 2015-09-23 11:23:57 +02:00
commit d2d3a3f216
88 changed files with 2834 additions and 93 deletions

View file

@ -250,6 +250,7 @@ public class SAMLEndpoint {
builder.relayState(relayState); builder.relayState(relayState);
if (config.isWantAuthnRequestsSigned()) { if (config.isWantAuthnRequestsSigned()) {
builder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) builder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signatureAlgorithm(provider.getSignatureAlgorithm())
.signDocument(); .signDocument();
} }
try { try {

View file

@ -36,6 +36,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.saml.SAML2AuthnRequestBuilder; import org.keycloak.protocol.saml.SAML2AuthnRequestBuilder;
import org.keycloak.protocol.saml.SAML2LogoutRequestBuilder; import org.keycloak.protocol.saml.SAML2LogoutRequestBuilder;
import org.keycloak.protocol.saml.SAML2NameIDPolicyBuilder; import org.keycloak.protocol.saml.SAML2NameIDPolicyBuilder;
import org.keycloak.protocol.saml.SignatureAlgorithm;
import org.keycloak.saml.common.constants.GeneralConstants; import org.keycloak.saml.common.constants.GeneralConstants;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants; import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ConfigurationException;
@ -110,6 +111,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
KeyPair keypair = new KeyPair(publicKey, privateKey); KeyPair keypair = new KeyPair(publicKey, privateKey);
authnRequestBuilder.signWith(keypair); authnRequestBuilder.signWith(keypair);
authnRequestBuilder.signatureAlgorithm(getSignatureAlgorithm());
authnRequestBuilder.signDocument(); authnRequestBuilder.signDocument();
} }
@ -196,6 +198,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
.relayState(userSession.getId()); .relayState(userSession.getId());
if (getConfig().isWantAuthnRequestsSigned()) { if (getConfig().isWantAuthnRequestsSigned()) {
logoutBuilder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate()) logoutBuilder.signWith(realm.getPrivateKey(), realm.getPublicKey(), realm.getCertificate())
.signatureAlgorithm(getSignatureAlgorithm())
.signDocument(); .signDocument();
} }
return logoutBuilder; return logoutBuilder;
@ -245,4 +248,13 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
"</EntityDescriptor>\n"; "</EntityDescriptor>\n";
return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build(); return Response.ok(descriptor, MediaType.APPLICATION_XML_TYPE).build();
} }
public SignatureAlgorithm getSignatureAlgorithm() {
String alg = getConfig().getSignatureAlgorithm();
if (alg != null) {
SignatureAlgorithm algorithm = SignatureAlgorithm.valueOf(alg);
if (algorithm != null) return algorithm;
}
return SignatureAlgorithm.RSA_SHA256;
}
} }

View file

@ -87,6 +87,14 @@ public class SAMLIdentityProviderConfig extends IdentityProviderModel {
getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned)); getConfig().put("wantAuthnRequestsSigned", String.valueOf(wantAuthnRequestsSigned));
} }
public String getSignatureAlgorithm() {
return getConfig().get("signatureAlgorithm");
}
public void setSignatureAlgorithm(String signatureAlgorithm) {
getConfig().put("signatureAlgorithm", signatureAlgorithm);
}
public String getEncryptionPublicKey() { public String getEncryptionPublicKey() {
return getConfig().get("encryptionPublicKey"); return getConfig().get("encryptionPublicKey");
} }

View file

@ -0,0 +1,44 @@
<?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="mposolda@redhat.com" id="1.6.0">
<addColumn tableName="CLIENT">
<column name="OFFLINE_TOKENS_ENABLED" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
<createTable tableName="OFFLINE_USER_SESSION">
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="USER_SESSION_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="DATA" type="CLOB"/>
</createTable>
<createTable tableName="OFFLINE_CLIENT_SESSION">
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="CLIENT_SESSION_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="USER_SESSION_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="DATA" type="CLOB"/>
</createTable>
<addPrimaryKey columnNames="USER_SESSION_ID" constraintName="CONSTRAINT_OFFLINE_US_SES_PK" tableName="OFFLINE_USER_SESSION"/>
<addPrimaryKey columnNames="CLIENT_SESSION_ID" constraintName="CONSTRAINT_OFFLINE_CL_SES_PK" tableName="OFFLINE_CLIENT_SESSION"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="OFFLINE_USER_SESSION" constraintName="FK_OFFLINE_US_SES_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="OFFLINE_CLIENT_SESSION" constraintName="FK_OFFLINE_CL_SES_USER" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addForeignKeyConstraint baseColumnNames="USER_SESSION_ID" baseTableName="OFFLINE_CLIENT_SESSION" constraintName="FK_OFFLINE_CL_US_SES" referencedColumnNames="USER_SESSION_ID" referencedTableName="OFFLINE_USER_SESSION"/>
</changeSet>
</databaseChangeLog>

View file

@ -9,4 +9,5 @@
<include file="META-INF/jpa-changelog-1.3.0.xml"/> <include file="META-INF/jpa-changelog-1.3.0.xml"/>
<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.0.xml"/>
</databaseChangeLog> </databaseChangeLog>

View file

@ -29,6 +29,8 @@
<class>org.keycloak.models.jpa.entities.AuthenticationExecutionEntity</class> <class>org.keycloak.models.jpa.entities.AuthenticationExecutionEntity</class>
<class>org.keycloak.models.jpa.entities.AuthenticatorConfigEntity</class> <class>org.keycloak.models.jpa.entities.AuthenticatorConfigEntity</class>
<class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class> <class>org.keycloak.models.jpa.entities.RequiredActionProviderEntity</class>
<class>org.keycloak.models.jpa.entities.OfflineUserSessionEntity</class>
<class>org.keycloak.models.jpa.entities.OfflineClientSessionEntity</class>
<!-- JpaAuditProviders --> <!-- JpaAuditProviders -->
<class>org.keycloak.events.jpa.EventEntity</class> <class>org.keycloak.events.jpa.EventEntity</class>

View file

@ -48,6 +48,8 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
"org.keycloak.models.entities.AuthenticationFlowEntity", "org.keycloak.models.entities.AuthenticationFlowEntity",
"org.keycloak.models.entities.AuthenticatorConfigEntity", "org.keycloak.models.entities.AuthenticatorConfigEntity",
"org.keycloak.models.entities.RequiredActionProviderEntity", "org.keycloak.models.entities.RequiredActionProviderEntity",
"org.keycloak.models.entities.OfflineUserSessionEntity",
"org.keycloak.models.entities.OfflineClientSessionEntity",
}; };
private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class); private static final Logger logger = Logger.getLogger(DefaultMongoConnectionFactoryProvider.class);

View file

@ -38,6 +38,9 @@ public interface OAuth2Constants {
// https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03#section-2.2 // https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03#section-2.2
String CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; String CLIENT_ASSERTION_TYPE_JWT = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
String OFFLINE_ACCESS = "offline_access";
} }

View file

@ -1,6 +1,7 @@
package org.keycloak.representations; package org.keycloak.representations;
import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonProperty;
import org.keycloak.util.RefreshTokenUtil;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -11,9 +12,8 @@ import java.util.Map;
*/ */
public class RefreshToken extends AccessToken { public class RefreshToken extends AccessToken {
private RefreshToken() { private RefreshToken() {
type("REFRESH"); type(RefreshTokenUtil.TOKEN_TYPE_REFRESH);
} }
/** /**

View file

@ -24,6 +24,7 @@ public class ClientRepresentation {
protected Boolean bearerOnly; protected Boolean bearerOnly;
protected Boolean consentRequired; protected Boolean consentRequired;
protected Boolean serviceAccountsEnabled; protected Boolean serviceAccountsEnabled;
protected Boolean offlineTokensEnabled;
protected Boolean directGrantsOnly; protected Boolean directGrantsOnly;
protected Boolean publicClient; protected Boolean publicClient;
protected Boolean frontchannelLogout; protected Boolean frontchannelLogout;
@ -162,6 +163,14 @@ public class ClientRepresentation {
this.serviceAccountsEnabled = serviceAccountsEnabled; this.serviceAccountsEnabled = serviceAccountsEnabled;
} }
public Boolean isOfflineTokensEnabled() {
return offlineTokensEnabled;
}
public void setOfflineTokensEnabled(Boolean offlineTokensEnabled) {
this.offlineTokensEnabled = offlineTokensEnabled;
}
public Boolean isDirectGrantsOnly() { public Boolean isDirectGrantsOnly() {
return directGrantsOnly; return directGrantsOnly;
} }

View file

@ -0,0 +1,62 @@
package org.keycloak.util;
import java.io.IOException;
import org.keycloak.OAuth2Constants;
import org.keycloak.representations.RefreshToken;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class RefreshTokenUtil {
public static final String TOKEN_TYPE_REFRESH = "REFRESH";
public static final String TOKEN_TYPE_OFFLINE = "OFFLINE";
public static boolean isOfflineTokenRequested(String scopeParam) {
if (scopeParam == null) {
return false;
}
String[] scopes = scopeParam.split(" ");
for (String scope : scopes) {
if (OAuth2Constants.OFFLINE_ACCESS.equals(scope)) {
return true;
}
}
return false;
}
/**
* Return refresh token or offline otkne
*
* @param decodedToken
* @return
*/
public static RefreshToken getRefreshToken(byte[] decodedToken) throws IOException {
return JsonSerialization.readValue(decodedToken, RefreshToken.class);
}
private static RefreshToken getRefreshToken(String refreshToken) throws IOException {
byte[] decodedToken = Base64Url.decode(refreshToken);
return getRefreshToken(decodedToken);
}
/**
* Return true if given refreshToken represents offline token
*
* @param refreshToken
* @return
*/
public static boolean isOfflineToken(String refreshToken) {
try {
RefreshToken token = getRefreshToken(refreshToken);
return token.getType().equals(TOKEN_TYPE_OFFLINE);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}

View file

@ -898,7 +898,7 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
} }
]]></programlisting> ]]></programlisting>
where the <literal>mysecret</literal> needs to be replaced with the real value of client secret. You can obtain it from client admin console. where the <literal>mysecret</literal> needs to be replaced with the real value of client secret. You can obtain it from admin console from client configuration.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
@ -906,7 +906,7 @@ public class SecretQuestionRequiredActionFactory implements RequiredActionFactor
<term>Authentication with signed JWT</term> <term>Authentication with signed JWT</term>
<listitem> <listitem>
<para> <para>
This is based on the <ulink url="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">JWT Bearer Token Profiles for OAuth 2.0</ulink> specification. This is based on the <ulink url="https://tools.ietf.org/html/rfc7523">JWT Bearer Token Profiles for OAuth 2.0</ulink> specification.
The client/adapter generates the <ulink url="https://tools.ietf.org/html/rfc7519">JWT</ulink> and signs it with his private key. The client/adapter generates the <ulink url="https://tools.ietf.org/html/rfc7519">JWT</ulink> and signs it with his private key.
The Keycloak then verifies the signed JWT with the client's public key and authenticates client based on it. The Keycloak then verifies the signed JWT with the client's public key and authenticates client based on it.
</para> </para>

View file

@ -20,6 +20,7 @@ public interface Details {
String REMEMBER_ME = "remember_me"; String REMEMBER_ME = "remember_me";
String TOKEN_ID = "token_id"; String TOKEN_ID = "token_id";
String REFRESH_TOKEN_ID = "refresh_token_id"; String REFRESH_TOKEN_ID = "refresh_token_id";
String REFRESH_TOKEN_TYPE = "refresh_token_type";
String VALIDATE_ACCESS_TOKEN = "validate_access_token"; String VALIDATE_ACCESS_TOKEN = "validate_access_token";
String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id"; String UPDATED_REFRESH_TOKEN_ID = "updated_refresh_token_id";
String NODE_HOST = "node_host"; String NODE_HOST = "node_host";

View file

@ -1,9 +1,11 @@
package org.keycloak.account.freemarker.model; package org.keycloak.account.freemarker.model;
import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
@ -11,6 +13,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.offline.OfflineUserSessionManager;
import org.keycloak.util.MultivaluedHashMap; import org.keycloak.util.MultivaluedHashMap;
/** /**
@ -21,6 +24,9 @@ public class ApplicationsBean {
private List<ApplicationEntry> applications = new LinkedList<ApplicationEntry>(); private List<ApplicationEntry> applications = new LinkedList<ApplicationEntry>();
public ApplicationsBean(RealmModel realm, UserModel user) { public ApplicationsBean(RealmModel realm, UserModel user) {
Set<ClientModel> offlineClients = new OfflineUserSessionManager().findClientsWithOfflineToken(realm, user);
List<ClientModel> realmClients = realm.getClients(); List<ClientModel> realmClients = realm.getClients();
for (ClientModel client : realmClients) { for (ClientModel client : realmClients) {
// Don't show bearerOnly clients // Don't show bearerOnly clients
@ -52,7 +58,13 @@ public class ApplicationsBean {
} }
} }
ApplicationEntry appEntry = new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, realmRolesGranted, resourceRolesGranted, client, claimsGranted); List<String> additionalGrants = new ArrayList<>();
if (offlineClients.contains(client)) {
additionalGrants.add("${offlineAccess}");
}
ApplicationEntry appEntry = new ApplicationEntry(realmRolesAvailable, resourceRolesAvailable, realmRolesGranted, resourceRolesGranted, client,
claimsGranted, additionalGrants);
applications.add(appEntry); applications.add(appEntry);
} }
} }
@ -82,16 +94,18 @@ public class ApplicationsBean {
private final MultivaluedHashMap<String, ClientRoleEntry> resourceRolesGranted; private final MultivaluedHashMap<String, ClientRoleEntry> resourceRolesGranted;
private final ClientModel client; private final ClientModel client;
private final List<String> claimsGranted; private final List<String> claimsGranted;
private final List<String> additionalGrants;
public ApplicationEntry(List<RoleModel> realmRolesAvailable, MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable, public ApplicationEntry(List<RoleModel> realmRolesAvailable, MultivaluedHashMap<String, ClientRoleEntry> resourceRolesAvailable,
List<RoleModel> realmRolesGranted, MultivaluedHashMap<String, ClientRoleEntry> resourceRolesGranted, List<RoleModel> realmRolesGranted, MultivaluedHashMap<String, ClientRoleEntry> resourceRolesGranted,
ClientModel client, List<String> claimsGranted) { ClientModel client, List<String> claimsGranted, List<String> additionalGrants) {
this.realmRolesAvailable = realmRolesAvailable; this.realmRolesAvailable = realmRolesAvailable;
this.resourceRolesAvailable = resourceRolesAvailable; this.resourceRolesAvailable = resourceRolesAvailable;
this.realmRolesGranted = realmRolesGranted; this.realmRolesGranted = realmRolesGranted;
this.resourceRolesGranted = resourceRolesGranted; this.resourceRolesGranted = resourceRolesGranted;
this.client = client; this.client = client;
this.claimsGranted = claimsGranted; this.claimsGranted = claimsGranted;
this.additionalGrants = additionalGrants;
} }
public List<RoleModel> getRealmRolesAvailable() { public List<RoleModel> getRealmRolesAvailable() {
@ -118,6 +132,9 @@ public class ApplicationsBean {
return claimsGranted; return claimsGranted;
} }
public List<String> getAdditionalGrants() {
return additionalGrants;
}
} }
// Same class used in OAuthGrantBean as well. Maybe should be merged into common-freemarker... // Same class used in OAuthGrantBean as well. Maybe should be merged into common-freemarker...

View file

@ -18,6 +18,7 @@
<td>${msg("availablePermissions")}</td> <td>${msg("availablePermissions")}</td>
<td>${msg("grantedPermissions")}</td> <td>${msg("grantedPermissions")}</td>
<td>${msg("grantedPersonalInfo")}</td> <td>${msg("grantedPersonalInfo")}</td>
<td>${msg("additionalGrants")}</td>
<td>${msg("action")}</td> <td>${msg("action")}</td>
</tr> </tr>
</thead> </thead>
@ -76,7 +77,13 @@
</td> </td>
<td> <td>
<#if application.client.consentRequired && application.claimsGranted?has_content> <#list application.additionalGrants as grant>
${advancedMsg(grant)}<#if grant_has_next>, </#if>
</#list>
</td>
<td>
<#if (application.client.consentRequired && application.claimsGranted?has_content) || application.additionalGrants?has_content>
<button type='submit' class='${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!}' id='revoke-${application.client.clientId}' name='clientId' value="${application.client.id}">${msg("revoke")}</button> <button type='submit' class='${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!}' id='revoke-${application.client.clientId}' name='clientId' value="${application.client.id}">${msg("revoke")}</button>
</#if> </#if>
</td> </td>

View file

@ -85,9 +85,11 @@ application=Application
availablePermissions=Available Permissions availablePermissions=Available Permissions
grantedPermissions=Granted Permissions grantedPermissions=Granted Permissions
grantedPersonalInfo=Granted Personal Info grantedPersonalInfo=Granted Personal Info
additionalGrants=Additional Grants
action=Action action=Action
inResource=in inResource=in
fullAccess=Full Access fullAccess=Full Access
offlineAccess=Offline Access
revoke=Revoke Grant revoke=Revoke Grant
configureAuthenticators=Configured Authenticators configureAuthenticators=Configured Authenticators

View file

@ -693,10 +693,17 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload
} }
]; ];
$scope.signatureAlgorithms = [
"RSA_SHA1",
"RSA_SHA256",
"RSA_SHA512",
"DSA_SHA1"
];
if (instance && instance.alias) { if (instance && instance.alias) {
} else { } else {
$scope.identityProvider.config.nameIDPolicyFormat = $scope.nameIdFormats[0].format; $scope.identityProvider.config.nameIDPolicyFormat = $scope.nameIdFormats[0].format;
$scope.identityProvider.config.signatureAlgorithm = $scope.signatureAlgorithms[1];
$scope.identityProvider.updateProfileFirstLoginMode = "off"; $scope.identityProvider.updateProfileFirstLoginMode = "off";
} }
} }

View file

@ -79,6 +79,13 @@
<input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch /> <input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch />
</div> </div>
</div> </div>
<div class="form-group" data-ng-show="protocol == 'openid-connect' && !client.bearerOnly">
<label class="col-md-2 control-label" for="offlineTokensEnabled">Offline Tokens Enabled</label>
<kc-tooltip>Allows you to retrieve offline tokens for users. Offline token can be stored by client application and is valid even if user is not logged anymore.</kc-tooltip>
<div class="col-md-6">
<input ng-model="client.offlineTokensEnabled" name="offlineTokensEnabled" id="offlineTokensEnabled" onoffswitch />
</div>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'"> <div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-md-2 control-label" for="samlServerSignature">Include AuthnStatement</label> <label class="col-md-2 control-label" for="samlServerSignature">Include AuthnStatement</label>
<div class="col-sm-6"> <div class="col-sm-6">

View file

@ -34,7 +34,7 @@
placeholder="No value assigned" min="1" required> placeholder="No value assigned" min="1" required>
</td> </td>
<td class="kc-action-cell"> <td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" ng-click="removePolicy($index)">Delete</button> <button type="button" class="btn btn-default btn-block btn-sm" ng-click="removePolicy($index)">Delete</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -135,6 +135,18 @@
</div> </div>
<kc-tooltip> Indicates whether the identity provider expects signed a AuthnRequest.</kc-tooltip> <kc-tooltip> Indicates whether the identity provider expects signed a AuthnRequest.</kc-tooltip>
</div> </div>
<div class="form-group" data-ng-show="identityProvider.config.wantAuthnRequestsSigned == 'true'">
<label class="col-md-2 control-label" for="signatureAlgorithm">Signature Algorithm</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="signatureAlgorithm"
ng-model="identityProvider.config.signatureAlgorithm"
ng-options="alg for alg in signatureAlgorithms">
</select>
</div>
</div>
<kc-tooltip>The signature algorithm to use to sign documents.</kc-tooltip>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="forceAuthn">Force Authentication</label> <label class="col-md-2 control-label" for="forceAuthn">Force Authentication</label>
<div class="col-md-6"> <div class="col-md-6">

View file

@ -6,6 +6,16 @@
${msg("loginProfileTitle")} ${msg("loginProfileTitle")}
<#elseif section = "form"> <#elseif section = "form">
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post"> <form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<#if realm.editUsernameAllowed>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('username',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("username")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" name="username" value="${(user.username!'')?html}" class="${properties.kcInputClass!}"/>
</div>
</div>
</#if>
<div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}"> <div class="${properties.kcFormGroupClass!} ${messagesPerField.printIfExists('email',properties.kcFormGroupErrorClass!)}">
<div class="${properties.kcLabelWrapperClass!}"> <div class="${properties.kcLabelWrapperClass!}">
<label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label> <label for="email" class="${properties.kcLabelClass!}">${msg("email")}</label>

View file

@ -70,6 +70,8 @@ public class ProfileBean {
} }
public String getUsername() { return formData != null ? formData.getFirst("username") : user.getUsername(); }
public String getFirstName() { public String getFirstName() {
return formData != null ? formData.getFirst("firstName") : user.getFirstName(); return formData != null ? formData.getFirst("firstName") : user.getFirstName();
} }

View file

@ -66,6 +66,10 @@ public class RealmBean {
return realm.isInternationalizationEnabled(); return realm.isInternationalizationEnabled();
} }
public boolean isEditUsernameAllowed() {
return realm.isEditUsernameAllowed();
}
public boolean isPassword() { public boolean isPassword() {
for (RequiredCredentialModel r : realm.getRequiredCredentials()) { for (RequiredCredentialModel r : realm.getRequiredCredentials()) {
if (r.getType().equals(CredentialRepresentation.PASSWORD)) { if (r.getType().equals(CredentialRepresentation.PASSWORD)) {

View file

@ -135,6 +135,9 @@ public class OAuthRequestAuthenticator {
String idpHint = getQueryParamValue(AdapterConstants.KC_IDP_HINT); String idpHint = getQueryParamValue(AdapterConstants.KC_IDP_HINT);
url = UriUtils.stripQueryParam(url, AdapterConstants.KC_IDP_HINT); url = UriUtils.stripQueryParam(url, AdapterConstants.KC_IDP_HINT);
String scope = getQueryParamValue(OAuth2Constants.SCOPE);
url = UriUtils.stripQueryParam(url, OAuth2Constants.SCOPE);
KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone() KeycloakUriBuilder redirectUriBuilder = deployment.getAuthUrl().clone()
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
@ -147,6 +150,9 @@ public class OAuthRequestAuthenticator {
if (idpHint != null && idpHint.length() > 0) { if (idpHint != null && idpHint.length() > 0) {
redirectUriBuilder.queryParam(AdapterConstants.KC_IDP_HINT,idpHint); redirectUriBuilder.queryParam(AdapterConstants.KC_IDP_HINT,idpHint);
} }
if (scope != null && scope.length() > 0) {
redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, scope);
}
return redirectUriBuilder.build().toString(); return redirectUriBuilder.build().toString();
} }

View file

@ -117,7 +117,12 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext
} }
this.token = token; this.token = token;
this.refreshToken = response.getRefreshToken(); if (response.getRefreshToken() != null) {
if (log.isTraceEnabled()) {
log.trace("Setup new refresh token to the security context");
}
this.refreshToken = response.getRefreshToken();
}
this.tokenString = tokenString; this.tokenString = tokenString;
tokenStore.refreshCallback(this); tokenStore.refreshCallback(this);
return true; return true;

View file

@ -14,7 +14,7 @@ import org.keycloak.adapters.KeycloakDeployment;
* *
* You must specify a file * You must specify a file
* META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module * META-INF/services/org.keycloak.adapters.authentication.ClientCredentialsProvider in the WAR that this class is contained in (or in the JAR that is attached to the WEB-INF/lib or as jboss module
* if you want to share the implementation among more WARs). This file must have the fully qualified class name of all your ClientAuthenticatorFactory classes * if you want to share the implementation among more WARs).
* *
* NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support * NOTE: The SPI is not finished and method signatures are still subject to change in future versions (for example to support
* authentication with client certificate) * authentication with client certificate)

View file

@ -13,7 +13,7 @@ import org.keycloak.util.Time;
/** /**
* Client authentication based on JWT signed by client private key . * Client authentication based on JWT signed by client private key .
* See <a href="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">specs</a> for more details. * See <a href="https://tools.ietf.org/html/rfc7519">specs</a> for more details.
* *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/ */

View file

@ -164,6 +164,10 @@
url += '&kc_idp_hint=' + options.idpHint; url += '&kc_idp_hint=' + options.idpHint;
} }
if (options && options.scope) {
url += '&scope=' + options.scope;
}
return url; return url;
} }

View file

@ -109,6 +109,9 @@ public interface ClientModel extends RoleContainerModel {
boolean isServiceAccountsEnabled(); boolean isServiceAccountsEnabled();
void setServiceAccountsEnabled(boolean serviceAccountsEnabled); void setServiceAccountsEnabled(boolean serviceAccountsEnabled);
boolean isOfflineTokensEnabled();
void setOfflineTokensEnabled(boolean offlineTokensEnabled);
Set<RoleModel> getScopeMappings(); Set<RoleModel> getScopeMappings();
void addScopeMapping(RoleModel role); void addScopeMapping(RoleModel role);
void deleteScopeMapping(RoleModel role); void deleteScopeMapping(RoleModel role);

View file

@ -0,0 +1,44 @@
package org.keycloak.models;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineClientSessionModel {
private String clientSessionId;
private String userSessionId;
private String clientId;
private String data;
public String getClientSessionId() {
return clientSessionId;
}
public void setClientSessionId(String clientSessionId) {
this.clientSessionId = clientSessionId;
}
public String getUserSessionId() {
return userSessionId;
}
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}

View file

@ -0,0 +1,26 @@
package org.keycloak.models;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineUserSessionModel {
private String userSessionId;
private String data;
public String getUserSessionId() {
return userSessionId;
}
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}

View file

@ -1,5 +1,6 @@
package org.keycloak.models; package org.keycloak.models;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -113,6 +114,15 @@ public interface UserModel {
void updateConsent(UserConsentModel consent); void updateConsent(UserConsentModel consent);
boolean revokeConsentForClient(String clientInternalId); boolean revokeConsentForClient(String clientInternalId);
void addOfflineUserSession(OfflineUserSessionModel offlineUserSession);
OfflineUserSessionModel getOfflineUserSession(String userSessionId);
Collection<OfflineUserSessionModel> getOfflineUserSessions();
boolean removeOfflineUserSession(String userSessionId);
void addOfflineClientSession(OfflineClientSessionModel offlineClientSession);
OfflineClientSessionModel getOfflineClientSession(String clientSessionId);
Collection<OfflineClientSessionModel> getOfflineClientSessions();
boolean removeOfflineClientSession(String clientSessionId);
public static enum RequiredAction { public static enum RequiredAction {
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
} }

View file

@ -40,6 +40,7 @@ public interface UserSessionModel {
public String getNote(String name); public String getNote(String name);
public void setNote(String name, String value); public void setNote(String name, String value);
public void removeNote(String name); public void removeNote(String name);
public Map<String, String> getNotes();
State getState(); State getState();
void setState(State state); void setState(State state);

View file

@ -28,6 +28,7 @@ public class ClientEntity extends AbstractIdentifiableEntity {
private boolean bearerOnly; private boolean bearerOnly;
private boolean consentRequired; private boolean consentRequired;
private boolean serviceAccountsEnabled; private boolean serviceAccountsEnabled;
private boolean offlineTokensEnabled;
private boolean directGrantsOnly; private boolean directGrantsOnly;
private int nodeReRegistrationTimeout; private int nodeReRegistrationTimeout;
@ -228,6 +229,14 @@ public class ClientEntity extends AbstractIdentifiableEntity {
this.serviceAccountsEnabled = serviceAccountsEnabled; this.serviceAccountsEnabled = serviceAccountsEnabled;
} }
public boolean isOfflineTokensEnabled() {
return offlineTokensEnabled;
}
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
this.offlineTokensEnabled = offlineTokensEnabled;
}
public boolean isDirectGrantsOnly() { public boolean isDirectGrantsOnly() {
return directGrantsOnly; return directGrantsOnly;
} }

View file

@ -0,0 +1,35 @@
package org.keycloak.models.entities;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineClientSessionEntity {
private String clientSessionId;
private String clientId;
private String data;
public String getClientSessionId() {
return clientSessionId;
}
public void setClientSessionId(String clientSessionId) {
this.clientSessionId = clientSessionId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}

View file

@ -0,0 +1,37 @@
package org.keycloak.models.entities;
import java.util.List;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineUserSessionEntity {
private String userSessionId;
private String data;
private List<OfflineClientSessionEntity> offlineClientSessions;
public String getUserSessionId() {
return userSessionId;
}
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public List<OfflineClientSessionEntity> getOfflineClientSessions() {
return offlineClientSessions;
}
public void setOfflineClientSessions(List<OfflineClientSessionEntity> offlineClientSessions) {
this.offlineClientSessions = offlineClientSessions;
}
}

View file

@ -28,6 +28,7 @@ public class UserEntity extends AbstractIdentifiableEntity {
private List<FederatedIdentityEntity> federatedIdentities; private List<FederatedIdentityEntity> federatedIdentities;
private String federationLink; private String federationLink;
private String serviceAccountClientLink; private String serviceAccountClientLink;
private List<OfflineUserSessionEntity> offlineUserSessions;
public String getUsername() { public String getUsername() {
return username; return username;
@ -157,5 +158,13 @@ public class UserEntity extends AbstractIdentifiableEntity {
public void setServiceAccountClientLink(String serviceAccountClientLink) { public void setServiceAccountClientLink(String serviceAccountClientLink) {
this.serviceAccountClientLink = serviceAccountClientLink; this.serviceAccountClientLink = serviceAccountClientLink;
} }
public List<OfflineUserSessionEntity> getOfflineUserSessions() {
return offlineUserSessions;
}
public void setOfflineUserSessions(List<OfflineUserSessionEntity> offlineUserSessions) {
this.offlineUserSessions = offlineUserSessions;
}
} }

View file

@ -303,6 +303,7 @@ public class ModelToRepresentation {
rep.setBearerOnly(clientModel.isBearerOnly()); rep.setBearerOnly(clientModel.isBearerOnly());
rep.setConsentRequired(clientModel.isConsentRequired()); rep.setConsentRequired(clientModel.isConsentRequired());
rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled()); rep.setServiceAccountsEnabled(clientModel.isServiceAccountsEnabled());
rep.setOfflineTokensEnabled(clientModel.isOfflineTokensEnabled());
rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly()); rep.setDirectGrantsOnly(clientModel.isDirectGrantsOnly());
rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired()); rep.setSurrogateAuthRequired(clientModel.isSurrogateAuthRequired());
rep.setBaseUrl(clientModel.getBaseUrl()); rep.setBaseUrl(clientModel.getBaseUrl());

View file

@ -692,6 +692,7 @@ public class RepresentationToModel {
if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly()); if (resourceRep.isBearerOnly() != null) client.setBearerOnly(resourceRep.isBearerOnly());
if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired()); if (resourceRep.isConsentRequired() != null) client.setConsentRequired(resourceRep.isConsentRequired());
if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled()); if (resourceRep.isServiceAccountsEnabled() != null) client.setServiceAccountsEnabled(resourceRep.isServiceAccountsEnabled());
if (resourceRep.isOfflineTokensEnabled() != null) client.setOfflineTokensEnabled(resourceRep.isOfflineTokensEnabled());
if (resourceRep.isDirectGrantsOnly() != null) client.setDirectGrantsOnly(resourceRep.isDirectGrantsOnly()); if (resourceRep.isDirectGrantsOnly() != null) client.setDirectGrantsOnly(resourceRep.isDirectGrantsOnly());
if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient()); if (resourceRep.isPublicClient() != null) client.setPublicClient(resourceRep.isPublicClient());
if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout()); if (resourceRep.isFrontchannelLogout() != null) client.setFrontchannelLogout(resourceRep.isFrontchannelLogout());
@ -788,6 +789,7 @@ public class RepresentationToModel {
if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly()); if (rep.isBearerOnly() != null) resource.setBearerOnly(rep.isBearerOnly());
if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired()); if (rep.isConsentRequired() != null) resource.setConsentRequired(rep.isConsentRequired());
if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled()); if (rep.isServiceAccountsEnabled() != null) resource.setServiceAccountsEnabled(rep.isServiceAccountsEnabled());
if (rep.isOfflineTokensEnabled() != null) resource.setOfflineTokensEnabled(rep.isOfflineTokensEnabled());
if (rep.isDirectGrantsOnly() != null) resource.setDirectGrantsOnly(rep.isDirectGrantsOnly()); if (rep.isDirectGrantsOnly() != null) resource.setDirectGrantsOnly(rep.isDirectGrantsOnly());
if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient()); if (rep.isPublicClient() != null) resource.setPublicClient(rep.isPublicClient());
if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed()); if (rep.isFullScopeAllowed() != null) resource.setFullScopeAllowed(rep.isFullScopeAllowed());

View file

@ -1,12 +1,15 @@
package org.keycloak.models.utils; package org.keycloak.models.utils;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -255,4 +258,44 @@ public class UserModelDelegate implements UserModel {
public void setCreatedTimestamp(Long timestamp){ public void setCreatedTimestamp(Long timestamp){
delegate.setCreatedTimestamp(timestamp); delegate.setCreatedTimestamp(timestamp);
} }
@Override
public void addOfflineUserSession(OfflineUserSessionModel userSession) {
delegate.addOfflineUserSession(userSession);
}
@Override
public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
return delegate.getOfflineUserSession(userSessionId);
}
@Override
public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
return delegate.getOfflineUserSessions();
}
@Override
public boolean removeOfflineUserSession(String userSessionId) {
return delegate.removeOfflineUserSession(userSessionId);
}
@Override
public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
delegate.addOfflineClientSession(clientSession);
}
@Override
public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
return delegate.getOfflineClientSession(clientSessionId);
}
@Override
public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
return delegate.getOfflineClientSessions();
}
@Override
public boolean removeOfflineClientSession(String clientSessionId) {
return delegate.removeOfflineClientSession(clientSessionId);
}
} }

View file

@ -461,6 +461,16 @@ public class ClientAdapter implements ClientModel {
entity.setServiceAccountsEnabled(serviceAccountsEnabled); entity.setServiceAccountsEnabled(serviceAccountsEnabled);
} }
@Override
public boolean isOfflineTokensEnabled() {
return entity.isOfflineTokensEnabled();
}
@Override
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
entity.setOfflineTokensEnabled(offlineTokensEnabled);
}
@Override @Override
public boolean isDirectGrantsOnly() { public boolean isDirectGrantsOnly() {
return entity.isDirectGrantsOnly(); return entity.isDirectGrantsOnly();

View file

@ -22,7 +22,10 @@ import org.keycloak.models.ClientModel;
import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt; import static org.keycloak.models.utils.Pbkdf2PasswordEncoder.getSalt;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.PasswordPolicy; import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -32,6 +35,8 @@ import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.entities.CredentialEntity; import org.keycloak.models.entities.CredentialEntity;
import org.keycloak.models.entities.FederatedIdentityEntity; import org.keycloak.models.entities.FederatedIdentityEntity;
import org.keycloak.models.entities.OfflineClientSessionEntity;
import org.keycloak.models.entities.OfflineUserSessionEntity;
import org.keycloak.models.entities.RoleEntity; import org.keycloak.models.entities.RoleEntity;
import org.keycloak.models.entities.UserEntity; import org.keycloak.models.entities.UserEntity;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
@ -39,6 +44,7 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -216,7 +222,7 @@ public class UserAdapter implements UserModel, Comparable {
@Override @Override
public Map<String, List<String>> getAttributes() { public Map<String, List<String>> getAttributes() {
return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map)user.getAttributes()); return user.getAttributes()==null ? Collections.<String, List<String>>emptyMap() : Collections.unmodifiableMap((Map) user.getAttributes());
} }
@Override @Override
@ -568,6 +574,142 @@ public class UserAdapter implements UserModel, Comparable {
return false; return false;
} }
@Override
public void addOfflineUserSession(OfflineUserSessionModel userSession) {
if (user.getOfflineUserSessions() == null) {
user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
}
if (getUserSessionEntityById(userSession.getUserSessionId()) != null) {
throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + user.getUsername());
}
OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
entity.setUserSessionId(userSession.getUserSessionId());
entity.setData(userSession.getData());
entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
user.getOfflineUserSessions().add(entity);
}
@Override
public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
return entity==null ? null : toModel(entity);
}
@Override
public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
if (user.getOfflineUserSessions()==null) {
return Collections.emptyList();
} else {
List<OfflineUserSessionModel> result = new ArrayList<>();
for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
result.add(toModel(entity));
}
return result;
}
}
private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
OfflineUserSessionModel model = new OfflineUserSessionModel();
model.setUserSessionId(entity.getUserSessionId());
model.setData(entity.getData());
return model;
}
@Override
public boolean removeOfflineUserSession(String userSessionId) {
OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
if (entity != null) {
user.getOfflineUserSessions().remove(entity);
return true;
} else {
return false;
}
}
private OfflineUserSessionEntity getUserSessionEntityById(String userSessionId) {
if (user.getOfflineUserSessions() != null) {
for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
if (entity.getUserSessionId().equals(userSessionId)) {
return entity;
}
}
}
return null;
}
@Override
public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(clientSession.getUserSessionId());
if (userSessionEntity == null) {
throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
}
OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
clEntity.setClientSessionId(clientSession.getClientSessionId());
clEntity.setClientId(clientSession.getClientId());
clEntity.setData(clientSession.getData());
userSessionEntity.getOfflineClientSessions().add(clEntity);
}
@Override
public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
if (user.getOfflineUserSessions() != null) {
for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
if (clSession.getClientSessionId().equals(clientSessionId)) {
return toModel(clSession, userSession.getUserSessionId());
}
}
}
}
return null;
}
private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
OfflineClientSessionModel model = new OfflineClientSessionModel();
model.setClientSessionId(cls.getClientSessionId());
model.setClientId(cls.getClientId());
model.setData(cls.getData());
model.setUserSessionId(userSessionId);
return model;
}
@Override
public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
List<OfflineClientSessionModel> result = new ArrayList<>();
if (user.getOfflineUserSessions() != null) {
for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
result.add(toModel(clSession, userSession.getUserSessionId()));
}
}
}
return result;
}
@Override
public boolean removeOfflineClientSession(String clientSessionId) {
if (user.getOfflineUserSessions() != null) {
for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
if (clSession.getClientSessionId().equals(clientSessionId)) {
userSession.getOfflineClientSessions().remove(clSession);
return true;
}
}
}
}
return false;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -430,6 +430,18 @@ public class ClientAdapter implements ClientModel {
updated.setServiceAccountsEnabled(serviceAccountsEnabled); updated.setServiceAccountsEnabled(serviceAccountsEnabled);
} }
@Override
public boolean isOfflineTokensEnabled() {
if (updated != null) return updated.isOfflineTokensEnabled();
return cached.isOfflineTokensEnabled();
}
@Override
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
getDelegateForUpdate();
updated.setOfflineTokensEnabled(offlineTokensEnabled);
}
@Override @Override
public RoleModel getRole(String name) { public RoleModel getRole(String name) {
if (updated != null) return updated.getRole(name); if (updated != null) return updated.getRole(name);

View file

@ -319,6 +319,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
@Override @Override
public void preRemove(RealmModel realm, ClientModel client) { public void preRemove(RealmModel realm, ClientModel client) {
realmInvalidations.add(realm.getId()); // easier to just invalidate whole realm
getDelegate().preRemove(realm, client); getDelegate().preRemove(realm, client);
} }

View file

@ -348,4 +348,52 @@ public class UserAdapter implements UserModel {
getDelegateForUpdate(); getDelegateForUpdate();
return updated.revokeConsentForClient(clientId); return updated.revokeConsentForClient(clientId);
} }
@Override
public void addOfflineUserSession(OfflineUserSessionModel userSession) {
getDelegateForUpdate();
updated.addOfflineUserSession(userSession);
}
@Override
public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
if (updated != null) return updated.getOfflineUserSession(userSessionId);
return cached.getOfflineUserSessions().get(userSessionId);
}
@Override
public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
if (updated != null) return updated.getOfflineUserSessions();
return cached.getOfflineUserSessions().values();
}
@Override
public boolean removeOfflineUserSession(String userSessionId) {
getDelegateForUpdate();
return updated.removeOfflineUserSession(userSessionId);
}
@Override
public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
getDelegateForUpdate();
updated.addOfflineClientSession(clientSession);
}
@Override
public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
if (updated != null) return updated.getOfflineClientSession(clientSessionId);
return cached.getOfflineClientSessions().get(clientSessionId);
}
@Override
public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
if (updated != null) return updated.getOfflineClientSessions();
return cached.getOfflineClientSessions().values();
}
@Override
public boolean removeOfflineClientSession(String clientSessionId) {
getDelegateForUpdate();
return updated.removeOfflineClientSession(clientSessionId);
}
} }

View file

@ -47,6 +47,7 @@ public class CachedClient implements Serializable {
private boolean bearerOnly; private boolean bearerOnly;
private boolean consentRequired; private boolean consentRequired;
private boolean serviceAccountsEnabled; private boolean serviceAccountsEnabled;
private boolean offlineTokensEnabled;
private Map<String, String> roles = new HashMap<String, String>(); private Map<String, String> roles = new HashMap<String, String>();
private int nodeReRegistrationTimeout; private int nodeReRegistrationTimeout;
private Map<String, Integer> registeredNodes; private Map<String, Integer> registeredNodes;
@ -81,6 +82,7 @@ public class CachedClient implements Serializable {
bearerOnly = model.isBearerOnly(); bearerOnly = model.isBearerOnly();
consentRequired = model.isConsentRequired(); consentRequired = model.isConsentRequired();
serviceAccountsEnabled = model.isServiceAccountsEnabled(); serviceAccountsEnabled = model.isServiceAccountsEnabled();
offlineTokensEnabled = model.isOfflineTokensEnabled();
for (RoleModel role : model.getRoles()) { for (RoleModel role : model.getRoles()) {
roles.put(role.getName(), role.getId()); roles.put(role.getName(), role.getId());
cache.addCachedRole(new CachedClientRole(id, role, realm)); cache.addCachedRole(new CachedClientRole(id, role, realm));
@ -189,6 +191,10 @@ public class CachedClient implements Serializable {
return serviceAccountsEnabled; return serviceAccountsEnabled;
} }
public boolean isOfflineTokensEnabled() {
return offlineTokensEnabled;
}
public Map<String, String> getRoles() { public Map<String, String> getRoles() {
return roles; return roles;
} }

View file

@ -1,5 +1,7 @@
package org.keycloak.models.cache.entities; package org.keycloak.models.cache.entities;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
@ -7,9 +9,11 @@ import org.keycloak.models.UserModel;
import org.keycloak.util.MultivaluedHashMap; import org.keycloak.util.MultivaluedHashMap;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -33,6 +37,8 @@ public class CachedUser implements Serializable {
private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>(); private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
private Set<String> requiredActions = new HashSet<>(); private Set<String> requiredActions = new HashSet<>();
private Set<String> roleMappings = new HashSet<String>(); private Set<String> roleMappings = new HashSet<String>();
private Map<String, OfflineUserSessionModel> offlineUserSessions = new HashMap<>();
private Map<String, OfflineClientSessionModel> offlineClientSessions = new HashMap<>();
public CachedUser(RealmModel realm, UserModel user) { public CachedUser(RealmModel realm, UserModel user) {
this.id = user.getId(); this.id = user.getId();
@ -53,6 +59,12 @@ public class CachedUser implements Serializable {
for (RoleModel role : user.getRoleMappings()) { for (RoleModel role : user.getRoleMappings()) {
roleMappings.add(role.getId()); roleMappings.add(role.getId());
} }
for (OfflineUserSessionModel offlineSession : user.getOfflineUserSessions()) {
offlineUserSessions.put(offlineSession.getUserSessionId(), offlineSession);
}
for (OfflineClientSessionModel offlineSession : user.getOfflineClientSessions()) {
offlineClientSessions.put(offlineSession.getClientSessionId(), offlineSession);
}
} }
public String getId() { public String getId() {
@ -118,4 +130,12 @@ public class CachedUser implements Serializable {
public String getServiceAccountClientLink() { public String getServiceAccountClientLink() {
return serviceAccountClientLink; return serviceAccountClientLink;
} }
public Map<String, OfflineUserSessionModel> getOfflineUserSessions() {
return offlineUserSessions;
}
public Map<String, OfflineClientSessionModel> getOfflineClientSessions() {
return offlineClientSessions;
}
} }

View file

@ -481,6 +481,16 @@ public class ClientAdapter implements ClientModel {
entity.setServiceAccountsEnabled(serviceAccountsEnabled); entity.setServiceAccountsEnabled(serviceAccountsEnabled);
} }
@Override
public boolean isOfflineTokensEnabled() {
return entity.isOfflineTokensEnabled();
}
@Override
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
entity.setOfflineTokensEnabled(offlineTokensEnabled);
}
@Override @Override
public boolean isDirectGrantsOnly() { public boolean isDirectGrantsOnly() {
return entity.isDirectGrantsOnly(); return entity.isDirectGrantsOnly();

View file

@ -169,6 +169,10 @@ public class JpaUserProvider implements UserProvider {
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserAttributesByRealm") num = em.createNamedQuery("deleteUserAttributesByRealm")
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteOfflineClientSessionsByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteOfflineUserSessionsByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUsersByRealm") num = em.createNamedQuery("deleteUsersByRealm")
.setParameter("realmId", realm.getId()).executeUpdate(); .setParameter("realmId", realm.getId()).executeUpdate();
} }
@ -195,6 +199,14 @@ public class JpaUserProvider implements UserProvider {
.setParameter("realmId", realm.getId()) .setParameter("realmId", realm.getId())
.setParameter("link", link.getId()) .setParameter("link", link.getId())
.executeUpdate(); .executeUpdate();
num = em.createNamedQuery("deleteOfflineClientSessionsByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.executeUpdate();
num = em.createNamedQuery("deleteOfflineUserSessionsByRealmAndLink")
.setParameter("realmId", realm.getId())
.setParameter("link", link.getId())
.executeUpdate();
num = em.createNamedQuery("deleteUsersByRealmAndLink") num = em.createNamedQuery("deleteUsersByRealmAndLink")
.setParameter("realmId", realm.getId()) .setParameter("realmId", realm.getId())
.setParameter("link", link.getId()) .setParameter("link", link.getId())
@ -212,6 +224,8 @@ public class JpaUserProvider implements UserProvider {
em.createNamedQuery("deleteUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate(); em.createNamedQuery("deleteUserConsentProtMappersByClient").setParameter("clientId", client.getId()).executeUpdate();
em.createNamedQuery("deleteUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate(); em.createNamedQuery("deleteUserConsentRolesByClient").setParameter("clientId", client.getId()).executeUpdate();
em.createNamedQuery("deleteUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate(); em.createNamedQuery("deleteUserConsentsByClient").setParameter("clientId", client.getId()).executeUpdate();
em.createNamedQuery("deleteOfflineClientSessionsByClient").setParameter("clientId", client.getId()).executeUpdate();
em.createNamedQuery("deleteDetachedOfflineUserSessions").executeUpdate();
} }
@Override @Override

View file

@ -2,6 +2,8 @@ package org.keycloak.models.jpa;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
@ -14,6 +16,8 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.jpa.entities.CredentialEntity; import org.keycloak.models.jpa.entities.CredentialEntity;
import org.keycloak.models.jpa.entities.OfflineClientSessionEntity;
import org.keycloak.models.jpa.entities.OfflineUserSessionEntity;
import org.keycloak.models.jpa.entities.UserConsentEntity; import org.keycloak.models.jpa.entities.UserConsentEntity;
import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity; import org.keycloak.models.jpa.entities.UserConsentProtocolMapperEntity;
import org.keycloak.models.jpa.entities.UserConsentRoleEntity; import org.keycloak.models.jpa.entities.UserConsentRoleEntity;
@ -37,6 +41,7 @@ import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -750,6 +755,124 @@ public class UserAdapter implements UserModel {
em.flush(); em.flush();
} }
@Override
public void addOfflineUserSession(OfflineUserSessionModel offlineSession) {
OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
entity.setUser(user);
entity.setUserSessionId(offlineSession.getUserSessionId());
entity.setData(offlineSession.getData());
em.persist(entity);
user.getOfflineUserSessions().add(entity);
em.flush();
}
@Override
public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
if (entity.getUserSessionId().equals(userSessionId)) {
return toModel(entity);
}
}
return null;
}
private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
OfflineUserSessionModel model = new OfflineUserSessionModel();
model.setUserSessionId(entity.getUserSessionId());
model.setData(entity.getData());
return model;
}
@Override
public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
List<OfflineUserSessionModel> result = new LinkedList<>();
for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
result.add(toModel(entity));
}
return result;
}
@Override
public boolean removeOfflineUserSession(String userSessionId) {
OfflineUserSessionEntity found = null;
for (OfflineUserSessionEntity session : user.getOfflineUserSessions()) {
if (session.getUserSessionId().equals(userSessionId)) {
found = session;
break;
}
}
if (found == null) {
return false;
} else {
user.getOfflineUserSessions().remove(found);
em.remove(found);
em.flush();
return true;
}
}
@Override
public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
OfflineClientSessionEntity entity = new OfflineClientSessionEntity();
entity.setUser(user);
entity.setClientSessionId(clientSession.getClientSessionId());
entity.setUserSessionId(clientSession.getUserSessionId());
entity.setClientId(clientSession.getClientId());
entity.setData(clientSession.getData());
em.persist(entity);
user.getOfflineClientSessions().add(entity);
em.flush();
}
@Override
public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
for (OfflineClientSessionEntity entity : user.getOfflineClientSessions()) {
if (entity.getClientSessionId().equals(clientSessionId)) {
return toModel(entity);
}
}
return null;
}
private OfflineClientSessionModel toModel(OfflineClientSessionEntity entity) {
OfflineClientSessionModel model = new OfflineClientSessionModel();
model.setClientSessionId(entity.getClientSessionId());
model.setClientId(entity.getClientId());
model.setUserSessionId(entity.getUserSessionId());
model.setData(entity.getData());
return model;
}
@Override
public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
List<OfflineClientSessionModel> result = new LinkedList<>();
for (OfflineClientSessionEntity entity : user.getOfflineClientSessions()) {
result.add(toModel(entity));
}
return result;
}
@Override
public boolean removeOfflineClientSession(String clientSessionId) {
OfflineClientSessionEntity found = null;
for (OfflineClientSessionEntity session : user.getOfflineClientSessions()) {
if (session.getClientSessionId().equals(clientSessionId)) {
found = session;
break;
}
}
if (found == null) {
return false;
} else {
user.getOfflineClientSessions().remove(found);
em.remove(found);
em.flush();
return true;
}
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -100,6 +100,9 @@ public class ClientEntity {
@Column(name="SERVICE_ACCOUNTS_ENABLED") @Column(name="SERVICE_ACCOUNTS_ENABLED")
private boolean serviceAccountsEnabled; private boolean serviceAccountsEnabled;
@Column(name="OFFLINE_TOKENS_ENABLED")
private boolean offlineTokensEnabled;
@Column(name="NODE_REREG_TIMEOUT") @Column(name="NODE_REREG_TIMEOUT")
private int nodeReRegistrationTimeout; private int nodeReRegistrationTimeout;
@ -316,6 +319,14 @@ public class ClientEntity {
this.serviceAccountsEnabled = serviceAccountsEnabled; this.serviceAccountsEnabled = serviceAccountsEnabled;
} }
public boolean isOfflineTokensEnabled() {
return offlineTokensEnabled;
}
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
this.offlineTokensEnabled = offlineTokensEnabled;
}
public boolean isDirectGrantsOnly() { public boolean isDirectGrantsOnly() {
return directGrantsOnly; return directGrantsOnly;
} }

View file

@ -0,0 +1,81 @@
package org.keycloak.models.jpa.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@NamedQueries({
@NamedQuery(name="deleteOfflineClientSessionsByRealm", query="delete from OfflineClientSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId)"),
@NamedQuery(name="deleteOfflineClientSessionsByRealmAndLink", query="delete from OfflineClientSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
@NamedQuery(name="deleteOfflineClientSessionsByClient", query="delete from OfflineClientSessionEntity sess where sess.clientId=:clientId")
})
@Table(name="OFFLINE_CLIENT_SESSION")
@Entity
public class OfflineClientSessionEntity {
@Id
@Column(name="CLIENT_SESSION_ID", length = 36)
protected String clientSessionId;
@Column(name="USER_SESSION_ID", length = 36)
protected String userSessionId;
@Column(name="CLIENT_ID", length = 36)
protected String clientId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="USER_ID")
protected UserEntity user;
@Column(name="DATA")
protected String data;
public String getClientSessionId() {
return clientSessionId;
}
public void setClientSessionId(String clientSessionId) {
this.clientSessionId = clientSessionId;
}
public String getUserSessionId() {
return userSessionId;
}
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public UserEntity getUser() {
return user;
}
public void setUser(UserEntity user) {
this.user = user;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}

View file

@ -0,0 +1,59 @@
package org.keycloak.models.jpa.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@NamedQueries({
@NamedQuery(name="deleteOfflineUserSessionsByRealm", query="delete from OfflineUserSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId)"),
@NamedQuery(name="deleteOfflineUserSessionsByRealmAndLink", query="delete from OfflineUserSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
@NamedQuery(name="deleteDetachedOfflineUserSessions", query="delete from OfflineUserSessionEntity sess where sess.userSessionId NOT IN (select c.userSessionId from OfflineClientSessionEntity c)")
})
@Table(name="OFFLINE_USER_SESSION")
@Entity
public class OfflineUserSessionEntity {
@Id
@Column(name="USER_SESSION_ID", length = 36)
protected String userSessionId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="USER_ID")
protected UserEntity user;
@Column(name="DATA")
protected String data;
public String getUserSessionId() {
return userSessionId;
}
public void setUserSessionId(String userSessionId) {
this.userSessionId = userSessionId;
}
public UserEntity getUser() {
return user;
}
public void setUser(UserEntity user) {
this.user = user;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}

View file

@ -3,9 +3,13 @@ package org.keycloak.models.jpa.entities;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.CascadeType; import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.MapKeyColumn;
import javax.persistence.NamedQueries; import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
import javax.persistence.OneToMany; import javax.persistence.OneToMany;
@ -14,6 +18,8 @@ import javax.persistence.UniqueConstraint;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -83,6 +89,12 @@ public class UserEntity {
@Column(name="SERVICE_ACCOUNT_CLIENT_LINK") @Column(name="SERVICE_ACCOUNT_CLIENT_LINK")
protected String serviceAccountClientLink; protected String serviceAccountClientLink;
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user")
protected Collection<OfflineUserSessionEntity> offlineUserSessions = new ArrayList<>();
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="user")
protected Collection<OfflineClientSessionEntity> offlineClientSessions = new ArrayList<>();
public String getId() { public String getId() {
return id; return id;
} }
@ -212,6 +224,22 @@ public class UserEntity {
this.serviceAccountClientLink = serviceAccountClientLink; this.serviceAccountClientLink = serviceAccountClientLink;
} }
public Collection<OfflineUserSessionEntity> getOfflineUserSessions() {
return offlineUserSessions;
}
public void setOfflineUserSessions(Collection<OfflineUserSessionEntity> offlineUserSessions) {
this.offlineUserSessions = offlineUserSessions;
}
public Collection<OfflineClientSessionEntity> getOfflineClientSessions() {
return offlineClientSessions;
}
public void setOfflineClientSessions(Collection<OfflineClientSessionEntity> offlineClientSessions) {
this.offlineClientSessions = offlineClientSessions;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -483,6 +483,17 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
updateMongoEntity(); updateMongoEntity();
} }
@Override
public boolean isOfflineTokensEnabled() {
return getMongoEntity().isOfflineTokensEnabled();
}
@Override
public void setOfflineTokensEnabled(boolean offlineTokensEnabled) {
getMongoEntity().setOfflineTokensEnabled(offlineTokensEnabled);
updateMongoEntity();
}
@Override @Override
public boolean isDirectGrantsOnly() { public boolean isDirectGrantsOnly() {
return getMongoEntity().isDirectGrantsOnly(); return getMongoEntity().isDirectGrantsOnly();

View file

@ -19,6 +19,8 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider; import org.keycloak.models.UserProvider;
import org.keycloak.models.entities.FederatedIdentityEntity; import org.keycloak.models.entities.FederatedIdentityEntity;
import org.keycloak.models.entities.OfflineClientSessionEntity;
import org.keycloak.models.entities.OfflineUserSessionEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.utils.CredentialValidation; import org.keycloak.models.utils.CredentialValidation;
@ -399,6 +401,36 @@ public class MongoUserProvider implements UserProvider {
.and("clientId").is(client.getId()) .and("clientId").is(client.getId())
.get(); .get();
getMongoStore().removeEntities(MongoUserConsentEntity.class, query, false, invocationContext); getMongoStore().removeEntities(MongoUserConsentEntity.class, query, false, invocationContext);
// Remove all offlineClientSessions
query = new QueryBuilder()
.and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
.get();
List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, invocationContext);
for (MongoUserEntity user : users) {
boolean anyRemoved = false;
for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
for (OfflineClientSessionEntity clientSession : userSession.getOfflineClientSessions()) {
if (clientSession.getClientId().equals(client.getId())) {
userSession.getOfflineClientSessions().remove(clientSession);
anyRemoved = true;
break;
}
}
// Check if it was last clientSession. Then remove userSession too
if (userSession.getOfflineClientSessions().size() == 0) {
user.getOfflineUserSessions().remove(userSession);
anyRemoved = true;
break;
}
}
if (anyRemoved) {
getMongoStore().updateEntity(user, invocationContext);
}
}
} }
@Override @Override

View file

@ -8,6 +8,8 @@ import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.OTPPolicy; import org.keycloak.models.OTPPolicy;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserConsentModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
@ -20,6 +22,8 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.entities.CredentialEntity; import org.keycloak.models.entities.CredentialEntity;
import org.keycloak.models.entities.OfflineClientSessionEntity;
import org.keycloak.models.entities.OfflineUserSessionEntity;
import org.keycloak.models.entities.UserConsentEntity; import org.keycloak.models.entities.UserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity;
import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity; import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
@ -30,6 +34,7 @@ import org.keycloak.models.utils.Pbkdf2PasswordEncoder;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -627,6 +632,145 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
return getMongoStore().removeEntity(entity, invocationContext); return getMongoStore().removeEntity(entity, invocationContext);
} }
@Override
public void addOfflineUserSession(OfflineUserSessionModel userSession) {
if (user.getOfflineUserSessions() == null) {
user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
}
if (getUserSessionEntityById(userSession.getUserSessionId()) != null) {
throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + getMongoEntity().getUsername());
}
OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
entity.setUserSessionId(userSession.getUserSessionId());
entity.setData(userSession.getData());
entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
user.getOfflineUserSessions().add(entity);
updateUser();
}
@Override
public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
return entity==null ? null : toModel(entity);
}
@Override
public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
if (user.getOfflineUserSessions()==null) {
return Collections.emptyList();
} else {
List<OfflineUserSessionModel> result = new ArrayList<>();
for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
result.add(toModel(entity));
}
return result;
}
}
private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
OfflineUserSessionModel model = new OfflineUserSessionModel();
model.setUserSessionId(entity.getUserSessionId());
model.setData(entity.getData());
return model;
}
@Override
public boolean removeOfflineUserSession(String userSessionId) {
OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
if (entity != null) {
user.getOfflineUserSessions().remove(entity);
updateUser();
return true;
} else {
return false;
}
}
private OfflineUserSessionEntity getUserSessionEntityById(String userSessionId) {
if (user.getOfflineUserSessions() != null) {
for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
if (entity.getUserSessionId().equals(userSessionId)) {
return entity;
}
}
}
return null;
}
@Override
public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(clientSession.getUserSessionId());
if (userSessionEntity == null) {
throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + getMongoEntity().getUsername());
}
OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
clEntity.setClientSessionId(clientSession.getClientSessionId());
clEntity.setClientId(clientSession.getClientId());
clEntity.setData(clientSession.getData());
userSessionEntity.getOfflineClientSessions().add(clEntity);
updateUser();
}
@Override
public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
if (user.getOfflineUserSessions() != null) {
for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
if (clSession.getClientSessionId().equals(clientSessionId)) {
return toModel(clSession, userSession.getUserSessionId());
}
}
}
}
return null;
}
private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
OfflineClientSessionModel model = new OfflineClientSessionModel();
model.setClientSessionId(cls.getClientSessionId());
model.setClientId(cls.getClientId());
model.setData(cls.getData());
model.setUserSessionId(userSessionId);
return model;
}
@Override
public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
List<OfflineClientSessionModel> result = new ArrayList<>();
if (user.getOfflineUserSessions() != null) {
for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
result.add(toModel(clSession, userSession.getUserSessionId()));
}
}
}
return result;
}
@Override
public boolean removeOfflineClientSession(String clientSessionId) {
if (user.getOfflineUserSessions() != null) {
for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
if (clSession.getClientSessionId().equals(clientSessionId)) {
userSession.getOfflineClientSessions().remove(clSession);
updateUser();
return true;
}
}
}
}
return false;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -14,6 +14,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -109,6 +110,11 @@ public class UserSessionAdapter implements UserSessionModel {
} }
} }
@Override
public Map<String, String> getNotes() {
return entity.getNotes();
}
@Override @Override
public State getState() { public State getState() {
return entity.getState(); return entity.getState();

View file

@ -10,6 +10,7 @@ import org.keycloak.models.sessions.infinispan.compat.entities.UserSessionEntity
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -144,5 +145,8 @@ public class UserSessionAdapter implements UserSessionModel {
} }
@Override
public Map<String, String> getNotes() {
return entity.getNotes();
}
} }

View file

@ -27,7 +27,7 @@ import org.keycloak.services.Urls;
/** /**
* Client authentication based on JWT signed by client private key . * Client authentication based on JWT signed by client private key .
* See <a href="https://tools.ietf.org/html/draft-jones-oauth-jwt-bearer-03">specs</a> for more details. * See <a href="https://tools.ietf.org/html/rfc7519">specs</a> for more details.
* *
* This is server side, which verifies JWT from client_assertion parameter, where the assertion was created on adapter side by * This is server side, which verifies JWT from client_assertion parameter, where the assertion was created on adapter side by
* org.keycloak.adapters.authentication.JWTClientCredentialsProvider * org.keycloak.adapters.authentication.JWTClientCredentialsProvider

View file

@ -52,7 +52,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
RealmModel realm = context.getRealm(); RealmModel realm = context.getRealm();
List<FormMessage> errors = Validation.validateUpdateProfileForm(formData); List<FormMessage> errors = Validation.validateUpdateProfileForm(realm, formData);
if (errors != null && !errors.isEmpty()) { if (errors != null && !errors.isEmpty()) {
Response challenge = context.form() Response challenge = context.form()
.setErrors(errors) .setErrors(errors)
@ -62,6 +62,28 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact
return; return;
} }
if (realm.isEditUsernameAllowed()) {
String username = formData.getFirst("username");
String oldUsername = user.getUsername();
boolean usernameChanged = oldUsername != null ? !oldUsername.equals(username) : username != null;
if (usernameChanged) {
if (session.users().getUserByUsername(username, realm) != null) {
Response challenge = context.form()
.setError(Messages.USERNAME_EXISTS)
.setFormData(formData)
.createResponse(UserModel.RequiredAction.UPDATE_PROFILE);
context.challenge(challenge);
return;
}
user.setUsername(username);
}
}
user.setFirstName(formData.getFirst("firstName")); user.setFirstName(formData.getFirst("firstName"));
user.setLastName(formData.getFirst("lastName")); user.setLastName(formData.getFirst("lastName"));

View file

@ -20,7 +20,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256"); public static final List<String> DEFAULT_ID_TOKEN_SIGNING_ALG_VALUES_SUPPORTED = list("RS256");
public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD); public static final List<String> DEFAULT_GRANT_TYPES_SUPPORTED = list(OAuth2Constants.AUTHORIZATION_CODE, OAuth2Constants.REFRESH_TOKEN, OAuth2Constants.PASSWORD, OAuth2Constants.CLIENT_CREDENTIALS);
public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE); public static final List<String> DEFAULT_RESPONSE_TYPES_SUPPORTED = list(OAuth2Constants.CODE);

View file

@ -3,8 +3,8 @@ package org.keycloak.protocol.oidc;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.ClientConnection; import org.keycloak.ClientConnection;
import org.keycloak.OAuthErrorException; import org.keycloak.OAuthErrorException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
@ -27,11 +27,15 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.offline.OfflineUserSessionManager;
import org.keycloak.util.RefreshTokenUtil;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.io.IOException; import java.io.IOException;
import java.util.HashSet; import java.util.HashSet;
@ -85,16 +89,31 @@ public class TokenManager {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "User disabled", "User disabled");
} }
UserSessionModel userSession = session.sessions().getUserSession(realm, oldToken.getSessionState()); UserSessionModel userSession = null;
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
}
ClientSessionModel clientSession = null; ClientSessionModel clientSession = null;
for (ClientSessionModel clientSessionModel : userSession.getClientSessions()) { if (RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
if (clientSessionModel.getId().equals(oldToken.getClientSession())) { // Check if offline tokens still allowed for the client
clientSession = clientSessionModel; clientSession = new OfflineUserSessionManager().findOfflineClientSession(realm, user, oldToken.getClientSession(), oldToken.getSessionState());
break; if (clientSession != null) {
if (!clientSession.getClient().isOfflineTokensEnabled()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Offline tokens not allowed for client", "Offline tokens not allowed for client");
}
userSession = clientSession.getUserSession();
}
} else {
// Find userSession regularly for online tokens
userSession = session.sessions().getUserSession(realm, oldToken.getSessionState());
if (!AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, connection, headers, true);
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
}
for (ClientSessionModel clientSessionModel : userSession.getClientSessions()) {
if (clientSessionModel.getId().equals(oldToken.getClientSession())) {
clientSession = clientSessionModel;
break;
}
} }
} }
@ -126,10 +145,12 @@ public class TokenManager {
} }
public AccessTokenResponse refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException { public RefreshResult refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient, String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException {
RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken); RefreshToken refreshToken = verifyRefreshToken(realm, encodedRefreshToken);
event.user(refreshToken.getSubject()).session(refreshToken.getSessionState()).detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()); event.user(refreshToken.getSubject()).session(refreshToken.getSessionState())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, refreshToken.getType());
TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken, headers); TokenValidation validation = validateToken(session, uriInfo, connection, realm, refreshToken, headers);
// validate authorizedClient is same as validated client // validate authorizedClient is same as validated client
@ -140,11 +161,17 @@ public class TokenManager {
int currentTime = Time.currentTime(); int currentTime = Time.currentTime();
validation.userSession.setLastSessionRefresh(currentTime); validation.userSession.setLastSessionRefresh(currentTime);
AccessTokenResponse res = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession) AccessTokenResponseBuilder responseBuilder = responseBuilder(realm, authorizedClient, event, session, validation.userSession, validation.clientSession)
.accessToken(validation.newToken) .accessToken(validation.newToken)
.generateIDToken() .generateIDToken();
.generateRefreshToken().build();
return res; // Don't generate refresh token again if refresh was triggered with offline token
if (!refreshToken.getType().equals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE)) {
responseBuilder.generateRefreshToken();
}
AccessTokenResponse res = responseBuilder.build();
return new RefreshResult(res, RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(refreshToken.getType()));
} }
public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException { public RefreshToken verifyRefreshToken(RealmModel realm, String encodedRefreshToken) throws OAuthErrorException {
@ -158,7 +185,7 @@ public class TokenManager {
} catch (Exception e) { } catch (Exception e) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
} }
if (refreshToken.isExpired()) { if (refreshToken.getExpiration() != 0 && refreshToken.isExpired()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired");
} }
@ -172,18 +199,18 @@ public class TokenManager {
IDToken idToken = null; IDToken idToken = null;
try { try {
if (!RSAProvider.verify(jws, realm.getPublicKey())) { if (!RSAProvider.verify(jws, realm.getPublicKey())) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken");
} }
idToken = jws.readJsonContent(IDToken.class); idToken = jws.readJsonContent(IDToken.class);
} catch (IOException e) { } catch (IOException e) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid IDToken", e);
} }
if (idToken.isExpired()) { if (idToken.isExpired()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Refresh token expired"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "IDToken expired");
} }
if (idToken.getIssuedAt() < realm.getNotBefore()) { if (idToken.getIssuedAt() < realm.getNotBefore()) {
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale refresh token"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Stale IDToken");
} }
return idToken; return idToken;
} }
@ -250,9 +277,7 @@ public class TokenManager {
if (client.isFullScopeAllowed()) return roleMappings; if (client.isFullScopeAllowed()) return roleMappings;
Set<RoleModel> scopeMappings = client.getScopeMappings(); Set<RoleModel> scopeMappings = client.getScopeMappings();
if (client instanceof ClientModel) { scopeMappings.addAll(client.getRoles());
scopeMappings.addAll(((ClientModel) client).getRoles());
}
for (RoleModel role : roleMappings) { for (RoleModel role : roleMappings) {
for (RoleModel desiredRole : scopeMappings) { for (RoleModel desiredRole : scopeMappings) {
@ -409,7 +434,9 @@ public class TokenManager {
return this; return this;
} }
public AccessTokenResponseBuilder generateAccessToken(KeycloakSession session, String scopeParam, ClientModel client, UserModel user, UserSessionModel userSession, ClientSessionModel clientSession) { public AccessTokenResponseBuilder generateAccessToken() {
UserModel user = userSession.getUser();
String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
Set<RoleModel> requestedRoles = getAccess(scopeParam, client, user); Set<RoleModel> requestedRoles = getAccess(scopeParam, client, user);
accessToken = createClientAccessToken(session, requestedRoles, realm, client, user, userSession, clientSession); accessToken = createClientAccessToken(session, requestedRoles, realm, client, user, userSession, clientSession);
return this; return this;
@ -419,10 +446,24 @@ public class TokenManager {
if (accessToken == null) { if (accessToken == null) {
throw new IllegalStateException("accessToken not set"); throw new IllegalStateException("accessToken not set");
} }
refreshToken = new RefreshToken(accessToken);
String scopeParam = clientSession.getNote(OIDCLoginProtocol.SCOPE_PARAM);
boolean offlineTokenRequested = RefreshTokenUtil.isOfflineTokenRequested(scopeParam);
if (offlineTokenRequested) {
if (!clientSession.getClient().isOfflineTokensEnabled()) {
event.error(Errors.INVALID_CLIENT);
throw new ErrorResponseException("invalid_client", "Offline tokens not allowed for the client", Response.Status.BAD_REQUEST);
}
refreshToken = new RefreshToken(accessToken);
refreshToken.type(RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
new OfflineUserSessionManager().persistOfflineSession(clientSession, userSession);
} else {
refreshToken = new RefreshToken(accessToken);
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
}
refreshToken.id(KeycloakModelUtils.generateId()); refreshToken.id(KeycloakModelUtils.generateId());
refreshToken.issuedNow(); refreshToken.issuedNow();
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
return this; return this;
} }
@ -459,6 +500,7 @@ public class TokenManager {
} else { } else {
event.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId()); event.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId());
} }
event.detail(Details.REFRESH_TOKEN_TYPE, refreshToken.getType());
} }
AccessTokenResponse res = new AccessTokenResponse(); AccessTokenResponse res = new AccessTokenResponse();
@ -489,4 +531,23 @@ public class TokenManager {
} }
} }
public class RefreshResult {
private final AccessTokenResponse response;
private final boolean offlineToken;
private RefreshResult(AccessTokenResponse response, boolean offlineToken) {
this.response = response;
this.offlineToken = offlineToken;
}
public AccessTokenResponse getResponse() {
return response;
}
public boolean isOfflineToken() {
return offlineToken;
}
}
} }

View file

@ -262,11 +262,14 @@ public class TokenEndpoint {
AccessTokenResponse res; AccessTokenResponse res;
try { try {
res = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event, headers); TokenManager.RefreshResult result = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event, headers);
res = result.getResponse();
UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState()); if (!result.isOfflineToken()) {
updateClientSessions(userSession.getClientSessions()); UserSessionModel userSession = session.sessions().getUserSession(realm, res.getSessionState());
updateUserSessionFromClientAuth(userSession); updateClientSessions(userSession.getClientSessions());
updateUserSessionFromClientAuth(userSession);
}
} catch (OAuthErrorException e) { } catch (OAuthErrorException e) {
event.error(Errors.INVALID_TOKEN); event.error(Errors.INVALID_TOKEN);
@ -337,6 +340,8 @@ public class TokenEndpoint {
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
AuthenticationFlowModel flow = realm.getDirectGrantFlow(); AuthenticationFlowModel flow = realm.getDirectGrantFlow();
String flowId = flow.getId(); String flowId = flow.getId();
AuthenticationProcessor processor = new AuthenticationProcessor(); AuthenticationProcessor processor = new AuthenticationProcessor();
@ -363,7 +368,7 @@ public class TokenEndpoint {
updateUserSessionFromClientAuth(userSession); updateUserSessionFromClientAuth(userSession);
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
.generateAccessToken(session, scope, client, user, userSession, clientSession) .generateAccessToken()
.generateRefreshToken() .generateRefreshToken()
.generateIDToken() .generateIDToken()
.build(); .build();
@ -415,6 +420,7 @@ public class TokenEndpoint {
ClientSessionModel clientSession = sessions.createClientSession(realm, client); ClientSessionModel clientSession = sessions.createClientSession(realm, client);
clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL);
clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); clientSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
clientSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null); UserSessionModel userSession = sessions.createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
event.session(userSession); event.session(userSession);
@ -429,7 +435,7 @@ public class TokenEndpoint {
updateUserSessionFromClientAuth(userSession); updateUserSessionFromClientAuth(userSession);
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession) AccessTokenResponse res = tokenManager.responseBuilder(realm, client, event, session, userSession, clientSession)
.generateAccessToken(session, scope, client, clientUser, userSession, clientSession) .generateAccessToken()
.generateRefreshToken() .generateRefreshToken()
.generateIDToken() .generateIDToken()
.build(); .build();

View file

@ -1,11 +0,0 @@
package org.keycloak.services.managers;
import org.keycloak.login.LoginFormsProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface HttpAuthenticationChallenge {
void addChallenge(LoginFormsProvider loginFormsProvider);
}

View file

@ -0,0 +1,289 @@
package org.keycloak.services.offline;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.codehaus.jackson.annotate.JsonProperty;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineClientSessionAdapter implements ClientSessionModel {
private final OfflineClientSessionModel model;
private final RealmModel realm;
private final ClientModel client;
private final OfflineUserSessionAdapter userSession;
private OfflineClientSessionData data;
public OfflineClientSessionAdapter(OfflineClientSessionModel model, RealmModel realm, ClientModel client, OfflineUserSessionAdapter userSession) {
this.model = model;
this.realm = realm;
this.client = client;
this.userSession = userSession;
}
// lazily init representation
private OfflineClientSessionData getData() {
if (data == null) {
try {
data = JsonSerialization.readValue(model.getData(), OfflineClientSessionData.class);
} catch (IOException ioe) {
throw new ModelException(ioe);
}
}
return data;
}
@Override
public String getId() {
return model.getClientSessionId();
}
@Override
public RealmModel getRealm() {
return realm;
}
@Override
public ClientModel getClient() {
return client;
}
@Override
public UserSessionModel getUserSession() {
return userSession;
}
@Override
public void setUserSession(UserSessionModel userSession) {
throw new IllegalStateException("Not supported setUserSession");
}
@Override
public String getRedirectUri() {
return data.getRedirectUri();
}
@Override
public void setRedirectUri(String uri) {
throw new IllegalStateException("Not supported setRedirectUri");
}
@Override
public int getTimestamp() {
return 0;
}
@Override
public void setTimestamp(int timestamp) {
throw new IllegalStateException("Not supported setTimestamp");
}
@Override
public String getAction() {
return null;
}
@Override
public void setAction(String action) {
throw new IllegalStateException("Not supported setAction");
}
@Override
public Set<String> getRoles() {
return getData().getRoles();
}
@Override
public void setRoles(Set<String> roles) {
throw new IllegalStateException("Not supported setRoles");
}
@Override
public Set<String> getProtocolMappers() {
return getData().getProtocolMappers();
}
@Override
public void setProtocolMappers(Set<String> protocolMappers) {
throw new IllegalStateException("Not supported setProtocolMappers");
}
@Override
public Map<String, ExecutionStatus> getExecutionStatus() {
return getData().getAuthenticatorStatus();
}
@Override
public void setExecutionStatus(String authenticator, ExecutionStatus status) {
throw new IllegalStateException("Not supported setExecutionStatus");
}
@Override
public void clearExecutionStatus() {
throw new IllegalStateException("Not supported clearExecutionStatus");
}
@Override
public UserModel getAuthenticatedUser() {
return userSession.getUser();
}
@Override
public void setAuthenticatedUser(UserModel user) {
throw new IllegalStateException("Not supported setAuthenticatedUser");
}
@Override
public String getAuthMethod() {
return getData().getAuthMethod();
}
@Override
public void setAuthMethod(String method) {
throw new IllegalStateException("Not supported setAuthMethod");
}
@Override
public String getNote(String name) {
return getData().getNotes()==null ? null : getData().getNotes().get(name);
}
@Override
public void setNote(String name, String value) {
throw new IllegalStateException("Not supported setNote");
}
@Override
public void removeNote(String name) {
throw new IllegalStateException("Not supported removeNote");
}
@Override
public Map<String, String> getNotes() {
return getData().getNotes();
}
@Override
public Set<String> getRequiredActions() {
throw new IllegalStateException("Not supported getRequiredActions");
}
@Override
public void addRequiredAction(String action) {
throw new IllegalStateException("Not supported addRequiredAction");
}
@Override
public void removeRequiredAction(String action) {
throw new IllegalStateException("Not supported removeRequiredAction");
}
@Override
public void addRequiredAction(UserModel.RequiredAction action) {
throw new IllegalStateException("Not supported addRequiredAction");
}
@Override
public void removeRequiredAction(UserModel.RequiredAction action) {
throw new IllegalStateException("Not supported removeRequiredAction");
}
@Override
public void setUserSessionNote(String name, String value) {
throw new IllegalStateException("Not supported setUserSessionNote");
}
@Override
public Map<String, String> getUserSessionNotes() {
throw new IllegalStateException("Not supported getUserSessionNotes");
}
@Override
public void clearUserSessionNotes() {
throw new IllegalStateException("Not supported clearUserSessionNotes");
}
protected static class OfflineClientSessionData {
@JsonProperty("authMethod")
private String authMethod;
@JsonProperty("redirectUri")
private String redirectUri;
@JsonProperty("protocolMappers")
private Set<String> protocolMappers;
@JsonProperty("roles")
private Set<String> roles;
@JsonProperty("notes")
private Map<String, String> notes;
@JsonProperty("authenticatorStatus")
private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
public String getAuthMethod() {
return authMethod;
}
public void setAuthMethod(String authMethod) {
this.authMethod = authMethod;
}
public String getRedirectUri() {
return redirectUri;
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
public Set<String> getProtocolMappers() {
return protocolMappers;
}
public void setProtocolMappers(Set<String> protocolMappers) {
this.protocolMappers = protocolMappers;
}
public Set<String> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
this.roles = roles;
}
public Map<String, String> getNotes() {
return notes;
}
public void setNotes(Map<String, String> notes) {
this.notes = notes;
}
public Map<String, ClientSessionModel.ExecutionStatus> getAuthenticatorStatus() {
return authenticatorStatus;
}
public void setAuthenticatorStatus(Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus) {
this.authenticatorStatus = authenticatorStatus;
}
}
}

View file

@ -0,0 +1,215 @@
package org.keycloak.services.offline;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import org.codehaus.jackson.annotate.JsonProperty;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineUserSessionAdapter implements UserSessionModel {
private final OfflineUserSessionModel model;
private final UserModel user;
private OfflineUserSessionData data;
public OfflineUserSessionAdapter(OfflineUserSessionModel model, UserModel user) {
this.model = model;
this.user = user;
}
// lazily init representation
private OfflineUserSessionData getData() {
if (data == null) {
try {
data = JsonSerialization.readValue(model.getData(), OfflineUserSessionData.class);
} catch (IOException ioe) {
throw new ModelException(ioe);
}
}
return data;
}
@Override
public String getId() {
return model.getUserSessionId();
}
@Override
public String getBrokerSessionId() {
return getData().getBrokerSessionId();
}
@Override
public String getBrokerUserId() {
return getData().getBrokerUserId();
}
@Override
public UserModel getUser() {
return user;
}
@Override
public String getLoginUsername() {
return user.getUsername();
}
@Override
public String getIpAddress() {
return getData().getIpAddress();
}
@Override
public String getAuthMethod() {
return getData().getAuthMethod();
}
@Override
public boolean isRememberMe() {
return getData().isRememberMe();
}
@Override
public int getStarted() {
return getData().getStarted();
}
@Override
public int getLastSessionRefresh() {
return 0;
}
@Override
public void setLastSessionRefresh(int seconds) {
// Ignore
}
@Override
public List<ClientSessionModel> getClientSessions() {
throw new IllegalStateException("Not yet supported");
}
@Override
public String getNote(String name) {
return getData().getNotes()==null ? null : getData().getNotes().get(name);
}
@Override
public void setNote(String name, String value) {
throw new IllegalStateException("Illegal to set note offline session");
}
@Override
public void removeNote(String name) {
throw new IllegalStateException("Illegal to remove note from offline session");
}
@Override
public Map<String, String> getNotes() {
return getData().getNotes();
}
@Override
public State getState() {
return null;
}
@Override
public void setState(State state) {
throw new IllegalStateException("Illegal to set state on offline session");
}
protected static class OfflineUserSessionData {
@JsonProperty("brokerSessionId")
private String brokerSessionId;
@JsonProperty("brokerUserId")
private String brokerUserId;
@JsonProperty("ipAddress")
private String ipAddress;
@JsonProperty("authMethod")
private String authMethod;
@JsonProperty("rememberMe")
private boolean rememberMe;
@JsonProperty("started")
private int started;
@JsonProperty("notes")
private Map<String, String> notes;
public String getBrokerSessionId() {
return brokerSessionId;
}
public void setBrokerSessionId(String brokerSessionId) {
this.brokerSessionId = brokerSessionId;
}
public String getBrokerUserId() {
return brokerUserId;
}
public void setBrokerUserId(String brokerUserId) {
this.brokerUserId = brokerUserId;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public String getAuthMethod() {
return authMethod;
}
public void setAuthMethod(String authMethod) {
this.authMethod = authMethod;
}
public boolean isRememberMe() {
return rememberMe;
}
public void setRememberMe(boolean rememberMe) {
this.rememberMe = rememberMe;
}
public int getStarted() {
return started;
}
public void setStarted(int started) {
this.started = started;
}
public Map<String, String> getNotes() {
return notes;
}
public void setNotes(Map<String, String> notes) {
this.notes = notes;
}
}
}

View file

@ -0,0 +1,182 @@
package org.keycloak.services.offline;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.util.JsonSerialization;
/**
* TODO: Change to utils?
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineUserSessionManager {
protected static Logger logger = Logger.getLogger(OfflineUserSessionManager.class);
public void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
UserModel user = userSession.getUser();
ClientModel client = clientSession.getClient();
// First verify if we already have offlineToken for this user+client . If yes, then invalidate it (This is to avoid leaks)
Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
for (OfflineClientSessionModel existing : clientSessions) {
if (existing.getClientId().equals(client.getId())) {
if (logger.isTraceEnabled()) {
logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' . Offline token will be replaced with new one",
user.getUsername(), client.getClientId(), existing.getClientSessionId());
}
user.removeOfflineClientSession(existing.getClientSessionId());
// Check if userSession is ours. If not, then check if it has other clientSessions and remove it otherwise
if (!existing.getUserSessionId().equals(userSession.getId())) {
checkUserSessionHasClientSessions(user, existing.getUserSessionId());
}
}
}
// Verify if we already have UserSession with this ID. If yes, don't create another one
OfflineUserSessionModel userSessionRep = user.getOfflineUserSession(userSession.getId());
if (userSessionRep == null) {
createOfflineUserSession(user, userSession);
}
// Create clientRep and save to DB.
createOfflineClientSession(user, clientSession, userSession);
}
// userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
public ClientSessionModel findOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
OfflineClientSessionModel clientSession = user.getOfflineClientSession(clientSessionId);
if (clientSession == null) {
return null;
}
if (!userSessionId.equals(clientSession.getUserSessionId())) {
throw new ModelException("User session don't match. Offline client session " + clientSession.getClientSessionId() + ", It's user session " + clientSession.getUserSessionId() +
" Wanted user session: " + userSessionId);
}
OfflineUserSessionModel userSession = user.getOfflineUserSession(userSessionId);
if (userSession == null) {
throw new ModelException("Found clientSession " + clientSessionId + " but not userSession " + userSessionId);
}
OfflineUserSessionAdapter userSessionAdapter = new OfflineUserSessionAdapter(userSession, user);
ClientModel client = realm.getClientById(clientSession.getClientId());
OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(clientSession, realm, client, userSessionAdapter);
return clientSessionAdapter;
}
public Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
Set<ClientModel> clients = new HashSet<>();
for (OfflineClientSessionModel clientSession : clientSessions) {
ClientModel client = realm.getClientById(clientSession.getClientId());
clients.add(client);
}
return clients;
}
public boolean revokeOfflineToken(UserModel user, ClientModel client) {
Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
boolean anyRemoved = false;
for (OfflineClientSessionModel clientSession : clientSessions) {
if (clientSession.getClientId().equals(client.getId())) {
if (logger.isTraceEnabled()) {
logger.tracef("Removing existing offline token for user '%s' and client '%s' . ClientSessionID was '%s' .",
user.getUsername(), client.getClientId(), clientSession.getClientSessionId());
}
user.removeOfflineClientSession(clientSession.getClientSessionId());
checkUserSessionHasClientSessions(user, clientSession.getUserSessionId());
anyRemoved = true;
}
}
return anyRemoved;
}
private void createOfflineUserSession(UserModel user, UserSessionModel userSession) {
if (logger.isTraceEnabled()) {
logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername());
}
OfflineUserSessionAdapter.OfflineUserSessionData rep = new OfflineUserSessionAdapter.OfflineUserSessionData();
rep.setBrokerUserId(userSession.getBrokerUserId());
rep.setBrokerSessionId(userSession.getBrokerSessionId());
rep.setIpAddress(userSession.getIpAddress());
rep.setAuthMethod(userSession.getAuthMethod());
rep.setRememberMe(userSession.isRememberMe());
rep.setStarted(userSession.getStarted());
rep.setNotes(userSession.getNotes());
try {
String stringRep = JsonSerialization.writeValueAsString(rep);
OfflineUserSessionModel sessionModel = new OfflineUserSessionModel();
sessionModel.setUserSessionId(userSession.getId());
sessionModel.setData(stringRep);
user.addOfflineUserSession(sessionModel);
} catch (IOException ioe) {
throw new ModelException(ioe);
}
}
private void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
if (logger.isTraceEnabled()) {
logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" ,
clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
}
OfflineClientSessionAdapter.OfflineClientSessionData rep = new OfflineClientSessionAdapter.OfflineClientSessionData();
rep.setAuthMethod(clientSession.getAuthMethod());
rep.setRedirectUri(clientSession.getRedirectUri());
rep.setProtocolMappers(clientSession.getProtocolMappers());
rep.setRoles(clientSession.getRoles());
rep.setNotes(clientSession.getNotes());
rep.setAuthenticatorStatus(clientSession.getExecutionStatus());
try {
String stringRep = JsonSerialization.writeValueAsString(rep);
OfflineClientSessionModel clsModel = new OfflineClientSessionModel();
clsModel.setClientSessionId(clientSession.getId());
clsModel.setClientId(clientSession.getClient().getId());
clsModel.setUserSessionId(userSession.getId());
clsModel.setData(stringRep);
user.addOfflineClientSession(clsModel);
} catch (IOException ioe) {
throw new ModelException(ioe);
}
}
// Check if userSession has any offline clientSessions attached to it. Remove userSession if not
private void checkUserSessionHasClientSessions(UserModel user, String userSessionId) {
Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
for (OfflineClientSessionModel clientSession : clientSessions) {
if (clientSession.getUserSessionId().equals(userSessionId)) {
return;
}
}
if (logger.isTraceEnabled()) {
logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId);
}
user.removeOfflineUserSession(userSessionId);
}
}

View file

@ -58,6 +58,7 @@ import org.keycloak.services.managers.Auth;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.offline.OfflineUserSessionManager;
import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation; import org.keycloak.services.validation.Validation;
import org.keycloak.util.UriUtils; import org.keycloak.util.UriUtils;
@ -486,6 +487,7 @@ public class AccountService extends AbstractSecuredLocalService {
// Revoke grant in UserModel // Revoke grant in UserModel
UserModel user = auth.getUser(); UserModel user = auth.getUser();
user.revokeConsentForClient(client.getId()); user.revokeConsentForClient(client.getId());
new OfflineUserSessionManager().revokeOfflineToken(user, client);
// Logout clientSessions for this user and client // Logout clientSessions for this user and client
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers); AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);

View file

@ -30,6 +30,7 @@ import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.util.RefreshTokenUtil;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -156,6 +157,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
.detail(Details.CODE_ID, codeId) .detail(Details.CODE_ID, codeId)
.detail(Details.TOKEN_ID, isUUID()) .detail(Details.TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_ID, isUUID()) .detail(Details.REFRESH_TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_REFRESH)
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
.session(sessionId); .session(sessionId);
} }
@ -164,6 +166,7 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
return expect(EventType.REFRESH_TOKEN) return expect(EventType.REFRESH_TOKEN)
.detail(Details.TOKEN_ID, isUUID()) .detail(Details.TOKEN_ID, isUUID())
.detail(Details.REFRESH_TOKEN_ID, refreshTokenId) .detail(Details.REFRESH_TOKEN_ID, refreshTokenId)
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_REFRESH)
.detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID()) .detail(Details.UPDATED_REFRESH_TOKEN_ID, isUUID())
.detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID) .detail(Details.CLIENT_AUTH_METHOD, ClientIdAndSecretAuthenticator.PROVIDER_ID)
.session(sessionId); .session(sessionId);

View file

@ -76,6 +76,8 @@ public class OAuthClient {
private String state = "mystate"; private String state = "mystate";
private String scope;
private String uiLocales = null; private String uiLocales = null;
private PublicKey realmPublicKey; private PublicKey realmPublicKey;
@ -192,6 +194,9 @@ public class OAuthClient {
if (clientSessionHost != null) { if (clientSessionHost != null) {
parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost)); parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost));
} }
if (scope != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
}
UrlEncodedFormEntity formEntity; UrlEncodedFormEntity formEntity;
try { try {
@ -218,6 +223,10 @@ public class OAuthClient {
List<NameValuePair> parameters = new LinkedList<NameValuePair>(); List<NameValuePair> parameters = new LinkedList<NameValuePair>();
parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS)); parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
if (scope != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.SCOPE, scope));
}
UrlEncodedFormEntity formEntity; UrlEncodedFormEntity formEntity;
try { try {
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
@ -390,6 +399,9 @@ public class OAuthClient {
if(uiLocales != null){ if(uiLocales != null){
b.queryParam(LocaleHelper.UI_LOCALES_PARAM, uiLocales); b.queryParam(LocaleHelper.UI_LOCALES_PARAM, uiLocales);
} }
if (scope != null) {
b.queryParam(OAuth2Constants.SCOPE, scope);
}
return b.build(realm).toString(); return b.build(realm).toString();
} }
@ -452,6 +464,11 @@ public class OAuthClient {
return this; return this;
} }
public OAuthClient scope(String scope) {
this.scope = scope;
return this;
}
public OAuthClient uiLocales(String uiLocales){ public OAuthClient uiLocales(String uiLocales){
this.uiLocales = uiLocales; this.uiLocales = uiLocales;
return this; return this;

View file

@ -646,7 +646,7 @@ public class AccountTest {
} }
} }
// More tests (including revoke) are in OAuthGrantTest // More tests (including revoke) are in OAuthGrantTest and OfflineTokenTest
@Test @Test
public void applications() { public void applications() {
applicationsPage.open(); applicationsPage.open();

View file

@ -33,11 +33,8 @@ import org.keycloak.models.UserModel.RequiredAction;
import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.*;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebResource;
@ -83,7 +80,7 @@ public class RequiredActionMultipleActionsTest {
protected LoginPasswordUpdatePage changePasswordPage; protected LoginPasswordUpdatePage changePasswordPage;
@WebResource @WebResource
protected LoginUpdateProfilePage updateProfilePage; protected LoginUpdateProfileEditUsernameAllowedPage updateProfilePage;
@Test @Test
public void updateProfileAndPassword() throws Exception { public void updateProfileAndPassword() throws Exception {
@ -121,7 +118,7 @@ public class RequiredActionMultipleActionsTest {
} }
public String updateProfile(String sessionId) { public String updateProfile(String sessionId) {
updateProfilePage.update("New first", "New last", "new@email.com"); updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");
AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com"); AssertEvents.ExpectedEvent expectedEvent = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com");
if (sessionId != null) { if (sessionId != null) {

View file

@ -21,11 +21,7 @@
*/ */
package org.keycloak.testsuite.actions; package org.keycloak.testsuite.actions;
import org.junit.Assert; import org.junit.*;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
@ -36,7 +32,7 @@ import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage; import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage;
import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule; import org.keycloak.testsuite.rule.WebRule;
@ -66,7 +62,7 @@ public class RequiredActionUpdateProfileTest {
protected LoginPage loginPage; protected LoginPage loginPage;
@WebResource @WebResource
protected LoginUpdateProfilePage updateProfilePage; protected LoginUpdateProfileEditUsernameAllowedPage updateProfilePage;
@Before @Before
public void before() { public void before() {
@ -75,6 +71,8 @@ public class RequiredActionUpdateProfileTest {
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm); UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE); user.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
UserModel anotherUser = manager.getSession().users().getUserByEmail("john-doh@localhost", appRealm);
anotherUser.addRequiredAction(UserModel.RequiredAction.UPDATE_PROFILE);
} }
}); });
} }
@ -87,7 +85,7 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "new@email.com"); updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");
String sessionId = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent().getSessionId(); String sessionId = events.expectRequiredAction(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent().getSessionId();
events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent(); events.expectRequiredAction(EventType.UPDATE_PROFILE).session(sessionId).assertEvent();
@ -101,6 +99,41 @@ public class RequiredActionUpdateProfileTest {
Assert.assertEquals("New first", user.getFirstName()); Assert.assertEquals("New first", user.getFirstName());
Assert.assertEquals("New last", user.getLastName()); Assert.assertEquals("New last", user.getLastName());
Assert.assertEquals("new@email.com", user.getEmail()); Assert.assertEquals("new@email.com", user.getEmail());
Assert.assertEquals("test-user@localhost", user.getUsername());
}
@Test
public void updateUsername() {
loginPage.open();
loginPage.login("john-doh@localhost", "password");
String userId = keycloakRule.getUser("test", "john-doh@localhost").getId();
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "john-doh@localhost", "new");
String sessionId = events
.expectLogin()
.event(EventType.UPDATE_PROFILE)
.detail(Details.USERNAME, "john-doh@localhost")
.user(userId)
.session(AssertEvents.isUUID())
.removeDetail(Details.CONSENT)
.assertEvent()
.getSessionId();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().detail(Details.USERNAME, "john-doh@localhost").user(userId).session(sessionId).assertEvent();
// assert user is really updated in persistent store
UserRepresentation user = keycloakRule.getUser("test", "new");
Assert.assertEquals("New first", user.getFirstName());
Assert.assertEquals("New last", user.getLastName());
Assert.assertEquals("john-doh@localhost", user.getEmail());
Assert.assertEquals("new", user.getUsername());
} }
@Test @Test
@ -111,7 +144,7 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.update("", "New last", "new@email.com"); updateProfilePage.update("", "New last", "new@email.com", "new");
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
@ -133,7 +166,7 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "", "new@email.com"); updateProfilePage.update("New first", "", "new@email.com", "new");
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
@ -155,7 +188,7 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", ""); updateProfilePage.update("New first", "New last", "", "new");
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
@ -177,7 +210,7 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "invalidemail"); updateProfilePage.update("New first", "New last", "invalidemail", "invalid");
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
@ -191,6 +224,52 @@ public class RequiredActionUpdateProfileTest {
events.assertEmpty(); events.assertEmpty();
} }
@Test
public void updateProfileMissingUsername() {
loginPage.open();
loginPage.login("john-doh@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "new@email.com", "");
updateProfilePage.assertCurrent();
// assert that form holds submitted values during validation error
Assert.assertEquals("New first", updateProfilePage.getFirstName());
Assert.assertEquals("New last", updateProfilePage.getLastName());
Assert.assertEquals("new@email.com", updateProfilePage.getEmail());
Assert.assertEquals("", updateProfilePage.getUsername());
Assert.assertEquals("Please specify username.", updateProfilePage.getError());
events.assertEmpty();
}
@Test
public void updateProfileDuplicateUsername() {
loginPage.open();
loginPage.login("john-doh@localhost", "password");
updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost");
updateProfilePage.assertCurrent();
// assert that form holds submitted values during validation error
Assert.assertEquals("New first", updateProfilePage.getFirstName());
Assert.assertEquals("New last", updateProfilePage.getLastName());
Assert.assertEquals("new@email.com", updateProfilePage.getEmail());
Assert.assertEquals("test-user@localhost", updateProfilePage.getUsername());
Assert.assertEquals("Username already exists.", updateProfilePage.getError());
events.assertEmpty();
}
@Test @Test
public void updateProfileDuplicatedEmail() { public void updateProfileDuplicatedEmail() {
loginPage.open(); loginPage.open();
@ -199,7 +278,7 @@ public class RequiredActionUpdateProfileTest {
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();
updateProfilePage.update("New first", "New last", "keycloak-user@localhost"); updateProfilePage.update("New first", "New last", "keycloak-user@localhost", "test-user@localhost");
updateProfilePage.assertCurrent(); updateProfilePage.assertCurrent();

View file

@ -17,7 +17,6 @@
*/ */
package org.keycloak.testsuite.broker; package org.keycloak.testsuite.broker;
import org.codehaus.jackson.map.ObjectMapper;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;

View file

@ -20,6 +20,7 @@ public class LDAPTestConfiguration {
private static final Logger log = Logger.getLogger(LDAPTestConfiguration.class); private static final Logger log = Logger.getLogger(LDAPTestConfiguration.class);
private String connectionPropertiesLocation; private String connectionPropertiesLocation;
private int sleepTime;
private boolean startEmbeddedLdapLerver = true; private boolean startEmbeddedLdapLerver = true;
private Map<String, String> config; private Map<String, String> config;
@ -109,6 +110,7 @@ public class LDAPTestConfiguration {
} }
startEmbeddedLdapLerver = Boolean.parseBoolean(p.getProperty("idm.test.ldap.start.embedded.ldap.server", "true")); startEmbeddedLdapLerver = Boolean.parseBoolean(p.getProperty("idm.test.ldap.start.embedded.ldap.server", "true"));
sleepTime = Integer.parseInt(p.getProperty("idm.test.ldap.sleepTime", "1000"));
log.info("Start embedded server: " + startEmbeddedLdapLerver); log.info("Start embedded server: " + startEmbeddedLdapLerver);
log.info("Read config: " + config); log.info("Read config: " + config);
} }
@ -125,4 +127,8 @@ public class LDAPTestConfiguration {
return startEmbeddedLdapLerver; return startEmbeddedLdapLerver;
} }
public int getSleepTime() {
return sleepTime;
}
} }

View file

@ -56,7 +56,6 @@ public class SyncProvidersTest {
Map<String,String> ldapConfig = ldapRule.getConfig(); Map<String,String> ldapConfig = ldapRule.getConfig();
ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "false"); ldapConfig.put(LDAPConstants.SYNC_REGISTRATIONS, "false");
ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString()); ldapConfig.put(LDAPConstants.EDIT_MODE, UserFederationProvider.EditMode.WRITABLE.toString());
ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap", ldapModel = appRealm.addUserFederationProvider(LDAPFederationProviderFactory.PROVIDER_NAME, ldapConfig, 0, "test-ldap",
-1, -1, 0); -1, -1, 0);
@ -91,7 +90,7 @@ public class SyncProvidersTest {
UsersSyncManager usersSyncManager = new UsersSyncManager(); UsersSyncManager usersSyncManager = new UsersSyncManager();
// wait a bit // wait a bit
sleep(1000); sleep(ldapRule.getSleepTime());
KeycloakSession session = keycloakRule.startSession(); KeycloakSession session = keycloakRule.startSession();
try { try {
@ -125,7 +124,7 @@ public class SyncProvidersTest {
} }
// wait a bit // wait a bit
sleep(1000); sleep(ldapRule.getSleepTime());
// Add user to LDAP and update 'user5' in LDAP // Add user to LDAP and update 'user5' in LDAP
LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel);
@ -391,9 +390,9 @@ public class SyncProvidersTest {
} }
private void assertSyncEquals(UserFederationSyncResult syncResult, int expectedAdded, int expectedUpdated, int expectedRemoved, int expectedFailed) { private void assertSyncEquals(UserFederationSyncResult syncResult, int expectedAdded, int expectedUpdated, int expectedRemoved, int expectedFailed) {
Assert.assertEquals(syncResult.getAdded(), expectedAdded); Assert.assertEquals(expectedAdded, syncResult.getAdded());
Assert.assertEquals(syncResult.getUpdated(), expectedUpdated); Assert.assertEquals(expectedUpdated, syncResult.getUpdated());
Assert.assertEquals(syncResult.getRemoved(), expectedRemoved); Assert.assertEquals(expectedRemoved, syncResult.getRemoved());
Assert.assertEquals(syncResult.getFailed(), expectedFailed); Assert.assertEquals(expectedFailed, syncResult.getFailed());
} }
} }

View file

@ -1,6 +1,7 @@
package org.keycloak.testsuite.model; package org.keycloak.testsuite.model;
import java.util.List; import java.util.List;
import java.util.Set;
import org.junit.Assert; import org.junit.Assert;
import org.junit.ClassRule; import org.junit.ClassRule;
@ -89,4 +90,51 @@ public class CacheTest {
} }
} }
// KEYCLOAK-1842
@Test
public void testRoleMappingsInvalidatedWhenClientRemoved() {
KeycloakSession session = kc.startSession();
try {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().addUser(realm, "joel");
ClientModel client = realm.addClient("foo");
RoleModel fooRole = client.addRole("foo-role");
user.grantRole(fooRole);
} finally {
session.getTransaction().commit();
session.close();
}
// Remove client
session = kc.startSession();
int grantedRolesCount;
try {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername("joel", realm);
grantedRolesCount = user.getRoleMappings().size();
ClientModel client = realm.getClientByClientId("foo");
realm.removeClient(client.getId());
} finally {
session.getTransaction().commit();
session.close();
}
// Assert role mappings was removed from user as well
session = kc.startSession();
try {
RealmModel realm = session.realms().getRealmByName("test");
UserModel user = session.users().getUserByUsername("joel", realm);
Set<RoleModel> roles = user.getRoleMappings();
for (RoleModel role : roles) {
Assert.assertNotNull(role.getContainer());
}
Assert.assertEquals(roles.size(), grantedRolesCount - 1);
} finally {
session.getTransaction().commit();
session.close();
}
}
} }

View file

@ -326,6 +326,8 @@ public class ImportTest extends AbstractModelTest {
// Test service accounts // Test service accounts
Assert.assertFalse(application.isServiceAccountsEnabled()); Assert.assertFalse(application.isServiceAccountsEnabled());
Assert.assertTrue(otherApp.isServiceAccountsEnabled()); Assert.assertTrue(otherApp.isServiceAccountsEnabled());
Assert.assertFalse(application.isOfflineTokensEnabled());
Assert.assertTrue(otherApp.isOfflineTokensEnabled());
Assert.assertNull(session.users().getUserByServiceAccountClient(application)); Assert.assertNull(session.users().getUserByServiceAccountClient(application));
UserModel linked = session.users().getUserByServiceAccountClient(otherApp); UserModel linked = session.users().getUserByServiceAccountClient(otherApp);
Assert.assertNotNull(linked); Assert.assertNotNull(linked);

View file

@ -4,6 +4,8 @@ import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserModel.RequiredAction;
@ -283,6 +285,59 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertNull(session.users().getUserByUsername("user1", realm)); Assert.assertNull(session.users().getUserByUsername("user1", realm));
} }
@Test
public void testOfflineSessionsRemoved() {
RealmModel realm = realmManager.createRealm("original");
ClientModel fooClient = realm.addClient("foo");
ClientModel barClient = realm.addClient("bar");
UserModel user1 = session.users().addUser(realm, "user1");
addOfflineUserSession(user1, "123", "something1");
addOfflineClientSession(user1, "456", "123", fooClient.getId(), "something2");
addOfflineClientSession(user1, "789", "123", barClient.getId(), "something3");
commit();
realm = realmManager.getRealmByName("original");
realm.removeClient(barClient.getId());
commit();
realm = realmManager.getRealmByName("original");
user1 = session.users().getUserByUsername("user1", realm);
Assert.assertEquals("something1", user1.getOfflineUserSession("123").getData());
Assert.assertEquals("something2", user1.getOfflineClientSession("456").getData());
Assert.assertNull(user1.getOfflineClientSession("789"));
realm.removeClient(fooClient.getId());
commit();
realm = realmManager.getRealmByName("original");
user1 = session.users().getUserByUsername("user1", realm);
Assert.assertNull(user1.getOfflineClientSession("456"));
Assert.assertNull(user1.getOfflineClientSession("789"));
Assert.assertNull(user1.getOfflineUserSession("123"));
Assert.assertEquals(0, user1.getOfflineUserSessions().size());
Assert.assertEquals(0, user1.getOfflineClientSessions().size());
}
private void addOfflineUserSession(UserModel user, String userSessionId, String data) {
OfflineUserSessionModel model = new OfflineUserSessionModel();
model.setUserSessionId(userSessionId);
model.setData(data);
user.addOfflineUserSession(model);
}
private void addOfflineClientSession(UserModel user, String clientSessionId, String userSessionId, String clientId, String data) {
OfflineClientSessionModel model = new OfflineClientSessionModel();
model.setClientSessionId(clientSessionId);
model.setUserSessionId(userSessionId);
model.setClientId(clientId);
model.setData(data);
user.addOfflineClientSession(model);
}
public static void assertEquals(UserModel expected, UserModel actual) { public static void assertEquals(UserModel expected, UserModel actual) {
Assert.assertEquals(expected.getUsername(), actual.getUsername()); Assert.assertEquals(expected.getUsername(), actual.getUsername());
Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp()); Assert.assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());

View file

@ -180,7 +180,11 @@ public class AccessTokenTest {
Assert.assertEquals("invalid_grant", response.getError()); Assert.assertEquals("invalid_grant", response.getError());
Assert.assertEquals("Incorrect redirect_uri", response.getErrorDescription()); Assert.assertEquals("Incorrect redirect_uri", response.getErrorDescription());
events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).assertEvent(); events.expectCodeToToken(codeId, loginEvent.getSessionId()).error("invalid_code")
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_TYPE)
.assertEvent();
} }
@Test @Test
@ -201,7 +205,13 @@ public class AccessTokenTest {
assertNull(tokenResponse.getAccessToken()); assertNull(tokenResponse.getAccessToken());
assertNull(tokenResponse.getRefreshToken()); assertNull(tokenResponse.getRefreshToken());
events.expectCodeToToken(codeId, sessionId).removeDetail(Details.TOKEN_ID).user((String) null).session((String) null).removeDetail(Details.REFRESH_TOKEN_ID).error(Errors.INVALID_CODE).assertEvent(); events.expectCodeToToken(codeId, sessionId)
.removeDetail(Details.TOKEN_ID)
.user((String) null)
.session((String) null)
.removeDetail(Details.REFRESH_TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_TYPE)
.error(Errors.INVALID_CODE).assertEvent();
events.clear(); events.clear();
} }
@ -230,7 +240,11 @@ public class AccessTokenTest {
Assert.assertEquals(400, response.getStatusCode()); Assert.assertEquals(400, response.getStatusCode());
AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null); AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null);
expectedEvent.error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).user((String) null); expectedEvent.error("invalid_code")
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_TYPE)
.user((String) null);
expectedEvent.assertEvent(); expectedEvent.assertEvent();
events.clear(); events.clear();
@ -264,7 +278,11 @@ public class AccessTokenTest {
Assert.assertEquals(400, response.getStatusCode()); Assert.assertEquals(400, response.getStatusCode());
AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null); AssertEvents.ExpectedEvent expectedEvent = events.expectCodeToToken(codeId, null);
expectedEvent.error("invalid_code").removeDetail(Details.TOKEN_ID).removeDetail(Details.REFRESH_TOKEN_ID).user((String) null); expectedEvent.error("invalid_code")
.removeDetail(Details.TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_ID)
.removeDetail(Details.REFRESH_TOKEN_TYPE)
.user((String) null);
expectedEvent.assertEvent(); expectedEvent.assertEvent();
events.clear(); events.clear();

View file

@ -0,0 +1,441 @@
package org.keycloak.testsuite.oauth;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.UriBuilder;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.Event;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.pages.AccountApplicationsPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.util.JsonSerialization;
import org.keycloak.util.RefreshTokenUtil;
import org.keycloak.util.Time;
import org.keycloak.util.UriUtils;
import org.openqa.selenium.WebDriver;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OfflineTokenTest {
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
// For testing
appRealm.setAccessTokenLifespan(10);
appRealm.setSsoSessionIdleTimeout(30);
ClientModel app = new ClientManager(manager).createClient(appRealm, "offline-client");
app.setSecret("secret1");
String testAppRedirectUri = appRealm.getClientByClientId("test-app").getRedirectUris().iterator().next();
offlineClientAppUri = UriUtils.getOrigin(testAppRedirectUri) + "/offline-client";
app.setRedirectUris(new HashSet<>(Arrays.asList(offlineClientAppUri)));
app.setManagementUrl(offlineClientAppUri);
new ClientManager(manager).enableServiceAccount(app);
UserModel serviceAccountUser = manager.getSession().users().getUserByUsername(ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client", appRealm);
RoleModel customerUserRole = appRealm.getClientByClientId("test-app").getRole("customer-user");
serviceAccountUser.grantRole(customerUserRole);
app.setOfflineTokensEnabled(true);
userId = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm).getId();
URL url = getClass().getResource("/oidc/offline-client-keycloak.json");
keycloakRule.createApplicationDeployment()
.name("offline-client").contextPath("/offline-client")
.servletClass(OfflineTokenServlet.class).adapterConfigPath(url.getPath())
.role("user").deployApplication();
}
});
private static String userId;
private static String offlineClientAppUri;
@Rule
public WebRule webRule = new WebRule(this);
@WebResource
protected WebDriver driver;
@WebResource
protected OAuthClient oauth;
@WebResource
protected LoginPage loginPage;
@WebResource
protected AccountApplicationsPage accountAppPage;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
// @Test
// public void testSleep() throws Exception {
// Thread.sleep(9999000);
// }
@Test
public void offlineTokenDisabledForClient() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.doLogin("test-user@localhost", "password");
Event loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
assertEquals(400, tokenResponse.getStatusCode());
assertEquals("invalid_client", tokenResponse.getError());
events.expectCodeToToken(codeId, sessionId)
.error("invalid_client")
.clearDetails()
.assertEvent();
}
@Test
public void offlineTokenBrowserFlow() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
oauth.redirectUri(offlineClientAppUri);
oauth.doLogin("test-user@localhost", "password");
Event loginEvent = events.expectLogin()
.client("offline-client")
.detail(Details.REDIRECT_URI, offlineClientAppUri)
.assertEvent();
final String sessionId = loginEvent.getSessionId();
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "secret1");
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
String offlineTokenString = tokenResponse.getRefreshToken();
RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
events.expectCodeToToken(codeId, sessionId)
.client("offline-client")
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
.assertEvent();
Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, sessionId, userId);
}
private void testRefreshWithOfflineToken(AccessToken oldToken, RefreshToken offlineToken, String offlineTokenString,
final String sessionId, String userId) {
// Change offset to big value to ensure userSession expired
Time.setOffset(99999);
Assert.assertFalse(oldToken.isActive());
Assert.assertTrue(offlineToken.isActive());
// Assert userSession expired
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
manager.getSession().sessions().removeExpiredUserSessions(appRealm);
}
});
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
Assert.assertNull(manager.getSession().sessions().getUserSession(appRealm, sessionId));
}
});
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(200, response.getStatusCode());
Assert.assertEquals(sessionId, refreshedToken.getSessionState());
// Assert no refreshToken in the response
Assert.assertNull(response.getRefreshToken());
Assert.assertNotEquals(oldToken.getId(), refreshedToken.getId());
Assert.assertEquals(userId, refreshedToken.getSubject());
Assert.assertEquals(1, refreshedToken.getRealmAccess().getRoles().size());
Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
Assert.assertEquals(1, refreshedToken.getResourceAccess("test-app").getRoles().size());
Assert.assertTrue(refreshedToken.getResourceAccess("test-app").isUserInRole("customer-user"));
Event refreshEvent = events.expectRefresh(offlineToken.getId(), sessionId)
.client("offline-client")
.user(userId)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
.assertEvent();
Assert.assertNotEquals(oldToken.getId(), refreshEvent.getDetails().get(Details.TOKEN_ID));
Time.setOffset(0);
}
@Test
public void offlineTokenDirectGrantFlow() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("secret1", "test-user@localhost", "password");
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
String offlineTokenString = tokenResponse.getRefreshToken();
RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
events.expectLogin()
.client("offline-client")
.user(userId)
.session(token.getSessionState())
.detail(Details.RESPONSE_TYPE, "token")
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
.detail(Details.USERNAME, "test-user@localhost")
.removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI)
.removeDetail(Details.CONSENT)
.assertEvent();
Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), userId);
}
@Test
public void offlineTokenServiceAccountFlow() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
String offlineTokenString = tokenResponse.getRefreshToken();
RefreshToken offlineToken = oauth.verifyRefreshToken(offlineTokenString);
String serviceAccountUserId = keycloakRule.getUser("test", ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client").getId();
events.expectClientLogin()
.client("offline-client")
.user(serviceAccountUserId)
.session(token.getSessionState())
.detail(Details.TOKEN_ID, token.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken.getId())
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
.assertEvent();
Assert.assertEquals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE, offlineToken.getType());
Assert.assertEquals(0, offlineToken.getExpiration());
testRefreshWithOfflineToken(token, offlineToken, offlineTokenString, token.getSessionState(), serviceAccountUserId);
// Now retrieve another offline token and verify that previous offline token is not valid anymore
tokenResponse = oauth.doClientCredentialsGrantAccessTokenRequest("secret1");
AccessToken token2 = oauth.verifyToken(tokenResponse.getAccessToken());
String offlineTokenString2 = tokenResponse.getRefreshToken();
RefreshToken offlineToken2 = oauth.verifyRefreshToken(offlineTokenString2);
events.expectClientLogin()
.client("offline-client")
.user(serviceAccountUserId)
.session(token2.getSessionState())
.detail(Details.TOKEN_ID, token2.getId())
.detail(Details.REFRESH_TOKEN_ID, offlineToken2.getId())
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
.detail(Details.USERNAME, ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + "offline-client")
.assertEvent();
// Refresh with old offline token should fail
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(offlineTokenString, "secret1");
Assert.assertEquals(400, response.getStatusCode());
Assert.assertEquals("invalid_grant", response.getError());
events.expectRefresh(offlineToken.getId(), offlineToken.getSessionState())
.error(Errors.INVALID_TOKEN)
.client("offline-client")
.user(serviceAccountUserId)
.removeDetail(Details.UPDATED_REFRESH_TOKEN_ID)
.removeDetail(Details.TOKEN_ID)
.detail(Details.REFRESH_TOKEN_TYPE, RefreshTokenUtil.TOKEN_TYPE_OFFLINE)
.assertEvent();
// Refresh with new offline token is ok
testRefreshWithOfflineToken(token2, offlineToken2, offlineTokenString2, token2.getSessionState(), serviceAccountUserId);
}
@Test
public void testServlet() {
OfflineTokenServlet.tokenInfo = null;
String servletUri = UriBuilder.fromUri(offlineClientAppUri)
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
.build().toString();
driver.navigate().to(servletUri);
loginPage.login("test-user@localhost", "password");
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getExpiration(), 0);
String accessTokenId = OfflineTokenServlet.tokenInfo.accessToken.getId();
String refreshTokenId = OfflineTokenServlet.tokenInfo.refreshToken.getId();
// Assert access token will be refreshed, but offline token will be still the same
Time.setOffset(9999);
driver.navigate().to(offlineClientAppUri);
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getId(), refreshTokenId);
Assert.assertNotEquals(OfflineTokenServlet.tokenInfo.accessToken.getId(), accessTokenId);
// Ensure that logout works for webapp (even if offline token will be still valid in Keycloak DB)
driver.navigate().to(offlineClientAppUri + "/logout");
loginPage.assertCurrent();
driver.navigate().to(offlineClientAppUri);
loginPage.assertCurrent();
Time.setOffset(0);
events.clear();
}
@Test
public void testServletWithRevoke() {
// Login to servlet first with offline token
String servletUri = UriBuilder.fromUri(offlineClientAppUri)
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.OFFLINE_ACCESS)
.build().toString();
driver.navigate().to(servletUri);
loginPage.login("test-user@localhost", "password");
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
Assert.assertEquals(OfflineTokenServlet.tokenInfo.refreshToken.getType(), RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
// Assert refresh works with increased time
Time.setOffset(9999);
driver.navigate().to(offlineClientAppUri);
Assert.assertTrue(driver.getCurrentUrl().startsWith(offlineClientAppUri));
Time.setOffset(0);
events.clear();
// Go to account service and revoke grant
accountAppPage.open();
List<String> additionalGrants = accountAppPage.getApplications().get("offline-client").getAdditionalGrants();
Assert.assertEquals(additionalGrants.size(), 1);
Assert.assertEquals(additionalGrants.get(0), "Offline Access");
accountAppPage.revokeGrant("offline-client");
Assert.assertEquals(accountAppPage.getApplications().get("offline-client").getAdditionalGrants().size(), 0);
events.expect(EventType.REVOKE_GRANT)
.client("account").detail(Details.REVOKED_CLIENT, "offline-client").assertEvent();
// Assert refresh doesn't work now (increase time one more time)
Time.setOffset(9999);
driver.navigate().to(offlineClientAppUri);
Assert.assertFalse(driver.getCurrentUrl().startsWith(offlineClientAppUri));
loginPage.assertCurrent();
Time.setOffset(0);
}
public static class OfflineTokenServlet extends HttpServlet {
private static TokenInfo tokenInfo;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if (req.getRequestURI().endsWith("logout")) {
UriBuilder redirectUriBuilder = UriBuilder.fromUri(offlineClientAppUri);
if (req.getParameter(OAuth2Constants.SCOPE) != null) {
redirectUriBuilder.queryParam(OAuth2Constants.SCOPE, req.getParameter(OAuth2Constants.SCOPE));
}
String redirectUri = redirectUriBuilder.build().toString();
String origin = UriUtils.getOrigin(req.getRequestURL().toString());
String serverLogoutRedirect = UriBuilder.fromUri(origin + "/auth/realms/test/protocol/openid-connect/logout")
.queryParam("redirect_uri", redirectUri)
.build().toString();
resp.sendRedirect(serverLogoutRedirect);
return;
}
StringBuilder response = new StringBuilder("<html><head><title>Offline token servlet</title></head><body><pre>");
RefreshableKeycloakSecurityContext ctx = (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
String accessTokenPretty = JsonSerialization.writeValueAsPrettyString(ctx.getToken());
RefreshToken refreshToken = new JWSInput(ctx.getRefreshToken()).readJsonContent(RefreshToken.class);
String refreshTokenPretty = JsonSerialization.writeValueAsPrettyString(refreshToken);
response = response.append(accessTokenPretty)
.append(refreshTokenPretty)
.append("</pre></body></html>");
resp.getWriter().println(response.toString());
tokenInfo = new TokenInfo(ctx.getToken(), refreshToken);
}
}
private static class TokenInfo {
private final AccessToken accessToken;
private final RefreshToken refreshToken;
public TokenInfo(AccessToken accessToken, RefreshToken refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
}

View file

@ -77,6 +77,15 @@ public class AccountApplicationsPage extends AbstractAccountPage {
currentEntry.addMapper(protMapper); currentEntry.addMapper(protMapper);
} }
break; break;
case 5:
String additionalGrant = col.getText();
if (additionalGrant.isEmpty()) break;
String[] grants = additionalGrant.split(",");
for (String grant : grants) {
grant = grant.trim();
currentEntry.addAdditionalGrant(grant);
}
break;
} }
} }
} }
@ -89,6 +98,7 @@ public class AccountApplicationsPage extends AbstractAccountPage {
private final List<String> rolesAvailable = new ArrayList<String>(); private final List<String> rolesAvailable = new ArrayList<String>();
private final List<String> rolesGranted = new ArrayList<String>(); private final List<String> rolesGranted = new ArrayList<String>();
private final List<String> protocolMappersGranted = new ArrayList<String>(); private final List<String> protocolMappersGranted = new ArrayList<String>();
private final List<String> additionalGrants = new ArrayList<>();
private void addAvailableRole(String role) { private void addAvailableRole(String role) {
rolesAvailable.add(role); rolesAvailable.add(role);
@ -102,6 +112,10 @@ public class AccountApplicationsPage extends AbstractAccountPage {
protocolMappersGranted.add(protocolMapper); protocolMappersGranted.add(protocolMapper);
} }
private void addAdditionalGrant(String grant) {
additionalGrants.add(grant);
}
public List<String> getRolesGranted() { public List<String> getRolesGranted() {
return rolesGranted; return rolesGranted;
} }
@ -113,5 +127,9 @@ public class AccountApplicationsPage extends AbstractAccountPage {
public List<String> getProtocolMappersGranted() { public List<String> getProtocolMappersGranted() {
return protocolMappersGranted; return protocolMappersGranted;
} }
public List<String> getAdditionalGrants() {
return additionalGrants;
}
} }
} }

View file

@ -0,0 +1,51 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2012, Red Hat, Inc., and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.keycloak.testsuite.pages;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class LoginUpdateProfileEditUsernameAllowedPage extends LoginUpdateProfilePage {
@FindBy(id = "username")
private WebElement usernameInput;
public void update(String firstName, String lastName, String email, String username) {
usernameInput.clear();
usernameInput.sendKeys(username);
update(firstName, lastName, email);
}
public String getUsername() {
return usernameInput.getAttribute("value");
}
public boolean isCurrent() {
return driver.getTitle().equals("Update Account Information");
}
@Override
public void open() {
throw new UnsupportedOperationException();
}
}

View file

@ -57,4 +57,8 @@ public class LDAPRule extends ExternalResource {
public Map<String, String> getConfig() { public Map<String, String> getConfig() {
return ldapTestConfiguration.getLDAPConfig(); return ldapTestConfiguration.getLDAPConfig();
} }
public int getSleepTime() {
return ldapTestConfiguration.getSleepTime();
}
} }

View file

@ -164,6 +164,7 @@
"name": "Other Application", "name": "Other Application",
"enabled": true, "enabled": true,
"serviceAccountsEnabled": true, "serviceAccountsEnabled": true,
"offlineTokensEnabled": true,
"clientAuthenticatorType": "client-jwt", "clientAuthenticatorType": "client-jwt",
"protocolMappers" : [ "protocolMappers" : [
{ {

View file

@ -0,0 +1,10 @@
{
"realm": "test",
"resource": "offline-client",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "http://localhost:8081/auth",
"ssl-required" : "external",
"credentials": {
"secret": "secret1"
}
}

View file

@ -5,6 +5,7 @@
"sslRequired": "external", "sslRequired": "external",
"registrationAllowed": true, "registrationAllowed": true,
"resetPasswordAllowed": true, "resetPasswordAllowed": true,
"editUsernameAllowed" : true,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ], "requiredCredentials": [ "password" ],
@ -32,7 +33,23 @@
} }
}, },
{ {
"username" : "keycloak-user@localhost", "username" : "john-doh@localhost",
"enabled": true,
"email" : "john-doh@localhost",
"firstName": "John",
"lastName": "Doh",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"realmRoles": ["user"],
"clientRoles": {
"test-app": [ "customer-user" ],
"account": [ "view-profile", "manage-account" ]
}
},
{
"username" : "keycloak-user@localhost",
"enabled": true, "enabled": true,
"email" : "keycloak-user@localhost", "email" : "keycloak-user@localhost",
"credentials" : [ "credentials" : [