Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/public-api-server/pkg/identityprovider/idp_test.go
2500 views
1
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package identityprovider
6
7
import (
8
"context"
9
"crypto"
10
"io"
11
"net/http"
12
"net/http/httptest"
13
"testing"
14
15
"github.com/golang-jwt/jwt/v5"
16
"github.com/google/go-cmp/cmp"
17
"github.com/google/go-cmp/cmp/cmpopts"
18
"github.com/zitadel/oidc/pkg/oidc"
19
"gopkg.in/square/go-jose.v2"
20
)
21
22
const (
23
issuerBaseURL = "https://api.gitpod.io/idp"
24
)
25
26
func TestRouter(t *testing.T) {
27
type Expectation struct {
28
Error string
29
Response string
30
}
31
tests := []struct {
32
Name string
33
Expectation Expectation
34
ResponseExpectation func(*Service) string
35
ExpectedHeaders map[string]string
36
Path string
37
}{
38
{
39
Name: "OIDC discovery",
40
Path: oidc.DiscoveryEndpoint,
41
Expectation: Expectation{
42
Response: `{"issuer":"https://api.gitpod.io/idp","authorization_endpoint":"https://api.gitpod.io/idp/not-supported","token_endpoint":"https://api.gitpod.io/idp/not-supported","introspection_endpoint":"https://api.gitpod.io/idp/not-supported","userinfo_endpoint":"https://api.gitpod.io/idp/not-supported","revocation_endpoint":"https://api.gitpod.io/idp/not-supported","end_session_endpoint":"https://api.gitpod.io/idp/not-supported","jwks_uri":"https://api.gitpod.io/idp/keys","scopes_supported":["openid","profile","email","phone","address","offline_access"],"response_types_supported":["code","id_token","id_token token"],"grant_types_supported":["authorization_code","implicit"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"revocation_endpoint_auth_methods_supported":["none"],"introspection_endpoint_auth_methods_supported":["none"],"introspection_endpoint_auth_signing_alg_values_supported":["RS256"],"claims_supported":["sub","aud","exp","iat","iss","auth_time","nonce","acr","amr","c_hash","at_hash","act","scopes","client_id","azp","preferred_username","name","family_name","given_name","locale","email"],"request_uri_parameter_supported":false}` + "\n",
43
},
44
ExpectedHeaders: map[string]string{
45
"Content-Type": "application/json",
46
},
47
},
48
{
49
Name: "keys",
50
Path: "/keys",
51
ResponseExpectation: func(s *Service) string {
52
r, _ := s.keys.PublicKeys(context.Background())
53
return string(r)
54
},
55
ExpectedHeaders: map[string]string{
56
"Content-Type": "application/json",
57
},
58
},
59
}
60
61
for _, test := range tests {
62
t.Run(test.Name, func(t *testing.T) {
63
service, err := NewService(issuerBaseURL, NewInMemoryCache())
64
if err != nil {
65
t.Fatal(err)
66
}
67
server := httptest.NewServer(service.Router())
68
t.Cleanup(server.Close)
69
70
resp, err := http.Get(server.URL + test.Path)
71
if err != nil {
72
t.Fatal(err)
73
}
74
respBody, err := io.ReadAll(resp.Body)
75
76
var act Expectation
77
act.Response = string(respBody)
78
if err != nil {
79
act.Error = err.Error()
80
}
81
82
if test.ResponseExpectation != nil {
83
test.Expectation.Response = test.ResponseExpectation(service)
84
}
85
86
if diff := cmp.Diff(test.Expectation, act); diff != "" {
87
t.Errorf("Router() mismatch (-want +got):\n%s", diff)
88
}
89
90
for name, expected := range test.ExpectedHeaders {
91
actual := resp.Header.Get(name)
92
if actual != expected {
93
t.Errorf("Unexpected value for header '%s'. got: '%s', want: '%s'", name, actual, expected)
94
}
95
}
96
})
97
}
98
}
99
100
func TestIDToken(t *testing.T) {
101
type Expectation struct {
102
Error string
103
Token *jwt.Token
104
}
105
tests := []struct {
106
Name string
107
Expectation Expectation
108
Org string
109
Audience []string
110
UserInfo oidc.UserInfo
111
}{
112
{
113
Name: "all empty",
114
Expectation: Expectation{
115
Error: "audience cannot be empty",
116
},
117
},
118
{
119
Name: "just audience",
120
Audience: []string{"some.audience.com"},
121
Expectation: Expectation{
122
Error: "user info cannot be nil",
123
},
124
},
125
{
126
Name: "with user info",
127
Audience: []string{"some.audience.com"},
128
UserInfo: func() oidc.UserInfo {
129
userInfo := oidc.NewUserInfo()
130
userInfo.SetName("foo")
131
userInfo.SetSubject("bar")
132
return userInfo
133
}(),
134
Expectation: Expectation{
135
Token: &jwt.Token{
136
Method: &jwt.SigningMethodRSA{Name: "RS256", Hash: crypto.SHA256},
137
Header: map[string]interface{}{"alg": string(jose.RS256)},
138
Claims: jwt.MapClaims{
139
"aud": []any{string("some.audience.com")},
140
"azp": string("some.audience.com"),
141
"iss": string("https://api.gitpod.io/idp"),
142
"name": "foo",
143
"sub": "bar",
144
},
145
Valid: true,
146
},
147
},
148
},
149
{
150
Name: "with custom claims",
151
Audience: []string{"some.audience.com"},
152
UserInfo: func() oidc.UserInfo {
153
userInfo := oidc.NewUserInfo()
154
userInfo.AppendClaims("scope", "foobar")
155
return userInfo
156
}(),
157
Expectation: Expectation{
158
Token: &jwt.Token{
159
Method: &jwt.SigningMethodRSA{Name: "RS256", Hash: crypto.SHA256},
160
Header: map[string]interface{}{"alg": string(jose.RS256)},
161
Claims: jwt.MapClaims{
162
"aud": []any{string("some.audience.com")},
163
"azp": string("some.audience.com"),
164
"iss": string("https://api.gitpod.io/idp"),
165
"scope": "foobar",
166
},
167
Valid: true,
168
},
169
},
170
},
171
}
172
173
for _, test := range tests {
174
t.Run(test.Name, func(t *testing.T) {
175
cache := NewInMemoryCache()
176
service, err := NewService(issuerBaseURL, cache)
177
if err != nil {
178
t.Fatal(err)
179
}
180
181
var act Expectation
182
token, err := service.IDToken(context.TODO(), test.Org, test.Audience, test.UserInfo)
183
if err != nil {
184
act.Error = err.Error()
185
} else {
186
parsedToken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { return &cache.current.PublicKey, nil })
187
if err != nil {
188
t.Fatalf("cannot parse IDToken result: %v", err)
189
}
190
act.Token = parsedToken
191
}
192
193
if diff := cmp.Diff(test.Expectation, act, cmpJWTToken()...); diff != "" {
194
t.Errorf("IDToken() mismatch (-want +got):\n%s", diff)
195
}
196
})
197
}
198
}
199
200
func cmpJWTToken() []cmp.Option {
201
return []cmp.Option{
202
cmpopts.IgnoreFields(jwt.Token{}, "Raw", "Signature"),
203
cmpopts.IgnoreMapEntries(func(k string, v any) bool {
204
_, ignore := map[string]struct{}{
205
"auth_time": {},
206
"c_hash": {},
207
"exp": {},
208
"iat": {},
209
"iss": {},
210
}[k]
211
return ignore
212
}),
213
}
214
}
215
216