This is an automated email from the ASF dual-hosted git repository.
lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new d74fb4f84 KNOX-3028 - add support for OAuth Token Exchange to
KNOXTOKEN (#900)
d74fb4f84 is described below
commit d74fb4f8492191d24ab556fbefd50bbf0ebc8ad8
Author: lmccay <[email protected]>
AuthorDate: Mon Apr 15 13:55:16 2024 -0400
KNOX-3028 - add support for OAuth Token Exchange to KNOXTOKEN (#900)
* KNOX-3028 - add support for OAuth Token Exchange to KNOXTOKEN
---
.../gateway/service/knoxtoken/OAuthResource.java | 133 +++++++
.../gateway/service/knoxtoken/TokenResource.java | 420 +++++++++++++--------
.../deploy/TokenServiceDeploymentContributor.java | 2 +-
.../knoxtoken/TokenServiceResourceTest.java | 41 +-
4 files changed, 436 insertions(+), 160 deletions(-)
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 000000000..71cf28b2d
--- /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 8698ce842..b5703aec3 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 @@ import static javax.ws.rs.core.MediaType.APPLICATION_XML;
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 class TokenResource {
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 @@ public class TokenResource {
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 @@ public class TokenResource {
private String tokenIssuer;
enum UserLimitExceededAction {REMOVE_OLDEST, RETURN_ERROR};
+
private UserLimitExceededAction userLimitExceededAction =
UserLimitExceededAction.RETURN_ERROR;
private List<String> allowedRenewers;
@@ -362,7 +363,7 @@ public class TokenResource {
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 @@ public class TokenResource {
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 @@ public class TokenResource {
@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 @@ public class TokenResource {
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 @@ public class TokenResource {
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 @@ public class TokenResource {
}
}
- 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 @@ public class TokenResource {
/**
* @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 class TokenResource {
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 @@ public class TokenResource {
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 @@ public class TokenResource {
}
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 @@ public class TokenResource {
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 @@ public class TokenResource {
@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 @@ public class TokenResource {
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,31 +888,21 @@ public class TokenResource {
}
}
}
+ return new UserContext(userName, createdBy);
+ }
- long expires = getExpiry();
-
- if (endpointPublicCert == null) {
- // acquire PEM for gateway identity of this gateway instance
- KeystoreService ks = services.getService(ServiceType.KEYSTORE_SERVICE);
- if (ks != null) {
- try {
- Certificate cert = ks.getCertificateForGateway();
- byte[] bytes = cert.getEncoded();
- endpointPublicCert = Base64.encodeBase64String(bytes);
- } catch (KeyStoreException | KeystoreServiceException |
CertificateEncodingException e) {
- // assuming that certs will be properly provisioned across all
clients
- log.unableToAcquireCertForEndpointClients(e);
- }
- }
- }
+ protected static class UserContext {
+ public final String userName;
+ public final String createdBy;
- 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;
+ 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);
@@ -853,105 +915,148 @@ public class TokenResource {
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();
+ 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()) {
- return
Response.status(Response.Status.fromStatusCode(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;
+ }
- 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);
+ 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);
+ if (ks != null) {
+ try {
+ Certificate cert = ks.getCertificateForGateway();
+ byte[] bytes = cert.getEncoded();
+ endpointPublicCert = Base64.encodeBase64String(bytes);
+ } catch (KeyStoreException | KeystoreServiceException |
CertificateEncodingException e) {
+ // assuming that certs will be properly provisioned across all
clients
+ log.unableToAcquireCertForEndpointClients(e);
+ }
}
- 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();
+ }
+ }
+
+ 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();
}
+ } else {
+ response = Response.status(Response.Status.FORBIDDEN)
+ .entity("{ \"Unable to get token - client cert
required.\" }")
+ .build();
}
+ }
+ return response;
+ }
- jwtAttributes = jwtAttributesBuilder.build();
- token = ts.issueToken(jwtAttributes);
+ 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));
+ }
+ }
- 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);
- }
+ 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));
- }
+ 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);
+ }
- 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));
- }
+ 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;
+ }
+ }
- return Response.ok().entity(jsonResponse).build();
- } else {
- return Response.serverError().build();
- }
- } catch (TokenServiceException e) {
- log.unableToIssueToken(e);
+ 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);
}
- return Response.ok().entity("{ \"Unable to acquire token.\" }").build();
+ if (shouldIncludeGroups()) {
+ jwtAttributesBuilder.setGroups(groups());
+ }
+
+ jwtAttributes = jwtAttributesBuilder.build();
+ token = ts.issueToken(jwtAttributes);
+ return token;
}
private boolean shouldIncludeGroups() {
@@ -995,7 +1100,7 @@ public class TokenResource {
}
}
- private long getExpiry() {
+ protected long getExpiry() {
long expiry = 0L;
long millis = tokenTTL;
@@ -1004,8 +1109,7 @@ public class TokenResource {
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 b46ddebd6..e80db1dd9 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 @@ public class TokenServiceDeploymentContributor extends
JerseyServiceDeploymentCo
@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 169650e02..b072b8d85 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 @@ public class TokenServiceResourceTest {
}
}
+ @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 @@ public class TokenServiceResourceTest {
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);