diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectingTokenService.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectingTokenService.java index 46363a656..96b1e494e 100644 --- a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectingTokenService.java +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectingTokenService.java @@ -38,30 +38,32 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +/** + * This ResourceServerTokenServices implementation introspects incoming tokens at a + * server's introspection endpoint URL and passes an Authentication object along + * based on the response from the introspection endpoint. + * @author jricher + * + */ public class IntrospectingTokenService implements ResourceServerTokenServices { - private String clientId; private String clientSecret; - private String introspectionUrl; - + private IntrospectionUrlProvider introspectionUrlProvider; + // Inner class to store in the hash map - private class TokenCacheObject { OAuth2AccessToken token; OAuth2Authentication auth; - private TokenCacheObject(OAuth2AccessToken token, OAuth2Authentication auth) { - this.token = token; - this.auth = auth; - } + private class TokenCacheObject { + OAuth2AccessToken token; + OAuth2Authentication auth; + + private TokenCacheObject(OAuth2AccessToken token, OAuth2Authentication auth) { + this.token = token; + this.auth = auth; + } } + private Map<String, TokenCacheObject> authCache = new HashMap<String, TokenCacheObject>(); - public String getIntrospectionUrl() { - return introspectionUrl; - } - - public void setIntrospectionUrl(String introspectionUrl) { - this.introspectionUrl = introspectionUrl; - } - public String getClientId() { return clientId; } @@ -78,10 +80,24 @@ public class IntrospectingTokenService implements ResourceServerTokenServices { this.clientSecret = clientSecret; } + /** + * @return the introspectionUrlProvider + */ + public IntrospectionUrlProvider getIntrospectionUrlProvider() { + return introspectionUrlProvider; + } + + /** + * @param introspectionUrlProvider the introspectionUrlProvider to set + */ + public void setIntrospectionUrlProvider(IntrospectionUrlProvider introspectionUrlProvider) { + this.introspectionUrlProvider = introspectionUrlProvider; + } + // Check if there is a token and authentication in the cache - // and check if it is not expired. + // and check if it is not expired. private TokenCacheObject checkCache(String key) { - if(authCache.containsKey(key)) { + if (authCache.containsKey(key)) { TokenCacheObject tco = authCache.get(key); if (tco.token.getExpiration().after(new Date())) { return tco; @@ -99,26 +115,30 @@ public class IntrospectingTokenService implements ResourceServerTokenServices { } // create a default authentication object with authority ROLE_API - private Authentication createAuthentication(JsonObject token){ + private Authentication createAuthentication(JsonObject token) { // TODO: make role/authority configurable somehow return new PreAuthenticatedAuthenticationToken(token.get("sub").getAsString(), null, AuthorityUtils.createAuthorityList("ROLE_API")); } - private OAuth2AccessToken createAccessToken(final JsonObject token, final String tokenString){ + private OAuth2AccessToken createAccessToken(final JsonObject token, final String tokenString) { OAuth2AccessToken accessToken = new OAuth2AccessTokenImpl(token, tokenString); return accessToken; } // Validate a token string against the introspection endpoint, - // then parse it and store it in the local cache. Return true on - // sucess, false otherwise. + // then parse it and store it in the local cache. Return true on + // sucess, false otherwise. private boolean parseToken(String accessToken) { - String validatedToken = null; - // Use the SpringFramework RestTemplate to send the request to the endpoint + // find out which URL to ask + String introspectionUrl = introspectionUrlProvider.getIntrospectionUrl(accessToken); + + // Use the SpringFramework RestTemplate to send the request to the + // endpoint + String validatedToken = null; RestTemplate restTemplate = new RestTemplate(); MultiValueMap<String, String> form = new LinkedMultiValueMap<String, String>(); - form.add("token",accessToken); + form.add("token", accessToken); form.add("client_id", this.clientId); form.add("client_secret", this.clientSecret); @@ -142,7 +162,7 @@ public class IntrospectingTokenService implements ResourceServerTokenServices { return false; } - if (!tokenResponse.get("active").getAsBoolean()){ + if (!tokenResponse.get("active").getAsBoolean()) { // non-valid token return false; } @@ -151,9 +171,9 @@ public class IntrospectingTokenService implements ResourceServerTokenServices { // create an OAuth2AccessToken OAuth2AccessToken token = createAccessToken(tokenResponse, accessToken); - if (token.getExpiration().after(new Date())){ + if (token.getExpiration().after(new Date())) { // Store them in the cache - authCache.put(accessToken, new TokenCacheObject(token,auth)); + authCache.put(accessToken, new TokenCacheObject(token, auth)); return true; } @@ -165,7 +185,8 @@ public class IntrospectingTokenService implements ResourceServerTokenServices { @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException { - // First check if the in memory cache has an Authentication object, and that it is still valid + // First check if the in memory cache has an Authentication object, and + // that it is still valid // If Valid, return it TokenCacheObject cacheAuth = checkCache(accessToken); if (cacheAuth != null) { @@ -186,7 +207,8 @@ public class IntrospectingTokenService implements ResourceServerTokenServices { @Override public OAuth2AccessToken readAccessToken(String accessToken) { - // First check if the in memory cache has a Token object, and that it is still valid + // First check if the in memory cache has a Token object, and that it is + // still valid // If Valid, return it TokenCacheObject cacheAuth = checkCache(accessToken); if (cacheAuth != null) { diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectionUrlProvider.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectionUrlProvider.java new file mode 100644 index 000000000..78c39ba67 --- /dev/null +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/IntrospectionUrlProvider.java @@ -0,0 +1,19 @@ +/** + * + */ +package org.mitre.oauth2.introspectingfilter; + +/** + * @author jricher + * + */ +public interface IntrospectionUrlProvider { + + /** + * Get the introspection URL based on the access token. + * @param accessToken + * @return + */ + public String getIntrospectionUrl(String accessToken); + +} diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/JWTParsingIntrospectionUrlProvider.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/JWTParsingIntrospectionUrlProvider.java new file mode 100644 index 000000000..c6d5eb621 --- /dev/null +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/JWTParsingIntrospectionUrlProvider.java @@ -0,0 +1,73 @@ +/** + * + */ +package org.mitre.oauth2.introspectingfilter; + +import java.text.ParseException; + +import org.mitre.openid.connect.client.service.ServerConfigurationService; +import org.mitre.openid.connect.config.ServerConfiguration; + +import com.google.common.base.Strings; +import com.nimbusds.jwt.JWT; +import com.nimbusds.jwt.JWTParser; + +/** + * + * Parses the incoming accesstoken as a JWT and determines the issuer based on + * the "iss" field inside the JWT. Uses the ServerConfigurationService to determine + * the introspection URL for that issuer. + * + * @author jricher + * + */ +public class JWTParsingIntrospectionUrlProvider implements IntrospectionUrlProvider { + + private ServerConfigurationService serverConfigurationService; + + /** + * @return the serverConfigurationService + */ + public ServerConfigurationService getServerConfigurationService() { + return serverConfigurationService; + } + + /** + * @param serverConfigurationService the serverConfigurationService to set + */ + public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) { + this.serverConfigurationService = serverConfigurationService; + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.introspectingfilter.IntrospectionUrlProvider#getIntrospectionUrl(java.lang.String) + */ + @Override + public String getIntrospectionUrl(String accessToken) { + + try { + JWT jwt = JWTParser.parse(accessToken); + + String issuer = jwt.getJWTClaimsSet().getIssuer(); + if (!Strings.isNullOrEmpty(issuer)) { + ServerConfiguration server = serverConfigurationService.getServerConfiguration(issuer); + if (server != null) { + if (!Strings.isNullOrEmpty(server.getIntrospectionEndpointUri())) { + return server.getIntrospectionEndpointUri(); + } else { + throw new IllegalArgumentException("Server does not have Introspection Endpoint defined"); + } + } else { + throw new IllegalArgumentException("Could not find server configuration for issuer " + issuer); + } + } else { + throw new IllegalArgumentException("No issuer claim found in JWT"); + } + + } catch (ParseException e) { + throw new IllegalArgumentException("Unable to parse JWT", e); + } + + } + +} diff --git a/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/StaticIntrospectionUrlProvider.java b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/StaticIntrospectionUrlProvider.java new file mode 100644 index 000000000..a8e1aeaf9 --- /dev/null +++ b/openid-connect-client/src/main/java/org/mitre/oauth2/introspectingfilter/StaticIntrospectionUrlProvider.java @@ -0,0 +1,40 @@ +/** + * + */ +package org.mitre.oauth2.introspectingfilter; + +/** + * + * Always provides the (configured) IntrospectionURL regardless of token. Useful for talking to + * a single, trusted authorization server. + * + * @author jricher + * + */ +public class StaticIntrospectionUrlProvider implements IntrospectionUrlProvider { + + private String introspectionUrl; + + /** + * @return the introspectionUrl + */ + public String getIntrospectionUrl() { + return introspectionUrl; + } + + /** + * @param introspectionUrl the introspectionUrl to set + */ + public void setIntrospectionUrl(String introspectionUrl) { + this.introspectionUrl = introspectionUrl; + } + + /* (non-Javadoc) + * @see org.mitre.oauth2.introspectingfilter.IntrospectionUrlProvider#getIntrospectionUrl(java.lang.String) + */ + @Override + public String getIntrospectionUrl(String accessToken) { + return getIntrospectionUrl(); + } + +} diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicServerConfigurationService.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicServerConfigurationService.java index f4b02a60e..a2017a4bc 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicServerConfigurationService.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/service/impl/DynamicServerConfigurationService.java @@ -95,17 +95,35 @@ public class DynamicServerConfigurationService implements ServerConfigurationSer JsonObject o = parsed.getAsJsonObject(); // sanity checks + if (!o.has("issuer")) { + throw new IllegalStateException("Returned object did not have an 'issuer' field"); + } + if (!issuer.equals(o.get("issuer").getAsString())) { throw new IllegalStateException("Discovered issuers didn't match, expected " + issuer + " got " + o.get("issuer").getAsString()); } conf.setIssuer(o.get("issuer").getAsString()); - conf.setAuthorizationEndpointUri(o.get("authorization_endpoint").getAsString()); - conf.setTokenEndpointUri(o.get("token_endpoint").getAsString()); - conf.setJwksUri(o.get("jwks_uri").getAsString()); - conf.setUserInfoUri(o.get("userinfo_endpoint").getAsString()); - conf.setRegistrationEndpointUri(o.get("registration_endpoint").getAsString()); - + + if (o.has("authorization_endpoint")) { + conf.setAuthorizationEndpointUri(o.get("authorization_endpoint").getAsString()); + } + if (o.has("token_endpoint")) { + conf.setTokenEndpointUri(o.get("token_endpoint").getAsString()); + } + if (o.has("jwks_uri")) { + conf.setJwksUri(o.get("jwks_uri").getAsString()); + } + if (o.has("userinfo_endpoint")) { + conf.setUserInfoUri(o.get("userinfo_endpoint").getAsString()); + } + if (o.has("registration_endpoint")) { + conf.setRegistrationEndpointUri(o.get("registration_endpoint").getAsString()); + } + if (o.has("introspection_endpoint")) { + conf.setIntrospectionEndpointUri(o.get("introspection_endpoint").getAsString()); + } + return conf; } else { throw new IllegalStateException("Couldn't parse server discovery results for " + url); diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ServerConfiguration.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ServerConfiguration.java index 7adf15cb7..0b0809e59 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ServerConfiguration.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/ServerConfiguration.java @@ -39,6 +39,8 @@ public class ServerConfiguration { private String userInfoUri; + private String introspectionEndpointUri; + /** * @return the authorizationEndpointUri */ @@ -123,81 +125,103 @@ public class ServerConfiguration { this.registrationEndpointUri = registrationEndpointUri; } + /** + * @return the introspectionEndpointUri + */ + public String getIntrospectionEndpointUri() { + return introspectionEndpointUri; + } + + /** + * @param introspectionEndpointUri the introspectionEndpointUri to set + */ + public void setIntrospectionEndpointUri(String introspectionEndpointUri) { + this.introspectionEndpointUri = introspectionEndpointUri; + } + /* (non-Javadoc) * @see java.lang.Object#hashCode() */ - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((authorizationEndpointUri == null) ? 0 : authorizationEndpointUri.hashCode()); - result = prime * result + ((issuer == null) ? 0 : issuer.hashCode()); - result = prime * result + ((jwksUri == null) ? 0 : jwksUri.hashCode()); - result = prime * result + ((registrationEndpointUri == null) ? 0 : registrationEndpointUri.hashCode()); - result = prime * result + ((tokenEndpointUri == null) ? 0 : tokenEndpointUri.hashCode()); - result = prime * result + ((userInfoUri == null) ? 0 : userInfoUri.hashCode()); - return result; - } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((authorizationEndpointUri == null) ? 0 : authorizationEndpointUri.hashCode()); + result = prime * result + ((introspectionEndpointUri == null) ? 0 : introspectionEndpointUri.hashCode()); + result = prime * result + ((issuer == null) ? 0 : issuer.hashCode()); + result = prime * result + ((jwksUri == null) ? 0 : jwksUri.hashCode()); + result = prime * result + ((registrationEndpointUri == null) ? 0 : registrationEndpointUri.hashCode()); + result = prime * result + ((tokenEndpointUri == null) ? 0 : tokenEndpointUri.hashCode()); + result = prime * result + ((userInfoUri == null) ? 0 : userInfoUri.hashCode()); + return result; + } /* (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ServerConfiguration other = (ServerConfiguration) obj; - if (authorizationEndpointUri == null) { - if (other.authorizationEndpointUri != null) { - return false; - } - } else if (!authorizationEndpointUri.equals(other.authorizationEndpointUri)) { - return false; - } - if (issuer == null) { - if (other.issuer != null) { - return false; - } - } else if (!issuer.equals(other.issuer)) { - return false; - } - if (jwksUri == null) { - if (other.jwksUri != null) { - return false; - } - } else if (!jwksUri.equals(other.jwksUri)) { - return false; - } - if (registrationEndpointUri == null) { - if (other.registrationEndpointUri != null) { - return false; - } - } else if (!registrationEndpointUri.equals(other.registrationEndpointUri)) { - return false; - } - if (tokenEndpointUri == null) { - if (other.tokenEndpointUri != null) { - return false; - } - } else if (!tokenEndpointUri.equals(other.tokenEndpointUri)) { - return false; - } - if (userInfoUri == null) { - if (other.userInfoUri != null) { - return false; - } - } else if (!userInfoUri.equals(other.userInfoUri)) { - return false; - } - return true; - } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ServerConfiguration)) { + return false; + } + ServerConfiguration other = (ServerConfiguration) obj; + if (authorizationEndpointUri == null) { + if (other.authorizationEndpointUri != null) { + return false; + } + } else if (!authorizationEndpointUri.equals(other.authorizationEndpointUri)) { + return false; + } + if (introspectionEndpointUri == null) { + if (other.introspectionEndpointUri != null) { + return false; + } + } else if (!introspectionEndpointUri.equals(other.introspectionEndpointUri)) { + return false; + } + if (issuer == null) { + if (other.issuer != null) { + return false; + } + } else if (!issuer.equals(other.issuer)) { + return false; + } + if (jwksUri == null) { + if (other.jwksUri != null) { + return false; + } + } else if (!jwksUri.equals(other.jwksUri)) { + return false; + } + if (registrationEndpointUri == null) { + if (other.registrationEndpointUri != null) { + return false; + } + } else if (!registrationEndpointUri.equals(other.registrationEndpointUri)) { + return false; + } + if (tokenEndpointUri == null) { + if (other.tokenEndpointUri != null) { + return false; + } + } else if (!tokenEndpointUri.equals(other.tokenEndpointUri)) { + return false; + } + if (userInfoUri == null) { + if (other.userInfoUri != null) { + return false; + } + } else if (!userInfoUri.equals(other.userInfoUri)) { + return false; + } + return true; + } }