KEYCLOAK-904 Offline tokens
This commit is contained in:
parent
c11539cccb
commit
7ec3f86efb
65 changed files with 2568 additions and 57 deletions
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
62
core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
Normal file
62
core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
|
@ -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...
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" : [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue