Skip to content

Commit b0101aa

Browse files
committed
github oauth2 device flow backend
1 parent 53f0007 commit b0101aa

File tree

6 files changed

+127
-6
lines changed

6 files changed

+127
-6
lines changed

cli/server.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,12 +677,13 @@ 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.ClientSecret != "" || vals.OAuth2.Github.DeviceFlow.Value() {
681681
options.GithubOAuth2Config, err = configureGithubOAuth2(
682682
oauthInstrument,
683683
vals.AccessURL.Value(),
684684
vals.OAuth2.Github.ClientID.String(),
685685
vals.OAuth2.Github.ClientSecret.String(),
686+
vals.OAuth2.Github.DeviceFlow.Value(),
686687
vals.OAuth2.Github.AllowSignups.Value(),
687688
vals.OAuth2.Github.AllowEveryone.Value(),
688689
vals.OAuth2.Github.AllowedOrgs,
@@ -1832,7 +1833,7 @@ func configureCAPool(tlsClientCAFile string, tlsConfig *tls.Config) error {
18321833
}
18331834

18341835
//nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive)
1835-
func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, clientID, clientSecret string, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) {
1836+
func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, clientID, clientSecret string, deviceFlow, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) {
18361837
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
18371838
if err != nil {
18381839
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
@@ -1898,6 +1899,16 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl
18981899
return github.NewClient(client), nil
18991900
}
19001901

1902+
createDeviceAuth := func() *externalauth.DeviceAuth {
1903+
return &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+
}
1910+
}
1911+
19011912
return &coderd.GithubOAuth2Config{
19021913
OAuth2Config: instrumentedOauth,
19031914
AllowSignups: allowSignups,
@@ -1941,6 +1952,21 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl
19411952
team, _, err := api.Teams.GetTeamMembershipBySlug(ctx, org, teamSlug, username)
19421953
return team, err
19431954
},
1955+
DeviceFlowEnabled: deviceFlow,
1956+
ExchangeDeviceCode: func(ctx context.Context, deviceCode string) (*oauth2.Token, error) {
1957+
if !deviceFlow {
1958+
return nil, xerrors.New("device flow is not enabled")
1959+
}
1960+
deviceAuth := createDeviceAuth()
1961+
return deviceAuth.ExchangeDeviceCode(ctx, deviceCode)
1962+
},
1963+
AuthorizeDevice: func(ctx context.Context) (*codersdk.ExternalAuthDevice, error) {
1964+
if !deviceFlow {
1965+
return nil, xerrors.New("device flow is not enabled")
1966+
}
1967+
deviceAuth := createDeviceAuth()
1968+
return deviceAuth.AuthorizeDevice(ctx)
1969+
},
19441970
}, nil
19451971
}
19461972

coderd/coderd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,7 @@ 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)
10901091
r.Route("/github", func(r chi.Router) {
10911092
r.Use(
10921093
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/userauth.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,12 +748,30 @@ type GithubOAuth2Config struct {
748748
ListOrganizationMemberships func(ctx context.Context, client *http.Client) ([]*github.Membership, error)
749749
TeamMembership func(ctx context.Context, client *http.Client, org, team, username string) (*github.Membership, error)
750750

751+
DeviceFlowEnabled bool
752+
ExchangeDeviceCode func(ctx context.Context, deviceCode string) (*oauth2.Token, error)
753+
AuthorizeDevice func(ctx context.Context) (*codersdk.ExternalAuthDevice, error)
754+
751755
AllowSignups bool
752756
AllowEveryone bool
753757
AllowOrganizations []string
754758
AllowTeams []GithubOAuth2Team
755759
}
756760

761+
func (c *GithubOAuth2Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
762+
if !c.DeviceFlowEnabled {
763+
return c.OAuth2Config.Exchange(ctx, code, opts...)
764+
}
765+
return c.ExchangeDeviceCode(ctx, code)
766+
}
767+
768+
func (c *GithubOAuth2Config) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
769+
if !c.DeviceFlowEnabled {
770+
return c.OAuth2Config.AuthCodeURL(state, opts...)
771+
}
772+
return "/login/device?state=" + state
773+
}
774+
757775
// @Summary Get authentication methods
758776
// @ID get-authentication-methods
759777
// @Security CoderSessionToken
@@ -786,6 +804,53 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
786804
})
787805
}
788806

807+
// @Summary Get Github device auth.
808+
// @ID get-github-device-auth
809+
// @Security CoderSessionToken
810+
// @Produce json
811+
// @Tags Users
812+
// @Success 200 {object} codersdk.ExternalAuthDevice
813+
// @Router /users/oauth2/github/device [get]
814+
func (api *API) userOAuth2GithubDevice(rw http.ResponseWriter, r *http.Request) {
815+
var (
816+
ctx = r.Context()
817+
auditor = api.Auditor.Load()
818+
aReq, commitAudit = audit.InitRequest[database.APIKey](rw, &audit.RequestParams{
819+
Audit: *auditor,
820+
Log: api.Logger,
821+
Request: r,
822+
Action: database.AuditActionLogin,
823+
})
824+
)
825+
aReq.Old = database.APIKey{}
826+
defer commitAudit()
827+
828+
if api.GithubOAuth2Config == nil {
829+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
830+
Message: "Github OAuth2 is not enabled.",
831+
})
832+
return
833+
}
834+
835+
if !api.GithubOAuth2Config.DeviceFlowEnabled {
836+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
837+
Message: "Device flow is not enabled for Github OAuth2.",
838+
})
839+
return
840+
}
841+
842+
deviceAuth, err := api.GithubOAuth2Config.AuthorizeDevice(ctx)
843+
if err != nil {
844+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
845+
Message: "Failed to authorize device.",
846+
Detail: err.Error(),
847+
})
848+
return
849+
}
850+
851+
httpapi.Write(ctx, rw, http.StatusOK, deviceAuth)
852+
}
853+
789854
// @Summary OAuth 2.0 GitHub Callback
790855
// @ID oauth-20-github-callback
791856
// @Security CoderSessionToken
@@ -1016,7 +1081,14 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
10161081
}
10171082

10181083
redirect = uriFromURL(redirect)
1019-
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
1084+
if api.GithubOAuth2Config.DeviceFlowEnabled {
1085+
// In the device flow, the redirect is handled client-side.
1086+
httpapi.Write(ctx, rw, http.StatusOK, codersdk.OAuth2DeviceFlowCallbackResponse{
1087+
RedirectURL: redirect,
1088+
})
1089+
} else {
1090+
http.Redirect(rw, r, redirect, http.StatusTemporaryRedirect)
1091+
}
10201092
}
10211093

10221094
type OIDCConfig struct {

codersdk/deployment.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,7 @@ type OAuth2Config struct {
505505
type OAuth2GithubConfig struct {
506506
ClientID serpent.String `json:"client_id" typescript:",notnull"`
507507
ClientSecret serpent.String `json:"client_secret" typescript:",notnull"`
508+
DeviceFlow serpent.Bool `json:"device_flow" typescript:",notnull"`
508509
AllowedOrgs serpent.StringArray `json:"allowed_orgs" typescript:",notnull"`
509510
AllowedTeams serpent.StringArray `json:"allowed_teams" typescript:",notnull"`
510511
AllowSignups serpent.Bool `json:"allow_signups" typescript:",notnull"`
@@ -1572,6 +1573,16 @@ func (c *DeploymentValues) Options() serpent.OptionSet {
15721573
Annotations: serpent.Annotations{}.Mark(annotationSecretKey, "true"),
15731574
Group: &deploymentGroupOAuth2GitHub,
15741575
},
1576+
{
1577+
Name: "OAuth2 GitHub Device Flow",
1578+
Description: "Enable device flow for Login with GitHub.",
1579+
Flag: "oauth2-github-device-flow",
1580+
Env: "CODER_OAUTH2_GITHUB_DEVICE_FLOW",
1581+
Value: &c.OAuth2.Github.DeviceFlow,
1582+
Group: &deploymentGroupOAuth2GitHub,
1583+
YAML: "deviceFlow",
1584+
Default: "false",
1585+
},
15751586
{
15761587
Name: "OAuth2 GitHub Allowed Orgs",
15771588
Description: "Organizations the user must be a member of to Login with GitHub.",

codersdk/oauth2.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,7 @@ func (c *Client) RevokeOAuth2ProviderApp(ctx context.Context, appID uuid.UUID) e
227227
}
228228
return nil
229229
}
230+
231+
type OAuth2DeviceFlowCallbackResponse struct {
232+
RedirectURL string `json:"redirect_url"`
233+
}

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