mirror of
https://gitee.com/jmix/cuba.git
synced 2024-12-02 19:27:57 +08:00
PL-8070 Persistent token store for REST API
This commit is contained in:
parent
717b4761bb
commit
1a8f7221a1
@ -812,6 +812,22 @@ create table SEC_REMEMBER_ME (
|
||||
|
||||
------------------------------------------------------------------------------------------------------------
|
||||
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID varchar(36) not null,
|
||||
CREATE_TS timestamp,
|
||||
CREATED_BY varchar(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar(255),
|
||||
ACCESS_TOKEN_BYTES longvarbinary,
|
||||
AUTHENTICATION_KEY varchar(255),
|
||||
AUTHENTICATION_BYTES longvarbinary,
|
||||
EXPIRY timestamp,
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
||||
|
||||
------------------------------------------------------------------------------------------------------------
|
||||
|
||||
create function NEWID() returns varchar(36)
|
||||
return uuid(uuid());
|
||||
|
||||
|
@ -873,6 +873,22 @@ create index IDX_SEC_REMEMBER_ME_TOKEN on SEC_REMEMBER_ME(TOKEN)^
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID uniqueidentifier not null,
|
||||
CREATE_TS datetime,
|
||||
CREATED_BY varchar(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar(255),
|
||||
ACCESS_TOKEN_BYTES image,
|
||||
AUTHENTICATION_KEY varchar(255),
|
||||
AUTHENTICATION_BYTES image,
|
||||
EXPIRY datetime,
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
||||
|
||||
------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
insert into SEC_GROUP (ID, CREATE_TS, VERSION, NAME, PARENT_ID)
|
||||
values ('0fa2b1a5-1d68-4d69-9fbd-dff348347f93', current_timestamp, 0, 'Company', null)^
|
||||
|
||||
|
@ -875,6 +875,22 @@ create index IDX_SEC_REMEMBER_ME_TOKEN on SEC_REMEMBER_ME(TOKEN)^
|
||||
|
||||
/**********************************************************************************************/
|
||||
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID varchar(32) not null,
|
||||
CREATE_TS datetime(3),
|
||||
CREATED_BY varchar(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar(255),
|
||||
ACCESS_TOKEN_BYTES longblob,
|
||||
AUTHENTICATION_KEY varchar(255),
|
||||
AUTHENTICATION_BYTES longblob,
|
||||
EXPIRY datetime(3),
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
||||
|
||||
/**********************************************************************************************/
|
||||
|
||||
drop function if exists newid^
|
||||
create function newid() returns varchar(32) not deterministic
|
||||
return replace(uuid(), '-', '')^
|
||||
|
@ -657,6 +657,20 @@ create table SEC_REMEMBER_ME (
|
||||
create index IDX_SEC_REMEMBER_ME_USER on SEC_REMEMBER_ME(USER_ID)^
|
||||
create index IDX_SEC_REMEMBER_ME_TOKEN on SEC_REMEMBER_ME(TOKEN)^
|
||||
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID varchar2(32) not null,
|
||||
CREATE_TS timestamp,
|
||||
CREATED_BY varchar2(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar2(255),
|
||||
ACCESS_TOKEN_BYTES blob,
|
||||
AUTHENTICATION_KEY varchar2(255),
|
||||
AUTHENTICATION_BYTES blob,
|
||||
EXPIRY timestamp,
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
||||
|
||||
alter table SYS_APP_FOLDER add constraint FK_SYS_APP_FOLDER_FOLDER foreign key (FOLDER_ID) references SYS_FOLDER(ID)^
|
||||
|
||||
alter table SYS_ATTR_VALUE add constraint SYS_ATTR_VALUE_CATEGORY_ATT_ID foreign key (CATEGORY_ATTR_ID) references SYS_CATEGORY_ATTR(ID)^
|
||||
|
@ -844,6 +844,22 @@ create index IDX_SEC_REMEMBER_ME_TOKEN on SEC_REMEMBER_ME(TOKEN)^
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID uuid not null,
|
||||
CREATE_TS timestamp,
|
||||
CREATED_BY varchar(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar(255),
|
||||
ACCESS_TOKEN_BYTES bytea,
|
||||
AUTHENTICATION_KEY varchar(255),
|
||||
AUTHENTICATION_BYTES bytea,
|
||||
EXPIRY timestamp,
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
||||
|
||||
--------------------------------------------------------------------------------------------------------------
|
||||
|
||||
-- This function is designed only for convenient generation of UUID identifiers in database creation and updating scripts.
|
||||
-- Do not use it in application. At runtime, all UUID identifiers are generated by the middleware.
|
||||
create or replace function newid() returns uuid
|
||||
|
@ -0,0 +1,13 @@
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID varchar(36) not null,
|
||||
CREATE_TS timestamp,
|
||||
CREATED_BY varchar(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar(255),
|
||||
ACCESS_TOKEN_BYTES longvarbinary,
|
||||
AUTHENTICATION_KEY varchar(255),
|
||||
AUTHENTICATION_BYTES longvarbinary,
|
||||
EXPIRY timestamp,
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
@ -0,0 +1,13 @@
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID uniqueidentifier not null,
|
||||
CREATE_TS datetime,
|
||||
CREATED_BY varchar(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar(255),
|
||||
ACCESS_TOKEN_BYTES image,
|
||||
AUTHENTICATION_KEY varchar(255),
|
||||
AUTHENTICATION_BYTES image,
|
||||
EXPIRY datetime,
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
@ -0,0 +1,13 @@
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID varchar(32) not null,
|
||||
CREATE_TS datetime(3),
|
||||
CREATED_BY varchar(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar(255),
|
||||
ACCESS_TOKEN_BYTES longblob,
|
||||
AUTHENTICATION_KEY varchar(255),
|
||||
AUTHENTICATION_BYTES longblob,
|
||||
EXPIRY datetime(3),
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
@ -0,0 +1,13 @@
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID varchar2(32) not null,
|
||||
CREATE_TS timestamp,
|
||||
CREATED_BY varchar2(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar2(255),
|
||||
ACCESS_TOKEN_BYTES blob,
|
||||
AUTHENTICATION_KEY varchar2(255),
|
||||
AUTHENTICATION_BYTES blob,
|
||||
EXPIRY timestamp,
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
@ -0,0 +1,13 @@
|
||||
create table SYS_REST_API_TOKEN (
|
||||
ID uuid not null,
|
||||
CREATE_TS timestamp,
|
||||
CREATED_BY varchar(50),
|
||||
--
|
||||
ACCESS_TOKEN_VALUE varchar(255),
|
||||
ACCESS_TOKEN_BYTES bytea,
|
||||
AUTHENTICATION_KEY varchar(255),
|
||||
AUTHENTICATION_BYTES bytea,
|
||||
EXPIRY timestamp,
|
||||
--
|
||||
primary key (ID)
|
||||
)^
|
@ -239,4 +239,13 @@ public interface ServerConfig extends Config {
|
||||
@Source(type = SourceType.DATABASE)
|
||||
@DefaultBoolean(true)
|
||||
boolean getUseReadOnlyTransactionForLoad();
|
||||
|
||||
/**
|
||||
* @return whether to store REST API OAuth tokens in the database
|
||||
*/
|
||||
@Property("cuba.rest.storeTokensInDb")
|
||||
@Source(type = SourceType.DATABASE)
|
||||
@DefaultBoolean(false)
|
||||
boolean getRestStoreTokensInDb();
|
||||
|
||||
}
|
@ -16,9 +16,17 @@
|
||||
|
||||
package com.haulmont.cuba.restapi;
|
||||
|
||||
import com.haulmont.cuba.core.EntityManager;
|
||||
import com.haulmont.cuba.core.Persistence;
|
||||
import com.haulmont.cuba.core.Transaction;
|
||||
import com.haulmont.cuba.core.app.ClusterListener;
|
||||
import com.haulmont.cuba.core.app.ClusterListenerAdapter;
|
||||
import com.haulmont.cuba.core.app.ClusterManagerAPI;
|
||||
import com.haulmont.cuba.core.app.ServerConfig;
|
||||
import com.haulmont.cuba.core.entity.RestApiToken;
|
||||
import com.haulmont.cuba.core.global.Metadata;
|
||||
import com.haulmont.cuba.core.global.TimeSource;
|
||||
import com.haulmont.cuba.core.global.View;
|
||||
import com.haulmont.cuba.security.global.NoUserSessionException;
|
||||
import com.haulmont.cuba.security.global.UserSession;
|
||||
import com.haulmont.cuba.security.sys.UserSessionManager;
|
||||
@ -26,11 +34,16 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.inject.Inject;
|
||||
import java.io.*;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.DelayQueue;
|
||||
import java.util.concurrent.Delayed;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
@ -45,6 +58,18 @@ public class ServerTokenStoreImpl implements ServerTokenStore {
|
||||
@Inject
|
||||
protected ClusterManagerAPI clusterManagerAPI;
|
||||
|
||||
@Inject
|
||||
protected ServerConfig serverConfig;
|
||||
|
||||
@Inject
|
||||
protected Persistence persistence;
|
||||
|
||||
@Inject
|
||||
protected Metadata metadata;
|
||||
|
||||
@Inject
|
||||
protected TimeSource timeSource;
|
||||
|
||||
protected Logger log = LoggerFactory.getLogger(ServerTokenStoreImpl.class);
|
||||
|
||||
protected ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
@ -55,6 +80,8 @@ public class ServerTokenStoreImpl implements ServerTokenStore {
|
||||
private ConcurrentHashMap<String, UUID> tokenValueToSessionIdStore = new ConcurrentHashMap<>();
|
||||
private ConcurrentHashMap<String, String> tokenValueToAuthenticationKeyStore = new ConcurrentHashMap<>();
|
||||
|
||||
private final DelayQueue<TokenExpiry> expiryQueue = new DelayQueue<>();
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
initClusterListeners();
|
||||
@ -64,10 +91,11 @@ public class ServerTokenStoreImpl implements ServerTokenStore {
|
||||
clusterManagerAPI.addListener(TokenStoreAddTokenMsg.class, new ClusterListener<TokenStoreAddTokenMsg>() {
|
||||
@Override
|
||||
public void receive(TokenStoreAddTokenMsg message) {
|
||||
_storeAccessToken(message.getTokenValue(),
|
||||
storeAccessTokenToMemory(message.getTokenValue(),
|
||||
message.getAccessTokenBytes(),
|
||||
message.getAuthenticationKey(),
|
||||
message.getAuthenticationBytes());
|
||||
message.getAuthenticationBytes(),
|
||||
message.getTokenExpiry());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -103,6 +131,7 @@ public class ServerTokenStoreImpl implements ServerTokenStore {
|
||||
}
|
||||
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(state);
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
ObjectInputStream ois = new ObjectInputStream(bis);
|
||||
tokenValueToAccessTokenStore = (ConcurrentHashMap<String, byte[]>) ois.readObject();
|
||||
@ -112,8 +141,9 @@ public class ServerTokenStoreImpl implements ServerTokenStore {
|
||||
tokenValueToAuthenticationKeyStore = (ConcurrentHashMap<String, String>) ois.readObject();
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log.error("Error receiving state", e);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
@ -127,39 +157,161 @@ public class ServerTokenStoreImpl implements ServerTokenStore {
|
||||
clusterManagerAPI.addListener(TokenStoreRemoveTokenMsg.class, new ClusterListenerAdapter<TokenStoreRemoveTokenMsg>() {
|
||||
@Override
|
||||
public void receive(TokenStoreRemoveTokenMsg message) {
|
||||
_removeAccessToken(message.getTokenValue());
|
||||
removeAccessTokenFromMemory(message.getTokenValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getAccessTokenByAuthentication(String authenticationKey) {
|
||||
byte[] accessTokenBytes;
|
||||
accessTokenBytes = getAccessTokenByAuthenticationFromMemory(authenticationKey);
|
||||
if (accessTokenBytes == null && serverConfig.getRestStoreTokensInDb()) {
|
||||
RestApiToken restApiToken = getRestApiTokenByAuthenticationKeyFromDatabase(authenticationKey);
|
||||
if (restApiToken != null) {
|
||||
accessTokenBytes = restApiToken.getAccessTokenBytes();
|
||||
restoreInMemoryTokenData(restApiToken);
|
||||
}
|
||||
}
|
||||
return accessTokenBytes;
|
||||
}
|
||||
|
||||
protected byte[] getAccessTokenByAuthenticationFromMemory(String authenticationKey) {
|
||||
return authenticationToAccessTokenStore.get(authenticationKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeAccessToken(String tokenValue, byte[] accessTokenBytes, String authenticationKey, byte[] authenticationBytes) {
|
||||
_storeAccessToken(tokenValue, accessTokenBytes, authenticationKey, authenticationBytes);
|
||||
clusterManagerAPI.send(new TokenStoreAddTokenMsg(tokenValue, accessTokenBytes, authenticationKey, authenticationBytes));
|
||||
public void storeAccessToken(String tokenValue,
|
||||
byte[] accessTokenBytes,
|
||||
String authenticationKey,
|
||||
byte[] authenticationBytes,
|
||||
Date tokenExpiry) {
|
||||
storeAccessTokenToMemory(tokenValue, accessTokenBytes, authenticationKey, authenticationBytes, tokenExpiry);
|
||||
if (serverConfig.getRestStoreTokensInDb()) {
|
||||
removeAccessTokenFromDatabase(tokenValue);
|
||||
storeAccessTokenToDatabase(tokenValue, accessTokenBytes, authenticationKey, authenticationBytes, tokenExpiry);
|
||||
}
|
||||
clusterManagerAPI.send(new TokenStoreAddTokenMsg(tokenValue, accessTokenBytes, authenticationKey, authenticationBytes, tokenExpiry));
|
||||
}
|
||||
|
||||
protected void _storeAccessToken(String tokenValue, byte[] accessTokenBytes, String authenticationKey, byte[] authenticationBytes) {
|
||||
tokenValueToAccessTokenStore.put(tokenValue, accessTokenBytes);
|
||||
authenticationToAccessTokenStore.put(authenticationKey, accessTokenBytes);
|
||||
tokenValueToAuthenticationStore.put(tokenValue, authenticationBytes);
|
||||
tokenValueToAuthenticationKeyStore.put(tokenValue, authenticationKey);
|
||||
protected void storeAccessTokenToMemory(String tokenValue,
|
||||
byte[] accessTokenBytes,
|
||||
String authenticationKey,
|
||||
byte[] authenticationBytes,
|
||||
Date tokenExpiry) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
tokenValueToAccessTokenStore.put(tokenValue, accessTokenBytes);
|
||||
authenticationToAccessTokenStore.put(authenticationKey, accessTokenBytes);
|
||||
tokenValueToAuthenticationStore.put(tokenValue, authenticationBytes);
|
||||
tokenValueToAuthenticationKeyStore.put(tokenValue, authenticationKey);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
|
||||
if (tokenExpiry != null) {
|
||||
TokenExpiry expiry = new TokenExpiry(tokenValue, tokenExpiry);
|
||||
this.expiryQueue.put(expiry);
|
||||
}
|
||||
}
|
||||
|
||||
protected void storeAccessTokenToDatabase(String tokenValue,
|
||||
byte[] accessTokenBytes,
|
||||
String authenticationKey,
|
||||
byte[] authenticationBytes,
|
||||
Date tokenExpiry) {
|
||||
try (Transaction tx = persistence.createTransaction()) {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
RestApiToken restApiToken = metadata.create(RestApiToken.class);
|
||||
restApiToken.setAccessTokenValue(tokenValue);
|
||||
restApiToken.setAccessTokenBytes(accessTokenBytes);
|
||||
restApiToken.setAuthenticationKey(authenticationKey);
|
||||
restApiToken.setAuthenticationBytes(authenticationBytes);
|
||||
restApiToken.setExpiry(tokenExpiry);
|
||||
em.persist(restApiToken);
|
||||
tx.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getAccessTokenByTokenValue(String tokenValue) {
|
||||
public byte[] getAccessTokenByTokenValue(String accessTokenValue) {
|
||||
byte[] accessTokenBytes;
|
||||
accessTokenBytes = getAccessTokenByTokenValueFromMemory(accessTokenValue);
|
||||
if (accessTokenBytes == null && serverConfig.getRestStoreTokensInDb()) {
|
||||
RestApiToken restApiToken = getRestApiTokenByTokenValueFromDatabase(accessTokenValue);
|
||||
if (restApiToken != null) {
|
||||
accessTokenBytes = restApiToken.getAccessTokenBytes();
|
||||
restoreInMemoryTokenData(restApiToken);
|
||||
}
|
||||
}
|
||||
return accessTokenBytes;
|
||||
}
|
||||
|
||||
protected byte[] getAccessTokenByTokenValueFromMemory(String tokenValue) {
|
||||
return tokenValueToAccessTokenStore.get(tokenValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getAuthenticationByTokenValue(String tokenValue) {
|
||||
byte[] authenticationBytes;
|
||||
authenticationBytes = getAuthenticationByTokenValueFromMemory(tokenValue);
|
||||
if (authenticationBytes == null && serverConfig.getRestStoreTokensInDb()) {
|
||||
RestApiToken restApiToken = getRestApiTokenByTokenValueFromDatabase(tokenValue);
|
||||
if (restApiToken != null) {
|
||||
authenticationBytes = restApiToken.getAuthenticationBytes();
|
||||
restoreInMemoryTokenData(restApiToken);
|
||||
}
|
||||
}
|
||||
return authenticationBytes;
|
||||
}
|
||||
|
||||
protected byte[] getAuthenticationByTokenValueFromMemory(String tokenValue) {
|
||||
return tokenValueToAuthenticationStore.get(tokenValue);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected RestApiToken getRestApiTokenByTokenValueFromDatabase(String accessTokenValue) {
|
||||
RestApiToken restApiToken;
|
||||
try (Transaction tx = persistence.createTransaction()) {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
restApiToken = em.createQuery("select e from sys$RestApiToken e where e.accessTokenValue = :accessTokenValue", RestApiToken.class)
|
||||
.setParameter("accessTokenValue", accessTokenValue)
|
||||
.setViewName(View.LOCAL)
|
||||
.getFirstResult();
|
||||
tx.commit();
|
||||
return restApiToken;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected RestApiToken getRestApiTokenByAuthenticationKeyFromDatabase(String authenticationKey) {
|
||||
RestApiToken restApiToken;
|
||||
try (Transaction tx = persistence.createTransaction()) {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
restApiToken = em.createQuery("select e from sys$RestApiToken e where e.authenticationKey = :authenticationKey", RestApiToken.class)
|
||||
.setParameter("authenticationKey", authenticationKey)
|
||||
.setViewName(View.LOCAL)
|
||||
.getFirstResult();
|
||||
tx.commit();
|
||||
return restApiToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method fills in-memory maps from the {@link RestApiToken} object got from the database
|
||||
*/
|
||||
protected void restoreInMemoryTokenData(RestApiToken restApiToken) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
tokenValueToAccessTokenStore.put(restApiToken.getAccessTokenValue(), restApiToken.getAccessTokenBytes());
|
||||
authenticationToAccessTokenStore.put(restApiToken.getAuthenticationKey(), restApiToken.getAccessTokenBytes());
|
||||
tokenValueToAuthenticationStore.put(restApiToken.getAccessTokenValue(), restApiToken.getAuthenticationBytes());
|
||||
tokenValueToAuthenticationKeyStore.put(restApiToken.getAccessTokenValue(), restApiToken.getAuthenticationKey());
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getSessionIdByTokenValue(String tokenValue) {
|
||||
return tokenValueToSessionIdStore.get(tokenValue);
|
||||
@ -173,26 +325,104 @@ public class ServerTokenStoreImpl implements ServerTokenStore {
|
||||
}
|
||||
|
||||
protected UUID _putSessionId(String tokenValue, UUID sessionId) {
|
||||
return tokenValueToSessionIdStore.put(tokenValue, sessionId);
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
return tokenValueToSessionIdStore.put(tokenValue, sessionId);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAccessToken(String tokenValue) {
|
||||
_removeAccessToken(tokenValue);
|
||||
removeAccessTokenFromMemory(tokenValue);
|
||||
if (serverConfig.getRestStoreTokensInDb()) {
|
||||
removeAccessTokenFromDatabase(tokenValue);
|
||||
}
|
||||
clusterManagerAPI.send(new TokenStoreRemoveTokenMsg(tokenValue));
|
||||
}
|
||||
|
||||
protected void _removeAccessToken(String tokenValue) {
|
||||
tokenValueToAccessTokenStore.remove(tokenValue);
|
||||
tokenValueToAuthenticationStore.remove(tokenValue);
|
||||
String authenticationKey = tokenValueToAuthenticationKeyStore.remove(tokenValue);
|
||||
authenticationToAccessTokenStore.remove(authenticationKey);
|
||||
UUID sessionId = tokenValueToSessionIdStore.remove(tokenValue);
|
||||
protected void removeAccessTokenFromMemory(String tokenValue) {
|
||||
UUID sessionId;
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
tokenValueToAccessTokenStore.remove(tokenValue);
|
||||
tokenValueToAuthenticationStore.remove(tokenValue);
|
||||
String authenticationKey = tokenValueToAuthenticationKeyStore.remove(tokenValue);
|
||||
authenticationToAccessTokenStore.remove(authenticationKey);
|
||||
sessionId = tokenValueToSessionIdStore.remove(tokenValue);
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
if (sessionId != null) {
|
||||
try {
|
||||
UserSession session = userSessionManager.getSession(sessionId);
|
||||
userSessionManager.removeSession(session);
|
||||
} catch (NoUserSessionException ignored) {}
|
||||
} catch (NoUserSessionException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void removeAccessTokenFromDatabase(String accessTokenValue) {
|
||||
try (Transaction tx = persistence.createTransaction()) {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
em.createQuery("delete from sys$RestApiToken t where t.accessTokenValue = :accessTokenValue")
|
||||
.setParameter("accessTokenValue", accessTokenValue)
|
||||
.executeUpdate();
|
||||
tx.commit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteExpiredTokens() {
|
||||
deleteExpiredTokensInMemory();
|
||||
if (serverConfig.getRestStoreTokensInDb() && clusterManagerAPI.isMaster()) {
|
||||
deleteExpiredTokensInDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
protected void deleteExpiredTokensInMemory() {
|
||||
TokenExpiry expiry = expiryQueue.poll();
|
||||
while (expiry != null) {
|
||||
removeAccessToken(expiry.getValue());
|
||||
expiry = expiryQueue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
protected void deleteExpiredTokensInDatabase() {
|
||||
try (Transaction tx = persistence.createTransaction()) {
|
||||
EntityManager em = persistence.getEntityManager();
|
||||
em.createQuery("delete from sys$RestApiToken t where t.expiry < CURRENT_TIMESTAMP")
|
||||
.executeUpdate();
|
||||
tx.commit();
|
||||
}
|
||||
}
|
||||
|
||||
protected static class TokenExpiry implements Delayed {
|
||||
|
||||
private final long expiry;
|
||||
|
||||
private final String value;
|
||||
|
||||
public TokenExpiry(String value, Date date) {
|
||||
this.value = value;
|
||||
this.expiry = date.getTime();
|
||||
}
|
||||
|
||||
public int compareTo(Delayed other) {
|
||||
if (this == other) {
|
||||
return 0;
|
||||
}
|
||||
long diff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS);
|
||||
return (diff == 0 ? 0 : ((diff < 0) ? -1 : 1));
|
||||
}
|
||||
|
||||
public long getDelay(TimeUnit unit) {
|
||||
return expiry - System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
package com.haulmont.cuba.restapi;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Cluster message containing an information about the REST API token to be stored
|
||||
@ -26,12 +27,18 @@ public class TokenStoreAddTokenMsg implements Serializable {
|
||||
protected byte[] accessTokenBytes;
|
||||
protected String authenticationKey;
|
||||
protected byte[] authenticationBytes;
|
||||
protected Date tokenExpiry;
|
||||
|
||||
public TokenStoreAddTokenMsg(String tokenValue, byte[] accessTokenBytes, String authenticationKey, byte[] authenticationBytes) {
|
||||
public TokenStoreAddTokenMsg(String tokenValue,
|
||||
byte[] accessTokenBytes,
|
||||
String authenticationKey,
|
||||
byte[] authenticationBytes,
|
||||
Date tokenExpiry) {
|
||||
this.tokenValue = tokenValue;
|
||||
this.accessTokenBytes = accessTokenBytes;
|
||||
this.authenticationKey = authenticationKey;
|
||||
this.authenticationBytes = authenticationBytes;
|
||||
this.tokenExpiry = tokenExpiry;
|
||||
}
|
||||
|
||||
public String getTokenValue() {
|
||||
@ -49,4 +56,8 @@ public class TokenStoreAddTokenMsg implements Serializable {
|
||||
public byte[] getAuthenticationBytes() {
|
||||
return authenticationBytes;
|
||||
}
|
||||
|
||||
public Date getTokenExpiry() {
|
||||
return tokenExpiry;
|
||||
}
|
||||
}
|
||||
|
@ -111,4 +111,11 @@ cuba.cluster.enabled=false
|
||||
cuba.cluster.jgroupsConfig=jgroups.xml
|
||||
|
||||
#pretty time property paths
|
||||
cuba.prettyTimeProperties=com/haulmont/cuba/core/app/prettytime/prettytime.properties
|
||||
cuba.prettyTimeProperties=com/haulmont/cuba/core/app/prettytime/prettytime.properties
|
||||
|
||||
###############################################################################
|
||||
# REST API #
|
||||
###############################################################################
|
||||
|
||||
# Cron expression for deleting expired tokens in the REST API token store
|
||||
cuba.rest.deleteExpiredTokensCron=0 0 3 * * ?
|
@ -196,6 +196,7 @@
|
||||
<task:scheduled ref="cuba_TriggerFilesProcessor" method="process"
|
||||
fixed-delay="${cuba.triggerFilesCheckInterval?:5000}"/>
|
||||
<task:scheduled ref="cuba_MiddlewareStatisticsAccumulator" method="gatherParameters" fixed-rate="5000"/>
|
||||
<task:scheduled ref="cuba_ServerTokenStore" method="deleteExpiredTokens" cron="${cuba.rest.deleteExpiredTokensCron}"/>
|
||||
<!--<task:scheduled ref="cuba_QueryResultsManager" method="deleteForInactiveSessions" fixed-rate="600000"/>-->
|
||||
</task:scheduled-tasks>
|
||||
|
||||
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2008-2017 Haulmont.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.haulmont.cuba.core.entity;
|
||||
|
||||
import com.haulmont.cuba.core.entity.annotation.SystemLevel;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Table;
|
||||
import javax.persistence.Entity;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
*/
|
||||
@Entity(name = "sys$RestApiToken")
|
||||
@Table(name = "SYS_REST_API_TOKEN")
|
||||
@SystemLevel
|
||||
public class RestApiToken extends BaseUuidEntity implements Creatable{
|
||||
|
||||
@Column(name = "CREATE_TS")
|
||||
protected Date createTs;
|
||||
|
||||
@Column(name = "CREATED_BY", length = 50)
|
||||
protected String createdBy;
|
||||
|
||||
@Column(name = "ACCESS_TOKEN_VALUE")
|
||||
protected String accessTokenValue;
|
||||
|
||||
@Column(name = "ACCESS_TOKEN_BYTES")
|
||||
protected byte[] accessTokenBytes;
|
||||
|
||||
@Column(name = "AUTHENTICATION_KEY")
|
||||
protected String authenticationKey;
|
||||
|
||||
@Column(name = "AUTHENTICATION_BYTES")
|
||||
protected byte[] authenticationBytes;
|
||||
|
||||
@Column(name = "EXPIRY")
|
||||
protected Date expiry;
|
||||
|
||||
public String getAccessTokenValue() {
|
||||
return accessTokenValue;
|
||||
}
|
||||
|
||||
public void setAccessTokenValue(String accessTokenValue) {
|
||||
this.accessTokenValue = accessTokenValue;
|
||||
}
|
||||
|
||||
public byte[] getAccessTokenBytes() {
|
||||
return accessTokenBytes;
|
||||
}
|
||||
|
||||
public void setAccessTokenBytes(byte[] accessTokenBytes) {
|
||||
this.accessTokenBytes = accessTokenBytes;
|
||||
}
|
||||
|
||||
public String getAuthenticationKey() {
|
||||
return authenticationKey;
|
||||
}
|
||||
|
||||
public void setAuthenticationKey(String authenticationKey) {
|
||||
this.authenticationKey = authenticationKey;
|
||||
}
|
||||
|
||||
public byte[] getAuthenticationBytes() {
|
||||
return authenticationBytes;
|
||||
}
|
||||
|
||||
public void setAuthenticationBytes(byte[] authenticationBytes) {
|
||||
this.authenticationBytes = authenticationBytes;
|
||||
}
|
||||
|
||||
public Date getExpiry() {
|
||||
return expiry;
|
||||
}
|
||||
|
||||
public void setExpiry(Date expiry) {
|
||||
this.expiry = expiry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getCreateTs() {
|
||||
return createTs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreateTs(Date createTs) {
|
||||
this.createTs = createTs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCreatedBy(String createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
|
||||
package com.haulmont.cuba.restapi;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@ -27,7 +28,7 @@ public interface ServerTokenStore {
|
||||
|
||||
byte[] getAccessTokenByAuthentication(String authenticationKey);
|
||||
|
||||
void storeAccessToken(String tokenValue, byte[] accessTokenBytes, String authenticationKey, byte[] authenticationBytes);
|
||||
void storeAccessToken(String tokenValue, byte[] accessTokenBytes, String authenticationKey, byte[] authenticationBytes, Date tokenExpiry);
|
||||
|
||||
byte[] getAccessTokenByTokenValue(String tokenValue);
|
||||
|
||||
@ -38,4 +39,6 @@ public interface ServerTokenStore {
|
||||
UUID putSessionId(String authenticationKey, UUID sessionId);
|
||||
|
||||
void removeAccessToken(String tokenValue);
|
||||
|
||||
void deleteExpiredTokens();
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
<class>com.haulmont.cuba.core.entity.ScheduledTask</class>
|
||||
<class>com.haulmont.cuba.core.entity.ScheduledExecution</class>
|
||||
<class>com.haulmont.cuba.core.entity.QueryResult</class>
|
||||
<class>com.haulmont.cuba.core.entity.RestApiToken</class>
|
||||
|
||||
<class>com.haulmont.cuba.security.entity.User</class>
|
||||
<class>com.haulmont.cuba.security.entity.Role</class>
|
||||
|
@ -33,7 +33,6 @@ import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
|
||||
import org.springframework.security.oauth2.common.util.SerializationUtils;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
|
||||
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
|
||||
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -59,10 +58,14 @@ public class ClientProxyTokenStore implements TokenStore {
|
||||
@Inject
|
||||
protected LoginService loginService;
|
||||
|
||||
protected AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
|
||||
protected AuthenticationKeyGenerator authenticationKeyGenerator;
|
||||
|
||||
private Logger log = LoggerFactory.getLogger(ClientProxyTokenStore.class);
|
||||
|
||||
public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
|
||||
this.authenticationKeyGenerator = authenticationKeyGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
|
||||
return readAuthentication(token.getValue());
|
||||
@ -84,7 +87,8 @@ public class ClientProxyTokenStore implements TokenStore {
|
||||
serverTokenStore.storeAccessToken(token.getValue(),
|
||||
serializeAccessToken(token),
|
||||
authenticationKey,
|
||||
serializeAuthentication(authentication));
|
||||
serializeAuthentication(authentication),
|
||||
token.getExpiration());
|
||||
processSession(authentication, token.getValue());
|
||||
log.info("REST API access token stored: [{}] {}", authentication.getPrincipal(), token.getValue()) ;
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2008-2017 Haulmont.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.haulmont.restapi.auth;
|
||||
|
||||
import org.springframework.security.oauth2.common.util.OAuth2Utils;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Request;
|
||||
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Key generator that uses random UUID for authentication keys. This makes token endpoint return unique access token on
|
||||
* each token request
|
||||
*/
|
||||
public class UniqueAuthenticationKeyGenerator implements AuthenticationKeyGenerator {
|
||||
|
||||
private static final String CLIENT_ID = "client_id";
|
||||
|
||||
private static final String SCOPE = "scope";
|
||||
|
||||
private static final String USERNAME = "username";
|
||||
|
||||
private static final String UUID_KEY = "uuid";
|
||||
|
||||
public String extractKey(OAuth2Authentication authentication) {
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
OAuth2Request authorizationRequest = authentication.getOAuth2Request();
|
||||
if (!authentication.isClientOnly()) {
|
||||
values.put(USERNAME, authentication.getName());
|
||||
}
|
||||
values.put(CLIENT_ID, authorizationRequest.getClientId());
|
||||
if (authorizationRequest.getScope() != null) {
|
||||
values.put(SCOPE, OAuth2Utils.formatParameterList(authorizationRequest.getScope()));
|
||||
}
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
values.put(UUID_KEY, uuid);
|
||||
|
||||
MessageDigest digest;
|
||||
try {
|
||||
digest = MessageDigest.getInstance("MD5");
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));
|
||||
return String.format("%032x", new BigInteger(1, bytes));
|
||||
}
|
||||
catch (UnsupportedEncodingException e) {
|
||||
throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK).");
|
||||
}
|
||||
}
|
||||
}
|
@ -93,7 +93,11 @@
|
||||
<property name="clientDetailsService" ref="clientDetailsService"/>
|
||||
</bean>
|
||||
|
||||
<bean id="tokenStore" class="com.haulmont.restapi.auth.ClientProxyTokenStore"/>
|
||||
<bean id="tokenStore" class="com.haulmont.restapi.auth.ClientProxyTokenStore">
|
||||
<property name="authenticationKeyGenerator">
|
||||
<bean class="com.haulmont.restapi.auth.UniqueAuthenticationKeyGenerator"/>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<oauth2:resource-server id="resourceFilter" token-services-ref="tokenServices" />
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user