/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed 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.keycloak.protocol.openshift;

import org.keycloak.RSATokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.PublicKey;
import java.util.List;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
public class OpenShiftTokenReviewEndpoint implements OIDCExtProvider {

    private KeycloakSession session;
    private TokenManager tokenManager;
    private EventBuilder event;

    public OpenShiftTokenReviewEndpoint(KeycloakSession session) {
        this.session = session;
        this.tokenManager = new TokenManager();
    }

    @Override
    public void setEvent(EventBuilder event) {
        this.event = event;
    }

    @Path("/")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response tokenReview(OpenShiftTokenReviewRequestRepresentation reviewRequest) throws Exception {
        return tokenReview(null, reviewRequest);
    }

    @Path("/{client_id}")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response tokenReview(@PathParam("client_id") String clientId, OpenShiftTokenReviewRequestRepresentation reviewRequest) throws Exception {
        event.event(EventType.INTROSPECT_TOKEN);

        if (clientId != null) {
            session.setAttribute("client_id", clientId);
        }

        checkSsl();
        checkRealm();
        authorizeClient();

        RealmModel realm = session.getContext().getRealm();

        AccessToken token = null;
        try {
            RSATokenVerifier verifier = RSATokenVerifier.create(reviewRequest.getSpec().getToken())
                    .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));

            PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
            if (publicKey == null) {
                error(401, Errors.INVALID_TOKEN, "Invalid public key");
            } else {
                verifier.publicKey(publicKey);
                verifier.verify();
                token = verifier.getToken();
            }
        } catch (VerificationException e) {
            error(401, Errors.INVALID_TOKEN, "Token verification failure");
        }

        if (!tokenManager.isTokenValid(session, realm, token)) {
            error(401, Errors.INVALID_TOKEN, "Token verification failure");
        }

        OpenShiftTokenReviewResponseRepresentation response = new OpenShiftTokenReviewResponseRepresentation();
        response.getStatus().setAuthenticated(true);
        response.getStatus().setUser(new OpenShiftTokenReviewResponseRepresentation.User());

        OpenShiftTokenReviewResponseRepresentation.User userRep = response.getStatus().getUser();
        userRep.setUid(token.getSubject());
        userRep.setUsername(token.getPreferredUsername());

        if (token.getScope() != null && !token.getScope().isEmpty()) {
            OpenShiftTokenReviewResponseRepresentation.Extra extra = new OpenShiftTokenReviewResponseRepresentation.Extra();
            extra.setScopes(token.getScope().split(" "));
            userRep.setExtra(extra);
        }

        if (token.getOtherClaims() != null && token.getOtherClaims().get("groups") != null) {
            List<String> groups = (List<String>) token.getOtherClaims().get("groups");
            userRep.setGroups(groups);
        }

        event.success();
        return Response.ok(response, MediaType.APPLICATION_JSON).build();
    }

    private void checkSsl() {
        if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && session.getContext().getRealm().getSslRequired().isRequired(session.getContext().getConnection())) {
            error(401, Errors.SSL_REQUIRED, null);
        }
    }

    private void checkRealm() {
        if (!session.getContext().getRealm().isEnabled()) {
            error(401, Errors.REALM_DISABLED,null);
        }
    }

    private void authorizeClient() {
        try {
            ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
            event.client(client);

            if (client == null || client.isPublicClient()) {
                error(401, Errors.INVALID_CLIENT, "Public client is not permitted to invoke token review endpoint");
            }

        } catch (ErrorResponseException ere) {
            error(401, Errors.INVALID_CLIENT_CREDENTIALS, ere.getErrorDescription());
        } catch (Exception e) {
            error(401, Errors.INVALID_CLIENT_CREDENTIALS, null);
        }
    }

    private void error(int statusCode, String error, String description) {
        OpenShiftTokenReviewResponseRepresentation rep = new OpenShiftTokenReviewResponseRepresentation();
        rep.getStatus().setAuthenticated(false);

        Response response = Response.status(statusCode).entity(rep).type(MediaType.APPLICATION_JSON_TYPE).build();

        event.error(error);
        event.detail(Details.REASON, description);

        throw new ErrorResponseException(response);
    }

}
