Alex Tugarev 7952153237
Add verify param to OIDC start request – WEB-333 (#17576)
* [papi] Add `verified` flag to config and `ActivateClientConfig` to OIDC service

* [papi] implement `ActivateClientConfig`

* [gitpod-db/go] add `setClientConfigVerifiedFlag`

* [gitpod-db/migration] add `d_b_oidc_client_config.verified` field

* [papi] Don't deactive on UpdateClientConfig

* [gitpod-db/go] add missing `Verified` field

* On "activate" request also mark as verified.

* [gitpod-db/go] fix mapping of `Verified` field

* [papi] ensure only verified OIDC client configs can be activated

* [papi] Skip the sign-in on verify-only requests.

* [papi] fix skipped tests

* [papi] fix mapping of OIDC configs

* rename RPC method

* fix tests after adding validation of claims

* fix: activation of record should deactivate others

* fix: update should unverify the entry

* remove Debug()

* [db-migration] fix: mark active entries as verified
2023-05-16 21:09:02 +08:00

194 lines
5.6 KiB
Go

// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.
package oidc
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
goidc "github.com/coreos/go-oidc/v3/oidc"
)
func TestRoute_start(t *testing.T) {
// setup fake OIDC service
idpUrl := newFakeIdP(t)
// setup test server with client routes
baseUrl, _, configId, _ := newTestServer(t, testServerParams{
issuer: idpUrl,
state: StateParams{
ReturnToURL: "",
},
})
// go to /start
// don't follow redirect
client := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(baseUrl + "/oidc/start?id=" + configId + "&activate=true")
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
redirectUrl, err := resp.Location()
require.NoError(t, err)
require.Contains(t, redirectUrl.String(), idpUrl, "should redirect to IdP")
state := redirectUrl.Query().Get("state")
require.NotEmpty(t, state, "should contain state param")
token, _, err := new(jwt.Parser).ParseUnverified(state, jwt.MapClaims{})
require.NoError(t, err, "state param should be a JWT")
claims, ok := token.Claims.(jwt.MapClaims)
require.True(t, ok)
stateParams, ok := claims["stateParams"].(map[string]interface{})
require.True(t, ok, "JWT is missing 'stateParams'")
require.Equal(t, true, stateParams["activate"], "`activate` is missing in state")
require.Equal(t, "/", stateParams["returnTo"], "`returnTo` is missing in state")
}
func TestRoute_callback(t *testing.T) {
// setup fake OIDC service
idpUrl := newFakeIdP(t)
// setup test server with client routes
baseUrl, stateParam, _, service := newTestServer(t, testServerParams{
clientID: "client-id",
issuer: idpUrl,
state: StateParams{
ReturnToURL: "/relative/url/to/some/page",
},
})
state, err := service.encodeStateParam(*stateParam)
require.NoError(t, err)
// hit the /callback endpoint
client := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
req, err := http.NewRequest("GET", baseUrl+"/oidc/callback?code=123&state="+state, nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "state", Value: state, MaxAge: 60,
})
req.AddCookie(&http.Cookie{
Name: "nonce", Value: "111", MaxAge: 60,
})
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode, "callback should response with redirect (307)")
require.NotEmpty(t, resp.Cookies(), "missing cookies on redirect")
require.Equal(t, "test-cookie", resp.Cookies()[0].Name, "missing cookie on redirect")
url, err := resp.Location()
require.NoError(t, err)
require.Equal(t, "/relative/url/to/some/page", url.Path, "callback redirects properly")
}
func TestRoute_callback_verify_only(t *testing.T) {
// setup fake OIDC service
idpUrl := newFakeIdP(t)
// setup test server with client routes
baseUrl, stateParam, _, service := newTestServer(t, testServerParams{
clientID: "client-id",
issuer: idpUrl,
state: StateParams{
ReturnToURL: "/relative/url/to/some/page",
Verify: true,
},
})
state, err := service.encodeStateParam(*stateParam)
require.NoError(t, err)
// hit the /callback endpoint
client := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
req, err := http.NewRequest("GET", baseUrl+"/oidc/callback?code=123&state="+state, nil)
require.NoError(t, err)
req.AddCookie(&http.Cookie{
Name: "state", Value: state, MaxAge: 60,
})
req.AddCookie(&http.Cookie{
Name: "nonce", Value: "111", MaxAge: 60,
})
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode, "callback should response with redirect (307)")
require.Len(t, resp.Cookies(), 0, "unexpected session cookie on verify request")
url, err := resp.Location()
require.NoError(t, err)
require.Equal(t, "/relative/url/to/some/page", url.Path, "callback redirects properly")
}
type testServerParams struct {
issuer string
clientID string
state StateParams
}
func newTestServer(t *testing.T, params testServerParams) (url string, state *StateParams, configId string, oidcService *Service) {
router := chi.NewRouter()
oidcService, dbConn := setupOIDCServiceForTests(t)
router.Mount("/oidc", Router(oidcService))
ts := httptest.NewServer(router)
url = ts.URL
oidcConfig := &goidc.Config{
ClientID: params.clientID,
SkipClientIDCheck: true,
SkipIssuerCheck: true,
SkipExpiryCheck: true,
InsecureSkipSignatureCheck: true,
}
oauth2Config := &oauth2.Config{
ClientID: params.clientID,
ClientSecret: "secret",
Scopes: []string{goidc.ScopeOpenID, "profile", "email"},
}
clientConfig := &ClientConfig{
Issuer: params.issuer,
OAuth2Config: oauth2Config,
VerifierConfig: oidcConfig,
}
config, _ := createConfig(t, dbConn, clientConfig)
configId = config.ID.String()
stateParam := &StateParams{
ClientConfigID: configId,
ReturnToURL: params.state.ReturnToURL,
Activate: params.state.Activate,
Verify: params.state.Verify,
}
return url, stateParam, configId, oidcService
}