package server import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "path" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/dexidp/dex/server/internal" "github.com/dexidp/dex/storage" ) func toJSON(a interface{}) string { b, err := json.Marshal(a) if err != nil { return "" } return string(b) } func mockTestStorage(t *testing.T, s storage.Storage) { ctx := t.Context() c := storage.Client{ ID: "test", Secret: "barfoo", RedirectURIs: []string{"foo://bar.com/", "https://auth.example.com"}, Name: "dex client", LogoURL: "https://goo.gl/JIyzIC", } err := s.CreateClient(ctx, c) require.NoError(t, err) c1 := storage.Connector{ ID: "test", Type: "mockPassword", Name: "mockPassword", Config: []byte(`{ "username": "test", "password": "test" }`), } err = s.CreateConnector(ctx, c1) require.NoError(t, err) err = s.CreateRefresh(ctx, storage.RefreshToken{ ID: "test", Token: "bar", ObsoleteToken: "", Nonce: "foo", ClientID: "test", ConnectorID: "test", 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"}`), }) require.NoError(t, err) err = s.CreateRefresh(ctx, storage.RefreshToken{ ID: "expired", Token: "bar", ObsoleteToken: "", Nonce: "foo", ClientID: "test", ConnectorID: "test", Scopes: []string{"openid", "email", "profile"}, CreatedAt: time.Now().AddDate(-1, 0, 0).UTC().Round(time.Millisecond), LastUsed: time.Now().AddDate(-1, 0, 0).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"}`), }) require.NoError(t, err) err = s.CreateOfflineSessions(ctx, storage.OfflineSessions{ UserID: "1", ConnID: "test", Refresh: map[string]*storage.RefreshTokenRef{ "test": {ID: "test", ClientID: "test"}, "expired": {ID: "expired", ClientID: "test"}, }, ConnectorData: nil, }) require.NoError(t, err) } func getIntrospectionValue(issuerURL url.URL, issuedAt time.Time, expiry time.Time, tokenUse string) *Introspection { trueValue := true return &Introspection{ Active: true, ClientID: "test", Subject: "CgExEgR0ZXN0", Expiry: expiry.Unix(), IssuedAt: issuedAt.Unix(), NotBefore: issuedAt.Unix(), Audience: []string{ "test", }, Issuer: issuerURL.String(), TokenType: "Bearer", TokenUse: tokenUse, Extra: IntrospectionExtra{ Email: "jane.doe@example.com", EmailVerified: &trueValue, Groups: []string{ "a", "b", }, Name: "jane", }, } } func TestGetTokenFromRequestSuccess(t *testing.T) { t0 := time.Now() ctx := t.Context() now := func() time.Time { return t0 } // Setup a dex server. httpServer, s := newTestServer(t, func(c *Config) { c.Issuer += "/non-root-path" c.Now = now }) defer httpServer.Close() mockTestStorage(t, s.storage) // Generate a valid RS256-signed access token accessToken, _, err := s.newIDToken(ctx, "test", storage.Claims{ UserID: "1", Username: "jane", }, []string{"openid"}, "nonce", "", "", "test") require.NoError(t, err) tests := []struct { testName string expectedToken string expectedTokenType TokenTypeEnum }{ // Access Token { testName: "Access Token", expectedToken: accessToken, expectedTokenType: AccessToken, }, // Refresh Token { testName: "Refresh token", expectedToken: "CgR0ZXN0EgNiYXI", expectedTokenType: RefreshToken, }, // Unknown token { testName: "Unknown token", expectedToken: "AaAaAaA", expectedTokenType: RefreshToken, }, } for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { data := url.Values{} data.Set("token", tc.expectedToken) req := httptest.NewRequest(http.MethodPost, "https://test.tech/token/introspect", bytes.NewBufferString(data.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") token, tokenType, err := s.getTokenFromRequest(req) if err != nil { t.Fatalf("Error returned: %s", err.Error()) } if token != tc.expectedToken { t.Fatalf("Wrong token returned. Expected %v got %v", tc.expectedToken, token) } if tokenType != tc.expectedTokenType { t.Fatalf("Wrong token type returned. Expected %v got %v", tc.expectedTokenType, tokenType) } }) } } func TestGetTokenFromRequestFailure(t *testing.T) { t0 := time.Now() now := func() time.Time { return t0 } // Setup a dex server. httpServer, s := newTestServer(t, func(c *Config) { c.Issuer += "/non-root-path" c.Now = now }) defer httpServer.Close() _, _, err := s.getTokenFromRequest(httptest.NewRequest(http.MethodGet, "https://test.tech/token/introspect", nil)) require.ErrorIs(t, err, &introspectionError{ typ: errInvalidRequest, desc: "HTTP method is \"GET\", expected \"POST\".", code: http.StatusBadRequest, }) _, _, err = s.getTokenFromRequest(httptest.NewRequest(http.MethodPost, "https://test.tech/token/introspect", nil)) require.ErrorIs(t, err, &introspectionError{ typ: errInvalidRequest, desc: "The POST body can not be empty.", code: http.StatusBadRequest, }) req := httptest.NewRequest(http.MethodPost, "https://test.tech/token/introspect", strings.NewReader("token_type_hint=access_token")) req.Header.Add("Content-Type", "application/x-www-form-urlencoded") _, _, err = s.getTokenFromRequest(req) require.ErrorIs(t, err, &introspectionError{ typ: errInvalidRequest, desc: "The POST body doesn't contain 'token' parameter.", code: http.StatusBadRequest, }) } func TestHandleIntrospect(t *testing.T) { t0 := time.Now() ctx := t.Context() // Setup a dex server. now := func() time.Time { return t0 } logger := newLogger(t) refreshTokenPolicy, err := NewRefreshTokenPolicy(logger, false, "", "24h", "") if err != nil { t.Fatalf("failed to prepare rotation policy: %v", err) } refreshTokenPolicy.now = now httpServer, s := newTestServer(t, func(c *Config) { c.Issuer += "/non-root-path" c.RefreshTokenPolicy = refreshTokenPolicy c.Now = now }) defer httpServer.Close() mockTestStorage(t, s.storage) activeAccessToken, expiry, err := s.newIDToken(ctx, "test", storage.Claims{ UserID: "1", Username: "jane", Email: "jane.doe@example.com", EmailVerified: true, Groups: []string{"a", "b"}, }, []string{"openid", "email", "profile", "groups"}, "foo", "", "", "test") require.NoError(t, err) activeRefreshToken, err := internal.Marshal(&internal.RefreshToken{RefreshId: "test", Token: "bar"}) require.NoError(t, err) expiredRefreshToken, err := internal.Marshal(&internal.RefreshToken{RefreshId: "expired", Token: "bar"}) require.NoError(t, err) inactiveResponse := "{\"active\":false}\n" badRequestResponse := `{"error":"invalid_request","error_description":"The POST body can not be empty."}` tests := []struct { testName string token string tokenType string response string responseStatusCode int }{ // No token { testName: "No token", response: badRequestResponse, responseStatusCode: 400, }, // Access token tests { testName: "Access Token: active", token: activeAccessToken, response: toJSON(getIntrospectionValue(s.issuerURL, t0, expiry, "access_token")), responseStatusCode: 200, }, { testName: "Access Token: wrong", token: "fake-token", response: inactiveResponse, responseStatusCode: 200, }, // Refresh token tests { testName: "Refresh Token: active", token: activeRefreshToken, response: toJSON(getIntrospectionValue(s.issuerURL, t0, t0.Add(s.refreshTokenPolicy.absoluteLifetime), "refresh_token")), responseStatusCode: 200, }, { testName: "Refresh Token: expired", token: expiredRefreshToken, response: inactiveResponse, responseStatusCode: 200, }, { testName: "Refresh Token: active => false (wrong)", token: "fake-token", response: inactiveResponse, responseStatusCode: 200, }, } for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { data := url.Values{} if tc.token != "" { data.Set("token", tc.token) } if tc.tokenType != "" { data.Set("token_type_hint", tc.tokenType) } u, err := url.Parse(s.issuerURL.String()) if err != nil { t.Fatalf("Could not parse issuer URL %v", err) } u.Path = path.Join(u.Path, "token", "introspect") req, _ := http.NewRequest("POST", u.String(), bytes.NewBufferString(data.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") rr := httptest.NewRecorder() s.ServeHTTP(rr, req) if rr.Code != tc.responseStatusCode { t.Errorf("%s: Unexpected Response Type. Expected %v got %v", tc.testName, tc.responseStatusCode, rr.Code) } result, _ := io.ReadAll(rr.Body) if string(result) != tc.response { t.Errorf("%s: Unexpected Response. Expected %q got %q", tc.testName, tc.response, result) } }) } } func TestIntrospectErrHelper(t *testing.T) { t0 := time.Now() now := func() time.Time { return t0 } // Setup a dex server. httpServer, s := newTestServer(t, func(c *Config) { c.Issuer += "/non-root-path" c.Now = now }) defer httpServer.Close() tests := []struct { testName string err *introspectionError resStatusCode int resBody string }{ { testName: "Inactive Token", err: newIntrospectInactiveTokenError(), resStatusCode: http.StatusOK, resBody: "{\"active\":false}\n", }, { testName: "Bad Request", err: newIntrospectBadRequestError("This is a bad request"), resStatusCode: http.StatusBadRequest, resBody: `{"error":"invalid_request","error_description":"This is a bad request"}`, }, { testName: "Internal Server Error", err: newIntrospectInternalServerError(), resStatusCode: http.StatusInternalServerError, resBody: `{"error":"server_error"}`, }, } for _, tc := range tests { t.Run(tc.testName, func(t *testing.T) { w1 := httptest.NewRecorder() s.introspectErrHelper(w1, tc.err.typ, tc.err.desc, tc.err.code) res := w1.Result() require.Equal(t, tc.resStatusCode, res.StatusCode) require.Equal(t, "application/json", res.Header.Get("Content-Type")) data, err := io.ReadAll(res.Body) defer res.Body.Close() require.NoError(t, err) require.Equal(t, tc.resBody, string(data)) }) } }