So you’ve brought something up that has certainly been ticking over in the back
of my mind for a long time. I’m roughly 90% certain that the user DB interface 
is
indeed the right extension point for hooking authentication systems into the 
system
(And would be a better one than having to write a PAM module per authentication 
system
- which often ends up just repeating the work of shuffling the username and 
password 
to the backend system over some custom protocol...)


> On 21 Jul 2025, at 12:39, Dominik George <n...@naturalnet.de> wrote:
> 
> Hi,
> 
> currently, the userdb system only allows querying for User Records and
> Group Records, hence providing a modern replacement for NSS.
> 

So note that currently there are only two ways that the User DB generically 
gets involved in user authentication:

  * pam_unix calls getpwnam and nss_systemd does the userdb lookups, or
  * you’re for some reason using my nss module[1] that
    I (initially) mostly created to solve problems I was having specific
    to NixOS (specifically: plugins can never work in the NSS shadow stack on 
NixOS)

pam_systemd_home interacts with homed specifically and directly.

(You probably know this, but its important to ensure everyone is on the same 
page)

Whatever such a protocol ends up looking like, I think “you could replace 
pam_systemd_home with a generic UserDB PAM module and said generic protocol” 
is an important criterion

> I would like to propose an addition to make it support authentication as
> well. The additions to the io.systemd.UserDatabase Varlink interface
> are:


I don’t think there’s any strict necessity for it to be in the 
io.systemd.UserDatabase
Interface. In many ways I think it would be better for any such methods in a 
separate 
interface (because it means when you get an 
`org.varlink.service.InterfaceNotFound` error
back for a given userdb service you know you can just skip all advanced 
functionality
For that service for the rest of the authentication sequence)


> ```varlink
> # Start the authentication process for a user
> #
> # Requires a username, and an optional authentication authToken
> # (e.g. a password).
> #
> # The method can return:
> #
> #  * a User Record object of the user that was authenticated
> #  * a conversation token for the AuthenticateContinue method
> #  * nothing at all (authentication finished and successful, but no details 
> provided)
> method Authenticate(
>        userName : string,
>        authToken : ?string,
>        variables : []string,
>        client : ?string,
>        service : string
> ) -> (
>        convToken : ?int,
>        user : ?object
> )
> 
> # Continue an ongogin conversation
> #
> # For example, the Authenticate method might have requested a conversation
> # with the user, like in the OAuth Device Authorization Grant Flow where the
> # user will need to open a web page on another device to log in.
> # In such a case, the Authenticate method will return a matching error with
> # a conversation token, and the client requesting authentication can later
> # continue the process using this conversation token.
> method AuthenticateContinue(
>        convToken : int,
>        authToken : ?string,
>        variables : []string,
>        client : ?string,
>        service : string
> ) -> (
>        user : ?object
> )
> 
> # Cancel an ongogin authentication process for which a conversation token
> # has been issued
> method AuthenticateCancel(
>        convToken : int,
>        client : ?string,
>        service : string
> )
> 
> # The authentication process was finished, but the authentication token was 
> invalid
> error InvalidAuthToken(message: ?string)
> 
> # An authentication token was not provided, but is required
> # If a conversation token is issued, the client can use AuthenticateContinue.
> # Otherwise, it has to restart the process.
> error AuthTokenRequired(convToken: ?int, message: ?string)
> 
> # A conversation with the user is required (e.g. ask a non-auth-token 
> question,
> # open a website, etc.).
> # Once a reply was acquired, the client must continue the process using
> # AuthenticateContinue.
> error ConvRequired(convToken: int, message: string)
> 
> # The authentication is ongoing, but cannot complete right now.
> # The service might be waiting for some backend to complete a task,
> # or for the user to acknowledge a second factor in some external app
> # or whatever. The client should ask for progress after the specified
> # interval, but does not need to provide any new information.
> error RetryRequired(convToken: int, interval: int, message: ?string)
> 
> # An ongoging conversation timed out while waiting for the client to
> # continue.
> error ConvTimeout(message: ?string)
> ```
> 
> The protocol is designed with PAM compatibility in mind, so it borrows
> some of its terminology. However, it is tailored to asynchronous
> authentication mechanisms, like various OAuth / OIDC flows.

So a minor thing here: If you’re doing conversation tokens, I would be
inclined to make them a string  and exchange it at each step, so stateless
applications can be supported.

But I’m not sure if (for the auth step in particular, which is very much
driven by the module) it would be better to use sd_varlink_push_fd to send
a file descriptor across for a “reversed” varlink socket that the other end
can use to drive the PAM conversation. A nice advantage here is that you also
get automatic resource release by just closing the file descriptor.

As described the interface proposed diverges a bunch from how 
pam_sm_authenticate
and the conversation function work. As much as I dislike PAM, I don’t think you
can get away form it; it's the lingua franca of Unix authentication after all. 

I think trying to fit OAuth token flows into “normal” PAM like this is probably 
more trouble than its worth. If you’re looking to do things beyond the basic 
text 
messages and prompts that PAM understands then I think we need to look towards 
GDM’s
extensions [2]. They’re definitely imperfect but they’re the closest thing we 
have to 
a “modern” login extension to PAM right now.

Anything that slots in at this extension point likely needs the ability to see 
any
PAM environment variables (perhaps you pass them across by calling 
pam_getenvlist first)
and likely set its own; and also the ability to (when feasible) return a token 
in
the PAM_AUTHTOK item that e.g. can be used to establish disk encryption or 
Kerberos 
credentials (where appropriate/separate from the UserDB backend)

I don’t see a need to return the user record here. To figure out which service 
you need
to call to authenticate you already need to be in possession of it.


> Concerning backwards compatibility, I propose that:
> 
> * The userdbd multiplexer should try to call the Authenticate methods
>   on downstreams, and watch out for any MethodNotImplemented errors.
>   If a backend does not implement the Authenticate methods, the
>   multiplexer can try to authenticate against a hashedPassword in
>   the User Record itself.

The multiplexer is completely uninvolved in login today (besides very indirectly
i.e. through the nss_systemd compatibility shim). I don’t think this logic 
belongs
in there. It belongs in whichever PAM module talks directly to the user DB.

I agree about fallback to traditional behaviour on MethodNotImplemented

        Actually I think for the pam_acct extension point there’s a good 
argument
        for a backend being able to return a value which means “I have no 
objections
        - do lockout condition validation on the record as normal"


> * Implementations that have been done before will continue to work,
>   because the proposal only contains additions, but no changes to
>   existing methods.
> 
> Instead of making a new interface for this, I propose to add it to the
> UserDatabase interface, so most code can jsut continue to work, and we
> can build on top of the existing multiplexer and socket infrastructure
> (however, this might also be possible while defining a second interface,
> but I also want to re-use the error types from UserDatabase).
> 

There’s really no reason why you can’t have multiple interfaces on a single 
socket
and indeed Varlink is designed to support this (and I expect most UserDB 
implementations
might supplement the normal interface with one of their own. My own certainly 
does)


[1] https://github.com/erincandescent/pam_sduserdb/
[2] https://gitlab.gnome.org/GNOME/gdm/-/tree/main/pam-extensions

Reply via email to