OATH Toolkit pam_oath/README
Copyright (C) 2009-2025 Simon Josefsson.  Licensed under the GPLv3+.

This directory holds an PAM module for authenticating users using OATH
one-time password technology.

Configuration
-------------

To use the module, first make sure that it is accessible as
/lib/security/pam_oath.so:

---------
# ln -s /usr/local/lib/security/pam_oath.so /lib/security/pam_oath.so
# ls -la /lib/security/pam_oath.so
lrwxrwxrwx 1 root root 35 Dec  4 10:49 /lib/security/pam_oath.so -> /usr/local/lib/security/pam_oath.so
#
---------

The next step is to configure PAM to use the module.  One simple way
for testing is to configure it for "su".  Make sure you have a root
window open before making any changes!

---------
# head -1 /etc/pam.d/su
auth [user_unknown=ignore success=ok] pam_oath.so debug usersfile=/etc/users.oath window=20
#
---------

The next step is to create a user credentials file.  As you can see,
the PAM configuration uses /etc/users.oath for this.  So we create the
file:

---------
# cat>/etc/users.oath
HOTP root - 00
# chmod go-rw /etc/users.oath
#
---------

The file format handles any whitespace as field separator.  You may
also add lines starting with '#' for comments.  The file format is
documented here: http://code.google.com/p/mod-authn-otp/wiki/UsersFile

WARNING!  The above added an OATH secret of all-zeros, which leads to
no security.  In production, replace "00" with a randomly generate hex
encoded data of say, 20 bytes in size.

To test the setup, we need to generate some one-time passwords.  The
"oathtool" is handy for this purpose.  Replace 00 with the key you
generated.

---------
# oathtool/oathtool -w 5 00
328482
812658
073348
887919
320986
435986
#
---------

So let's test this by running 'su'.  At the prompt, you type the
password (in this example, "pw") concatenated with the OTP (in this
example, "328482").

---------
jas@mocca:~$ su
[pam_oath.c:parse_cfg(122)] called.
[pam_oath.c:parse_cfg(123)] flags 0 argc 4
[pam_oath.c:parse_cfg(125)] argv[0]=alwaysok
[pam_oath.c:parse_cfg(125)] argv[1]=debug
[pam_oath.c:parse_cfg(125)] argv[2]=usersfile=/etc/users.oath
[pam_oath.c:parse_cfg(125)] argv[3]=window=20
[pam_oath.c:parse_cfg(126)] debug=1
[pam_oath.c:parse_cfg(127)] alwaysok=1
[pam_oath.c:parse_cfg(128)] try_first_pass=0
[pam_oath.c:parse_cfg(129)] use_first_pass=0
[pam_oath.c:parse_cfg(130)] usersfile=/etc/users.oath
[pam_oath.c:parse_cfg(131)] digits=0
[pam_oath.c:parse_cfg(132)] window=20
[pam_oath.c:pam_sm_authenticate(162)] get user returned: root
One-time password (OATH) for `root':
[pam_oath.c:pam_sm_authenticate(237)] conv returned: 328482
[pam_oath.c:pam_sm_authenticate(297)] OTP: 328482
[pam_oath.c:pam_sm_authenticate(308)] authenticate rc 0 last otp Wed Jun  3 19:22:50 1931

[pam_oath.c:pam_sm_authenticate(329)] done. [Success]
[pam_oath.c:pam_sm_setcred(341)] called.
[pam_oath.c:pam_sm_setcred(347)] retval: 0
[pam_oath.c:pam_sm_setcred(367)] done. [Success]
mocca:/home/jas#
---------

Success!

The code should have updated the /etc/users.oath file to record the
incremented counter, last OTP and timestamp.  Inspect the new value as
follows:

---------
# cat /etc/users.oath
HOTP	root	-	00	0	328482	2009-12-07T23:23:49L
#
---------

In production use you will want to remove the 'debug' parameter in the
PAM configuration, and make sure you use random keys instead of 00.

Two-factor authentication
-------------------------

To also verify passwords, you need to configure the one-time password
length for the PAM module:

---------
# head -1 /etc/pam.d/su
auth requisite pam_oath.so usersfile=/etc/users.oath window=20 digits=6
#
---------

Next specify a password in the users.oath file:

---------
# cat>/etc/users.oath
HOTP root pw 00
# chmod go-rw /etc/users.oath
#
---------

Then test it:

---------
jas@mocca:~$ su
[pam_oath.c:parse_cfg(122)] called.
[pam_oath.c:parse_cfg(123)] flags 0 argc 5
[pam_oath.c:parse_cfg(125)] argv[0]=alwaysok
[pam_oath.c:parse_cfg(125)] argv[1]=debug
[pam_oath.c:parse_cfg(125)] argv[2]=usersfile=/etc/users.oath
[pam_oath.c:parse_cfg(125)] argv[3]=window=20
[pam_oath.c:parse_cfg(125)] argv[4]=digits=6
[pam_oath.c:parse_cfg(126)] debug=1
[pam_oath.c:parse_cfg(127)] alwaysok=1
[pam_oath.c:parse_cfg(128)] try_first_pass=0
[pam_oath.c:parse_cfg(129)] use_first_pass=0
[pam_oath.c:parse_cfg(130)] usersfile=/etc/users.oath
[pam_oath.c:parse_cfg(131)] digits=6
[pam_oath.c:parse_cfg(132)] window=20
[pam_oath.c:pam_sm_authenticate(163)] get user returned: root
One-time password (OATH) for `root':
[pam_oath.c:pam_sm_authenticate(238)] conv returned: pw820368
[pam_oath.c:pam_sm_authenticate(279)] Password: pw
[pam_oath.c:pam_sm_authenticate(297)] OTP: 820368
[pam_oath.c:pam_sm_authenticate(308)] authenticate rc 0 last otp Mon Jun  1 20:43:54 1931

[pam_oath.c:pam_sm_authenticate(330)] done. [Success]
[pam_oath.c:pam_sm_setcred(342)] called.
[pam_oath.c:pam_sm_setcred(348)] retval: 0
[pam_oath.c:pam_sm_setcred(368)] done. [Success]
mocca:/home/jas#
---------

If you supply an incorrect password, you'll get:

---------
jas@mocca:~$ su
[pam_oath.c:parse_cfg(122)] called.
[pam_oath.c:parse_cfg(123)] flags 0 argc 5
[pam_oath.c:parse_cfg(125)] argv[0]=alwaysok
[pam_oath.c:parse_cfg(125)] argv[1]=debug
[pam_oath.c:parse_cfg(125)] argv[2]=usersfile=/etc/users.oath
[pam_oath.c:parse_cfg(125)] argv[3]=window=20
[pam_oath.c:parse_cfg(125)] argv[4]=digits=6
[pam_oath.c:parse_cfg(126)] debug=1
[pam_oath.c:parse_cfg(127)] alwaysok=1
[pam_oath.c:parse_cfg(128)] try_first_pass=0
[pam_oath.c:parse_cfg(129)] use_first_pass=0
[pam_oath.c:parse_cfg(130)] usersfile=/etc/users.oath
[pam_oath.c:parse_cfg(131)] digits=6
[pam_oath.c:parse_cfg(132)] window=20
[pam_oath.c:pam_sm_authenticate(163)] get user returned: root
One-time password (OATH) for `root':
[pam_oath.c:pam_sm_authenticate(238)] conv returned: grr525990
[pam_oath.c:pam_sm_authenticate(279)] Password: grr
[pam_oath.c:pam_sm_authenticate(297)] OTP: 525990
[pam_oath.c:pam_sm_authenticate(308)] authenticate rc -8 last otp Tue Jun  2 19:29:14 1931

[pam_oath.c:pam_sm_authenticate(314)] One-time password not authorized to login as user 'root'
[pam_oath.c:pam_sm_authenticate(327)] alwaysok needed (otherwise return with 9)
[pam_oath.c:pam_sm_authenticate(330)] done. [Success]
[pam_oath.c:pam_sm_setcred(342)] called.
[pam_oath.c:pam_sm_setcred(348)] retval: 0
[pam_oath.c:pam_sm_setcred(368)] done. [Success]
mocca:/home/jas#
---------

List of all parameters
----------------------

  "debug": Enable debug output to stdout.

  "alwaysok": Enable that all authentication attempts should succeed
              (aka presentation mode).

  "try_first_pass": Before prompting the user for their password, the
                    module first tries the previous stacked module´s
                    password in case that satisfies this module as
                    well.

  "use_first_pass": The argument use_first_pass forces the module to
                    use a previous stacked modules password and will
                    never prompt the user - if no password is
                    available or the password is not appropriate, the
                    user will be denied access.

  "usersfile": Specify filename where credentials are stored, for
               example "/etc/users.oath". The placeholder values
               "${USER}" and "${HOME}" may be used to specify the
               filename on a per-user basis. The values "${USER}" and
               "${HOME}" expand to the user's username and home
               directory, respectively.  See below on file protection.

  "digits": Specify number of digits in the one-time password,
            required when using passwords in usersfile.  Supported
            values are 6, 7, and 8.

  "window": Specify search depth, an integer typically from 5 to 50
            but other values can be useful too.

Usersfile protection
--------------------

The PAM module performs its operations as root when re-writing the
"usersfile".  Even if the file is owned by a non-root user, the file
modifications are performed as the root user and file ownership is
reset back to the original file owner.

Thus "usersfile" MUST point to a protected file where non-root users
cannot modify it.

The exception is when a ${HOME} string is used.  In this case the PAM
module will temporarily drop the effective user id to the user that is
being logged in via PAM before accessing and rewriting the file.

The following are the typically scenarios for the usersfile placement,
and its recommended file protection.  Note that a root-owned file
below a path controlled by a non-root user lead to a vulnerable
configuration.

1) usersfile=/etc/users.oath

Root owned that is read/write-protected against users, for centralized
control.

2) usersfile=${HOME}/.config/oath/secrets

User-owned file that is read/write-protected against other users.

The disadvantage is that user can read their OATH secret, and
potentially damage their ability to login by messing up the file, but
in some situations that may be an advantage.

3) usersfile=/var/oath/oath.${USER}.secrets

Root-owned per-user files that are all read/write-protected against
non-root users.  Typically more relevant for larger multi-user system
where you want to split up the /etc/users.oath file on a per-user
basis, for increased performance and/or administrative control.

WARNING!  Don't use usersfile=/home/foo/bar/oath.secrets when
/home/foo has write-permission for non-root user `foo` even when the
`oath.secrets` file happen to be owned by root, as the user may use
symbolic links at /home/foo/bar to redirection which file is opened.

SSH Configuration
-----------------

Configuring pam_oath for use with SSH is straight forward, however you
should make sure the /etc/ssh/sshd_config file contains the following:

---------
UsePAM yes
ChallengeResponseAuthentication yes
---------

Another reported cause of problems has been SELinux, and you could
experiment with disabling SELinux by doing "/usr/sbin/setenforce 0".

Alternatively, instead of "ChallengeResponseAuthentication" it should
work to enable "PasswordAuthentication" instead.

Debug hint
----------

By using a /lib/security symlink, you may point the module to a file
in your build directory (for testing purposes):

---------
# ln -s /home/jas/src/oath-toolkit/pam_oath/.libs/pam_oath.so /lib/security/pam_oath.so
# ls -la /lib/security/pam_oath.so
lrwxrwxrwx 1 root root 53 Dec  4 10:49 /lib/security/pam_oath.so -> /home/jas/src/oath-toolkit/pam_oath/.libs/pam_oath.so
#
---------

This makes it easy to test newly built code.
