Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: support the OAuth2 device flow with GitHub for signing in#16585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
hugodutka merged 5 commits intomainfromhugodutka/github-oauth2-device-flow
Feb 21, 2025

Conversation

hugodutka
Copy link
Contributor

@hugodutkahugodutka commentedFeb 16, 2025
edited
Loading

First PR in a series to address#16230.

Introduces support for logging in via theGitHub OAuth2 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.

Screen.Recording.2025-02-17.at.19.49.52.mov

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 theCODER_OAUTH2_GITHUB_DEVICE_FLOW env variable or a corresponding config setting.

@hugodutkahugodutkaforce-pushed thehugodutka/github-oauth2-device-flow branch 12 times, most recently fromf4a8918 todab3105CompareFebruary 19, 2025 16:06
@hugodutkahugodutka marked this pull request as ready for reviewFebruary 19, 2025 16:19
@hugodutka
Copy link
ContributorAuthor

@jaaydenh I've never made any changes to the frontend before. Would you like to see some storybooks or e2e tests added in this PR?

Copy link
Member

@EmyrkEmyrk left a comment
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Overall LG. Some nits.

I did not look at the backend code.

I would like to see a test with the oidctest.FakeIDP, but not sure how difficult that would actually be to plug in.

Someone else should approve the FE

@@ -1832,7 +1833,7 @@ func configureCAPool(tlsClientCAFile string, tlsConfig *tls.Config) error {
}

//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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We should really make these args a struct soon. Not necessary in this PR, just a ton of options that can be misorderd.

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Added a TODO

cli/server.go Outdated
Comment on lines 1902 to 1913
createDeviceAuth := func() *externalauth.DeviceAuth {
return &externalauth.DeviceAuth{
Config: instrumentedOauth,
ClientID: clientID,
TokenURL: endpoint.TokenURL,
Scopes: []string{"read:user", "read:org", "user:email"},
CodeURL: endpoint.DeviceAuthURL,
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Do you know if this has to be a new instance for each auth flow? If so, can you explain in a comment here?

As opposed to just

deviceAuth:=&externalauth.DeviceAuth{Config:instrumentedOauth,ClientID:clientID,TokenURL:endpoint.TokenURL,Scopes:   []string{"read:user","read:org","user:email"},CodeURL:endpoint.DeviceAuthURL,}

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I think it doesn't have to be a new instance. I mirrored what we were doing for the GitHub client without understanding why we did it. I changed it to your suggestion now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I did not have a strong opinion either way. Just curious 👍

Comment on lines +172 to +176
if detail == "authorization_pending" {
// In the device flow, the token may not be immediately
// available. This is expected, and the client will retry.
errorCode = http.StatusBadRequest
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is this error comparison correct?

Comparing to the usage in the oauth lib, it is checking theErrorCode field of theRetreiveError.

Usage:https://github.com/golang/oauth2/blob/master/deviceauth.go#L189-L190
Error type:https://github.com/golang/oauth2/blob/master/token.go#L187-L198

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

We actually don't use theDeviceAccessToken method from the oauth lib. We have a custom implementation of ExchangeDeviceCodewhich we use for the device flow. That implementationcallscodersdk.ReadBodyAsError on an HTTP response it gets from GitHub, which populates the detail field with"authorization_pending". I verified that the check is valid.

For context, we use a custom implementation instead of the lib'sDeviceAccessToken because the latter keeps retrying until the code is successfully exchanged. We keep the retry logic on the frontend. That way you don't have an HTTP call that hangs for what can be minutes until the user authenticates with GitHub.

if !c.DeviceFlowEnabled {
return c.OAuth2Config.AuthCodeURL(state, opts...)
}
return "/login/device?state=" + state
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Is this a relative url to our Coder app? Not an external auth url?

Can you just leave a comment that mentions this. I say that becausedAuthCodeURL in a regular Oauth flow is a link to an external domain. So it's not obvious imo.

hugodutka reacted with thumbs up emoji
Comment on lines +1085 to +1090
// In the device flow, the redirect is handled client-side.
httpapi.Write(ctx, rw, http.StatusOK, codersdk.OAuth2DeviceFlowCallbackResponse{
RedirectURL: redirect,
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

👍

@@ -882,6 +883,92 @@ func TestUserOAuth2Github(t *testing.T) {
require.Equal(t, user.ID, userID, "user_id is different, a new user was likely created")
require.Equal(t, user.Email, newEmail)
})
t.Run("DeviceFlow", func(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Just sharing some info. Ouroidctest.FakeIDP supports device auth flow.

Here is an example external auth test:

t.Run("WithFakeIDP",func(t*testing.T) {
t.Parallel()
fake:=oidctest.NewFakeIDP(t,oidctest.WithServing())
externalID:="fake-idp"
cfg:=fake.ExternalAuthConfig(t,externalID,&oidctest.ExternalAuthConfigOptions{
UseDeviceAuth:true,
})
client:=coderdtest.New(t,&coderdtest.Options{
ExternalAuthConfigs: []*externalauth.Config{cfg},
})
coderdtest.CreateFirstUser(t,client)
// Login!
fake.DeviceLogin(t,client,externalID)
extAuth,err:=client.ExternalAuthByID(context.Background(),externalID)
require.NoError(t,err)
require.True(t,extAuth.Authenticated)
})

So we could write a test against a "live" idp server. This test uses mocked out responses forExchangeDeviceCode andAuthorizeDevice. It does not test any of the oauth logic behind that.

@jaaydenh
Copy link
Contributor

@hugodutka Regarding some of the comments about the FE regarding Tailwind and Material UI components. It looks you were modifying existing code more than adding new code. I would say its preferable to make the changes but it makes sense to not do it now if you feel your are just modifying existing code.

@jaaydenh
Copy link
Contributor

jaaydenh commentedFeb 19, 2025
edited
Loading

@jaaydenh I've never made any changes to the frontend before. Would you like to see some storybooks or e2e tests added in this PR?

It would make sense to add storybook tests for the pageview that has been added:
LoginOAuthDevicePageView.tsx

Playwright e2e tests would make sense as well if you have a good way to test this.

Both of these could be done in a separate PR to keep things moving.

Is there a simple enough way to test the Github device flow locally?

@hugodutka
Copy link
ContributorAuthor

@jaaydenh

I would say its preferable to make the changes but it makes sense to not do it now if you feel your are just modifying existing code.

I indeed just copy pasted and modifiedsite/src/pages/ExternalAuthPage. I'd rather keep the refactoring to tailwind effort in a separate PR.

Both of these could be done in a separate PR to keep things moving.

Great, I'll do that in a separate PR.

Is there a simple enough way to test the Github device flow locally?

Yes, run the server like this:

export CODER_OAUTH2_GITHUB_DEVICE_FLOW=trueexport CODER_OAUTH2_GITHUB_CLIENT_ID=Iv23liSBHklRMBNx5lk9export CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS=trueexport CODER_OAUTH2_GITHUB_ALLOW_EVERYONE=truego run cmd/coder/main.go server

@hugodutkahugodutkaforce-pushed thehugodutka/github-oauth2-device-flow branch fromdab3105 tobe8cca1CompareFebruary 21, 2025 17:18
@hugodutkahugodutkaforce-pushed thehugodutka/github-oauth2-device-flow branch frombe8cca1 tocfc3645CompareFebruary 21, 2025 17:23
@hugodutka
Copy link
ContributorAuthor

Thank you for the reviews@jaaydenh@Emyrk! I'll add storybook tests, a fakeIDP test, and - if I find a good way to do it - an E2E test in a follow up PR.

@hugodutkahugodutka merged commit8c5e700 intomainFeb 21, 2025
32 of 35 checks passed
@hugodutkahugodutka deleted the hugodutka/github-oauth2-device-flow branchFebruary 21, 2025 17:42
@github-actionsgithub-actionsbot locked and limited conversation to collaboratorsFeb 21, 2025
Sign up for freeto subscribe to this conversation on GitHub. Already have an account?Sign in.
Reviewers

@EmyrkEmyrkEmyrk approved these changes

@jaaydenhjaaydenhjaaydenh approved these changes

Assignees

@hugodutkahugodutka

Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

3 participants
@hugodutka@jaaydenh@Emyrk

[8]ページ先頭

©2009-2025 Movatter.jp