token introspection now returns user "sub" when available in addition to "user_id", closes #507 (might cause incompatibility problems)

pull/612/head
Justin Richer 2014-06-04 17:27:38 -04:00
parent 85acfa90db
commit cdd23df7ee
2 changed files with 93 additions and 98 deletions

View File

@ -18,119 +18,39 @@ package org.mitre.oauth2.view;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Type;
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.validation.BeanPropertyBindingResult;
import org.springframework.web.servlet.view.AbstractView;
import com.google.common.base.Joiner;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
@Component("tokenIntrospection")
public class TokenIntrospectionView extends AbstractView {
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) {
Gson gson = new GsonBuilder().setExclusionStrategies(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes f) {
/*
if (f.getDeclaringClass().isAssignableFrom(OAuth2AccessTokenEntity.class)) {
// we don't want to serialize the whole object, just the scope and timeout
if (f.getName().equals("scope")) {
return false;
} else if (f.getName().equals("expiration")) {
return false;
} else {
// skip everything else on this class
return true;
}
} else {
// serialize other classes without filter (lists and sets and things)
return false;
}
*/
return false;
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
// skip the JPA binding wrapper
if (clazz.equals(BeanPropertyBindingResult.class)) {
return true;
} else {
return false;
}
}
})
.registerTypeAdapter(OAuth2AccessTokenEntity.class, new JsonSerializer<OAuth2AccessTokenEntity>() {
@Override
public JsonElement serialize(OAuth2AccessTokenEntity src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject token = new JsonObject();
token.addProperty("active", true);
token.addProperty("scope", Joiner.on(" ").join(src.getScope()));
token.add("exp", context.serialize(src.getExpiration()));
//token.addProperty("audience", src.getAuthenticationHolder().getAuthentication().getAuthorizationRequest().getClientId());
token.addProperty("sub", src.getAuthenticationHolder().getAuthentication().getName());
token.addProperty("client_id", src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getClientId());
token.addProperty("token_type", src.getTokenType());
return token;
}
})
.registerTypeAdapter(OAuth2RefreshTokenEntity.class, new JsonSerializer<OAuth2RefreshTokenEntity>() {
@Override
public JsonElement serialize(OAuth2RefreshTokenEntity src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject token = new JsonObject();
token.addProperty("active", true);
token.addProperty("scope", Joiner.on(" ").join(src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope()));
token.add("exp", context.serialize(src.getExpiration()));
//token.addProperty("audience", src.getAuthenticationHolder().getAuthentication().getAuthorizationRequest().getClientId());
token.addProperty("sub", src.getAuthenticationHolder().getAuthentication().getName());
token.addProperty("client_id", src.getAuthenticationHolder().getAuthentication().getOAuth2Request().getClientId());
return token;
}
})
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.create();
response.setContentType("application/json");
Writer out;
@ -138,13 +58,16 @@ public class TokenIntrospectionView extends AbstractView {
try {
out = response.getWriter();
Object obj = model.get("entity");
if (obj == null) {
obj = model;
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");
}
gson.toJson(obj, out);
} catch (IOException e) {
logger.error("IOException occurred in TokenIntrospectionView.java: ", e);
@ -153,4 +76,66 @@ public class TokenIntrospectionView extends AbstractView {
}
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

@ -26,6 +26,8 @@ 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.openid.connect.model.UserInfo;
import org.mitre.openid.connect.service.UserInfoService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -51,6 +53,9 @@ public class IntrospectionEndpoint {
@Autowired
private IntrospectionAuthorizer introspectionAuthorizer;
@Autowired
private UserInfoService userInfoService;
private static Logger logger = LoggerFactory.getLogger(IntrospectionEndpoint.class);
@ -76,10 +81,14 @@ public class IntrospectionEndpoint {
return "jsonEntityView";
}
// 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;
try {
@ -91,6 +100,8 @@ public class IntrospectionEndpoint {
token = access;
user = userInfoService.getByUsernameAndClientId(access.getAuthenticationHolder().getAuthentication().getName(), tokenClient.getClientId());
} catch (InvalidTokenException e) {
logger.error("Verify failed; Invalid access token. Checking refresh token.", e);
try {
@ -101,6 +112,8 @@ public class IntrospectionEndpoint {
tokenClient = refresh.getClient();
scopes = refresh.getAuthenticationHolder().getAuthentication().getOAuth2Request().getScope();
user = userInfoService.getByUsernameAndClientId(refresh.getAuthenticationHolder().getAuthentication().getName(), tokenClient.getClientId());
token = refresh;
} catch (InvalidTokenException e2) {
@ -111,15 +124,12 @@ public class IntrospectionEndpoint {
}
}
// clientID is the principal name in the authentication
String clientId = p.getName();
ClientDetailsEntity authClient = clientService.loadClientByClientId(clientId);
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("entity", token);
model.addAttribute("token", token);
model.addAttribute("user", user);
return "tokenIntrospection";
} else {
logger.error("Verify failed; client configuration or scope don't permit token introspection");