mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: support the OAuth2 device flow with GitHub for signing in (#16585)
First PR in a series to address https://github.com/coder/coder/issues/16230. Introduces support for logging in via the [GitHub OAuth2 Device Flow](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#device-flow). It's previously been possible to configure external auth with the device flow, but it's not been possible to use it for logging in. This PR builds on the existing support we had to extend it to sign ins. When a user clicks "sign in with GitHub" when device auth is configured, they are redirected to the new `/login/device` page, which makes the flow possible from the client's side. The recording below shows the full flow. https://github.com/user-attachments/assets/90c06f1f-e42f-43e9-a128-462270c80fdd I've also manually tested that it works for converting from password-based auth to oauth. Device auth can be enabled by a deployment's admin by setting the `CODER_OAUTH2_GITHUB_DEVICE_FLOW` env variable or a corresponding config setting.
This commit is contained in:
+29
-2
@@ -677,12 +677,13 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
|
||||
}
|
||||
}
|
||||
|
||||
if vals.OAuth2.Github.ClientSecret != "" {
|
||||
if vals.OAuth2.Github.ClientSecret != "" || vals.OAuth2.Github.DeviceFlow.Value() {
|
||||
options.GithubOAuth2Config, err = configureGithubOAuth2(
|
||||
oauthInstrument,
|
||||
vals.AccessURL.Value(),
|
||||
vals.OAuth2.Github.ClientID.String(),
|
||||
vals.OAuth2.Github.ClientSecret.String(),
|
||||
vals.OAuth2.Github.DeviceFlow.Value(),
|
||||
vals.OAuth2.Github.AllowSignups.Value(),
|
||||
vals.OAuth2.Github.AllowEveryone.Value(),
|
||||
vals.OAuth2.Github.AllowedOrgs,
|
||||
@@ -1831,8 +1832,10 @@ func configureCAPool(tlsClientCAFile string, tlsConfig *tls.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: convert the argument list to a struct, it's easy to mix up the order of the arguments
|
||||
//
|
||||
//nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive)
|
||||
func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, clientID, clientSecret string, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) {
|
||||
func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, clientID, clientSecret string, deviceFlow, allowSignups, allowEveryone bool, allowOrgs []string, rawTeams []string, enterpriseBaseURL string) (*coderd.GithubOAuth2Config, error) {
|
||||
redirectURL, err := accessURL.Parse("/api/v2/users/oauth2/github/callback")
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse github oauth callback url: %w", err)
|
||||
@@ -1898,6 +1901,17 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl
|
||||
return github.NewClient(client), nil
|
||||
}
|
||||
|
||||
var deviceAuth *externalauth.DeviceAuth
|
||||
if deviceFlow {
|
||||
deviceAuth = &externalauth.DeviceAuth{
|
||||
Config: instrumentedOauth,
|
||||
ClientID: clientID,
|
||||
TokenURL: endpoint.TokenURL,
|
||||
Scopes: []string{"read:user", "read:org", "user:email"},
|
||||
CodeURL: endpoint.DeviceAuthURL,
|
||||
}
|
||||
}
|
||||
|
||||
return &coderd.GithubOAuth2Config{
|
||||
OAuth2Config: instrumentedOauth,
|
||||
AllowSignups: allowSignups,
|
||||
@@ -1941,6 +1955,19 @@ func configureGithubOAuth2(instrument *promoauth.Factory, accessURL *url.URL, cl
|
||||
team, _, err := api.Teams.GetTeamMembershipBySlug(ctx, org, teamSlug, username)
|
||||
return team, err
|
||||
},
|
||||
DeviceFlowEnabled: deviceFlow,
|
||||
ExchangeDeviceCode: func(ctx context.Context, deviceCode string) (*oauth2.Token, error) {
|
||||
if !deviceFlow {
|
||||
return nil, xerrors.New("device flow is not enabled")
|
||||
}
|
||||
return deviceAuth.ExchangeDeviceCode(ctx, deviceCode)
|
||||
},
|
||||
AuthorizeDevice: func(ctx context.Context) (*codersdk.ExternalAuthDevice, error) {
|
||||
if !deviceFlow {
|
||||
return nil, xerrors.New("device flow is not enabled")
|
||||
}
|
||||
return deviceAuth.AuthorizeDevice(ctx)
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user