package no.bankid.oidc;

import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.glassfish.jersey.client.oauth2.OAuth2ClientSupport;
import org.json.JSONObject;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.UUID;

import static java.net.URLEncoder.encode;
import static no.bankid.oidc.ClientDefinition.CLIENT_ID;
import static no.bankid.oidc.ClientDefinition.CLIENT_PWD;

public class BankIdOIDCClient {

    public static final String CONFIG_URL = "https://prototype.bankidnorge.no/bankid-oauth/oauth/.well-known/openid-configuration";
    public static final String CALLBACK_URL = "http://localhost:8080/callback";

    private final String authorizationEndpoint;
    private final String token_endpoint;
    private final String userinfo_endpoint;
    private final JWTHandler JWTHandler;

    private static BankIdOIDCClient bankIdOIDCClient;

    public static BankIdOIDCClient getInstance() {
        if (bankIdOIDCClient == null) {
            bankIdOIDCClient = new BankIdOIDCClient();
        }
        return bankIdOIDCClient;
    }

    /**
     * On initialization the OIDC configuration is fetched from CONFIG_URL.
     * This contains relevant endpoints and information about the jwt (id_token) key.
     */
    private BankIdOIDCClient() {
        Client client = ClientBuilder.newClient();
        Response response = client.target(CONFIG_URL).request().get();
        JSONObject configuration = new JSONObject(response.readEntity(String.class));

        this.authorizationEndpoint = configuration.getString("authorization_endpoint");
        this.token_endpoint = configuration.getString("token_endpoint");
        this.userinfo_endpoint = configuration.getString("userinfo_endpoint");

        // JWTHandler fetches the key from jwks_uri
        JWTHandler = new JWTHandler(configuration.getString("jwks_uri"));
    }

    /**
     * Builds the authentication url, where the user shall be redirected upon starting of the autentication process.
     *
     */
    public String createAuthenticationUrl() {
        String state = UUID.randomUUID().toString();

        return String.format("%s?client_id=%s&redirect_uri=%s&response_type=code&scope=openid&state=%s&nonce=%s",
                authorizationEndpoint, CLIENT_ID, encoded(CALLBACK_URL), encoded(state), "somecorrelationnonce");
    }

    public static String encoded(String s) {
        try {
            return encode(s, Charset.forName("UTF-8").name());
        } catch (UnsupportedEncodingException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * After callback from oidc, the authentication_code must be exchanged with the access_token.
     *
     * This will be done with a POST against the token_endpoint.
     * the 'code' is attached in the body (x-www-form-urlencoded)
     * The endpoint requires basic auth.
     *
     * Finally, we put the access_token and id_token in a User object. It may typically be stored on the session.
     *
     */
    public User endAuthentication(String code) {
        HttpAuthenticationFeature basicAuth =
                HttpAuthenticationFeature
                        .basicBuilder()
                        .nonPreemptive()
                        .credentials(CLIENT_ID, CLIENT_PWD)
                        .build();

        ClientConfig clientConfig = new ClientConfig();
        clientConfig.register(basicAuth);

        Client client = ClientBuilder.newClient(clientConfig);

        WebTarget target = client.target(token_endpoint);

        MultivaluedMap<String, String> formData = new MultivaluedHashMap<String, String>();
        formData.add("code", code);
        formData.add("grant_type", "authorization_code");

        Response response = target.request().post(Entity.form(formData));

        JSONObject json = new JSONObject(response.readEntity(String.class));

        String access_token = json.getString("access_token");
        String id_token = json.getString("id_token");

        JSONObject jwsPayload = JWTHandler.getPayload(id_token);

        return new User(access_token, jwsPayload);
    }

    /**
     * Fetch the protected resource UserInfo by using the access_token.
     */
    public JSONObject getUserInfo(User user) {
        Client client = ClientBuilder.newClient();

        Feature feature = OAuth2ClientSupport.feature(user.getAccessToken());
        client.register(feature);

        Response response = client.target(userinfo_endpoint).request().get();

        return new JSONObject(response.readEntity(String.class));
    }
}

@@ -2441,24 +2441,152 @@
 tionUrl() %7B%0A
+        // state is a value used to maintain state between the request and the callback. Actually not used in this application.%0A
         Stri

Comment about state