PL-8070 Persistent token store for REST API

This commit is contained in:
Maxim Gorbunkov 2017-02-28 17:04:46 +04:00
parent 717b4761bb
commit 1a8f7221a1
21 changed files with 630 additions and 29 deletions

View File

@ -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());

View File

@ -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)^

View File

@ -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(), '-', '')^

View File

@ -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)^

View File

@ -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

View File

@ -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)
)^

View File

@ -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)
)^

View File

@ -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)
)^

View File

@ -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)
)^

View File

@ -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)
)^

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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 * * ?

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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>

View File

@ -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()) ;
}

View File

@ -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).");
}
}
}

View File

@ -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" />