@@ -0,0 +1,237 @@
13+ "github.com/golang-jwt/jwt/v5"
14+ "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
15+ . "github.com/onsi/gomega"
19+ dttSSOTestClientID = "redash-gate"
20+ // Shorter than 32 bytes, and contains characters golang.org/x/oauth2 would
21+ // URL-escape in Basic auth.
22+ dttSSOTestSecret = "s3cr+t/abc=="
25+// dttSSOTestServer mimics the SSO's token and check_token endpoints.
26+type dttSSOTestServer struct {
28+ claims map[string]interface{}
31+ validTokens map[string]bool
35+func newDTTSSOTestServer() *dttSSOTestServer {
36+ s := &dttSSOTestServer{
37+ signingKey: []byte(dttSSOTestSecret),
38+ validTokens: map[string]bool{},
40+ s.Server = httptest.NewServer(http.HandlerFunc(s.handle))
41+ u, _ := url.Parse(s.URL)
42+ s.claims = map[string]interface{}{
43+ "iss": "http://" + u.Host,
44+ "aud": dttSSOTestClientID,
45+ "sub": "0b5c1e1a-1111-2222-3333-444455556666",
46+ "user_id": "0b5c1e1a-1111-2222-3333-444455556666",
47+ "email": "admin@example.com",
49+ "exp": time.Now().Add(time.Hour).Unix(),
54+func (s *dttSSOTestServer) handle(w http.ResponseWriter, r *http.Request) {
57+ want := "Basic " + base64.StdEncoding.EncodeToString([]byte(dttSSOTestClientID+":"+dttSSOTestSecret))
58+ if r.Header.Get("Authorization") != want {
59+ w.WriteHeader(http.StatusUnauthorized)
63+ s.lastForm = r.PostForm
64+ if r.PostForm.Get("grant_type") == "refresh_token" && !s.validTokens["refresh-"+r.PostForm.Get("refresh_token")] {
65+ w.WriteHeader(http.StatusBadRequest)
66+ _, _ = w.Write([]byte(`{"error":"invalid_grant","error_description":"Invalid refresh token"}`))
70+ access := "access-" + string(rune('0'+s.tokenCounter))
71+ refresh := "refresh-" + string(rune('0'+s.tokenCounter))
72+ s.validTokens[access] = true
73+ s.validTokens["refresh-"+refresh] = true
74+ w.Header().Set("Content-Type", "application/json")
75+ _ = json.NewEncoder(w).Encode(map[string]interface{}{
76+ "access_token": access,
77+ "token_type": "bearer",
79+ "refresh_token": refresh,
80+ "id_token": s.idToken(),
83+ case "/oauth/check_token":
84+ token := r.Header.Get("Authorization")
85+ if len(token) > len("Bearer ") && s.validTokens[token[len("Bearer "):]] {
86+ _, _ = w.Write([]byte(`{"user_id":"0b5c1e1a-1111-2222-3333-444455556666","real_user_id":null,"issued_by":"redash-gate"}`))
89+ w.WriteHeader(http.StatusUnauthorized)
90+ _, _ = w.Write([]byte("Invalid access token"))
92+ w.WriteHeader(http.StatusNotFound)
96+func (s *dttSSOTestServer) idToken() string {
100+ raw, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(s.claims)).SignedString(s.signingKey)
107+func testDTTSSOProvider(server *dttSSOTestServer) *DTTSSOProvider {
108+ p := NewDTTSSOProvider(&ProviderData{ClientID: dttSSOTestClientID, ClientSecret: dttSSOTestSecret})
109+ u, _ := url.Parse(server.URL)
110+ updateURL(p.LoginURL, u.Host)
111+ updateURL(p.RedeemURL, u.Host)
112+ updateURL(p.ValidateURL, u.Host)
116+func TestNewDTTSSOProvider(t *testing.T) {
119+ providerData := NewDTTSSOProvider(&ProviderData{}).Data()
120+ g.Expect(providerData.ProviderName).To(Equal("DTT SSO"))
121+ g.Expect(providerData.LoginURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/authorize"))
122+ g.Expect(providerData.RedeemURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/token"))
123+ g.Expect(providerData.ValidateURL.String()).To(Equal("https://auth.thebestagent.pro/oauth/check_token"))
124+ g.Expect(providerData.Scope).To(Equal("openid"))
127+func TestDTTSSOProviderRedeem(t *testing.T) {
129+ server := newDTTSSOTestServer()
130+ defer server.Close()
131+ p := testDTTSSOProvider(server)
133+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
134+ g.Expect(err).ToNot(HaveOccurred())
135+ g.Expect(s.Email).To(Equal("admin@example.com"))
136+ g.Expect(s.User).To(Equal("0b5c1e1a-1111-2222-3333-444455556666"))
137+ g.Expect(s.Groups).To(Equal([]string{"dtt_admin"}))
138+ g.Expect(s.AccessToken).To(Equal("access-1"))
139+ g.Expect(s.RefreshToken).To(Equal("refresh-1"))
140+ g.Expect(s.IDToken).ToNot(BeEmpty())
141+ g.Expect(s.ExpiresOn).ToNot(BeNil())
142+ g.Expect(*s.ExpiresOn).To(BeTemporally("~", time.Now().Add(time.Hour), time.Minute))
144+ g.Expect(server.lastForm.Get("grant_type")).To(Equal("authorization_code"))
145+ g.Expect(server.lastForm.Get("code")).To(Equal("the-code"))
146+ g.Expect(server.lastForm.Get("redirect_uri")).To(Equal("https://redash.example.com/oauth2/callback"))
149+func TestDTTSSOProviderRedeemRejectsBadIDTokens(t *testing.T) {
150+ testCases := map[string]func(s *dttSSOTestServer){
151+ "signed with another secret": func(s *dttSSOTestServer) { s.signingKey = []byte("some-other-client-secret") },
152+ "other audience": func(s *dttSSOTestServer) { s.claims["aud"] = "frappe-gate" },
153+ "other issuer": func(s *dttSSOTestServer) { s.claims["iss"] = "https://evil.example.com" },
154+ "expired": func(s *dttSSOTestServer) { s.claims["exp"] = time.Now().Add(-time.Minute).Unix() },
155+ "no email": func(s *dttSSOTestServer) { delete(s.claims, "email") },
156+ "no id_token": func(s *dttSSOTestServer) { s.claims = nil },
158+ for name, tamper := range testCases {
159+ t.Run(name, func(t *testing.T) {
161+ server := newDTTSSOTestServer()
162+ defer server.Close()
165+ _, err := testDTTSSOProvider(server).Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
166+ g.Expect(err).To(HaveOccurred())
171+func TestDTTSSOProviderRedeemRejectsUnsignedIDToken(t *testing.T) {
173+ server := newDTTSSOTestServer()
174+ defer server.Close()
175+ p := testDTTSSOProvider(server)
177+ header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none"}`))
178+ payload, _ := json.Marshal(server.claims)
179+ unsigned := header + "." + base64.RawURLEncoding.EncodeToString(payload) + "."
181+ _, err := p.verifyIDToken(unsigned, dttSSOTestSecret)
182+ g.Expect(err).To(HaveOccurred())
185+func TestDTTSSOProviderRefreshSession(t *testing.T) {
187+ server := newDTTSSOTestServer()
188+ defer server.Close()
189+ p := testDTTSSOProvider(server)
191+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
192+ g.Expect(err).ToNot(HaveOccurred())
193+ nonce := []byte("keep-me")
196+ server.claims["role"] = "agent"
197+ server.claims["email"] = "renamed@example.com"
198+ refreshed, err := p.RefreshSession(context.Background(), s)
199+ g.Expect(err).ToNot(HaveOccurred())
200+ g.Expect(refreshed).To(BeTrue())
201+ g.Expect(s.AccessToken).To(Equal("access-2"))
202+ g.Expect(s.RefreshToken).To(Equal("refresh-2"))
203+ g.Expect(s.Groups).To(Equal([]string{"agent"}))
204+ g.Expect(s.Email).To(Equal("renamed@example.com"))
205+ g.Expect(s.Nonce).To(Equal(nonce))
206+ g.Expect(server.lastForm.Get("grant_type")).To(Equal("refresh_token"))
207+ g.Expect(server.lastForm.Get("refresh_token")).To(Equal("refresh-1"))
210+func TestDTTSSOProviderRefreshSessionFailures(t *testing.T) {
212+ server := newDTTSSOTestServer()
213+ defer server.Close()
214+ p := testDTTSSOProvider(server)
216+ refreshed, err := p.RefreshSession(context.Background(), &sessions.SessionState{})
217+ g.Expect(err).ToNot(HaveOccurred())
218+ g.Expect(refreshed).To(BeFalse())
220+ refreshed, err = p.RefreshSession(context.Background(), &sessions.SessionState{RefreshToken: "revoked"})
221+ g.Expect(err).To(HaveOccurred())
222+ g.Expect(refreshed).To(BeFalse())
225+func TestDTTSSOProviderValidateSession(t *testing.T) {
227+ server := newDTTSSOTestServer()
228+ defer server.Close()
229+ p := testDTTSSOProvider(server)
231+ s, err := p.Redeem(context.Background(), "https://redash.example.com/oauth2/callback", "the-code", "")
232+ g.Expect(err).ToNot(HaveOccurred())
233+ g.Expect(p.ValidateSession(context.Background(), s)).To(BeTrue())
235+ delete(server.validTokens, s.AccessToken)
236+ g.Expect(p.ValidateSession(context.Background(), s)).To(BeFalse())