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: implement oauth2 RFC 7009 token revocation endpoint#20362

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
Emyrk merged 11 commits intomainfromstevenmasley/revoke_oauth
Oct 22, 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
42 changes: 42 additions & 0 deletionscoderd/apidoc/docs.go
View file
Open in desktop

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

38 changes: 38 additions & 0 deletionscoderd/apidoc/swagger.json
View file
Open in desktop

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

10 changes: 10 additions & 0 deletionscoderd/coderd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -985,6 +985,16 @@ func New(options *Options) *API {
r.Post("/", api.postOAuth2ProviderAppToken())
})

// RFC 7009 Token Revocation Endpoint
r.Route("/revoke", func(r chi.Router) {
r.Use(
// RFC 7009 endpoint uses OAuth2 client authentication, not API key
httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderAppWithOAuth2Errors(options.Database)),
)
// POST /revoke is the standard OAuth2 token revocation endpoint per RFC 7009
r.Post("/", api.revokeOAuth2Token())
})

// RFC 7591 Dynamic Client Registration - Public endpoint
r.Post("/register", api.postOAuth2ClientRegistration())

Expand Down
3 changes: 3 additions & 0 deletionscoderd/database/db2sdk/db2sdk.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -383,6 +383,9 @@ func OAuth2ProviderApp(accessURL *url.URL, dbApp database.OAuth2ProviderApp) cod
}).String(),
// We do not currently support DeviceAuth.
DeviceAuth: "",
TokenRevoke: accessURL.ResolveReference(&url.URL{
Path: "/oauth2/revoke",
}).String(),
},
}
}
Expand Down
34 changes: 34 additions & 0 deletionscoderd/database/dbauthz/dbauthz.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -446,6 +446,34 @@ var (
Scope: rbac.ScopeAll,
}.WithCachedASTValue()

subjectSystemOAuth2 = rbac.Subject{
Type: rbac.SubjectTypeSystemOAuth,
FriendlyName: "System OAuth2",
ID: uuid.Nil.String(),
Roles: rbac.Roles([]rbac.Role{
{
Identifier: rbac.RoleIdentifier{Name: "system-oauth2"},
DisplayName: "System OAuth2",
Site: rbac.Permissions(map[string][]policy.Action{
// OAuth2 resources - full CRUD permissions
rbac.ResourceOauth2App.Type: rbac.ResourceOauth2App.AvailableActions(),
rbac.ResourceOauth2AppSecret.Type: rbac.ResourceOauth2AppSecret.AvailableActions(),
rbac.ResourceOauth2AppCodeToken.Type: rbac.ResourceOauth2AppCodeToken.AvailableActions(),

// API key permissions needed for OAuth2 token revocation
rbac.ResourceApiKey.Type: {policy.ActionRead, policy.ActionDelete},

// Minimal read permissions that might be needed for OAuth2 operations
rbac.ResourceUser.Type: {policy.ActionRead},
rbac.ResourceOrganization.Type: {policy.ActionRead},
}),
User: []rbac.Permission{},
ByOrgID: map[string]rbac.OrgPermissions{},
},
}),
Scope: rbac.ScopeAll,
}.WithCachedASTValue()

subjectSystemReadProvisionerDaemons = rbac.Subject{
Type: rbac.SubjectTypeSystemReadProvisionerDaemons,
FriendlyName: "Provisioner Daemons Reader",
Expand DownExpand Up@@ -643,6 +671,12 @@ func AsSystemRestricted(ctx context.Context) context.Context {
return As(ctx, subjectSystemRestricted)
}

// AsSystemOAuth2 returns a context with an actor that has permissions
// required for OAuth2 provider operations (token revocation, device codes, registration).
func AsSystemOAuth2(ctx context.Context) context.Context {
return As(ctx, subjectSystemOAuth2)
}

// AsSystemReadProvisionerDaemons returns a context with an actor that has permissions
// to read provisioner daemons.
func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context {
Expand Down
13 changes: 13 additions & 0 deletionscoderd/oauth2.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -160,6 +160,19 @@ func (api *API) deleteOAuth2ProviderAppTokens() http.HandlerFunc {
return oauth2provider.RevokeApp(api.Database)
}

// @Summary Revoke OAuth2 tokens (RFC 7009).
// @ID oauth2-token-revocation
// @Accept x-www-form-urlencoded
// @Tags Enterprise
// @Param client_id formData string true "Client ID for authentication"
// @Param token formData string true "The token to revoke"
// @Param token_type_hint formData string false "Hint about token type (access_token or refresh_token)"
// @Success 200 "Token successfully revoked"
// @Router /oauth2/revoke [post]
func (api *API) revokeOAuth2Token() http.HandlerFunc {
return oauth2provider.RevokeToken(api.Database, api.Logger)
}

// @Summary OAuth2 authorization server metadata.
// @ID oauth2-authorization-server-metadata
// @Produce json
Expand Down
77 changes: 76 additions & 1 deletioncoderd/oauth2_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -720,7 +720,7 @@ func TestOAuth2ProviderRevoke(t *testing.T) {
},
},
{
name: "DeleteToken",
name: "DeleteApp",
fn: func(ctx context.Context, client *codersdk.Client, s exchangeSetup) {
err := client.RevokeOAuth2ProviderApp(ctx, s.app.ID)
require.NoError(t, err)
Expand DownExpand Up@@ -1603,5 +1603,80 @@ func TestOAuth2RegistrationAccessToken(t *testing.T) {
})
}

// TestOAuth2CoderClient verfies a codersdk client can be used with an oauth client.
func TestOAuth2CoderClient(t *testing.T) {
t.Parallel()

owner := coderdtest.New(t, nil)
first := coderdtest.CreateFirstUser(t, owner)

// Setup an oauth app
ctx := testutil.Context(t, testutil.WaitLong)
app, err := owner.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
Name: "new-app",
CallbackURL: "http://localhost",
})
require.NoError(t, err)

appsecret, err := owner.PostOAuth2ProviderAppSecret(ctx, app.ID)
require.NoError(t, err)

cfg := &oauth2.Config{
ClientID: app.ID.String(),
ClientSecret: appsecret.ClientSecretFull,
Endpoint: oauth2.Endpoint{
AuthURL: app.Endpoints.Authorization,
DeviceAuthURL: app.Endpoints.DeviceAuth,
TokenURL: app.Endpoints.Token,
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: app.CallbackURL,
Scopes: []string{},
}

// Make a new user
client, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID)

// Do an OAuth2 token exchange and get a new client with an oauth token
state := uuid.NewString()

// Get an OAuth2 code for a token exchange
code, err := oidctest.OAuth2GetCode(
cfg.AuthCodeURL(state),
func(req *http.Request) (*http.Response, error) {
// Change to POST to simulate the form submission
req.Method = http.MethodPost

// Prevent automatic redirect following
client.HTTPClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
return client.Request(ctx, req.Method, req.URL.String(), nil)
},
)
require.NoError(t, err)

token, err := cfg.Exchange(ctx, code)
require.NoError(t, err)

// Use the oauth client's authentication
// TODO: The SDK could probably support this with a better syntax/api.
oauthClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token))
usingOauth := codersdk.New(owner.URL)
usingOauth.HTTPClient = oauthClient

me, err := usingOauth.User(ctx, codersdk.Me)
require.NoError(t, err)
require.Equal(t, user.ID, me.ID)

// Revoking the refresh token should prevent further access
// Revoking the refresh also invalidates the associated access token.
err = usingOauth.RevokeOAuth2Token(ctx, app.ID, token.RefreshToken)
require.NoError(t, err)

_, err = usingOauth.User(ctx, codersdk.Me)
require.Error(t, err)
}

// NOTE: OAuth2 client registration validation tests have been migrated to
// oauth2provider/validation_test.go for better separation of concerns
31 changes: 2 additions & 29 deletionscoderd/oauth2provider/registration.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,12 +26,6 @@ import (
"github.com/coder/coder/v2/cryptorand"
)

// Constants for OAuth2 secret generation (RFC 7591)
const (
secretLength = 40 // Length of the actual secret part
displaySecretLength = 6 // Length of visible part in UI (last 6 characters)
)

// CreateDynamicClientRegistration returns an http.HandlerFunc that handles POST /oauth2/register
func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, auditor *audit.Auditor, logger slog.Logger) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
Expand DownExpand Up@@ -121,7 +115,7 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
}

// Create client secret - parse the formatted secret to get components
parsedSecret, err :=parseFormattedSecret(clientSecret)
parsedSecret, err :=ParseFormattedSecret(clientSecret)
if err != nil {
writeOAuth2RegistrationError(ctx, rw, http.StatusInternalServerError,
"server_error", "Failed to parse generated secret")
Expand All@@ -132,7 +126,7 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
_, err = db.InsertOAuth2ProviderAppSecret(dbauthz.AsSystemRestricted(ctx), database.InsertOAuth2ProviderAppSecretParams{
ID: uuid.New(),
CreatedAt: now,
SecretPrefix: []byte(parsedSecret.prefix),
SecretPrefix: []byte(parsedSecret.Prefix),
HashedSecret: []byte(hashedSecret),
DisplaySecret: createDisplaySecret(clientSecret),
AppID: clientID,
Expand DownExpand Up@@ -551,27 +545,6 @@ func writeOAuth2RegistrationError(_ context.Context, rw http.ResponseWriter, sta
_ = json.NewEncoder(rw).Encode(errorResponse)
}

// parsedSecret represents the components of a formatted OAuth2 secret
type parsedSecret struct {
prefix string
secret string
}

// parseFormattedSecret parses a formatted secret like "coder_prefix_secret"
func parseFormattedSecret(secret string) (parsedSecret, error) {
parts := strings.Split(secret, "_")
if len(parts) != 3 {
return parsedSecret{}, xerrors.Errorf("incorrect number of parts: %d", len(parts))
}
if parts[0] != "coder" {
return parsedSecret{}, xerrors.Errorf("incorrect scheme: %s", parts[0])
}
return parsedSecret{
prefix: parts[1],
secret: parts[2],
}, nil
}

// createDisplaySecret creates a display version of the secret showing only the last few characters
func createDisplaySecret(secret string) string {
if len(secret) <= displaySecretLength {
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp