forgejo/tests/integration/oauth_test.go

802 lines
32 KiB
Go
Raw Normal View History

2019-03-08 19:42:50 +03:00
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
2019-03-08 19:42:50 +03:00
package integration
2019-03-08 19:42:50 +03:00
import (
"bytes"
[TESTS] coverage for SignInOAuthCallback (cherry picked from commit f8e1619b993943eafb8ee12bf06f4cdb5862de70) (cherry picked from commit 46d8bc9bdf68b53767211dc103e6130f55bcdb64) (cherry picked from commit e0c7b7055f5f4eeca84f1d0b1260b7b9622d3aab) (cherry picked from commit faab747f8e7eb09262f755445462a77f8a6fb953) (cherry picked from commit 46acb6a9a79e7ce588b2863aa37bf26805afb2b1) (cherry picked from commit 22d964e74407c52ffcd3d3a84b0a66e2c186b0fa) (cherry picked from commit 4c8a6031acf760c2383d9e103c703ee5ececb8e8) (cherry picked from commit 032e8c7a9a357a13f41410063c2f7fb925dba5ac) (cherry picked from commit 7a17a3b0fb979e2923019de4b9a7318f578b73b8) (cherry picked from commit 8ea71c2a31ea7492f5f2e3de529c7fd0b232d3e3) (cherry picked from commit 4b027e2d37cb91c5951f1d10a018778b19590eb0) (cherry picked from commit d787089a5de09fa11f8e82a66ec43e4abdde1b2e) (cherry picked from commit 7b9999357a5d34861b5fd7390cc400f497896246) (cherry picked from commit 80eb531c380914c66d30a29159b81154e7adefeb) (cherry picked from commit 373b198bfbc29855c409294ee487639f83516a55) (cherry picked from commit 15781eedf755713ad4bbc83cf0b82e899e05d075) (cherry picked from commit 46bdb17a2fb25c23336ef493449ff3ff0eb05409) (cherry picked from commit 22ec6c11ee779cc06c2e6e6dca3213129033389e) (cherry picked from commit 3f94b9a11103458d6b4f44dfda8158b748a2e3ad) (cherry picked from commit a4194c29ffcca46f20d2ccc660f8c95cf527c7a4) (cherry picked from commit aa80ba2ed1e529a85eda01beeb25c6732d2bc9bf) (cherry picked from commit d349f3e80ec764f6f402ea6183e41511f73cd33f) (cherry picked from commit ccb073f71ac855b1d7c7dd1e71a29939a14a20c5) (cherry picked from commit d8a996a9c1052a7c4b7693cb75f10ee0cbce1534) (cherry picked from commit af12965737bf60bb74fed2ca5363b034eca15fe4) (cherry picked from commit 3867b17a485e441198b248be08cbe14bb8bd3946) (cherry picked from commit 0c48072b2e19f70530d76de459bddd9e7c539c0d) (cherry picked from commit 9c5d675ded22eb2777df5b4bbd24e4b1341b8b26) (cherry picked from commit 665119370f4e9103978853c53c6ae9258a415cbc) (cherry picked from commit 658417e7ad06e8a03ee562f3c8ef8b3c2abd158e)
2023-06-27 16:00:15 +03:00
"context"
"crypto/sha256"
"encoding/base64"
[TESTS] coverage for SignInOAuthCallback (cherry picked from commit f8e1619b993943eafb8ee12bf06f4cdb5862de70) (cherry picked from commit 46d8bc9bdf68b53767211dc103e6130f55bcdb64) (cherry picked from commit e0c7b7055f5f4eeca84f1d0b1260b7b9622d3aab) (cherry picked from commit faab747f8e7eb09262f755445462a77f8a6fb953) (cherry picked from commit 46acb6a9a79e7ce588b2863aa37bf26805afb2b1) (cherry picked from commit 22d964e74407c52ffcd3d3a84b0a66e2c186b0fa) (cherry picked from commit 4c8a6031acf760c2383d9e103c703ee5ececb8e8) (cherry picked from commit 032e8c7a9a357a13f41410063c2f7fb925dba5ac) (cherry picked from commit 7a17a3b0fb979e2923019de4b9a7318f578b73b8) (cherry picked from commit 8ea71c2a31ea7492f5f2e3de529c7fd0b232d3e3) (cherry picked from commit 4b027e2d37cb91c5951f1d10a018778b19590eb0) (cherry picked from commit d787089a5de09fa11f8e82a66ec43e4abdde1b2e) (cherry picked from commit 7b9999357a5d34861b5fd7390cc400f497896246) (cherry picked from commit 80eb531c380914c66d30a29159b81154e7adefeb) (cherry picked from commit 373b198bfbc29855c409294ee487639f83516a55) (cherry picked from commit 15781eedf755713ad4bbc83cf0b82e899e05d075) (cherry picked from commit 46bdb17a2fb25c23336ef493449ff3ff0eb05409) (cherry picked from commit 22ec6c11ee779cc06c2e6e6dca3213129033389e) (cherry picked from commit 3f94b9a11103458d6b4f44dfda8158b748a2e3ad) (cherry picked from commit a4194c29ffcca46f20d2ccc660f8c95cf527c7a4) (cherry picked from commit aa80ba2ed1e529a85eda01beeb25c6732d2bc9bf) (cherry picked from commit d349f3e80ec764f6f402ea6183e41511f73cd33f) (cherry picked from commit ccb073f71ac855b1d7c7dd1e71a29939a14a20c5) (cherry picked from commit d8a996a9c1052a7c4b7693cb75f10ee0cbce1534) (cherry picked from commit af12965737bf60bb74fed2ca5363b034eca15fe4) (cherry picked from commit 3867b17a485e441198b248be08cbe14bb8bd3946) (cherry picked from commit 0c48072b2e19f70530d76de459bddd9e7c539c0d) (cherry picked from commit 9c5d675ded22eb2777df5b4bbd24e4b1341b8b26) (cherry picked from commit 665119370f4e9103978853c53c6ae9258a415cbc) (cherry picked from commit 658417e7ad06e8a03ee562f3c8ef8b3c2abd158e)
2023-06-27 16:00:15 +03:00
"fmt"
"io"
"net/http"
"net/url"
2019-03-08 19:42:50 +03:00
"testing"
[TESTS] coverage for SignInOAuthCallback (cherry picked from commit f8e1619b993943eafb8ee12bf06f4cdb5862de70) (cherry picked from commit 46d8bc9bdf68b53767211dc103e6130f55bcdb64) (cherry picked from commit e0c7b7055f5f4eeca84f1d0b1260b7b9622d3aab) (cherry picked from commit faab747f8e7eb09262f755445462a77f8a6fb953) (cherry picked from commit 46acb6a9a79e7ce588b2863aa37bf26805afb2b1) (cherry picked from commit 22d964e74407c52ffcd3d3a84b0a66e2c186b0fa) (cherry picked from commit 4c8a6031acf760c2383d9e103c703ee5ececb8e8) (cherry picked from commit 032e8c7a9a357a13f41410063c2f7fb925dba5ac) (cherry picked from commit 7a17a3b0fb979e2923019de4b9a7318f578b73b8) (cherry picked from commit 8ea71c2a31ea7492f5f2e3de529c7fd0b232d3e3) (cherry picked from commit 4b027e2d37cb91c5951f1d10a018778b19590eb0) (cherry picked from commit d787089a5de09fa11f8e82a66ec43e4abdde1b2e) (cherry picked from commit 7b9999357a5d34861b5fd7390cc400f497896246) (cherry picked from commit 80eb531c380914c66d30a29159b81154e7adefeb) (cherry picked from commit 373b198bfbc29855c409294ee487639f83516a55) (cherry picked from commit 15781eedf755713ad4bbc83cf0b82e899e05d075) (cherry picked from commit 46bdb17a2fb25c23336ef493449ff3ff0eb05409) (cherry picked from commit 22ec6c11ee779cc06c2e6e6dca3213129033389e) (cherry picked from commit 3f94b9a11103458d6b4f44dfda8158b748a2e3ad) (cherry picked from commit a4194c29ffcca46f20d2ccc660f8c95cf527c7a4) (cherry picked from commit aa80ba2ed1e529a85eda01beeb25c6732d2bc9bf) (cherry picked from commit d349f3e80ec764f6f402ea6183e41511f73cd33f) (cherry picked from commit ccb073f71ac855b1d7c7dd1e71a29939a14a20c5) (cherry picked from commit d8a996a9c1052a7c4b7693cb75f10ee0cbce1534) (cherry picked from commit af12965737bf60bb74fed2ca5363b034eca15fe4) (cherry picked from commit 3867b17a485e441198b248be08cbe14bb8bd3946) (cherry picked from commit 0c48072b2e19f70530d76de459bddd9e7c539c0d) (cherry picked from commit 9c5d675ded22eb2777df5b4bbd24e4b1341b8b26) (cherry picked from commit 665119370f4e9103978853c53c6ae9258a415cbc) (cherry picked from commit 658417e7ad06e8a03ee562f3c8ef8b3c2abd158e)
2023-06-27 16:00:15 +03:00
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
[TESTS] coverage for SignInOAuthCallback (cherry picked from commit f8e1619b993943eafb8ee12bf06f4cdb5862de70) (cherry picked from commit 46d8bc9bdf68b53767211dc103e6130f55bcdb64) (cherry picked from commit e0c7b7055f5f4eeca84f1d0b1260b7b9622d3aab) (cherry picked from commit faab747f8e7eb09262f755445462a77f8a6fb953) (cherry picked from commit 46acb6a9a79e7ce588b2863aa37bf26805afb2b1) (cherry picked from commit 22d964e74407c52ffcd3d3a84b0a66e2c186b0fa) (cherry picked from commit 4c8a6031acf760c2383d9e103c703ee5ececb8e8) (cherry picked from commit 032e8c7a9a357a13f41410063c2f7fb925dba5ac) (cherry picked from commit 7a17a3b0fb979e2923019de4b9a7318f578b73b8) (cherry picked from commit 8ea71c2a31ea7492f5f2e3de529c7fd0b232d3e3) (cherry picked from commit 4b027e2d37cb91c5951f1d10a018778b19590eb0) (cherry picked from commit d787089a5de09fa11f8e82a66ec43e4abdde1b2e) (cherry picked from commit 7b9999357a5d34861b5fd7390cc400f497896246) (cherry picked from commit 80eb531c380914c66d30a29159b81154e7adefeb) (cherry picked from commit 373b198bfbc29855c409294ee487639f83516a55) (cherry picked from commit 15781eedf755713ad4bbc83cf0b82e899e05d075) (cherry picked from commit 46bdb17a2fb25c23336ef493449ff3ff0eb05409) (cherry picked from commit 22ec6c11ee779cc06c2e6e6dca3213129033389e) (cherry picked from commit 3f94b9a11103458d6b4f44dfda8158b748a2e3ad) (cherry picked from commit a4194c29ffcca46f20d2ccc660f8c95cf527c7a4) (cherry picked from commit aa80ba2ed1e529a85eda01beeb25c6732d2bc9bf) (cherry picked from commit d349f3e80ec764f6f402ea6183e41511f73cd33f) (cherry picked from commit ccb073f71ac855b1d7c7dd1e71a29939a14a20c5) (cherry picked from commit d8a996a9c1052a7c4b7693cb75f10ee0cbce1534) (cherry picked from commit af12965737bf60bb74fed2ca5363b034eca15fe4) (cherry picked from commit 3867b17a485e441198b248be08cbe14bb8bd3946) (cherry picked from commit 0c48072b2e19f70530d76de459bddd9e7c539c0d) (cherry picked from commit 9c5d675ded22eb2777df5b4bbd24e4b1341b8b26) (cherry picked from commit 665119370f4e9103978853c53c6ae9258a415cbc) (cherry picked from commit 658417e7ad06e8a03ee562f3c8ef8b3c2abd158e)
2023-06-27 16:00:15 +03:00
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/routers/web/auth"
"code.gitea.io/gitea/tests"
[TESTS] coverage for SignInOAuthCallback (cherry picked from commit f8e1619b993943eafb8ee12bf06f4cdb5862de70) (cherry picked from commit 46d8bc9bdf68b53767211dc103e6130f55bcdb64) (cherry picked from commit e0c7b7055f5f4eeca84f1d0b1260b7b9622d3aab) (cherry picked from commit faab747f8e7eb09262f755445462a77f8a6fb953) (cherry picked from commit 46acb6a9a79e7ce588b2863aa37bf26805afb2b1) (cherry picked from commit 22d964e74407c52ffcd3d3a84b0a66e2c186b0fa) (cherry picked from commit 4c8a6031acf760c2383d9e103c703ee5ececb8e8) (cherry picked from commit 032e8c7a9a357a13f41410063c2f7fb925dba5ac) (cherry picked from commit 7a17a3b0fb979e2923019de4b9a7318f578b73b8) (cherry picked from commit 8ea71c2a31ea7492f5f2e3de529c7fd0b232d3e3) (cherry picked from commit 4b027e2d37cb91c5951f1d10a018778b19590eb0) (cherry picked from commit d787089a5de09fa11f8e82a66ec43e4abdde1b2e) (cherry picked from commit 7b9999357a5d34861b5fd7390cc400f497896246) (cherry picked from commit 80eb531c380914c66d30a29159b81154e7adefeb) (cherry picked from commit 373b198bfbc29855c409294ee487639f83516a55) (cherry picked from commit 15781eedf755713ad4bbc83cf0b82e899e05d075) (cherry picked from commit 46bdb17a2fb25c23336ef493449ff3ff0eb05409) (cherry picked from commit 22ec6c11ee779cc06c2e6e6dca3213129033389e) (cherry picked from commit 3f94b9a11103458d6b4f44dfda8158b748a2e3ad) (cherry picked from commit a4194c29ffcca46f20d2ccc660f8c95cf527c7a4) (cherry picked from commit aa80ba2ed1e529a85eda01beeb25c6732d2bc9bf) (cherry picked from commit d349f3e80ec764f6f402ea6183e41511f73cd33f) (cherry picked from commit ccb073f71ac855b1d7c7dd1e71a29939a14a20c5) (cherry picked from commit d8a996a9c1052a7c4b7693cb75f10ee0cbce1534) (cherry picked from commit af12965737bf60bb74fed2ca5363b034eca15fe4) (cherry picked from commit 3867b17a485e441198b248be08cbe14bb8bd3946) (cherry picked from commit 0c48072b2e19f70530d76de459bddd9e7c539c0d) (cherry picked from commit 9c5d675ded22eb2777df5b4bbd24e4b1341b8b26) (cherry picked from commit 665119370f4e9103978853c53c6ae9258a415cbc) (cherry picked from commit 658417e7ad06e8a03ee562f3c8ef8b3c2abd158e)
2023-06-27 16:00:15 +03:00
"github.com/markbates/goth"
2019-03-08 19:42:50 +03:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2019-03-08 19:42:50 +03:00
)
func TestAuthorizeNoClientID(t *testing.T) {
defer tests.PrepareTestEnv(t)()
2019-03-08 19:42:50 +03:00
req := NewRequest(t, "GET", "/login/oauth/authorize")
ctx := loginUser(t, "user2")
resp := ctx.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "Client ID not registered")
}
func TestAuthorizeUnregisteredRedirect(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=UNREGISTERED&response_type=code&state=thestate")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusBadRequest)
assert.Contains(t, resp.Body.String(), "Unregistered Redirect URI")
}
func TestAuthorizeUnsupportedResponseType(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=UNEXPECTED&state=thestate")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location()
require.NoError(t, err)
assert.Equal(t, "unsupported_response_type", u.Query().Get("error"))
assert.Equal(t, "Only code response type is supported.", u.Query().Get("error_description"))
}
func TestAuthorizeUnsupportedCodeChallengeMethod(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate&code_challenge_method=UNEXPECTED")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location()
require.NoError(t, err)
assert.Equal(t, "invalid_request", u.Query().Get("error"))
assert.Equal(t, "unsupported code challenge method", u.Query().Get("error_description"))
2019-03-08 19:42:50 +03:00
}
func TestAuthorizeLoginRedirect(t *testing.T) {
defer tests.PrepareTestEnv(t)()
2019-03-08 19:42:50 +03:00
req := NewRequest(t, "GET", "/login/oauth/authorize")
assert.Contains(t, MakeRequest(t, req, http.StatusSeeOther).Body.String(), "/user/login")
2019-03-08 19:42:50 +03:00
}
func TestAuthorizeShow(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate")
2019-03-08 19:42:50 +03:00
ctx := loginUser(t, "user4")
resp := ctx.MakeRequest(t, req, http.StatusOK)
2019-03-08 19:42:50 +03:00
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "#authorize-app", true)
htmlDoc.GetCSRF()
}
func TestOAuth_AuthorizeConfidentialTwice(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// da7da3ba-9a13-4167-856f-3899de0b0138 a confidential client in models/fixtures/oauth2_application.yml
// request authorization for the first time shows the grant page ...
authorizeURL := "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate"
req := NewRequest(t, "GET", authorizeURL)
ctx := loginUser(t, "user4")
resp := ctx.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "#authorize-app", true)
// ... and the user grants the authorization
req = NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"redirect_uri": "a",
"state": "thestate",
"granted": "true",
})
resp = ctx.MakeRequest(t, req, http.StatusSeeOther)
assert.Contains(t, test.RedirectURL(resp), "code=")
// request authorization the second time and the grant page is not shown again, redirection happens immediately
req = NewRequest(t, "GET", authorizeURL)
resp = ctx.MakeRequest(t, req, http.StatusSeeOther)
assert.Contains(t, test.RedirectURL(resp), "code=")
}
func TestOAuth_AuthorizePublicTwice(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// ce5a1322-42a7-11ed-b878-0242ac120002 is a public client in models/fixtures/oauth2_application.yml
authorizeURL := "/login/oauth/authorize?client_id=ce5a1322-42a7-11ed-b878-0242ac120002&redirect_uri=b&response_type=code&code_challenge_method=plain&code_challenge=CODE&state=thestate"
ctx := loginUser(t, "user4")
// a public client must be authorized every time
for _, name := range []string{"First", "Second"} {
t.Run(name, func(t *testing.T) {
req := NewRequest(t, "GET", authorizeURL)
resp := ctx.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "#authorize-app", true)
req = NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
"redirect_uri": "b",
"state": "thestate",
"granted": "true",
})
resp = ctx.MakeRequest(t, req, http.StatusSeeOther)
assert.Contains(t, test.RedirectURL(resp), "code=")
})
}
}
func TestAuthorizeRedirectWithExistingGrant(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=https%3A%2F%2Fexample.com%2Fxyzzy&response_type=code&state=thestate")
2019-03-08 19:42:50 +03:00
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
2019-03-08 19:42:50 +03:00
u, err := resp.Result().Location()
require.NoError(t, err)
2019-03-08 19:42:50 +03:00
assert.Equal(t, "thestate", u.Query().Get("state"))
assert.Greaterf(t, len(u.Query().Get("code")), 30, "authorization code '%s' should be longer then 30", u.Query().Get("code"))
u.RawQuery = ""
assert.Equal(t, "https://example.com/xyzzy", u.String())
2019-03-08 19:42:50 +03:00
}
Record OAuth client type at registration (#21316) The OAuth spec [defines two types of client](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1), confidential and public. Previously Gitea assumed all clients to be confidential. > OAuth defines two client types, based on their ability to authenticate securely with the authorization server (i.e., ability to > maintain the confidentiality of their client credentials): > > confidential > Clients capable of maintaining the confidentiality of their credentials (e.g., client implemented on a secure server with > restricted access to the client credentials), or capable of secure client authentication using other means. > > **public > Clients incapable of maintaining the confidentiality of their credentials (e.g., clients executing on the device used by the resource owner, such as an installed native application or a web browser-based application), and incapable of secure client authentication via any other means.** > > The client type designation is based on the authorization server's definition of secure authentication and its acceptable exposure levels of client credentials. The authorization server SHOULD NOT make assumptions about the client type. https://datatracker.ietf.org/doc/html/rfc8252#section-8.4 > Authorization servers MUST record the client type in the client registration details in order to identify and process requests accordingly. Require PKCE for public clients: https://datatracker.ietf.org/doc/html/rfc8252#section-8.1 > Authorization servers SHOULD reject authorization requests from native apps that don't use PKCE by returning an error message Fixes #21299 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-10-24 10:59:24 +03:00
func TestAuthorizePKCERequiredForPublicClient(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=ce5a1322-42a7-11ed-b878-0242ac120002&redirect_uri=http%3A%2F%2F127.0.0.1&response_type=code&state=thestate")
ctx := loginUser(t, "user1")
resp := ctx.MakeRequest(t, req, http.StatusSeeOther)
u, err := resp.Result().Location()
require.NoError(t, err)
Record OAuth client type at registration (#21316) The OAuth spec [defines two types of client](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1), confidential and public. Previously Gitea assumed all clients to be confidential. > OAuth defines two client types, based on their ability to authenticate securely with the authorization server (i.e., ability to > maintain the confidentiality of their client credentials): > > confidential > Clients capable of maintaining the confidentiality of their credentials (e.g., client implemented on a secure server with > restricted access to the client credentials), or capable of secure client authentication using other means. > > **public > Clients incapable of maintaining the confidentiality of their credentials (e.g., clients executing on the device used by the resource owner, such as an installed native application or a web browser-based application), and incapable of secure client authentication via any other means.** > > The client type designation is based on the authorization server's definition of secure authentication and its acceptable exposure levels of client credentials. The authorization server SHOULD NOT make assumptions about the client type. https://datatracker.ietf.org/doc/html/rfc8252#section-8.4 > Authorization servers MUST record the client type in the client registration details in order to identify and process requests accordingly. Require PKCE for public clients: https://datatracker.ietf.org/doc/html/rfc8252#section-8.1 > Authorization servers SHOULD reject authorization requests from native apps that don't use PKCE by returning an error message Fixes #21299 Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2022-10-24 10:59:24 +03:00
assert.Equal(t, "invalid_request", u.Query().Get("error"))
assert.Equal(t, "PKCE is required for public clients", u.Query().Get("error_description"))
}
2019-03-08 19:42:50 +03:00
func TestAccessTokenExchange(t *testing.T) {
defer tests.PrepareTestEnv(t)()
2019-03-08 19:42:50 +03:00
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
2019-03-08 19:42:50 +03:00
})
resp := MakeRequest(t, req, http.StatusOK)
2019-03-08 19:42:50 +03:00
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
assert.Greater(t, len(parsed.AccessToken), 10)
assert.Greater(t, len(parsed.RefreshToken), 10)
}
func TestAccessTokenExchangeWithPublicClient(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "ce5a1322-42a7-11ed-b878-0242ac120002",
"redirect_uri": "http://127.0.0.1",
"code": "authcodepublic",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp := MakeRequest(t, req, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
assert.Greater(t, len(parsed.AccessToken), 10)
assert.Greater(t, len(parsed.RefreshToken), 10)
2019-03-08 19:42:50 +03:00
}
func TestAccessTokenExchangeJSON(t *testing.T) {
defer tests.PrepareTestEnv(t)()
2019-04-15 18:54:50 +03:00
req := NewRequestWithJSON(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
2019-04-15 18:54:50 +03:00
})
resp := MakeRequest(t, req, http.StatusOK)
2019-04-15 18:54:50 +03:00
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
assert.Greater(t, len(parsed.AccessToken), 10)
assert.Greater(t, len(parsed.RefreshToken), 10)
2019-04-15 18:54:50 +03:00
}
func TestAccessTokenExchangeWithoutPKCE(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
2019-03-08 19:42:50 +03:00
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": "authcode",
})
resp := MakeRequest(t, req, http.StatusBadRequest)
parsedError := new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "failed PKCE code challenge", parsedError.ErrorDescription)
2019-03-08 19:42:50 +03:00
}
func TestAccessTokenExchangeWithInvalidCredentials(t *testing.T) {
defer tests.PrepareTestEnv(t)()
2019-03-08 19:42:50 +03:00
// invalid client id
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "???",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
2019-03-08 19:42:50 +03:00
})
resp := MakeRequest(t, req, http.StatusBadRequest)
parsedError := new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
assert.Equal(t, "cannot load client with client id: '???'", parsedError.ErrorDescription)
2019-03-08 19:42:50 +03:00
// invalid client secret
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "???",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
2019-03-08 19:42:50 +03:00
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "invalid client secret", parsedError.ErrorDescription)
2019-03-08 19:42:50 +03:00
// invalid redirect uri
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "???",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
2019-03-08 19:42:50 +03:00
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "unexpected redirect URI", parsedError.ErrorDescription)
2019-03-08 19:42:50 +03:00
// invalid authorization code
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": "???",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
2019-03-08 19:42:50 +03:00
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "client is not authorized", parsedError.ErrorDescription)
2019-03-08 19:42:50 +03:00
// invalid grant_type
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "???",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
2019-03-08 19:42:50 +03:00
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unsupported_grant_type", string(parsedError.ErrorCode))
assert.Equal(t, "Only refresh_token or authorization_code grant type is supported", parsedError.ErrorDescription)
2019-03-08 19:42:50 +03:00
}
func TestAccessTokenExchangeWithBasicAuth(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
resp := MakeRequest(t, req, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
assert.Greater(t, len(parsed.AccessToken), 10)
assert.Greater(t, len(parsed.RefreshToken), 10)
// use wrong client_secret
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OmJsYWJsYQ==")
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError := new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "invalid client secret", parsedError.ErrorDescription)
// missing header
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
assert.Equal(t, "cannot load client with client id: ''", parsedError.ErrorDescription)
// client_id inconsistent with Authorization header
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"code": "authcode",
"client_id": "inconsistent",
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_request", string(parsedError.ErrorCode))
assert.Equal(t, "client_id in request body inconsistent with Authorization header", parsedError.ErrorDescription)
// client_secret inconsistent with Authorization header
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"redirect_uri": "a",
"code": "authcode",
"client_secret": "inconsistent",
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_request", string(parsedError.ErrorCode))
assert.Equal(t, "client_secret in request body inconsistent with Authorization header", parsedError.ErrorDescription)
}
func TestRefreshTokenInvalidation(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp := MakeRequest(t, req, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsed))
// test without invalidation
setting.OAuth2.InvalidateRefreshTokens = false
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
// omit secret
"redirect_uri": "a",
"refresh_token": parsed.RefreshToken,
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError := new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "invalid_client", string(parsedError.ErrorCode))
assert.Equal(t, "invalid empty client secret", parsedError.ErrorDescription)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"refresh_token": "UNEXPECTED",
})
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "unable to parse refresh token", parsedError.ErrorDescription)
req = NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "refresh_token",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"refresh_token": parsed.RefreshToken,
})
bs, err := io.ReadAll(req.Body)
require.NoError(t, err)
req.Body = io.NopCloser(bytes.NewReader(bs))
MakeRequest(t, req, http.StatusOK)
req.Body = io.NopCloser(bytes.NewReader(bs))
MakeRequest(t, req, http.StatusOK)
// test with invalidation
setting.OAuth2.InvalidateRefreshTokens = true
req.Body = io.NopCloser(bytes.NewReader(bs))
MakeRequest(t, req, http.StatusOK)
// repeat request should fail
req.Body = io.NopCloser(bytes.NewReader(bs))
resp = MakeRequest(t, req, http.StatusBadRequest)
parsedError = new(auth.AccessTokenError)
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), parsedError))
assert.Equal(t, "unauthorized_client", string(parsedError.ErrorCode))
assert.Equal(t, "token was already used", parsedError.ErrorDescription)
}
[TESTS] coverage for SignInOAuthCallback (cherry picked from commit f8e1619b993943eafb8ee12bf06f4cdb5862de70) (cherry picked from commit 46d8bc9bdf68b53767211dc103e6130f55bcdb64) (cherry picked from commit e0c7b7055f5f4eeca84f1d0b1260b7b9622d3aab) (cherry picked from commit faab747f8e7eb09262f755445462a77f8a6fb953) (cherry picked from commit 46acb6a9a79e7ce588b2863aa37bf26805afb2b1) (cherry picked from commit 22d964e74407c52ffcd3d3a84b0a66e2c186b0fa) (cherry picked from commit 4c8a6031acf760c2383d9e103c703ee5ececb8e8) (cherry picked from commit 032e8c7a9a357a13f41410063c2f7fb925dba5ac) (cherry picked from commit 7a17a3b0fb979e2923019de4b9a7318f578b73b8) (cherry picked from commit 8ea71c2a31ea7492f5f2e3de529c7fd0b232d3e3) (cherry picked from commit 4b027e2d37cb91c5951f1d10a018778b19590eb0) (cherry picked from commit d787089a5de09fa11f8e82a66ec43e4abdde1b2e) (cherry picked from commit 7b9999357a5d34861b5fd7390cc400f497896246) (cherry picked from commit 80eb531c380914c66d30a29159b81154e7adefeb) (cherry picked from commit 373b198bfbc29855c409294ee487639f83516a55) (cherry picked from commit 15781eedf755713ad4bbc83cf0b82e899e05d075) (cherry picked from commit 46bdb17a2fb25c23336ef493449ff3ff0eb05409) (cherry picked from commit 22ec6c11ee779cc06c2e6e6dca3213129033389e) (cherry picked from commit 3f94b9a11103458d6b4f44dfda8158b748a2e3ad) (cherry picked from commit a4194c29ffcca46f20d2ccc660f8c95cf527c7a4) (cherry picked from commit aa80ba2ed1e529a85eda01beeb25c6732d2bc9bf) (cherry picked from commit d349f3e80ec764f6f402ea6183e41511f73cd33f) (cherry picked from commit ccb073f71ac855b1d7c7dd1e71a29939a14a20c5) (cherry picked from commit d8a996a9c1052a7c4b7693cb75f10ee0cbce1534) (cherry picked from commit af12965737bf60bb74fed2ca5363b034eca15fe4) (cherry picked from commit 3867b17a485e441198b248be08cbe14bb8bd3946) (cherry picked from commit 0c48072b2e19f70530d76de459bddd9e7c539c0d) (cherry picked from commit 9c5d675ded22eb2777df5b4bbd24e4b1341b8b26) (cherry picked from commit 665119370f4e9103978853c53c6ae9258a415cbc) (cherry picked from commit 658417e7ad06e8a03ee562f3c8ef8b3c2abd158e)
2023-06-27 16:00:15 +03:00
func TestSignInOAuthCallbackSignIn(t *testing.T) {
defer tests.PrepareTestEnv(t)()
//
// OAuth2 authentication source GitLab
//
gitlabName := "gitlab"
gitlab := addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName))
//
// Create a user as if it had been previously been created by the GitLab
// authentication source.
//
userGitLabUserID := "5678"
userGitLab := &user_model.User{
Name: "gitlabuser",
Email: "gitlabuser@example.com",
Passwd: "gitlabuserpassword",
Type: user_model.UserTypeIndividual,
LoginType: auth_model.OAuth2,
LoginSource: gitlab.ID,
LoginName: userGitLabUserID,
}
defer createUser(context.Background(), t, userGitLab)()
//
// A request for user information sent to Goth will return a
// goth.User exactly matching the user created above.
//
defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{
Provider: gitlabName,
UserID: userGitLabUserID,
Email: userGitLab.Email,
}, nil
})()
req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName))
resp := MakeRequest(t, req, http.StatusSeeOther)
assert.Equal(t, "/", test.RedirectURL(resp))
[TESTS] coverage for SignInOAuthCallback (cherry picked from commit f8e1619b993943eafb8ee12bf06f4cdb5862de70) (cherry picked from commit 46d8bc9bdf68b53767211dc103e6130f55bcdb64) (cherry picked from commit e0c7b7055f5f4eeca84f1d0b1260b7b9622d3aab) (cherry picked from commit faab747f8e7eb09262f755445462a77f8a6fb953) (cherry picked from commit 46acb6a9a79e7ce588b2863aa37bf26805afb2b1) (cherry picked from commit 22d964e74407c52ffcd3d3a84b0a66e2c186b0fa) (cherry picked from commit 4c8a6031acf760c2383d9e103c703ee5ececb8e8) (cherry picked from commit 032e8c7a9a357a13f41410063c2f7fb925dba5ac) (cherry picked from commit 7a17a3b0fb979e2923019de4b9a7318f578b73b8) (cherry picked from commit 8ea71c2a31ea7492f5f2e3de529c7fd0b232d3e3) (cherry picked from commit 4b027e2d37cb91c5951f1d10a018778b19590eb0) (cherry picked from commit d787089a5de09fa11f8e82a66ec43e4abdde1b2e) (cherry picked from commit 7b9999357a5d34861b5fd7390cc400f497896246) (cherry picked from commit 80eb531c380914c66d30a29159b81154e7adefeb) (cherry picked from commit 373b198bfbc29855c409294ee487639f83516a55) (cherry picked from commit 15781eedf755713ad4bbc83cf0b82e899e05d075) (cherry picked from commit 46bdb17a2fb25c23336ef493449ff3ff0eb05409) (cherry picked from commit 22ec6c11ee779cc06c2e6e6dca3213129033389e) (cherry picked from commit 3f94b9a11103458d6b4f44dfda8158b748a2e3ad) (cherry picked from commit a4194c29ffcca46f20d2ccc660f8c95cf527c7a4) (cherry picked from commit aa80ba2ed1e529a85eda01beeb25c6732d2bc9bf) (cherry picked from commit d349f3e80ec764f6f402ea6183e41511f73cd33f) (cherry picked from commit ccb073f71ac855b1d7c7dd1e71a29939a14a20c5) (cherry picked from commit d8a996a9c1052a7c4b7693cb75f10ee0cbce1534) (cherry picked from commit af12965737bf60bb74fed2ca5363b034eca15fe4) (cherry picked from commit 3867b17a485e441198b248be08cbe14bb8bd3946) (cherry picked from commit 0c48072b2e19f70530d76de459bddd9e7c539c0d) (cherry picked from commit 9c5d675ded22eb2777df5b4bbd24e4b1341b8b26) (cherry picked from commit 665119370f4e9103978853c53c6ae9258a415cbc) (cherry picked from commit 658417e7ad06e8a03ee562f3c8ef8b3c2abd158e)
2023-06-27 16:00:15 +03:00
userAfterLogin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userGitLab.ID})
assert.Greater(t, userAfterLogin.LastLoginUnix, userGitLab.LastLoginUnix)
}
2024-06-10 11:04:00 +03:00
func TestSignInOAuthCallbackWithoutPKCEWhenUnsupported(t *testing.T) {
// https://codeberg.org/forgejo/forgejo/issues/4033
defer tests.PrepareTestEnv(t)()
// Setup authentication source
gitlabName := "gitlab"
gitlab := addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName))
// Create a user as if it had been previously been created by the authentication source.
userGitLabUserID := "5678"
userGitLab := &user_model.User{
Name: "gitlabuser",
Email: "gitlabuser@example.com",
Passwd: "gitlabuserpassword",
Type: user_model.UserTypeIndividual,
LoginType: auth_model.OAuth2,
LoginSource: gitlab.ID,
LoginName: userGitLabUserID,
}
defer createUser(context.Background(), t, userGitLab)()
// initial redirection (to generate the code_challenge)
session := emptyTestSession(t)
req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s", gitlabName))
resp := session.MakeRequest(t, req, http.StatusTemporaryRedirect)
dest, err := url.Parse(resp.Header().Get("Location"))
require.NoError(t, err)
2024-06-10 11:04:00 +03:00
assert.Empty(t, dest.Query().Get("code_challenge_method"))
assert.Empty(t, dest.Query().Get("code_challenge"))
// callback (to check the initial code_challenge)
defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
2024-06-10 11:04:00 +03:00
assert.Empty(t, req.URL.Query().Get("code_verifier"))
return goth.User{
Provider: gitlabName,
UserID: userGitLabUserID,
Email: userGitLab.Email,
}, nil
})()
req = NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName))
resp = session.MakeRequest(t, req, http.StatusSeeOther)
assert.Equal(t, "/", test.RedirectURL(resp))
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userGitLab.ID})
}
2024-06-10 11:04:00 +03:00
func TestSignInOAuthCallbackPKCE(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
// Setup authentication source
sourceName := "oidc"
authSource := addAuthSource(t, authSourcePayloadOpenIDConnect(sourceName, u.String()))
// Create a user as if it had been previously been created by the authentication source.
userID := "5678"
user := &user_model.User{
Name: "oidc.user",
Email: "oidc.user@example.com",
Passwd: "oidc.userpassword",
Type: user_model.UserTypeIndividual,
LoginType: auth_model.OAuth2,
LoginSource: authSource.ID,
LoginName: userID,
}
defer createUser(context.Background(), t, user)()
// initial redirection (to generate the code_challenge)
session := emptyTestSession(t)
req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s", sourceName))
resp := session.MakeRequest(t, req, http.StatusTemporaryRedirect)
dest, err := url.Parse(resp.Header().Get("Location"))
require.NoError(t, err)
2024-06-10 11:04:00 +03:00
assert.Equal(t, "S256", dest.Query().Get("code_challenge_method"))
codeChallenge := dest.Query().Get("code_challenge")
assert.NotEmpty(t, codeChallenge)
// callback (to check the initial code_challenge)
defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
codeVerifier := req.URL.Query().Get("code_verifier")
assert.NotEmpty(t, codeVerifier)
assert.Greater(t, len(codeVerifier), 40, codeVerifier)
sha2 := sha256.New()
io.WriteString(sha2, codeVerifier)
assert.Equal(t, codeChallenge, base64.RawURLEncoding.EncodeToString(sha2.Sum(nil)))
return goth.User{
Provider: sourceName,
UserID: userID,
Email: user.Email,
}, nil
})()
req = NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", sourceName))
resp = session.MakeRequest(t, req, http.StatusSeeOther)
assert.Equal(t, "/", test.RedirectURL(resp))
unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: user.ID})
})
}
func TestSignInOAuthCallbackRedirectToEscaping(t *testing.T) {
defer tests.PrepareTestEnv(t)()
//
// OAuth2 authentication source GitLab
//
gitlabName := "gitlab"
gitlab := addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName))
//
// Create a user as if it had been previously created by the GitLab
// authentication source.
//
userGitLabUserID := "5678"
userGitLab := &user_model.User{
Name: "gitlabuser",
Email: "gitlabuser@example.com",
Passwd: "gitlabuserpassword",
Type: user_model.UserTypeIndividual,
LoginType: auth_model.OAuth2,
LoginSource: gitlab.ID,
LoginName: userGitLabUserID,
}
defer createUser(context.Background(), t, userGitLab)()
//
// A request for user information sent to Goth will return a
// goth.User exactly matching the user created above.
//
defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{
Provider: gitlabName,
UserID: userGitLabUserID,
Email: userGitLab.Email,
}, nil
})()
req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName))
req.AddCookie(&http.Cookie{
Name: "redirect_to",
Value: "/login/oauth/authorize?redirect_uri=https%3A%2F%2Ftranslate.example.org",
Path: "/",
})
resp := MakeRequest(t, req, http.StatusSeeOther)
hasNewSessionCookie := false
sessionCookieName := setting.SessionConfig.CookieName
for _, c := range resp.Result().Cookies() {
if c.Name == sessionCookieName {
hasNewSessionCookie = true
break
}
t.Log("Got cookie", c.Name)
}
assert.True(t, hasNewSessionCookie, "Session cookie %q is missing", sessionCookieName)
assert.Equal(t, "/login/oauth/authorize?redirect_uri=https://translate.example.org", test.RedirectURL(resp))
}
func TestSignUpViaOAuthWithMissingFields(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// enable auto-creation of accounts via OAuth2
enableAutoRegistration := setting.OAuth2Client.EnableAutoRegistration
setting.OAuth2Client.EnableAutoRegistration = true
defer func() {
setting.OAuth2Client.EnableAutoRegistration = enableAutoRegistration
}()
// OAuth2 authentication source GitLab
gitlabName := "gitlab"
addAuthSource(t, authSourcePayloadGitLabCustom(gitlabName))
userGitLabUserID := "5678"
// The Goth User returned by the oauth2 integration is missing
// an email address, so we won't be able to automatically create a local account for it.
defer mockCompleteUserAuth(func(res http.ResponseWriter, req *http.Request) (goth.User, error) {
return goth.User{
Provider: gitlabName,
UserID: userGitLabUserID,
}, nil
})()
req := NewRequest(t, "GET", fmt.Sprintf("/user/oauth2/%s/callback?code=XYZ&state=XYZ", gitlabName))
resp := MakeRequest(t, req, http.StatusSeeOther)
assert.Equal(t, "/user/link_account", test.RedirectURL(resp))
}
func TestOAuth_GrantApplicationOAuth(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequest(t, "GET", "/login/oauth/authorize?client_id=da7da3ba-9a13-4167-856f-3899de0b0138&redirect_uri=a&response_type=code&state=thestate")
ctx := loginUser(t, "user4")
resp := ctx.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertElement(t, "#authorize-app", true)
req = NewRequestWithValues(t, "POST", "/login/oauth/grant", map[string]string{
"_csrf": htmlDoc.GetCSRF(),
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"redirect_uri": "a",
"state": "thestate",
"granted": "false",
})
resp = ctx.MakeRequest(t, req, http.StatusSeeOther)
assert.Contains(t, test.RedirectURL(resp), "error=access_denied&error_description=the+request+is+denied")
}
cherry-pick OIDC changes from gitea (#4724) These are the three conflicted changes from #4716: * https://github.com/go-gitea/gitea/pull/31632 * https://github.com/go-gitea/gitea/pull/31688 * https://github.com/go-gitea/gitea/pull/31706 cc @earl-warren; as per discussion on https://github.com/go-gitea/gitea/pull/31632 this involves a small compatibility break (OIDC introspection requests now require a valid client ID and secret, instead of a valid OIDC token) ## Checklist The [developer guide](https://forgejo.org/docs/next/developer/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [ ] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. <!--start release-notes-assistant--> ## Draft release notes <!--URL:https://codeberg.org/forgejo/forgejo--> - Breaking features - [PR](https://codeberg.org/forgejo/forgejo/pulls/4724): <!--number 4724 --><!--line 0 --><!--description T0lEQyBpbnRlZ3JhdGlvbnMgdGhhdCBQT1NUIHRvIGAvbG9naW4vb2F1dGgvaW50cm9zcGVjdGAgd2l0aG91dCBzZW5kaW5nIEhUVFAgYmFzaWMgYXV0aGVudGljYXRpb24gd2lsbCBub3cgZmFpbCB3aXRoIGEgNDAxIEhUVFAgVW5hdXRob3JpemVkIGVycm9yLiBUbyBmaXggdGhlIGVycm9yLCB0aGUgY2xpZW50IG11c3QgYmVnaW4gc2VuZGluZyBIVFRQIGJhc2ljIGF1dGhlbnRpY2F0aW9uIHdpdGggYSB2YWxpZCBjbGllbnQgSUQgYW5kIHNlY3JldC4gVGhpcyBlbmRwb2ludCB3YXMgcHJldmlvdXNseSBhdXRoZW50aWNhdGVkIHZpYSB0aGUgaW50cm9zcGVjdGlvbiB0b2tlbiBpdHNlbGYsIHdoaWNoIGlzIGxlc3Mgc2VjdXJlLg==-->OIDC integrations that POST to `/login/oauth/introspect` without sending HTTP basic authentication will now fail with a 401 HTTP Unauthorized error. To fix the error, the client must begin sending HTTP basic authentication with a valid client ID and secret. This endpoint was previously authenticated via the introspection token itself, which is less secure.<!--description--> <!--end release-notes-assistant--> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4724 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Shivaram Lingamneni <slingamn@cs.stanford.edu> Co-committed-by: Shivaram Lingamneni <slingamn@cs.stanford.edu>
2024-08-08 09:32:14 +03:00
func TestOAuthIntrospection(t *testing.T) {
defer tests.PrepareTestEnv(t)()
req := NewRequestWithValues(t, "POST", "/login/oauth/access_token", map[string]string{
"grant_type": "authorization_code",
"client_id": "da7da3ba-9a13-4167-856f-3899de0b0138",
"client_secret": "4MK8Na6R55smdCY0WuCCumZ6hjRPnGY5saWVRHHjJiA=",
"redirect_uri": "a",
"code": "authcode",
"code_verifier": "N1Zo9-8Rfwhkt68r1r29ty8YwIraXR8eh_1Qwxg7yQXsonBt",
})
resp := MakeRequest(t, req, http.StatusOK)
type response struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
parsed := new(response)
DecodeJSON(t, resp, parsed)
assert.Greater(t, len(parsed.AccessToken), 10)
assert.Greater(t, len(parsed.RefreshToken), 10)
type introspectResponse struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"`
Username string `json:"username"`
}
// successful request with a valid client_id/client_secret and a valid token
t.Run("successful request with valid token", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
"token": parsed.AccessToken,
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
resp := MakeRequest(t, req, http.StatusOK)
introspectParsed := new(introspectResponse)
DecodeJSON(t, resp, introspectParsed)
assert.True(t, introspectParsed.Active)
assert.Equal(t, "user1", introspectParsed.Username)
})
// successful request with a valid client_id/client_secret, but an invalid token
t.Run("successful request with invalid token", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
"token": "xyzzy",
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpKaUE9")
resp := MakeRequest(t, req, http.StatusOK)
introspectParsed := new(introspectResponse)
DecodeJSON(t, resp, introspectParsed)
assert.False(t, introspectParsed.Active)
})
// unsuccessful request with an invalid client_id/client_secret
t.Run("unsuccessful request due to invalid basic auth", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", "/login/oauth/introspect", map[string]string{
"token": parsed.AccessToken,
})
req.Header.Add("Authorization", "Basic ZGE3ZGEzYmEtOWExMy00MTY3LTg1NmYtMzg5OWRlMGIwMTM4OjRNSzhOYTZSNTVzbWRDWTBXdUNDdW1aNmhqUlBuR1k1c2FXVlJISGpK")
resp := MakeRequest(t, req, http.StatusUnauthorized)
assert.Contains(t, resp.Body.String(), "no valid authorization")
})
}