@@ -748,12 +748,32 @@ type GithubOAuth2Config struct {
748
748
ListOrganizationMemberships func (ctx context.Context ,client * http.Client ) ([]* github.Membership ,error )
749
749
TeamMembership func (ctx context.Context ,client * http.Client ,org ,team ,username string ) (* github.Membership ,error )
750
750
751
+ DeviceFlowEnabled bool
752
+ ExchangeDeviceCode func (ctx context.Context ,deviceCode string ) (* oauth2.Token ,error )
753
+ AuthorizeDevice func (ctx context.Context ) (* codersdk.ExternalAuthDevice ,error )
754
+
751
755
AllowSignups bool
752
756
AllowEveryone bool
753
757
AllowOrganizations []string
754
758
AllowTeams []GithubOAuth2Team
755
759
}
756
760
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
+ // This is an absolute path in the Coder app. The device flow is orchestrated
773
+ // by the Coder frontend, so we need to redirect the user to the device flow page.
774
+ return "/login/device?state=" + state
775
+ }
776
+
757
777
// @Summary Get authentication methods
758
778
// @ID get-authentication-methods
759
779
// @Security CoderSessionToken
@@ -786,6 +806,53 @@ func (api *API) userAuthMethods(rw http.ResponseWriter, r *http.Request) {
786
806
})
787
807
}
788
808
809
+ // @Summary Get Github device auth.
810
+ // @ID get-github-device-auth
811
+ // @Security CoderSessionToken
812
+ // @Produce json
813
+ // @Tags Users
814
+ // @Success 200 {object} codersdk.ExternalAuthDevice
815
+ // @Router /users/oauth2/github/device [get]
816
+ func (api * API )userOAuth2GithubDevice (rw http.ResponseWriter ,r * http.Request ) {
817
+ var (
818
+ ctx = r .Context ()
819
+ auditor = api .Auditor .Load ()
820
+ aReq ,commitAudit = audit .InitRequest [database.APIKey ](rw ,& audit.RequestParams {
821
+ Audit :* auditor ,
822
+ Log :api .Logger ,
823
+ Request :r ,
824
+ Action :database .AuditActionLogin ,
825
+ })
826
+ )
827
+ aReq .Old = database.APIKey {}
828
+ defer commitAudit ()
829
+
830
+ if api .GithubOAuth2Config == nil {
831
+ httpapi .Write (ctx ,rw ,http .StatusBadRequest , codersdk.Response {
832
+ Message :"Github OAuth2 is not enabled." ,
833
+ })
834
+ return
835
+ }
836
+
837
+ if ! api .GithubOAuth2Config .DeviceFlowEnabled {
838
+ httpapi .Write (ctx ,rw ,http .StatusBadRequest , codersdk.Response {
839
+ Message :"Device flow is not enabled for Github OAuth2." ,
840
+ })
841
+ return
842
+ }
843
+
844
+ deviceAuth ,err := api .GithubOAuth2Config .AuthorizeDevice (ctx )
845
+ if err != nil {
846
+ httpapi .Write (ctx ,rw ,http .StatusInternalServerError , codersdk.Response {
847
+ Message :"Failed to authorize device." ,
848
+ Detail :err .Error (),
849
+ })
850
+ return
851
+ }
852
+
853
+ httpapi .Write (ctx ,rw ,http .StatusOK ,deviceAuth )
854
+ }
855
+
789
856
// @Summary OAuth 2.0 GitHub Callback
790
857
// @ID oauth-20-github-callback
791
858
// @Security CoderSessionToken
@@ -1016,7 +1083,14 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
1016
1083
}
1017
1084
1018
1085
redirect = uriFromURL (redirect )
1019
- http .Redirect (rw ,r ,redirect ,http .StatusTemporaryRedirect )
1086
+ if api .GithubOAuth2Config .DeviceFlowEnabled {
1087
+ // In the device flow, the redirect is handled client-side.
1088
+ httpapi .Write (ctx ,rw ,http .StatusOK , codersdk.OAuth2DeviceFlowCallbackResponse {
1089
+ RedirectURL :redirect ,
1090
+ })
1091
+ }else {
1092
+ http .Redirect (rw ,r ,redirect ,http .StatusTemporaryRedirect )
1093
+ }
1020
1094
}
1021
1095
1022
1096
type OIDCConfig struct {