mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
509 lines
13 KiB
509 lines
13 KiB
package server |
|
|
|
import ( |
|
"context" |
|
"net" |
|
"os" |
|
"testing" |
|
"time" |
|
|
|
"github.com/sirupsen/logrus" |
|
"google.golang.org/grpc" |
|
"google.golang.org/grpc/credentials/insecure" |
|
|
|
"github.com/dexidp/dex/api/v2" |
|
"github.com/dexidp/dex/pkg/log" |
|
"github.com/dexidp/dex/server/internal" |
|
"github.com/dexidp/dex/storage" |
|
"github.com/dexidp/dex/storage/memory" |
|
) |
|
|
|
// apiClient is a test gRPC client. When constructed, it runs a server in |
|
// the background to exercise the serialization and network configuration |
|
// instead of just this package's server implementation. |
|
type apiClient struct { |
|
// Embedded gRPC client to talk to the server. |
|
api.DexClient |
|
// Close releases resources associated with this client, including shutting |
|
// down the background server. |
|
Close func() |
|
} |
|
|
|
// newAPI constructs a gRCP client connected to a backing server. |
|
func newAPI(s storage.Storage, logger log.Logger, t *testing.T) *apiClient { |
|
l, err := net.Listen("tcp", "127.0.0.1:0") |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
|
|
serv := grpc.NewServer() |
|
api.RegisterDexServer(serv, NewAPI(s, logger, "test")) |
|
go serv.Serve(l) |
|
|
|
// NewClient will retry automatically if the serv.Serve() goroutine |
|
// hasn't started yet. |
|
conn, err := grpc.NewClient(l.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
|
|
return &apiClient{ |
|
DexClient: api.NewDexClient(conn), |
|
Close: func() { |
|
conn.Close() |
|
serv.Stop() |
|
l.Close() |
|
}, |
|
} |
|
} |
|
|
|
// Attempts to create, update and delete a test Password |
|
func TestPassword(t *testing.T) { |
|
logger := &logrus.Logger{ |
|
Out: os.Stderr, |
|
Formatter: &logrus.TextFormatter{DisableColors: true}, |
|
Level: logrus.DebugLevel, |
|
} |
|
|
|
s := memory.New(logger) |
|
client := newAPI(s, logger, t) |
|
defer client.Close() |
|
|
|
ctx := context.Background() |
|
email := "test@example.com" |
|
p := api.Password{ |
|
Email: email, |
|
// bcrypt hash of the value "test1" with cost 10 |
|
Hash: []byte("$2a$10$XVMN/Fid.Ks4CXgzo8fpR.iU1khOMsP5g9xQeXuBm1wXjRX8pjUtO"), |
|
Username: "test", |
|
UserId: "test123", |
|
} |
|
|
|
createReq := api.CreatePasswordReq{ |
|
Password: &p, |
|
} |
|
|
|
if resp, err := client.CreatePassword(ctx, &createReq); err != nil || resp.AlreadyExists { |
|
if resp.AlreadyExists { |
|
t.Fatalf("Unable to create password since %s already exists", createReq.Password.Email) |
|
} |
|
t.Fatalf("Unable to create password: %v", err) |
|
} |
|
|
|
// Attempt to create a password that already exists. |
|
if resp, _ := client.CreatePassword(ctx, &createReq); !resp.AlreadyExists { |
|
t.Fatalf("Created password %s twice", createReq.Password.Email) |
|
} |
|
|
|
// Attempt to verify valid password and email |
|
goodVerifyReq := &api.VerifyPasswordReq{ |
|
Email: email, |
|
Password: "test1", |
|
} |
|
goodVerifyResp, err := client.VerifyPassword(ctx, goodVerifyReq) |
|
if err != nil { |
|
t.Fatalf("Unable to run verify password we expected to be valid for correct email: %v", err) |
|
} |
|
if !goodVerifyResp.Verified { |
|
t.Fatalf("verify password failed for password expected to be valid for correct email. expected %t, found %t", true, goodVerifyResp.Verified) |
|
} |
|
if goodVerifyResp.NotFound { |
|
t.Fatalf("verify password failed to return not found response. expected %t, found %t", false, goodVerifyResp.NotFound) |
|
} |
|
|
|
// Check not found response for valid password with wrong email |
|
badEmailVerifyReq := &api.VerifyPasswordReq{ |
|
Email: "somewrongaddress@email.com", |
|
Password: "test1", |
|
} |
|
badEmailVerifyResp, err := client.VerifyPassword(ctx, badEmailVerifyReq) |
|
if err != nil { |
|
t.Fatalf("Unable to run verify password for incorrect email: %v", err) |
|
} |
|
if badEmailVerifyResp.Verified { |
|
t.Fatalf("verify password passed for password expected to be not found. expected %t, found %t", false, badEmailVerifyResp.Verified) |
|
} |
|
if !badEmailVerifyResp.NotFound { |
|
t.Fatalf("expected not found response for verify password with bad email. expected %t, found %t", true, badEmailVerifyResp.NotFound) |
|
} |
|
|
|
// Check that wrong password fails |
|
badPassVerifyReq := &api.VerifyPasswordReq{ |
|
Email: email, |
|
Password: "wrong_password", |
|
} |
|
badPassVerifyResp, err := client.VerifyPassword(ctx, badPassVerifyReq) |
|
if err != nil { |
|
t.Fatalf("Unable to run verify password for password we expected to be invalid: %v", err) |
|
} |
|
if badPassVerifyResp.Verified { |
|
t.Fatalf("verify password passed for password we expected to fail. expected %t, found %t", false, badPassVerifyResp.Verified) |
|
} |
|
if badPassVerifyResp.NotFound { |
|
t.Fatalf("did not expect expected not found response for verify password with bad email. expected %t, found %t", false, badPassVerifyResp.NotFound) |
|
} |
|
|
|
updateReq := api.UpdatePasswordReq{ |
|
Email: email, |
|
NewUsername: "test1", |
|
} |
|
|
|
if _, err := client.UpdatePassword(ctx, &updateReq); err != nil { |
|
t.Fatalf("Unable to update password: %v", err) |
|
} |
|
|
|
pass, err := s.GetPassword(updateReq.Email) |
|
if err != nil { |
|
t.Fatalf("Unable to retrieve password: %v", err) |
|
} |
|
|
|
if pass.Username != updateReq.NewUsername { |
|
t.Fatalf("UpdatePassword failed. Expected username %s retrieved %s", updateReq.NewUsername, pass.Username) |
|
} |
|
|
|
deleteReq := api.DeletePasswordReq{ |
|
Email: "test@example.com", |
|
} |
|
|
|
if _, err := client.DeletePassword(ctx, &deleteReq); err != nil { |
|
t.Fatalf("Unable to delete password: %v", err) |
|
} |
|
} |
|
|
|
// Ensures checkCost returns expected values |
|
func TestCheckCost(t *testing.T) { |
|
logger := &logrus.Logger{ |
|
Out: os.Stderr, |
|
Formatter: &logrus.TextFormatter{DisableColors: true}, |
|
Level: logrus.DebugLevel, |
|
} |
|
|
|
s := memory.New(logger) |
|
client := newAPI(s, logger, t) |
|
defer client.Close() |
|
|
|
tests := []struct { |
|
name string |
|
inputHash []byte |
|
|
|
wantErr bool |
|
}{ |
|
{ |
|
name: "valid cost", |
|
// bcrypt hash of the value "test1" with cost 12 (default) |
|
inputHash: []byte("$2a$12$M2Ot95Qty1MuQdubh1acWOiYadJDzeVg3ve4n5b.dgcgPdjCseKx2"), |
|
}, |
|
{ |
|
name: "invalid hash", |
|
inputHash: []byte(""), |
|
wantErr: true, |
|
}, |
|
{ |
|
name: "cost below default", |
|
// bcrypt hash of the value "test1" with cost 4 |
|
inputHash: []byte("$2a$04$8bSTbuVCLpKzaqB3BmgI7edDigG5tIQKkjYUu/mEO9gQgIkw9m7eG"), |
|
wantErr: true, |
|
}, |
|
{ |
|
name: "cost above recommendation", |
|
// bcrypt hash of the value "test1" with cost 17 |
|
inputHash: []byte("$2a$17$tWuZkTxtSmRyWZAGWVHQE.7npdl.TgP8adjzLJD.SyjpFznKBftPe"), |
|
wantErr: true, |
|
}, |
|
} |
|
|
|
for _, tc := range tests { |
|
if err := checkCost(tc.inputHash); err != nil { |
|
if !tc.wantErr { |
|
t.Errorf("%s: %s", tc.name, err) |
|
} |
|
continue |
|
} |
|
|
|
if tc.wantErr { |
|
t.Errorf("%s: expected err", tc.name) |
|
continue |
|
} |
|
} |
|
} |
|
|
|
// Attempts to list and revoke an existing refresh token. |
|
func TestRefreshToken(t *testing.T) { |
|
logger := &logrus.Logger{ |
|
Out: os.Stderr, |
|
Formatter: &logrus.TextFormatter{DisableColors: true}, |
|
Level: logrus.DebugLevel, |
|
} |
|
|
|
s := memory.New(logger) |
|
client := newAPI(s, logger, t) |
|
defer client.Close() |
|
|
|
ctx := context.Background() |
|
|
|
// Creating a storage with an existing refresh token and offline session for the user. |
|
id := storage.NewID() |
|
r := storage.RefreshToken{ |
|
ID: id, |
|
Token: "bar", |
|
Nonce: "foo", |
|
ClientID: "client_id", |
|
ConnectorID: "client_secret", |
|
Scopes: []string{"openid", "email", "profile"}, |
|
CreatedAt: time.Now().UTC().Round(time.Millisecond), |
|
LastUsed: time.Now().UTC().Round(time.Millisecond), |
|
Claims: storage.Claims{ |
|
UserID: "1", |
|
Username: "jane", |
|
Email: "jane.doe@example.com", |
|
EmailVerified: true, |
|
Groups: []string{"a", "b"}, |
|
}, |
|
ConnectorData: []byte(`{"some":"data"}`), |
|
} |
|
|
|
if err := s.CreateRefresh(ctx, r); err != nil { |
|
t.Fatalf("create refresh token: %v", err) |
|
} |
|
|
|
tokenRef := storage.RefreshTokenRef{ |
|
ID: r.ID, |
|
ClientID: r.ClientID, |
|
CreatedAt: r.CreatedAt, |
|
LastUsed: r.LastUsed, |
|
} |
|
|
|
session := storage.OfflineSessions{ |
|
UserID: r.Claims.UserID, |
|
ConnID: r.ConnectorID, |
|
Refresh: make(map[string]*storage.RefreshTokenRef), |
|
} |
|
session.Refresh[tokenRef.ClientID] = &tokenRef |
|
|
|
if err := s.CreateOfflineSessions(ctx, session); err != nil { |
|
t.Fatalf("create offline session: %v", err) |
|
} |
|
|
|
subjectString, err := internal.Marshal(&internal.IDTokenSubject{ |
|
UserId: r.Claims.UserID, |
|
ConnId: r.ConnectorID, |
|
}) |
|
if err != nil { |
|
t.Errorf("failed to marshal offline session ID: %v", err) |
|
} |
|
|
|
// Testing the api. |
|
listReq := api.ListRefreshReq{ |
|
UserId: subjectString, |
|
} |
|
|
|
listResp, err := client.ListRefresh(ctx, &listReq) |
|
if err != nil { |
|
t.Fatalf("Unable to list refresh tokens for user: %v", err) |
|
} |
|
|
|
for _, tok := range listResp.RefreshTokens { |
|
if tok.CreatedAt != r.CreatedAt.Unix() { |
|
t.Errorf("Expected CreatedAt timestamp %v, got %v", r.CreatedAt.Unix(), tok.CreatedAt) |
|
} |
|
|
|
if tok.LastUsed != r.LastUsed.Unix() { |
|
t.Errorf("Expected LastUsed timestamp %v, got %v", r.LastUsed.Unix(), tok.LastUsed) |
|
} |
|
} |
|
|
|
revokeReq := api.RevokeRefreshReq{ |
|
UserId: subjectString, |
|
ClientId: r.ClientID, |
|
} |
|
|
|
resp, err := client.RevokeRefresh(ctx, &revokeReq) |
|
if err != nil { |
|
t.Fatalf("Unable to revoke refresh tokens for user: %v", err) |
|
} |
|
if resp.NotFound { |
|
t.Errorf("refresh token session wasn't found") |
|
} |
|
|
|
// Try to delete again. |
|
// |
|
// See https://github.com/dexidp/dex/issues/1055 |
|
resp, err = client.RevokeRefresh(ctx, &revokeReq) |
|
if err != nil { |
|
t.Fatalf("Unable to revoke refresh tokens for user: %v", err) |
|
} |
|
if !resp.NotFound { |
|
t.Errorf("refresh token session was found") |
|
} |
|
|
|
if resp, _ := client.ListRefresh(ctx, &listReq); len(resp.RefreshTokens) != 0 { |
|
t.Fatalf("Refresh token returned in spite of revoking it.") |
|
} |
|
} |
|
|
|
func TestUpdateClient(t *testing.T) { |
|
logger := &logrus.Logger{ |
|
Out: os.Stderr, |
|
Formatter: &logrus.TextFormatter{DisableColors: true}, |
|
Level: logrus.DebugLevel, |
|
} |
|
|
|
s := memory.New(logger) |
|
client := newAPI(s, logger, t) |
|
defer client.Close() |
|
ctx := context.Background() |
|
|
|
createClient := func(t *testing.T, clientId string) { |
|
resp, err := client.CreateClient(ctx, &api.CreateClientReq{ |
|
Client: &api.Client{ |
|
Id: clientId, |
|
Secret: "", |
|
RedirectUris: []string{}, |
|
TrustedPeers: nil, |
|
Public: true, |
|
Name: "", |
|
LogoUrl: "", |
|
}, |
|
}) |
|
if err != nil { |
|
t.Fatalf("unable to create the client: %v", err) |
|
} |
|
|
|
if resp == nil { |
|
t.Fatalf("create client returned no response") |
|
} |
|
if resp.AlreadyExists { |
|
t.Error("existing client was found") |
|
} |
|
|
|
if resp.Client == nil { |
|
t.Fatalf("no client created") |
|
} |
|
} |
|
|
|
deleteClient := func(t *testing.T, clientId string) { |
|
resp, err := client.DeleteClient(ctx, &api.DeleteClientReq{ |
|
Id: clientId, |
|
}) |
|
if err != nil { |
|
t.Fatalf("unable to delete the client: %v", err) |
|
} |
|
if resp == nil { |
|
t.Fatalf("delete client delete client returned no response") |
|
} |
|
} |
|
|
|
tests := map[string]struct { |
|
setup func(t *testing.T, clientId string) |
|
cleanup func(t *testing.T, clientId string) |
|
req *api.UpdateClientReq |
|
wantErr bool |
|
want *api.UpdateClientResp |
|
}{ |
|
"update client": { |
|
setup: createClient, |
|
cleanup: deleteClient, |
|
req: &api.UpdateClientReq{ |
|
Id: "test", |
|
RedirectUris: []string{"https://redirect"}, |
|
TrustedPeers: []string{"test"}, |
|
Name: "test", |
|
LogoUrl: "https://logout", |
|
}, |
|
wantErr: false, |
|
want: &api.UpdateClientResp{ |
|
NotFound: false, |
|
}, |
|
}, |
|
"update client without ID": { |
|
setup: createClient, |
|
cleanup: deleteClient, |
|
req: &api.UpdateClientReq{ |
|
Id: "", |
|
RedirectUris: nil, |
|
TrustedPeers: nil, |
|
Name: "test", |
|
LogoUrl: "test", |
|
}, |
|
wantErr: true, |
|
want: &api.UpdateClientResp{ |
|
NotFound: false, |
|
}, |
|
}, |
|
"update client which not exists ": { |
|
req: &api.UpdateClientReq{ |
|
Id: "test", |
|
RedirectUris: nil, |
|
TrustedPeers: nil, |
|
Name: "test", |
|
LogoUrl: "test", |
|
}, |
|
wantErr: true, |
|
want: &api.UpdateClientResp{ |
|
NotFound: false, |
|
}, |
|
}, |
|
} |
|
|
|
for name, tc := range tests { |
|
t.Run(name, func(t *testing.T) { |
|
if tc.setup != nil { |
|
tc.setup(t, tc.req.Id) |
|
} |
|
resp, err := client.UpdateClient(ctx, tc.req) |
|
if err != nil && !tc.wantErr { |
|
t.Fatalf("failed to update the client: %v", err) |
|
} |
|
|
|
if !tc.wantErr { |
|
if resp == nil { |
|
t.Fatalf("update client response not found") |
|
} |
|
|
|
if tc.want.NotFound != resp.NotFound { |
|
t.Errorf("expected in response NotFound: %t", tc.want.NotFound) |
|
} |
|
|
|
client, err := s.GetClient(tc.req.Id) |
|
if err != nil { |
|
t.Errorf("no client found in the storage: %v", err) |
|
} |
|
|
|
if tc.req.Id != client.ID { |
|
t.Errorf("expected stored client with ID: %s, found %s", tc.req.Id, client.ID) |
|
} |
|
if tc.req.Name != client.Name { |
|
t.Errorf("expected stored client with Name: %s, found %s", tc.req.Name, client.Name) |
|
} |
|
if tc.req.LogoUrl != client.LogoURL { |
|
t.Errorf("expected stored client with LogoURL: %s, found %s", tc.req.LogoUrl, client.LogoURL) |
|
} |
|
for _, redirectURI := range tc.req.RedirectUris { |
|
found := find(redirectURI, client.RedirectURIs) |
|
if !found { |
|
t.Errorf("expected redirect URI: %s", redirectURI) |
|
} |
|
} |
|
for _, peer := range tc.req.TrustedPeers { |
|
found := find(peer, client.TrustedPeers) |
|
if !found { |
|
t.Errorf("expected trusted peer: %s", peer) |
|
} |
|
} |
|
} |
|
|
|
if tc.cleanup != nil { |
|
tc.cleanup(t, tc.req.Id) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
func find(item string, items []string) bool { |
|
for _, i := range items { |
|
if item == i { |
|
return true |
|
} |
|
} |
|
return false |
|
}
|
|
|