Talking to FreeIPA API with sessions and JSON-RPC
Occasionally I see questions on how to drive FreeIPA programmatically.
One can use ipa <command>
from enrolled IPA clients or go directly to
Python API (as /usr/sbin/ipa
utility is just a tiny shim over the Python API).
However, if you want to drive operations from other frameworks or from non-IPA
clients, there is another way and it is actually very simple.
FreeIPA web UI is one example of such use. It is a JavaScript-based application which is downloaded by the browser when visiting IPA web site. The application bootstraps itself and issues JSON-RPC requests to the server. Browser does authentication and caching via cookies.
Surely, we can achieve the same from any other framework. There are two separate stages we’d have to go through to avoid constant re-authentication:
-
Authenticate against IPA server and remember the cookie for our session
-
Use cookie with session data while issuing our commands
There are separate authentication end points in IPA, one for Kerberos and another for password-based authentication. Both return a session cookie which we need to store and present to the server. If cookie is invalid, authentication need to be repeated again. This flow is well-known to any web application developer so no surprises here.
FreeIPA RPC expectations
In order to support secure web application development, FreeIPA web API expects web applications set HTTP referer back to the IPA host. Web browsers do this automatically, other frameworks have to provide it explicitly.
For example, with curl
you’d need to specify -H referer:https://$IPAHOSTNAME/ipa
.
Kerberos authentication
To authenticate with Kerberos, post an authorization request using Negotiate
scheme over HTTPS to the end point at
https://$IPAHOSTNAME/ipa/session/login_kerberos
. You need to have actual
credentials in your credentials cache (ccache) prior to sending the request.
The way Kerberos authentication is done, one obtains a ticket granting ticket
(TGT) first, storing it in a ccache and then an application can request a
ticket to a service using existing TGT. A typical non-interactive use is when
ccache is initialized with the TGT based on pre-existing key from a service
keytab:
$ export KRB5CCNAME=FILE:/path/to/ccache
$ export COOKIEJAR=/path/to/my.cookie
$ export IPAHOSTNAME=ipa-master.example.com
$ kinit -k -t /path/to/service.keytab service/ipa-client.example.com
$ curl -v \
-H referer:https://$IPAHOSTNAME/ipa \
-c $COOKIEJAR -b $COOKIEJAR \
--cacert /etc/ipa/ca.crt \
--negotiate -u : \
-X POST \
https://$IPAHOSTNAME/ipa/session/login_kerberos
If authentication was successful, our $COOKIEJAR
file will contain all
session cookies returned by FreeIPA web server. We don’t need to authenticate
anymore until the session expires. All we need to do is to ensure we pass back
the session cookies back to the server.
Password Authentication
Password authentication is more traditional: post over HTTPS a form with username
and password fields to the end point at https://$IPAHOSTNAME/ipa/session/login_password
.
A form has to be setup with appropriate MIME type, application/x-www-form-urlencoded
but
the rest is plain old HTTPS post.
$ export COOKIEJAR=/path/to/my.cookie
$ export IPAHOSTNAME=ipa-master.example.com
$ s_username=admin s_password=mYSecReT1P2 curl -v \
-H referer:https://$IPAHOSTNAME/ipa \
-H "Content-Type:application/x-www-form-urlencoded" \
-H "Accept:text/plain"\
-c $COOKIEJAR -b $COOKIEJAR \
--cacert /etc/ipa/ca.crt \
--data "user=$s_username&password=$s_password" \
-X POST \
https://$IPAHOSTNAME/ipa/session/login_password
If authentication was successful, our $COOKIEJAR
file will contain all
session cookies returned by FreeIPA web server. We don’t need to authenticate
anymore until the session expires. All we need to do is to ensure we pass back
the session cookies back to the server.
Sending JSON-RPC request
A session-based request needs to be posted to
https://$IPAHOSTNAME/ipa/session/json
end point over HTTPS. A content type
should set to application/json
and HTTP POST method has to be used. There
isn’t any difference, again, from a typical JSON-RPC. Session cookies should
not be forgotten, of course, or our request will fail spectacular.
FreeIPA JSON-RPC interface is not documented. However, it is easy to discover
via existing command line utility, /usr/sbin/ipa
by supplying -vv
option to
it:
$ ipa -vv ping
ipa: INFO: trying https://ipa-master.example.com/ipa/session/json
ipa: INFO: Forwarding 'ping' to json server 'https://ipa-master.example.com/ipa/session/json'
ipa: INFO: Request: {
"id": 0,
"method": "ping",
"params": [
[],
{
"version": "2.117"
}
]
}
ipa: INFO: Response: {
"error": null,
"id": 0,
"principal": "admin@EXAMPLE.COM",
"result": {
"summary": "IPA server version 4.1.99.201505121153GITed639c7. API version 2.117"
},
"version": "4.1.99.201505121153GITed639c7"
}
-------------------------------------------------------------------
IPA server version 4.1.99.201505121153GITed639c7. API version 2.117
-------------------------------------------------------------------
Each request has a method
and parameters, params
structure. A method is a
command to execute. Commands of /usr/sbin/ipa
utility have simple structure
of topic-action
and methods corresponding for them are topic_action
, i.e.
dash is replaced by underscore. This is because they map one to one to Python
API classes.
Structure of parameters is simple too. params
is an array of two elements:
. an array of positional arguments, and
. a dictionary of options
As can be seen above, ping
method does not have positional arguments and IPA
command line client always sends own version as an option.
Another example is user-show
with --raw
option passed:
$ ipa -vv user-show admin --raw
ipa: INFO: trying https://ipa-master.example.com/ipa/session/json
ipa: INFO: Forwarding 'user_show' to json server 'https://ipa-master.example.com/ipa/session/json'
ipa: INFO: Request: {
"id": 0,
"method": "user_show",
"params": [
[
"admin"
],
{
"all": false,
"no_members": false,
"raw": true,
"rights": false,
"version": "2.117"
}
]
}
[.... lots of output ....]
The resulting entry output is skipped for brevity.
Gathering things together
Finally, a script below combines both password and Kerberos authentication
and then sends user_find
request to receive list of all users.
# testcurl.sh
s_username=admin
s_password=mYSecReT1P2
IPAHOSTNAME=ipa-master.example.com
COOKIEJAR=my.cookie.jar
#rm -f $COOKIEJAR
klist -s
use_kerberos=$?
if [ ! -f $COOKIEJAR ] ; then
if [ $use_kerberos -eq 0 ] ; then
# Login with Kerberos
curl -v \
-H referer:https://$IPAHOSTNAME/ipa \
-c $COOKIEJAR -b $COOKIEJAR \
--cacert /etc/ipa/ca.crt \
--negotiate -u : \
-X POST \
https://$IPAHOSTNAME/ipa/session/login_kerberos
else
# Login with user name and password
curl -v \
-H referer:https://$IPAHOSTNAME/ipa \
-H "Content-Type:application/x-www-form-urlencoded" \
-H "Accept:text/plain"\
-c $COOKIEJAR -b $COOKIEJAR \
--cacert /etc/ipa/ca.crt \
--data "user=$s_username&password=$s_password" \
-X POST \
https://$IPAHOSTNAME/ipa/session/login_password
fi
fi
# Send user_find method request
curl -v \
-H referer:https://$IPAHOSTNAME/ipa \
-H "Content-Type:application/json" \
-H "Accept:applicaton/json"\
-c $COOKIEJAR -b $COOKIEJAR \
--cacert /etc/ipa/ca.crt \
-d '{"method":"user_find","params":[[""],{}],"id":0}' \
-X POST \
https://$IPAHOSTNAME/ipa/session/json
Using JSON-RPC calls on non-IPA clients
If FreeIPA JSON-RPC API needs to be access from non-enrolled client, there is a
bit more work. Kerberos authentication would most likely require properly
configured /etc/krb5.conf
. Luckily, the configuration can be copied over from
existing IPA client and placed somewhere --- Kerberos library allows to specify
configuration file via KRB5_CONFIG
environmental variable. A keytab can be
copied over too but make sure to store it securely.
Another thing to copy over to a non-IPA client is a CA root certificate to allow secure HTTPS communication. Other than that, everything stays the same — authenticate first, store session cookies, and re-use them.