ubuntu / authd

Authentication daemon for external Brokers
GNU Lesser General Public License v3.0
108 stars 9 forks source link

Feature: How to integrate authd with my own greeter for Entra ID login #443

Closed chenyu-vmware closed 2 weeks ago

chenyu-vmware commented 2 months ago

Is there an existing request for this feature?

Describe the feature

Authd works perfectly to login with Entra ID credentials at ubuntu24.04, thanks for this good project.

Is it possible to integrate my own greeter (not using ubuntu24.04 default greeter) with authd to login to ubuntu24.04? If the answer is yes, could you please give me some guidance how can I implement this requirement, appreciated for any suggestions.

Hereunder are some of unclear things in mind, very thanks for any help.

  1. when one specific username is entered at greeter, how do I know it's one Entra ID user, then display the "Device Authentication" option at greeter?
  2. When selected "Device Authentication", does authd-oidc-brokers provide some interface for me to get the QR code and Login code for "https://microsoft.com/devicelogin"?
  3. After authenticate with Entra ID credentials at browser, authd-oidc-brokers can get the success/error notification from Azure, right? is there some callback way to let broker notify my greeter as well.

Thanks a lot for any help.

Describe the ideal solution

at ubuntu24.04, my own greeter can works fine with authd to authenticated with Entra ID credentials.

Alternatives and current workarounds

N/A

System information and logs

No response

Relevant information

No response

Double check your logs

3v1n0 commented 2 months ago

is it possible to integrate my own greeter (not using ubuntu24.04 default greeter) with authd to login to ubuntu24.04? If the answer is yes, could you please give me some guidance how can I implement this requirement, appreciated for any suggestions.

Can I ask what greeter are you referring to? I assume it's another display manager implementation, isn't it?

Technically any greeter that supports PAM conversations works with authd (in the same way SSH does), (by just using common-auth service) the problem of some greeters is that they don't handle all the PAM items/messages as they should.

So, to see the normal PAM behavior, you can just force using the module native interface by passing the force_native_client=true argument to the pam module and then any tool such as sudo or login should use it.


That said, if you want instead to implement a similar behavior of what GDM in ubuntu does, it's still possible through PAM by implementing our gdm protocol that uses PAM binary messages with JSON content.

You can see examples of what passes through the wire by running go test -C pam/integration-tests -run TestGdmModule -v, so for example in TestGdmModule/Authenticates_user_after_regenerating_the_qrcode:

The main concept is that we use binary conversations to implement the data polling on authd side, and receive back information from it.

    gdm-module-handler_test.go:241: -> {"type":"hello"}
    gdm-module-handler_test.go:244: <- {"type":"hello", "hello":{"version":1}}
    gdm-module-handler_test.go:241: -> {"type":"request", "request":{"type":"uiLayoutCapabilities", "uiLayoutCapabilities":{}}}
    gdm-module-handler_test.go:244: <- {"type":"response", "response":{"type":"uiLayoutCapabilities", "uiLayoutCapabilities":{"supportedUiLayouts":[{"type":"form", "label":"required", "button":"optional", "wait":"optional:true,false", "entry":"optional:chars,chars_password"}, {"type":"qrcode", "label":"optional", "button":"optional", "wait":"required:true,false", "content":"required"}]}}}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"userSelected", "userSelected":{"userId":"user-integration-Authenticates-user-after-regenerating-the-qrcode"}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"brokersReceived", "brokersReceived":{"brokersInfos":[{"id":"local", "name":"local", "brandIcon":""}, {"id":"3434009568", "name":"ExampleBroker", "brandIcon":"/usr/share/backgrounds/warty-final-ubuntu.png"}]}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"request", "request":{"type":"changeStage", "changeStage":{"stage":"brokerSelection"}}}
    gdm-module-handler_test.go:244: <- {"type":"response", "response":{"type":"changeStage", "ack":{}}}
    gdm-module-handler_test.go:241: -> {"type":"poll"}
    gdm-module-handler_test.go:244: <- {"type":"pollResponse", "pollResponse":[{"type":"brokerSelected", "brokerSelected":{"brokerId":"3434009568"}}]}
    gdm-module-handler_test.go:127: Using broker 'ExampleBroker'
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"brokerSelected", "brokerSelected":{"brokerId":"3434009568"}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"request", "request":{"type":"changeStage", "changeStage":{"stage":"authModeSelection"}}}
    gdm-module-handler_test.go:244: <- {"type":"response", "response":{"type":"changeStage", "ack":{}}}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"authModeSelected", "authModeSelected":{"authModeId":"password"}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"authModesReceived", "authModesReceived":{"authModes":[{"id":"password", "label":"Password authentication"}, {"id":"entry_or_wait_for_user-integration-Authenticates-user-after-regenerating-the-qrcode_gmail.com", "label":"Send URL to user-integration-Authenticates-user-after-regenerating-the-qrcode@gmail.com"}, {"id":"fidodevice1", "label":"Use your fido device foo"}, {"id":"phoneack1", "label":"Use your phone +33…"}, {"id":"phoneack2", "label":"Use your phone +1…"}, {"id":"qrcodewithtypo", "label":"Use a QR code"}, {"id":"totp_with_button", "label":"Authentication code"}]}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:140: Gimme your password:
    gdm-module-handler_test.go:143: :
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"uiLayoutReceived", "uiLayoutReceived":{"uiLayout":{"type":"form", "label":"Gimme your password", "button":"", "wait":"", "entry":"chars_password", "content":"", "code":""}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"startAuthentication", "startAuthentication":{}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"request", "request":{"type":"changeStage", "changeStage":{"stage":"challenge"}}}
    gdm-module-handler_test.go:244: <- {"type":"response", "response":{"type":"changeStage", "ack":{}}}
    gdm-module-handler_test.go:241: -> {"type":"poll"}
    gdm-module-handler_test.go:244: <- {"type":"pollResponse", "pollResponse":[{"type":"stageChanged", "stageChanged":{"stage":"authModeSelection"}}, {"type":"authModeSelected", "authModeSelected":{"authModeId":"qrcodewithtypo"}}]}
    gdm-module-handler_test.go:173: Authentication event: access:"cancelled"
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"authEvent", "authEvent":{"response":{"access":"cancelled"}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"request", "request":{"type":"changeStage", "changeStage":{"stage":"authModeSelection"}}}
    gdm-module-handler_test.go:244: <- {"type":"response", "response":{"type":"changeStage", "ack":{}}}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"authModeSelected", "authModeSelected":{"authModeId":"qrcodewithtypo"}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:140: Enter the following code after flashing the address: 1337:
    gdm-module-handler_test.go:143: https://ubuntu.com:
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"uiLayoutReceived", "uiLayoutReceived":{"uiLayout":{"type":"qrcode", "label":"Enter the following code after flashing the address: 1337", "button":"Regenerate code", "wait":"true", "entry":"", "content":"https://ubuntu.com", "code":""}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"startAuthentication", "startAuthentication":{}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"request", "request":{"type":"changeStage", "changeStage":{"stage":"challenge"}}}
    gdm-module-handler_test.go:244: <- {"type":"response", "response":{"type":"changeStage", "ack":{}}}
    gdm-module-handler_test.go:241: -> {"type":"poll"}
    gdm-module-handler_test.go:244: <- {"type":"pollResponse", "pollResponse":[{"type":"isAuthenticatedRequested", "isAuthenticatedRequested":{"authenticationData":{"wait":"true"}}}, {"type":"reselectAuthMode", "reselectAuthMode":{}}]}
    gdm-module-handler_test.go:173: Authentication event: access:"cancelled"
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"authEvent", "authEvent":{"response":{"access":"cancelled"}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:140: Enter the following code after flashing the address: 1338:
    gdm-module-handler_test.go:143: https://ubuntu.fr/:
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"uiLayoutReceived", "uiLayoutReceived":{"uiLayout":{"type":"qrcode", "label":"Enter the following code after flashing the address: 1338", "button":"Regenerate code", "wait":"true", "entry":"", "content":"https://ubuntu.fr/", "code":""}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"startAuthentication", "startAuthentication":{}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"poll"}
    gdm-module-handler_test.go:244: <- {"type":"pollResponse", "pollResponse":[{"type":"reselectAuthMode", "reselectAuthMode":{}}]}
    gdm-module-handler_test.go:140: Enter the following code after flashing the address: 1339:
    gdm-module-handler_test.go:143: https://ubuntuforum-br.org/:
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"uiLayoutReceived", "uiLayoutReceived":{"uiLayout":{"type":"qrcode", "label":"Enter the following code after flashing the address: 1339", "button":"Regenerate code", "wait":"true", "entry":"", "content":"https://ubuntuforum-br.org/", "code":""}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"startAuthentication", "startAuthentication":{}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"poll"}
    gdm-module-handler_test.go:244: <- {"type":"pollResponse", "pollResponse":[{"type":"isAuthenticatedRequested", "isAuthenticatedRequested":{"authenticationData":{"wait":"true"}}}, {"type":"reselectAuthMode", "reselectAuthMode":{}}]}
    gdm-module-handler_test.go:173: Authentication event: access:"cancelled"
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"authEvent", "authEvent":{"response":{"access":"cancelled"}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:140: Enter the following code after flashing the address: 1340:
    gdm-module-handler_test.go:143: https://www.ubuntu-it.org/:
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"uiLayoutReceived", "uiLayoutReceived":{"uiLayout":{"type":"qrcode", "label":"Enter the following code after flashing the address: 1340", "button":"Regenerate code", "wait":"true", "entry":"", "content":"https://www.ubuntu-it.org/", "code":""}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"startAuthentication", "startAuthentication":{}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"poll"}
    gdm-module-handler_test.go:244: <- {"type":"pollResponse", "pollResponse":[{"type":"reselectAuthMode", "reselectAuthMode":{}}]}
    gdm-module-handler_test.go:140: Enter the following code after flashing the address: 1341:
    gdm-module-handler_test.go:143: https://ubuntu.com:
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"uiLayoutReceived", "uiLayoutReceived":{"uiLayout":{"type":"qrcode", "label":"Enter the following code after flashing the address: 1341", "button":"Regenerate code", "wait":"true", "entry":"", "content":"https://ubuntu.com", "code":""}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"startAuthentication", "startAuthentication":{}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
    gdm-module-handler_test.go:241: -> {"type":"poll"}
    gdm-module-handler_test.go:244: <- {"type":"pollResponse", "pollResponse":[{"type":"isAuthenticatedRequested", "isAuthenticatedRequested":{"authenticationData":{"wait":"true"}}}]}
    gdm-module-handler_test.go:173: Authentication event: access:"granted"
    gdm-module-handler_test.go:241: -> {"type":"event", "event":{"type":"authEvent", "authEvent":{"response":{"access":"granted"}}}}
    gdm-module-handler_test.go:244: <- {"type":"eventAck"}
chenyu-vmware commented 2 months ago

Appreciated for your helpful message, @3v1n0, you bring light to me.

The greeter is simple implemented by ourselves, and its backend does use pam service to do the authentication. Then according to your message, suppose authd can be used in our scenario for Entra ID user login workflow, right?

common-account common-auth common-password gdm-authd Looks authd update its so or binary to above pam services, "gdm-authd" @include common-account, commn-auth, and common-password services, is it correct that ubuntu default greeter loads gdm-authd service for login workflow?

Then turn to my case, I create my own pam service similar with "gdm-authd", like "my-authd", then my greeter load it to trigger authentication conversations, is it right? And pam_authd_exec.so will echo the QR image and login device code (polling from authd) to my greeter for display, not sure whether my understanding is correct.

Could you please help to guide me about these queries, then I can get basic idea to start my POC work.

As I'm a newer to authd and go language, I have trouble to fully understand your below description, I will practice and learn more try to understand them and apply to my requirement. Really thanks for your help.


So, to see the normal PAM behavior, you can just force using the module native interface by passing the force_native_client=true argument to the pam module and then any tool such as sudo or login should use it.

That said, if you want instead to implement a similar behavior of what GDM in ubuntu does, it's still possible through PAM by implementing our [gdm protocol](https://github.com/ubuntu/authd/blob/main/pam/internal/adapter/gdmmodel.go) that uses [PAM binary messages](https://github.com/ubuntu/authd/blob/main/pam/internal/gdm/extension.go) with [JSON content](https://github.com/ubuntu/authd/blob/main/pam/internal/gdm/extensions/gdm-custom-json-pam-extension.h).

You can see examples of what passes through the wire by running go test -C pam/integration-tests -run TestGdmModule -v, so for example in TestGdmModule/Authenticates_user_after_regenerating_the_qrcode:

The main concept is that we use binary conversations to implement the data polling on authd side, and receive back information from it.
3v1n0 commented 4 weeks ago

Oh, it looks like I didn't see your message... I'm sorry @chenyu-vmware!

Not sure if you had some progresses yourself so far, but as mentioned if you want to implement this in a UI greeter, I strongly recommend you to follow a similar behavior of what we do for GDM, since if you need to show the qrcode as an image you may want to be able to get it from a communication protocol.

So, indeed what you need to do is:

A very simpler example of how events are managed (in Go, since it was done for this repo):


package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "slices"

    "github.com/msteinert/pam/v2"
    "github.com/ubuntu/authd"
    "github.com/ubuntu/authd/internal/brokers"
    "github.com/ubuntu/authd/internal/log"
    "github.com/ubuntu/authd/pam/internal/gdm"
    "github.com/ubuntu/authd/pam/internal/pam_test"
    "github.com/ubuntu/authd/pam/internal/proto"
)

// var (
//  socketPath = flag.String("socket-path", "", "the socket path")
// )

var currentStage proto.Stage
var pollResponses = []*gdm.EventData{}
var authModes []*authd.GAMResponse_AuthenticationMode
var brokersInfos []*authd.ABResponse_BrokerInfo

func exampleHandleGdmData(gdmData *gdm.Data) (*gdm.Data, error) {
    switch gdmData.Type {
    case gdm.DataType_hello:
        return &gdm.Data{
            Type:  gdm.DataType_hello,
            Hello: &gdm.HelloData{Version: gdm.ProtoVersion},
        }, nil

    case gdm.DataType_request:
        return exampleHandleAuthDRequest(gdmData)

    case gdm.DataType_poll:
        responses := pollResponses
        pollResponses = nil
        return &gdm.Data{
            Type:         gdm.DataType_pollResponse,
            PollResponse: responses,
        }, nil

    case gdm.DataType_event:
        err := exampleHandleEvent(gdmData.Event.Data)
        if err != nil {
            return nil, err
        }
        return &gdm.Data{
            Type: gdm.DataType_eventAck,
        }, nil
    }

    return nil, fmt.Errorf("unhandled protocol message %s",
        gdmData.Type.String())
}

func exampleHandleEvent(event gdm.Event) error {
    switch ev := event.(type) {
    case *gdm.EventData_BrokersReceived:
        if len(ev.BrokersReceived.BrokersInfos) == 0 {
            return errors.New("no brokers available")
        }
        brokersInfos = ev.BrokersReceived.BrokersInfos
        pollResponses = append(pollResponses, &gdm.EventData{
            Type: gdm.EventType_brokerSelected,
            Data: &gdm.EventData_BrokerSelected{
                BrokerSelected: &gdm.Events_BrokerSelected{
                    BrokerId: brokersInfos[1].Id,
                },
            },
        })

    case *gdm.EventData_BrokerSelected:
        idx := slices.IndexFunc(brokersInfos, func(broker *authd.ABResponse_BrokerInfo) bool {
            return broker.Id == ev.BrokerSelected.BrokerId
        })
        if idx < 0 {
            return fmt.Errorf("unknown auth mode type: %s", ev.BrokerSelected.BrokerId)
        }
        log.Infof(context.TODO(), "Using broker %q", brokersInfos[idx].Name)

    case *gdm.EventData_AuthModesReceived:
        authModes = ev.AuthModesReceived.AuthModes

    case *gdm.EventData_AuthModeSelected:
        idx := slices.IndexFunc(authModes, func(mode *authd.GAMResponse_AuthenticationMode) bool {
            return mode.Id == ev.AuthModeSelected.AuthModeId
        })
        if idx < 0 {
            return fmt.Errorf("unknown auth mode type: %s", ev.AuthModeSelected.AuthModeId)
        }

    case *gdm.EventData_UiLayoutReceived:
        layout := ev.UiLayoutReceived.UiLayout
        if layout.Label != nil {
            log.Infof(context.TODO(), "%s:", *layout.Label)
        }

    case *gdm.EventData_AuthEvent:
        if msg := ev.AuthEvent.Response.Msg; msg != "" {
            var msgData map[string]any
            if err := json.Unmarshal([]byte(msg), &msgData); err != nil {
                return err
            }
            if msg, ok := msgData["message"]; ok {
                log.Infof(context.TODO(), "Got message: %s", msg)
            }
        }
        if ev.AuthEvent.Response.Access == brokers.AuthGranted {
            return nil
        }
        if ev.AuthEvent.Response.Access == brokers.AuthDenied {
            return nil
        }
        if ev.AuthEvent.Response.Access == brokers.AuthRetry {
            pollResponses = append(pollResponses, &gdm.EventData{
                Type: gdm.EventType_isAuthenticatedRequested,
                Data: &gdm.EventData_IsAuthenticatedRequested{
                    IsAuthenticatedRequested: &gdm.Events_IsAuthenticatedRequested{
                        AuthenticationData: &authd.IARequest_AuthenticationData{
                            Item: &authd.IARequest_AuthenticationData_Challenge{
                                Challenge: "goodpass",
                            },
                        },
                    },
                },
            })
            return nil
        }

    case *gdm.EventData_StartAuthentication:
        pollResponses = append(pollResponses, &gdm.EventData{
            Type: gdm.EventType_isAuthenticatedRequested,
            Data: &gdm.EventData_IsAuthenticatedRequested{
                IsAuthenticatedRequested: &gdm.Events_IsAuthenticatedRequested{
                    AuthenticationData: &authd.IARequest_AuthenticationData{
                        Item: &authd.IARequest_AuthenticationData_Challenge{
                            Challenge: "wrong-pass",
                        },
                    },
                },
            },
        })
    }
    return nil
}

func exampleHandleAuthDRequest(gdmData *gdm.Data) (*gdm.Data, error) {
    switch req := gdmData.Request.Data.(type) {
    case *gdm.RequestData_UiLayoutCapabilities:
        required, _ := "required", "optional"
        supportedEntries := "optional:chars,chars_password"
        // requiredWithBooleans := "required:true,false"
        optionalWithBooleans := "optional:true,false"

        return &gdm.Data{
            Type: gdm.DataType_response,
            Response: &gdm.ResponseData{
                Type: gdmData.Request.Type,
                Data: &gdm.ResponseData_UiLayoutCapabilities{
                    UiLayoutCapabilities: &gdm.Responses_UiLayoutCapabilities{
                        SupportedUiLayouts: []*authd.UILayout{
                            {
                                Type:  "form",
                                Label: &required,
                                Entry: &supportedEntries,
                                Wait:  &optionalWithBooleans,
                                // Button: &optional,
                            },
                            {
                                Type:  "newpassword",
                                Label: &required,
                                Entry: &supportedEntries,
                                // Button: &optional,
                            },
                        },
                    },
                },
            },
        }, nil

    case *gdm.RequestData_ChangeStage:
        if gdmData.Request.Data == nil {
            return nil, fmt.Errorf("missing stage data")
        }
        currentStage = req.ChangeStage.Stage
        log.Debugf(context.TODO(), "Switching to stage %d", currentStage)

        return &gdm.Data{
            Type: gdm.DataType_response,
            Response: &gdm.ResponseData{
                Type: gdmData.Request.Type,
                Data: &gdm.ResponseData_Ack{},
            },
        }, nil

    default:
        return nil, fmt.Errorf("unknown request type")
    }
}

// LoadModule compiles and loads a pam module.
func LoadModule(socketPath string, userName string) (*pam.Transaction, error) {
    servicePath, err := os.MkdirTemp(os.TempDir(), "test-pam-loader-*")
    if err != nil {
        return nil, fmt.Errorf("can't create service path %v", err)
    }
    defer os.RemoveAll(servicePath)

    libPath := filepath.Join(servicePath, "libpam_authd.so")
    log.Debugf(context.TODO(), "Compiling module at %s", libPath)
    _, currentFile, _, ok := runtime.Caller(1)
    if !ok {
        return nil, errors.New("can't get current binary path")
    }
    buildArgs := []string{
        "build",
        "-C", filepath.Join(filepath.Dir(currentFile), "..", "..", "..", "pam"),
        "-buildmode=c-shared",
        "-o", libPath, `-gcflags=-dwarflocationlists=true`,
    }
    if pam_test.IsAddressSanitizerActive() {
        buildArgs = append(buildArgs, "-asan")
    }

    // #nosec:G204 - we control the command arguments in tests
    cmd := exec.Command("go", buildArgs...)
    cmd.Dir = filepath.Dir(currentFile)
    out, err := cmd.CombinedOutput()
    if err != nil {
        return nil, fmt.Errorf("can't build pam module %v: %s", err, out)
    }

    serviceName := "module-loader"
    serviceFile := filepath.Join(servicePath, serviceName)
    log.Debugf(context.TODO(), "Creating service file at %s", serviceFile)

    if err := os.WriteFile(serviceFile,
        []byte(fmt.Sprintf("auth requisite %s socket=%s", libPath, socketPath)),
        0600); err != nil {
        return nil, fmt.Errorf("can't create service file %v", err)
    }

    tx, err := pam.StartConfDir(serviceName, userName, gdm.DataConversationFunc(
        func(inData *gdm.Data) (*gdm.Data, error) {
            outData, err := exampleHandleGdmData(inData)
            if err != nil {
                return nil, err
            }
            if inData.Type == gdm.DataType_poll && len(outData.PollResponse) == 0 {
                return outData, err
            }
            json, err := inData.JSON()
            if err != nil {
                return nil, err
            }
            log.Debug(context.TODO(), "->", string(json))
            json, err = outData.JSON()
            if err != nil {
                return nil, err
            }
            log.Debug(context.TODO(), "<-", string(json))
            return outData, nil
        }), servicePath)
    if err != nil {
        return nil, fmt.Errorf("can't create PAM handler: %v", err)
    }

    log.Debug(context.TODO(), "PAM Handler created")

    return tx, nil
}

func startAuthentication() error {
    tx, err := LoadModule("/tmp/authd.sock", "")
    if err != nil {
        return err
    }

    err = tx.SetItem(pam.User, "user1")
    if err != nil {
        return err
    }

    return tx.Authenticate(pam.Flags(0))
}

func main() {
    // TODO: Add option to simulate different loading types
    log.SetLevel(log.DebugLevel)

    gdm.AdvertisePamExtensions([]string{gdm.PamExtensionCustomJSON})

    if err := startAuthentication(); err != nil {
        log.Error(context.TODO(), err)
        var pamError pam.Error
        if errors.Is(err, &pamError) {
            os.Exit(int(pamError))
        }
        os.Exit(1)
    }

    os.Exit(0)
}
yliu5-vmware commented 2 weeks ago

Thanks for your support @3v1n0 , everything went smoothly and we used the same method you said. :-)

3v1n0 commented 2 weeks ago

Cool, happy it worked well!