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