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.
415 lines
11 KiB
415 lines
11 KiB
package server |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"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 := context.Background() |
|
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() |
|
|
|
now := func() time.Time { return t0 } |
|
ctx, cancel := context.WithCancel(context.Background()) |
|
defer cancel() |
|
|
|
// Setup a dex server. |
|
httpServer, s := newTestServer(ctx, t, func(c *Config) { |
|
c.Issuer += "/non-root-path" |
|
c.Now = now |
|
}) |
|
defer httpServer.Close() |
|
|
|
tests := []struct { |
|
testName string |
|
expectedToken string |
|
expectedTokenType TokenTypeEnum |
|
}{ |
|
// Access Token |
|
{ |
|
testName: "Access Token", |
|
expectedToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", |
|
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 } |
|
ctx, cancel := context.WithCancel(context.Background()) |
|
defer cancel() |
|
|
|
// Setup a dex server. |
|
httpServer, s := newTestServer(ctx, 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, cancel := context.WithCancel(context.Background()) |
|
defer cancel() |
|
|
|
// Setup a dex server. |
|
now := func() time.Time { return t0 } |
|
|
|
refreshTokenPolicy, err := NewRefreshTokenPolicy(logger, false, "", "24h", "") |
|
if err != nil { |
|
t.Fatalf("failed to prepare rotation policy: %v", err) |
|
} |
|
refreshTokenPolicy.now = now |
|
|
|
httpServer, s := newTestServer(ctx, 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, time.Now(), 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, time.Now(), time.Now().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 } |
|
ctx, cancel := context.WithCancel(context.Background()) |
|
defer cancel() |
|
|
|
// Setup a dex server. |
|
httpServer, s := newTestServer(ctx, 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)) |
|
}) |
|
} |
|
}
|
|
|