|
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net"
|
|
|
|
|
"slices"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"google.golang.org/grpc"
|
|
|
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
|
|
|
|
|
|
|
|
"github.com/dexidp/dex/api/v2"
|
|
|
|
|
"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()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newLogger(t *testing.T) *slog.Logger {
|
|
|
|
|
return slog.New(slog.NewTextHandler(t.Output(), &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newAPI constructs a gRCP client connected to a backing server.
|
|
|
|
|
func newAPI(t *testing.T, s storage.Storage, logger *slog.Logger) *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", nil))
|
|
|
|
|
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 := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
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(ctx, 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 := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
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 := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
// 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 := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
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(ctx, 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 := slices.Contains(client.RedirectURIs, redirectURI)
|
|
|
|
|
if !found {
|
|
|
|
|
t.Errorf("expected redirect URI: %s", redirectURI)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, peer := range tc.req.TrustedPeers {
|
|
|
|
|
found := slices.Contains(client.TrustedPeers, peer)
|
|
|
|
|
if !found {
|
|
|
|
|
t.Errorf("expected trusted peer: %s", peer)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tc.cleanup != nil {
|
|
|
|
|
tc.cleanup(t, tc.req.Id)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCreateConnector(t *testing.T) {
|
|
|
|
|
t.Setenv("DEX_API_CONNECTORS_CRUD", "true")
|
|
|
|
|
|
|
|
|
|
logger := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
connectorID := "connector123"
|
|
|
|
|
connectorName := "TestConnector"
|
|
|
|
|
connectorType := "TestType"
|
|
|
|
|
connectorConfig := []byte(`{"key": "value"}`)
|
|
|
|
|
|
|
|
|
|
createReq := api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
Name: connectorName,
|
|
|
|
|
Type: connectorType,
|
|
|
|
|
Config: connectorConfig,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test valid connector creation
|
|
|
|
|
if resp, err := client.CreateConnector(ctx, &createReq); err != nil || resp.AlreadyExists {
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unable to create connector: %v", err)
|
|
|
|
|
} else if resp.AlreadyExists {
|
|
|
|
|
t.Fatalf("Unable to create connector since %s already exists", connectorID)
|
|
|
|
|
}
|
|
|
|
|
t.Fatalf("Unable to create connector: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test creating the same connector again (expecting failure)
|
|
|
|
|
if resp, _ := client.CreateConnector(ctx, &createReq); !resp.AlreadyExists {
|
|
|
|
|
t.Fatalf("Created connector %s twice", connectorID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createReq.Connector.Config = []byte("invalid_json")
|
|
|
|
|
|
|
|
|
|
// Test invalid JSON config
|
|
|
|
|
if _, err := client.CreateConnector(ctx, &createReq); err == nil {
|
|
|
|
|
t.Fatal("Expected an error for invalid JSON config, but none occurred")
|
|
|
|
|
} else if !strings.Contains(err.Error(), "invalid config supplied") {
|
|
|
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUpdateConnector(t *testing.T) {
|
|
|
|
|
t.Setenv("DEX_API_CONNECTORS_CRUD", "true")
|
|
|
|
|
|
|
|
|
|
logger := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
connectorID := "connector123"
|
|
|
|
|
newConnectorName := "UpdatedConnector"
|
|
|
|
|
newConnectorType := "UpdatedType"
|
|
|
|
|
newConnectorConfig := []byte(`{"updated_key": "updated_value"}`)
|
|
|
|
|
|
|
|
|
|
// Create a connector for testing
|
|
|
|
|
createReq := api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
Name: "TestConnector",
|
|
|
|
|
Type: "TestType",
|
|
|
|
|
Config: []byte(`{"key": "value"}`),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
client.CreateConnector(ctx, &createReq)
|
|
|
|
|
|
|
|
|
|
updateReq := api.UpdateConnectorReq{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
NewName: newConnectorName,
|
|
|
|
|
NewType: newConnectorType,
|
|
|
|
|
NewConfig: newConnectorConfig,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test valid connector update
|
|
|
|
|
if _, err := client.UpdateConnector(ctx, &updateReq); err != nil {
|
|
|
|
|
t.Fatalf("Unable to update connector: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := client.ListConnectors(ctx, &api.ListConnectorReq{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, connector := range resp.Connectors {
|
|
|
|
|
if connector.Id == connectorID {
|
|
|
|
|
if connector.Name != newConnectorName {
|
|
|
|
|
t.Fatal("connector name should have been updated")
|
|
|
|
|
}
|
|
|
|
|
if string(connector.Config) != string(newConnectorConfig) {
|
|
|
|
|
t.Fatal("connector config should have been updated")
|
|
|
|
|
}
|
|
|
|
|
if connector.Type != newConnectorType {
|
|
|
|
|
t.Fatal("connector type should have been updated")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateReq.NewConfig = []byte("invalid_json")
|
|
|
|
|
|
|
|
|
|
// Test invalid JSON config in update request
|
|
|
|
|
if _, err := client.UpdateConnector(ctx, &updateReq); err == nil {
|
|
|
|
|
t.Fatal("Expected an error for invalid JSON config in update, but none occurred")
|
|
|
|
|
} else if !strings.Contains(err.Error(), "invalid config supplied") {
|
|
|
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestUpdateConnectorGrantTypes(t *testing.T) {
|
|
|
|
|
t.Setenv("DEX_API_CONNECTORS_CRUD", "true")
|
|
|
|
|
|
|
|
|
|
logger := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
connectorID := "connector-gt"
|
|
|
|
|
|
|
|
|
|
// Create a connector without grant types
|
|
|
|
|
createReq := api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
Name: "TestConnector",
|
|
|
|
|
Type: "TestType",
|
|
|
|
|
Config: []byte(`{"key": "value"}`),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
_, err := client.CreateConnector(ctx, &createReq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to create connector: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set grant types
|
|
|
|
|
_, err = client.UpdateConnector(ctx, &api.UpdateConnectorReq{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
NewGrantTypes: &api.GrantTypes{GrantTypes: []string{"authorization_code", "refresh_token"}},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to update connector grant types: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := client.ListConnectors(ctx, &api.ListConnectorReq{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to list connectors: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for _, c := range resp.Connectors {
|
|
|
|
|
if c.Id == connectorID {
|
|
|
|
|
if !slices.Equal(c.GrantTypes, []string{"authorization_code", "refresh_token"}) {
|
|
|
|
|
t.Fatalf("expected grant types [authorization_code refresh_token], got %v", c.GrantTypes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear grant types by passing empty GrantTypes message
|
|
|
|
|
_, err = client.UpdateConnector(ctx, &api.UpdateConnectorReq{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
NewGrantTypes: &api.GrantTypes{},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to clear connector grant types: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err = client.ListConnectors(ctx, &api.ListConnectorReq{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("failed to list connectors: %v", err)
|
|
|
|
|
}
|
|
|
|
|
for _, c := range resp.Connectors {
|
|
|
|
|
if c.Id == connectorID {
|
|
|
|
|
if len(c.GrantTypes) != 0 {
|
|
|
|
|
t.Fatalf("expected empty grant types after clear, got %v", c.GrantTypes)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reject invalid grant type on update
|
|
|
|
|
_, err = client.UpdateConnector(ctx, &api.UpdateConnectorReq{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
NewGrantTypes: &api.GrantTypes{GrantTypes: []string{"bogus"}},
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected error for invalid grant type, got nil")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), `unknown grant type "bogus"`) {
|
|
|
|
|
t.Fatalf("unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reject invalid grant type on create
|
|
|
|
|
_, err = client.CreateConnector(ctx, &api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: "bad-gt",
|
|
|
|
|
Name: "Bad",
|
|
|
|
|
Type: "TestType",
|
|
|
|
|
Config: []byte(`{}`),
|
|
|
|
|
GrantTypes: []string{"invalid_type"},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected error for invalid grant type on create, got nil")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), `unknown grant type "invalid_type"`) {
|
|
|
|
|
t.Fatalf("unexpected error: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDeleteConnector(t *testing.T) {
|
|
|
|
|
t.Setenv("DEX_API_CONNECTORS_CRUD", "true")
|
|
|
|
|
|
|
|
|
|
logger := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
connectorID := "connector123"
|
|
|
|
|
|
|
|
|
|
// Create a connector for testing
|
|
|
|
|
createReq := api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
Name: "TestConnector",
|
|
|
|
|
Type: "TestType",
|
|
|
|
|
Config: []byte(`{"key": "value"}`),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
client.CreateConnector(ctx, &createReq)
|
|
|
|
|
|
|
|
|
|
deleteReq := api.DeleteConnectorReq{
|
|
|
|
|
Id: connectorID,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test valid connector deletion
|
|
|
|
|
if _, err := client.DeleteConnector(ctx, &deleteReq); err != nil {
|
|
|
|
|
t.Fatalf("Unable to delete connector: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test non existent connector deletion
|
|
|
|
|
resp, err := client.DeleteConnector(ctx, &deleteReq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unable to delete connector: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !resp.NotFound {
|
|
|
|
|
t.Fatal("Should return not found")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestListConnectors(t *testing.T) {
|
|
|
|
|
t.Setenv("DEX_API_CONNECTORS_CRUD", "true")
|
|
|
|
|
|
|
|
|
|
logger := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
// Create connectors for testing
|
|
|
|
|
createReq1 := api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: "connector1",
|
|
|
|
|
Name: "Connector1",
|
|
|
|
|
Type: "Type1",
|
|
|
|
|
Config: []byte(`{"key": "value1"}`),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
client.CreateConnector(ctx, &createReq1)
|
|
|
|
|
|
|
|
|
|
createReq2 := api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: "connector2",
|
|
|
|
|
Name: "Connector2",
|
|
|
|
|
Type: "Type2",
|
|
|
|
|
Config: []byte(`{"key": "value2"}`),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
client.CreateConnector(ctx, &createReq2)
|
|
|
|
|
|
|
|
|
|
listReq := api.ListConnectorReq{}
|
|
|
|
|
|
|
|
|
|
// Test listing connectors
|
|
|
|
|
if resp, err := client.ListConnectors(ctx, &listReq); err != nil {
|
|
|
|
|
t.Fatalf("Unable to list connectors: %v", err)
|
|
|
|
|
} else if len(resp.Connectors) != 2 { // Check the number of connectors in the response
|
|
|
|
|
t.Fatalf("Expected 2 connectors, found %d", len(resp.Connectors))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMissingConnectorsCRUDFeatureFlag(t *testing.T) {
|
|
|
|
|
logger := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
// Create connectors for testing
|
|
|
|
|
createReq1 := api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: "connector1",
|
|
|
|
|
Name: "Connector1",
|
|
|
|
|
Type: "Type1",
|
|
|
|
|
Config: []byte(`{"key": "value1"}`),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
client.CreateConnector(ctx, &createReq1)
|
|
|
|
|
|
|
|
|
|
createReq2 := api.CreateConnectorReq{
|
|
|
|
|
Connector: &api.Connector{
|
|
|
|
|
Id: "connector2",
|
|
|
|
|
Name: "Connector2",
|
|
|
|
|
Type: "Type2",
|
|
|
|
|
Config: []byte(`{"key": "value2"}`),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
client.CreateConnector(ctx, &createReq2)
|
|
|
|
|
|
|
|
|
|
listReq := api.ListConnectorReq{}
|
|
|
|
|
|
|
|
|
|
if _, err := client.ListConnectors(ctx, &listReq); err == nil {
|
|
|
|
|
t.Fatal("ListConnectors should have returned an error")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestListClients(t *testing.T) {
|
|
|
|
|
logger := newLogger(t)
|
|
|
|
|
s := memory.New(logger)
|
|
|
|
|
|
|
|
|
|
client := newAPI(t, s, logger)
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
|
|
|
|
|
// List Clients
|
|
|
|
|
listResp, err := client.ListClients(ctx, &api.ListClientReq{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unable to list clients: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(listResp.Clients) != 0 {
|
|
|
|
|
t.Fatalf("Expected 0 clients, got %d", len(listResp.Clients))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client1 := &api.Client{
|
|
|
|
|
Id: "client1",
|
|
|
|
|
Secret: "secret1",
|
|
|
|
|
RedirectUris: []string{"http://localhost:8080/callback"},
|
|
|
|
|
TrustedPeers: []string{"peer1"},
|
|
|
|
|
Public: false,
|
|
|
|
|
Name: "Test Client 1",
|
|
|
|
|
LogoUrl: "http://example.com/logo1.png",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client2 := &api.Client{
|
|
|
|
|
Id: "client2",
|
|
|
|
|
Secret: "secret2",
|
|
|
|
|
RedirectUris: []string{"http://localhost:8081/callback"},
|
|
|
|
|
TrustedPeers: []string{"peer2"},
|
|
|
|
|
Public: true,
|
|
|
|
|
Name: "Test Client 2",
|
|
|
|
|
LogoUrl: "http://example.com/logo2.png",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = client.CreateClient(ctx, &api.CreateClientReq{Client: client1})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unable to create client1: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = client.CreateClient(ctx, &api.CreateClientReq{Client: client2})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unable to create client2: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listResp, err = client.ListClients(ctx, &api.ListClientReq{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("Unable to list clients: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(listResp.Clients) != 2 {
|
|
|
|
|
t.Fatalf("Expected 2 clients, got %d", len(listResp.Clients))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clientMap := make(map[string]*api.ClientInfo)
|
|
|
|
|
for _, c := range listResp.Clients {
|
|
|
|
|
clientMap[c.Id] = c
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c1, exists := clientMap["client1"]; !exists {
|
|
|
|
|
t.Fatal("client1 not found in list")
|
|
|
|
|
} else {
|
|
|
|
|
if c1.Name != "Test Client 1" {
|
|
|
|
|
t.Errorf("Expected client1 name 'Test Client 1', got '%s'", c1.Name)
|
|
|
|
|
}
|
|
|
|
|
if len(c1.RedirectUris) != 1 || c1.RedirectUris[0] != "http://localhost:8080/callback" {
|
|
|
|
|
t.Errorf("Expected client1 redirect URIs ['http://localhost:8080/callback'], got %v", c1.RedirectUris)
|
|
|
|
|
}
|
|
|
|
|
if c1.Public != false {
|
|
|
|
|
t.Errorf("Expected client1 public false, got %v", c1.Public)
|
|
|
|
|
}
|
|
|
|
|
if c1.LogoUrl != "http://example.com/logo1.png" {
|
|
|
|
|
t.Errorf("Expected client1 logo URL 'http://example.com/logo1.png', got '%s'", c1.LogoUrl)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c2, exists := clientMap["client2"]; !exists {
|
|
|
|
|
t.Fatal("client2 not found in list")
|
|
|
|
|
} else {
|
|
|
|
|
if c2.Name != "Test Client 2" {
|
|
|
|
|
t.Errorf("Expected client2 name 'Test Client 2', got '%s'", c2.Name)
|
|
|
|
|
}
|
|
|
|
|
if len(c2.RedirectUris) != 1 || c2.RedirectUris[0] != "http://localhost:8081/callback" {
|
|
|
|
|
t.Errorf("Expected client2 redirect URIs ['http://localhost:8081/callback'], got %v", c2.RedirectUris)
|
|
|
|
|
}
|
|
|
|
|
if c2.Public != true {
|
|
|
|
|
t.Errorf("Expected client2 public true, got %v", c2.Public)
|
|
|
|
|
}
|
|
|
|
|
if c2.LogoUrl != "http://example.com/logo2.png" {
|
|
|
|
|
t.Errorf("Expected client2 logo URL 'http://example.com/logo2.png', got '%s'", c2.LogoUrl)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|