Skip to content

Commit 94fda7f

Browse files
authored
ORCID OpenID Connect login (#258)
* Extracted abstract `AuthorizeBase` base class * Google-specific `IDTokenFilter` * ORCID-specific `IDTokenFilter` * Extracted common OAuth `LoginBase` class * JWT verification call * OAuth refactoring Proper JWT token verification * Fixed HTML reloading after OAuth login * Extracted `JWTVerifier` class It's reused by `IDTokenFilterBase` as well as `LoginBase` * Fixed signup? * Replaced relative URL check with `isSameSite` * Cleaned up unused imports * Ban agent and user account query requests
1 parent 19d06a3 commit 94fda7f

20 files changed

Lines changed: 1895 additions & 1163 deletions

File tree

src/main/java/com/atomgraph/linkeddatahub/Application.java

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,7 @@
100100
import com.atomgraph.linkeddatahub.server.factory.ServiceFactory;
101101
import com.atomgraph.linkeddatahub.server.filter.request.OntologyFilter;
102102
import com.atomgraph.linkeddatahub.server.filter.request.AuthorizationFilter;
103-
import com.atomgraph.linkeddatahub.server.filter.request.auth.IDTokenFilter;
104103
import com.atomgraph.linkeddatahub.server.filter.request.ContentLengthLimitFilter;
105-
import com.atomgraph.linkeddatahub.server.filter.request.auth.ORCIDTokenFilter;
106104
import com.atomgraph.linkeddatahub.server.filter.request.auth.ProxiedWebIDFilter;
107105
import com.atomgraph.linkeddatahub.server.filter.response.CORSFilter;
108106
import com.atomgraph.linkeddatahub.server.filter.response.ResponseHeadersFilter;
@@ -286,6 +284,7 @@ public class Application extends ResourceConfig
286284
private final List<Locale> supportedLanguages;
287285
private final ExpiringMap<URI, Model> webIDmodelCache = ExpiringMap.builder().expiration(1, TimeUnit.DAYS).build(); // TO-DO: config for the expiration period?
288286
private final ExpiringMap<String, Model> oidcModelCache = ExpiringMap.builder().variableExpiration().build();
287+
private final ExpiringMap<String, jakarta.json.JsonObject> jwksCache = ExpiringMap.builder().expiration(1, TimeUnit.DAYS).build(); // Cache JWKS responses
289288
private final Map<URI, XsltExecutable> xsltExecutableCache = new ConcurrentHashMap<>();
290289
private final MessageDigest messageDigest;
291290
private final boolean enableWebIDSignUp;
@@ -1002,11 +1001,24 @@ protected void configure()
10021001
protected void registerResourceClasses()
10031002
{
10041003
register(Dispatcher.class);
1005-
// OAuth endpoints - system-level resources not tied to dataspaces
1006-
register(com.atomgraph.linkeddatahub.resource.oauth2.google.Authorize.class);
1007-
register(com.atomgraph.linkeddatahub.resource.oauth2.google.Login.class);
1008-
// register(com.atomgraph.linkeddatahub.resource.admin.oauth2.orcid.Authorize.class);
1009-
// register(com.atomgraph.linkeddatahub.resource.admin.oauth2.orcid.Login.class);
1004+
1005+
// Conditionally register Google OAuth endpoints if configured
1006+
if (getProperty(com.atomgraph.linkeddatahub.vocabulary.Google.clientID.getURI()) != null &&
1007+
getProperty(com.atomgraph.linkeddatahub.vocabulary.Google.clientSecret.getURI()) != null)
1008+
{
1009+
register(com.atomgraph.linkeddatahub.resource.oauth2.google.Authorize.class);
1010+
register(com.atomgraph.linkeddatahub.resource.oauth2.google.Login.class);
1011+
if (log.isDebugEnabled()) log.debug("Google OAuth endpoints registered");
1012+
}
1013+
1014+
// Conditionally register ORCID OAuth endpoints if configured
1015+
if (getProperty(com.atomgraph.linkeddatahub.vocabulary.ORCID.clientID.getURI()) != null &&
1016+
getProperty(com.atomgraph.linkeddatahub.vocabulary.ORCID.clientSecret.getURI()) != null)
1017+
{
1018+
register(com.atomgraph.linkeddatahub.resource.oauth2.orcid.Authorize.class);
1019+
register(com.atomgraph.linkeddatahub.resource.oauth2.orcid.Login.class);
1020+
if (log.isDebugEnabled()) log.debug("ORCID OAuth endpoints registered");
1021+
}
10101022
}
10111023

10121024
/**
@@ -1018,11 +1030,25 @@ protected void registerContainerRequestFilters()
10181030
register(ApplicationFilter.class);
10191031
register(OntologyFilter.class);
10201032
register(ProxiedWebIDFilter.class);
1021-
register(IDTokenFilter.class);
1022-
register(ORCIDTokenFilter.class);
10231033
register(AuthorizationFilter.class);
10241034
if (getMaxContentLength() != null) register(new ContentLengthLimitFilter(getMaxContentLength()));
10251035
register(new RDFPostMediaTypeInterceptor()); // for application/x-www-form-urlencoded
1036+
1037+
// Conditionally register Google OAuth filter if configured
1038+
if (getProperty(com.atomgraph.linkeddatahub.vocabulary.Google.clientID.getURI()) != null &&
1039+
getProperty(com.atomgraph.linkeddatahub.vocabulary.Google.clientSecret.getURI()) != null)
1040+
{
1041+
register(com.atomgraph.linkeddatahub.server.filter.request.auth.google.IDTokenFilter.class);
1042+
if (log.isDebugEnabled()) log.debug("Google OAuth filter registered");
1043+
}
1044+
1045+
// Conditionally register ORCID OAuth filter if configured
1046+
if (getProperty(com.atomgraph.linkeddatahub.vocabulary.ORCID.clientID.getURI()) != null &&
1047+
getProperty(com.atomgraph.linkeddatahub.vocabulary.ORCID.clientSecret.getURI()) != null)
1048+
{
1049+
register(com.atomgraph.linkeddatahub.server.filter.request.auth.orcid.IDTokenFilter.class);
1050+
if (log.isDebugEnabled()) log.debug("ORCID OAuth filter registered");
1051+
}
10261052
}
10271053

10281054
/**
@@ -2050,18 +2076,29 @@ public ExpiringMap<URI, Model> getWebIDModelCache()
20502076
/**
20512077
* A map of cached OpenID connect agent graphs.
20522078
* User ID (ID token subject) is the cache key. Entries expire after the configured period of time.
2053-
*
2079+
*
20542080
* @return URI to model map
20552081
*/
20562082
public ExpiringMap<String, Model> getOIDCModelCache()
20572083
{
20582084
return oidcModelCache;
20592085
}
2060-
2086+
2087+
/**
2088+
* A map of cached JWKS (JSON Web Key Set) responses for JWT verification.
2089+
* JWKS endpoint URI is the cache key. Entries expire after 1 day.
2090+
*
2091+
* @return JWKS endpoint to JsonObject map
2092+
*/
2093+
public ExpiringMap<String, jakarta.json.JsonObject> getJWKSCache()
2094+
{
2095+
return jwksCache;
2096+
}
2097+
20612098
/**
20622099
* A map of cached (compiled) XSLT stylesheets.
20632100
* Stylesheet URI is the cache key.
2064-
*
2101+
*
20652102
* @return URI to stylesheet map
20662103
*/
20672104
public Map<URI, XsltExecutable> getXsltExecutableCache()

src/main/java/com/atomgraph/linkeddatahub/client/filter/auth/IDTokenDelegationFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import jakarta.ws.rs.client.ClientRequestFilter;
2222
import jakarta.ws.rs.core.Cookie;
2323
import jakarta.ws.rs.core.HttpHeaders;
24-
import com.atomgraph.linkeddatahub.server.filter.request.auth.IDTokenFilter;
24+
import com.atomgraph.linkeddatahub.server.filter.request.auth.google.IDTokenFilter;
2525
import com.atomgraph.linkeddatahub.server.filter.request.auth.WebIDFilter;
2626
import org.apache.jena.rdf.model.Resource;
2727

src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.atomgraph.linkeddatahub.apps.model.EndUserApplication;
2626
import com.atomgraph.linkeddatahub.model.Service;
2727
import com.atomgraph.linkeddatahub.listener.EMailListener;
28+
import com.atomgraph.linkeddatahub.server.filter.response.CacheInvalidationFilter;
2829
import com.atomgraph.linkeddatahub.server.model.impl.GraphStoreImpl;
2930
import com.atomgraph.linkeddatahub.server.security.AgentContext;
3031
import com.atomgraph.linkeddatahub.server.util.MessageBuilder;
@@ -77,6 +78,7 @@
7778
import org.apache.jena.ontology.Ontology;
7879
import org.apache.jena.query.ParameterizedSparqlString;
7980
import org.apache.jena.query.Query;
81+
import org.apache.jena.query.ResultSet;
8082
import org.apache.jena.rdf.model.Model;
8183
import org.apache.jena.rdf.model.ModelFactory;
8284
import org.apache.jena.rdf.model.Property;
@@ -89,6 +91,7 @@
8991
import org.apache.jena.vocabulary.DCTerms;
9092
import org.apache.jena.vocabulary.RDF;
9193
import org.glassfish.jersey.server.internal.process.MappableException;
94+
import org.glassfish.jersey.uri.UriComponent;
9295
import org.slf4j.Logger;
9396
import org.slf4j.LoggerFactory;
9497

@@ -198,10 +201,12 @@ public Response post(Model agentModel, @QueryParam("default") @DefaultValue("fal
198201
String password = validateAndRemovePassword(agent);
199202
// TO-DO: trim values
200203
Resource mbox = agent.getRequiredProperty(FOAF.mbox).getResource();
201-
204+
202205
ParameterizedSparqlString pss = new ParameterizedSparqlString(getAgentQuery().toString());
203206
pss.setParam(FOAF.mbox.getLocalName(), mbox);
204-
boolean agentExists = !getAgentService().getSPARQLClient().loadModel(pss.asQuery()).isEmpty();
207+
ResultSet rs = getAgentService().getSPARQLClient().select(pss.asQuery());
208+
boolean agentExists = rs.hasNext();
209+
rs.close();
205210
if (agentExists) throw createSPINConstraintViolationException(agent, FOAF.mbox, "Agent with this mailbox already exists");
206211

207212
String givenName = agent.getRequiredProperty(FOAF.givenName).getString();
@@ -282,6 +287,9 @@ public Response post(Model agentModel, @QueryParam("default") @DefaultValue("fal
282287
throw new InternalServerErrorException("Cannot create Authorization");
283288
}
284289

290+
// purge agent lookup from proxy cache
291+
if (getAgentService().getBackendProxy() != null) ban(getAgentService().getBackendProxy(), mbox.getURI());
292+
285293
// remove secretary WebID from cache
286294
getSystem().getEventBus().post(new com.atomgraph.linkeddatahub.server.event.SignUp(getSystem().getSecretaryWebIDURI()));
287295

@@ -565,5 +573,21 @@ public Query getAgentQuery()
565573
{
566574
return getSystem().getAgentQuery();
567575
}
568-
576+
577+
/**
578+
* Bans URL from the backend proxy cache.
579+
*
580+
* @param proxy proxy server URL
581+
* @param url banned URL
582+
* @return proxy server response
583+
*/
584+
public Response ban(Resource proxy, String url)
585+
{
586+
if (url == null) throw new IllegalArgumentException("Resource cannot be null");
587+
588+
return getSystem().getClient().target(proxy.getURI()).request().
589+
header(CacheInvalidationFilter.HEADER_NAME, UriComponent.encode(url, UriComponent.Type.UNRESERVED)). // the value has to be URL-encoded in order to match request URLs in Varnish
590+
method("BAN", Response.class);
591+
}
592+
569593
}

0 commit comments

Comments
 (0)