Introduce introspection result assembler to allow for customized introspection results

pull/693/merge
Alexander Imfeld 2014-10-07 15:57:11 +02:00 committed by Justin Richer
parent bf00c1f5e0
commit 9dfac35912
5 changed files with 424 additions and 199 deletions

View File

@ -0,0 +1,48 @@
/*******************************************************************************
* 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 org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.model.OAuth2RefreshTokenEntity;
import org.mitre.openid.connect.model.UserInfo;
import java.util.Map;
/**
* Strategy interface for assembling a token introspection result.
*/
public interface IntrospectionResultAssembler {
/**
* Assemble a token introspection result from the given access token and user info.
*
* @param accessToken the access token
* @param userInfo the user info
* @return the token introspection result
*/
Map<String, Object> assembleFrom(OAuth2AccessTokenEntity accessToken, UserInfo userInfo);
/**
* Assemble a token introspection result from the given refresh token and user info.
*
* @param refreshToken the refresh token
* @param userInfo the user info
* @return the token introspection result
*/
Map<String, Object> assembleFrom(OAuth2RefreshTokenEntity refreshToken, UserInfo userInfo);
}

View File

@ -0,0 +1,96 @@
/*******************************************************************************
* 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 com.google.common.base.Joiner;
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.model.OAuth2RefreshTokenEntity;
import org.mitre.oauth2.service.IntrospectionResultAssembler;
import org.mitre.openid.connect.model.UserInfo;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.stereotype.Service;
import java.util.Map;
import static com.google.common.collect.Maps.newLinkedHashMap;
/**
* Default implementation of the {@link IntrospectionResultAssembler} interface.
*/
@Service
public class DefaultIntrospectionResultAssembler implements IntrospectionResultAssembler {
@Override
public Map<String, Object> assembleFrom(OAuth2AccessTokenEntity accessToken, UserInfo userInfo) {
Map<String, Object> result = newLinkedHashMap();
OAuth2Authentication authentication = accessToken.getAuthenticationHolder().getAuthentication();
result.put("active", true);
result.put("scope", Joiner.on(" ").join(accessToken.getScope()));
if (accessToken.getExpiration() != null) {
result.put("exp", accessToken.getExpiration());
}
if (userInfo != null) {
// if we have a UserInfo, use that for the subject
result.put("sub", userInfo.getSub());
} else {
// otherwise, use the authentication's username
result.put("sub", authentication.getName());
}
result.put("user_id", authentication.getName());
result.put("client_id", authentication.getOAuth2Request().getClientId());
result.put("token_type", accessToken.getTokenType());
return result;
}
@Override
public Map<String, Object> assembleFrom(OAuth2RefreshTokenEntity refreshToken, UserInfo userInfo) {
Map<String, Object> result = newLinkedHashMap();
OAuth2Authentication authentication = refreshToken.getAuthenticationHolder().getAuthentication();
result.put("active", true);
result.put("scope", Joiner.on(" ").join(authentication.getOAuth2Request().getScope()));
if (refreshToken.getExpiration() != null) {
result.put("exp", refreshToken.getExpiration());
}
if (userInfo != null) {
// if we have a UserInfo, use that for the subject
result.put("sub", userInfo.getSub());
} else {
// otherwise, use the authentication's username
result.put("sub", authentication.getName());
}
result.put("user_id", authentication.getName());
result.put("client_id", authentication.getOAuth2Request().getClientId());
return result;
}
}

View File

@ -1,143 +0,0 @@
/*******************************************************************************
* 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.view;
import java.io.IOException;
import java.io.Writer;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.swing.text.DateFormatter;
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.model.OAuth2RefreshTokenEntity;
import org.mitre.openid.connect.model.UserInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;
import com.google.common.base.Joiner;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;
@Component(TokenIntrospectionView.VIEWNAME)
public class TokenIntrospectionView extends AbstractView {
public static final String VIEWNAME = "tokenIntrospection";
private static Logger logger = LoggerFactory.getLogger(TokenIntrospectionView.class);
private static DateFormatter isoDateFormatter = new DateFormatter(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"));
private Gson gson = new GsonBuilder().create();
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) {
response.setContentType("application/json");
Writer out;
try {
out = response.getWriter();
UserInfo user = (UserInfo)model.get("user");
Object obj = model.get("token");
if (obj instanceof OAuth2AccessTokenEntity) {
gson.toJson(renderAccessToken((OAuth2AccessTokenEntity)obj, user), out);
} else if (obj instanceof OAuth2RefreshTokenEntity) {
gson.toJson(renderRefreshToken((OAuth2RefreshTokenEntity)obj, user), out);
} else {
throw new IOException("Couldn't find a valid entity to render");
}
} catch (IOException e) {
logger.error("IOException occurred in TokenIntrospectionView.java: ", e);
}
}
private JsonObject renderAccessToken(OAuth2AccessTokenEntity src, UserInfo user) {
JsonObject token = new JsonObject();
token.addProperty("active", true);
token.addProperty("scope", Joiner.on(" ").join(src.getScope()));
if (src.getExpiration() != null) {
try {
token.addProperty("exp", isoDateFormatter.valueToString(src.getExpiration()));
} catch (ParseException e) {
logger.error("Problem formatting expiration date: " + src.getExpiration(), e);
}
}
if (user != null) {
// if we have a UserInfo, use that for the subject
token.addProperty("sub", user.getSub());
token.addProperty("user_id", src.getAuthenticationHolder().getAuthentication().getName());
} else {
// otherwise, use the authentication's username
token.addProperty("sub", src.getAuthenticationHolder().getAuthentication().getName());
token.addProperty("user_id", src.getAuthenticationHolder().getAuthentication().getName());
}
token.addProperty("client_id", src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getClientId());
token.addProperty("token_type", src.getTokenType());
return token;
}
private JsonObject renderRefreshToken(OAuth2RefreshTokenEntity src, UserInfo user) {
JsonObject token = new JsonObject();
token.addProperty("active", true);
token.addProperty("scope", Joiner.on(" ").join(src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope()));
if (src.getExpiration() != null) {
try {
token.addProperty("exp", isoDateFormatter.valueToString(src.getExpiration()));
} catch (ParseException e) {
logger.error("Problem formatting expiration date: " + src.getExpiration(), e);
}
}
if (user != null) {
// if we have a UserInfo, use that for the subject
token.addProperty("sub", user.getSub());
token.addProperty("user_id", src.getAuthenticationHolder().getAuthentication().getName());
} else {
// otherwise, use the authentication's username
token.addProperty("sub", src.getAuthenticationHolder().getAuthentication().getName());
token.addProperty("user_id", src.getAuthenticationHolder().getAuthentication().getName());
}
token.addProperty("client_id", src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getClientId());
return token;
}
}

View File

@ -1,13 +1,13 @@
/*******************************************************************************
* 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.
@ -16,17 +16,15 @@
******************************************************************************/
package org.mitre.oauth2.web;
import java.security.Principal;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
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.IntrospectionResultAssembler;
import org.mitre.oauth2.service.OAuth2TokenEntityService;
import org.mitre.oauth2.view.TokenIntrospectionView;
import org.mitre.openid.connect.model.UserInfo;
import org.mitre.openid.connect.service.UserInfoService;
import org.mitre.openid.connect.view.HttpCodeView;
@ -42,8 +40,9 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import java.security.Principal;
import java.util.Map;
import java.util.Set;
@Controller
public class IntrospectionEndpoint {
@ -56,7 +55,10 @@ public class IntrospectionEndpoint {
@Autowired
private IntrospectionAuthorizer introspectionAuthorizer;
@Autowired
private IntrospectionResultAssembler introspectionResultAssembler;
@Autowired
private UserInfoService userInfoService;
@ -84,72 +86,64 @@ public class IntrospectionEndpoint {
return JsonEntityView.VIEWNAME;
}
// clientID is the principal name in the authentication
String clientId = p.getName();
ClientDetailsEntity authClient = clientService.loadClientByClientId(clientId);
ClientDetailsEntity tokenClient = null;
Set<String> scopes = null;
Object token = null;
UserInfo user = null;
OAuth2AccessTokenEntity accessToken = null;
OAuth2RefreshTokenEntity refreshToken = null;
ClientDetailsEntity tokenClient;
Set<String> scopes;
UserInfo user;
try {
// check access tokens first (includes ID tokens)
OAuth2AccessTokenEntity access = tokenServices.readAccessToken(tokenValue);
accessToken = tokenServices.readAccessToken(tokenValue);
tokenClient = access.getClient();
scopes = access.getScope();
tokenClient = accessToken.getClient();
scopes = accessToken.getScope();
token = access;
user = userInfoService.getByUsernameAndClientId(accessToken.getAuthenticationHolder().getAuthentication().getName(), tokenClient.getClientId());
user = userInfoService.getByUsernameAndClientId(access.getAuthenticationHolder().getAuthentication().getName(), tokenClient.getClientId());
} catch (InvalidTokenException e) {
logger.error("Verify failed; Invalid access token. Checking refresh token.", e);
logger.info("Verify failed; Invalid access token. Checking refresh token.");
try {
// check refresh tokens next
OAuth2RefreshTokenEntity refresh = tokenServices.getRefreshToken(tokenValue);
refreshToken = tokenServices.getRefreshToken(tokenValue);
tokenClient = refresh.getClient();
scopes = refresh.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope();
tokenClient = refreshToken.getClient();
scopes = refreshToken.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope();
user = userInfoService.getByUsernameAndClientId(refresh.getAuthenticationHolder().getAuthentication().getName(), tokenClient.getClientId());
token = refresh;
user = userInfoService.getByUsernameAndClientId(refreshToken.getAuthenticationHolder().getAuthentication().getName(), tokenClient.getClientId());
} catch (InvalidTokenException e2) {
logger.error("Verify failed; Invalid refresh token", e2);
logger.error("Verify failed; Invalid access/refresh token", e2);
Map<String,Boolean> entity = ImmutableMap.of("active", Boolean.FALSE);
model.addAttribute("entity", entity);
return JsonEntityView.VIEWNAME;
}
}
if (tokenClient != null && authClient != null) {
if (authClient.isAllowIntrospection()) {
if (introspectionAuthorizer.isIntrospectionPermitted(authClient, tokenClient, scopes)) {
// if it's a valid token, we'll print out information on it
model.addAttribute("token", token);
model.addAttribute("user", user);
return TokenIntrospectionView.VIEWNAME;
} else {
logger.error("Verify failed; client configuration or scope don't permit token introspection");
model.addAttribute("code", HttpStatus.FORBIDDEN);
return HttpCodeView.VIEWNAME;
}
} else {
logger.error("Verify failed; client " + clientId + " is not allowed to call introspection endpoint");
model.addAttribute("code", HttpStatus.FORBIDDEN);
return HttpCodeView.VIEWNAME;
}
} else {
// This is a bad error -- I think it means we have a token outstanding that doesn't map to a client?
logger.error("Verify failed; client " + clientId + " not found.");
model.addAttribute("code", HttpStatus.NOT_FOUND);
return HttpCodeView.VIEWNAME;
}
// clientID is the principal name in the authentication
String clientId = p.getName();
ClientDetailsEntity authClient = clientService.loadClientByClientId(clientId);
if (authClient.isAllowIntrospection()) {
if (introspectionAuthorizer.isIntrospectionPermitted(authClient, tokenClient, scopes)) {
// if it's a valid token, we'll print out information on it
Map<String, Object> entity = accessToken != null
? introspectionResultAssembler.assembleFrom(accessToken, user)
: introspectionResultAssembler.assembleFrom(refreshToken, user);
model.addAttribute("entity", entity);
return JsonEntityView.VIEWNAME;
} else {
logger.error("Verify failed; client configuration or scope don't permit token introspection");
model.addAttribute("code", HttpStatus.FORBIDDEN);
return HttpCodeView.VIEWNAME;
}
} else {
logger.error("Verify failed; client " + clientId + " is not allowed to call introspection endpoint");
model.addAttribute("code", HttpStatus.FORBIDDEN);
return HttpCodeView.VIEWNAME;
}
}

View File

@ -0,0 +1,230 @@
/*******************************************************************************
* 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 com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.mitre.oauth2.model.OAuth2AccessTokenEntity;
import org.mitre.oauth2.model.OAuth2RefreshTokenEntity;
import org.mitre.openid.connect.model.UserInfo;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import static com.google.common.collect.Sets.newHashSet;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
public class TestDefaultIntrospectionResultAssembler {
private DefaultIntrospectionResultAssembler assembler = new DefaultIntrospectionResultAssembler();
@Test
public void shouldAssembleExpectedResultForAccessToken() {
// given
OAuth2AccessTokenEntity accessToken = accessToken(new Date(123), scopes("foo", "bar"), "Bearer",
authentication("name", request("clientId")));
UserInfo userInfo = userInfo("sub");
// when
Map<String, Object> result = assembler.assembleFrom(accessToken, userInfo);
// then
Map<String, Object> expected = new ImmutableMap.Builder<String, Object>()
.put("sub", "sub")
.put("exp", new Date(123))
.put("scope", "bar foo")
.put("active", Boolean.TRUE)
.put("user_id", "name")
.put("client_id", "clientId")
.put("token_type", "Bearer")
.build();
assertThat(result, is(equalTo(expected)));
}
@Test
public void shouldAssembleExpectedResultForAccessTokenWithoutUserInfo() {
// given
OAuth2AccessTokenEntity accessToken = accessToken(new Date(123), scopes("foo", "bar"), "Bearer",
authentication("name", request("clientId")));
// when
Map<String, Object> result = assembler.assembleFrom(accessToken, null);
// then
Map<String, Object> expected = new ImmutableMap.Builder<String, Object>()
.put("sub", "name")
.put("exp", new Date(123))
.put("scope", "bar foo")
.put("active", Boolean.TRUE)
.put("user_id", "name")
.put("client_id", "clientId")
.put("token_type", "Bearer")
.build();
assertThat(result, is(equalTo(expected)));
}
@Test
public void shouldAssembleExpectedResultForAccessTokenWithoutExpiry() {
// given
OAuth2AccessTokenEntity accessToken = accessToken(null, scopes("foo", "bar"), "Bearer",
authentication("name", request("clientId")));
UserInfo userInfo = userInfo("sub");
// when
Map<String, Object> result = assembler.assembleFrom(accessToken, userInfo);
// then
Map<String, Object> expected = new ImmutableMap.Builder<String, Object>()
.put("sub", "sub")
.put("scope", "bar foo")
.put("active", Boolean.TRUE)
.put("user_id", "name")
.put("client_id", "clientId")
.put("token_type", "Bearer")
.build();
assertThat(result, is(equalTo(expected)));
}
@Test
public void shouldAssembleExpectedResultForRefreshToken() {
// given
OAuth2RefreshTokenEntity refreshToken = refreshToken(new Date(123),
authentication("name", request("clientId", scopes("foo", "bar"))));
UserInfo userInfo = userInfo("sub");
// when
Map<String, Object> result = assembler.assembleFrom(refreshToken, userInfo);
// then
Map<String, Object> expected = new ImmutableMap.Builder<String, Object>()
.put("sub", "sub")
.put("exp", new Date(123))
.put("scope", "bar foo")
.put("active", Boolean.TRUE)
.put("user_id", "name")
.put("client_id", "clientId")
.build();
assertThat(result, is(equalTo(expected)));
}
@Test
public void shouldAssembleExpectedResultForRefreshTokenWithoutUserInfo() {
// given
OAuth2RefreshTokenEntity refreshToken = refreshToken(new Date(123),
authentication("name", request("clientId", scopes("foo", "bar"))));
// when
Map<String, Object> result = assembler.assembleFrom(refreshToken, null);
// then
Map<String, Object> expected = new ImmutableMap.Builder<String, Object>()
.put("sub", "name")
.put("exp", new Date(123))
.put("scope", "bar foo")
.put("active", Boolean.TRUE)
.put("user_id", "name")
.put("client_id", "clientId")
.build();
assertThat(result, is(equalTo(expected)));
}
@Test
public void shouldAssembleExpectedResultForRefreshTokenWithoutExpiry() {
// given
OAuth2RefreshTokenEntity refreshToken = refreshToken(null,
authentication("name", request("clientId", scopes("foo", "bar"))));
UserInfo userInfo = userInfo("sub");
// when
Map<String, Object> result = assembler.assembleFrom(refreshToken, userInfo);
// then
Map<String, Object> expected = new ImmutableMap.Builder<String, Object>()
.put("sub", "sub")
.put("scope", "bar foo")
.put("active", Boolean.TRUE)
.put("user_id", "name")
.put("client_id", "clientId")
.build();
assertThat(result, is(equalTo(expected)));
}
private UserInfo userInfo(String sub) {
UserInfo userInfo = mock(UserInfo.class);
given(userInfo.getSub()).willReturn(sub);
return userInfo;
}
private OAuth2AccessTokenEntity accessToken(Date exp, Set<String> scopes, String tokenType, OAuth2Authentication authentication) {
OAuth2AccessTokenEntity accessToken = mock(OAuth2AccessTokenEntity.class, RETURNS_DEEP_STUBS);
given(accessToken.getExpiration()).willReturn(exp);
given(accessToken.getScope()).willReturn(scopes);
given(accessToken.getTokenType()).willReturn(tokenType);
given(accessToken.getAuthenticationHolder().getAuthentication()).willReturn(authentication);
return accessToken;
}
private OAuth2RefreshTokenEntity refreshToken(Date exp, OAuth2Authentication authentication) {
OAuth2RefreshTokenEntity refreshToken = mock(OAuth2RefreshTokenEntity.class, RETURNS_DEEP_STUBS);
given(refreshToken.getExpiration()).willReturn(exp);
given(refreshToken.getAuthenticationHolder().getAuthentication()).willReturn(authentication);
return refreshToken;
}
private OAuth2Authentication authentication(String name, OAuth2Request request) {
OAuth2Authentication authentication = mock(OAuth2Authentication.class);
given(authentication.getName()).willReturn(name);
given(authentication.getOAuth2Request()).willReturn(request);
return authentication;
}
private OAuth2Request request(String clientId) {
return request(clientId, null);
}
private OAuth2Request request(String clientId, Set<String> scopes) {
return new OAuth2Request(null, clientId, null, true, scopes, null, null, null, null);
}
private Set<String> scopes(String... scopes) {
return newHashSet(scopes);
}
}