diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2cdda6a..9e6c856 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,14 @@ Find changes for the upcoming release in the project's [changelog.d](https://git
+
+## 2.4.5 (2024-07-24)
+
+### Changed
+
+- Introduce RubinIdentityManagerImpl, extends IdentityManager which replaces the deprecated Authenticator
+- Added deprecated AuthenticatorImpl, this is only useful in case this version of TAP is used with the old Auth params/implementations (Unlikely)
+
## 2.4.4 (2024-07-23)
diff --git a/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java b/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java
new file mode 100644
index 0000000..22374fa
--- /dev/null
+++ b/src/main/java/org/opencadc/tap/impl/AuthenticatorImpl.java
@@ -0,0 +1,152 @@
+package org.opencadc.tap.impl;
+
+import java.io.IOException;
+import java.net.http.HttpClient;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpRequest;
+import java.net.URI;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import ca.nrc.cadc.auth.Authenticator;
+import ca.nrc.cadc.auth.AuthMethod;
+import ca.nrc.cadc.auth.AuthorizationTokenPrincipal;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+import org.apache.log4j.Logger;
+
+/**
+ * @deprecated This class is deprecated and will be removed in future releases.
+ * The TAP Service now uses IdentityManager for authentication, available in the opencadc library
+ *
+ * @author cbanek
+ */
+@Deprecated
+public class AuthenticatorImpl implements Authenticator
+{
+ private static final Logger log = Logger.getLogger(AuthenticatorImpl.class);
+
+ // Size of the token cache is read from the maxTokenCache property, with
+ // a default of 1000 tokens cached.
+ private static final int maxTokenCache = Integer.getInteger("maxTokenCache", 1000);
+
+ private static final String gafaelfawr_url = System.getProperty("gafaelfawr_url");
+
+ private static final HttpClient client = HttpClient.newHttpClient();
+
+ private static final ConcurrentHashMap tokenCache = new ConcurrentHashMap<>();
+
+ private final class TokenInfo
+ {
+ public final String username;
+ public final int uid;
+
+ public TokenInfo(String username, int uid)
+ {
+ this.username = username;
+ this.uid = uid;
+ }
+ }
+
+ public AuthenticatorImpl()
+ {
+ }
+
+ public Subject validate(Subject subject) throws AccessControlException {
+ log.debug("Subject to augment starts as: " + subject);
+
+ // Check if the cache is too big, and if so, clear it out.
+ if (tokenCache.size() > maxTokenCache) {
+ tokenCache.clear();
+ }
+
+ List addedPrincipals = new ArrayList();
+ AuthorizationTokenPrincipal tokenPrincipal = null;
+
+ for (Principal principal : subject.getPrincipals()) {
+ if (principal instanceof AuthorizationTokenPrincipal) {
+ tokenPrincipal = (AuthorizationTokenPrincipal) principal;
+ TokenInfo tokenInfo = null;
+
+ for (int i = 1; i < 5 && tokenInfo == null; i++) {
+ try {
+ tokenInfo = getTokenInfo(tokenPrincipal.getHeaderValue());
+ } catch (IOException|InterruptedException e) {
+ log.warn("Exception thrown while getting info from Gafaelfawr");
+ log.warn(e);
+ }
+ }
+
+ if (tokenInfo != null) {
+ X500Principal xp = new X500Principal("CN=" + tokenInfo.username);
+ addedPrincipals.add(xp);
+
+ HttpPrincipal hp = new HttpPrincipal(tokenInfo.username);
+ addedPrincipals.add(hp);
+
+ UUID uuid = new UUID(0L, (long) tokenInfo.uid);
+ NumericPrincipal np = new NumericPrincipal(uuid);
+ addedPrincipals.add(np);
+ }
+ else {
+ log.error("Gave up retrying user-info requests to Gafaelfawr");
+ }
+ }
+ }
+
+ if (tokenPrincipal != null) {
+ subject.getPrincipals().remove(tokenPrincipal);
+ }
+
+ subject.getPrincipals().addAll(addedPrincipals);
+ subject.getPublicCredentials().add(AuthMethod.TOKEN);
+
+ log.debug("Augmented subject is " + subject);
+ return subject;
+ }
+
+ // Here we could check the token again, but gafaelfawr should be
+ // doing that for us already by the time it gets to us. So for
+ // this layer, we just let this go through.
+ public Subject augment(Subject subject) {
+ return subject;
+ }
+
+ private TokenInfo getTokenInfo(String token) throws IOException, InterruptedException {
+ // If the request has gotten this far, the token has already
+ // been checked upstream, so we know it's valid, we just need
+ // to determine the uid and the username.
+ if (!tokenCache.containsKey(token)) {
+ HttpRequest request = HttpRequest.newBuilder(URI.create(gafaelfawr_url))
+ .header("Accept", "application/json")
+ .header("Authorization", token)
+ .build();
+
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ String body = response.body();
+
+ Gson gson = new Gson();
+ JsonObject authData = gson.fromJson(body, JsonObject.class);
+ String username = authData.getAsJsonPrimitive("username").getAsString();
+ int uid = authData.getAsJsonPrimitive("uid").getAsInt();
+
+ // Insert the info into the cache here since we retrieved it.
+ tokenCache.put(token, new TokenInfo(username, uid));
+ }
+
+ return tokenCache.get(token);
+ }
+}
diff --git a/src/main/java/org/opencadc/tap/impl/RubinIdentityManagerImpl.java b/src/main/java/org/opencadc/tap/impl/RubinIdentityManagerImpl.java
new file mode 100644
index 0000000..1210a81
--- /dev/null
+++ b/src/main/java/org/opencadc/tap/impl/RubinIdentityManagerImpl.java
@@ -0,0 +1,238 @@
+package org.opencadc.tap.impl;
+
+import java.io.IOException;
+import java.net.http.HttpClient;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.net.http.HttpRequest;
+import java.net.URI;
+import java.security.AccessControlException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.Collections;
+
+import javax.security.auth.Subject;
+import javax.security.auth.x500.X500Principal;
+
+import ca.nrc.cadc.auth.AuthMethod;
+import ca.nrc.cadc.auth.AuthenticationUtil;
+import ca.nrc.cadc.auth.Authenticator;
+import ca.nrc.cadc.auth.AuthMethod;
+import ca.nrc.cadc.auth.IdentityManager;
+import ca.nrc.cadc.reg.Standards;
+
+import ca.nrc.cadc.auth.AuthorizationTokenPrincipal;
+import ca.nrc.cadc.auth.HttpPrincipal;
+import ca.nrc.cadc.auth.NumericPrincipal;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import org.apache.log4j.Logger;
+
+/**
+ * Implementes the Authenticator for processing Gafaelfawr auth,
+ * and using it to authenticate against the TAP service.
+ *
+ * The token in the authorization header is used to make a call
+ * to Gafaelfawr to retrieve details such as the uid and uidNumber.
+ *
+ * @author cbanek
+ * @author stvoutsin
+ */
+public class RubinIdentityManagerImpl implements IdentityManager
+{
+ private static final Logger log = Logger.getLogger(RubinIdentityManagerImpl.class);
+
+ // Size of the token cache is read from the maxTokenCache property, with
+ // a default of 1000 tokens cached.
+ private static final int maxTokenCache = Integer.getInteger("maxTokenCache", 1000);
+
+ private static final String gafaelfawr_url = System.getProperty("gafaelfawr_url");
+
+ private static final HttpClient client = HttpClient.newHttpClient();
+
+ private static final ConcurrentHashMap tokenCache = new ConcurrentHashMap<>();
+
+ private final class TokenInfo
+ {
+ public final String username;
+ public final int uid;
+
+ public TokenInfo(String username, int uid)
+ {
+ this.username = username;
+ this.uid = uid;
+ }
+ }
+ @Override
+ public Set getSecurityMethods() {
+ return SEC_METHODS;
+ }
+ private static final Set SEC_METHODS;
+
+ static {
+ Set tmp = new TreeSet<>();
+ tmp.add(Standards.SECURITY_METHOD_ANON);
+ tmp.add(Standards.SECURITY_METHOD_TOKEN);
+ SEC_METHODS = Collections.unmodifiableSet(tmp);
+ }
+
+ public RubinIdentityManagerImpl()
+ {
+ }
+
+ public Subject validate(Subject subject) throws AccessControlException {
+ log.debug("Subject to augment starts as: " + subject);
+
+ // Check if the cache is too big, and if so, clear it out.
+ if (tokenCache.size() > maxTokenCache) {
+ tokenCache.clear();
+ }
+
+ List addedPrincipals = new ArrayList();
+ AuthorizationTokenPrincipal tokenPrincipal = null;
+
+ for (Principal principal : subject.getPrincipals()) {
+ if (principal instanceof AuthorizationTokenPrincipal) {
+ tokenPrincipal = (AuthorizationTokenPrincipal) principal;
+ TokenInfo tokenInfo = null;
+
+ for (int i = 1; i < 5 && tokenInfo == null; i++) {
+ try {
+ tokenInfo = getTokenInfo(tokenPrincipal.getHeaderValue());
+ } catch (IOException|InterruptedException e) {
+ log.warn("Exception thrown while getting info from Gafaelfawr");
+ log.warn(e);
+ }
+ }
+
+ if (tokenInfo != null) {
+ X500Principal xp = new X500Principal("CN=" + tokenInfo.username);
+ addedPrincipals.add(xp);
+
+ HttpPrincipal hp = new HttpPrincipal(tokenInfo.username);
+ addedPrincipals.add(hp);
+
+ UUID uuid = new UUID(0L, (long) tokenInfo.uid);
+ NumericPrincipal np = new NumericPrincipal(uuid);
+ addedPrincipals.add(np);
+ }
+ else {
+ log.error("Gave up retrying user-info requests to Gafaelfawr");
+ }
+ }
+ }
+
+ if (tokenPrincipal != null) {
+ subject.getPrincipals().remove(tokenPrincipal);
+ }
+
+ subject.getPrincipals().addAll(addedPrincipals);
+ subject.getPublicCredentials().add(AuthMethod.TOKEN);
+
+ log.debug("Augmented subject is " + subject);
+ return subject;
+ }
+
+ // Here we could check the token again, but gafaelfawr should be
+ // doing that for us already by the time it gets to us. So for
+ // this layer, we just let this go through.
+ public Subject augment(Subject subject) {
+ return subject;
+ }
+
+ private TokenInfo getTokenInfo(String token) throws IOException, InterruptedException {
+ // If the request has gotten this far, the token has already
+ // been checked upstream, so we know it's valid, we just need
+ // to determine the uid and the username.
+ if (!tokenCache.containsKey(token)) {
+ HttpRequest request = HttpRequest.newBuilder(URI.create(gafaelfawr_url))
+ .header("Accept", "application/json")
+ .header("Authorization", token)
+ .build();
+
+ HttpResponse response = client.send(request, BodyHandlers.ofString());
+ String body = response.body();
+
+ Gson gson = new Gson();
+ JsonObject authData = gson.fromJson(body, JsonObject.class);
+ String username = authData.getAsJsonPrimitive("username").getAsString();
+ int uid = authData.getAsJsonPrimitive("uid").getAsInt();
+
+ // Insert the info into the cache here since we retrieved it.
+ tokenCache.put(token, new TokenInfo(username, uid));
+ }
+
+ return tokenCache.get(token);
+ }
+
+
+ @Override
+ public Subject toSubject(Object owner) {
+ Subject ret = new Subject();
+ if (owner != null) {
+ UUID uuid = null;
+ if (owner instanceof UUID) {
+ uuid = (UUID) owner;
+ } else if (owner instanceof String) {
+ String sub = (String) owner;
+ uuid = UUID.fromString(sub);
+ } else {
+ throw new RuntimeException("unexpected owner type: " + owner.getClass().getName() + " value: " + owner);
+ }
+ NumericPrincipal p = new NumericPrincipal(uuid);
+
+ // effectively augment by using the current subject as a "cache" of known identities
+ Subject s = AuthenticationUtil.getCurrentSubject();
+ if (s != null) {
+ for (Principal cp : s.getPrincipals()) {
+ if (AuthenticationUtil.equals(p, cp)) {
+ log.debug("[cache hit] caller Subject matches " + p + ": " + s);
+ ret.getPrincipals().addAll(s.getPrincipals());
+ return ret;
+ }
+ }
+ }
+
+ ret.getPrincipals().add(p);
+ // this is sufficient for some purposes, but not for output using toDisplayString (eg vospace node owner)
+ // TODO: use PosixMapperClient.augment() to try to add a PosixPrincipal and infer an HttpPrincipal?
+ }
+ return ret;
+ }
+
+ @Override
+ public Object toOwner(Subject subject) {
+ // use NumericPrincipal aka OIDC sub for persistence
+ Set ps = subject.getPrincipals(NumericPrincipal.class);
+ if (ps.isEmpty()) {
+ return null;
+ }
+ return ps.iterator().next().getUUID().toString();
+ }
+
+ @Override
+ public String toDisplayString(Subject subject) {
+ if (subject != null) {
+ // prefer HttpPrincipal aka OIDC preferred_username for string output, eg logging
+ Set ps = subject.getPrincipals(HttpPrincipal.class);
+ if (!ps.isEmpty()) {
+ return ps.iterator().next().getName(); // kind of ugh
+ }
+
+ // default
+ Set ps2 = subject.getPrincipals();
+ if (!ps2.isEmpty()) {
+ return ps2.iterator().next().getName();
+ }
+ }
+
+ return null;
+ }
+}