Skip to content

Commit 05b6a37

Browse files
committed
Add Github authentication
1 parent b1a6d9c commit 05b6a37

27 files changed

+636
-597
lines changed

.vscode/settings.json

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"nolint",
3636
"nosec",
3737
"ntqry",
38+
"OIDC",
3839
"oneof",
3940
"parameterscopeid",
4041
"pqtype",
@@ -46,6 +47,7 @@
4647
"ptytest",
4748
"retrier",
4849
"sdkproto",
50+
"Signup",
4951
"stretchr",
5052
"TCGETS",
5153
"tcpip",

cli/start.go

+42
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ import (
1818

1919
"github.com/briandowns/spinner"
2020
"github.com/coreos/go-systemd/daemon"
21+
"github.com/google/go-github/v43/github"
2122
"github.com/spf13/cobra"
23+
"golang.org/x/oauth2"
24+
xgithub "golang.org/x/oauth2/github"
2225
"golang.org/x/xerrors"
2326
"google.golang.org/api/idtoken"
2427
"google.golang.org/api/option"
@@ -153,13 +156,19 @@ func start() *cobra.Command {
153156
return xerrors.Errorf("parse ssh keygen algorithm %s: %w", sshKeygenAlgorithmRaw, err)
154157
}
155158

159+
githubOAuth2Config, err := configureGithubOAuth2(accessURLParsed, "", "")
160+
if err != nil {
161+
return xerrors.Errorf("configure github oauth2: %w", err)
162+
}
163+
156164
logger := slog.Make(sloghuman.Sink(os.Stderr))
157165
options := &coderd.Options{
158166
AccessURL: accessURLParsed,
159167
Logger: logger.Named("coderd"),
160168
Database: databasefake.New(),
161169
Pubsub: database.NewPubsubInMemory(),
162170
GoogleTokenValidator: validator,
171+
GithubOAuth2Config: githubOAuth2Config,
163172
SecureAuthCookie: secureAuthCookie,
164173
SSHKeygenAlgorithm: sshKeygenAlgorithm,
165174
}
@@ -534,3 +543,36 @@ func configureTLS(listener net.Listener, tlsMinVersion, tlsClientAuth, tlsCertFi
534543

535544
return tls.NewListener(listener, tlsConfig), nil
536545
}
546+
547+
func configureGithubOAuth2(accessURL *url.URL, clientID, clientSecret string) (*coderd.GithubOAuth2Config, error) {
548+
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
549+
if err != nil {
550+
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
551+
}
552+
return &coderd.GithubOAuth2Config{
553+
OAuth2Config: &oauth2.Config{
554+
ClientID: clientID,
555+
ClientSecret: clientSecret,
556+
Endpoint: xgithub.Endpoint,
557+
RedirectURL: redirectURL.String(),
558+
Scopes: []string{
559+
"read:user",
560+
"user:email",
561+
},
562+
},
563+
AllowSignups: true,
564+
AllowOrganizations: []string{"coder"},
565+
AuthenticatedUser: func(ctx context.Context, client *http.Client) (*github.User, error) {
566+
user, _, err := github.NewClient(client).Users.Get(ctx, "")
567+
return user, err
568+
},
569+
ListEmails: func(ctx context.Context, client *http.Client) ([]*github.UserEmail, error) {
570+
emails, _, err := github.NewClient(client).Users.ListEmails(ctx, &github.ListOptions{})
571+
return emails, err
572+
},
573+
ListOrganizations: func(ctx context.Context, client *http.Client) ([]*github.Organization, error) {
574+
orgs, _, err := github.NewClient(client).Organizations.List(ctx, "", &github.ListOptions{})
575+
return orgs, err
576+
},
577+
}, nil
578+
}

coderd/coderd.go

+18-15
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type Options struct {
3434

3535
AWSCertificates awsidentity.Certificates
3636
GoogleTokenValidator *idtoken.Validator
37-
GithubOAuth2Provider GithubOAuth2Provider
37+
GithubOAuth2Config *GithubOAuth2Config
3838

3939
SecureAuthCookie bool
4040
SSHKeygenAlgorithm gitsshkey.Algorithm
@@ -51,6 +51,9 @@ func New(options *Options) (http.Handler, func()) {
5151
api := &api{
5252
Options: options,
5353
}
54+
apiKeyMiddleware := httpmw.ExtractAPIKey(options.Database, &httpmw.OAuth2Configs{
55+
Github: options.GithubOAuth2Config,
56+
})
5457

5558
r := chi.NewRouter()
5659
r.Route("/api/v2", func(r chi.Router) {
@@ -75,7 +78,7 @@ func New(options *Options) (http.Handler, func()) {
7578
})
7679
r.Route("/files", func(r chi.Router) {
7780
r.Use(
78-
httpmw.ExtractAPIKey(options.Database, nil),
81+
apiKeyMiddleware,
7982
// This number is arbitrary, but reading/writing
8083
// file content is expensive so it should be small.
8184
httpmw.RateLimitPerMinute(12),
@@ -85,7 +88,7 @@ func New(options *Options) (http.Handler, func()) {
8588
})
8689
r.Route("/organizations/{organization}", func(r chi.Router) {
8790
r.Use(
88-
httpmw.ExtractAPIKey(options.Database, nil),
91+
apiKeyMiddleware,
8992
httpmw.ExtractOrganizationParam(options.Database),
9093
)
9194
r.Get("/", api.organization)
@@ -98,7 +101,7 @@ func New(options *Options) (http.Handler, func()) {
98101
})
99102
})
100103
r.Route("/parameters/{scope}/{id}", func(r chi.Router) {
101-
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
104+
r.Use(apiKeyMiddleware)
102105
r.Post("/", api.postParameter)
103106
r.Get("/", api.parameters)
104107
r.Route("/{name}", func(r chi.Router) {
@@ -107,7 +110,7 @@ func New(options *Options) (http.Handler, func()) {
107110
})
108111
r.Route("/templates/{template}", func(r chi.Router) {
109112
r.Use(
110-
httpmw.ExtractAPIKey(options.Database, nil),
113+
apiKeyMiddleware,
111114
httpmw.ExtractTemplateParam(options.Database),
112115
httpmw.ExtractOrganizationParam(options.Database),
113116
)
@@ -121,7 +124,7 @@ func New(options *Options) (http.Handler, func()) {
121124
})
122125
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
123126
r.Use(
124-
httpmw.ExtractAPIKey(options.Database, nil),
127+
apiKeyMiddleware,
125128
httpmw.ExtractTemplateVersionParam(options.Database),
126129
httpmw.ExtractOrganizationParam(options.Database),
127130
)
@@ -143,14 +146,14 @@ func New(options *Options) (http.Handler, func()) {
143146
r.Post("/first", api.postFirstUser)
144147
r.Post("/login", api.postLogin)
145148
r.Post("/logout", api.postLogout)
146-
r.Route("/auth", func(r chi.Router) {
147-
r.Route("/callback/github", func(r chi.Router) {
148-
r.Use(httpmw.ExtractOAuth2(options.GithubOAuth2Provider))
149-
r.Get("/", api.userAuthGithub)
149+
r.Route("/oauth2", func(r chi.Router) {
150+
r.Route("/github", func(r chi.Router) {
151+
r.Use(httpmw.ExtractOAuth2(options.GithubOAuth2Config))
152+
r.Get("/callback", api.userOAuth2Github)
150153
})
151154
})
152155
r.Group(func(r chi.Router) {
153-
r.Use(httpmw.ExtractAPIKey(options.Database, nil))
156+
r.Use(apiKeyMiddleware)
154157
r.Post("/", api.postUsers)
155158
r.Route("/{user}", func(r chi.Router) {
156159
r.Use(httpmw.ExtractUserParam(options.Database))
@@ -184,7 +187,7 @@ func New(options *Options) (http.Handler, func()) {
184187
})
185188
r.Route("/{workspaceagent}", func(r chi.Router) {
186189
r.Use(
187-
httpmw.ExtractAPIKey(options.Database, nil),
190+
apiKeyMiddleware,
188191
httpmw.ExtractWorkspaceAgentParam(options.Database),
189192
)
190193
r.Get("/", api.workspaceAgent)
@@ -193,15 +196,15 @@ func New(options *Options) (http.Handler, func()) {
193196
})
194197
r.Route("/workspaceresources/{workspaceresource}", func(r chi.Router) {
195198
r.Use(
196-
httpmw.ExtractAPIKey(options.Database, nil),
199+
apiKeyMiddleware,
197200
httpmw.ExtractWorkspaceResourceParam(options.Database),
198201
httpmw.ExtractWorkspaceParam(options.Database),
199202
)
200203
r.Get("/", api.workspaceResource)
201204
})
202205
r.Route("/workspaces/{workspace}", func(r chi.Router) {
203206
r.Use(
204-
httpmw.ExtractAPIKey(options.Database, nil),
207+
apiKeyMiddleware,
205208
httpmw.ExtractWorkspaceParam(options.Database),
206209
)
207210
r.Get("/", api.workspace)
@@ -219,7 +222,7 @@ func New(options *Options) (http.Handler, func()) {
219222
})
220223
r.Route("/workspacebuilds/{workspacebuild}", func(r chi.Router) {
221224
r.Use(
222-
httpmw.ExtractAPIKey(options.Database, nil),
225+
apiKeyMiddleware,
223226
httpmw.ExtractWorkspaceBuildParam(options.Database),
224227
httpmw.ExtractWorkspaceParam(options.Database),
225228
)

coderd/coderdtest/coderdtest.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import (
4949

5050
type Options struct {
5151
AWSInstanceIdentity awsidentity.Certificates
52-
GithubOAuth2Provider coderd.GithubOAuth2Provider
52+
GithubOAuth2Config *coderd.GithubOAuth2Config
5353
GoogleInstanceIdentity *idtoken.Validator
5454
SSHKeygenAlgorithm gitsshkey.Algorithm
5555
}
@@ -116,7 +116,7 @@ func New(t *testing.T, options *Options) *codersdk.Client {
116116
Pubsub: pubsub,
117117

118118
AWSCertificates: options.AWSInstanceIdentity,
119-
GithubOAuth2Provider: options.GithubOAuth2Provider,
119+
GithubOAuth2Config: options.GithubOAuth2Config,
120120
GoogleTokenValidator: options.GoogleInstanceIdentity,
121121
SSHKeygenAlgorithm: options.SSHKeygenAlgorithm,
122122
})

coderd/database/databasefake/databasefake.go

+15-18
Original file line numberDiff line numberDiff line change
@@ -797,21 +797,18 @@ func (q *fakeQuerier) InsertAPIKey(_ context.Context, arg database.InsertAPIKeyP
797797

798798
//nolint:gosimple
799799
key := database.APIKey{
800-
ID: arg.ID,
801-
HashedSecret: arg.HashedSecret,
802-
UserID: arg.UserID,
803-
Application: arg.Application,
804-
Name: arg.Name,
805-
LastUsed: arg.LastUsed,
806-
ExpiresAt: arg.ExpiresAt,
807-
CreatedAt: arg.CreatedAt,
808-
UpdatedAt: arg.UpdatedAt,
809-
LoginType: arg.LoginType,
810-
OIDCAccessToken: arg.OIDCAccessToken,
811-
OIDCRefreshToken: arg.OIDCRefreshToken,
812-
OIDCIDToken: arg.OIDCIDToken,
813-
OIDCExpiry: arg.OIDCExpiry,
814-
DevurlToken: arg.DevurlToken,
800+
ID: arg.ID,
801+
HashedSecret: arg.HashedSecret,
802+
UserID: arg.UserID,
803+
ExpiresAt: arg.ExpiresAt,
804+
CreatedAt: arg.CreatedAt,
805+
UpdatedAt: arg.UpdatedAt,
806+
LastUsed: arg.LastUsed,
807+
LoginType: arg.LoginType,
808+
OAuthAccessToken: arg.OAuthAccessToken,
809+
OAuthRefreshToken: arg.OAuthRefreshToken,
810+
OAuthIDToken: arg.OAuthIDToken,
811+
OAuthExpiry: arg.OAuthExpiry,
815812
}
816813
q.apiKeys = append(q.apiKeys, key)
817814
return key, nil
@@ -1126,9 +1123,9 @@ func (q *fakeQuerier) UpdateAPIKeyByID(_ context.Context, arg database.UpdateAPI
11261123
}
11271124
apiKey.LastUsed = arg.LastUsed
11281125
apiKey.ExpiresAt = arg.ExpiresAt
1129-
apiKey.OIDCAccessToken = arg.OIDCAccessToken
1130-
apiKey.OIDCRefreshToken = arg.OIDCRefreshToken
1131-
apiKey.OIDCExpiry = arg.OIDCExpiry
1126+
apiKey.OAuthAccessToken = arg.OAuthAccessToken
1127+
apiKey.OAuthRefreshToken = arg.OAuthRefreshToken
1128+
apiKey.OAuthExpiry = arg.OAuthExpiry
11321129
q.apiKeys[index] = apiKey
11331130
return nil
11341131
}

coderd/database/dump.sql

+6-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/migrations/000001_base.up.sql

+6-17
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@
44
-- All tables and types are stolen from:
55
-- https://github.com/coder/m/blob/47b6fc383347b9f9fab424d829c482defd3e1fe2/product/coder/pkg/database/dump.sql
66

7-
--
8-
-- Name: users; Type: TABLE; Schema: public; Owner: coder
9-
--
10-
117
CREATE TYPE login_type AS ENUM (
12-
'built-in',
13-
'saml',
14-
'oidc'
8+
'basic',
9+
'github'
1510
);
1611

1712
CREATE TABLE IF NOT EXISTS users (
@@ -31,10 +26,6 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users USING btree (email);
3126
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users USING btree (username);
3227
CREATE UNIQUE INDEX IF NOT EXISTS users_username_lower_idx ON users USING btree (lower(username));
3328

34-
--
35-
-- Name: organizations; Type: TABLE; Schema: Owner: coder
36-
--
37-
3829
CREATE TABLE IF NOT EXISTS organizations (
3930
id uuid NOT NULL,
4031
name text NOT NULL,
@@ -68,18 +59,16 @@ CREATE TABLE IF NOT EXISTS api_keys (
6859
id text NOT NULL,
6960
hashed_secret bytea NOT NULL,
7061
user_id uuid NOT NULL,
71-
application boolean NOT NULL,
7262
name text NOT NULL,
7363
last_used timestamp with time zone NOT NULL,
7464
expires_at timestamp with time zone NOT NULL,
7565
created_at timestamp with time zone NOT NULL,
7666
updated_at timestamp with time zone NOT NULL,
7767
login_type login_type NOT NULL,
78-
oidc_access_token text DEFAULT ''::text NOT NULL,
79-
oidc_refresh_token text DEFAULT ''::text NOT NULL,
80-
oidc_id_token text DEFAULT ''::text NOT NULL,
81-
oidc_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
82-
devurl_token boolean DEFAULT false NOT NULL,
68+
oauth_access_token text DEFAULT ''::text NOT NULL,
69+
oauth_refresh_token text DEFAULT ''::text NOT NULL,
70+
oauth_id_token text DEFAULT ''::text NOT NULL,
71+
oauth_expiry timestamp with time zone DEFAULT '0001-01-01 00:00:00+00'::timestamp with time zone NOT NULL,
8372
PRIMARY KEY (id)
8473
);
8574

0 commit comments

Comments
 (0)