Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Broker follows specification #40

Merged
merged 11 commits into from
Sep 19, 2023
252 changes: 126 additions & 126 deletions authd.pb.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion authd.proto
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ message IARequest {

message IAResponse {
string access = 1;
string data = 2;
string msg = 2;
}

message SDBFURequest {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/ubuntu/authd

go 1.21.0
go 1.21.1

require (
github.com/charmbracelet/bubbles v0.16.1
Expand Down
46 changes: 43 additions & 3 deletions internal/brokers/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/godbus/dbus/v5"
"github.com/ubuntu/authd/internal/brokers/responses"
"github.com/ubuntu/authd/internal/cache"
"github.com/ubuntu/authd/internal/log"
"github.com/ubuntu/decorate"
"golang.org/x/exp/slices"
Expand Down Expand Up @@ -153,12 +154,51 @@ func (b Broker) IsAuthenticated(ctx context.Context, sessionID, authenticationDa
return "", "", fmt.Errorf("invalid access authentication key: %v", access)
}

// Validate json
if data == "" {
data = "{}"
}
if !json.Valid([]byte(data)) {
return "", "", fmt.Errorf("invalid user information (not json formatted): %v", data)

// TODO: validate response from broker
switch access {
case responses.AuthGranted:
var returnedData map[string]json.RawMessage
err = json.Unmarshal([]byte(data), &returnedData)
if err != nil {
return "", "", fmt.Errorf("response returned by the broker is not a valid json: %v\nBroker returned: %v", err, data)
}

rawUserInfo, ok := returnedData["userinfo"]
if !ok {
return "", "", fmt.Errorf("missing userinfo key in granted user access, got: %v", data)
}

var uInfo struct {
cache.UserInfo
UUID string
UGID string
Groups []struct {
Name string
UGID string
}
}
err := json.Unmarshal(rawUserInfo, &uInfo)
if err != nil {
return "", "", fmt.Errorf("invalid user information (not json formatted): %v", err)
}
// TODO: transform UUID and UGID into UID and GID and validates that any required fields are here.
uInfo.UID = 65536 + len(b.ID+uInfo.UUID) // should not be above 100000
for _, g := range uInfo.Groups {
uInfo.UserInfo.Groups = append(uInfo.UserInfo.Groups, cache.GroupInfo{
Name: g.Name,
GID: 65536 + len(b.ID+g.UGID),
})
}

d, err := json.Marshal(uInfo.UserInfo)
if err != nil {
return "", "", fmt.Errorf("can't marshal UserInfo: %v", err)
}
data = string(d)
}

return access, data, nil
Expand Down
7 changes: 3 additions & 4 deletions internal/brokers/broker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,12 @@ func TestIsAuthenticated(t *testing.T) {
"Successfully authenticate after cancelling first call": {sessionID: "IA_second_call", secondCall: true},
"Denies authentication when broker times out": {sessionID: "IA_timeout"},

"Empty data gets JSON formatted": {sessionID: "IA_empty_data"},

// broker errors
"Error when authenticating": {sessionID: "IA_error"},
"Error when broker returns invalid access": {sessionID: "IA_invalid"},
"Error when broker returns invalid data": {sessionID: "IA_invalid_data"},
"Error when calling IsAuthenticated a second time without cancelling": {sessionID: "IA_second_call", secondCall: true, cancelFirstCall: true},
"Error on empty data even if granted": {sessionID: "IA_empty_data"},
}
for name, tc := range tests {
tc := tc
Expand All @@ -239,7 +238,7 @@ func TestIsAuthenticated(t *testing.T) {
go func() {
defer close(done)
access, gotData, err := b.IsAuthenticated(ctx, tc.sessionID, "password")
firstCallReturn = fmt.Sprintf("FIRST CALL:\n\taccess: %s\n\tdata: %s\n\terr: %v\n", access, gotData, err)
firstCallReturn = fmt.Sprintf("FIRST CALL:\n\taccess: %s\n\tdata: %+v\n\terr: %v\n", access, gotData, err)
}()

// Give some time for the first call to block
Expand All @@ -251,7 +250,7 @@ func TestIsAuthenticated(t *testing.T) {
<-done
}
access, gotData, err := b.IsAuthenticated(context.Background(), tc.sessionID, "password")
secondCallReturn = fmt.Sprintf("SECOND CALL:\n\taccess: %s\n\tdata: %s\n\terr: %v\n", access, gotData, err)
secondCallReturn = fmt.Sprintf("SECOND CALL:\n\taccess: %s\n\tdata: %+v\n\terr: %v\n", access, gotData, err)
}

<-done
Expand Down
49 changes: 37 additions & 12 deletions internal/brokers/examplebroker/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
package examplebroker

import (
"bytes"
"context"
"crypto/aes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"html/template"
"math/rand"
"sort"
"strings"
Expand Down Expand Up @@ -517,23 +519,24 @@ func (b *Broker) handleIsAuthenticated(ctx context.Context, sessionInfo sessionI
switch sessionInfo.selectedMode {
case "password":
if authData["challenge"] != "goodpass" {
return responses.AuthRetry, "", nil
return responses.AuthRetry, `{"message": "invalid password, should be goodpass"}`, nil
}

case "pincode":
if authData["challenge"] != "4242" {
return responses.AuthRetry, "", nil
return responses.AuthRetry, `{"message": "invalid pincode, should be 4242"}`, nil
}

case "totp_with_button", "totp":
wantedCode := sessionInfo.allModes[sessionInfo.selectedMode]["wantedCode"]
if authData["challenge"] != wantedCode {
return responses.AuthRetry, "", nil
return responses.AuthRetry, `{"message": "invalid totp code"}`, nil
}

case "phoneack1":
// TODO: should this be an error rather (not expected data from the PAM module?
if authData["wait"] != "true" {
return responses.AuthDenied, "", nil
return responses.AuthDenied, `{"message": "phoneack1 should have wait set to true"}`, nil
}
// Send notification to phone1 and wait on server signal to return if OK or not
select {
Expand All @@ -544,20 +547,20 @@ func (b *Broker) handleIsAuthenticated(ctx context.Context, sessionInfo sessionI

case "phoneack2":
if authData["wait"] != "true" {
return responses.AuthDenied, "", nil
return responses.AuthDenied, `{"message": "phoneack2 should have wait set to true"}`, nil
}

// This one is failing remotely as an example
select {
case <-time.After(2 * time.Second):
return responses.AuthDenied, "", nil
return responses.AuthDenied, `{"message": "Timeout reached"}`, nil
case <-ctx.Done():
return responses.AuthCancelled, "", nil
}

case "fidodevice1":
if authData["wait"] != "true" {
return responses.AuthDenied, "", nil
return responses.AuthDenied, `{"message": "fidodevice1 should have wait set to true"}`, nil
}

// simulate direct exchange with the FIDO device
Expand All @@ -569,7 +572,7 @@ func (b *Broker) handleIsAuthenticated(ctx context.Context, sessionInfo sessionI

case "qrcodewithtypo":
if authData["wait"] != "true" {
return responses.AuthDenied, "", nil
return responses.AuthDenied, `{"message": "qrcodewithtypo should have wait set to true"}`, nil
}
// Simulate connexion with remote server to check that the correct code was entered
select {
Expand All @@ -585,7 +588,7 @@ func (b *Broker) handleIsAuthenticated(ctx context.Context, sessionInfo sessionI
if authData["challenge"] != "" {
// validate challenge given manually by the user
if authData["challenge"] != "aaaaa" {
return responses.AuthDenied, "", nil
return responses.AuthDenied, `{"message": "invalid challenge, should be aaaaa"}`, nil
}
} else if authData["wait"] == "true" {
// we are simulating clicking on the url signal received by the broker
Expand All @@ -596,16 +599,16 @@ func (b *Broker) handleIsAuthenticated(ctx context.Context, sessionInfo sessionI
return responses.AuthCancelled, "", nil
}
} else {
return responses.AuthDenied, "", nil
return responses.AuthDenied, `{"message": "challenge timeout "}`, nil
}
}

user, exists := exampleUsers[sessionInfo.username]
if !exists {
return responses.AuthDenied, "", nil
return responses.AuthDenied, `{"message": "user not found"}`, nil
}

return responses.AuthGranted, user.String(), nil
return responses.AuthGranted, userInfoFromName(user.Name), nil
}

// EndSession ends the requested session and triggers the necessary clean up steps, if any.
Expand Down Expand Up @@ -719,3 +722,25 @@ func (b *Broker) updateSession(sessionID string, info sessionInfo) error {
b.currentSessions[sessionID] = info
return nil
}

// userInfoFromName transform a given name to the strinfigy userinfo string.
func userInfoFromName(name string) string {
user := struct {
Name string
}{Name: name}

var buf bytes.Buffer

// only used for the example, we can ignore the template execution error as the returned data will be failing.
_ = template.Must(template.New("").Parse(`{
"name": "{{.Name}}",
"uuid": "uuid-{{.Name}}",
"gecos": "gecos for {{.Name}}",
"dir": "/home/{{.Name}}",
"shell": "/bin/sh/{{.Name}}",
"avatar": "avatar for {{.Name}}",
"groups": [ {"name": "group-{{.Name}}", "ugid": "group-{{.Name}}"} ]
}`)).Execute(&buf, user)

return buf.String()
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FIRST CALL:
access: denied
data: {"mock_answer": "denied by time out"}
data: {"message": "denied by time out"}
err: <nil>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FIRST CALL:
access:
data:
err: missing userinfo key in granted user access, got: {}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FIRST CALL:
access:
data:
err: invalid user information (not json formatted): invalid
err: response returned by the broker is not a valid json: invalid character 'i' looking for beginning of value
Broker returned: invalid
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FIRST CALL:
access: granted
data: {"mock_answer": "authentication granted by timeout"}
data: {"Name":"IA_second_call","UID":65565,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","Groups":[{"Name":"group-IA_second_call","GID":65566}]}
err: <nil>
SECOND CALL:
access:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FIRST CALL:
access: granted
data: {"mock_answer": "authentication granted by default"}
data: {"Name":"success","UID":65558,"Gecos":"gecos for success","Dir":"/home/success","Shell":"/bin/sh/success","Groups":[{"Name":"group-success","GID":65559}]}
err: <nil>
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
FIRST CALL:
access: cancelled
data: {"mock_answer": "cancelled by user"}
data: {"message": "cancelled by user"}
err: <nil>
SECOND CALL:
access: granted
data: {"mock_answer": "authentication granted by timeout"}
data: {"Name":"IA_second_call","UID":65565,"Gecos":"gecos for IA_second_call","Dir":"/home/IA_second_call","Shell":"/bin/sh/IA_second_call","Groups":[{"Name":"group-IA_second_call","GID":65566}]}
err: <nil>
7 changes: 6 additions & 1 deletion internal/services/pam/pam.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/ubuntu/authd"
"github.com/ubuntu/authd/internal/brokers"
"github.com/ubuntu/authd/internal/brokers/responses"
"github.com/ubuntu/authd/internal/log"
"github.com/ubuntu/decorate"
)
Expand Down Expand Up @@ -177,9 +178,13 @@ func (s Service) IsAuthenticated(ctx context.Context, req *authd.IARequest) (res
return nil, err
}

if access == responses.AuthGranted {
data = ""
}

return &authd.IAResponse{
Access: access,
Data: data,
Msg: data,
}, nil
}

Expand Down
13 changes: 6 additions & 7 deletions internal/services/pam/pam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,18 +315,17 @@ func TestIsAuthenticated(t *testing.T) {
secondCall bool
cancelFirstCall bool
}{
"Successfully authenticate": {},
"Successfully authenticate": {username: "success"},
"Successfully authenticate if first call is canceled": {username: "IA_second_call", secondCall: true, cancelFirstCall: true},
"Denies authentication when broker times out": {username: "IA_timeout"},

"Empty data gets JSON formatted": {username: "IA_empty_data"},

// service errors
"Error when sessionID is empty": {sessionID: "-"},
"Error when there is no broker": {sessionID: "invalid-session"},

// broker errors
"Error when authenticating": {username: "IA_error"},
"Error on empty data even if granted": {username: "IA_empty_data"},
"Error when broker returns invalid access": {username: "IA_invalid"},
"Error when broker returns invalid data": {username: "IA_invalid_data"},
"Error when calling second time without cancelling": {username: "IA_second_call", secondCall: true},
Expand Down Expand Up @@ -361,9 +360,9 @@ func TestIsAuthenticated(t *testing.T) {
AuthenticationData: "some data",
}
iaResp, err := client.IsAuthenticated(ctx, iaReq)
firstCall = fmt.Sprintf("FIRST CALL:\n\taccess: %s\n\tdata: %s\n\terr: %v\n",
firstCall = fmt.Sprintf("FIRST CALL:\n\taccess: %s\n\tmsg: %s\n\terr: %v\n",
iaResp.GetAccess(),
iaResp.GetData(),
iaResp.GetMsg(),
err,
)
}()
Expand All @@ -381,9 +380,9 @@ func TestIsAuthenticated(t *testing.T) {
AuthenticationData: "some data",
}
iaResp, err := client.IsAuthenticated(context.Background(), iaReq)
secondCall = fmt.Sprintf("SECOND CALL:\n\taccess: %s\n\tdata: %s\n\terr: %v\n",
secondCall = fmt.Sprintf("SECOND CALL:\n\taccess: %s\n\tmsg: %s\n\terr: %v\n",
iaResp.GetAccess(),
iaResp.GetData(),
iaResp.GetMsg(),
err,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FIRST CALL:
access: denied
data: {"mock_answer": "denied by time out"}
msg: {"message": "denied by time out"}
err: <nil>

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FIRST CALL:
access:
msg:
err: rpc error: code = Unknown desc = can't check authentication: missing userinfo key in granted user access, got: {}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FIRST CALL:
access:
data:
msg:
err: rpc error: code = Unknown desc = can't check authentication: Broker "BrokerMock": IsAuthenticated errored out
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FIRST CALL:
access:
data:
msg:
err: rpc error: code = Unknown desc = can't check authentication: invalid access authentication key: invalid
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FIRST CALL:
access:
data:
err: rpc error: code = Unknown desc = can't check authentication: invalid user information (not json formatted): invalid
msg:
err: rpc error: code = Unknown desc = can't check authentication: response returned by the broker is not a valid json: invalid character 'i' looking for beginning of value
Broker returned: invalid
Loading