Reading user name from Kerberos ticket in Authorization header - java

I want to read the user's name from the Kerberos ticket in the Authorization HTTP header. I am using Java.
I have spent days trying to achieve this by reading through a bunch of sites on the subject but have not been able to do this. Kerberos is new/foreign to me.
This is what I've achieved:
When a user first accesses the site - without the Authorization header, the server responds with 401 + header: WWW-Authenticate=Negotiate.
All sorts of magical things happen on the client's side.
User returns with a HTTP request that contains the Authorization header with a value like: "Negotiate YHcGB...=="
Decode the base64 encoded ticket to a byte array.
From here on it's a terrifying journey through the unknown. As far as I can tell, the next steps should be:
Login to AD/Kerberos/ Server with a user.
Decode the ticket.
This is what I have:
login.conf
ServicePrincipalLoginContext
{
com.sun.security.auth.module.Krb5LoginModule
required
principal="HTTP/some.server.com#MY.DOMAIN.COM"
doNotPrompt=true
useTicketCache=true
password=mYpasSword
debug=true;
};
JavaClass.java
String encodedTicket = authorization.substring("Negotiate ".length());
byte[] ticket = Base64.decode(encodedTicket);
LoginContext lc = new LoginContext("ServicePrincipalLoginContext");
lc.login();
Subject serviceSubject = lc.getSubject();
Subject.doAs(serviceSubject, new ServiceTicketDecoder(ticket));
ServiceTicketDecoder.java
public String run() throws Exception {
Oid kerberos5Oid = new Oid("1.2.840.113554.1.2.2");
GSSManager gssManager = GSSManager.getInstance();
String service = "krbtgt/MY.DOMAIN.COM#MY.DOMAIN.COM";
GSSName serviceName = gssManager.createName(service, GSSName.NT_USER_NAME);
GSSCredential serviceCredentials = gssManager.createCredential(serviceName, GSSCredential.INDEFINITE_LIFETIME, kerberos5Oid, GSSCredential.ACCEPT_ONLY);
GSSContext gssContext = gssManager.createContext(serviceCredentials);
gssContext.acceptSecContext(this.serviceTicket, 0, this.serviceTicket.length);
GSSName srcName = gssContext.getSrcName();
return srcName.toString;
}
The login in JavaClass.java works ok, so I'm assuming the login.conf is ok. On "GSSCredential serviceCredentials = gssManager.createCredential(..." in the ServiceTicketDecoder.java the following exception is thrown:
java.security.PrivilegedActionException: GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos Key)
I am not sure if this is the right approach. I also don't know what the value of the "String service" should be or how to get that information. Can you help me?
EDIT:
login.conf
ServicePrincipalLoginContext
{
com.sun.security.auth.module.Krb5LoginModule
required
principal="HTTP/some.server.com#MY.DOMAIN.COM"
doNotPrompt=true
useTicketCache=true
keyTab="C:/server-http.keytab"
debug=true;
};
I have received a keytab file. Apparently the HTTP/some.server.com user's account was already a service principal account. I now have a problem on JavaClass.java at lc.login():
javax.security.auth.login.LoginException: KDC has no support for encryption type (14)
Caused by: KrbException: KDC has no support for encryption type (14)
Caused by: KrbException: Identifier doesn't match expected value (906)
The keytab file is encrypted with des-cbc-md5 and I have the following defined in the krb.conf file:
[libdefaults]
default_realm = MY.DOMAIN.COM
default_tkt_enctypes = des-cbc-md5
default_tgs_enctypes = des-cbc-md5
If I change the default enctypes to e.g. aes128-cts, I get the following exception:
javax.security.auth.login.LoginException: Do not have keys of types listed in default_tkt_enctypes available; only have keys of following type: DES CBC mode with MD5
I don't understand what is wrong...

Kerberos is a trusted third-party security system: the security token you receive from the client is decryptable only by you, and without contacting any Kerberos infrastructure servers (such as the KDC). You are on the right track; however, it appears you are missing this piece of background Kerberos knowledge to guide you in your further research.
The way this is achieved is that on the server you need a keytab file that contains your server's secret key. The Kerberos server (Microsoft Windows Server, I presume) must have a service principal account created for your service. An administrator can supply you with the keytab file generated for this account, which will contain the secret key.
You then need to configure the server to find this keytab file; it is used in the server-side step involving LoginContext.login. Your code that accepts the security context must be executed inside a doPrivileged code segment within which your server-side credentials are in effect.

If all you want is the username there is an easier way.
request.getUserPrincipal().getName()
http://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getUserPrincipal()
http://docs.oracle.com/javase/7/docs/api/java/security/Principal.html

Related

How can I implement the jwt token prosody plugin in my Docker self-hosted Jitsi server for my Android application?

I am trying to develop an Android application which integrates Jitsi for video conferencing. Normally, a room name is chosen and a room is created. However, anyone that knows or guesses the room name can join the call. In order to prevent this, I want to put a jwt token for conference rooms. I found a link that explains jwt token process for jitsi-meet.
The link is this: https://github.com/jitsi/lib-jitsi-meet/blob/master/doc/tokens.md
In this link I do not understand about three concepts:
Manual plugin configuration
Modify your Prosody config with these three steps:
\1. Adjust plugin_paths to contain the path pointing to jitsi meet Prosody plugins location. That's where plugins are copied on jitsi-meet-token package install. This should be included in global config section(possibly at the beginning of your host config file).
plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
Also optionally set the global settings for key authorization. Both these options default to the '*' parameter which means accept any issuer or audience string in incoming tokens
asap_accepted_issuers = { "jitsi", "some-other-issuer" }
asap_accepted_audiences = { "jitsi", "some-other-audience" }
\2. Under you domain config change authentication to "token" and provide application ID, secret and optionally token lifetime:
VirtualHost "jitmeet.example.com"
authentication = "token";
app_id = "example_app_id"; -- application identifier
app_secret = "example_app_secret"; -- application secret known only to your token
-- generator and the plugin
allow_empty_token = false; -- tokens are verified only if they are supplied by the client
Alternately instead of using a shared secret you can set an asap_key_server to the base URL where valid/accepted public keys can be found by taking a sha256() of the 'kid' field in the JWT token header, and appending .pem to the end
VirtualHost "jitmeet.example.com"
authentication = "token";
app_id = "example_app_id"; -- application identifier
asap_key_server = "https://keyserver.example.com/asap"; -- URL for public keyserver storing keys by kid
allow_empty_token = false; -- tokens are verified only if they are supplied
\3. Enable room name token verification plugin in your MUC component config section:
Component "conference.jitmeet.example.com" "muc"
modules_enabled = { "token_verification" }
In these three instructions, the words "host config file", "domain config file" and "MUC component config section". What are these? I do not know where to do these cahnges.
I think my reply arrives a little bit late, but I try the same to give my contribution :)
If you have installed Jitsi in "classic" way (without docker):
host config file: /etc/prosody/prosody.cfg.lua
domain config file: /etc/prosody/conf.d/<your_domain_name>.cfg.lua
MUC component config section: always in /etc/prosody/conf.d/<your_domain_name>.cfg.lua search the section that starts with Component "conference.<your_domain_name>" "muc"
I hope you have resolved your doubts :)

Programatic username/password access with KeyCloak using external IDP brokering

I'm using Identity Brokering feature and external IDP. So, user logs in into external IDP UI, then KeyCloak broker client receives JWT token from external IDP and KeyCloak provides JWT with which we access the resources. I've set up Default Identitiy Provider feature, so external IDP login screen is displayed to the user on login. That means that users and their passwords are stored on external IDP.
The problem occurs when I need to log in using "Direct Access Grant" (Resource Owner Password grant) programatically in tests. As password is not stored on KeyCloak, I always get 401 Unauthorized error from KeyCloak on login. When I tried to change user password it started to work, so the problem is that user password is not provisioned on KeyCloak and using "Direct Access Grant" KeyCloak doesn't invoke external IDP on programatic login.
I use the following code to obtain access token, but get 401 error everytime I pass valid username/password.
org.keycloak.authorization.client.util.HttpResponseException: Unexpected response from server: 401 / Unauthorized
Direct access grant is enabled for that client.
public static String login(final Configuration configuration) {
final AuthzClient authzClient = AuthzClient.create(configuration);
final AccessTokenResponse accessTokenResponse = authzClient.obtainAccessToken(USERNAME, PASSWORD);
return accessTokenResponse.getToken();
}
Is there any way it can be fixed? For example to call identity broker on "Direct Access Grant", so that KeyCloak provides us it's valid token?
The problem was that KeyCloak has no information about passwords from initial identity provider. They have a token exchange feature which should be used for programmatic token exchange.
External Token to Interanal Token Exchange should be used to achieve it.
Here is an example code in Python which does it (just place correct values in placeholders):
def login():
idp_access_token = idp_login()
return keycloak_token_exchange(idp_access_token)
def idp_login():
login_data = {
"client_id": <IDP-CLIENT-ID>,
"client_secret": <IDP-CLIENT-SECRET>,
"grant_type": <IDP-PASSWORD-GRANT-TYPE>,
"username": <USERNAME>,
"password": <PASSWORD>,
"scope": "openid",
"realm": "Username-Password-Authentication"
}
login_headers = {
"Content-Type": "application/json"
}
token_response = requests.post(<IDP-URL>, headers=login_headers, data=json.dumps(login_data))
return parse_response(token_response)['access_token']
def keycloak_token_exchange(idp_access_token):
token_exchange_url = <KEYCLOAK-SERVER-URL> + '/realms/master/protocol/openid-connect/token'
data = {
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
'subject_token': idp_access_token,
'subject_issuer': <IDP-PROVIDER-ALIAS>,
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
'audience': <KEYCLOAK-CLIENT-ID>
}
response = requests.post(token_exchange_url, data=data,
auth=(<KEYCLOAK-CLIENT-ID>, <KEYCLOAK-CLIENT-SECRET>))
logger.info(response)
return parse_response(response)['access_token']
This example was very useful for me, i only want to add more info about the KEYCLOAK-CLIENT used for the token-exchange (for me authorization_client). I have KEYCLOAK as a broker for IDP ADFS.
First you need to enable the token-exchange feature adding 2 parameters in your keycloak startup command line (depending of how you do it)
-Dkeycloak.profile=preview
-Dkeycloak.profile.feature.token_exchange=enabled
The IDP (for me ADFS) tab Permissions with the token-exchange permission will be available.
Add a policy to the "token-exchange" provider permission, to the client KEYCLOAK-CLIENT
Add this previous policy to the "token-exchange" client permission
With POSTMAN you can test the authentication flow:
External IDP ADFS Login Username/password
Token Exchange

Connecting to ldap using GSSAPI. Wrong service principal

I'm trying to connect to ldap server using SASL. I'm connecting using url ldaps://ldap.example.com but server hostname is host.example.com. ldap.example.com is cname for host.example.com. My program is trying to get service ticket for ldap/ldap.example.com instead of performing reverse dns request and getting ticket for ldap/host.example.com. Everything works fine when I'm using ldap://host.example.com but I prefer to use service CNAME.
There is my code for creating connection factory:
public DefaultConnectionFactory connectionFactory(){
return new DefaultConnectionFactory(connectionConfig());
}
private ConnectionConfig connectionConfig(){
final SaslConfig saslConfig = new SaslConfig();
saslConfig.setMechanism(Mechanism.GSSAPI);
final BindConnectionInitializer connectionInitializer = new BindConnectionInitializer();
connectionInitializer.setBindSaslConfig(saslConfig);
ConnectionConfig connConfig = new ConnectionConfig("ldaps://ldap.example.com");
connConfig.setConnectionInitializer(connectionInitializer);
return connConfig;
}
and jaas.config:
com.sun.security.jgss.initiate {
com.sun.security.auth.module.Krb5LoginModule required
doNotPrompt=true
keyTab="/etc/ldap.keytab"
principal="ldap#EXAMPLE.COM"
storeKey=true
useKeyTab=true
debug=true
;
};
Is there any way to change this behavior?
You should request a new certificate with ldap.example.com as the subject name and with host.example.com as a subject alternative name. The certificate negotiation is handled right before Kerberos.
A couple more suggestions:
All SPNs should be defined in your KDC:
LDAP/ldap.example.com
LDAP/host.example.com
Both of these A records should be set in DNS. Avoid use of CNAMES, while it might be OK at any given time, different browser versions and future updates could cause inconsistent behavior:
ldap.example.com
host.example.com
The principal in jaas.config and the keytab should match. You have:
principal="ldap#EXAMPLE.COM"
I suggest it should be: principal=“ldap/host.example.com“;
Finally, ldap/host.example.com should be defined as the SPN in your keytab. If it is not, it might be OK, as long as you either (1) add it as an additional SPN related in the keytab: How do you add multiple SPNs to the same keytab file for Spnego or Kerberos Configuration? or (2) see Setspn if you are using Active Directory and you application server supports it.
See further reading on GSSAPI.

GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt)

I am very much new to the MOngoDB + Java Configuration. I am trying to achive the connection from remote mongodb server from Java application. I want to use GSSAPI mechanism for connection with mongotemplate. Below code has been executing successfully. Below code is from my configuration file.
List<ServerAddress> serverAddresses = new ArrayList<ServerAddress>();
ServerAddress address = new ServerAddress(host, port);
serverAddresses.add(address);
List<MongoCredential> credentials = new ArrayList<MongoCredential>();
MongoCredential credential = MongoCredential.createGSSAPICredential(userName);
credential.withMechanismProperty("SERVICE_NAME", gssapiServiceName);
credential.withMechanismProperty("CANONICALIZE_HOST_NAME", true);
credentials.add(credential);
return new MongoClient(serverAddresses, credentials);
But when I am trying execute below code I am getting exception
DB db = mongoTemplate.getDb();
Set<String> dbCollections1 = db.getCollectionNames();
Exception:
GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt)
at sun.security.jgss.krb5.Krb5InitCredential.getInstance(Krb5InitCredential.java:147)
at sun.security.jgss.krb5.Krb5MechFactory.getCredentialElement(Krb5MechFactory.java:122)
at sun.security.jgss.GSSManagerImpl.getCredentialElement(GSSManagerImpl.java:193)
at sun.security.jgss.GSSCredentialImpl.add(GSSCredentialImpl.java:427)
at sun.security.jgss.GSSCredentialImpl.(GSSCredentialImpl.java:62)
at sun.security.jgss.GSSManagerImpl.createCredential(GSSManagerImpl.java:154)
at com.mongodb.DBPort$GSSAPIAuthenticator.getGSSCredential(DBPort.java:622)
at com.mongodb.DBPort$GSSAPIAuthenticator.createSaslClient(DBPort.java:593)
at com.mongodb.DBPort$SaslAuthenticator.authenticate(DBPort.java:895)
at com.mongodb.DBPort.authenticate(DBPort.java:432)
at com.mongodb.DBPort.checkAuth(DBPort.java:443)
at com.mongodb.DBTCPConnector.innerCall(DBTCPConnector.java:289)
at com.mongodb.DBTCPConnector.call(DBTCPConnector.java:269)
at com.mongodb.DBCollectionImpl.find(DBCollectionImpl.java:84)
at com.mongodb.DB.command(DB.java:320)
at com.mongodb.DB.command(DB.java:299)
at com.mongodb.DB.command(DB.java:388)
at com.mongodb.DBApiLayer.getCollectionNames(DBApiLayer.java:152)
Million thanks to all who have responded and take a look to my question.
After adding some System Properties and a new conf file, Finally I am able to get connected with MongoDB server. Herewith the updated code -
try {
System.setProperty("java.security.krb5.conf","C:/mongodb/UnixKeytab/krb5.conf");
System.setProperty("java.security.krb5.realm","EXAMPLE.COM");
System.setProperty("java.security.krb5.kdc","example.com");
System.setProperty("javax.security.auth.useSubjectCredsOnly","false");
System.setProperty("java.security.auth.login.config","C:/mongodb/UnixKeytab/gss-jaas.conf");
List<ServerAddress> serverAddresses = new ArrayList<ServerAddress>();
ServerAddress address = new ServerAddress(host, port);
serverAddresses.add(address);
List<MongoCredential> credentials = new ArrayList<MongoCredential>();
MongoCredential credential = MongoCredential.createGSSAPICredential(username);
credentials.add(credential);
MongoClient mongoClient1 = new MongoClient(serverAddresses, credentials);
DB db = mongoClient1.getDB(database);
} catch (UnknownHostException e) {
e.printStackTrace();
}
My krb5.conf file look like below -
[libdefaults]
default_realm = EXAMPLE.COM
default_tkt_enctypes = des-cbc-md5 rc4-hmac
default_tgs_enctypes = des-cbc-md5 rc4-hmac
default_keytab_name = <keytab file path>
[realms]
EXAMPLE.COM = {
kdc = example.com
master_kdc = example.com
default_domain = EXAMPLE.COM
}
INTRANET = {
kdc = example.com
master_kdc = example.com
default_domain = example.com
}
My gss-jaas.conf look like below -
com.sun.security.jgss.initiate {
com.sun.security.auth.module.Krb5LoginModule required
useKeyTab=true
useTicketCache=false
principal="my-account#MY_REALM"
doNotPrompt=true
keyTab="path-to-my-keytab-file"
debug=true;};
Code I have posted is working for me. Hope this will work for others.
Adding some information to this post as its extremely useful already.
If the Sasl/createSaslClient is not run within the Subject:doAs method
that is retrieved from the LoginContext, the credentials will not be picked up from the krb5.conf file. I.e the GSS code looks at the current thread's security manager for the Subject which is registered via the Subject:doAs method, and then uses the credentials from this subject. This Subject should've been obtained via jaas which in turn would read the correct jaas and krb5.conf credentials, but if you do not run the sasl and saslclient methods inside the Subject:doAs method all this doesn't matter.
You can get around it by setting javax.security.auth.useSubjectCredsOnly=false which means if no credentials can be found, some default names in the jaas file will be searched for see LoginConfigImpl.java#92, one is com.sun.security.jgss.initiate.
e.g
com.sun.security.jgss.initiate{
com.sun.security.auth.module.Krb5LoginModule required
doNotPrompt=true
useTicketCache=true
useKeyTab=true
keyTab="mykeytab"
principal="service/host#REALM";
};
I faced the same error "Mechanism level: Failed to find any Kerberos tgt". My problem looks different from yours, but it could be useful to other ones with the same error.
In my case it was caused by an error in writing the principal name in one of my configuration files.
I suggest to check the Jaas LoginManager configuration file (provided with java.security.auth.login.config) and policy files for principals. Typical error is the domain name in lowercase: gino#authdemo.it instead of gino#AUTHDEMO.IT
In the case you set/refer to the principal programmatically, you can also check the principal name correctness in your code.
Regards

LDAP Creating InitialLdapContext fails in Sub Realm / Cross Realm setup in Java using GSSAPI

I have a server process running on a machine KERBOS.COM and its trying to connect to a LDAP Server in IN.KERBOS.COM(Sub Realm) to sync user using GSSAPI Mechanism.
By looking at GSSLOGS we can see that
the spn its trying to authenticate belong to KERBOS.COM ldap/invr28ppqa36.in.kerbos.com#KERBOS.COM
Is this any way to make it pick IN.KERBOS.COM as realm.
The default realm has to be KERBOS.COM in krb5.conf . So changing the default realm to IN.KERBOS.COM is not an option.
I am also giving it fully qualified name so there should be some way to tell it to use IN.KERBOS.COM as realm.
env.put(Context.PROVIDER_URL, String.format("ldap://%s:%d", host, port));
Subject subject = new Subject();
subject.getPrivateCredentials().add(credential);
InitialLdapContext object = Subject.doAs(subject, new PrivilegedExceptionAction<InitialLdapContext>() {
public InitialLdapContext run() throws Exception {
env.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
return new InitialLdapContext(env, null);
} });
LOGS
Subject.doAs fails by throwing an exception (Intercept from Logs are given)
Found ticket for **sysadmin#IN.KERBOS.COM to go to krbtgt/IN.KERBOS.COM#IN.KERBOS.COM** Credentials acquireServiceCreds: obtaining service creds for **ldap/invr28ppqa36.in.kerbos.com#KERBOS.COM**
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 17 16 23 1 3.
KrbException: Fail to create credential. (63) - No service creds
at sun.security.krb5.internal.CredentialsUtil.acquireServiceCreds(CredentialsUtil.java:301)
at sun.security.krb5.Credentials.acquireServiceCreds(Credentials.java:442)
at sun.security.jgss.krb5.Krb5Context.initSecContext(Krb5Context.java:641)
at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:248)
at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:179)
at com.sun.security.sasl.gsskerb.GssKrb5Client.evaluateChallenge(GssKrb5Client.java:193)
at com.sun.jndi.ldap.sasl.LdapSasl.saslBind(LdapSasl.java:123)
at com.sun.jndi.ldap.LdapClient.authenticate(LdapClient.java:232)
at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2740)
at com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:316)
at com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:193)
at com.sun.jndi.ldap.LdapCtxFactory.getUsingURLs(LdapCtxFactory.java:211)
at com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:154)
at com.sun.jndi.ldap.LdapCtxFactory.getInitialContext(LdapCtxFactory.java:84)
at javax.naming.spi.NamingManager.getInitialContext(NamingManager.java:684)
at javax.naming.InitialContext.getDefaultInitCtx(InitialContext.java:307)
at javax.naming.InitialContext.init(InitialContext.java:242)
at javax.naming.ldap.InitialLdapContext.<init>(InitialLdapContext.java:153)
at (LDAPConnector.java:101)
By default a Kerberos implementation will start at the KDC of the client realm, hoping that KDC can provide a cross-realm referral to the service realm. So, you have a ticket in EXAMPLE.COM, and you're going to a host called server.subdomain.example.com. Your client is supposed to go to the EXAMPLE.COM KDC. That KDC can either say that it knows how to give you a service ticket, or can give you a special referral ticket to get closer to the service; in this example you'd kind of expect it to give you a krbtgt/SUBDOMAIN.EXAMPLE.COM#EXAMPLE.COM realm. That tells your client to go to the subdomain.example.com KDC.
Apparently, your KERBOS.COM KDC doesn't know that your LDAP server is served by the IN.KERBOS.COM realm. One way you could solve things is by fixing your KDC. That's going to be dependent on what KDC software you have. Alternatively, you can edit your krb5.conf and tell your client that everything under in.kerbos.com is served by the IN.KERBOS.COM KDC. Add a section like the following to your krb5.conf:
[domain_realm]
.in.kerbos.com = IN.KERBOS.COM
See this as an example of discussing krb5.conf including domain_realm sections in Java. See RFC 6806 for technical details of how the referrals should work.

Categories

Resources