@@ -2,11 +2,19 @@ package coderd_test
2
2
3
3
import (
4
4
"context"
5
+ "crypto"
6
+ "crypto/rand"
7
+ "crypto/rsa"
8
+ "io"
5
9
"net/http"
6
10
"net/url"
7
11
"testing"
12
+ "time"
8
13
14
+ "github.com/coreos/go-oidc/v3/oidc"
15
+ "github.com/golang-jwt/jwt"
9
16
"github.com/google/go-github/v43/github"
17
+ "github.com/stretchr/testify/assert"
10
18
"github.com/stretchr/testify/require"
11
19
"golang.org/x/oauth2"
12
20
"golang.org/x/xerrors"
@@ -16,13 +24,18 @@ import (
16
24
"github.com/coder/coder/codersdk"
17
25
)
18
26
19
- type oauth2Config struct {}
27
+ type oauth2Config struct {
28
+ token * oauth2.Token
29
+ }
20
30
21
31
func (* oauth2Config ) AuthCodeURL (state string , _ ... oauth2.AuthCodeOption ) string {
22
32
return "/?state=" + url .QueryEscape (state )
23
33
}
24
34
25
- func (* oauth2Config ) Exchange (context.Context , string , ... oauth2.AuthCodeOption ) (* oauth2.Token , error ) {
35
+ func (o * oauth2Config ) Exchange (context.Context , string , ... oauth2.AuthCodeOption ) (* oauth2.Token , error ) {
36
+ if o .token != nil {
37
+ return o .token , nil
38
+ }
26
39
return & oauth2.Token {
27
40
AccessToken : "token" ,
28
41
}, nil
@@ -249,6 +262,117 @@ func TestUserOAuth2Github(t *testing.T) {
249
262
})
250
263
}
251
264
265
+ func TestUserOIDC (t * testing.T ) {
266
+ t .Parallel ()
267
+
268
+ for _ , tc := range []struct {
269
+ Name string
270
+ Claims jwt.MapClaims
271
+ AllowSignups bool
272
+ EmailDomain string
273
+ Username string
274
+ StatusCode int
275
+ }{{
276
+ Name : "EmailNotVerified" ,
277
+ Claims : jwt.MapClaims {
278
+ "email" : "kyle@kwc.io" ,
279
+ },
280
+ AllowSignups : true ,
281
+ StatusCode : http .StatusForbidden ,
282
+ }, {
283
+ Name : "NotInRequiredEmailDomain" ,
284
+ Claims : jwt.MapClaims {
285
+ "email" : "kyle@kwc.io" ,
286
+ "email_verified" : true ,
287
+ },
288
+ AllowSignups : true ,
289
+ EmailDomain : "coder.com" ,
290
+ StatusCode : http .StatusForbidden ,
291
+ }, {
292
+ Name : "EmptyClaims" ,
293
+ Claims : jwt.MapClaims {},
294
+ AllowSignups : true ,
295
+ StatusCode : http .StatusBadRequest ,
296
+ }, {
297
+ Name : "NoSignups" ,
298
+ Claims : jwt.MapClaims {
299
+ "email" : "kyle@kwc.io" ,
300
+ "email_verified" : true ,
301
+ },
302
+ StatusCode : http .StatusForbidden ,
303
+ }, {
304
+ Name : "UsernameFromEmail" ,
305
+ Claims : jwt.MapClaims {
306
+ "email" : "kyle@kwc.io" ,
307
+ "email_verified" : true ,
308
+ },
309
+ Username : "kyle" ,
310
+ AllowSignups : true ,
311
+ StatusCode : http .StatusTemporaryRedirect ,
312
+ }, {
313
+ Name : "UsernameFromClaims" ,
314
+ Claims : jwt.MapClaims {
315
+ "email" : "kyle@kwc.io" ,
316
+ "email_verified" : true ,
317
+ "preferred_username" : "hotdog" ,
318
+ },
319
+ Username : "hotdog" ,
320
+ AllowSignups : true ,
321
+ StatusCode : http .StatusTemporaryRedirect ,
322
+ }} {
323
+ tc := tc
324
+ t .Run (tc .Name , func (t * testing.T ) {
325
+ t .Parallel ()
326
+ config := createOIDCConfig (t , tc .Claims )
327
+ config .AllowSignups = tc .AllowSignups
328
+ config .EmailDomain = tc .EmailDomain
329
+ client := coderdtest .New (t , & coderdtest.Options {
330
+ OIDCConfig : config ,
331
+ })
332
+ resp := oidcCallback (t , client )
333
+ assert .Equal (t , tc .StatusCode , resp .StatusCode )
334
+
335
+ if tc .Username != "" {
336
+ client .SessionToken = resp .Cookies ()[0 ].Value
337
+ user , err := client .User (context .Background (), "me" )
338
+ require .NoError (t , err )
339
+ require .Equal (t , tc .Username , user .Username )
340
+ }
341
+ })
342
+ }
343
+ }
344
+
345
+ // createOIDCConfig generates a new OIDCConfig that returns a static token
346
+ // with the claims provided.
347
+ func createOIDCConfig (t * testing.T , claims jwt.MapClaims ) * coderd.OIDCConfig {
348
+ t .Helper ()
349
+ key , err := rsa .GenerateKey (rand .Reader , 2048 )
350
+ require .NoError (t , err )
351
+
352
+ // https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
353
+ claims ["exp" ] = time .Now ().Add (time .Hour ).UnixMilli ()
354
+
355
+ signed , err := jwt .NewWithClaims (jwt .SigningMethodRS256 , claims ).SignedString (key )
356
+ require .NoError (t , err )
357
+
358
+ verifier := oidc .NewVerifier ("" , & oidc.StaticKeySet {
359
+ PublicKeys : []crypto.PublicKey {key .Public ()},
360
+ }, & oidc.Config {
361
+ SkipClientIDCheck : true ,
362
+ })
363
+
364
+ return & coderd.OIDCConfig {
365
+ OAuth2Config : & oauth2Config {
366
+ token : (& oauth2.Token {
367
+ AccessToken : "token" ,
368
+ }).WithExtra (map [string ]interface {}{
369
+ "id_token" : signed ,
370
+ }),
371
+ },
372
+ Verifier : verifier ,
373
+ }
374
+ }
375
+
252
376
func oauth2Callback (t * testing.T , client * codersdk.Client ) * http.Response {
253
377
client .HTTPClient .CheckRedirect = func (req * http.Request , via []* http.Request ) error {
254
378
return http .ErrUseLastResponse
@@ -269,3 +393,26 @@ func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response {
269
393
})
270
394
return res
271
395
}
396
+
397
+ func oidcCallback (t * testing.T , client * codersdk.Client ) * http.Response {
398
+ t .Helper ()
399
+ client .HTTPClient .CheckRedirect = func (req * http.Request , via []* http.Request ) error {
400
+ return http .ErrUseLastResponse
401
+ }
402
+ state := "somestate"
403
+ oauthURL , err := client .URL .Parse ("/api/v2/users/oidc/callback?code=asd&state=" + state )
404
+ require .NoError (t , err )
405
+ req , err := http .NewRequest ("GET" , oauthURL .String (), nil )
406
+ require .NoError (t , err )
407
+ req .AddCookie (& http.Cookie {
408
+ Name : "oauth_state" ,
409
+ Value : state ,
410
+ })
411
+ res , err := client .HTTPClient .Do (req )
412
+ require .NoError (t , err )
413
+ defer res .Body .Close ()
414
+ data , err := io .ReadAll (res .Body )
415
+ require .NoError (t , err )
416
+ t .Log (string (data ))
417
+ return res
418
+ }
0 commit comments