class ZTSImpl implements KeyStore, ZTSHandler {

    @Override
    public AccessTokenResponse postAccessTokenRequest(ResourceContext ctx, String request) {

        final String caller = ctx.getApiName();

        final String principalDomain = logPrincipalAndGetDomain(ctx);
        validateRequest(ctx.request(), principalDomain, caller);

        // get our principal's name

        final Principal principal = ((RsrcCtxWrapper) ctx).principal();
        String principalName = principal.getFullName();

        if (request == null || request.isEmpty()) {
            throw requestError("Empty request body", caller, ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
        }

        // we want to log the request body in our access log so
        // we know what is the client asking for but we'll just
        // limit the request up to 1K

        ctx.request().setAttribute(ACCESS_LOG_ADDL_QUERY, getQueryLogData(request));

        // decode and store the attributes that could exist in our
        // request body

        String grantType = null;
        String scope = null;
        String proxyForPrincipal = null;
        int expiryTime = 0;

        String[] comps = request.split("&");
        for (String comp : comps) {
            int idx = comp.indexOf('=');
            if (idx == -1) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("postAccessTokenRequest: skipping invalid component: {}", comp);
                }
                continue;
            }
            final String key = decodeString(comp.substring(0, idx));
            if (key == null) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("postAccessTokenRequest: skipping invalid component: {}", comp);
                }
                continue;
            }
            final String value = decodeString(comp.substring(idx + 1));
            if (value == null) {
                continue;
            }
            switch (key) {
                case KEY_GRANT_TYPE:
                    grantType = value.toLowerCase();
                    break;
                case KEY_SCOPE:
                    scope = value.toLowerCase();
                    break;
                case KEY_EXPIRES_IN:
                    expiryTime = ZTSUtils.parseInt(value, 0);
                    break;
                case KEY_PROXY_FOR_PRINCIPAL:
                    proxyForPrincipal = getProxyForPrincipalValue(value.toLowerCase(), principalName,
                            principalDomain, caller);
                    break;
            }
        }

        // validate the request data

        if (!OAUTH_GRANT_CREDENTIALS.equals(grantType)) {
            throw requestError("Invalid grant request: " + grantType, caller,
                    principal.getDomain(), principalDomain);
        }

        // we must have scope provided so we know what access
        // the client is looking for

        if (scope == null || scope.isEmpty()) {
            throw requestError("Invalid request: no scope provided", caller,
                    principal.getDomain(), principalDomain);
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("postAccessTokenRequest(principal: {}, grant-type: {}, scope: {}, expires-in: {}, proxy-for-principal: {})",
                    principalName, grantType, scope, expiryTime, proxyForPrincipal);
        }

        // our scopes are space separated list of values

        AccessTokenRequest tokenRequest = new AccessTokenRequest(scope);

        // before using any of our values let's validate that they
        // match our schema

        final String domainName = tokenRequest.getDomainName();
        setRequestDomain(ctx, domainName);
        validate(domainName, TYPE_DOMAIN_NAME, principalDomain, caller);

        String[] requestedRoles = tokenRequest.getRoleNames();
        if (requestedRoles != null) {
            for (String requestedRole : requestedRoles) {
                validate(requestedRole, TYPE_ENTITY_NAME, principalDomain, caller);
            }
        }

        // first retrieve our domain data object from the cache

        DataCache data = dataStore.getDataCache(domainName);
        if (data == null) {
            setRequestDomain(ctx, ZTSConsts.ZTS_UNKNOWN_DOMAIN);
            throw notFoundError("No such domain: " + domainName, caller,
                    ZTSConsts.ZTS_UNKNOWN_DOMAIN, principalDomain);
        }

        // check if the authorized service domain matches to the
        // requested domain name

        checkRoleTokenAuthorizedServiceRequest(principal, domainName, caller);

        // process our request and retrieve the roles for the principal

        Set<String> roles = new HashSet<>();
        dataStore.getAccessibleRoles(data, domainName, principalName, requestedRoles, roles, false);

        // we return failure if we don't have access to any roles

        if (roles.isEmpty()) {
            throw forbiddenError("No access to any roles in domain: " + domainName, caller,
                    domainName, principalDomain);
        }

        // if this is proxy for operation then we want to make sure that
        // both principals have access to the same set of roles so we'll
        // remove any roles that are authorized by only one of the principals

        String proxyUser = null;
        if (proxyForPrincipal != null) {

            // we also need to verify that we are not returning id tokens.
            // proxy principal functionality is only valid for access tokens

            if (tokenRequest.isOpenidScope()) {
                throw requestError("Proxy Principal cannot request id tokens", caller,
                        domainName, principalDomain);
            }

            // process the role lookup for the proxy principal

            Set<String> rolesForProxy = new HashSet<>();
            dataStore.getAccessibleRoles(data, domainName, proxyForPrincipal, requestedRoles, rolesForProxy, false);
            roles.retainAll(rolesForProxy);

            // check again in case we removed all the roles and ended up
            // with an empty set

            if (roles.isEmpty()) {
                throw forbiddenError("No access to any roles by User and Proxy Principals",
                        caller, domainName, principalDomain);
            }

            // we need to switch our principal and proxy for user

            proxyUser = principalName;
            principalName = proxyForPrincipal;
        }

        // if the request was done by a role certificate we need to make sure
        // that it is issued for the roles we're returning in the role token

        if (!isPrincipalRoleCertificateAccessValid(principal, domainName, roles)) {
            throw forbiddenError("Role based Principal does not include all roles",
                    caller, domainName, principalDomain);
        }

        long tokenTimeout = determineTokenTimeout(data, roles, null, expiryTime);
        long iat = System.currentTimeMillis() / 1000;

        AccessToken accessToken = new AccessToken();
        accessToken.setVersion(1);
        accessToken.setAudience(domainName);
        accessToken.setClientId(principalName);
        accessToken.setIssueTime(iat);
        accessToken.setAuthTime(iat);
        accessToken.setExpiryTime(iat + tokenTimeout);
        accessToken.setUserId(principalName);
        accessToken.setSubject(principalName);
        accessToken.setIssuer(ztsOAuthIssuer);
        accessToken.setProxyPrincipal(proxyUser);
        accessToken.setScope(new ArrayList<>(roles));

        // if we have a certificate used for mTLS authentication then
        // we're going to bind the certificate to the access token

        X509Certificate cert = principal.getX509Certificate();
        if (cert != null) {
            accessToken.setConfirmX509CertHash(cert);
        }

        String accessJwts = accessToken.getSignedToken(privateKey.getKey(), privateKey.getId(), privateKey.getAlgorithm());

        // now let's check to see if we need to create openid token

        String idJwts = null;
        if (tokenRequest.isOpenidScope()) {

            final String serviceName = tokenRequest.getServiceName();
            validate(serviceName, TYPE_SIMPLE_NAME, principalDomain, caller);

            IdToken idToken = new IdToken();
            idToken.setVersion(1);
            idToken.setAudience(tokenRequest.getDomainName() + "." + serviceName);
            idToken.setSubject(principalName);
            idToken.setIssuer(ztsOAuthIssuer);

            // id tokens are only valid for up to 12 hours max
            // (value configured as a system property).
            // we'll use the user specified timeout unless it's
            // over the configured max

            idToken.setIssueTime(iat);
            idToken.setAuthTime(iat);
            idToken.setExpiryTime(iat + determineIdTokenTimeout(tokenTimeout));

            idJwts = idToken.getSignedToken(privateKey.getKey(), privateKey.getId(), privateKey.getAlgorithm());
        }

        AccessTokenResponse response = new AccessTokenResponse().setAccess_token(accessJwts)
                .setToken_type(OAUTH_BEARER_TOKEN).setExpires_in((int) tokenTimeout).setId_token(idJwts);

        // if either we were asked for full domain roles or the requested list of roles
        // does not match the returned list of roles then we need to return the updated
        // set of scopes

        if (tokenRequest.sendScopeResponse() || requestedRoles != null && requestedRoles.length != roles.size()) {
            List<String> domainRoles = new ArrayList<>();
            for (String role : roles) {
                domainRoles.add(domainName + AccessTokenRequest.OBJECT_ROLE + role);
            }
            if (tokenRequest.isOpenidScope()) {
                domainRoles.add(AccessTokenRequest.OBJECT_OPENID);
            }
            response.setScope(String.join(" ", domainRoles));
        }

        return response;
    }

    public  ZTSImpl();
    public  ZTSImpl(CloudStore implCloudStore, DataStore implDataStore);

}