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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy