com.ibm.mfp.java.token.validator.TokenValidationManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mfp-java-token-validator Show documentation
Show all versions of mfp-java-token-validator Show documentation
IBM MFP Java token validator is used to validate Oauth tokens against an Authorization server, BuildNumber is : 8.0.2017020112
/*
* © Copyright IBM Corp. 2016
* All Rights Reserved. US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
*/
package com.ibm.mfp.java.token.validator;
import com.ibm.mfp.java.token.validator.utils.TokenValidationUtils;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpStatus;
import org.apache.http.message.BasicNameValuePair;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Implements TokenValidationManager
* Validates authorization headers (Access Tokens) against the AZ Server's introspection service.
* Created by Ore Poran on 11/18/15.
*/
public class TokenValidationManager {
private static final String BEARER = "Bearer";
private static final String INTROSPECTION_PATH = "introspection";
private static final String TOKEN_PATH = "token";
private static final long DEFAULT_CACHE_SIZE = 50000;
private static final String INTROSPECTION_SCOPE_KEY = "authorization.introspect";
public static final String INVALID_TOKEN_ERROR = "invalid_token";
private static final Logger logger = Logger.getLogger(TokenValidationManager.class.getName());
/*
Access Token fields
*/
private static final String ACCESS_TOKEN_KEY = "access_token";
private static final String EXPIRATION = "expires_in";
private TokenValidationCache cache;
private URI authorizationURI;
private String basicAuthorization;
/*
The number of retries if a resourceConfidentialClientToken is invalid
*/
private int attempts;
/*
This is the Resource Token for introspecting
*/
private String resourceConfidentialToken;
private long resourceConfidentialTokenExpiration;
/**
* Constructs a new TokenValidationManager
* @param authorizationURI The URI of the Authorization Server for example http://localhost:/mfp/api
* @param clientId , The confidential-client clientId of the resource
* @param clientSecret , The confidential-client secret of the resource
* @param cacheSize , the size of the cached access tokens mapping (default 10000)
*/
public TokenValidationManager(URI authorizationURI, String clientId, String clientSecret, long cacheSize) throws TokenValidationException {
if (authorizationURI == null) {
throw new TokenValidationException("Missing parameters");
}
this.authorizationURI = authorizationURI;
this.cache = new TokenValidationCache(cacheSize);
// If no clientId/Secret are configured, we assume this TokenValidationManager doesn't need a confidential client
// In order to introspect tokens (External AZ mode)
if(clientId != null && clientSecret != null) {
this.basicAuthorization = "Basic " + Base64.encodeBase64String((clientId+":"+clientSecret).getBytes());
} else {
logger.log(Level.FINE, "No clientId or clientSecret passed, if you are working in embedded-AZ mode, token validation will fail");
}
}
/**
* Constructs a new TokenValidationManager
* @param authorizationURI The URI of the Authorization Server for example http://localhost:/mfp/api
* @param clientId , The confidential-client clientId of the resource
* @param clientSecret , The confidential-client secret of the resource
*/
public TokenValidationManager(URI authorizationURI, String clientId, String clientSecret) throws TokenValidationException {
this(authorizationURI, clientId, clientSecret, DEFAULT_CACHE_SIZE);
}
/**
* Validates and returns the Introspection Data of the specified authorization header via the Introspection Endpoint of the AZ server
* @param authorizationHeader the authorization header to validate
* @param expectedScope the scope to validate this authorization header with
* @return TokenValidationResult object, with the authenticationError and the IntrospectionData
* @throws TokenValidationException in the case of an error connecting to the AZ Server
*/
public TokenValidationResult validate(String authorizationHeader, String expectedScope) throws TokenValidationException {
// Validate the token before sending to Introspection endpoint
AuthenticationError error = preProcessAuthHeader(authorizationHeader);
if (error != null) {
return new TokenValidationResult(error, null);
}
// Introspect
return introspect(authorizationHeader, expectedScope);
}
/**
* Validates and returns the Introspection Data of the specified authorization header via the Introspection Endpoint of the AZ server
* @param authorizationHeader the authorization header to validate
* @return TokenValidationResult object, with the authenticationError and the IntrospectionData
* @throws TokenValidationException in the case of an error connecting to the AZ Server
*/
public TokenValidationResult validate(String authorizationHeader) throws TokenValidationException {
return validate(authorizationHeader, null);
}
/**
* Obtains an access token from the Authorization Server's token endpoint.
* The Confidential Client credentials passed to the constructor are used to obtain the token
* If these credentials don't exist or are not allowed to obtain the given scope, an error is thrown
* Otherwise the access token map is returned
* @param scope the scope to obtain an access token to
* @return a Map holding the access token according to OAuth 2.0 Spec RFC6749
* @throws TokenValidationException if unable to obtain token, its possible this error is thrown if the confidential client of this manager is not allowed to obtain this scope
*/
public Map obtainAccessToken(String scope) throws TokenValidationException {
String response;
Map accessToken;
// Set Token endpoint path
String path = TokenValidationUtils.buildPath(authorizationURI.toString(), TOKEN_PATH);
// Add Basic Authorization header
Map headers = new HashMap<>();
if(hasBasicCredentials())
headers.put(HttpHeaders.AUTHORIZATION, basicAuthorization);
// Add scope param
List formParams = new ArrayList<>();
formParams.add(new BasicNameValuePair("grant_type", "client_credentials"));
formParams.add(new BasicNameValuePair("scope", scope));
try {
response = TokenValidationUtils.makePostRequest(path, headers, formParams);
accessToken = TokenValidationUtils.toMap(response);
} catch (Exception e) {
// Not able to obtain token - perhaps working in external AZ mode
logger.severe("Unable to obtain access token, if working in external-AZ mode, verify you have set clientId/clientSecret to null");
throw new TokenValidationException("Failed to make token request, reason: " + e.getMessage(), e);
}
// Get the access token
if (accessToken.get(ACCESS_TOKEN_KEY) == null) {
throw new TokenValidationException("Failed to make token request, " + response);
}
return accessToken;
}
private AuthenticationError preProcessAuthHeader(String authorizationHeader) {
if (authorizationHeader == null || authorizationHeader.length() < 1) {
return new AuthenticationError(HttpStatus.SC_UNAUTHORIZED, buildErrorMessage(null, null));
}
String token = authorizationHeader.startsWith(BEARER) ? authorizationHeader.substring(BEARER.length()) : "";
if (token.isEmpty()) {
return new AuthenticationError(HttpStatus.SC_UNAUTHORIZED, buildErrorMessage(INVALID_TOKEN_ERROR, null));
}
return null;
}
private TokenValidationResult introspect(String authorizationHeader, String expectedScope) throws TokenValidationException {
AuthenticationError authError = null;
TokenIntrospectionData data;
obtainToken();
// Attempt to pull from cache
data = cache.get(authorizationHeader);
if(data == null) {
try {
data = makeIntrospectionRequest(authorizationHeader);
cache.set(authorizationHeader, data);
} catch (TokenValidationException e) {
authError = handleConflictFailure(e);
}
}
// Validate the token introspection data
authError = authError != null ? authError : validateIntrospectionDataResponse(data, expectedScope);
// If we have an authorization error, we return invalid introspection data
data = authError != null ? TokenIntrospectionData.INACTIVE_TOKEN : data;
return new TokenValidationResult(authError, data);
}
private AuthenticationError validateIntrospectionDataResponse(TokenIntrospectionData data, String requiredScope) {
if (data == null) {
return new AuthenticationError(HttpStatus.SC_UNAUTHORIZED, buildErrorMessage(INVALID_TOKEN_ERROR, null));
}
if (!data.isActive()) {
return new AuthenticationError(HttpStatus.SC_UNAUTHORIZED, buildErrorMessage(INVALID_TOKEN_ERROR, null));
}
if (requiredScope != null && !data.isScopeCovered(requiredScope)) {
return new AuthenticationError(HttpStatus.SC_FORBIDDEN, buildErrorMessage("insufficient_scope", requiredScope));
}
return null;
}
/*
Makes an HTTPRequest using HTTPClient
*/
protected TokenIntrospectionData makeIntrospectionRequest(String authorizationHeader) throws TokenValidationException {
String path = (TokenValidationUtils.buildPath(authorizationURI.toString(), INTROSPECTION_PATH));
Map headers = new HashMap<>();
if(getResourceConfidentialToken() != null)
headers.put(HttpHeaders.AUTHORIZATION, TokenValidationUtils.toggleAccessTokenAndAuthHeader(getResourceConfidentialToken(), false));
List formParams = new ArrayList<>();
String token = TokenValidationUtils.toggleAccessTokenAndAuthHeader(authorizationHeader, true);
formParams.add(new BasicNameValuePair("token", token));
formParams.add(new BasicNameValuePair("token_type_hint", ACCESS_TOKEN_KEY));
try {
String response = TokenValidationUtils.makePostRequest(path, headers, formParams);
return TokenValidationUtils.toTokenIntrospectionData(response);
} catch (Exception e) {
if (e instanceof TokenValidationException) {
return handleIntrospectionFailure(authorizationHeader, (TokenValidationException) e);
}
throw new TokenValidationException("Failed to make introspection request, reason: " + e.getMessage(), e);
}
}
private TokenIntrospectionData handleIntrospectionFailure(String authorizationHeader, TokenValidationException e) throws TokenValidationException {
int responseStatus = e.getStatus();
if(responseStatus == HttpStatus.SC_UNAUTHORIZED || responseStatus == HttpStatus.SC_FORBIDDEN)
return handleUnauthorizedFailure(authorizationHeader);
throw e;
}
private boolean hasBasicCredentials() {
return basicAuthorization != null;
}
/*
In the case of a 401 during introspection, we obtain a new token and make the introspection request again
*/
private TokenIntrospectionData handleUnauthorizedFailure(String authorizationHeader) throws TokenValidationException {
int maxAttempts = 4;
if (++attempts < maxAttempts) {
// We were unauthorized to introspect, perhaps our token has become invalid - get a new one
setResourceConfidentialToken(null);
obtainToken();
// Re-invoke makeIntrospectionRequest
return makeIntrospectionRequest(authorizationHeader);
}
// Reached max attempts, throw exception
logger.severe("Introspection endpoint resulted in unauthorized response, if you are working in embedded AZ server, you must provide non-null clientId/clientSecret credentials");
throw new TokenValidationException("Error obtaining a token for the Resource Server using the specified clientId/clientSecret credentials");
}
private AuthenticationError handleConflictFailure(TokenValidationException e) throws TokenValidationException {
AuthenticationError authError;
if(e.getStatus() == HttpStatus.SC_CONFLICT) {
authError = new AuthenticationError(HttpStatus.SC_CONFLICT, null);
} else {
throw e;
}
return authError;
}
/*
Makes a Token endpoint request with Confidential Client credentials
A token is obtained only if we don't have it in memory
*/
private void obtainToken() throws TokenValidationException {
if (shouldObtainToken()) {
Map accessToken = obtainAccessToken(INTROSPECTION_SCOPE_KEY);
// Save the Obtained token
setResourceConfidentialToken(accessToken);
}
}
private String buildErrorMessage(String error, String scope) {
StringBuilder sb = new StringBuilder();
sb.append(BEARER);
if (error != null)
sb.append(" error=\"").append(error).append("\"");
if (scope != null)
sb.append(", scope=\"").append(scope).append("\"");
return sb.toString();
}
private boolean shouldObtainToken() {
return hasBasicCredentials() && getResourceConfidentialToken() == null;
}
/*
We cache the ResourceToken and use it until its expired
*/
private String getResourceConfidentialToken() {
if (System.currentTimeMillis() >= resourceConfidentialTokenExpiration)
setResourceConfidentialToken(null);
return resourceConfidentialToken;
}
/*
We cache the ResourceToken and use it until its expired
*/
private void setResourceConfidentialToken(Map accessTokenResponse) {
if (accessTokenResponse == null) {
this.resourceConfidentialToken = null;
this.resourceConfidentialTokenExpiration = 0;
} else {
this.resourceConfidentialToken = (String) accessTokenResponse.get(ACCESS_TOKEN_KEY);
Number expiration = (Number) accessTokenResponse.get(EXPIRATION);
this.resourceConfidentialTokenExpiration = System.currentTimeMillis() + (expiration.longValue() * 1000);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy