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

Commit7eb897a

Browse files
committed
Extract username into a separate package and add OIDC tests
1 parent50ee7ad commit7eb897a

File tree

9 files changed

+322
-79
lines changed

9 files changed

+322
-79
lines changed

‎.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"mattn",
4343
"mitchellh",
4444
"moby",
45+
"namesgenerator",
4546
"nfpms",
4647
"nhooyr",
4748
"nolint",

‎cli/server.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,6 @@ func server() *cobra.Command {
306306
Endpoint:oidcProvider.Endpoint(),
307307
Scopes:oidcScopes,
308308
},
309-
Provider:oidcProvider,
310309
Verifier:oidcProvider.Verifier(&oidc.Config{
311310
ClientID:oidcClientID,
312311
}),

‎coderd/coderdtest/coderdtest.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type Options struct {
6363
Authorizer rbac.Authorizer
6464
AzureCertificates x509.VerifyOptions
6565
GithubOAuth2Config*coderd.GithubOAuth2Config
66+
OIDCConfig*coderd.OIDCConfig
6667
GoogleTokenValidator*idtoken.Validator
6768
SSHKeygenAlgorithm gitsshkey.Algorithm
6869
APIRateLimitint
@@ -189,6 +190,7 @@ func newWithCloser(t *testing.T, options *Options) (*codersdk.Client, io.Closer)
189190
AWSCertificates:options.AWSCertificates,
190191
AzureCertificates:options.AzureCertificates,
191192
GithubOAuth2Config:options.GithubOAuth2Config,
193+
OIDCConfig:options.OIDCConfig,
192194
GoogleTokenValidator:options.GoogleTokenValidator,
193195
SSHKeygenAlgorithm:options.SSHKeygenAlgorithm,
194196
TURNServer:turnServer,

‎coderd/httpapi/httpapi.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,16 @@ import (
77
"fmt"
88
"net/http"
99
"reflect"
10-
"regexp"
1110
"strings"
1211

1312
"github.com/go-playground/validator/v10"
1413

14+
"github.com/coder/coder/coderd/username"
1515
"github.com/coder/coder/codersdk"
1616
)
1717

1818
var (
19-
validate*validator.Validate
20-
usernameRegex=regexp.MustCompile("^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$")
19+
validate*validator.Validate
2120
)
2221

2322
// This init is used to create a validator and register validation-specific
@@ -39,13 +38,7 @@ func init() {
3938
if!ok {
4039
returnfalse
4140
}
42-
iflen(str)>32 {
43-
returnfalse
44-
}
45-
iflen(str)<1 {
46-
returnfalse
47-
}
48-
returnusernameRegex.MatchString(str)
41+
returnusername.Valid(str)
4942
})
5043
iferr!=nil {
5144
panic(err)

‎coderd/httpapi/httpapi_test.go

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -81,71 +81,6 @@ func TestRead(t *testing.T) {
8181
})
8282
}
8383

84-
funcTestReadUsername(t*testing.T) {
85-
t.Parallel()
86-
// Tests whether usernames are valid or not.
87-
testCases:= []struct {
88-
Usernamestring
89-
Validbool
90-
}{
91-
{"1",true},
92-
{"12",true},
93-
{"123",true},
94-
{"12345678901234567890",true},
95-
{"123456789012345678901",true},
96-
{"a",true},
97-
{"a1",true},
98-
{"a1b2",true},
99-
{"a1b2c3d4e5f6g7h8i9j0",true},
100-
{"a1b2c3d4e5f6g7h8i9j0k",true},
101-
{"aa",true},
102-
{"abc",true},
103-
{"abcdefghijklmnopqrst",true},
104-
{"abcdefghijklmnopqrstu",true},
105-
{"wow-test",true},
106-
107-
{"",false},
108-
{" ",false},
109-
{" a",false},
110-
{" a ",false},
111-
{" 1",false},
112-
{"1 ",false},
113-
{" aa",false},
114-
{"aa ",false},
115-
{" 12",false},
116-
{"12 ",false},
117-
{" a1",false},
118-
{"a1 ",false},
119-
{" abcdefghijklmnopqrstu",false},
120-
{"abcdefghijklmnopqrstu ",false},
121-
{" 123456789012345678901",false},
122-
{" a1b2c3d4e5f6g7h8i9j0k",false},
123-
{"a1b2c3d4e5f6g7h8i9j0k ",false},
124-
{"bananas_wow",false},
125-
{"test--now",false},
126-
127-
{"123456789012345678901234567890123",false},
128-
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",false},
129-
{"123456789012345678901234567890123123456789012345678901234567890123",false},
130-
}
131-
typetoValidatestruct {
132-
Usernamestring`json:"username" validate:"username"`
133-
}
134-
for_,testCase:=rangetestCases {
135-
testCase:=testCase
136-
t.Run(testCase.Username,func(t*testing.T) {
137-
t.Parallel()
138-
rw:=httptest.NewRecorder()
139-
data,err:=json.Marshal(toValidate{testCase.Username})
140-
require.NoError(t,err)
141-
r:=httptest.NewRequest("POST","/",bytes.NewBuffer(data))
142-
143-
varvalidatetoValidate
144-
require.Equal(t,testCase.Valid,httpapi.Read(rw,r,&validate))
145-
})
146-
}
147-
}
148-
14984
funcWebsocketCloseMsg(t*testing.T) {
15085
t.Parallel()
15186

‎coderd/userauth.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/coder/coder/coderd/database"
1717
"github.com/coder/coder/coderd/httpapi"
1818
"github.com/coder/coder/coderd/httpmw"
19+
"github.com/coder/coder/coderd/username"
1920
"github.com/coder/coder/codersdk"
2021
)
2122

@@ -219,7 +220,6 @@ func (api *API) userOAuth2Github(rw http.ResponseWriter, r *http.Request) {
219220
typeOIDCConfigstruct {
220221
httpmw.OAuth2Config
221222

222-
Provider*oidc.Provider
223223
Verifier*oidc.IDTokenVerifier
224224
// EmailDomain is an optional domain to require when authenticating.
225225
EmailDomainstring
@@ -267,12 +267,30 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
267267
})
268268
return
269269
}
270+
ifclaims.Email=="" {
271+
httpapi.Write(rw,http.StatusBadRequest, codersdk.Response{
272+
Message:"No email found in OIDC payload!",
273+
})
274+
return
275+
}
270276
if!claims.Verified {
271277
httpapi.Write(rw,http.StatusForbidden, codersdk.Response{
272278
Message:fmt.Sprintf("Verify the %q email address on your OIDC provider to authenticate!",claims.Email),
273279
})
274280
return
275281
}
282+
// The username is a required property in Coder. We make a best-effort
283+
// attempt at using what the claims provide, but if that fails we will
284+
// generate a random username.
285+
if!username.Valid(claims.Username) {
286+
// If no username is provided, we can default to use the email address.
287+
// This will be converted in the from function below, so it's safe
288+
// to keep the domain.
289+
ifclaims.Username=="" {
290+
claims.Username=claims.Email
291+
}
292+
claims.Username=username.From(claims.Username)
293+
}
276294
ifapi.OIDCConfig.EmailDomain!="" {
277295
if!strings.HasSuffix(claims.Email,api.OIDCConfig.EmailDomain) {
278296
httpapi.Write(rw,http.StatusForbidden, codersdk.Response{
@@ -319,6 +337,7 @@ func (api *API) userOIDC(rw http.ResponseWriter, r *http.Request) {
319337
Message:"Failed to get user by email.",
320338
Detail:err.Error(),
321339
})
340+
return
322341
}
323342

324343
_,created:=api.createAPIKey(rw,r, database.InsertAPIKeyParams{

‎coderd/userauth_test.go

Lines changed: 149 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@ package coderd_test
22

33
import (
44
"context"
5+
"crypto"
6+
"crypto/rand"
7+
"crypto/rsa"
8+
"io"
59
"net/http"
610
"net/url"
711
"testing"
12+
"time"
813

14+
"github.com/coreos/go-oidc/v3/oidc"
15+
"github.com/golang-jwt/jwt"
916
"github.com/google/go-github/v43/github"
17+
"github.com/stretchr/testify/assert"
1018
"github.com/stretchr/testify/require"
1119
"golang.org/x/oauth2"
1220
"golang.org/x/xerrors"
@@ -16,13 +24,18 @@ import (
1624
"github.com/coder/coder/codersdk"
1725
)
1826

19-
typeoauth2Configstruct{}
27+
typeoauth2Configstruct {
28+
token*oauth2.Token
29+
}
2030

2131
func (*oauth2Config)AuthCodeURL(statestring,_...oauth2.AuthCodeOption)string {
2232
return"/?state="+url.QueryEscape(state)
2333
}
2434

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+
ifo.token!=nil {
37+
returno.token,nil
38+
}
2639
return&oauth2.Token{
2740
AccessToken:"token",
2841
},nil
@@ -249,6 +262,117 @@ func TestUserOAuth2Github(t *testing.T) {
249262
})
250263
}
251264

265+
funcTestUserOIDC(t*testing.T) {
266+
t.Parallel()
267+
268+
for_,tc:=range []struct {
269+
Namestring
270+
Claims jwt.MapClaims
271+
AllowSignupsbool
272+
EmailDomainstring
273+
Usernamestring
274+
StatusCodeint
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+
iftc.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+
funccreateOIDCConfig(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+
252376
funcoauth2Callback(t*testing.T,client*codersdk.Client)*http.Response {
253377
client.HTTPClient.CheckRedirect=func(req*http.Request,via []*http.Request)error {
254378
returnhttp.ErrUseLastResponse
@@ -269,3 +393,26 @@ func oauth2Callback(t *testing.T, client *codersdk.Client) *http.Response {
269393
})
270394
returnres
271395
}
396+
397+
funcoidcCallback(t*testing.T,client*codersdk.Client)*http.Response {
398+
t.Helper()
399+
client.HTTPClient.CheckRedirect=func(req*http.Request,via []*http.Request)error {
400+
returnhttp.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+
deferres.Body.Close()
414+
data,err:=io.ReadAll(res.Body)
415+
require.NoError(t,err)
416+
t.Log(string(data))
417+
returnres
418+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp