Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions 2 cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2692,6 +2692,8 @@ func parseExternalAuthProvidersFromEnv(prefix string, environ []string) ([]coder
provider.AuthURL = v.Value
case "TOKEN_URL":
provider.TokenURL = v.Value
case "REVOKE_URL":
provider.RevokeURL = v.Value
case "VALIDATE_URL":
provider.ValidateURL = v.Value
case "REGEX":
Expand Down
3 changes: 3 additions & 0 deletions 3 cli/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func TestReadExternalAuthProvidersFromEnv(t *testing.T) {
"CODER_EXTERNAL_AUTH_1_CLIENT_SECRET=hunter12",
"CODER_EXTERNAL_AUTH_1_TOKEN_URL=google.com",
"CODER_EXTERNAL_AUTH_1_VALIDATE_URL=bing.com",
"CODER_EXTERNAL_AUTH_1_REVOKE_URL=revoke.url",
"CODER_EXTERNAL_AUTH_1_SCOPES=repo:read repo:write",
"CODER_EXTERNAL_AUTH_1_NO_REFRESH=true",
"CODER_EXTERNAL_AUTH_1_DISPLAY_NAME=Google",
Expand All @@ -87,13 +88,15 @@ func TestReadExternalAuthProvidersFromEnv(t *testing.T) {
// Validate the first provider.
assert.Equal(t, "1", providers[0].ID)
assert.Equal(t, "gitlab", providers[0].Type)
assert.Equal(t, "", providers[0].RevokeURL)

// Validate the second provider.
assert.Equal(t, "2", providers[1].ID)
assert.Equal(t, "sid", providers[1].ClientID)
assert.Equal(t, "hunter12", providers[1].ClientSecret)
assert.Equal(t, "google.com", providers[1].TokenURL)
assert.Equal(t, "bing.com", providers[1].ValidateURL)
assert.Equal(t, "revoke.url", providers[1].RevokeURL)
assert.Equal(t, []string{"repo:read", "repo:write"}, providers[1].Scopes)
assert.Equal(t, true, providers[1].NoRefresh)
assert.Equal(t, "Google", providers[1].DisplayName)
Expand Down
26 changes: 25 additions & 1 deletion 26 coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 23 additions & 1 deletion 24 coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 59 additions & 6 deletions 65 coderd/coderdtest/oidctest/idp.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import (
"github.com/coder/coder/v2/testutil"
)

type HookRevokeTokenFn func() (httpStatus int, err error)

type token struct {
issued time.Time
email string
Expand Down Expand Up @@ -196,9 +198,11 @@ type FakeIDP struct {
// hookValidRedirectURL can be used to reject a redirect url from the
// IDP -> Application. Almost all IDPs have the concept of
// "Authorized Redirect URLs". This can be used to emulate that.
hookValidRedirectURL func(redirectURL string) error
hookUserInfo func(email string) (jwt.MapClaims, error)
hookAccessTokenJWT func(email string, exp time.Time) jwt.MapClaims
hookValidRedirectURL func(redirectURL string) error
hookUserInfo func(email string) (jwt.MapClaims, error)
hookRevokeToken HookRevokeTokenFn
revokeTokenGitHubFormat bool // GitHub doesn't follow token revocation RFC spec
hookAccessTokenJWT func(email string, exp time.Time) jwt.MapClaims
// defaultIDClaims is if a new client connects and we didn't preset
// some claims.
defaultIDClaims jwt.MapClaims
Expand Down Expand Up @@ -327,6 +331,19 @@ func WithStaticUserInfo(info jwt.MapClaims) func(*FakeIDP) {
}
}

func WithRevokeTokenRFC(revokeFunc HookRevokeTokenFn) func(*FakeIDP) {
return func(f *FakeIDP) {
f.hookRevokeToken = revokeFunc
}
}

func WithRevokeTokenGitHub(revokeFunc HookRevokeTokenFn) func(*FakeIDP) {
return func(f *FakeIDP) {
f.hookRevokeToken = revokeFunc
f.revokeTokenGitHubFormat = true
}
}

func WithDefaultIDClaims(claims jwt.MapClaims) func(*FakeIDP) {
return func(f *FakeIDP) {
f.defaultIDClaims = claims
Expand Down Expand Up @@ -358,6 +375,7 @@ type With429Arguments struct {
AuthorizePath bool
KeysPath bool
UserInfoPath bool
RevokePath bool
DeviceAuth bool
DeviceVerify bool
}
Expand Down Expand Up @@ -387,6 +405,10 @@ func With429(params With429Arguments) func(*FakeIDP) {
http.Error(rw, "429, being manually blocked (userinfo)", http.StatusTooManyRequests)
return
}
if params.RevokePath && strings.Contains(r.URL.Path, revokeTokenPath) {
http.Error(rw, "429, being manually blocked (revoke)", http.StatusTooManyRequests)
return
}
if params.DeviceAuth && strings.Contains(r.URL.Path, deviceAuth) {
http.Error(rw, "429, being manually blocked (device-auth)", http.StatusTooManyRequests)
return
Expand All @@ -408,8 +430,10 @@ const (
authorizePath = "/oauth2/authorize"
keysPath = "/oauth2/keys"
userInfoPath = "/oauth2/userinfo"
deviceAuth = "/login/device/code"
deviceVerify = "/login/device"
// nolint:gosec // It also thinks this is a secret lol
revokeTokenPath = "/oauth2/revoke"
deviceAuth = "/login/device/code"
deviceVerify = "/login/device"
)

func NewFakeIDP(t testing.TB, opts ...FakeIDPOpt) *FakeIDP {
Expand Down Expand Up @@ -486,6 +510,7 @@ func (f *FakeIDP) updateIssuerURL(t testing.TB, issuer string) {
TokenURL: u.ResolveReference(&url.URL{Path: tokenPath}).String(),
JWKSURL: u.ResolveReference(&url.URL{Path: keysPath}).String(),
UserInfoURL: u.ResolveReference(&url.URL{Path: userInfoPath}).String(),
RevokeURL: u.ResolveReference(&url.URL{Path: revokeTokenPath}).String(),
DeviceCodeURL: u.ResolveReference(&url.URL{Path: deviceAuth}).String(),
Algorithms: []string{
"RS256",
Expand Down Expand Up @@ -756,6 +781,7 @@ type ProviderJSON struct {
TokenURL string `json:"token_endpoint"`
JWKSURL string `json:"jwks_uri"`
UserInfoURL string `json:"userinfo_endpoint"`
RevokeURL string `json:"revocation_endpoint"`
DeviceCodeURL string `json:"device_authorization_endpoint"`
Algorithms []string `json:"id_token_signing_alg_values_supported"`
// This is custom
Expand Down Expand Up @@ -1146,6 +1172,29 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
_ = json.NewEncoder(rw).Encode(claims)
}))

mux.Handle(revokeTokenPath, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if f.revokeTokenGitHubFormat {
u, p, ok := r.BasicAuth()
if !ok || !(u == f.clientID && p == f.clientSecret) {
httpError(rw, http.StatusForbidden, xerrors.Errorf("basic auth failed"))
return
}
} else {
_, ok := validateMW(rw, r)
if !ok {
httpError(rw, http.StatusForbidden, xerrors.Errorf("token validation failed"))
return
}
}

code, err := f.hookRevokeToken()
if err != nil {
httpError(rw, code, xerrors.Errorf("hook err: %w", err))
return
}
httpapi.Write(r.Context(), rw, code, "")
}))

// There is almost no difference between this and /userinfo.
// The main tweak is that this route is "mounted" vs "handle" because "/userinfo"
// should be strict, and this one needs to handle sub routes.
Expand Down Expand Up @@ -1474,12 +1523,16 @@ func (f *FakeIDP) ExternalAuthConfig(t testing.TB, id string, custom *ExternalAu
DisplayName: id,
InstrumentedOAuth2Config: oauthCfg,
ID: id,
ClientID: f.clientID,
ClientSecret: f.clientSecret,
// No defaults for these fields by omitting the type
Type: "",
DisplayIcon: f.WellknownConfig().UserInfoURL,
// Omit the /user for the validate so we can easily append to it when modifying
// the cfg for advanced tests.
ValidateURL: f.locked.IssuerURL().ResolveReference(&url.URL{Path: "/external-auth-validate/"}).String(),
ValidateURL: f.locked.IssuerURL().ResolveReference(&url.URL{Path: "/external-auth-validate/"}).String(),
RevokeURL: f.locked.IssuerURL().ResolveReference(&url.URL{Path: revokeTokenPath}).String(),
RevokeTimeout: 1 * time.Second,
DeviceAuth: &externalauth.DeviceAuth{
Config: oauthCfg,
ClientID: f.clientID,
Expand Down
46 changes: 35 additions & 11 deletions 46 coderd/externalauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,37 @@ func (api *API) externalAuthByID(w http.ResponseWriter, r *http.Request) {
// @ID delete-external-auth-user-link-by-id
// @Security CoderSessionToken
// @Tags Git
// @Success 200
// @Produce json
// @Param externalauth path string true "Git Provider ID" format(string)
// @Success 200 {object} codersdk.DeleteExternalAuthByIDResponse
// @Router /external-auth/{externalauth} [delete]
func (api *API) deleteExternalAuthByID(w http.ResponseWriter, r *http.Request) {
config := httpmw.ExternalAuthParam(r)
apiKey := httpmw.APIKey(r)
ctx := r.Context()

err := api.Database.DeleteExternalAuthLink(ctx, database.DeleteExternalAuthLinkParams{
link, err := api.Database.GetExternalAuthLink(ctx, database.GetExternalAuthLinkParams{
ProviderID: config.ID,
UserID: apiKey.UserID,
})
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
if errors.Is(err, sql.ErrNoRows) {
httpapi.ResourceNotFound(w)
return
}
httpapi.Write(ctx, w, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to get external auth link during deletion.",
Detail: err.Error(),
})
return
}

err = api.Database.DeleteExternalAuthLink(ctx, database.DeleteExternalAuthLinkParams{
ProviderID: config.ID,
UserID: apiKey.UserID,
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
httpapi.ResourceNotFound(w)
return
}
Expand All @@ -109,7 +126,13 @@ func (api *API) deleteExternalAuthByID(w http.ResponseWriter, r *http.Request) {
return
}

httpapi.Write(ctx, w, http.StatusOK, "OK")
ok, err := config.RevokeToken(ctx, link)
resp := codersdk.DeleteExternalAuthByIDResponse{TokenRevoked: ok}

if err != nil {
resp.TokenRevocationError = err.Error()
}
httpapi.Write(ctx, w, http.StatusOK, resp)
}

// @Summary Post external auth device by ID
Expand Down Expand Up @@ -394,13 +417,14 @@ func ExternalAuthConfigs(auths []*externalauth.Config) []codersdk.ExternalAuthLi

func ExternalAuthConfig(cfg *externalauth.Config) codersdk.ExternalAuthLinkProvider {
return codersdk.ExternalAuthLinkProvider{
ID: cfg.ID,
Type: cfg.Type,
Device: cfg.DeviceAuth != nil,
DisplayName: cfg.DisplayName,
DisplayIcon: cfg.DisplayIcon,
AllowRefresh: !cfg.NoRefresh,
AllowValidate: cfg.ValidateURL != "",
ID: cfg.ID,
Type: cfg.Type,
Device: cfg.DeviceAuth != nil,
DisplayName: cfg.DisplayName,
DisplayIcon: cfg.DisplayIcon,
AllowRefresh: !cfg.NoRefresh,
AllowValidate: cfg.ValidateURL != "",
SupportsRevocation: cfg.RevokeURL != "",
}
}

Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.