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}