Skip to content

Commit 40e5ad5

Browse files
authored
feat: make OAuth2 provider not enterprise-only (coder#12732)
1 parent 60f3351 commit 40e5ad5

File tree

14 files changed

+92
-168
lines changed

14 files changed

+92
-168
lines changed

coderd/coderd.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,34 @@ func New(options *Options) *API {
689689
})
690690
}
691691

692+
// OAuth2 linking routes do not make sense under the /api/v2 path. These are
693+
// for an external application to use Coder as an OAuth2 provider, not for
694+
// logging into Coder with an external OAuth2 provider.
695+
r.Route("/oauth2", func(r chi.Router) {
696+
r.Use(
697+
api.oAuth2ProviderMiddleware,
698+
// Fetch the app as system because in the /tokens route there will be no
699+
// authenticated user.
700+
httpmw.AsAuthzSystem(httpmw.ExtractOAuth2ProviderApp(options.Database)),
701+
)
702+
r.Route("/authorize", func(r chi.Router) {
703+
r.Use(apiKeyMiddlewareRedirect)
704+
r.Get("/", api.getOAuth2ProviderAppAuthorize())
705+
})
706+
r.Route("/tokens", func(r chi.Router) {
707+
r.Group(func(r chi.Router) {
708+
r.Use(apiKeyMiddleware)
709+
// DELETE on /tokens is not part of the OAuth2 spec. It is our own
710+
// route used to revoke permissions from an application. It is here for
711+
// parity with POST on /tokens.
712+
r.Delete("/", api.deleteOAuth2ProviderAppTokens())
713+
})
714+
// The POST /tokens endpoint will be called from an unauthorized client so
715+
// we cannot require an API key.
716+
r.Post("/", api.postOAuth2ProviderAppToken())
717+
})
718+
})
719+
692720
r.Route("/api/v2", func(r chi.Router) {
693721
api.APIHandler = r
694722

@@ -1098,6 +1126,34 @@ func New(options *Options) *API {
10981126
}
10991127
r.Method("GET", "/expvar", expvar.Handler()) // contains DERP metrics as well as cmdline and memstats
11001128
})
1129+
// Manage OAuth2 applications that can use Coder as an OAuth2 provider.
1130+
r.Route("/oauth2-provider", func(r chi.Router) {
1131+
r.Use(
1132+
apiKeyMiddleware,
1133+
api.oAuth2ProviderMiddleware,
1134+
)
1135+
r.Route("/apps", func(r chi.Router) {
1136+
r.Get("/", api.oAuth2ProviderApps)
1137+
r.Post("/", api.postOAuth2ProviderApp)
1138+
1139+
r.Route("/{app}", func(r chi.Router) {
1140+
r.Use(httpmw.ExtractOAuth2ProviderApp(options.Database))
1141+
r.Get("/", api.oAuth2ProviderApp)
1142+
r.Put("/", api.putOAuth2ProviderApp)
1143+
r.Delete("/", api.deleteOAuth2ProviderApp)
1144+
1145+
r.Route("/secrets", func(r chi.Router) {
1146+
r.Get("/", api.oAuth2ProviderAppSecrets)
1147+
r.Post("/", api.postOAuth2ProviderAppSecret)
1148+
1149+
r.Route("/{secretID}", func(r chi.Router) {
1150+
r.Use(httpmw.ExtractOAuth2ProviderAppSecret(options.Database))
1151+
r.Delete("/", api.deleteOAuth2ProviderAppSecret)
1152+
})
1153+
})
1154+
})
1155+
})
1156+
})
11011157
})
11021158

11031159
if options.SwaggerEndpoint {

enterprise/coderd/oauth2.go renamed to coderd/oauth2.go

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import (
1313
"github.com/coder/coder/v2/coderd/database/dbtime"
1414
"github.com/coder/coder/v2/coderd/httpapi"
1515
"github.com/coder/coder/v2/coderd/httpmw"
16+
"github.com/coder/coder/v2/coderd/identityprovider"
1617
"github.com/coder/coder/v2/codersdk"
17-
"github.com/coder/coder/v2/enterprise/coderd/identityprovider"
1818
)
1919

20-
func (api *API) oAuth2ProviderMiddleware(next http.Handler) http.Handler {
20+
func (*API) oAuth2ProviderMiddleware(next http.Handler) http.Handler {
2121
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
2222
if !buildinfo.IsDev() {
2323
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
@@ -26,17 +26,6 @@ func (api *API) oAuth2ProviderMiddleware(next http.Handler) http.Handler {
2626
return
2727
}
2828

29-
api.entitlementsMu.RLock()
30-
entitled := api.entitlements.Features[codersdk.FeatureOAuth2Provider].Entitlement != codersdk.EntitlementNotEntitled
31-
api.entitlementsMu.RUnlock()
32-
33-
if !entitled {
34-
httpapi.Write(r.Context(), rw, http.StatusForbidden, codersdk.Response{
35-
Message: "OAuth2 provider is an Enterprise feature. Contact sales!",
36-
})
37-
return
38-
}
39-
4029
next.ServeHTTP(rw, r)
4130
})
4231
}
@@ -111,7 +100,7 @@ func (api *API) oAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
111100
func (api *API) postOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
112101
var (
113102
ctx = r.Context()
114-
auditor = api.AGPL.Auditor.Load()
103+
auditor = api.Auditor.Load()
115104
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderApp](rw, &audit.RequestParams{
116105
Audit: *auditor,
117106
Log: api.Logger,
@@ -157,7 +146,7 @@ func (api *API) putOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request) {
157146
var (
158147
ctx = r.Context()
159148
app = httpmw.OAuth2ProviderApp(r)
160-
auditor = api.AGPL.Auditor.Load()
149+
auditor = api.Auditor.Load()
161150
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderApp](rw, &audit.RequestParams{
162151
Audit: *auditor,
163152
Log: api.Logger,
@@ -200,7 +189,7 @@ func (api *API) deleteOAuth2ProviderApp(rw http.ResponseWriter, r *http.Request)
200189
var (
201190
ctx = r.Context()
202191
app = httpmw.OAuth2ProviderApp(r)
203-
auditor = api.AGPL.Auditor.Load()
192+
auditor = api.Auditor.Load()
204193
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderApp](rw, &audit.RequestParams{
205194
Audit: *auditor,
206195
Log: api.Logger,
@@ -263,7 +252,7 @@ func (api *API) postOAuth2ProviderAppSecret(rw http.ResponseWriter, r *http.Requ
263252
var (
264253
ctx = r.Context()
265254
app = httpmw.OAuth2ProviderApp(r)
266-
auditor = api.AGPL.Auditor.Load()
255+
auditor = api.Auditor.Load()
267256
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderAppSecret](rw, &audit.RequestParams{
268257
Audit: *auditor,
269258
Log: api.Logger,
@@ -317,7 +306,7 @@ func (api *API) deleteOAuth2ProviderAppSecret(rw http.ResponseWriter, r *http.Re
317306
var (
318307
ctx = r.Context()
319308
secret = httpmw.OAuth2ProviderAppSecret(r)
320-
auditor = api.AGPL.Auditor.Load()
309+
auditor = api.Auditor.Load()
321310
aReq, commitAudit = audit.InitRequest[database.OAuth2ProviderAppSecret](rw, &audit.RequestParams{
322311
Audit: *auditor,
323312
Log: api.Logger,

enterprise/coderd/oauth2_test.go renamed to coderd/oauth2_test.go

Lines changed: 28 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,10 @@ import (
1919
"github.com/coder/coder/v2/coderd/database"
2020
"github.com/coder/coder/v2/coderd/database/dbtestutil"
2121
"github.com/coder/coder/v2/coderd/database/dbtime"
22+
"github.com/coder/coder/v2/coderd/identityprovider"
2223
"github.com/coder/coder/v2/coderd/userpassword"
2324
"github.com/coder/coder/v2/coderd/util/ptr"
2425
"github.com/coder/coder/v2/codersdk"
25-
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
26-
"github.com/coder/coder/v2/enterprise/coderd/identityprovider"
27-
"github.com/coder/coder/v2/enterprise/coderd/license"
2826
"github.com/coder/coder/v2/testutil"
2927
)
3028

@@ -34,11 +32,8 @@ func TestOAuth2ProviderApps(t *testing.T) {
3432
t.Run("Validation", func(t *testing.T) {
3533
t.Parallel()
3634

37-
client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
38-
Features: license.Features{
39-
codersdk.FeatureOAuth2Provider: 1,
40-
},
41-
}})
35+
client := coderdtest.New(t, nil)
36+
_ = coderdtest.CreateFirstUser(t, client)
4237

4338
topCtx := testutil.Context(t, testutil.WaitLong)
4439

@@ -178,11 +173,8 @@ func TestOAuth2ProviderApps(t *testing.T) {
178173
t.Run("DeleteNonExisting", func(t *testing.T) {
179174
t.Parallel()
180175

181-
client, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
182-
Features: license.Features{
183-
codersdk.FeatureOAuth2Provider: 1,
184-
},
185-
}})
176+
client := coderdtest.New(t, nil)
177+
owner := coderdtest.CreateFirstUser(t, client)
186178
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
187179

188180
ctx := testutil.Context(t, testutil.WaitLong)
@@ -194,11 +186,8 @@ func TestOAuth2ProviderApps(t *testing.T) {
194186
t.Run("OK", func(t *testing.T) {
195187
t.Parallel()
196188

197-
client, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
198-
Features: license.Features{
199-
codersdk.FeatureOAuth2Provider: 1,
200-
},
201-
}})
189+
client := coderdtest.New(t, nil)
190+
owner := coderdtest.CreateFirstUser(t, client)
202191
another, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
203192

204193
ctx := testutil.Context(t, testutil.WaitLong)
@@ -269,11 +258,8 @@ func TestOAuth2ProviderApps(t *testing.T) {
269258

270259
t.Run("ByUser", func(t *testing.T) {
271260
t.Parallel()
272-
client, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
273-
Features: license.Features{
274-
codersdk.FeatureOAuth2Provider: 1,
275-
},
276-
}})
261+
client := coderdtest.New(t, nil)
262+
owner := coderdtest.CreateFirstUser(t, client)
277263
another, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
278264
ctx := testutil.Context(t, testutil.WaitLong)
279265
_ = generateApps(ctx, t, client, "by-user")
@@ -288,11 +274,8 @@ func TestOAuth2ProviderApps(t *testing.T) {
288274
func TestOAuth2ProviderAppSecrets(t *testing.T) {
289275
t.Parallel()
290276

291-
client, _ := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
292-
Features: license.Features{
293-
codersdk.FeatureOAuth2Provider: 1,
294-
},
295-
}})
277+
client := coderdtest.New(t, nil)
278+
_ = coderdtest.CreateFirstUser(t, client)
296279

297280
topCtx := testutil.Context(t, testutil.WaitLong)
298281

@@ -383,17 +366,11 @@ func TestOAuth2ProviderTokenExchange(t *testing.T) {
383366
t.Parallel()
384367

385368
db, pubsub := dbtestutil.NewDB(t)
386-
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
387-
Options: &coderdtest.Options{
388-
Database: db,
389-
Pubsub: pubsub,
390-
},
391-
LicenseOptions: &coderdenttest.LicenseOptions{
392-
Features: license.Features{
393-
codersdk.FeatureOAuth2Provider: 1,
394-
},
395-
},
369+
ownerClient := coderdtest.New(t, &coderdtest.Options{
370+
Database: db,
371+
Pubsub: pubsub,
396372
})
373+
owner := coderdtest.CreateFirstUser(t, ownerClient)
397374
topCtx := testutil.Context(t, testutil.WaitLong)
398375
apps := generateApps(topCtx, t, ownerClient, "token-exchange")
399376

@@ -764,17 +741,11 @@ func TestOAuth2ProviderTokenRefresh(t *testing.T) {
764741
topCtx := testutil.Context(t, testutil.WaitLong)
765742

766743
db, pubsub := dbtestutil.NewDB(t)
767-
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
768-
Options: &coderdtest.Options{
769-
Database: db,
770-
Pubsub: pubsub,
771-
},
772-
LicenseOptions: &coderdenttest.LicenseOptions{
773-
Features: license.Features{
774-
codersdk.FeatureOAuth2Provider: 1,
775-
},
776-
},
744+
ownerClient := coderdtest.New(t, &coderdtest.Options{
745+
Database: db,
746+
Pubsub: pubsub,
777747
})
748+
owner := coderdtest.CreateFirstUser(t, ownerClient)
778749
apps := generateApps(topCtx, t, ownerClient, "token-refresh")
779750

780751
//nolint:gocritic // OAauth2 app management requires owner permission.
@@ -935,11 +906,8 @@ type exchangeSetup struct {
935906
func TestOAuth2ProviderRevoke(t *testing.T) {
936907
t.Parallel()
937908

938-
client, owner := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{
939-
Features: license.Features{
940-
codersdk.FeatureOAuth2Provider: 1,
941-
},
942-
}})
909+
client := coderdtest.New(t, nil)
910+
owner := coderdtest.CreateFirstUser(t, client)
943911

944912
tests := []struct {
945913
name string
@@ -1138,3 +1106,10 @@ func authorizationFlow(ctx context.Context, client *codersdk.Client, cfg *oauth2
11381106
},
11391107
)
11401108
}
1109+
1110+
func must[T any](value T, err error) T {
1111+
if err != nil {
1112+
panic(err)
1113+
}
1114+
return value
1115+
}

codersdk/deployment.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ const (
5353
FeatureExternalTokenEncryption FeatureName = "external_token_encryption"
5454
FeatureWorkspaceBatchActions FeatureName = "workspace_batch_actions"
5555
FeatureAccessControl FeatureName = "access_control"
56-
FeatureOAuth2Provider FeatureName = "oauth2_provider"
5756
FeatureControlSharedPorts FeatureName = "control_shared_ports"
5857
)
5958

@@ -74,7 +73,6 @@ var FeatureNames = []FeatureName{
7473
FeatureExternalTokenEncryption,
7574
FeatureWorkspaceBatchActions,
7675
FeatureAccessControl,
77-
FeatureOAuth2Provider,
7876
FeatureControlSharedPorts,
7977
}
8078

@@ -85,8 +83,6 @@ func (n FeatureName) Humanize() string {
8583
return "Template RBAC"
8684
case FeatureSCIM:
8785
return "SCIM"
88-
case FeatureOAuth2Provider:
89-
return "OAuth Provider"
9086
default:
9187
return strings.Title(strings.ReplaceAll(string(n), "_", " "))
9288
}

0 commit comments

Comments
 (0)