Closed chenyu-vmware closed 2 weeks 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"}
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.
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:
gdm-authd
that loads the authd moduleA 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)
}
Thanks for your support @3v1n0 , everything went smoothly and we used the same method you said. :-)
Cool, happy it worked well!
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.
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