Skip to content

Commit 0490fa1

Browse files
committed
device flow poc for sign in
1 parent d52d239 commit 0490fa1

File tree

6 files changed

+177
-64
lines changed

6 files changed

+177
-64
lines changed

cmd/what/main.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
10+
"golang.org/x/oauth2"
11+
"golang.org/x/oauth2/github"
12+
)
13+
14+
func main() {
15+
clientID := os.Getenv("CODER_OAUTH2_GITHUB_CLIENT_ID")
16+
if clientID == "" {
17+
panic("CODER_OAUTH2_GITHUB_CLIENT_ID environment variable is not set")
18+
}
19+
20+
config := oauth2.Config{
21+
ClientID: clientID,
22+
Endpoint: github.Endpoint,
23+
Scopes: []string{"repo"},
24+
}
25+
26+
ctx := context.Background()
27+
28+
// Request device code
29+
deviceCode, err := config.DeviceAuth(ctx)
30+
if err != nil {
31+
panic(err)
32+
}
33+
// Marshal and print deviceCode as JSON
34+
jsonData, err := json.Marshal(deviceCode)
35+
if err != nil {
36+
panic(err)
37+
}
38+
fmt.Printf("Device code as JSON:\n%s\n\n", string(jsonData))
39+
40+
// Convert to base64 and print
41+
base64Data := base64.StdEncoding.EncodeToString(jsonData)
42+
fmt.Printf("Device code as base64:\n%s\n\n", base64Data)
43+
44+
// Display instructions to user
45+
fmt.Printf("Please visit: %s\n", deviceCode.VerificationURI)
46+
fmt.Printf("And enter code: %s\n", deviceCode.UserCode)
47+
48+
// // Wait for user to complete authentication and get token
49+
// token, err := config.DeviceAccessToken(ctx, deviceCode)
50+
// if err != nil {
51+
// panic(err)
52+
// }
53+
54+
// fmt.Printf("Access token: %s\n", token.AccessToken)
55+
}

coderd/httpmw/oauth2.go

Lines changed: 90 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package httpmw
22

33
import (
44
"context"
5+
"encoding/base64"
6+
"encoding/json"
57
"fmt"
68
"net/http"
79
"net/url"
@@ -15,7 +17,6 @@ import (
1517
"github.com/coder/coder/v2/coderd/httpapi"
1618
"github.com/coder/coder/v2/coderd/promoauth"
1719
"github.com/coder/coder/v2/codersdk"
18-
"github.com/coder/coder/v2/cryptorand"
1920
)
2021

2122
type oauth2StateKey struct{}
@@ -84,7 +85,8 @@ func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOp
8485
return
8586
}
8687

87-
code := r.URL.Query().Get("code")
88+
// code := r.URL.Query().Get("code")
89+
deviceCode := r.URL.Query().Get("device_code")
8890
state := r.URL.Query().Get("state")
8991
redirect := r.URL.Query().Get("redirect")
9092
if redirect != "" {
@@ -96,76 +98,105 @@ func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOp
9698
redirect = uriFromURL(redirect)
9799
}
98100

99-
if code == "" {
100-
// If the code isn't provided, we'll redirect!
101-
var state string
102-
// If this url param is provided, then a user is trying to merge
103-
// their account with an OIDC account. Their password would have
104-
// been required to get to this point, so we do not need to verify
105-
// their password again.
106-
oidcMergeState := r.URL.Query().Get("oidc_merge_state")
107-
if oidcMergeState != "" {
108-
state = oidcMergeState
109-
} else {
110-
var err error
111-
state, err = cryptorand.String(32)
112-
if err != nil {
113-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
114-
Message: "Internal error generating state string.",
115-
Detail: err.Error(),
116-
})
117-
return
118-
}
101+
var da *oauth2.DeviceAuthResponse
102+
if deviceCode != "" {
103+
// Decode base64-encoded device code
104+
decodedBytes, err := base64.StdEncoding.DecodeString(deviceCode)
105+
if err != nil {
106+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
107+
Message: "Invalid device code format",
108+
Detail: err.Error(),
109+
})
110+
return
119111
}
120112

121-
http.SetCookie(rw, &http.Cookie{
122-
Name: codersdk.OAuth2StateCookie,
123-
Value: state,
124-
Path: "/",
125-
HttpOnly: true,
126-
SameSite: http.SameSiteLaxMode,
127-
})
128-
// Redirect must always be specified, otherwise
129-
// an old redirect could apply!
130-
http.SetCookie(rw, &http.Cookie{
131-
Name: codersdk.OAuth2RedirectCookie,
132-
Value: redirect,
133-
Path: "/",
134-
HttpOnly: true,
135-
SameSite: http.SameSiteLaxMode,
136-
})
137-
138-
http.Redirect(rw, r, config.AuthCodeURL(state, opts...), http.StatusTemporaryRedirect)
139-
return
140-
}
141-
142-
if state == "" {
113+
// Unmarshal JSON into DeviceAuthResponse
114+
da = &oauth2.DeviceAuthResponse{}
115+
if err := json.Unmarshal(decodedBytes, da); err != nil {
116+
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
117+
Message: "Invalid device code data",
118+
Detail: err.Error(),
119+
})
120+
return
121+
}
122+
} else {
143123
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
144-
Message: "State must be provided.",
124+
Message: "Invalid device code data",
125+
Detail: "Device code is required for device flow.",
145126
})
146127
return
147128
}
148129

149-
stateCookie, err := r.Cookie(codersdk.OAuth2StateCookie)
150-
if err != nil {
151-
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
152-
Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.OAuth2StateCookie),
153-
})
154-
return
155-
}
156-
if stateCookie.Value != state {
157-
httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
158-
Message: "State mismatched.",
159-
})
160-
return
161-
}
130+
// if code == "" {
131+
// // If the code isn't provided, we'll redirect!
132+
// var state string
133+
// // If this url param is provided, then a user is trying to merge
134+
// // their account with an OIDC account. Their password would have
135+
// // been required to get to this point, so we do not need to verify
136+
// // their password again.
137+
// oidcMergeState := r.URL.Query().Get("oidc_merge_state")
138+
// if oidcMergeState != "" {
139+
// state = oidcMergeState
140+
// } else {
141+
// var err error
142+
// state, err = cryptorand.String(32)
143+
// if err != nil {
144+
// httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
145+
// Message: "Internal error generating state string.",
146+
// Detail: err.Error(),
147+
// })
148+
// return
149+
// }
150+
// }
151+
152+
http.SetCookie(rw, &http.Cookie{
153+
Name: codersdk.OAuth2StateCookie,
154+
Value: "hello",
155+
Path: "/",
156+
HttpOnly: true,
157+
SameSite: http.SameSiteLaxMode,
158+
})
159+
// // Redirect must always be specified, otherwise
160+
// // an old redirect could apply!
161+
// http.SetCookie(rw, &http.Cookie{
162+
// Name: codersdk.OAuth2RedirectCookie,
163+
// Value: redirect,
164+
// Path: "/",
165+
// HttpOnly: true,
166+
// SameSite: http.SameSiteLaxMode,
167+
// })
168+
169+
// http.Redirect(rw, r, config.AuthCodeURL(state, opts...), http.StatusTemporaryRedirect)
170+
// return
171+
// }
172+
173+
// if state == "" {
174+
// httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
175+
// Message: "State must be provided.",
176+
// })
177+
// return
178+
// }
179+
180+
// stateCookie, err := r.Cookie(codersdk.OAuth2StateCookie)
181+
// if err != nil {
182+
// httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
183+
// Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.OAuth2StateCookie),
184+
// })
185+
// return
186+
// }
187+
// if stateCookie.Value != state {
188+
// httpapi.Write(ctx, rw, http.StatusUnauthorized, codersdk.Response{
189+
// Message: "State mismatched.",
190+
// })
191+
// return
192+
// }
162193

163194
stateRedirect, err := r.Cookie(codersdk.OAuth2RedirectCookie)
164195
if err == nil {
165196
redirect = stateRedirect.Value
166197
}
167198

168-
oauthToken, err := config.Exchange(ctx, code)
199+
oauthToken, err := config.DeviceAccessToken(ctx, da)
169200
if err != nil {
170201
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
171202
Message: "Internal error exchanging Oauth code.",

coderd/httpmw/oauth2_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ func (*testOAuth2Provider) Exchange(_ context.Context, _ string, _ ...oauth2.Aut
3232
}, nil
3333
}
3434

35+
func (*testOAuth2Provider) DeviceAccessToken(_ context.Context, _ *oauth2.DeviceAuthResponse, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
36+
return &oauth2.Token{
37+
AccessToken: "hello",
38+
}, nil
39+
}
40+
3541
func (*testOAuth2Provider) TokenSource(_ context.Context, _ *oauth2.Token) oauth2.TokenSource {
3642
return nil
3743
}

coderd/oauthpki/oidcpki.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ func (ja *Config) Exchange(ctx context.Context, code string, opts ...oauth2.Auth
133133
return ja.cfg.Exchange(ctx, code, opts...)
134134
}
135135

136+
func (*Config) DeviceAccessToken(_ context.Context, _ *oauth2.DeviceAuthResponse, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
137+
panic("not implemented")
138+
}
139+
136140
func (ja *Config) jwtToken() (string, error) {
137141
now := time.Now()
138142
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{

coderd/promoauth/oauth2.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ import (
1414
type Oauth2Source string
1515

1616
const (
17-
SourceValidateToken Oauth2Source = "ValidateToken"
18-
SourceExchange Oauth2Source = "Exchange"
19-
SourceTokenSource Oauth2Source = "TokenSource"
20-
SourceAppInstallations Oauth2Source = "AppInstallations"
21-
SourceAuthorizeDevice Oauth2Source = "AuthorizeDevice"
17+
SourceValidateToken Oauth2Source = "ValidateToken"
18+
SourceExchange Oauth2Source = "Exchange"
19+
SourceTokenSource Oauth2Source = "TokenSource"
20+
SourceDeviceAccessToken Oauth2Source = "DeviceAccessToken"
21+
SourceAppInstallations Oauth2Source = "AppInstallations"
22+
SourceAuthorizeDevice Oauth2Source = "AuthorizeDevice"
2223

2324
SourceGitAPIAuthUser Oauth2Source = "GitAPIAuthUser"
2425
SourceGitAPIListEmails Oauth2Source = "GitAPIListEmails"
@@ -31,6 +32,7 @@ const (
3132
type OAuth2Config interface {
3233
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
3334
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
35+
DeviceAccessToken(ctx context.Context, da *oauth2.DeviceAuthResponse, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
3436
TokenSource(context.Context, *oauth2.Token) oauth2.TokenSource
3537
}
3638

@@ -226,6 +228,10 @@ func (c *Config) Exchange(ctx context.Context, code string, opts ...oauth2.AuthC
226228
return c.underlying.Exchange(c.wrapClient(ctx, SourceExchange), code, opts...)
227229
}
228230

231+
func (c *Config) DeviceAccessToken(ctx context.Context, da *oauth2.DeviceAuthResponse, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
232+
return c.underlying.DeviceAccessToken(c.wrapClient(ctx, SourceDeviceAccessToken), da, opts...)
233+
}
234+
229235
func (c *Config) TokenSource(ctx context.Context, token *oauth2.Token) oauth2.TokenSource {
230236
return c.underlying.TokenSource(c.wrapClient(ctx, SourceTokenSource), token)
231237
}

testutil/oauth2.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ func (c *OAuth2Config) Exchange(_ context.Context, _ string, _ ...oauth2.AuthCod
3535
return c.Token, nil
3636
}
3737

38+
func (c *OAuth2Config) DeviceAccessToken(_ context.Context, _ *oauth2.DeviceAuthResponse, _ ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
39+
if c.Token == nil {
40+
return &oauth2.Token{
41+
AccessToken: "access_token",
42+
RefreshToken: "refresh_token",
43+
Expiry: time.Now().Add(time.Hour),
44+
}, nil
45+
}
46+
return c.Token, nil
47+
}
48+
3849
func (c *OAuth2Config) TokenSource(_ context.Context, _ *oauth2.Token) oauth2.TokenSource {
3950
if c.TokenSourceFunc == nil {
4051
return OAuth2TokenSource(func() (*oauth2.Token, error) {

0 commit comments

Comments
 (0)