Skip to content

Commit

Permalink
pam: Add support for exposing if the UI supports qrcode rendering
Browse files Browse the repository at this point in the history
As we know not all the PAM client supports rendering the qrcode properly
so the broker must be aware of this in order to provide the proper user
guidance.

To make this possible, add a further value to the UI layout that exposes
if a client is able to render the qr code or not, and depending on this
the broker can behave.

Since this is a new feature, we don't want to make the old brokers
depending on it, or we'll break their assumptions, so in case a client
doesn't support this feature, we just assume that rendering is supported
as it was before.
  • Loading branch information
3v1n0 committed Nov 6, 2024
1 parent c3f6fbf commit 656f117
Show file tree
Hide file tree
Showing 13 changed files with 508 additions and 412 deletions.
372 changes: 192 additions & 180 deletions authd.pb.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions authd.proto
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ message UILayout {
// qr code only.
optional string content = 6;
optional string code = 7;
optional bool renders_qrcode = 8;
}

message GAMResponse {
Expand Down
15 changes: 11 additions & 4 deletions examplebroker/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func (b *Broker) GetAuthenticationModes(ctx context.Context, sessionID string, s
return nil, err
}

//var candidatesAuthenticationModes []map[string]string
log.Debugf(ctx, "Supported UI layouts by %s, %#v", sessionID, supportedUILayouts)
allModes := getSupportedModes(sessionInfo, supportedUILayouts)

// If the user needs mfa, we remove the last used mode from the list of available modes.
Expand Down Expand Up @@ -271,6 +271,7 @@ func (b *Broker) GetAuthenticationModes(ctx context.Context, sessionID string, s
"label": authMode["selection_label"],
})
}
log.Debugf(ctx, "Supported authentication modes for %s: %#v", sessionID, allModes)
sessionInfo.allModes = allModes

if err := b.updateSession(sessionID, sessionInfo); err != nil {
Expand Down Expand Up @@ -380,13 +381,19 @@ func getSupportedModes(sessionInfo sessionInfo, supportedUILayouts []map[string]

case "qrcode":
modeName := "qrcodewithtypo"
modeSelectionLabel := "Use a QR code"
modeLabel := "Enter the following code after flashing the address: "
if layout["code"] != "" {
modeName = "qrcodeandcodewithtypo"
modeLabel = "Scan the qrcode or enter the code in the login page"
}
if layout["renders_qrcode"] != "true" {
modeName = "codewithtypo"
modeSelectionLabel = "Use a Login code"
modeLabel = "Enter the code in the login page"
}
allModes[modeName] = map[string]string{
"selection_label": "Use a QR code",
"selection_label": modeSelectionLabel,
"ui": mapToJSON(map[string]string{
"type": "qrcode",
"label": modeLabel,
Expand Down Expand Up @@ -489,7 +496,7 @@ func (b *Broker) SelectAuthenticationMode(ctx context.Context, sessionID, authen
// send request to sessionInfo.allModes[authenticationModeName]["phone"]
case "fidodevice1":
// start transaction with fido device
case "qrcodeandcodewithtypo":
case "qrcodeandcodewithtypo", "codewithtypo":
uiLayoutInfo["content"], uiLayoutInfo["code"] = qrcodeData(&sessionInfo)
case "qrcodewithtypo":
// generate the url and finish the prompt on the fly.
Expand Down Expand Up @@ -648,7 +655,7 @@ func (b *Broker) handleIsAuthenticated(ctx context.Context, sessionInfo sessionI
return AuthCancelled, ""
}

case "qrcodewithtypo", "qrcodeandcodewithtypo":
case "qrcodewithtypo", "qrcodeandcodewithtypo", "codewithtypo":
if authData["wait"] != "true" {
return AuthDenied, fmt.Sprintf(`{"message": "%s should have wait set to true"}`, sessionInfo.currentAuthMode)
}
Expand Down
12 changes: 12 additions & 0 deletions internal/services/pam/pam.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,15 @@ func uiLayoutToMap(layout *authd.UILayout) (mapLayout map[string]string, err err
if c := layout.GetCode(); c != "" {
r["code"] = c
}

if layout.GetType() != "qrcode" {
return r, nil
}
if rc := layout.RendersQrcode; rc == nil || *rc {
// If the field is not set, we keep retro-compatibility with what we were
// dong before of the addition of the field.
r["renders_qrcode"] = "true"
}
return r, nil
}

Expand All @@ -337,6 +346,9 @@ func mapToUILayout(layout map[string]string) (r *authd.UILayout) {
content := layout["content"]
code := layout["code"]

// We don't return whether the qrcode rendering is enabled back to the
// client on purpose, since it's something it mandates.

return &authd.UILayout{
Type: typ,
Label: &label,
Expand Down
17 changes: 10 additions & 7 deletions internal/services/pam/pam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,17 @@ var (
optionalEntries = "optional:entry_type,other_entry_type"
optional = "optional"

rendersQrCode = true

requiredEntry = &authd.UILayout{
Type: "required-entry",
Label: &optional,
Button: &optional,
Wait: &optional,
Entry: &requiredEntries,
Content: &optional,
Code: &optional,
Type: "required-entry",
Label: &optional,
Button: &optional,
Wait: &optional,
Entry: &requiredEntries,
Content: &optional,
Code: &optional,
RendersQrcode: &rendersQrCode,
}
optionalEntry = &authd.UILayout{
Type: "optional-entry",
Expand Down
52 changes: 46 additions & 6 deletions pam/integration-tests/gdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ const (
exampleBrokerName = "ExampleBroker"
ignoredBrokerName = "<ignored-broker>"

passwordAuthID = "password"
newPasswordAuthID = "mandatoryreset"
fido1AuthID = "fidodevice1"
phoneAck1ID = "phoneack1"
qrcodeID = "qrcodeandcodewithtypo"
qrcodeWithoutCodeID = "qrcodewithtypo"
passwordAuthID = "password"
newPasswordAuthID = "mandatoryreset"
fido1AuthID = "fidodevice1"
phoneAck1ID = "phoneack1"
qrcodeID = "qrcodeandcodewithtypo"
qrcodeWithoutCodeID = "qrcodewithtypo"
qrcodeWithoutRenderingID = "codewithtypo"
)

var testPasswordUILayout = authd.UILayout{
Expand Down Expand Up @@ -80,6 +81,16 @@ var testQrcodeUIWithoutCodeLayout = authd.UILayout{
Entry: ptrValue(""),
}

var testQrcodeUIWithoutRendering = authd.UILayout{
Type: "qrcode",
Label: ptrValue("Enter the code in the login page"),
Content: ptrValue("https://ubuntu.com"),
Wait: ptrValue("true"),
Button: ptrValue("Regenerate code"),
Code: ptrValue("1337"),
Entry: ptrValue(""),
}

var testFidoDeviceUILayout = authd.UILayout{
Type: "form",
Label: ptrValue("Plug your fido device and press with your thumb"),
Expand Down Expand Up @@ -429,6 +440,35 @@ func TestGdmModule(t *testing.T) {
},
wantUILayouts: []*authd.UILayout{&testQrcodeUIWithoutCodeLayout},
},
"Authenticates user with qrcode without rendering support": {
wantAuthModeIDs: []string{qrcodeWithoutRenderingID},
supportedLayouts: []*authd.UILayout{
pam_test.QrCodeUILayout(pam_test.WithQrCodeRenders(ptrValue(false))),
},
eventPollResponses: map[gdm.EventType][]*gdm.EventData{
gdm.EventType_startAuthentication: {
gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Wait{
Wait: "true",
}),
},
},
wantUILayouts: []*authd.UILayout{&testQrcodeUIWithoutRendering},
},
"Authenticates user with qrcode without explicit rendering support": {
// This checks that we're backward compatible
wantAuthModeIDs: []string{qrcodeID},
supportedLayouts: []*authd.UILayout{
pam_test.QrCodeUILayout(pam_test.WithQrCodeRenders(nil)),
},
eventPollResponses: map[gdm.EventType][]*gdm.EventData{
gdm.EventType_startAuthentication: {
gdm_test.IsAuthenticatedEvent(&authd.IARequest_AuthenticationData_Wait{
Wait: "true",
}),
},
},
wantUILayouts: []*authd.UILayout{&testQrcodeUILayout},
},
"Authenticates user after switching to qrcode": {
wantAuthModeIDs: []string{passwordAuthID, qrcodeID},
supportedLayouts: []*authd.UILayout{
Expand Down
32 changes: 20 additions & 12 deletions pam/integration-tests/native_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ func TestNativeAuthenticate(t *testing.T) {
const socketPathEnv = "AUTHD_TESTS_CLI_AUTHENTICATE_TESTS_SOCK"

tests := map[string]struct {
tape string
tapeSettings []tapeSetting
tape string
tapeSettings []tapeSetting
tapeVariables map[string]string

clientOptions clientOptions
currentUserNotRoot bool
Expand All @@ -43,43 +44,49 @@ func TestNativeAuthenticate(t *testing.T) {
"Authenticate user with qr code": {
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 2300}},
tapeVariables: map[string]string{"AUTHD_QRCODE_TAPE_ITEM": "7"},
clientOptions: clientOptions{PamUser: "user-integration-qr-code"},
},
"Authenticate user with qr code in a TTY": {
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3700}},
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3700}},
tapeVariables: map[string]string{"AUTHD_QRCODE_TAPE_ITEM": "7"},
clientOptions: clientOptions{
PamUser: "user-integration-qr-code-tty",
Term: "linux",
},
},
"Authenticate user with qr code in a TTY session": {
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3700}},
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3700}},
tapeVariables: map[string]string{"AUTHD_QRCODE_TAPE_ITEM": "7"},
clientOptions: clientOptions{
PamUser: "user-integration-qr-code-tty-session",
Term: "xterm-256color", SessionType: "tty",
},
},
"Authenticate user with qr code in screen": {
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3700}},
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3700}},
tapeVariables: map[string]string{"AUTHD_QRCODE_TAPE_ITEM": "7"},
clientOptions: clientOptions{
PamUser: "user-integration-qr-code-screen",
Term: "screen",
},
},
"Authenticate user with qr code in polkit": {
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3500}},
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3500}},
tapeVariables: map[string]string{"AUTHD_QRCODE_TAPE_ITEM": "2"},
clientOptions: clientOptions{
PamUser: "user-integration-qr-code-polkit",
PamServiceName: "polkit-1",
},
},
"Authenticate user with qr code in ssh": {
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3500}},
tape: "qr_code",
tapeSettings: []tapeSetting{{vhsHeight, 3500}},
tapeVariables: map[string]string{"AUTHD_QRCODE_TAPE_ITEM": "2"},
clientOptions: clientOptions{
PamUser: "user-integration-pre-check-ssh-service-qr-code",
PamServiceName: "sshd",
Expand Down Expand Up @@ -218,6 +225,7 @@ func TestNativeAuthenticate(t *testing.T) {
td := newTapeData(tc.tape, tc.tapeSettings...)
td.Env[socketPathEnv] = socketPath
td.Env[pam_test.RunnerEnvSupportsConversation] = "1"
td.Variables = tc.tapeVariables
td.AddClientOptions(t, tc.clientOptions)
td.RunVhs(t, "native", outDir, cliEnv)
got := td.ExpectedOutput(t, outDir)
Expand Down
Loading

0 comments on commit 656f117

Please sign in to comment.