@@ -10,6 +10,7 @@ import (
10
10
"errors"
11
11
"fmt"
12
12
"io"
13
+ "mime"
13
14
"net"
14
15
"net/http"
15
16
"net/http/cookiejar"
@@ -34,6 +35,7 @@ import (
34
35
"cdr.dev/slog/sloggers/slogtest"
35
36
"github.com/coder/coder/v2/coderd"
36
37
"github.com/coder/coder/v2/coderd/externalauth"
38
+ "github.com/coder/coder/v2/coderd/httpapi"
37
39
"github.com/coder/coder/v2/coderd/promoauth"
38
40
"github.com/coder/coder/v2/coderd/util/syncmap"
39
41
"github.com/coder/coder/v2/codersdk"
@@ -226,6 +228,7 @@ const (
226
228
authorizePath = "/oauth2/authorize"
227
229
keysPath = "/oauth2/keys"
228
230
userInfoPath = "/oauth2/userinfo"
231
+ deviceAuth = "/login/device/code"
229
232
)
230
233
231
234
func NewFakeIDP (t testing.TB ,opts ... FakeIDPOpt )* FakeIDP {
@@ -784,6 +787,8 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
784
787
f .refreshTokensUsed .Store (refreshToken ,true )
785
788
// Always invalidate the refresh token after it is used.
786
789
f .refreshTokens .Delete (refreshToken )
790
+ case "urn:ietf:params:oauth:grant-type:device_code" :
791
+ // Device flow
787
792
default :
788
793
t .Errorf ("unexpected grant_type %q" ,values .Get ("grant_type" ))
789
794
http .Error (rw ,"invalid grant_type" ,http .StatusBadRequest )
@@ -886,6 +891,48 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler {
886
891
_ = json .NewEncoder (rw ).Encode (set )
887
892
}))
888
893
894
+ mux .Handle (deviceAuth ,http .HandlerFunc (func (rw http.ResponseWriter ,r * http.Request ) {
895
+ p := httpapi .NewQueryParamParser ()
896
+ p .Required ("client_id" )
897
+ p .Required ("scopes" )
898
+ clientID := p .String (r .URL .Query (),"" ,"client_id" )
899
+ _ = p .String (r .URL .Query (),"" ,"scopes" )
900
+ if len (p .Errors )> 0 {
901
+ httpapi .Write (r .Context (),rw ,http .StatusBadRequest , codersdk.Response {
902
+ Message :fmt .Sprintf ("Invalid query params" ),
903
+ Validations :p .Errors ,
904
+ })
905
+ return
906
+ }
907
+
908
+ if clientID != f .clientID {
909
+ httpapi .Write (r .Context (),rw ,http .StatusBadRequest , codersdk.Response {
910
+ Message :fmt .Sprintf ("Invalid client id" ),
911
+ })
912
+ return
913
+ }
914
+
915
+ if mediaType ,_ ,_ := mime .ParseMediaType (r .Header .Get ("Accept" ));mediaType == "application/json" {
916
+ httpapi .Write (r .Context (),rw ,http .StatusOK ,map [string ]any {
917
+ "device_code" :uuid .NewString (),
918
+ "user_code" :"1234" ,
919
+ "verification_uri" :"" ,
920
+ "expires_in" :900 ,
921
+ "interval" :0 ,
922
+ })
923
+ return
924
+ }
925
+
926
+ // By default, GitHub form encodes these.
927
+ _ ,_ = fmt .Fprint (rw , url.Values {
928
+ "device_code" : {uuid .NewString ()},
929
+ "user_code" : {"1234" },
930
+ "verification_uri" : {"" },
931
+ "expires_in" : {"900" },
932
+ "interval" : {"0" },
933
+ })
934
+ }))
935
+
889
936
mux .NotFound (func (rw http.ResponseWriter ,r * http.Request ) {
890
937
f .logger .Error (r .Context (),"http call not found" ,slog .F ("path" ,r .URL .Path ))
891
938
t .Errorf ("unexpected request to IDP at path %q. Not supported" ,r .URL .Path )