not logged in | [Login]

This document describes how to set up FreeRADIUS to authenticate users in two steps. First the username/password is authenticated against Active Directory. If successful, an Access-Challenge message is returned to the client requesting it to send a second Access-Request with an OTP code. The second request is then proxied by FreeRADIUS to an external RADIUS OTP service for verification.

This guide was tested and verified using Gemalto Safenet Authentication Services (SAS) as the OTP service. As Gemalto SAS currently doesn't support pre-authenticating users AD-password before OTP, we add a FreeRADIUS server in front of the SAS service to pre-authenticate the users AD credentials.

Prerequisites

Install FreeRADIUS on your favourite Linux distribution. In this guide we have used CentOS 7, and FreeRADIUS v3.0.13 that is available in the CentOS repos:

yum install -y freeradius freeradius-ldap freeradius-utils

FreeRADIUS Configuration

LDAP Authentication

In this guide we'll use the LDAP module to perform AD authentication. To perform LDAP authentication against Active Directory, FreeRADIUS must know the users ClearText password, meaning the client must be configured to use PAP authentication.

If you require supporting MS-CHAPv2 authentication, you should look into using Samba and winbind for authentication instead of LDAP. Please see the following HOWTO:

http://wiki.freeradius.org/guide/Active-Directory-direct-via-winbind

Authenticating using LDAP can take a few different approaches:

  1. Bind with an admin-user, perform a search for auth-user and return the Cleartext-Password. Verfify with PAP module.
  2. Bind with an admin-user, perform a search for auth-user and then attempt to re-bind as authenticating user.
  3. Attempt a direct bind as the authenticating user.

Method #1 doesn't work with Active Directory as LDAP source as it doesn't allow you to poll user passwords, and #2 doesn't really gain us anything in this scenario, so in this guide we'll use method #3 which requires minimal configuration and no admin/service-account is needed in the AD.

Edit /etc/raddb/modules-available/ldap:

ldap {
        server = 'IP of Domain Controller'
}

Proxy Realm

Edit /etc/raddb/proxy.conf and configure a home_server, home_server_pool and realm:

home_server test {
        ipaddr          = 10.0.0.100
        port            = 1812
        secret          = testing123
        type            = auth

        # By explicitly setting our SourceIP we can define multiple different 
        # Proxy Realms and proxy with different SourceIPs to differentiate between
        # multiple different customers:
        src_ipaddr      = 10.0.0.10
}

home_server_pool test_pool {
        home_server     = test
}

realm proxy-test {
        auth_pool       = test_pool
}

Virtual Server configuration

Create a new Virtual Server site configuration file in /etc/raddb/sites-available or edit the default site:

authorize {
        if (!State) {
                if (&User-Password) {
                        # If !State and User-Password (PAP), then force LDAP:
                        update control {
                                Ldap-UserDN := "%{User-Name}@my-domain.com"
                                Auth-Type := LDAP
                        }
                }
                else {
                        reject
                }
        }
        else {
                # If State, then proxy request:
                update control {
                        Proxy-To-Realm := "proxy-test"
                }
        }
}

authenticate {
        Auth-Type LDAP {
                # Attempt authentication with a direct LDAP bind:
                ldap
                if (ok) {
                        update reply {
                                # Create a random State attribute:
                                State := "%{randstr:aaaaaaaaaaaaaaaa}"
                                Reply-Message := "Please enter OTP"
                        }
                        # Return Access-Challenge:
                        challenge
                }
        }
}

pre-proxy {
        # Enable pre-proxy to filter State attribute from proxied requests:
        attr_filter.pre-proxy
}

Configuration break-down

So, what is actually happening here? Lets break it down:

        if (!State) {

First we check if this is the first Access-Request packet we receive by checking for the existence of a State attribute. If it's absent that means this is the initial request.

                if (&User-Password) {

As we only support PAP authentication, if check for the existence of the User-Password attribute. If it's available we proceed:

                                Ldap-UserDN := "%{User-Name}@my-domain.com"

To force a direct LDAP bind using the authenticating users credentials we explicitly set the Ldap-UserDN attribute. For Active Directory LDAP the syntax username@my-domain.com is usually working.

                                Auth-Type := LDAP

Force authentication to be done using Auth-Type LDAP.

        else {
                # If State, then proxy request:
                update control {
                        Proxy-To-Realm := "proxy-test"
                }

We did have a State attribute available meaning this is the second Access-Request we receive, so lets proxy it to our external OTP service provider by manually specifying the Realm we want to proxy it to.

authenticate {
        Auth-Type LDAP {
                # Attempt authentication with a direct LDAP bind:
                ldap
                if (ok) {
                        update reply {
                                # Create a random State attribute:
                                State := "%{randstr:aaaaaaaaaaaaaaaa}"
                                Reply-Message := "Please enter OTP"
                        }
                        # Return Access-Challenge:
                        challenge
                }
        }
}

In the authenticate section we send the request off to the ldap module for authentication by testing a direct bind using the credentials we received in the request. If it succeeds we create a new random State attribute and tell FreeRADIUS to return an Access-Challenge message to the client.

Pre-Proxy Attributes filter

As our external OTP service provider only sees the second Access-Request message and is unaware that we've created a State, we need to filter out the State attribute from the proxied requests. We accomplish this by enabling attr_filter.pre-proxy in the pre-proxy section:

pre-proxy {
        # Enable pre-proxy to filter State attribute from proxied requests:
        attr_filter.pre-proxy
}

And comment out State in /etc/raddb/mods-config/attr_filter:

DEFAULT
        User-Name =* ANY,
        User-Password =* ANY,
        CHAP-Password =* ANY,
        CHAP-Challenge =* ANY,
        MS-CHAP-Challenge =* ANY,
        MS-CHAP-Response =* ANY,
        EAP-Message =* ANY,
        Message-Authenticator =* ANY,
        #State =* ANY,
        NAS-IP-Address =* ANY,
        NAS-Identifier =* ANY,
        Operator-Name =* ANY,
        Calling-Station-Id =* ANY,
        Chargeable-User-Identity =* ANY,
        Proxy-State =* ANY