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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy