main
1package main
2
3import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "net/http"
8 "net/url"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/playwright-community/playwright-go"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16 "github.com/xlgmokha/x/pkg/env"
17 "github.com/xlgmokha/x/pkg/serde"
18 "github.com/xlgmokha/x/pkg/x"
19 "golang.org/x/oauth2"
20)
21
22func TestAuthx(t *testing.T) {
23 if env.Fetch("SKIP_E2E", "") != "" {
24 t.Skip()
25 }
26
27 _ = playwright.Install()
28
29 pw := x.Must(playwright.Run())
30 browser := x.Must(pw.Firefox.Launch(playwright.BrowserTypeLaunchOptions{
31 Headless: playwright.Bool(env.Fetch("HEADLESS", "true") == "true"),
32 SlowMo: playwright.Float(1000),
33 }))
34 page := x.Must(browser.NewPage())
35
36 client := &http.Client{Timeout: 2 * time.Second}
37
38 defer func() {
39 x.Check(browser.Close())
40 x.Check(pw.Stop())
41 }()
42
43 t.Run("SAML", func(t *testing.T) {
44 for _, url := range []string{"http://idp.example.com:8080/saml/metadata.xml", "http://ui.example.com:8080/saml/metadata.xml"} {
45 t.Run("GET "+url, func(t *testing.T) {
46 response := x.Must(http.Get(url))
47 assert.Equal(t, http.StatusOK, response.StatusCode)
48 })
49 }
50
51 t.Run("GET http://ui.example.com:8080/saml/new", func(t *testing.T) {
52 assert.NoError(t, page.Context().ClearCookies())
53 x.Must(page.Goto("http://ui.example.com:8080/saml/new"))
54 action := x.Must(page.Locator("#idp-form").GetAttribute("action"))
55 assert.Equal(t, "http://idp.example.com:8080/saml/new", action)
56 assert.NoError(t, page.Locator("#submit-button").Click())
57
58 page.Locator("#username").Fill("root")
59 page.Locator("#password").Fill("root")
60 assert.NoError(t, page.Locator("#login-button").Click())
61
62 action = x.Must(page.Locator("#postback-form").GetAttribute("action"))
63 assert.Equal(t, "http://ui.example.com:8080/saml/assertions", action)
64 assert.NoError(t, page.Locator("#submit-button").Click())
65 assert.Contains(t, x.Must(page.Content()), "Received SAML Response")
66
67 t.Run("generates a usable access token", func(t *testing.T) {
68 rawToken := x.Must(page.Locator("#access-token").TextContent())
69 accessToken := strings.Replace(rawToken, "\"", "", -1)
70 assert.NotEmpty(t, accessToken)
71
72 t.Run("GET http://api.example.com:8080/projects.json", func(t *testing.T) {
73 request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/projects.json", nil))
74 request.Header.Add("Authorization", "Bearer "+accessToken)
75 response := x.Must(client.Do(request))
76 require.Equal(t, http.StatusOK, response.StatusCode)
77 projects := x.Must(serde.FromJSON[[]map[string]string](response.Body))
78 assert.NotNil(t, projects)
79 })
80 })
81
82 t.Run("exchange SAML assertion for access token", func(t *testing.T) {
83 samlAssertion := x.Must(page.Locator("#raw-saml-response").TextContent())
84 io := bytes.NewBuffer(nil)
85 assert.NoError(t, serde.ToJSON(io, map[string]string{
86 "assertion": samlAssertion,
87 "grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer",
88 }))
89 request := x.Must(http.NewRequestWithContext(t.Context(), "POST", "http://idp.example.com:8080/oauth/token", io))
90 request.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("client_id:client_secret")))
91 request.Header.Add("Content-Type", "application/json ")
92 response := x.Must(client.Do(request))
93 require.Equal(t, http.StatusOK, response.StatusCode)
94 })
95 })
96 })
97
98 t.Run("OIDC", func(t *testing.T) {
99 t.Run("GET http://ui.example.com:8080/oidc/new", func(t *testing.T) {
100 assert.NoError(t, page.Context().ClearCookies())
101 x.Must(page.Goto("http://ui.example.com:8080/oidc/new"))
102
103 assert.Contains(t, page.URL(), "http://idp.example.com:8080/sessions/new")
104 page.Locator("#username").Fill("root")
105 page.Locator("#password").Fill("root")
106 assert.NoError(t, page.Locator("#login-button").Click())
107
108 assert.Contains(t, page.URL(), "http://idp.example.com:8080/oauth/authorize/continue")
109 assert.NoError(t, page.Locator("#submit-button").Click())
110
111 assert.Contains(t, page.URL(), "http://ui.example.com:8080/oauth/callback")
112 content := x.Must(page.Locator("pre").First().InnerText())
113 item := x.Must(serde.FromJSON[oauth2.Token](strings.NewReader(content)))
114 require.NotEmpty(t, item.AccessToken)
115 require.Equal(t, "Bearer", item.TokenType)
116 require.NotEmpty(t, item.RefreshToken)
117
118 t.Run("GET http://api.example.com:8080/organizations.json", func(t *testing.T) {
119 response := x.Must(http.Get("http://api.example.com:8080/organizations.json"))
120 assert.Equal(t, http.StatusForbidden, response.StatusCode)
121 })
122
123 t.Run("GET http://api.example.com:8080/organizations.json with Authorization", func(t *testing.T) {
124 request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/organizations.json", nil))
125 request.Header.Add("Authorization", "Bearer "+item.AccessToken)
126 response := x.Must(client.Do(request))
127 require.Equal(t, http.StatusOK, response.StatusCode)
128 organizations := x.Must(serde.FromJSON[[]map[string]string](response.Body))
129 assert.NotNil(t, organizations)
130 })
131
132 t.Run("GET http://api.example.com:8080/groups.json", func(t *testing.T) {
133 response := x.Must(http.Get("http://api.example.com:8080/groups.json"))
134 assert.Equal(t, http.StatusForbidden, response.StatusCode)
135 })
136
137 t.Run("GET http://api.example.com:8080/groups.json with Authorization", func(t *testing.T) {
138 request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/groups.json", nil))
139 request.Header.Add("Authorization", "Bearer "+item.AccessToken)
140 response := x.Must(client.Do(request))
141 require.Equal(t, http.StatusOK, response.StatusCode)
142 groups := x.Must(serde.FromJSON[[]map[string]string](response.Body))
143 assert.NotNil(t, groups)
144 })
145
146 t.Run("GET http://api.example.com:8080/projects.json", func(t *testing.T) {
147 response := x.Must(http.Get("http://api.example.com:8080/projects.json"))
148 assert.Equal(t, http.StatusForbidden, response.StatusCode)
149 })
150
151 t.Run("GET http://api.example.com:8080/projects.json with Authorization", func(t *testing.T) {
152 request := x.Must(http.NewRequestWithContext(t.Context(), "GET", "http://api.example.com:8080/projects.json", nil))
153 request.Header.Add("Authorization", "Bearer "+item.AccessToken)
154 response := x.Must(client.Do(request))
155 require.Equal(t, http.StatusOK, response.StatusCode)
156 projects := x.Must(serde.FromJSON[[]map[string]string](response.Body))
157 assert.NotNil(t, projects)
158 })
159
160 t.Run("creates a new project", func(t *testing.T) {
161 io := bytes.NewBuffer(nil)
162 assert.NoError(t, serde.ToJSON(io, map[string]string{"name": "example"}))
163 request := x.Must(http.NewRequestWithContext(t.Context(), "POST", "http://api.example.com:8080/projects", io))
164 request.Header.Add("Authorization", "Bearer "+item.AccessToken)
165 response := x.Must(client.Do(request))
166 require.Equal(t, http.StatusCreated, response.StatusCode)
167 project := x.Must(serde.FromJSON[map[string]string](response.Body))
168 assert.Equal(t, "example", project["name"])
169 })
170
171 t.Run("creates another project", func(t *testing.T) {
172 io := bytes.NewBuffer(nil)
173 assert.NoError(t, serde.ToJSON(io, map[string]string{"name": "example2"}))
174 request := x.Must(http.NewRequestWithContext(t.Context(), "POST", "http://api.example.com:8080/projects.json", io))
175 request.Header.Add("Authorization", "Bearer "+item.AccessToken)
176 response := x.Must(client.Do(request))
177 require.Equal(t, http.StatusCreated, response.StatusCode)
178 project := x.Must(serde.FromJSON[map[string]string](response.Body))
179 assert.Equal(t, "example2", project["name"])
180 })
181 })
182 })
183
184 t.Run("OAuth", func(t *testing.T) {
185 t.Run("GET /.well-known/oauth-authorization-server", func(t *testing.T) {
186 response := x.Must(client.Get("http://idp.example.com:8080/.well-known/oauth-authorization-server"))
187 require.Equal(t, http.StatusOK, response.StatusCode)
188 metadata := x.Must(serde.FromJSON[map[string]interface{}](response.Body))
189 assert.Equal(t, "http://idp.example.com:8080/.well-known/oauth-authorization-server", metadata["issuer"])
190 assert.Equal(t, "http://idp.example.com:8080/oauth/authorize", metadata["authorization_endpoint"])
191 assert.Equal(t, "http://idp.example.com:8080/oauth/token", metadata["token_endpoint"])
192 // assert.NotEmpty(t, metadata["jwks_uri"])
193 // assert.NotEmpty(t, metadata["registration_endpoint"])
194 assert.NotEmpty(t, metadata["scopes_supported"])
195 assert.NotEmpty(t, metadata["response_types_supported"])
196 assert.NotEmpty(t, metadata["response_modes_supported"])
197 assert.NotEmpty(t, metadata["grant_types_supported"])
198 assert.NotEmpty(t, metadata["token_endpoint_auth_methods_supported"])
199 assert.NotEmpty(t, metadata["token_endpoint_auth_signing_alg_values_supported"])
200 // assert.NotEmpty(t, metadata["service_documentation"])
201 assert.NotEmpty(t, metadata["ui_locales_supported"])
202 // assert.NotEmpty(t, metadata["op_policy_uri"])
203 // assert.NotEmpty(t, metadata["op_tos_uri"])
204 assert.NotEmpty(t, metadata["revocation_endpoint"])
205 assert.NotEmpty(t, metadata["revocation_endpoint_auth_methods_supported"])
206 assert.NotEmpty(t, metadata["revocation_endpoint_auth_signing_alg_values_supported"])
207 assert.NotEmpty(t, metadata["introspection_endpoint"])
208 assert.NotEmpty(t, metadata["introspection_endpoint_auth_methods_supported"])
209 assert.NotEmpty(t, metadata["introspection_endpoint_auth_signing_alg_values_supported"])
210 // assert.NotEmpty(t, metadata["code_challenge_methods_supported"])
211 })
212
213 t.Run("GET /.well-known/openid-configuration", func(t *testing.T) {
214 response := x.Must(client.Get("http://idp.example.com:8080/.well-known/openid-configuration"))
215 require.Equal(t, http.StatusOK, response.StatusCode)
216 metadata := x.Must(serde.FromJSON[map[string]interface{}](response.Body))
217 assert.Equal(t, "http://idp.example.com:8080/.well-known/oauth-authorization-server", metadata["issuer"])
218 assert.Equal(t, "http://idp.example.com:8080/oauth/authorize", metadata["authorization_endpoint"])
219 assert.Equal(t, "http://idp.example.com:8080/oauth/token", metadata["token_endpoint"])
220 assert.NotEmpty(t, metadata["userinfo_endpoint"])
221 // assert.NotEmpty(t, metadata["jwks_uri"])
222 // assert.NotEmpty(t, metadata["registration_endpoint"])
223 assert.NotEmpty(t, metadata["scopes_supported"])
224 assert.NotEmpty(t, metadata["response_types_supported"])
225 assert.NotEmpty(t, metadata["response_modes_supported"])
226 assert.NotEmpty(t, metadata["grant_types_supported"])
227 // assert.NotEmpty(t, metadata["acr_values_supported"])
228 assert.NotEmpty(t, metadata["subject_types_supported"])
229 assert.NotEmpty(t, metadata["id_token_signing_alg_values_supported"])
230 // assert.NotEmpty(t, metadata["id_token_encryption_alg_values_supported"])
231 // assert.NotEmpty(t, metadata["id_token_encryption_enc_values_supported"])
232 assert.NotEmpty(t, metadata["userinfo_signing_alg_values_supported"])
233 // assert.NotEmpty(t, metadata["userinfo_encryption_alg_values_supported"])
234 // assert.NotEmpty(t, metadata["userinfo_encryption_enc_values_supported"])
235 assert.NotEmpty(t, metadata["request_object_signing_alg_values_supported"])
236 // assert.NotEmpty(t, metadata["request_object_encryption_alg_values_supported"])
237 // assert.NotEmpty(t, metadata["request_object_encryption_enc_values_supported"])
238 assert.NotEmpty(t, metadata["token_endpoint_auth_methods_supported"])
239 // assert.NotEmpty(t, metadata["token_endpoint_auth_signing_alg_values_supported"])
240 // assert.NotEmpty(t, metadata["display_values_supported"])
241 assert.NotEmpty(t, metadata["claim_types_supported"])
242 assert.NotEmpty(t, metadata["claims_supported"])
243 // assert.NotEmpty(t, metadata["service_documentation"])
244 // assert.NotEmpty(t, metadata["claims_locales_supported"])
245 assert.NotEmpty(t, metadata["ui_locales_supported"])
246 // assert.True(t, metadata["claims_parameter_supported"])
247 // assert.True(t, metadata["request_parameter_supported"])
248 // assert.True(t, metadata["request_uri_parameter_supported"])
249 // assert.True(t, metadata["require_request_uri_registration"])
250 // assert.NotEmpty(t, metadata["op_policy_uri"])
251 // assert.NotEmpty(t, metadata["op_tos_uri"])
252 })
253
254 t.Run("authorization code grant", func(t *testing.T) {
255 conf := &oauth2.Config{
256 ClientID: "client_id",
257 ClientSecret: "client_secret",
258 Scopes: []string{"openid"},
259 Endpoint: oauth2.Endpoint{
260 TokenURL: "http://idp.example.com:8080/oauth/token",
261 AuthURL: "http://idp.example.com:8080/oauth/authorize",
262 },
263 }
264
265 authURL := conf.AuthCodeURL(
266 "state",
267 oauth2.SetAuthURLParam("client_id", "client_id"),
268 oauth2.SetAuthURLParam("scope", "openid"),
269 oauth2.SetAuthURLParam("redirect_uri", "http://example.org/oauth/callback"),
270 oauth2.SetAuthURLParam("response_type", "code"),
271 oauth2.SetAuthURLParam("response_mode", "fragment"),
272 )
273 assert.NoError(t, page.Context().ClearCookies())
274 x.Must(page.Goto(authURL))
275
276 page.Locator("#username").Fill("root")
277 page.Locator("#password").Fill("root")
278 assert.NoError(t, page.Locator("#login-button").Click())
279
280 assert.NoError(t, page.Locator("#submit-button").Click())
281
282 uri := x.Must(url.Parse(page.URL()))
283 values := x.Must(url.ParseQuery(uri.Fragment))
284 code := values.Get("code")
285
286 ctx := t.Context()
287 ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
288 credentials := x.Must(conf.Exchange(ctx, code))
289 assert.NotEmpty(t, credentials.AccessToken)
290 assert.Equal(t, "Bearer", credentials.TokenType)
291 assert.NotEmpty(t, credentials.RefreshToken)
292
293 t.Run("cannot re-use the same authorization grant", func(t *testing.T) {
294 newCredentials, err := conf.Exchange(ctx, code)
295
296 assert.Error(t, err)
297 assert.Empty(t, newCredentials)
298 })
299
300 t.Run("token is usable against REST API", func(t *testing.T) {
301 client := conf.Client(ctx, credentials)
302 response := x.Must(client.Get("http://api.example.com:8080/projects.json"))
303 require.Equal(t, http.StatusOK, response.StatusCode)
304 projects := x.Must(serde.FromJSON[[]map[string]string](response.Body))
305 assert.NotNil(t, projects)
306
307 io := bytes.NewBuffer(nil)
308 assert.NoError(t, serde.ToJSON(io, map[string]string{"name": "foo"}))
309 response = x.Must(client.Post("http://api.example.com:8080/projects", "application/json", io))
310 require.Equal(t, http.StatusCreated, response.StatusCode)
311 project := x.Must(serde.FromJSON[map[string]string](response.Body))
312 assert.Equal(t, "foo", project["name"])
313 })
314
315 t.Run("token can be introspected", func(t *testing.T) {
316 client := conf.Client(ctx, credentials)
317
318 io := bytes.NewBuffer(nil)
319 assert.NoError(t, serde.ToJSON(io, map[string]string{"token": credentials.AccessToken}))
320 response := x.Must(client.Post("http://idp.example.com:8080/oauth/introspect", "application/json", io))
321 require.Equal(t, http.StatusOK, response.StatusCode)
322
323 claims := x.Must(serde.FromJSON[map[string]interface{}](response.Body))
324 assert.Equal(t, true, claims["active"])
325 assert.Equal(t, "gid://example/User/1", claims["sub"])
326 })
327 })
328 })
329}