/*
 * 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.example.kerberos;

import org.ietf.jgss.GSSCredential;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.KerberosSerializationUtils;
import org.keycloak.representations.AccessToken;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.security.sasl.Sasl;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Hashtable;

/**
 * Sample client able to authenticate against ApacheDS LDAP server with Krb5 GSS Credential.
 *
 * Credential was previously retrieved from SPNEGO authentication against Keycloak auth-server and transmitted from
 * Keycloak to the application in OIDC access token
 *
 * We can use GSSCredential to further GSS API calls . Note that if you will use GSS API directly, you can
 * attach GSSCredential when creating GSSContext like this:
 * GSSContext context = gssManager.createContext(serviceName, KerberosSerializationUtils.KRB5_OID, deserializedGssCredential, GSSContext.DEFAULT_LIFETIME);
 *
 * In this example we authenticate against LDAP server, which calls GSS API under the hood when credential is attached to env under Sasl.CREDENTIALS key
 *
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
public class GSSCredentialsClient {

    public static LDAPUser getUserFromLDAP(HttpServletRequest req) throws Exception {
        KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) req.getUserPrincipal();
        AccessToken accessToken = keycloakPrincipal.getKeycloakSecurityContext().getToken();
        String username = accessToken.getPreferredUsername();

        // Retrieve kerberos credential from accessToken and deserialize it
        String serializedGssCredential = (String) accessToken.getOtherClaims().get(KerberosConstants.GSS_DELEGATION_CREDENTIAL);
        GSSCredential deserializedGssCredential = KerberosSerializationUtils.deserializeCredential(serializedGssCredential);

        // First try to invoke without gssCredential. It should fail. This is here just for illustration purposes
        try {
            invokeLdap(null, username);
            throw new RuntimeException("Not expected to authenticate to LDAP without credential");
        } catch (NamingException nse) {
            System.out.println("GSSCredentialsClient: Expected exception: " + nse.getMessage());
        }

        return invokeLdap(deserializedGssCredential, username);
    }

    private static LDAPUser invokeLdap(GSSCredential gssCredential, String username) throws NamingException {
        Hashtable env = new Hashtable(11);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://localhost:10389");

        if (gssCredential != null) {
            env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
            env.put(Sasl.CREDENTIALS, gssCredential);
        }

        DirContext ctx = new InitialDirContext(env);
        try {
            Attributes attrs = ctx.getAttributes("uid=" + username + ",ou=People,dc=keycloak,dc=org");
            String uid = username;
            String cn = (String) attrs.get("cn").get();
            String sn = (String) attrs.get("sn").get();
            return new LDAPUser(uid, cn, sn);
        } finally {
            ctx.close();
        }
    }

    public static class LDAPUser {

        private final String uid;
        private final String cn;
        private final String sn;

        public LDAPUser(String uid, String cn, String sn) {
            this.uid = uid;
            this.cn = cn;
            this.sn = sn;
        }

        public String getUid() {
            return uid;
        }

        public String getCn() {
            return cn;
        }

        public String getSn() {
            return sn;
        }
    }
}
