class PrincipalAuthority implements Authority, AuthorityKeyStore {

    @Override
    public Principal authenticate(String signedToken, String remoteAddr, String httpMethod,
            StringBuilder errMsg) {

        errMsg = errMsg == null ? new StringBuilder(512) : errMsg;
        if (LOG.isDebugEnabled()) {
            LOG.debug("Authenticating PrincipalToken: " + signedToken);
        }

        PrincipalToken serviceToken;
        try {
            serviceToken = new PrincipalToken(signedToken);
        } catch (IllegalArgumentException ex) {
            errMsg.append("PrincipalAuthority:authenticate: Invalid token: exc=").
                   append(ex.getMessage()).append(" : credential=").
                   append(Token.getUnsignedToken(signedToken));
            LOG.error(errMsg.toString());
            return null;
        }

        /* before authenticating verify that if this is a valid
         * authorized service token or not and if required
         * components are provided (the method already logs
         * all error messages) */
        
        StringBuilder errDetail = new StringBuilder(512);
        if (!serviceToken.isValidAuthorizedServiceToken(errDetail)) {
            errMsg.append("PrincipalAuthority:authenticate: Invalid authorized service token: ");
            errMsg.append(errDetail).append(" : credential=").
                   append(Token.getUnsignedToken(signedToken));
            return null;
        }
        
        String tokenDomain = serviceToken.getDomain().toLowerCase();
        String tokenName = serviceToken.getName().toLowerCase();
        String keyService = serviceToken.getKeyService();
        boolean userToken = tokenDomain.equals(userDomain);
        
        /* get the public key for this token to validate signature */
        
        String publicKey = getPublicKey(tokenDomain, tokenName, keyService,
                serviceToken.getKeyId(), userToken);

        /* the validate method logs all error messages */
        
        boolean writeOp = isWriteOperation(httpMethod);
        if (!serviceToken.validate(publicKey, allowedOffset, !writeOp, errDetail)) {
            errMsg.append("PrincipalAuthority:authenticate: service token validation failure: ");
            errMsg.append(errDetail).append(" : credential=").
                   append(Token.getUnsignedToken(signedToken));
            return null;
        }

        /* if an authorized service signature is available then we're going to validate
         * that signature as well to support token chaining in Athenz and, if necessary,
         * bypass IP address mismatch for users */
        
        String authorizedServiceName = null;
        if (serviceToken.getAuthorizedServiceSignature() != null) {
            authorizedServiceName = validateAuthorizeService(serviceToken, errDetail);
            if (authorizedServiceName == null) {
                errMsg.append("PrincipalAuthority:authenticate: validation of authorized service failure: ").
                       append(errDetail).append(" : credential=").
                       append(Token.getUnsignedToken(signedToken));
                return null;
            }
        }
        
        /* if we have a usertoken and our remote ip check enabled, verify that the IP address
         * matches before allowing the operation go through */
        
        if (userToken && !remoteIpCheck(remoteAddr, writeOp, serviceToken, authorizedServiceName)) {
            errMsg.append("PrincipalAuthority:authenticate: IP Mismatch - token (").
                append(serviceToken.getIP()).append(") request (").
                append(remoteAddr).append(")");
            LOG.error(errMsg.toString());
            return null;
        }
        
        /* all the role members in Athenz are normalized to lower case so we need to make
         * sure our principal's name and domain are created with lower case as well */
        
        SimplePrincipal princ = (SimplePrincipal) SimplePrincipal.create(tokenDomain,
                tokenName, signedToken, serviceToken.getTimestamp(), this);
        princ.setUnsignedCreds(serviceToken.getUnsignedToken());
        princ.setAuthorizedService(authorizedServiceName);
        princ.setOriginalRequestor(serviceToken.getOriginalRequestor());
        princ.setKeyService(keyService);
        princ.setIP(serviceToken.getIP());
        princ.setKeyId(serviceToken.getKeyId());
        return princ;
    }

    public  PrincipalAuthority();

}