Home

Awesome

This page describes how to set up FreeRADIUS using rlm_perl to communicate with an OAuth2 identity provider backend allowing users to connect to a wireless 802.1X (WPA Enterprise) network without needing on premise systems.

Your OAuth2 provider must support the Resource Owner Password Credentials Grant; this means (for now) only Microsoft Azure Active Directory is supported. The Password Grant is necessary as it is the only grant flows that does not require user interaction with a web browser (think "log in via Google/Facebook/LinkedIn/...") which is impossible during an 802.1X authentication as the user's workstation does not have an IP address.

For 802.1X (wired and WPA Enterprise wireless) authentication, you must use EAP-TTLS/PAP so that the cleartext password is securely transported to your RADIUS server and usable with the password grant flow. Fortunately client support is widespread and so Linux, Android, BB10, macOS/iOS (via a .mobileconfig) and Microsoft Windows 8 or later (use a supplicant extension such as SecureW2 Enterprise Client for earlier versions) users will have have no problems. Ignore the FUD around EAP-TTLS/PAP which in practice works identically to how web browsers transmit credentials over HTTPS (PEAP/MSCHAPv2 is similarly vulnerable); like HTTPS though you must use a valid certificate and configure your clients to verify the server name for it to be safe.

N.B. this will not work with MFA enabled accounts but you can workaround this with a conditional access policy

Features

Many of these features aim to try to not communicate with Azure so to hide both latency and throttling problems.

Support

These instructions assume you are familiar with using FreeRADIUS in an 802.1X environment and if you are not you should start with a EAP-TTLS/PAP 802.1X deployment using static credentials stored in a local users file.

If you run into problems getting a users file environment to run, then please seek support from the FreeRADIUS community but do not ask there for help on how to use this module.

Once you are more familiar with using FreeRADIUS and have the above working, then you should try to follow these instructions. If you run into problems then do seek non-guaranteed 'best effort' help from me through a GitHub issue including the full and complete output of freeradius -X | tee /tmp/radiusd.log with both client secrets and User-Password obscured. Do not send truncated output, or the output you think is important, if you do, it is likely your issue will be closed.

If you do open a GitHub issue you must be either using the packaging from Network RADIUS (process described below) or have compiled from source the v3.2.x branch (or v3.0.x branch). If you do not do this, for example instead use your distribution's (Redhat, Ubuntu, ...) packaging, your issue is likely to be closed as I am unable to provide Pro Bono consultancy for your organisation.

This project is a volunteer backed effort and the volunteer (ie. me) requests when asking for free assistance you use a supported environment. This is a reasonable request.

Consultancy services are available through coreMem Limited.

Preflight

On the target RADIUS server, as root fetch a copy of the project, the recommended approach is to use git with:

cd /opt
git clone https://github.com/jimdigriz/freeradius-oauth2-perl.git
cd freeradius-oauth2-perl

N.B. alternatively open the URL above in your browser, click on 'Clone or download' and use the 'Download ZIP'

You now need to install FreeRADIUS 3.2.x as your target, and it is strongly recommended you use the packages distributed by Network RADIUS.

How to use Debian is described below, but the instructions should be adaptable with ease to Ubuntu and with not too much work for CentOS. Pull requests are welcomed from those who worked out how to get this working on other OS's (eg. *BSD, another Linux, macOS, ...) and/or a later version of FreeRADIUS.

Debian/Ubuntu

Starting with a fresh empty Debian 'bookworm' 12.x (or Ubuntu 'jammy' 22.04) or later installation, as root run the following:

apt-get update
apt-get -y install --no-install-recommends ca-certificates curl libjson-pp-perl libwww-perl

Now follow the instructions at the FreeRADIUS Packages site to install their release of FreeRADIUS.

You should now have a working FreeRADIUS 3.2.x installation but to verify you have done this step correctly, please run the following:

dpkg-query --showformat '${Maintainer}\n' -W freeradius
Network RADIUS SARL <info@networkradius.com>

Confirm the output states "Network RADIUS SARL", if it lists "Debian" (or "Ubuntu") then you need to recheck what you did as it was incorrect.

N.B. these instructions were tested using docker run -it --rm -v $(pwd):/opt/freeradius-oauth2-perl debian:bookworm (and on ubuntu:jammy) with FreeRADIUS 3.2.3, so your numbering may be different!

It is strongly recommended at this point you create a backup of the original configuration:

cp -a /etc/freeradius /etc/freeradius.orig

This will let you to track the changes you made using:

diff -u -N -r /etc/freeradius.orig /etc/freeradius

N.B. if your configuration shows to have a 3.2 (or 3.0) directory in /etc/freeradius then you have not correctly followed the instructions above so recheck!

Configuration

Microsoft Azure AD (Office 365)

  1. Log into your Microsoft Azure account as an administrator
  2. open the 'Azure Active Directory' service
  3. under 'Manage' in the left hand panel, go to 'App registrations' and select 'New registration'
  4. use the following settings and then click on 'Register':
    • Name: freeradius-oauth2-perl
    • Supported account types: Accounts in this organizational directory only - (Single tenant)
    • Redirect URI (optional): [blank]
  5. make a note of the 'Client ID' for later
  6. for your new application, go to 'Certificates & secrets' and click on 'New client secret'
    • it is recommended for the description you use the server name of your RADIUS server
    • if you set an expiry, remember to set a reminder in your calendar!
  7. make a note of the newly created 'Client secret' (you will not be able to retrieve it later!)
  8. for your new application, go to 'API permissions'
    1. click on 'Add a permission'
      1. go to the 'Microsoft APIs' tab
      2. select 'Microsoft Graph'
      3. select 'Application permissions'
      4. check Directory.Read.All
      5. click on 'Add permissions'
    2. User.Read should be an already present 'Delegated' permission type
    3. click on the 'Grant admin consent' button (you will receive an email notification that you have done this)

FreeRADIUS

After updating the following files as described below (you may need to replace freeradius with raddb), you should restart FreeRADIUS (sudo systemctl restart freeradius) to apply the changes.

Add the following to /etc/freeradius/proxy.conf:

realm example.com {
    oauth2 {
        discovery = "https://login.microsoftonline.com/%{Realm}/v2.0"
        client_id = "..."
        client_secret = "..."
        cache_password = yes
    }
}

Replacing example.com with your domain and oauth2_client_{id,secret} with the noted values from earlier and if you maintain multiple domains you should add multiple blocks here too.

N.B. do not use regular expression to capture your realm, you must create an entry for each and every (sub-)domain you intend to use

If local policy requires you to disable password caching then you can set cache_password = no (default: yes, anything else is treated as no) but it is strongly recommended this is enabled as it improves user-experience and provides protection from potential service outages if Azure decides to throttle you.

Run the following as root:

printf '\n$INCLUDE /opt/freeradius-oauth2-perl/dictionary\n' >> /etc/freeradius/dictionary
ln -s /opt/freeradius-oauth2-perl/module /etc/freeradius/mods-enabled/oauth2
ln -s /opt/freeradius-oauth2-perl/policy /etc/freeradius/policy.d/oauth2

Edit your /etc/freeradius/sites-enabled/default:

N.B. start with the stock/upstream packaged default and add to it, do not strip or change anything until you have a working configuration. Once you have a working configuration then do explore customising it to fit your needs but if you break something this module will return invalid (ie. dependency on the suffix module setting the Realm attribute)

This should look something like:

authorize {
    ...

    -ldap

    oauth2
    #if (updated) {
    #
    #    # uncomment to enforce the group membership 'network-users'
    #    if (!(&OAuth2-Group && &OAuth2-Group[*] == "network-users")) {
    #        reject
    #    }
    #
    #    # uncomment to use group membership for VLAN assignment
    #    update {
    #        Tunnel-Type := VLAN
    #        Tunnel-Medium-Type := IEEE-802
    #        Tunnel-Private-Group-ID := 11
    #    }
    #    if (&OAuth2-Group) {
    #        if (&OAuth2-Group[*] == "staff") {
    #            update {
    #                Tunnel-Private-Group-ID := 13
    #            }
    #        } elsif (&OAuth2-Group[*] == "students") {
    #            update {
    #                Tunnel-Private-Group-ID := 15
    #            }
    #        } else {
    #            update {
    #                Tunnel-Private-Group-ID := 17
    #            }
    #        }
    #    }
    #
    #}

    #daily

    ...
}

...

authenticate {
    ...

#   Auth-Type eap {
#       ...
#   }

    Auth-Type oauth2 {
        oauth2
    }
}

post-auth {
    ...

    #ldap

    oauth2

    exec

    ...
}

802.1X

You should edit your /etc/freeradius/sites-enabled/inner-tunnel file similarly to how you amended /etc/freeradius/sites-enabled/default above.

N.B. start with the stock/upstream packaged inner-tunnel and add to it, do not strip or change anything until you have a working configuration. Once you have a working configuration then do explore customising it to fit your needs but if you break something this module will return invalid (ie. dependency on the suffix module setting the Realm attribute)

Troubleshooting

After a restart, you should be able to do an authentication against the server using radtest:

radtest USERNAME@example.com PASSWORD 127.0.0.1 0 testing123

Please note that due to limitations in FreeRADIUS and around rlm_perl:

If your authentication fails, then you may see some Reply-Message attributes from Azure if there is a problem with the account. If there is no Reply-Message then your next step is to stop FreeRADIUS and run it in debugging mode:

sudo systemctl stop freeradius
sudo freeradius -X

Now from another terminal re-run radtest and in the debugging output from FreeRADIUS should be clues to the underlying problem.

Whilst FreeRADIUS is in debugging mode, you can monitor the database replication by looking for (this may be interleaved with other debug output so do use grep 'oauth2 worker'):

rlm_perl: oauth2 worker (example.com): sync                   <-- process starts
rlm_perl: oauth2 worker (example.com): sync users             <-- starting fetch of users
rlm_perl: oauth2 worker (example.com): users page             <-- page of user results (initial sync has lots of these!)
rlm_perl: oauth2 worker (example.com): sync groups            <-- starting fetch of group memberships
rlm_perl: oauth2 worker (example.com): groups page            <-- page of group results (initial sync has lots of these!)
rlm_perl: oauth2 worker (example.com): apply                  <-- process complete new data made live
rlm_perl: oauth2 worker (example.com): syncing in 32 seconds  <-- next sync ('ttl' scheduled with 33% fuzz)

HTTPS Requests

If you edit module and set debug = yes for the configuration for the Perl oauth2_perl section, the debugging output will also include the plaintext HTTP requests and responses between the module and Azure; the output includes passwords and credential tokens used.

N.B. do not leave this enabled in production!

802.1X

N.B. do not try to debug an 802.1X authentication until after you have managed to get the much simpler radtest to work for you

You will require a copy of eapol_test which to build from source on your target RADIUS server you type:

sudo apt-get -y install --no-install-recommends build-essential git libdbus-1-dev libnl-3-dev libnl-genl-3-dev libnl-route-3-dev libssl-dev pkg-config
git clone git://w1.fi/hostap.git
cd hostap
sed -e 's/^#CONFIG_EAPOL_TEST=y/CONFIG_EAPOL_TEST=y/' wpa_supplicant/defconfig > wpa_supplicant/.config
make -C wpa_supplicant -j$(($(getconf _NPROCESSORS_ONLN)+1)) eapol_test

Once built, you will need a configuration file (amending USERNAME, PASSWORD and example.com):

cat <<'EOF' > eapol_test.conf
network={
    key_mgmt=IEEE8021X
    eap=TTLS
    anonymous_identity="@example.com"
    identity="USERNAME@example.com"
    password="PASSWORD"
    phase2="auth=PAP"
}
EOF

N.B. do not use this configuration on your clients without adding certificate validation options such as ca_path and domain_match

To test it works run:

$ ./wpa_supplicant/eapol_test -s testing123 -c eapol_test.conf

A successful test will have again an Access-Accept towards the end of the output:

Received RADIUS message
RADIUS message: code=2 (Access-Accept) identifier=6 length=174
   Attribute 26 (Vendor-Specific) length=58
      Value: 00000137113...df32a90a69
   Attribute 26 (Vendor-Specific) length=58
      Value: 00000137103...59ae28081b
   Attribute 79 (EAP-Message) length=6
      Value: 036a0004
   Attribute 80 (Message-Authenticator) length=18
      Value: 3c4829e4901baac9bb9880acfd69feab
   Attribute 1 (User-Name) length=14
      Value: '@example.com'
STA 02:00:00:00:00:01: Received RADIUS packet matched with a pending request, round trip time 0.02 sec

N.B. in the case of a failure you will not get a set of Reply-Message attributes in the Access-Reject as EAP does not allow this