/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.oauth.token;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.audit.event.SystemDeletable;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.util.StringUtils;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import static org.cloudfoundry.identity.uaa.oauth.token.RevocableToken.TokenType.REFRESH_TOKEN;
import static org.springframework.util.StringUtils.isEmpty;
public class JdbcRevocableTokenProvisioning implements RevocableTokenProvisioning, SystemDeletable {
protected JdbcTemplate jdbcTemplate;
private final static String REFRESH_TOKEN_RESPONSE_TYPE = REFRESH_TOKEN.toString();
protected final static String FIELDS = "token_id,client_id,user_id,format,response_type,issued_at,expires_at,scope,data,identity_zone_id";
protected static final String UPDATE_FIELDS = FIELDS.substring(FIELDS.indexOf(',')+1, FIELDS.lastIndexOf(',')).replace(",","=?,") + "=?";
protected final static String TABLE = "revocable_tokens";
protected final static String GET_QUERY = "SELECT " + FIELDS +" FROM "+TABLE + " WHERE token_id=? AND identity_zone_id=?";
protected final static String GET_BY_USER_QUERY = "SELECT " + FIELDS +" FROM "+TABLE + " WHERE user_id=? AND identity_zone_id=?";
protected final static String GET_BY_CLIENT_QUERY = "SELECT " + FIELDS +" FROM "+TABLE + " WHERE client_id=? AND identity_zone_id=?";
protected final static String UPDATE_QUERY = "UPDATE "+TABLE+" SET "+UPDATE_FIELDS+" WHERE token_id=? and identity_zone_id=?";
protected final static String INSERT_QUERY = "INSERT INTO " + TABLE + " ("+FIELDS+") VALUES (?,?,?,?,?,?,?,?,?,?)";
protected final static String DELETE_QUERY = "DELETE FROM " + TABLE + " WHERE token_id=? and identity_zone_id=?";
protected final static String DELETE_EXPIRED_QUERY = "DELETE FROM " + TABLE + " WHERE expires_at < ?";
protected final static String DELETE_REFRESH_TOKEN_QUERY = "DELETE FROM " + TABLE + " WHERE user_id=? AND client_id=? AND response_type='" +REFRESH_TOKEN_RESPONSE_TYPE+ "' AND identity_zone_id=?";
protected final static String DELETE_BY_CLIENT_QUERY = "DELETE FROM " + TABLE + " WHERE client_id = ? AND identity_zone_id=?";
protected final static String DELETE_BY_USER_QUERY = "DELETE FROM " + TABLE + " WHERE user_id = ? AND identity_zone_id=?";
protected final static String DELETE_BY_ZONE_QUERY = "DELETE FROM " + TABLE + " WHERE identity_zone_id=?";
protected static final Log logger = LogFactory.getLog(JdbcRevocableTokenProvisioning.class);
protected final RowMapper<RevocableToken> rowMapper;
protected final JdbcTemplate template;
protected AtomicLong lastExpiredCheck = new AtomicLong(0);
protected long expirationCheckInterval = 30000; //30 seconds
protected JdbcRevocableTokenProvisioning(JdbcTemplate jdbcTemplate) {
this.rowMapper = new RevocableTokenRowMapper();
this.template = jdbcTemplate;
}
@Override
public List<RevocableToken> retrieveAll() {
return null;
}
public RevocableToken retrieve(String id, boolean checkExpired) {
if (checkExpired) {
checkExpired();
}
RevocableToken result = template.queryForObject(GET_QUERY, rowMapper, id, IdentityZoneHolder.get().getId());
if (checkExpired && result.getExpiresAt() < System.currentTimeMillis()) {
delete(id, 0);
throw new EmptyResultDataAccessException("Token expired.", 1);
}
return result;
}
@Override
public RevocableToken retrieve(String id) {
return retrieve(id, true);
}
@Override
public int deleteRefreshTokensForClientAndUserId(String clientId, String userId) {
String zoneId = IdentityZoneHolder.get().getId();
int deleted_rows = template.update(DELETE_REFRESH_TOKEN_QUERY, userId, clientId, zoneId);
return deleted_rows;
}
@Override
public RevocableToken create(RevocableToken t) {
checkExpired();
String zoneId = IdentityZoneHolder.get().getId();
template.update(INSERT_QUERY,
t.getTokenId(),
t.getClientId(),
t.getUserId(),
t.getFormat(),
t.getResponseType().toString(),
t.getIssuedAt(),
t.getExpiresAt(),
t.getScope(),
t.getValue(),
zoneId);
return retrieve(t.getTokenId(), false);
}
@Override
public RevocableToken update(String id, RevocableToken t) {
String zoneId = IdentityZoneHolder.get().getId();
template.update(UPDATE_QUERY,
t.getClientId(),
t.getUserId(),
t.getFormat(),
t.getResponseType().toString(),
t.getIssuedAt(),
t.getExpiresAt(),
t.getScope(),
t.getValue(),
id,
zoneId);
return retrieve(id, false);
}
@Override
public RevocableToken delete(String id, int version) {
RevocableToken previous = retrieve(id, false);
template.update(DELETE_QUERY, id, IdentityZoneHolder.get().getId());
return previous;
}
@Override
public int deleteByIdentityZone(String zoneId) {
return template.update(DELETE_BY_ZONE_QUERY, IdentityZoneHolder.get().getId());
}
@Override
public int deleteByOrigin(String origin, String zoneId) {
return 0;
}
@Override
public int deleteByClient(String clientId, String zoneId) {
return template.update(DELETE_BY_CLIENT_QUERY, clientId, zoneId);
}
@Override
public int deleteByUser(String userId, String zoneId) {
return template.update(DELETE_BY_USER_QUERY, userId, zoneId);
}
@Override
public Log getLogger() {
return logger;
}
@Override
public List<RevocableToken> getUserTokens(String userId) {
return template.query(GET_BY_USER_QUERY, rowMapper, userId, IdentityZoneHolder.get().getId());
}
@Override
public List<RevocableToken> getUserTokens(String userId, String clientId) {
if (isEmpty(clientId)) {
throw new NullPointerException("Client ID can not be null when retrieving tokens.");
}
return getUserTokens(userId).stream().filter(r -> clientId.equals(r.getClientId())).collect(Collectors.toList());
}
@Override
public List<RevocableToken> getClientTokens(String clientId) {
return template.query(GET_BY_CLIENT_QUERY, rowMapper, clientId, IdentityZoneHolder.get().getId());
}
public long getExpirationCheckInterval() {
return expirationCheckInterval;
}
public void setExpirationCheckInterval(long expirationCheckInterval) {
this.expirationCheckInterval = expirationCheckInterval;
}
public void checkExpired() {
long now = System.currentTimeMillis();
long lastCheck = lastExpiredCheck.get();
if ((now - lastCheck) > getExpirationCheckInterval() && lastExpiredCheck.compareAndSet(lastCheck, now)) {
int removed = template.update(DELETE_EXPIRED_QUERY, now);
logger.info("Removed " + removed + " expired revocable tokens.");
}
}
protected static final class RevocableTokenRowMapper implements RowMapper<RevocableToken> {
@Override
public RevocableToken mapRow(ResultSet rs, int rowNum) throws SQLException {
int pos = 1;
RevocableToken revocableToken = new RevocableToken();
revocableToken.setTokenId(rs.getString(pos++));
revocableToken.setClientId(rs.getString(pos++));
revocableToken.setUserId(rs.getString(pos++));
revocableToken.setFormat(rs.getString(pos++));
String responseType = rs.getString(pos++);
if(StringUtils.hasText(responseType)) {
revocableToken.setResponseType(RevocableToken.TokenType.valueOf(responseType));
}
revocableToken.setIssuedAt(rs.getLong(pos++));
revocableToken.setExpiresAt(rs.getLong(pos++));
revocableToken.setScope(rs.getString(pos++));
revocableToken.setValue(rs.getString(pos++));
revocableToken.setZoneId(rs.getString(pos++));
return revocableToken;
}
}
}