Skip to content

Commit 1360cd4

Browse files
committed
functional login page
1 parent 0f5a932 commit 1360cd4

File tree

14 files changed

+596
-21
lines changed

14 files changed

+596
-21
lines changed

cli/server.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
677677
}
678678
}
679679

680-
if vals.OAuth2.Github.ClientSecret != "" {
680+
if vals.OAuth2.Github.ClientID != "" {
681681
options.GithubOAuth2Config, err = configureGithubOAuth2(
682682
oauthInstrument,
683683
vals.AccessURL.Value(),
@@ -1899,7 +1899,14 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl
18991899
}
19001900

19011901
return &coderd.GithubOAuth2Config{
1902-
OAuth2Config: instrumentedOauth,
1902+
OAuth2Config: instrumentedOauth,
1903+
DeviceAuth: &externalauth.DeviceAuth{
1904+
Config: instrumentedOauth,
1905+
ClientID: clientID,
1906+
TokenURL: endpoint.TokenURL,
1907+
Scopes: []string{"read:user", "read:org", "user:email"},
1908+
CodeURL: endpoint.DeviceAuthURL,
1909+
},
19031910
AllowSignups: allowSignups,
19041911
AllowEveryone: allowEveryone,
19051912
AllowOrganizations: allowOrgs,

coderd/apidoc/docs.go

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

coderd/apidoc/swagger.json

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

coderd/coderd.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,8 @@ func New(options *Options) *API {
10871087
r.Post("/validate-password", api.validateUserPassword)
10881088
r.Post("/otp/change-password", api.postChangePasswordWithOneTimePasscode)
10891089
r.Route("/oauth2", func(r chi.Router) {
1090+
r.Get("/github/device", api.userOAuth2GithubDevice)
1091+
r.Post("/github/device", api.postGithubOAuth2Device)
10901092
r.Route("/github", func(r chi.Router) {
10911093
r.Use(
10921094
httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil),

coderd/httpmw/oauth2.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,16 @@ func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOp
167167

168168
oauthToken, err := config.Exchange(ctx, code)
169169
if err != nil {
170-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
171-
Message: "Internal error exchanging Oauth code.",
172-
Detail: err.Error(),
170+
errorCode := http.StatusInternalServerError
171+
detail := err.Error()
172+
if detail == "authorization_pending" {
173+
// In the device flow, the token may not be immediately
174+
// available. This is expected, and the client will retry.
175+
errorCode = http.StatusBadRequest
176+
}
177+
httpapi.Write(ctx, rw, errorCode, codersdk.Response{
178+
Message: "Failed exchanging Oauth code.",
179+
Detail: detail,
173180
})
174181
return
175182
}

coderd/promoauth/oauth2.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ const (
3232
type OAuth2Config interface {
3333
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
3434
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
35-
DeviceAccessToken(ctx context.Context, da *oauth2.DeviceAuthResponse, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
3635
TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource
3736
}
3837

@@ -228,10 +227,6 @@ func (c *Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthC
228227
return c.underlying.Exchange(c.wrapClient(ctx, SourceExchange), code, opts...)
229228
}
230229

231-
func (c *Config) DeviceAccessToken(ctx context.Context, da *oauth2.DeviceAuthResponse, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
232-
return c.underlying.DeviceAccessToken(c.wrapClient(ctx, SourceDeviceAccessToken), da, opts...)
233-
}
234-
235230
func (c *Config) TokenSource(ctx context.Context, token *oauth2.Token) oauth2.TokenSource {
236231
return c.underlying.TokenSource(c.wrapClient(ctx, SourceTokenSource), token)
237232
}

coderd/userauth.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,22 @@ type GithubOAuth2Config struct {
752752
AllowEveryone bool
753753
AllowOrganizations []string
754754
AllowTeams []GithubOAuth2Team
755+
// DeviceAuth is set if the provider uses the device flow.
756+
DeviceAuth *externalauth.DeviceAuth
757+
}
758+
759+
func (c *GithubOAuth2Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
760+
if c.DeviceAuth == nil {
761+
return c.OAuth2Config.Exchange(ctx, code, opts...)
762+
}
763+
return c.DeviceAuth.ExchangeDeviceCode(ctx, code)
764+
}
765+
766+
func (c *GithubOAuth2Config) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
767+
if c.DeviceAuth == nil {
768+
return c.OAuth2Config.AuthCodeURL(state, opts...)
769+
}
770+
return "/device-login?state=" + state
755771
}
756772

757773
// @Summary Get authentication methods
@@ -786,6 +802,102 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
786802
})
787803
}
788804

805+
// @Summary Get Github device auth.
806+
// @ID get-github-device-auth
807+
// @Produce json
808+
// @Tags Users
809+
// @Success 200 {object} codersdk.ExternalAuthDevice
810+
// @Router /users/oauth2/github/device [get]
811+
func (api *API) userOAuth2GithubDevice(rw http.ResponseWriter, r *http.Request) {
812+
var (
813+
ctx = r.Context()
814+
auditor = api.Auditor.Load()
815+
aReq, commitAudit = audit.InitRequest[database.APIKey](rw, &audit.RequestParams{
816+
Audit: *auditor,
817+
Log: api.Logger,
818+
Request: r,
819+
Action: database.AuditActionLogin,
820+
})
821+
)
822+
aReq.Old = database.APIKey{}
823+
defer commitAudit()
824+
825+
if api.GithubOAuth2Config == nil {
826+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
827+
Message: "Github OAuth2 is not enabled.",
828+
})
829+
return
830+
}
831+
832+
if api.GithubOAuth2Config.DeviceAuth == nil {
833+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
834+
Message: "Device flow is not enabled for Github OAuth2.",
835+
})
836+
return
837+
}
838+
839+
deviceAuth, err := api.GithubOAuth2Config.DeviceAuth.AuthorizeDevice(ctx)
840+
if err != nil {
841+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
842+
Message: "Failed to authorize device.",
843+
Detail: err.Error(),
844+
})
845+
return
846+
}
847+
848+
httpapi.Write(ctx, rw, http.StatusOK, deviceAuth)
849+
}
850+
851+
// @Summary Excha Github device auth.
852+
// @ID get-github-device-auth
853+
// @Produce json
854+
// @Tags Users
855+
// @Success 200 {object} codersdk.ExternalAuthDevice
856+
// @Router /users/oauth2/github/device [get]
857+
func (api *API) postGithubOAuth2Device(rw http.ResponseWriter, r *http.Request) {
858+
var (
859+
ctx = r.Context()
860+
auditor = api.Auditor.Load()
861+
aReq, commitAudit = audit.InitRequest[database.APIKey](rw, &audit.RequestParams{
862+
Audit: *auditor,
863+
Log: api.Logger,
864+
Request: r,
865+
Action: database.AuditActionLogin,
866+
})
867+
)
868+
aReq.Old = database.APIKey{}
869+
defer commitAudit()
870+
871+
if api.GithubOAuth2Config == nil {
872+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
873+
Message: "Github OAuth2 is not enabled.",
874+
})
875+
return
876+
}
877+
if api.GithubOAuth2Config.DeviceAuth == nil {
878+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
879+
Message: "Device flow is not enabled for Github OAuth2.",
880+
})
881+
return
882+
}
883+
884+
var req codersdk.ExternalAuthDeviceExchange
885+
if !httpapi.Read(ctx, rw, r, &req) {
886+
return
887+
}
888+
889+
token, err := api.GithubOAuth2Config.DeviceAuth.ExchangeDeviceCode(ctx, req.DeviceCode)
890+
if err != nil {
891+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
892+
Message: "Failed to exchange device code.",
893+
Detail: err.Error(),
894+
})
895+
return
896+
}
897+
898+
httpapi.Write(ctx, rw, http.StatusOK, token)
899+
}
900+
789901
// @Summary OAuth 2.0 GitHub Callback
790902
// @ID oauth-20-github-callback
791903
// @Security CoderSessionToken

docs/reference/api/users.md

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

site/src/api/api.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1585,6 +1585,21 @@ class ApiMethods {
15851585
return resp.data;
15861586
};
15871587

1588+
getOAuth2GitHubCallback = async (code: string, state: string): Promise<string> => {
1589+
const resp =await this.axios.get(`/api/v2/users/oauth2/github/callback?code=${code}&state=${state}`);
1590+
const location = resp.headers.location;
1591+
if (typeof location !== "string") {
1592+
console.warn("OAuth2 GitHub callback location is not a string", location);
1593+
return "/";
1594+
}
1595+
return location;
1596+
};
1597+
1598+
getOAuth2GitHubDevice = async (): Promise<TypesGen.ExternalAuthDevice> => {
1599+
const resp = await this.axios.get("/api/v2/users/oauth2/github/device");
1600+
return resp.data;
1601+
};
1602+
15881603
getOAuth2ProviderApps = async (
15891604
filter?: TypesGen.OAuth2ProviderAppFilter,
15901605
): Promise<TypesGen.OAuth2ProviderApp[]> => {

site/src/api/queries/oauth2.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ const userAppsKey = (userId: string) => appsKey.concat(userId);
77
const appKey = (appId: string) => appsKey.concat(appId);
88
const appSecretsKey = (appId: string) => appKey(appId).concat("secrets");
99

10+
export const getGitHubDevice = () => {
11+
return {
12+
queryKey: ["oauth2-provider", "github", "device"],
13+
queryFn: () => API.getOAuth2GitHubDevice(),
14+
};
15+
};
16+
17+
export const getGitHubCallback = (code: string, state: string) => {
18+
return {
19+
queryKey: ["oauth2-provider", "github", "callback", code, state],
20+
queryFn: () => API.getOAuth2GitHubCallback(code, state),
21+
};
22+
};
23+
1024
export const getApps = (userId?: string) => {
1125
return {
1226
queryKey: userId ? appsKey.concat(userId) : appsKey,

0 commit comments

Comments
 (0)