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}