KNOX-3028 - add support for OAuth Token Exchange to KNOXTOKEN (#900)
* KNOX-3028 - add support for OAuth Token Exchange to KNOXTOKEN
diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/OAuthResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/OAuthResource.java
new file mode 100644
index 0000000..71cf28b
--- /dev/null
+++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/OAuthResource.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.knox.gateway.service.knoxtoken;
+
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.util.JsonUtils;
+
+import javax.inject.Singleton;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+import java.util.HashMap;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+
+@Singleton
+@Path(OAuthResource.RESOURCE_PATH)
+public class OAuthResource extends TokenResource {
+ private static TokenServiceMessages log = MessagesFactory.get(TokenServiceMessages.class);
+ static final String RESOURCE_PATH = "/{serviceName:.*}/v1/{oauthSegment:(oauth|token)}{path:(/tokens)?}";
+ public static final String ISSUED_TOKEN_TYPE = "issued_token_type";
+ public static final String REFRESH_TOKEN = "refresh_token";
+ public static final String ISSUED_TOKEN_TYPE_ACCESS_TOKEN_VALUE = "urn:ietf:params:oauth:token-type:access_token";
+
+ @Override
+ @GET
+ @Produces({ APPLICATION_JSON, APPLICATION_XML })
+ public Response doGet() {
+ return super.doGet();
+ }
+
+ @Override
+ @POST
+ @Produces({ APPLICATION_JSON, APPLICATION_XML })
+ public Response doPost() {
+ return super.doPost();
+ }
+
+ @Override
+ public Response getAuthenticationToken() {
+
+ Response response = enforceClientCertIfRequired();
+ if (response != null) { return response; }
+
+ response = onlyAllowGroupsToBeAddedWhenEnabled();
+ if (response != null) { return response; }
+
+ UserContext context = buildUserContext(request);
+
+ response = enforceTokenLimitsAsRequired(context.userName);
+ if (response != null) { return response; }
+
+ TokenResponseContext resp = getTokenResponse(context);
+ // if the responseMap isn't null then the knoxtoken request was successful
+ // if not then there may have been an error and the underlying response
+ // builder will communicate those details
+ if (resp.responseMap != null) {
+ // let's get the subset of the KnoxToken Response needed for OAuth
+ String accessToken = resp.responseMap.accessToken;
+ String passcode = resp.responseMap.passcode;
+ long expires = (long) resp.responseMap.map.get(EXPIRES_IN);
+ String tokenType = (String) resp.responseMap.map.get(TOKEN_TYPE);
+
+ // build and return the expected OAuth response
+ final HashMap<String, Object> map = new HashMap<>();
+ map.put(ACCESS_TOKEN, accessToken);
+ map.put(TOKEN_TYPE, tokenType);
+ map.put(EXPIRES_IN, expires);
+ map.put(ISSUED_TOKEN_TYPE, ISSUED_TOKEN_TYPE_ACCESS_TOKEN_VALUE);
+ // let's use the passcode as the refresh token
+ map.put(REFRESH_TOKEN, passcode);
+ String jsonResponse = JsonUtils.renderAsJsonString(map);
+ return resp.responseBuilder.entity(jsonResponse).build();
+ }
+ // there was an error if we got here - let's surface it appropriately
+ // TODO: LJM we may need to translate certain errors into OAuth error messages
+ if (resp.responseStr != null) {
+ return resp.responseBuilder.entity(resp.responseStr).build();
+ }
+ else {
+ return resp.responseBuilder.build();
+ }
+ }
+
+ @Override
+ protected long getExpiry() {
+ long secs = tokenTTL/1000;
+
+ String lifetimeStr = request.getParameter(LIFESPAN);
+ if (lifetimeStr == null || lifetimeStr.isEmpty()) {
+ if (tokenTTL == -1) {
+ return -1;
+ }
+ }
+ else {
+ try {
+ long lifetime = Duration.parse(lifetimeStr).toMillis()/1000;
+ if (tokenTTL == -1) {
+ // if TTL is set to -1 the topology owner grants unlimited lifetime therefore no additional check is needed on lifespan
+ secs = lifetime;
+ } else if (lifetime <= tokenTTL/1000) {
+ //this is expected due to security reasons: the configured TTL acts as an upper limit regardless of the supplied lifespan
+ secs = lifetime;
+ }
+ }
+ catch (DateTimeParseException e) {
+ log.invalidLifetimeValue(lifetimeStr);
+ }
+ }
+ return secs;
+ }
+}
diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
index 8698ce8..b5703ae 100644
--- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
+++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
@@ -108,15 +108,15 @@
public class TokenResource {
static final String LIFESPAN = "lifespan";
static final String COMMENT = "comment";
- private static final String EXPIRES_IN = "expires_in";
- private static final String TOKEN_TYPE = "token_type";
- private static final String ACCESS_TOKEN = "access_token";
- private static final String TOKEN_ID = "token_id";
+ protected static final String EXPIRES_IN = "expires_in";
+ protected static final String TOKEN_TYPE = "token_type";
+ protected static final String ACCESS_TOKEN = "access_token";
+ protected static final String TOKEN_ID = "token_id";
static final String PASSCODE = "passcode";
- private static final String MANAGED_TOKEN = "managed";
+ protected static final String MANAGED_TOKEN = "managed";
private static final String TARGET_URL = "target_url";
private static final String ENDPOINT_PUBLIC_CERT = "endpoint_public_cert";
- private static final String BEARER = "Bearer";
+ protected static final String BEARER = "Bearer";
private static final String TOKEN_PARAM_PREFIX = "knox.token.";
private static final String TOKEN_TTL_PARAM = TOKEN_PARAM_PREFIX + "ttl";
private static final String TOKEN_TYPE_PARAM = TOKEN_PARAM_PREFIX + "type";
@@ -160,7 +160,7 @@
public static final String KNOX_TOKEN_ISSUER = TOKEN_PARAM_PREFIX + "issuer";
private static TokenServiceMessages log = MessagesFactory.get(TokenServiceMessages.class);
private static final Gson GSON = new Gson();
- private long tokenTTL = TOKEN_TTL_DEFAULT;
+ protected long tokenTTL = TOKEN_TTL_DEFAULT;
private String tokenType;
private String tokenTTLAsText;
private List<String> targetAudiences = new ArrayList<>();
@@ -172,7 +172,7 @@
private String endpointPublicCert;
// Optional token store service
- private TokenStateService tokenStateService;
+ protected TokenStateService tokenStateService;
private TokenMAC tokenMAC;
private final Map<String, String> tokenStateServiceStatusMap = new HashMap<>();
@@ -185,6 +185,7 @@
private String tokenIssuer;
enum UserLimitExceededAction {REMOVE_OLDEST, RETURN_ERROR};
+
private UserLimitExceededAction userLimitExceededAction = UserLimitExceededAction.RETURN_ERROR;
private List<String> allowedRenewers;
@@ -362,7 +363,7 @@
final GatewayConfig config = (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE);
final String configuredTokenStateServiceImpl = config.getServiceParameter(ServiceType.TOKEN_STATE_SERVICE.getShortName(), "impl");
final String configuredTokenServiceName = StringUtils.isBlank(configuredTokenStateServiceImpl) ? ""
- : configuredTokenStateServiceImpl.substring(configuredTokenStateServiceImpl.lastIndexOf('.') + 1);
+ : configuredTokenStateServiceImpl.substring(configuredTokenStateServiceImpl.lastIndexOf('.') + 1);
final String actualTokenStateServiceImpl = tokenStateService.getClass().getCanonicalName();
final String actualTokenServiceName = actualTokenStateServiceImpl.substring(actualTokenStateServiceImpl.lastIndexOf('.') + 1);
tokenStateServiceStatusMap.put(TSS_STATUS_CONFIFURED_BACKEND, configuredTokenServiceName);
@@ -483,7 +484,7 @@
tokens.addAll(userTokens);
} else {
userTokens.forEach(knoxToken -> {
- for (Map.Entry<String, List<String>> entry : metadataMap.entrySet()) {
+ for (Map.Entry<String, List<String>> entry : metadataMap.entrySet()) {
if (entry.getValue().contains("*")) {
// we should only filter tokens by metadata name
if (knoxToken.hasMetadata(entry.getKey())) {
@@ -504,15 +505,15 @@
@GET
@Path(GET_TSS_STATUS_PATH)
- @Produces({ APPLICATION_JSON })
+ @Produces({APPLICATION_JSON})
public Response getTokenStateServiceStatus() {
return Response.status(Response.Status.OK).entity(JsonUtils.renderAsJsonString(tokenStateServiceStatusMap)).build();
}
/**
* @deprecated This method is no longer acceptable for token renewal. Please
- * use the '/knoxtoken/v2/api/token/renew' path; instead which is a
- * PUT HTTP request.
+ * use the '/knoxtoken/v2/api/token/renew' path; instead which is a
+ * PUT HTTP request.
*/
@POST
@Path(RENEW_PATH)
@@ -523,8 +524,8 @@
long expiration = 0;
- String error = "";
- ErrorCode errorCode = ErrorCode.UNKNOWN;
+ String error = "";
+ ErrorCode errorCode = ErrorCode.UNKNOWN;
Response.Status errorStatus = Response.Status.BAD_REQUEST;
if (tokenStateService == null) {
@@ -532,8 +533,8 @@
try {
JWTToken jwt = new JWTToken(token);
log.renewalDisabled(getTopologyName(),
- Tokens.getTokenDisplayText(token),
- Tokens.getTokenIDDisplayText(TokenUtils.getTokenId(jwt)));
+ Tokens.getTokenDisplayText(token),
+ Tokens.getTokenIDDisplayText(TokenUtils.getTokenId(jwt)));
expiration = Long.parseLong(jwt.getExpires());
} catch (ParseException e) {
log.invalidToken(getTopologyName(), Tokens.getTokenDisplayText(token), e);
@@ -571,15 +572,15 @@
}
}
- if(error.isEmpty()) {
- resp = Response.status(Response.Status.OK)
- .entity("{\n \"renewed\": \"true\",\n \"expires\": \"" + expiration + "\"\n}\n")
- .build();
+ if (error.isEmpty()) {
+ resp = Response.status(Response.Status.OK)
+ .entity("{\n \"renewed\": \"true\",\n \"expires\": \"" + expiration + "\"\n}\n")
+ .build();
} else {
log.badRenewalRequest(getTopologyName(), Tokens.getTokenDisplayText(token), error);
resp = Response.status(errorStatus)
- .entity("{\n \"renewed\": \"false\",\n \"error\": \"" + error + "\",\n \"code\": " + errorCode.toInt() + "\n}\n")
- .build();
+ .entity("{\n \"renewed\": \"false\",\n \"error\": \"" + error + "\",\n \"code\": " + errorCode.toInt() + "\n}\n")
+ .build();
}
return resp;
@@ -603,8 +604,8 @@
/**
* @deprecated This method is no longer acceptable for token revocation. Please
- * use the '/knoxtoken/v2/api/token/revoke' path; instead which is a
- * DELETE HTTP request.
+ * use the '/knoxtoken/v2/api/token/revoke' path; instead which is a
+ * DELETE HTTP request.
*/
@POST
@Path(REVOKE_PATH)
@@ -613,8 +614,8 @@
public Response revoke(String token) {
Response resp;
- String error = "";
- ErrorCode errorCode = ErrorCode.UNKNOWN;
+ String error = "";
+ ErrorCode errorCode = ErrorCode.UNKNOWN;
Response.Status errorStatus = Response.Status.BAD_REQUEST;
if (tokenStateService == null) {
@@ -626,14 +627,14 @@
final String tokenId = getTokenId(token);
if (isKnoxSsoCookie(tokenId)) {
errorStatus = Response.Status.FORBIDDEN;
- error = "SSO cookie (" + Tokens.getTokenIDDisplayText(tokenId) + ") cannot not be revoked." ;
+ error = "SSO cookie (" + Tokens.getTokenIDDisplayText(tokenId) + ") cannot not be revoked.";
errorCode = ErrorCode.UNAUTHORIZED;
} else if (triesToRevokeOwnToken(tokenId, revoker) || allowedRenewers.contains(revoker)) {
tokenStateService.revokeToken(tokenId);
log.revokedToken(getTopologyName(),
- Tokens.getTokenDisplayText(token),
- Tokens.getTokenIDDisplayText(tokenId),
- revoker);
+ Tokens.getTokenDisplayText(token),
+ Tokens.getTokenIDDisplayText(tokenId),
+ revoker);
} else {
errorStatus = Response.Status.FORBIDDEN;
error = "Caller (" + revoker + ") not authorized to revoke tokens.";
@@ -650,14 +651,14 @@
}
if (error.isEmpty()) {
- resp = Response.status(Response.Status.OK)
- .entity("{\n \"revoked\": \"true\"\n}\n")
- .build();
+ resp = Response.status(Response.Status.OK)
+ .entity("{\n \"revoked\": \"true\"\n}\n")
+ .build();
} else {
log.badRevocationRequest(getTopologyName(), Tokens.getTokenDisplayText(token), error);
resp = Response.status(errorStatus)
- .entity("{\n \"revoked\": \"false\",\n \"error\": \"" + error + "\",\n \"code\": " + errorCode.toInt() + "\n}\n")
- .build();
+ .entity("{\n \"revoked\": \"false\",\n \"error\": \"" + error + "\",\n \"code\": " + errorCode.toInt() + "\n}\n")
+ .build();
}
return resp;
@@ -671,7 +672,7 @@
private boolean triesToRevokeOwnToken(String tokenId, String revoker) throws UnknownTokenException {
final TokenMetadata metadata = tokenStateService.getTokenMetadata(tokenId);
final String tokenUserName = metadata == null ? "" : metadata.getUserName();
- final String tokenCreatedBy = metadata == null ? "" : metadata.getCreatedBy();
+ final String tokenCreatedBy = metadata == null ? "" : metadata.getCreatedBy();
return StringUtils.isNotBlank(revoker) && (revoker.equals(tokenUserName) || revoker.equals(tokenCreatedBy));
}
@@ -693,30 +694,30 @@
@PUT
@Path(ENABLE_PATH)
- @Produces({ APPLICATION_JSON })
+ @Produces({APPLICATION_JSON})
public Response enable(String tokenId) {
return setTokenEnabledFlag(tokenId, true, false);
}
@PUT
@Path(BATCH_ENABLE_PATH)
- @Consumes({ APPLICATION_JSON })
- @Produces({ APPLICATION_JSON })
+ @Consumes({APPLICATION_JSON})
+ @Produces({APPLICATION_JSON})
public Response enableTokens(String tokenIds) {
return setTokenEnabledFlags(tokenIds, true);
}
@PUT
@Path(DISABLE_PATH)
- @Produces({ APPLICATION_JSON })
+ @Produces({APPLICATION_JSON})
public Response disable(String tokenId) {
return setTokenEnabledFlag(tokenId, false, false);
}
@PUT
@Path(BATCH_DISABLE_PATH)
- @Consumes({ APPLICATION_JSON })
- @Produces({ APPLICATION_JSON })
+ @Consumes({APPLICATION_JSON})
+ @Produces({APPLICATION_JSON})
public Response disableTokens(String tokenIds) {
return setTokenEnabledFlags(tokenIds, false);
}
@@ -780,26 +781,97 @@
return null;
}
- private Response getAuthenticationToken() {
- if (clientCertRequired) {
- X509Certificate cert = extractCertificate(request);
- if (cert != null) {
- if (!allowedDNs.contains(cert.getSubjectDN().getName().replaceAll("\\s+", ""))) {
- return Response.status(Response.Status.FORBIDDEN)
- .entity("{ \"Unable to get token - untrusted client cert.\" }")
- .build();
- }
+ protected Response getAuthenticationToken() {
+ Response response = enforceClientCertIfRequired();
+ if (response != null) { return response; }
+
+ response = onlyAllowGroupsToBeAddedWhenEnabled();
+ if (response != null) { return response; }
+
+ UserContext context = buildUserContext(request);
+
+ response = enforceTokenLimitsAsRequired(context.userName);
+ if (response != null) { return response; }
+
+ TokenResponseContext resp = getTokenResponse(context);
+ return resp.build();
+ }
+
+ protected TokenResponseContext getTokenResponse(UserContext context) {
+ TokenResponseContext response = null;
+ long expires = getExpiry();
+ setupPublicCertPEM();
+ String jku = getJku();
+ try
+ {
+ JWT token = getJWT(context.userName, expires, jku);
+ if (token != null) {
+ ResponseMap result = buildResponseMap(token, expires);
+ String jsonResponse = JsonUtils.renderAsJsonString(result.map);
+ persistTokenDetails(result, expires, context.userName, context.createdBy);
+
+ response = new TokenResponseContext(result, jsonResponse, Response.ok());
} else {
- return Response.status(Response.Status.FORBIDDEN)
- .entity("{ \"Unable to get token - client cert required.\" }")
- .build();
+ response = new TokenResponseContext(null, null, Response.serverError());
}
+ } catch (TokenServiceException e) {
+ log.unableToIssueToken(e);
+ response = new TokenResponseContext(null
+ , "{ \"Unable to acquire token.\" }"
+ , Response.serverError());
}
- GatewayServices services = (GatewayServices) request.getServletContext()
+ return response;
+ }
+
+ protected static class TokenResponseContext {
+ public ResponseMap responseMap;
+ public String responseStr;
+ public Response.ResponseBuilder responseBuilder;
+
+ public TokenResponseContext(ResponseMap respMap, String resp, Response.ResponseBuilder builder) {
+ responseMap = respMap;
+ responseStr = resp;
+ responseBuilder = builder;
+ }
+
+ public Response build() {
+ Response response = null;
+ if (responseStr != null) {
+ response = responseBuilder.entity(responseStr).build();
+ } else {
+ response = responseBuilder.build();
+ }
+ return response;
+ }
+ }
+
+ protected GatewayServices getGatewayServices() {
+ return (GatewayServices) request.getServletContext()
.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ }
- JWTokenAuthority ts = services.getService(ServiceType.TOKEN_SERVICE);
+ protected String getJku() {
+ String jku = null;
+ /* remove .../token and replace it with ..../jwks.json */
+ final int idx = request.getRequestURL().lastIndexOf("/");
+ if(idx > 1) {
+ jku = request.getRequestURL().substring(0, idx) + JWKSResource.JWKS_PATH;
+ }
+ return jku;
+ }
+ protected Response onlyAllowGroupsToBeAddedWhenEnabled() {
+ Response response = null;
+ if (shouldIncludeGroups() && !includeGroupsInTokenAllowed) {
+ response = Response
+ .status(Response.Status.BAD_REQUEST)
+ .entity("{\n \"error\": \"Including group information in tokens is disabled\"\n}\n")
+ .build();
+ }
+ return response;
+ }
+
+ protected UserContext buildUserContext(HttpServletRequest request) {
String userName = request.getUserPrincipal().getName();
String createdBy = null;
// checking the doAs user only makes sense if tokens are managed (this is where we store the userName/createdBy information)
@@ -816,9 +888,52 @@
}
}
}
+ return new UserContext(userName, createdBy);
+ }
- long expires = getExpiry();
+ protected static class UserContext {
+ public final String userName;
+ public final String createdBy;
+ public UserContext(String userName, String createdBy) {
+ this.userName = userName;
+ this.createdBy = createdBy;
+ }
+ }
+
+ protected Response enforceTokenLimitsAsRequired(String userName) {
+ Response response = null;
+ if (tokenStateService != null) {
+ if (tokenLimitPerUser != -1) { // if -1 => unlimited tokens for all users
+ final Collection<KnoxToken> allUserTokens = tokenStateService.getTokens(userName);
+ final Collection<KnoxToken> userTokens = new LinkedList<>();
+ allUserTokens.stream().forEach(token -> {
+ if(!token.getMetadata().isKnoxSsoCookie()) {
+ userTokens.add(token);
+ }
+ });
+ if (userTokens.size() >= tokenLimitPerUser) {
+ log.tokenLimitExceeded(userName);
+ if (UserLimitExceededAction.RETURN_ERROR == userLimitExceededAction) {
+ response = Response.status(Response.Status.FORBIDDEN).entity("{ \"Unable to get token - token limit exceeded.\" }").build();
+ } else {
+ // userTokens is an ordered collection (by issue time) -> the first element is the oldest one
+ final String oldestTokenId = userTokens.iterator().next().getTokenId();
+ log.generalInfoMessage(String.format(Locale.getDefault(), "Revoking %s's oldest token %s ...", userName, Tokens.getTokenIDDisplayText(oldestTokenId)));
+ final Response revocationResponse = revoke(oldestTokenId);
+ if (Response.Status.OK.getStatusCode() != revocationResponse.getStatus()) {
+ response = Response.status(Response.Status.fromStatusCode(revocationResponse.getStatus()))
+ .entity("{\n \"error\": \"An error occurred during the oldest token revocation of " + userName + " \"\n}\n").build();
+ }
+ }
+ }
+ }
+ }
+ return response;
+ }
+
+ protected void setupPublicCertPEM() {
+ GatewayServices services = getGatewayServices();
if (endpointPublicCert == null) {
// acquire PEM for gateway identity of this gateway instance
KeystoreService ks = services.getService(ServiceType.KEYSTORE_SERVICE);
@@ -833,125 +948,115 @@
}
}
}
+ }
- String jku = null;
- /* remove .../token and replace it with ..../jwks.json */
- final int idx = request.getRequestURL().lastIndexOf("/");
- if(idx > 1) {
- jku = request.getRequestURL().substring(0, idx) + JWKSResource.JWKS_PATH;
- }
-
- if (tokenStateService != null) {
- if (tokenLimitPerUser != -1) { // if -1 => unlimited tokens for all users
- final Collection<KnoxToken> allUserTokens = tokenStateService.getTokens(userName);
- final Collection<KnoxToken> userTokens = new LinkedList<>();
- allUserTokens.stream().forEach(token -> {
- if(!token.getMetadata().isKnoxSsoCookie()) {
- userTokens.add(token);
- }
- });
- if (userTokens.size() >= tokenLimitPerUser) {
- log.tokenLimitExceeded(userName);
- if (UserLimitExceededAction.RETURN_ERROR == userLimitExceededAction) {
- return Response.status(Response.Status.FORBIDDEN).entity("{ \"Unable to get token - token limit exceeded.\" }").build();
- } else {
- // userTokens is an ordered collection (by issue time) -> the first element is the oldest one
- final String oldestTokenId = userTokens.iterator().next().getTokenId();
- log.generalInfoMessage(String.format(Locale.getDefault(), "Revoking %s's oldest token %s ...", userName, Tokens.getTokenIDDisplayText(oldestTokenId)));
- final Response revocationResponse = revoke(oldestTokenId);
- if (Response.Status.OK.getStatusCode() != revocationResponse.getStatus()) {
- return Response.status(Response.Status.fromStatusCode(revocationResponse.getStatus()))
- .entity("{\n \"error\": \"An error occurred during the oldest token revocation of " + userName + " \"\n}\n").build();
- }
- }
+ protected Response enforceClientCertIfRequired() {
+ Response response = null;
+ if (clientCertRequired) {
+ X509Certificate cert = extractCertificate(request);
+ if (cert != null) {
+ if (!allowedDNs.contains(cert.getSubjectDN().getName().replaceAll("\\s+", ""))) {
+ response = Response.status(Response.Status.FORBIDDEN)
+ .entity("{ \"Unable to get token - untrusted client cert.\" }")
+ .build();
}
- }
- }
-
- try {
- final boolean managedToken = tokenStateService != null;
- JWT token;
- JWTokenAttributes jwtAttributes;
- final JWTokenAttributesBuilder jwtAttributesBuilder = new JWTokenAttributesBuilder();
- jwtAttributesBuilder
- .setIssuer(tokenIssuer)
- .setUserName(userName)
- .setAlgorithm(signatureAlgorithm)
- .setExpires(expires)
- .setManaged(managedToken)
- .setJku(jku)
- .setType(tokenType);
- if (!targetAudiences.isEmpty()) {
- jwtAttributesBuilder.setAudiences(targetAudiences);
- }
- if (shouldIncludeGroups()) {
- if (includeGroupsInTokenAllowed) {
- jwtAttributesBuilder.setGroups(groups());
- } else {
- return Response
- .status(Response.Status.BAD_REQUEST)
- .entity("{\n \"error\": \"Including group information in tokens is disabled\"\n}\n")
- .build();
- }
- }
-
- jwtAttributes = jwtAttributesBuilder.build();
- token = ts.issueToken(jwtAttributes);
-
- if (token != null) {
- String accessToken = token.toString();
- String tokenId = TokenUtils.getTokenId(token);
- log.issuedToken(getTopologyName(), Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId));
-
- final HashMap<String, Object> map = new HashMap<>();
- map.put(ACCESS_TOKEN, accessToken);
- map.put(TOKEN_ID, tokenId);
- map.put(MANAGED_TOKEN, String.valueOf(managedToken));
- map.put(TOKEN_TYPE, BEARER);
- map.put(EXPIRES_IN, expires);
- if (tokenTargetUrl != null) {
- map.put(TARGET_URL, tokenTargetUrl);
- }
- if (tokenClientDataMap != null) {
- map.putAll(tokenClientDataMap);
- }
- if (endpointPublicCert != null) {
- map.put(ENDPOINT_PUBLIC_CERT, endpointPublicCert);
- }
-
- final String passcode = UUID.randomUUID().toString();
- if (tokenStateService != null && tokenStateService instanceof PersistentTokenStateService) {
- map.put(PASSCODE, generatePasscodeField(tokenId, passcode));
- }
-
- String jsonResponse = JsonUtils.renderAsJsonString(map);
-
- // Optional token store service persistence
- if (tokenStateService != null) {
- final long issueTime = System.currentTimeMillis();
- tokenStateService.addToken(tokenId,
- issueTime,
- expires,
- maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration()));
- final String comment = request.getParameter(COMMENT);
- final TokenMetadata tokenMetadata = new TokenMetadata(userName, StringUtils.isBlank(comment) ? null : comment);
- tokenMetadata.setPasscode(tokenMAC.hash(tokenId, issueTime, userName, passcode));
- addArbitraryTokenMetadata(tokenMetadata);
- if (createdBy != null) {
- tokenMetadata.setCreatedBy(createdBy);
- }
- tokenStateService.addMetadata(tokenId, tokenMetadata);
- log.storedToken(getTopologyName(), Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId));
- }
-
- return Response.ok().entity(jsonResponse).build();
} else {
- return Response.serverError().build();
+ response = Response.status(Response.Status.FORBIDDEN)
+ .entity("{ \"Unable to get token - client cert required.\" }")
+ .build();
}
- } catch (TokenServiceException e) {
- log.unableToIssueToken(e);
}
- return Response.ok().entity("{ \"Unable to acquire token.\" }").build();
+ return response;
+ }
+
+ protected void persistTokenDetails(ResponseMap result, long expires, String userName, String createdBy) {
+ // Optional token store service persistence
+ if (tokenStateService != null) {
+ final long issueTime = System.currentTimeMillis();
+ tokenStateService.addToken(result.tokenId,
+ issueTime,
+ expires,
+ maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration()));
+ final String comment = request.getParameter(COMMENT);
+ final TokenMetadata tokenMetadata = new TokenMetadata(userName, StringUtils.isBlank(comment) ? null : comment);
+ tokenMetadata.setPasscode(tokenMAC.hash(result.tokenId, issueTime, userName, result.passcode));
+ addArbitraryTokenMetadata(tokenMetadata);
+ if (createdBy != null) {
+ tokenMetadata.setCreatedBy(createdBy);
+ }
+ tokenStateService.addMetadata(result.tokenId, tokenMetadata);
+ log.storedToken(getTopologyName(), Tokens.getTokenDisplayText(result.accessToken), Tokens.getTokenIDDisplayText(result.tokenId));
+ }
+ }
+
+ protected ResponseMap buildResponseMap(JWT token, long expires) {
+ String accessToken = token.toString();
+ String tokenId = TokenUtils.getTokenId(token);
+ final boolean managedToken = tokenStateService != null;
+
+ log.issuedToken(getTopologyName(), Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId));
+
+ final Map<String, Object> map = new HashMap<>();
+ map.put(ACCESS_TOKEN, accessToken);
+ map.put(TOKEN_ID, tokenId);
+ map.put(MANAGED_TOKEN, String.valueOf(managedToken));
+ map.put(TOKEN_TYPE, BEARER);
+ map.put(EXPIRES_IN, expires);
+ if (tokenTargetUrl != null) {
+ map.put(TARGET_URL, tokenTargetUrl);
+ }
+ if (tokenClientDataMap != null) {
+ map.putAll(tokenClientDataMap);
+ }
+ if (endpointPublicCert != null) {
+ map.put(ENDPOINT_PUBLIC_CERT, endpointPublicCert);
+ }
+
+ final String passcode = UUID.randomUUID().toString();
+ if (tokenStateService != null && tokenStateService instanceof PersistentTokenStateService) {
+ map.put(PASSCODE, generatePasscodeField(tokenId, passcode));
+ }
+ return new ResponseMap(accessToken, tokenId, map, passcode);
+ }
+
+ protected static class ResponseMap {
+ public final String accessToken;
+ public final String tokenId;
+ public final Map<String, Object> map;
+ public final String passcode;
+
+ public ResponseMap(String accessToken, String tokenId, Map<String, Object> map, String passcode) {
+ this.accessToken = accessToken;
+ this.tokenId = tokenId;
+ this.map = map;
+ this.passcode = passcode;
+ }
+ }
+
+ protected JWT getJWT(String userName, long expires, String jku) throws TokenServiceException {
+ JWTokenAttributes jwtAttributes;
+ JWT token;
+ JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE);
+ final boolean managedToken = tokenStateService != null;
+ final JWTokenAttributesBuilder jwtAttributesBuilder = new JWTokenAttributesBuilder();
+ jwtAttributesBuilder
+ .setIssuer(tokenIssuer)
+ .setUserName(userName)
+ .setAlgorithm(signatureAlgorithm)
+ .setExpires(expires)
+ .setManaged(managedToken)
+ .setJku(jku)
+ .setType(tokenType);
+ if (!targetAudiences.isEmpty()) {
+ jwtAttributesBuilder.setAudiences(targetAudiences);
+ }
+ if (shouldIncludeGroups()) {
+ jwtAttributesBuilder.setGroups(groups());
+ }
+
+ jwtAttributes = jwtAttributesBuilder.build();
+ token = ts.issueToken(jwtAttributes);
+ return token;
}
private boolean shouldIncludeGroups() {
@@ -995,7 +1100,7 @@
}
}
- private long getExpiry() {
+ protected long getExpiry() {
long expiry = 0L;
long millis = tokenTTL;
@@ -1004,8 +1109,7 @@
if (tokenTTL == -1) {
return -1;
}
- }
- else {
+ } else {
try {
long lifetime = Duration.parse(lifetimeStr).toMillis();
if (tokenTTL == -1) {
diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/deploy/TokenServiceDeploymentContributor.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/deploy/TokenServiceDeploymentContributor.java
index b46ddeb..e80db1d 100644
--- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/deploy/TokenServiceDeploymentContributor.java
+++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/deploy/TokenServiceDeploymentContributor.java
@@ -40,6 +40,6 @@
@Override
protected String[] getPatterns() {
- return new String[]{ "knoxtoken/api/**?**" };
+ return new String[]{ "knoxtoken/api/**?**", "/oauth/v1/token/", "/**/v1/oauth/tokens/" };
}
}
diff --git a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
index 169650e..b072b8d 100644
--- a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
+++ b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java
@@ -1346,6 +1346,45 @@
}
}
+ @Test
+ public void testOAuthTokenResponse() throws Exception {
+ Map<String, String> contextExpectations = new HashMap<>();
+ configureCommonExpectations(contextExpectations, Boolean.TRUE);
+
+ OAuthResource or = new OAuthResource();
+ or.request = request;
+ or.context = context;
+ or.init();
+
+ Response response = or.doPost();
+ assertEquals(200, response.getStatus());
+
+ String accessToken = getTagValue(response.getEntity().toString(), TokenResource.ACCESS_TOKEN);
+ assertNotNull(accessToken);
+ String expiresIn = getTagValue(response.getEntity().toString(), TokenResource.EXPIRES_IN);
+ // default value for TTL for KNOXTOKEN is 30 secs - OAuth response has this in secs
+ assertEquals("30", expiresIn);
+ // there is no passcode or token_id in OAuth responses
+ String passcode = getTagValue(response.getEntity().toString(), TokenResource.PASSCODE);
+ assertNull(passcode);
+ String tokenId = getTagValue(response.getEntity().toString(), TokenResource.TOKEN_ID);
+ assertNull(tokenId);
+ String tokenType = getTagValue(response.getEntity().toString(), TokenResource.TOKEN_TYPE);
+ assertEquals(TokenResource.BEARER, tokenType);
+ // oauth requires issued token type so we are hardcoding this
+ String issuedTokenType = getTagValue(response.getEntity().toString(), OAuthResource.ISSUED_TOKEN_TYPE);
+ assertEquals(OAuthResource.ISSUED_TOKEN_TYPE_ACCESS_TOKEN_VALUE, issuedTokenType);
+ // oauth credentials flow sometimes requires a refresh token even though they can just get a
+ // new access_token with the client_id and client_secret. Since this token service can't actually
+ // assume the credentials flow is being used even though it is most likely, we will include the
+ // passcode as the refresh token
+ String refreshToken = getTagValue(response.getEntity().toString(), OAuthResource.REFRESH_TOKEN);
+ assertNotNull(refreshToken);
+
+ Map<String, Object> payload = parseJSONResponse(JWTToken.parseToken(accessToken).getPayload());
+ assertFalse(payload.containsKey(KNOX_GROUPS_CLAIM));
+ }
+
/**
*
* @param isTokenStateServerManaged true, if server-side token state management should be enabled; Otherwise, false or null.
@@ -1621,7 +1660,7 @@
if (!token.contains(tagName)) {
return null;
}
- String searchString = tagName + "\":";
+ String searchString = "\"" + tagName + "\":";
String value = token.substring(token.indexOf(searchString) + searchString.length());
if (value.startsWith("\"")) {
value = value.substring(1);