Movatterモバイル変換


[0]ホーム

URL:


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

Sign up
Appearance settings

feat: add best effort attempt to revoke oauth access token in external auth provider#19775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
pawbana merged 4 commits intomainfromoauth-revocation-15575
Sep 19, 2025
Merged
Show file tree
Hide file tree
Changes fromall 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 deletionscli/server.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletionscli/server_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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 deletioncoderd/apidoc/docs.go
View file
Open in desktop

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

24 changes: 23 additions & 1 deletioncoderd/apidoc/swagger.json
View file
Open in desktop

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

65 changes: 59 additions & 6 deletionscoderd/coderdtest/oidctest/idp.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand Up@@ -358,6 +375,7 @@ type With429Arguments struct {
AuthorizePath bool
KeysPath bool
UserInfoPath bool
RevokePath bool
DeviceAuth bool
DeviceVerify bool
}
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 deletionscoderd/externalauth.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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 DownExpand 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

[8]ページ先頭

©2009-2025 Movatter.jp