Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitdd9cb2f

Browse files
committed
chore: add OAuth2 device flow test scripts
Change-Id: Ic232851727e683ab3d8b7ce970c505588da2f827Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parentbdc12a1 commitdd9cb2f

File tree

4 files changed

+424
-8
lines changed

4 files changed

+424
-8
lines changed

‎scripts/oauth2/README.md

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,39 @@ export STATE="your-state"
102102
go run ./scripts/oauth2/oauth2-test-server.go
103103
```
104104

105+
###`test-device-flow.sh`
106+
107+
Tests the OAuth2 Device Authorization Flow (RFC 8628) using the golang.org/x/oauth2 library. This flow is designed for devices that either lack a web browser or have limited input capabilities.
108+
109+
Usage:
110+
111+
```bash
112+
# First set up an app
113+
eval$(./scripts/oauth2/setup-test-app.sh)
114+
115+
# Run the device flow test
116+
./scripts/oauth2/test-device-flow.sh
117+
```
118+
119+
Features:
120+
121+
- Implements the complete device authorization flow
122+
- Uses the`/x/oauth2` library for OAuth2 operations
123+
- Displays user code and verification URL
124+
- Automatically polls for token completion
125+
- Tests the access token with an API call
126+
- Colored output for better readability
127+
128+
###`oauth2-device-flow.go`
129+
130+
A Go program that implements the OAuth2 device authorization flow. Used internally by`test-device-flow.sh` but can also be run standalone:
131+
132+
```bash
133+
export CLIENT_ID="your-client-id"
134+
export CLIENT_SECRET="your-client-secret"
135+
go run ./scripts/oauth2/oauth2-device-flow.go
136+
```
137+
105138
##Example Workflow
106139

107140
1.**Run automated tests:**
@@ -126,7 +159,23 @@ go run ./scripts/oauth2/oauth2-test-server.go
126159
./scripts/oauth2/cleanup-test-app.sh
127160
```
128161

129-
3.**Generate PKCE for custom testing:**
162+
3.**Device authorization flow testing:**
163+
164+
```bash
165+
# Create app
166+
eval$(./scripts/oauth2/setup-test-app.sh)
167+
168+
# Run the device flow test
169+
./scripts/oauth2/test-device-flow.sh
170+
# - Shows device code and verification URL
171+
# - Polls for authorization completion
172+
# - Tests access token
173+
174+
# Clean up when done
175+
./scripts/oauth2/cleanup-test-app.sh
176+
```
177+
178+
4.**Generate PKCE for custom testing:**
130179

131180
```bash
132181
./scripts/oauth2/generate-pkce.sh
@@ -147,4 +196,5 @@ All scripts respect these environment variables:
147196
- Metadata:`GET /.well-known/oauth-authorization-server`
148197
- Authorization:`GET/POST /oauth2/authorize`
149198
- Token:`POST /oauth2/token`
199+
- Device Authorization:`POST /oauth2/device`
150200
- Apps API:`/api/v2/oauth2-provider/apps`

‎scripts/oauth2/device/server.go

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"strings"
12+
"time"
13+
14+
"golang.org/x/oauth2"
15+
"golang.org/x/xerrors"
16+
)
17+
18+
const (
19+
// ANSI color codes
20+
colorReset="\033[0m"
21+
colorRed="\033[31m"
22+
colorGreen="\033[32m"
23+
colorYellow="\033[33m"
24+
colorBlue="\033[34m"
25+
colorPurple="\033[35m"
26+
colorCyan="\033[36m"
27+
colorWhite="\033[37m"
28+
)
29+
30+
typeDeviceCodeResponsestruct {
31+
DeviceCodestring`json:"device_code"`
32+
UserCodestring`json:"user_code"`
33+
VerificationURIstring`json:"verification_uri"`
34+
VerificationURICompletestring`json:"verification_uri_complete,omitempty"`
35+
ExpiresInint`json:"expires_in"`
36+
Intervalint`json:"interval"`
37+
}
38+
39+
typeTokenResponsestruct {
40+
AccessTokenstring`json:"access_token"`
41+
TokenTypestring`json:"token_type"`
42+
ExpiresInint`json:"expires_in"`
43+
RefreshTokenstring`json:"refresh_token,omitempty"`
44+
Scopestring`json:"scope,omitempty"`
45+
}
46+
47+
typeErrorResponsestruct {
48+
Errorstring`json:"error"`
49+
ErrorDescriptionstring`json:"error_description,omitempty"`
50+
}
51+
52+
typeConfigstruct {
53+
ClientIDstring
54+
ClientSecretstring
55+
BaseURLstring
56+
}
57+
58+
funcmain() {
59+
config:=&Config{
60+
ClientID:os.Getenv("CLIENT_ID"),
61+
ClientSecret:os.Getenv("CLIENT_SECRET"),
62+
BaseURL:getEnvOrDefault("BASE_URL","http://localhost:3000"),
63+
}
64+
65+
ifconfig.ClientID==""||config.ClientSecret=="" {
66+
log.Fatal("CLIENT_ID and CLIENT_SECRET must be set. Run: eval $(./setup-test-app.sh) first")
67+
}
68+
69+
ctx:=context.Background()
70+
71+
// Step 1: Request device code
72+
_,_=fmt.Printf("%s=== Step 1: Device Code Request ===%s\n",colorBlue,colorReset)
73+
deviceResp,err:=requestDeviceCode(ctx,config)
74+
iferr!=nil {
75+
log.Fatalf("Failed to get device code: %v",err)
76+
}
77+
78+
_,_=fmt.Printf("%sDevice Code Response:%s\n",colorGreen,colorReset)
79+
prettyJSON,_:=json.MarshalIndent(deviceResp,""," ")
80+
_,_=fmt.Printf("%s\n",prettyJSON)
81+
_,_=fmt.Println()
82+
83+
// Step 2: Display user instructions
84+
_,_=fmt.Printf("%s=== Step 2: User Authorization ===%s\n",colorYellow,colorReset)
85+
_,_=fmt.Printf("Please visit: %s%s%s\n",colorCyan,deviceResp.VerificationURI,colorReset)
86+
_,_=fmt.Printf("Enter code: %s%s%s\n",colorPurple,deviceResp.UserCode,colorReset)
87+
_,_=fmt.Println()
88+
89+
ifdeviceResp.VerificationURIComplete!="" {
90+
_,_=fmt.Printf("Or visit the complete URL: %s%s%s\n",colorCyan,deviceResp.VerificationURIComplete,colorReset)
91+
_,_=fmt.Println()
92+
}
93+
94+
_,_=fmt.Printf("Waiting for authorization (expires in %d seconds)...\n",deviceResp.ExpiresIn)
95+
_,_=fmt.Printf("Polling every %d seconds...\n",deviceResp.Interval)
96+
_,_=fmt.Println()
97+
98+
// Step 3: Poll for token
99+
_,_=fmt.Printf("%s=== Step 3: Token Polling ===%s\n",colorBlue,colorReset)
100+
tokenResp,err:=pollForToken(ctx,config,deviceResp)
101+
iferr!=nil {
102+
log.Fatalf("Failed to get access token: %v",err)
103+
}
104+
105+
_,_=fmt.Printf("%s=== Authorization Successful! ===%s\n",colorGreen,colorReset)
106+
_,_=fmt.Printf("%sAccess Token Response:%s\n",colorGreen,colorReset)
107+
prettyTokenJSON,_:=json.MarshalIndent(tokenResp,""," ")
108+
_,_=fmt.Printf("%s\n",prettyTokenJSON)
109+
_,_=fmt.Println()
110+
111+
// Step 4: Test the access token
112+
_,_=fmt.Printf("%s=== Step 4: Testing Access Token ===%s\n",colorBlue,colorReset)
113+
iferr:=testAccessToken(ctx,config,tokenResp.AccessToken);err!=nil {
114+
log.Printf("%sWarning: Failed to test access token: %v%s",colorYellow,err,colorReset)
115+
}else {
116+
_,_=fmt.Printf("%sAccess token is valid and working!%s\n",colorGreen,colorReset)
117+
}
118+
119+
_,_=fmt.Println()
120+
_,_=fmt.Printf("%sDevice authorization flow completed successfully!%s\n",colorGreen,colorReset)
121+
_,_=fmt.Printf("You can now use the access token to make authenticated API requests.\n")
122+
}
123+
124+
funcrequestDeviceCode(ctx context.Context,config*Config) (*DeviceCodeResponse,error) {
125+
// Use x/oauth2 clientcredentials config to structure the request
126+
// clientConfig := &clientcredentials.Config{
127+
// ClientID: config.ClientID,
128+
// ClientSecret: config.ClientSecret,
129+
// TokenURL: config.BaseURL + "/oauth2/device", // Device code endpoint (RFC 8628)
130+
// }
131+
132+
// Create form data for device code request
133+
data:= url.Values{}
134+
data.Set("client_id",config.ClientID)
135+
136+
// Optional: Add scope parameter
137+
// data.Set("scope", "openid profile")
138+
139+
// Make the request to the device authorization endpoint
140+
req,err:=http.NewRequestWithContext(ctx,"POST",config.BaseURL+"/oauth2/device",strings.NewReader(data.Encode()))
141+
iferr!=nil {
142+
returnnil,xerrors.Errorf("creating request: %w",err)
143+
}
144+
145+
// Set up basic auth with client credentials
146+
req.SetBasicAuth(config.ClientID,config.ClientSecret)
147+
req.Header.Set("Content-Type","application/x-www-form-urlencoded")
148+
149+
client:=&http.Client{Timeout:30*time.Second}
150+
resp,err:=client.Do(req)
151+
iferr!=nil {
152+
returnnil,xerrors.Errorf("making request: %w",err)
153+
}
154+
deferfunc() {_=resp.Body.Close() }()
155+
156+
ifresp.StatusCode!=http.StatusOK {
157+
varerrRespErrorResponse
158+
iferr:=json.NewDecoder(resp.Body).Decode(&errResp);err==nil {
159+
returnnil,xerrors.Errorf("device code request failed: %s - %s",errResp.Error,errResp.ErrorDescription)
160+
}
161+
returnnil,xerrors.Errorf("device code request failed with status %d",resp.StatusCode)
162+
}
163+
164+
vardeviceRespDeviceCodeResponse
165+
iferr:=json.NewDecoder(resp.Body).Decode(&deviceResp);err!=nil {
166+
returnnil,xerrors.Errorf("decoding response: %w",err)
167+
}
168+
169+
return&deviceResp,nil
170+
}
171+
172+
funcpollForToken(ctx context.Context,config*Config,deviceResp*DeviceCodeResponse) (*TokenResponse,error) {
173+
// Use x/oauth2 config for token exchange
174+
oauth2Config:=&oauth2.Config{
175+
ClientID:config.ClientID,
176+
ClientSecret:config.ClientSecret,
177+
Endpoint: oauth2.Endpoint{
178+
TokenURL:config.BaseURL+"/oauth2/token",
179+
},
180+
}
181+
182+
interval:=time.Duration(deviceResp.Interval)*time.Second
183+
ifinterval<5*time.Second {
184+
interval=5*time.Second// Minimum polling interval
185+
}
186+
187+
deadline:=time.Now().Add(time.Duration(deviceResp.ExpiresIn)*time.Second)
188+
ticker:=time.NewTicker(interval)
189+
deferticker.Stop()
190+
191+
for {
192+
select {
193+
case<-ctx.Done():
194+
returnnil,ctx.Err()
195+
case<-ticker.C:
196+
iftime.Now().After(deadline) {
197+
returnnil,xerrors.New("device code expired")
198+
}
199+
200+
_,_=fmt.Printf("Polling for token...\n")
201+
202+
// Create token exchange request using device_code grant
203+
data:= url.Values{}
204+
data.Set("grant_type","urn:ietf:params:oauth:grant-type:device_code")
205+
data.Set("device_code",deviceResp.DeviceCode)
206+
data.Set("client_id",config.ClientID)
207+
208+
req,err:=http.NewRequestWithContext(ctx,"POST",oauth2Config.Endpoint.TokenURL,strings.NewReader(data.Encode()))
209+
iferr!=nil {
210+
returnnil,xerrors.Errorf("creating token request: %w",err)
211+
}
212+
213+
req.SetBasicAuth(config.ClientID,config.ClientSecret)
214+
req.Header.Set("Content-Type","application/x-www-form-urlencoded")
215+
216+
client:=&http.Client{Timeout:30*time.Second}
217+
resp,err:=client.Do(req)
218+
iferr!=nil {
219+
_,_=fmt.Printf("Request error: %v\n",err)
220+
continue
221+
}
222+
223+
varresultmap[string]interface{}
224+
iferr:=json.NewDecoder(resp.Body).Decode(&result);err!=nil {
225+
_=resp.Body.Close()
226+
_,_=fmt.Printf("Decode error: %v\n",err)
227+
continue
228+
}
229+
_=resp.Body.Close()
230+
231+
iferrorCode,ok:=result["error"].(string);ok {
232+
switcherrorCode {
233+
case"authorization_pending":
234+
_,_=fmt.Printf("Authorization pending... continuing to poll\n")
235+
continue
236+
case"slow_down":
237+
_,_=fmt.Printf("Slow down request - increasing polling interval by 5 seconds\n")
238+
interval+=5*time.Second
239+
ticker.Reset(interval)
240+
continue
241+
case"access_denied":
242+
returnnil,xerrors.New("access denied by user")
243+
case"expired_token":
244+
returnnil,xerrors.New("device code expired")
245+
default:
246+
desc:=""
247+
iferrorDesc,ok:=result["error_description"].(string);ok {
248+
desc=" - "+errorDesc
249+
}
250+
returnnil,xerrors.Errorf("token error: %s%s",errorCode,desc)
251+
}
252+
}
253+
254+
// Success case - convert to TokenResponse
255+
vartokenRespTokenResponse
256+
ifaccessToken,ok:=result["access_token"].(string);ok {
257+
tokenResp.AccessToken=accessToken
258+
}
259+
iftokenType,ok:=result["token_type"].(string);ok {
260+
tokenResp.TokenType=tokenType
261+
}
262+
ifexpiresIn,ok:=result["expires_in"].(float64);ok {
263+
tokenResp.ExpiresIn=int(expiresIn)
264+
}
265+
ifrefreshToken,ok:=result["refresh_token"].(string);ok {
266+
tokenResp.RefreshToken=refreshToken
267+
}
268+
ifscope,ok:=result["scope"].(string);ok {
269+
tokenResp.Scope=scope
270+
}
271+
272+
iftokenResp.AccessToken=="" {
273+
returnnil,xerrors.New("no access token in response")
274+
}
275+
276+
return&tokenResp,nil
277+
}
278+
}
279+
}
280+
281+
functestAccessToken(ctx context.Context,config*Config,accessTokenstring)error {
282+
req,err:=http.NewRequestWithContext(ctx,"GET",config.BaseURL+"/api/v2/users/me",nil)
283+
iferr!=nil {
284+
returnxerrors.Errorf("creating request: %w",err)
285+
}
286+
287+
req.Header.Set("Coder-Session-Token",accessToken)
288+
289+
client:=&http.Client{Timeout:10*time.Second}
290+
resp,err:=client.Do(req)
291+
iferr!=nil {
292+
returnxerrors.Errorf("making request: %w",err)
293+
}
294+
deferfunc() {_=resp.Body.Close() }()
295+
296+
ifresp.StatusCode!=http.StatusOK {
297+
returnxerrors.Errorf("API request failed with status %d",resp.StatusCode)
298+
}
299+
300+
varuserInfomap[string]interface{}
301+
iferr:=json.NewDecoder(resp.Body).Decode(&userInfo);err!=nil {
302+
returnxerrors.Errorf("decoding response: %w",err)
303+
}
304+
305+
_,_=fmt.Printf("%sAPI Test Response:%s\n",colorGreen,colorReset)
306+
prettyJSON,_:=json.MarshalIndent(userInfo,""," ")
307+
_,_=fmt.Printf("%s\n",prettyJSON)
308+
309+
returnnil
310+
}
311+
312+
funcgetEnvOrDefault(key,defaultValuestring)string {
313+
ifvalue:=os.Getenv(key);value!="" {
314+
returnvalue
315+
}
316+
returndefaultValue
317+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp