@@ -22,6 +22,7 @@ import (
2222"github.com/prometheus/client_golang/prometheus"
2323"github.com/stretchr/testify/assert"
2424"github.com/stretchr/testify/require"
25+ "golang.org/x/oauth2"
2526"golang.org/x/xerrors"
2627
2728"cdr.dev/slog"
@@ -882,6 +883,92 @@ func TestUserOAuth2Github(t *testing.T) {
882883require .Equal (t ,user .ID ,userID ,"user_id is different, a new user was likely created" )
883884require .Equal (t ,user .Email ,newEmail )
884885})
886+ t .Run ("DeviceFlow" ,func (t * testing.T ) {
887+ t .Parallel ()
888+ client := coderdtest .New (t ,& coderdtest.Options {
889+ GithubOAuth2Config :& coderd.GithubOAuth2Config {
890+ OAuth2Config :& testutil.OAuth2Config {},
891+ AllowOrganizations : []string {"coder" },
892+ AllowSignups :true ,
893+ ListOrganizationMemberships :func (_ context.Context ,_ * http.Client ) ([]* github.Membership ,error ) {
894+ return []* github.Membership {{
895+ State :& stateActive ,
896+ Organization :& github.Organization {
897+ Login :github .String ("coder" ),
898+ },
899+ }},nil
900+ },
901+ AuthenticatedUser :func (_ context.Context ,_ * http.Client ) (* github.User ,error ) {
902+ return & github.User {
903+ ID :github .Int64 (100 ),
904+ Login :github .String ("testuser" ),
905+ Name :github .String ("The Right Honorable Sir Test McUser" ),
906+ },nil
907+ },
908+ ListEmails :func (_ context.Context ,_ * http.Client ) ([]* github.UserEmail ,error ) {
909+ return []* github.UserEmail {{
910+ Email :github .String ("testuser@coder.com" ),
911+ Verified :github .Bool (true ),
912+ Primary :github .Bool (true ),
913+ }},nil
914+ },
915+ DeviceFlowEnabled :true ,
916+ ExchangeDeviceCode :func (_ context.Context ,_ string ) (* oauth2.Token ,error ) {
917+ return & oauth2.Token {
918+ AccessToken :"access_token" ,
919+ RefreshToken :"refresh_token" ,
920+ Expiry :time .Now ().Add (time .Hour ),
921+ },nil
922+ },
923+ AuthorizeDevice :func (_ context.Context ) (* codersdk.ExternalAuthDevice ,error ) {
924+ return & codersdk.ExternalAuthDevice {
925+ DeviceCode :"device_code" ,
926+ UserCode :"user_code" ,
927+ },nil
928+ },
929+ },
930+ })
931+ client .HTTPClient .CheckRedirect = func (* http.Request , []* http.Request )error {
932+ return http .ErrUseLastResponse
933+ }
934+
935+ // Ensure that we redirect to the device login page when the user is not logged in.
936+ oauthURL ,err := client .URL .Parse ("/api/v2/users/oauth2/github/callback" )
937+ require .NoError (t ,err )
938+
939+ req ,err := http .NewRequestWithContext (context .Background (),"GET" ,oauthURL .String (),nil )
940+
941+ require .NoError (t ,err )
942+ res ,err := client .HTTPClient .Do (req )
943+ require .NoError (t ,err )
944+ defer res .Body .Close ()
945+
946+ require .Equal (t ,http .StatusTemporaryRedirect ,res .StatusCode )
947+ location ,err := res .Location ()
948+ require .NoError (t ,err )
949+ require .Equal (t ,"/login/device" ,location .Path )
950+ query := location .Query ()
951+ require .NotEmpty (t ,query .Get ("state" ))
952+
953+ // Ensure that we return a JSON response when the code is successfully exchanged.
954+ oauthURL ,err = client .URL .Parse ("/api/v2/users/oauth2/github/callback?code=hey&state=somestate" )
955+ require .NoError (t ,err )
956+
957+ req ,err = http .NewRequestWithContext (context .Background (),"GET" ,oauthURL .String (),nil )
958+ req .AddCookie (& http.Cookie {
959+ Name :"oauth_state" ,
960+ Value :"somestate" ,
961+ })
962+ require .NoError (t ,err )
963+ res ,err = client .HTTPClient .Do (req )
964+ require .NoError (t ,err )
965+ defer res .Body .Close ()
966+
967+ require .Equal (t ,http .StatusOK ,res .StatusCode )
968+ var resp codersdk.OAuth2DeviceFlowCallbackResponse
969+ require .NoError (t ,json .NewDecoder (res .Body ).Decode (& resp ))
970+ require .Equal (t ,"/" ,resp .RedirectURL )
971+ })
885972}
886973
887974// nolint:bodyclose