From 7cd36b471fb1ec5ca21faf23b171caa5d382c48d Mon Sep 17 00:00:00 2001 From: Alexander Imfeld Date: Wed, 7 May 2014 16:44:56 +0200 Subject: [PATCH] Make introspection endpoint access authorization pluggable. --- .../service/IntrospectionAuthorizer.java | 36 ++++++ .../impl/DefaultIntrospectionAuthorizer.java | 43 +++++++ .../oauth2/web/IntrospectionEndpoint.java | 10 +- .../TestDefaultIntrospectionAuthorizer.java | 107 ++++++++++++++++++ 4 files changed, 190 insertions(+), 6 deletions(-) create mode 100755 openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionAuthorizer.java create mode 100755 openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionAuthorizer.java create mode 100755 openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionAuthorizer.java diff --git a/openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionAuthorizer.java b/openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionAuthorizer.java new file mode 100755 index 000000000..66b72b7a7 --- /dev/null +++ b/openid-connect-common/src/main/java/org/mitre/oauth2/service/IntrospectionAuthorizer.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright 2014 The MITRE Corporation + * and the MIT Kerberos and Internet Trust Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.mitre.oauth2.service; + +import java.util.Set; + +import org.springframework.security.oauth2.provider.ClientDetails; + +/** + * Strategy interface used for authorizing token introspection. + */ +public interface IntrospectionAuthorizer { + + /** + * @param authClient the authenticated client wanting to perform token introspection + * @param tokenClient the client the token was issued to + * @param tokenScope the scope associated with the token + * @return {@code true} in case introspection is permitted; {@code false} otherwise + */ + boolean isIntrospectionPermitted(ClientDetails authClient, ClientDetails tokenClient, Set tokenScope); + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionAuthorizer.java b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionAuthorizer.java new file mode 100755 index 000000000..a281687fe --- /dev/null +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/service/impl/DefaultIntrospectionAuthorizer.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright 2014 The MITRE Corporation + * and the MIT Kerberos and Internet Trust Consortium + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ******************************************************************************/ +package org.mitre.oauth2.service.impl; + +import java.util.Set; + +import org.mitre.oauth2.service.IntrospectionAuthorizer; +import org.mitre.oauth2.service.SystemScopeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.stereotype.Service; + +@Service +public class DefaultIntrospectionAuthorizer implements IntrospectionAuthorizer { + + @Autowired + private SystemScopeService scopeService; + + @Override + public boolean isIntrospectionPermitted(ClientDetails authClient, + ClientDetails tokenClient, Set tokenScope) { + // permit introspection if it's the same client that the token was + // issued to, or it at least has all the scopes the token was issued + // with + return authClient.getClientId().equals(tokenClient.getClientId()) + || scopeService.scopesMatch(authClient.getScope(), tokenScope); + } + +} diff --git a/openid-connect-server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java b/openid-connect-server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java index 6664d18e3..049fc9c37 100644 --- a/openid-connect-server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java +++ b/openid-connect-server/src/main/java/org/mitre/oauth2/web/IntrospectionEndpoint.java @@ -24,8 +24,8 @@ import org.mitre.oauth2.model.ClientDetailsEntity; import org.mitre.oauth2.model.OAuth2AccessTokenEntity; import org.mitre.oauth2.model.OAuth2RefreshTokenEntity; import org.mitre.oauth2.service.ClientDetailsEntityService; +import org.mitre.oauth2.service.IntrospectionAuthorizer; import org.mitre.oauth2.service.OAuth2TokenEntityService; -import org.mitre.oauth2.service.SystemScopeService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -50,7 +50,7 @@ public class IntrospectionEndpoint { private ClientDetailsEntityService clientService; @Autowired - private SystemScopeService scopeService; + private IntrospectionAuthorizer introspectionAuthorizer; private static Logger logger = LoggerFactory.getLogger(IntrospectionEndpoint.class); @@ -117,14 +117,12 @@ public class IntrospectionEndpoint { if (tokenClient != null && authClient != null) { if (authClient.isAllowIntrospection()) { - - // if it's the same client that the token was issued to, or it at least has all the scopes the token was issued with - if (authClient.getClientId().equals(tokenClient.getClientId()) || scopeService.scopesMatch(authClient.getScope(), scopes)) { + if (introspectionAuthorizer.isIntrospectionPermitted(authClient, tokenClient, scopes)) { // if it's a valid token, we'll print out information on it model.addAttribute("entity", token); return "tokenIntrospection"; } else { - logger.error("Verify failed; client tried to introspect a token of an incorrect scope"); + logger.error("Verify failed; client configuration or scope don't permit token introspection"); model.addAttribute("code", HttpStatus.FORBIDDEN); return "httpCodeView"; } diff --git a/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionAuthorizer.java b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionAuthorizer.java new file mode 100755 index 000000000..d60cae506 --- /dev/null +++ b/openid-connect-server/src/test/java/org/mitre/oauth2/service/impl/TestDefaultIntrospectionAuthorizer.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2014 by Netcetera AG. + * All rights reserved. + * + * The copyright to the computer program(s) herein is the property of Netcetera AG, Switzerland. + * The program(s) may be used and/or copied only with the written permission of Netcetera AG or + * in accordance with the terms and conditions stipulated in the agreement/contract under which + * the program(s) have been supplied. + */ +package org.mitre.oauth2.service.impl; + +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mitre.oauth2.service.SystemScopeService; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.security.oauth2.provider.ClientDetails; + +import static com.google.common.collect.Sets.newHashSet; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@RunWith(MockitoJUnitRunner.class) +public class TestDefaultIntrospectionAuthorizer { + + @InjectMocks + private DefaultIntrospectionAuthorizer introspectionPermitter; + + @Mock + private SystemScopeService scopeService; + + @Test + public void shouldPermitIntrospectionToSameClientTheTokenWasIssuedTo() { + + // given + String sameClient = "same"; + + // when + boolean permitted = introspectionPermitter.isIntrospectionPermitted( + clientWithId(sameClient), clientWithId(sameClient), + scope("scope")); + + // then + assertThat(permitted, is(true)); + } + + @Test + public void shouldPermitIntrospectionToDifferentClientIfScopesMatch() { + + // given + String authClient = "auth"; + String tokenClient = "token"; + Set authScope = scope("scope1", "scope2", "scope3"); + Set tokenScope = scope("scope1", "scope2"); + given(scopeService.scopesMatch(authScope, tokenScope)).willReturn(true); + + // when + boolean permitted = introspectionPermitter.isIntrospectionPermitted( + clientWithIdAndScope(authClient, authScope), + clientWithId(tokenClient), tokenScope); + + // then + assertThat(permitted, is(true)); + } + + @Test + public void shouldNotPermitIntrospectionToDifferentClientIfScopesDontMatch() { + + // given + String authClient = "auth"; + String tokenClient = "token"; + Set authScope = scope("scope1", "scope2"); + Set tokenScope = scope("scope1", "scope2", "scope3"); + given(scopeService.scopesMatch(authScope, tokenScope)) + .willReturn(false); + + // when + boolean permitted = introspectionPermitter.isIntrospectionPermitted( + clientWithIdAndScope(authClient, authScope), + clientWithId(tokenClient), tokenScope); + + // then + assertThat(permitted, is(false)); + } + + private ClientDetails clientWithId(String clientId) { + ClientDetails client = mock(ClientDetails.class); + given(client.getClientId()).willReturn(clientId); + return client; + } + + private ClientDetails clientWithIdAndScope(String clientId, + Set scope) { + ClientDetails client = clientWithId(clientId); + given(client.getScope()).willReturn(scope); + return client; + } + + private Set scope(String... scopeItems) { + return newHashSet(scopeItems); + } +}