001/*
002 *  Copyright 2022 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.extrausermgt.authentication.kerberos;
017
018import java.net.InetAddress;
019import java.net.UnknownHostException;
020import java.util.List;
021import java.util.Map;
022import java.util.concurrent.Callable;
023
024import javax.security.auth.Subject;
025import javax.security.auth.callback.Callback;
026import javax.security.auth.callback.CallbackHandler;
027import javax.security.auth.callback.NameCallback;
028import javax.security.auth.callback.PasswordCallback;
029import javax.security.auth.login.AppConfigurationEntry;
030import javax.security.auth.login.Configuration;
031import javax.security.auth.login.LoginContext;
032import javax.security.auth.login.LoginException;
033
034import org.ietf.jgss.GSSContext;
035import org.ietf.jgss.GSSCredential;
036import org.ietf.jgss.GSSException;
037import org.ietf.jgss.GSSManager;
038import org.ietf.jgss.GSSName;
039import org.ietf.jgss.Oid;
040
041import org.ametys.runtime.model.checker.ItemChecker;
042import org.ametys.runtime.model.checker.ItemCheckerTestFailureException;
043import org.ametys.runtime.plugin.component.AbstractLogEnabled;
044
045import com.google.common.net.InetAddresses;
046
047/**
048 * This checks that the parameters are the one of a Kerberos server
049 */
050public class KerberosChecker extends AbstractLogEnabled implements ItemChecker
051{
052    public void check(List<String> values) throws ItemCheckerTestFailureException
053    {
054        String realm = values.get(0);
055        String svcLogin = values.get(1);
056        String svcPassword = values.get(2);
057        String kdc = values.get(3);
058        String testDomain = values.get(4);
059        String testLogin = values.get(5);
060        String testPassword = values.get(6);
061        
062        try
063        {
064            System.setProperty("java.security.krb5.kdc", kdc);
065            
066            Configuration loginConfig = null;
067            if (System.getProperty("java.security.auth.login.config") == null)
068            {
069                loginConfig = new Configuration() 
070                {
071                    @Override
072                    public AppConfigurationEntry[] getAppConfigurationEntry(String name) 
073                    {
074                        return new AppConfigurationEntry[] {new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, Map.of())};
075                    }
076                };
077            }
078            
079            LoginContext loginContext = new LoginContext("kerberos-client", null, new CallbackHandler()
080            {
081                public void handle(final Callback[] callbacks)
082                {
083                    for (Callback callback : callbacks)
084                    {
085                        if (callback instanceof NameCallback)
086                        {
087                            ((NameCallback) callback).setName(testLogin + "@" + realm.toUpperCase());
088                        }
089                        else if (callback instanceof PasswordCallback)
090                        {
091                            ((PasswordCallback) callback).setPassword(testPassword.toCharArray());
092                        }
093                        else
094                        {
095                            throw new RuntimeException("Invalid callback received during KerberosCredentialProvider initialization");
096                        }
097                    }
098                }
099            }, loginConfig);
100          
101            getLogger().debug("***** Authenticating " + testLogin);
102            
103            loginContext.login();
104            Subject subject = loginContext.getSubject();
105            
106            getLogger().debug("***** TGT obtained");
107            getLogger().debug(subject.toString());
108
109            GSSManager manager = GSSManager.getInstance();
110            
111            Callable<GSSCredential> action = new Callable<>() 
112            {
113                public GSSCredential call() throws GSSException 
114                {
115                    return manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, new Oid("1.3.6.1.5.5.2"), GSSCredential.INITIATE_ONLY);
116                } 
117            };
118            
119            GSSCredential gssCredential = Subject.callAs(subject, action);
120            
121            String receivedToken = null;
122            try
123            {
124                receivedToken = _getToken(manager, testDomain, realm, gssCredential);
125            }
126            catch (GSSException e)
127            {
128                if (e.getMajor() == 13) // no valid credentials, possibly due to wrong SPN
129                {
130                    String resolvedDomain = null;
131                    
132                    try
133                    {
134                        resolvedDomain = InetAddress.getByName(testDomain).getCanonicalHostName();
135                    }
136                    catch (UnknownHostException ex)
137                    {
138                        getLogger().debug("***** Cannot get ticket for host {} and also fail to resolve", testDomain, ex);
139                        throw e; // rethrow the initial exception
140                    }
141                    
142                    if (InetAddresses.isInetAddress(resolvedDomain) || resolvedDomain.equals(testDomain))
143                    {
144                        // reverse DNS not set or resolved to the same host => nothing to do
145                        throw e;
146                    }
147                    
148                    getLogger().debug("***** Cannot get ticket for host {}, try with {}", resolvedDomain);
149                    receivedToken = _getToken(manager, resolvedDomain, realm, gssCredential);
150                }
151                else
152                {
153                    throw e;
154                }
155            }
156
157            getLogger().debug("***** Decoding token");
158            
159            LoginContext srvLoginContext = KerberosCredentialProvider.createLoginContext(realm, svcLogin, svcPassword);
160            
161            action = new Callable<>() 
162            {
163                public GSSCredential call() throws GSSException 
164                {
165                    return manager.createCredential(null, GSSCredential.INDEFINITE_LIFETIME, new Oid("1.3.6.1.5.5.2"), GSSCredential.ACCEPT_ONLY);
166                } 
167            };
168            
169            gssCredential = Subject.callAs(srvLoginContext.getSubject(), action);
170            GSSContext gssContext = GSSManager.getInstance().createContext(gssCredential);
171
172            byte[] token = java.util.Base64.getDecoder().decode(receivedToken);
173          
174            gssContext.acceptSecContext(token, 0, token.length);
175
176            GSSName gssSrcName = gssContext.getSrcName();
177            getLogger().debug("***** User authenticated: " + gssSrcName);
178        }
179        catch (LoginException | GSSException e)
180        {
181            throw new ItemCheckerTestFailureException("Unable to connect to the KDC (" + e.getMessage() + ")", e);
182        }
183    }
184    
185    private String _getToken(GSSManager manager, String host, String realm, GSSCredential gssCredential) throws GSSException
186    {
187        getLogger().debug("***** Getting ticket for {}", host);
188        
189        GSSName peer = manager.createName("HTTP/" + host + "@" + realm.toUpperCase(), GSSName.NT_USER_NAME);
190        GSSContext gssContext = GSSManager.getInstance().createContext(peer, new Oid("1.3.6.1.5.5.2"), gssCredential, GSSContext.INDEFINITE_LIFETIME);
191        
192        byte[] kdcTokenAnswer = gssContext.initSecContext(new byte[0], 0, 0);
193        String receivedToken = kdcTokenAnswer != null ? java.util.Base64.getEncoder().encodeToString(kdcTokenAnswer) : null;
194
195        getLogger().debug("***** Token generated\n{}", receivedToken);
196        return receivedToken;
197    }
198}