001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.rest.server.auth.oauth; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.security.*; 022import java.time.*; 023import java.util.*; 024 025import org.apache.juneau.rest.auth.oauth.flow.*; 026 027/** 028 * Thread-safe bounded LRU implementation of {@link TokenCache}. 029 * 030 * <p> 031 * Backed by a {@link LinkedHashMap} configured with {@code accessOrder=true} so iteration order tracks 032 * most-recently-used. Eviction policy is the conjunction of: 033 * 034 * <ul> 035 * <li><b>Size cap</b> — when the entry count would exceed {@code maxEntries}, the least-recently-used 036 * entry is removed before the new entry is inserted. Default {@code maxEntries=1000}. 037 * <li><b>TTL eviction</b> — on lookup, an entry whose stored expiry instant has passed (relative to 038 * the supplied {@code now} clock) is removed and treated as a miss. 039 * </ul> 040 * 041 * @since 10.0.0 042 */ 043public class BoundedLruTokenCache implements TokenCache { 044 045 /** Default maximum entry count. */ 046 public static final int DEFAULT_MAX_ENTRIES = 1000; 047 048 /** 049 * Static creator with the default max-entries. 050 * 051 * @return A new cache. 052 */ 053 public static BoundedLruTokenCache create() { 054 return new BoundedLruTokenCache(DEFAULT_MAX_ENTRIES); 055 } 056 057 /** 058 * Static creator with a custom max-entries. 059 * 060 * @param maxEntries The maximum entry count. Must be positive. 061 * @return A new cache. 062 */ 063 public static BoundedLruTokenCache create(int maxEntries) { 064 if (maxEntries <= 0) 065 throw iaex("maxEntries must be positive (was %s)", maxEntries); 066 return new BoundedLruTokenCache(maxEntries); 067 } 068 069 private final int maxEntries; 070 private final Map<String,Object> entries; 071 private final Object lock = new Object(); 072 073 /** 074 * Constructor. 075 * 076 * @param maxEntries The maximum entry count. 077 */ 078 protected BoundedLruTokenCache(int maxEntries) { 079 this.maxEntries = maxEntries; 080 this.entries = new LinkedHashMap<>(16, 0.75f, true) { 081 private static final long serialVersionUID = 1L; 082 @Override 083 protected boolean removeEldestEntry(Map.Entry<String,Object> eldest) { 084 return size() > BoundedLruTokenCache.this.maxEntries; 085 } 086 }; 087 } 088 089 /** 090 * Returns the configured maximum entry count. 091 * 092 * @return The maximum entry count. 093 */ 094 public int getMaxEntries() { 095 return maxEntries; 096 } 097 098 @Override /* Overridden from TokenCache */ 099 public void putPrincipal(String key, Principal principal, Duration ttl) { 100 reqnb("key", key); 101 reqnn("principal", principal); 102 reqnn("ttl", ttl); 103 if (ttl.isZero() || ttl.isNegative()) 104 throw iaex("ttl must be positive (was %s)", ttl); 105 var expiresAt = Instant.now().plus(ttl); 106 synchronized (lock) { 107 entries.put(key, new CachedPrincipal(principal, expiresAt)); 108 } 109 } 110 111 @Override /* Overridden from TokenCache */ 112 public Optional<Principal> getPrincipal(String key) { 113 reqnb("key", key); 114 synchronized (lock) { 115 var v = entries.get(key); 116 if (!(v instanceof CachedPrincipal v2)) 117 return oe(); 118 if (!Instant.now().isBefore(v2.expiresAt())) { 119 entries.remove(key); 120 return oe(); 121 } 122 return o(v2.principal()); 123 } 124 } 125 126 @Override /* Overridden from TokenCache */ 127 public void putToken(String key, OAuthToken token) { 128 reqnb("key", key); 129 reqnn("token", token); 130 synchronized (lock) { 131 entries.put(key, token); 132 } 133 } 134 135 @Override /* Overridden from TokenCache */ 136 public Optional<OAuthToken> getToken(String key, Instant now, Duration skew) { 137 reqnb("key", key); 138 reqnn("now", now); 139 reqnn("skew", skew); 140 if (skew.isNegative()) 141 throw iaex("skew must be non-negative (was %s)", skew); 142 synchronized (lock) { 143 var v = entries.get(key); 144 if (!(v instanceof OAuthToken v2)) 145 return oe(); 146 if (v2.isExpired(now, skew)) { 147 entries.remove(key); 148 return oe(); 149 } 150 return o(v2); 151 } 152 } 153 154 @Override /* Overridden from TokenCache */ 155 public void invalidate(String key) { 156 reqnb("key", key); 157 synchronized (lock) { 158 entries.remove(key); 159 } 160 } 161 162 @Override /* Overridden from TokenCache */ 163 public int size() { 164 synchronized (lock) { 165 return entries.size(); 166 } 167 } 168}