All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.mfp.java.token.validator.TokenValidationManager Maven / Gradle / Ivy

Go to download

IBM MFP Java token validator is used to validate Oauth tokens against an Authorization server, BuildNumber is : 8.0.2017020112

There is a newer version: 8.0.2017020112
Show newest version
/*
 * IBM Confidential OCO Source Materials
 *
 * 5725-I43 Copyright IBM Corp. 2006, 2015
 *
 * The source code for this program is not published or otherwise
 * divested of its trade secrets, irrespective of what has
 * been deposited with the U.S. Copyright Office.
 */
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.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
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:9080/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:9080/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 = 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 = 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 makePostRequest(String path, Map headers, List formParams) throws IOException, TokenValidationException {
        HttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(path);

        for (Map.Entry entry : headers.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            httpPost.setHeader(key, value);
        }
        httpPost.setEntity(new UrlEncodedFormEntity(formParams));
        HttpResponse resp = httpClient.execute(httpPost);
        if (resp == null || resp.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
            // If the AZ server is down
            int status = resp != null ? resp.getStatusLine().getStatusCode() : 0;
            String body = resp != null ? EntityUtils.toString(resp.getEntity()) : null;
            throw new TokenValidationException("Unsuccessful request to Authorization Server, server responded with status code: "+status+" and body : "+body+", check the Authorization URL: " + path, status);
        }
        return EntityUtils.toString(resp.getEntity());
    }

    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