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

Commit4bd7c7b

Browse files
authored
feat: implement oauth2 RFC 7009 token revocation endpoint (#20362)
Adds RFC 7009 token revocation endpoint
1 parent5f97ad0 commit4bd7c7b

File tree

17 files changed

+552
-57
lines changed

17 files changed

+552
-57
lines changed

‎coderd/apidoc/docs.go‎

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/apidoc/swagger.json‎

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎coderd/coderd.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,16 @@ func New(options *Options) *API {
985985
r.Post("/",api.postOAuth2ProviderAppToken())
986986
})
987987

988+
// RFC 7009 Token Revocation Endpoint
989+
r.Route("/revoke",func(r chi.Router) {
990+
r.Use(
991+
// RFC 7009 endpoint uses OAuth2 client authentication, not API key
992+
httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderAppWithOAuth2Errors(options.Database)),
993+
)
994+
// POST /revoke is the standard OAuth2 token revocation endpoint per RFC 7009
995+
r.Post("/",api.revokeOAuth2Token())
996+
})
997+
988998
// RFC 7591 Dynamic Client Registration - Public endpoint
989999
r.Post("/register",api.postOAuth2ClientRegistration())
9901000

‎coderd/database/db2sdk/db2sdk.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,9 @@ func OAuth2ProviderApp(accessURL *url.URL, dbApp database.OAuth2ProviderApp) cod
383383
}).String(),
384384
// We do not currently support DeviceAuth.
385385
DeviceAuth:"",
386+
TokenRevoke:accessURL.ResolveReference(&url.URL{
387+
Path:"/oauth2/revoke",
388+
}).String(),
386389
},
387390
}
388391
}

‎coderd/database/dbauthz/dbauthz.go‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,34 @@ var (
446446
Scope:rbac.ScopeAll,
447447
}.WithCachedASTValue()
448448

449+
subjectSystemOAuth2= rbac.Subject{
450+
Type:rbac.SubjectTypeSystemOAuth,
451+
FriendlyName:"System OAuth2",
452+
ID:uuid.Nil.String(),
453+
Roles:rbac.Roles([]rbac.Role{
454+
{
455+
Identifier: rbac.RoleIdentifier{Name:"system-oauth2"},
456+
DisplayName:"System OAuth2",
457+
Site:rbac.Permissions(map[string][]policy.Action{
458+
// OAuth2 resources - full CRUD permissions
459+
rbac.ResourceOauth2App.Type:rbac.ResourceOauth2App.AvailableActions(),
460+
rbac.ResourceOauth2AppSecret.Type:rbac.ResourceOauth2AppSecret.AvailableActions(),
461+
rbac.ResourceOauth2AppCodeToken.Type:rbac.ResourceOauth2AppCodeToken.AvailableActions(),
462+
463+
// API key permissions needed for OAuth2 token revocation
464+
rbac.ResourceApiKey.Type: {policy.ActionRead,policy.ActionDelete},
465+
466+
// Minimal read permissions that might be needed for OAuth2 operations
467+
rbac.ResourceUser.Type: {policy.ActionRead},
468+
rbac.ResourceOrganization.Type: {policy.ActionRead},
469+
}),
470+
User: []rbac.Permission{},
471+
ByOrgID:map[string]rbac.OrgPermissions{},
472+
},
473+
}),
474+
Scope:rbac.ScopeAll,
475+
}.WithCachedASTValue()
476+
449477
subjectSystemReadProvisionerDaemons= rbac.Subject{
450478
Type:rbac.SubjectTypeSystemReadProvisionerDaemons,
451479
FriendlyName:"Provisioner Daemons Reader",
@@ -643,6 +671,12 @@ func AsSystemRestricted(ctx context.Context) context.Context {
643671
returnAs(ctx,subjectSystemRestricted)
644672
}
645673

674+
// AsSystemOAuth2 returns a context with an actor that has permissions
675+
// required for OAuth2 provider operations (token revocation, device codes, registration).
676+
funcAsSystemOAuth2(ctx context.Context) context.Context {
677+
returnAs(ctx,subjectSystemOAuth2)
678+
}
679+
646680
// AsSystemReadProvisionerDaemons returns a context with an actor that has permissions
647681
// to read provisioner daemons.
648682
funcAsSystemReadProvisionerDaemons(ctx context.Context) context.Context {

‎coderd/oauth2.go‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,19 @@ func (api *API) deleteOAuth2ProviderAppTokens() http.HandlerFunc {
160160
returnoauth2provider.RevokeApp(api.Database)
161161
}
162162

163+
// @Summary Revoke OAuth2 tokens (RFC 7009).
164+
// @ID oauth2-token-revocation
165+
// @Accept x-www-form-urlencoded
166+
// @Tags Enterprise
167+
// @Param client_id formData string true "Client ID for authentication"
168+
// @Param token formData string true "The token to revoke"
169+
// @Param token_type_hint formData string false "Hint about token type (access_token or refresh_token)"
170+
// @Success 200 "Token successfully revoked"
171+
// @Router /oauth2/revoke [post]
172+
func (api*API)revokeOAuth2Token() http.HandlerFunc {
173+
returnoauth2provider.RevokeToken(api.Database,api.Logger)
174+
}
175+
163176
// @Summary OAuth2 authorization server metadata.
164177
// @ID oauth2-authorization-server-metadata
165178
// @Produce json

‎coderd/oauth2_test.go‎

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ func TestOAuth2ProviderRevoke(t *testing.T) {
720720
},
721721
},
722722
{
723-
name:"DeleteToken",
723+
name:"DeleteApp",
724724
fn:func(ctx context.Context,client*codersdk.Client,sexchangeSetup) {
725725
err:=client.RevokeOAuth2ProviderApp(ctx,s.app.ID)
726726
require.NoError(t,err)
@@ -1603,5 +1603,80 @@ func TestOAuth2RegistrationAccessToken(t *testing.T) {
16031603
})
16041604
}
16051605

1606+
// TestOAuth2CoderClient verfies a codersdk client can be used with an oauth client.
1607+
funcTestOAuth2CoderClient(t*testing.T) {
1608+
t.Parallel()
1609+
1610+
owner:=coderdtest.New(t,nil)
1611+
first:=coderdtest.CreateFirstUser(t,owner)
1612+
1613+
// Setup an oauth app
1614+
ctx:=testutil.Context(t,testutil.WaitLong)
1615+
app,err:=owner.PostOAuth2ProviderApp(ctx, codersdk.PostOAuth2ProviderAppRequest{
1616+
Name:"new-app",
1617+
CallbackURL:"http://localhost",
1618+
})
1619+
require.NoError(t,err)
1620+
1621+
appsecret,err:=owner.PostOAuth2ProviderAppSecret(ctx,app.ID)
1622+
require.NoError(t,err)
1623+
1624+
cfg:=&oauth2.Config{
1625+
ClientID:app.ID.String(),
1626+
ClientSecret:appsecret.ClientSecretFull,
1627+
Endpoint: oauth2.Endpoint{
1628+
AuthURL:app.Endpoints.Authorization,
1629+
DeviceAuthURL:app.Endpoints.DeviceAuth,
1630+
TokenURL:app.Endpoints.Token,
1631+
AuthStyle:oauth2.AuthStyleInParams,
1632+
},
1633+
RedirectURL:app.CallbackURL,
1634+
Scopes: []string{},
1635+
}
1636+
1637+
// Make a new user
1638+
client,user:=coderdtest.CreateAnotherUser(t,owner,first.OrganizationID)
1639+
1640+
// Do an OAuth2 token exchange and get a new client with an oauth token
1641+
state:=uuid.NewString()
1642+
1643+
// Get an OAuth2 code for a token exchange
1644+
code,err:=oidctest.OAuth2GetCode(
1645+
cfg.AuthCodeURL(state),
1646+
func(req*http.Request) (*http.Response,error) {
1647+
// Change to POST to simulate the form submission
1648+
req.Method=http.MethodPost
1649+
1650+
// Prevent automatic redirect following
1651+
client.HTTPClient.CheckRedirect=func(req*http.Request,via []*http.Request)error {
1652+
returnhttp.ErrUseLastResponse
1653+
}
1654+
returnclient.Request(ctx,req.Method,req.URL.String(),nil)
1655+
},
1656+
)
1657+
require.NoError(t,err)
1658+
1659+
token,err:=cfg.Exchange(ctx,code)
1660+
require.NoError(t,err)
1661+
1662+
// Use the oauth client's authentication
1663+
// TODO: The SDK could probably support this with a better syntax/api.
1664+
oauthClient:=oauth2.NewClient(ctx,oauth2.StaticTokenSource(token))
1665+
usingOauth:=codersdk.New(owner.URL)
1666+
usingOauth.HTTPClient=oauthClient
1667+
1668+
me,err:=usingOauth.User(ctx,codersdk.Me)
1669+
require.NoError(t,err)
1670+
require.Equal(t,user.ID,me.ID)
1671+
1672+
// Revoking the refresh token should prevent further access
1673+
// Revoking the refresh also invalidates the associated access token.
1674+
err=usingOauth.RevokeOAuth2Token(ctx,app.ID,token.RefreshToken)
1675+
require.NoError(t,err)
1676+
1677+
_,err=usingOauth.User(ctx,codersdk.Me)
1678+
require.Error(t,err)
1679+
}
1680+
16061681
// NOTE: OAuth2 client registration validation tests have been migrated to
16071682
// oauth2provider/validation_test.go for better separation of concerns

‎coderd/oauth2provider/registration.go‎

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ import (
2626
"github.com/coder/coder/v2/cryptorand"
2727
)
2828

29-
// Constants for OAuth2 secret generation (RFC 7591)
30-
const (
31-
secretLength=40// Length of the actual secret part
32-
displaySecretLength=6// Length of visible part in UI (last 6 characters)
33-
)
34-
3529
// CreateDynamicClientRegistration returns an http.HandlerFunc that handles POST /oauth2/register
3630
funcCreateDynamicClientRegistration(db database.Store,accessURL*url.URL,auditor*audit.Auditor,logger slog.Logger) http.HandlerFunc {
3731
returnfunc(rw http.ResponseWriter,r*http.Request) {
@@ -121,7 +115,7 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
121115
}
122116

123117
// Create client secret - parse the formatted secret to get components
124-
parsedSecret,err:=parseFormattedSecret(clientSecret)
118+
parsedSecret,err:=ParseFormattedSecret(clientSecret)
125119
iferr!=nil {
126120
writeOAuth2RegistrationError(ctx,rw,http.StatusInternalServerError,
127121
"server_error","Failed to parse generated secret")
@@ -132,7 +126,7 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
132126
_,err=db.InsertOAuth2ProviderAppSecret(dbauthz.AsSystemRestricted(ctx), database.InsertOAuth2ProviderAppSecretParams{
133127
ID:uuid.New(),
134128
CreatedAt:now,
135-
SecretPrefix: []byte(parsedSecret.prefix),
129+
SecretPrefix: []byte(parsedSecret.Prefix),
136130
HashedSecret: []byte(hashedSecret),
137131
DisplaySecret:createDisplaySecret(clientSecret),
138132
AppID:clientID,
@@ -551,27 +545,6 @@ func writeOAuth2RegistrationError(_ context.Context, rw http.ResponseWriter, sta
551545
_=json.NewEncoder(rw).Encode(errorResponse)
552546
}
553547

554-
// parsedSecret represents the components of a formatted OAuth2 secret
555-
typeparsedSecretstruct {
556-
prefixstring
557-
secretstring
558-
}
559-
560-
// parseFormattedSecret parses a formatted secret like "coder_prefix_secret"
561-
funcparseFormattedSecret(secretstring) (parsedSecret,error) {
562-
parts:=strings.Split(secret,"_")
563-
iflen(parts)!=3 {
564-
returnparsedSecret{},xerrors.Errorf("incorrect number of parts: %d",len(parts))
565-
}
566-
ifparts[0]!="coder" {
567-
returnparsedSecret{},xerrors.Errorf("incorrect scheme: %s",parts[0])
568-
}
569-
returnparsedSecret{
570-
prefix:parts[1],
571-
secret:parts[2],
572-
},nil
573-
}
574-
575548
// createDisplaySecret creates a display version of the secret showing only the last few characters
576549
funccreateDisplaySecret(secretstring)string {
577550
iflen(secret)<=displaySecretLength {

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp