feat(auth)!: remove support for obsolete OpenID 2.0 draft (#40231)

Remove OpenID 2.0 authentication. The spec is an obsolete draft, major
providers dropped it years ago, and the code keeps needing security
fixes like https://github.com/go-gitea/gitea/pull/39642. OpenID Connect
and OAuth2 are unaffected. Docs: https://gitea.com/gitea/docs/pulls/633.

- The `[openid]` settings, OpenID sign-in, sign-up and account
association are gone
- A migration drops the `user_open_id` table with all user OpenID URIs
- Accounts registered via OpenID have a random password and regain
access via password reset

Fixes: https://github.com/go-gitea/gitea/issues/1310
Fixes: https://github.com/go-gitea/gitea/issues/3521
This commit is contained in:
silverwind
2026-11-06 07:21:19 +00:00
committed by GitHub
parent fd98ab5472
commit 87ebf3b1ba
48 changed files with 59 additions and 1641 deletions

View File

@@ -954,11 +954,6 @@
"path": "github.com/x448/float16/LICENSE",
"licenseText": "MIT License\n\nCopyright (c) 2019 Montgomery Edwards⁴⁴⁸ and Faye Amacker\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
},
{
"name": "github.com/yohcop/openid-go",
"path": "github.com/yohcop/openid-go/LICENSE",
"licenseText": "Copyright 2015 Yohann Coppel\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
},
{
"name": "github.com/yuin/goldmark",
"path": "github.com/yuin/goldmark/LICENSE",

View File

@@ -1721,58 +1721,18 @@ LEVEL = Info
;; - manage_ssh_keys: a user cannot configure ssh keys
;; - manage_gpg_keys: a user cannot configure gpg keys
;; - manage_mfa: a user cannot configure mfa devices
;; - manage_credentials: a user cannot configure emails, passwords, or openid
;; - manage_credentials: a user cannot configure emails or passwords
;USER_DISABLED_FEATURES =
;; Comma separated list of disabled features ONLY if the user has an external login type (eg. LDAP, Oauth, etc.), could be "deletion", "manage_ssh_keys", "manage_gpg_keys", "manage_mfa", "manage_credentials". This setting is independent from `USER_DISABLED_FEATURES` and supplements its behavior.
;; - deletion: a user cannot delete their own account
;; - manage_ssh_keys: a user cannot configure ssh keys
;; - manage_gpg_keys: a user cannot configure gpg keys
;; - manage_mfa: a user cannot configure mfa devices
;; - manage_credentials: a user cannot configure emails, passwords, or openid
;; - manage_credentials: a user cannot configure emails or passwords
;; - change_username: a user cannot change their username
;; - change_full_name: a user cannot change their full name
;;EXTERNAL_USER_DISABLE_FEATURES =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[openid]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; OpenID is an open, standard and decentralized authentication protocol.
;; Your identity is the address of a webpage you provide, which describes
;; how to prove you are in control of that page.
;;
;; For more info: https://en.wikipedia.org/wiki/OpenID
;;
;; Current implementation supports OpenID-2.0
;;
;; Tested to work providers at the time of writing:
;; - Any GNUSocial node (your.hostname.tld/username)
;; - Any SimpleID provider (http://simpleid.koinic.net)
;; - http://openid.org.cn/
;; - openid.stackexchange.com
;; - login.launchpad.net
;; - <username>.livejournal.com
;;
;; Whether to allow sign-in via OpenID
;ENABLE_OPENID_SIGNIN = false
;;
;; Whether to allow registering via OpenID
;;ENABLE_OPENID_SIGNUP = false
;;
;; Allowed URI patterns (POSIX regexp).
;; Space separated.
;; Only these would be allowed if non-blank.
;; Example value: trusted.domain.org trusted.domain.net
;WHITELISTED_URIS =
;;
;; Forbidden URI patterns (POSIX regexp).
;; Space separated.
;; Only used if WHITELISTED_URIS is blank.
;; Example value: loadaverage.org/badguy stackexchange.com/.*spammer
;BLACKLISTED_URIS =
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[oauth2_client]

1
go.mod
View File

@@ -82,7 +82,6 @@ require (
github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.14.0
github.com/wneessen/go-mail v0.8.1
github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.8.6
gitlab.com/gitlab-org/api/client-go/v3 v3.39.1
go.yaml.in/yaml/v4 v4.0.0

16
go.sum
View File

@@ -550,10 +550,7 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yohcop/openid-go v1.0.1 h1:DPRd3iPO5F6O5zX2e62XpVAbPT6wV51cuucH0z9g3js=
github.com/yohcop/openid-go v1.0.1/go.mod h1:b/AvD03P0KHj4yuihb+VtLD6bYYgsy0zqBzPCRjkCNs=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
@@ -592,7 +589,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
@@ -600,7 +596,6 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aI
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -609,8 +604,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
@@ -619,7 +612,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -633,24 +625,17 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
@@ -658,7 +643,6 @@ golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs=
golang.org/x/tools v0.52.0/go.mod h1:ItcP5UNaVGG+Yc4eIYmaFLNJ4soEopkx5Y2d4lcEurs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -442,6 +442,7 @@ func prepareMigrationTasks() []*migration {
newMigration(363, "Add release_id to reaction table", v29.AddReleaseIDToReaction),
newMigration(364, "Add new table project_workflow", v29.AddProjectWorkflow),
newMigration(365, "Normalize legacy repository unit and login source JSON", v29.NormalizeLegacyConfigJSON),
newMigration(366, "Drop user_open_id table", v29.DropUserOpenIDTable),
}
return preparedMigrations
}

View File

@@ -0,0 +1,14 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"context"
"gitea.dev/modelmigration/base"
)
func DropUserOpenIDTable(_ctx context.Context, x base.EngineMigration) error {
return x.DropTables("user_open_id")
}

View File

@@ -0,0 +1,33 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v29
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"github.com/stretchr/testify/require"
)
func TestDropUserOpenIDTable(t *testing.T) {
type UserOpenID struct {
ID int64 `xorm:"pk autoincr"`
}
x, cleanup := migrationtest.PrepareTestEnv(t, 0, new(UserOpenID))
defer cleanup()
if x == nil || t.Failed() {
return
}
exists, err := x.IsTableExist("user_open_id")
require.NoError(t, err)
require.True(t, exists)
require.NoError(t, DropUserOpenIDTable(t.Context(), x))
exists, err = x.IsTableExist("user_open_id")
require.NoError(t, err)
require.False(t, exists)
require.NoError(t, DropUserOpenIDTable(t.Context(), x))
}

View File

@@ -88,8 +88,6 @@ var (
UserWebAuthRemove = define("user:webauth:remove", "Removed WebAuthn key {credential} from user {scope}.")
UserExternalLoginAdd = define("user:externallogin:add", "Added external login {external_id} for user {scope} using provider {provider}.")
UserExternalLoginRemove = define("user:externallogin:remove", "Removed external login from authentication source {auth_source_id} for user {scope}.")
UserOpenIDAdd = define("user:openid:add", "Associated OpenID {openid} to user {scope}.")
UserOpenIDRemove = define("user:openid:remove", "Removed OpenID {openid} from user {scope}.")
UserAccessTokenAdd = define("user:accesstoken:add", "Added access token {token} for user {scope} with scope {token_scope}.")
UserAccessTokenRegenerate = define("user:accesstoken:regenerate", "Regenerated access token {token} of user {scope}.")
UserAccessTokenRemove = define("user:accesstoken:remove", "Removed access token {token} from user {scope}.")

View File

@@ -1,19 +0,0 @@
-
id: 1
uid: 1
uri: https://user1.domain1.tld/
show: false
-
id: 2
uid: 1
uri: http://user1.domain2.tld/
show: true
-
id: 3
uid: 2
uri: https://domain1.tld/user2/
show: true
# DO NOT add more test data in the fixtures, test case should prepare their own test data separately and clearly

View File

@@ -1,127 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"fmt"
"gitea.dev/models/db"
"gitea.dev/modules/util"
"xorm.io/builder"
)
// UserOpenID is the list of all OpenID identities of a user.
// Since this is a middle table, name it OpenID is not suitable, so we ignore the lint here
type UserOpenID struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"`
UID int64 `xorm:"INDEX NOT NULL"`
URI string `xorm:"UNIQUE NOT NULL"`
Show bool `xorm:"DEFAULT false"`
}
func init() {
db.RegisterModel(new(UserOpenID))
}
// GetUserOpenIDs returns all openid addresses that belongs to given user.
func GetUserOpenIDs(ctx context.Context, uid int64) ([]*UserOpenID, error) {
openids := make([]*UserOpenID, 0, 5)
if err := db.GetEngine(ctx).
Where("uid=?", uid).
Asc("id").
Find(&openids); err != nil {
return nil, err
}
return openids, nil
}
// isOpenIDUsed returns true if the openid has been used.
func isOpenIDUsed(ctx context.Context, uri string) (bool, error) {
if len(uri) == 0 {
return true, nil
}
return db.Exist[UserOpenID](ctx, builder.Eq{"uri": uri})
}
// ErrOpenIDAlreadyUsed represents a "OpenIDAlreadyUsed" kind of error.
type ErrOpenIDAlreadyUsed struct {
OpenID string
}
// IsErrOpenIDAlreadyUsed checks if an error is a ErrOpenIDAlreadyUsed.
func IsErrOpenIDAlreadyUsed(err error) bool {
_, ok := err.(ErrOpenIDAlreadyUsed)
return ok
}
func (err ErrOpenIDAlreadyUsed) Error() string {
return fmt.Sprintf("OpenID already in use [oid: %s]", err.OpenID)
}
func (err ErrOpenIDAlreadyUsed) Unwrap() error {
return util.ErrAlreadyExist
}
// AddUserOpenID adds an pre-verified/normalized OpenID URI to given user.
// NOTE: make sure openid.URI is normalized already
func AddUserOpenID(ctx context.Context, openid *UserOpenID) error {
used, err := isOpenIDUsed(ctx, openid.URI)
if err != nil {
return err
} else if used {
return ErrOpenIDAlreadyUsed{openid.URI}
}
return db.Insert(ctx, openid)
}
// GetUserOpenIDByID returns the OpenID with the given ID owned by uid.
func GetUserOpenIDByID(ctx context.Context, id, uid int64) (*UserOpenID, error) {
oid, has, err := db.Get[UserOpenID](ctx, builder.Eq{"id": id, "uid": uid})
if err != nil {
return nil, err
} else if !has {
return nil, util.NewNotExistErrorf("OpenID is unknown")
}
return oid, nil
}
// DeleteUserOpenID deletes an openid address of given user.
func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
var deleted int64
// ask to check UID
address := UserOpenID{
UID: openid.UID,
}
if openid.ID > 0 {
deleted, err = db.GetEngine(ctx).ID(openid.ID).Delete(&address)
} else {
deleted, err = db.GetEngine(ctx).
Where("openid=?", openid.URI).
Delete(&address)
}
if err != nil {
return err
} else if deleted != 1 {
return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
}
// ToggleUserOpenIDVisibility toggles visibility of an openid address of given user.
func ToggleUserOpenIDVisibility(ctx context.Context, id int64, user *User) error {
affected, err := db.GetEngine(ctx).Exec("update `user_open_id` set `show` = not `show` where `id` = ? AND uid = ?", id, user.ID)
if err != nil {
return err
}
if n, _ := affected.RowsAffected(); n != 1 {
return util.NewNotExistErrorf("OpenID is unknown")
}
return nil
}

View File

@@ -1,70 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user_test
import (
"testing"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserOpenIDs(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("GetUserOpenIDs", func(t *testing.T) {
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(1))
if assert.NoError(t, err) && assert.Len(t, oids, 2) {
assert.Equal(t, "https://user1.domain1.tld/", oids[0].URI)
assert.False(t, oids[0].Show)
assert.Equal(t, "http://user1.domain2.tld/", oids[1].URI)
assert.True(t, oids[1].Show)
}
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
if assert.NoError(t, err) && assert.Len(t, oids, 1) {
assert.Equal(t, "https://domain1.tld/user2/", oids[0].URI)
assert.True(t, oids[0].Show)
}
})
t.Run("ToggleUserOpenIDVisibilityRequiresOwnership", func(t *testing.T) {
unauthorizedUser, err := user_model.GetUserByID(t.Context(), int64(2))
require.NoError(t, err)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), int64(1), unauthorizedUser)
require.Error(t, err)
assert.ErrorIs(t, err, util.ErrNotExist)
})
t.Run("ToggleUserOpenIDVisibility", func(t *testing.T) {
user, err := user_model.GetUserByID(t.Context(), int64(2))
require.NoError(t, err)
oids, err := user_model.GetUserOpenIDs(t.Context(), int64(2))
require.NoError(t, err)
require.Len(t, oids, 1)
assert.True(t, oids[0].Show)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
require.NoError(t, err)
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
require.NoError(t, err)
require.Len(t, oids, 1)
assert.False(t, oids[0].Show)
err = user_model.ToggleUserOpenIDVisibility(t.Context(), oids[0].ID, user)
require.NoError(t, err)
oids, err = user_model.GetUserOpenIDs(t.Context(), int64(2))
require.NoError(t, err)
if assert.Len(t, oids, 1) {
assert.True(t, oids[0].Show)
}
})
}

View File

@@ -22,7 +22,6 @@ import (
"gitea.dev/models/auth"
"gitea.dev/models/db"
"gitea.dev/modules/auth/openid"
"gitea.dev/modules/auth/password/hash"
"gitea.dev/modules/base"
"gitea.dev/modules/container"
@@ -1441,32 +1440,6 @@ func GetIndividualUserByLoginSource(ctx context.Context, loginType auth.Type, lo
return user, has, err
}
// GetUserByOpenID returns the user object by given OpenID if exists.
func GetUserByOpenID(ctx context.Context, uri string) (*User, error) {
if len(uri) == 0 {
return nil, ErrUserNotExist{Name: uri}
}
uri, err := openid.Normalize(uri)
if err != nil {
return nil, err
}
log.Trace("Normalized OpenID URI: " + uri)
// Otherwise, check in openid table
oid := &UserOpenID{}
has, err := db.GetEngine(ctx).Where("uri=?", uri).Get(oid)
if err != nil {
return nil, err
}
if has {
return GetUserByID(ctx, oid.UID)
}
return nil, ErrUserNotExist{Name: uri}
}
// GetAdminUser returns the first administrator
func GetAdminUser(ctx context.Context) (*User, error) {
var admin User

View File

@@ -417,25 +417,6 @@ func TestNewUserRedirect(t *testing.T) {
}
}
func TestGetUserByOpenID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
_, err := user_model.GetUserByOpenID(t.Context(), "https://unknown")
if assert.Error(t, err) {
assert.True(t, user_model.IsErrUserNotExist(err))
}
user, err := user_model.GetUserByOpenID(t.Context(), "https://user1.domain1.tld")
if assert.NoError(t, err) {
assert.Equal(t, int64(1), user.ID)
}
user, err = user_model.GetUserByOpenID(t.Context(), "https://domain1.tld/user2/")
if assert.NoError(t, err) {
assert.Equal(t, int64(2), user.ID)
}
}
func TestFollowUser(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())

View File

@@ -1,57 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openid
import (
"sync"
"time"
"github.com/yohcop/openid-go"
)
type timedDiscoveredInfo struct {
info openid.DiscoveredInfo
time time.Time
}
type timedDiscoveryCache struct {
cache map[string]timedDiscoveredInfo
ttl time.Duration
mutex *sync.Mutex
}
func newTimedDiscoveryCache(ttl time.Duration) *timedDiscoveryCache {
return &timedDiscoveryCache{cache: map[string]timedDiscoveredInfo{}, ttl: ttl, mutex: &sync.Mutex{}}
}
func (s *timedDiscoveryCache) Put(id string, info openid.DiscoveredInfo) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.cache[id] = timedDiscoveredInfo{info: info, time: time.Now()}
}
// Delete timed-out cache entries
func (s *timedDiscoveryCache) cleanTimedOut() {
now := time.Now()
for k, e := range s.cache {
diff := now.Sub(e.time)
if diff > s.ttl {
delete(s.cache, k)
}
}
}
func (s *timedDiscoveryCache) Get(id string) openid.DiscoveredInfo {
s.mutex.Lock()
defer s.mutex.Unlock()
// Delete old cached while we are at it.
s.cleanTimedOut()
if info, has := s.cache[id]; has {
return info.info
}
return nil
}

View File

@@ -1,49 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openid
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type testDiscoveredInfo struct{}
func (s *testDiscoveredInfo) ClaimedID() string {
return "claimedID"
}
func (s *testDiscoveredInfo) OpEndpoint() string {
return "opEndpoint"
}
func (s *testDiscoveredInfo) OpLocalID() string {
return "opLocalID"
}
func TestTimedDiscoveryCache(t *testing.T) {
ttl := 50 * time.Millisecond
dc := newTimedDiscoveryCache(ttl)
// Put some initial values
dc.Put("foo", &testDiscoveredInfo{}) // openid.opEndpoint: "a", openid.opLocalID: "b", openid.claimedID: "c"})
// Make sure we can retrieve them
di := dc.Get("foo")
require.NotNil(t, di)
assert.Equal(t, "opEndpoint", di.OpEndpoint())
assert.Equal(t, "opLocalID", di.OpLocalID())
assert.Equal(t, "claimedID", di.ClaimedID())
// Attempt to get a non-existent value
assert.Nil(t, dc.Get("bar"))
// Sleep for a while and try to retrieve again
time.Sleep(ttl * 3 / 2)
assert.Nil(t, dc.Get("foo"))
}

View File

@@ -1,49 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openid
import (
"net/http"
"sync"
"time"
"gitea.dev/modules/egress"
"github.com/yohcop/openid-go"
)
// For the demo, we use in-memory infinite storage nonce and discovery
// cache. In your app, do not use this as it will eat up memory and
// never
// free it. Use your own implementation, on a better database system.
// If you have multiple servers for example, you may need to share at
// least
// the nonceStore between them.
var (
nonceStore = openid.NewSimpleNonceStore()
discoveryCache = newTimedDiscoveryCache(24 * time.Hour)
// openIDInstance keeps user-supplied OpenID identifiers within [security] ALLOWED_HOST_LIST
openIDInstance = sync.OnceValue(func() *openid.OpenID {
return openid.NewOpenID(&http.Client{
Timeout: 30 * time.Second,
Transport: egress.NewSecurityPolicy("openid").NewHTTPTransport(),
})
})
)
// Verify handles response from OpenID provider
func Verify(fullURL string) (id string, err error) {
return openIDInstance().Verify(fullURL, discoveryCache, nonceStore)
}
// Normalize normalizes an OpenID URI
func Normalize(url string) (id string, err error) {
return openid.Normalize(url)
}
// RedirectURL redirects browser
func RedirectURL(id, callbackURL, realm string) (string, error) {
return openIDInstance().RedirectURL(id, callbackURL, realm)
}

View File

@@ -1,29 +0,0 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package openid
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOpenIDDiscoveryBlocksInternalHost(t *testing.T) {
var reached atomic.Bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reached.Store(true)
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
// RedirectURL performs server-side discovery of the identifier URL; a loopback URL
// must be refused at dial time instead of reaching the internal server
_, err := RedirectURL(srv.URL, "http://example.com/callback", "http://example.com/")
require.Error(t, err)
assert.False(t, reached.Load(), "OpenID discovery must not reach an internal/loopback host")
}

View File

@@ -4,7 +4,6 @@
package setting
import (
"regexp"
"runtime"
"strings"
"time"
@@ -82,12 +81,6 @@ var Service = struct {
UserDeleteWithCommentsMaxTime time.Duration
ValidSiteURLSchemes []string
// OpenID settings
EnableOpenIDSignIn bool
EnableOpenIDSignUp bool
OpenIDWhitelist []*regexp.Regexp
OpenIDBlacklist []*regexp.Regexp
// Explore page settings
Explore struct {
RequireSigninView bool `ini:"REQUIRE_SIGNIN_VIEW"`
@@ -257,30 +250,9 @@ func loadServiceFrom(rootCfg ConfigProvider) {
mustMapSetting(rootCfg, "service.explore", &Service.Explore)
loadOpenIDSetting(rootCfg)
loadQosSetting(rootCfg)
}
func loadOpenIDSetting(rootCfg ConfigProvider) {
sec := rootCfg.Section("openid")
Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(false)
Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(false)
pats := sec.Key("WHITELISTED_URIS").Strings(" ")
if len(pats) != 0 {
Service.OpenIDWhitelist = make([]*regexp.Regexp, len(pats))
for i, p := range pats {
Service.OpenIDWhitelist[i] = regexp.MustCompilePOSIX(p)
}
}
pats = sec.Key("BLACKLISTED_URIS").Strings(" ")
if len(pats) != 0 {
Service.OpenIDBlacklist = make([]*regexp.Regexp, len(pats))
for i, p := range pats {
Service.OpenIDBlacklist[i] = regexp.MustCompilePOSIX(p)
}
}
}
func loadQosSetting(rootCfg ConfigProvider) {
sec := rootCfg.Section("qos")
Service.QoS.Enabled = sec.Key("ENABLED").MustBool(false)

View File

@@ -37,7 +37,6 @@ func CommonTemplateContextData() reqctx.ContextData {
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
"EnableSwagger": setting.API.EnableSwagger,
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
"PageStartTime": time.Now(),
"RunModeIsProd": setting.IsProd,

View File

@@ -409,7 +409,6 @@
"auth.twofa_scratch_token_incorrect": "Your scratch code is incorrect.",
"auth.twofa_required": "You must set up two-factor authentication to get access to repositories, or try to log in again.",
"auth.login_userpass": "Sign In",
"auth.login_openid": "OpenID",
"auth.oauth_signup_tab": "Register New Account",
"auth.oauth_signup_title": "Complete New Account",
"auth.oauth_signup_submit": "Complete Account",
@@ -420,12 +419,6 @@
"auth.oauth.signin.error.access_denied": "The authorization request was denied.",
"auth.oauth.signin.error.temporarily_unavailable": "Authorization failed because the authentication server is temporarily unavailable. Please try again later.",
"auth.oauth_callback_unable_auto_reg": "Auto Registration is enabled, but OAuth2 Provider %[1]s returned missing fields: %[2]s, unable to create an account automatically. Please create or link to an account, or contact the site administrator.",
"auth.openid_connect_submit": "Connect",
"auth.openid_connect_title": "Connect to an existing account",
"auth.openid_connect_desc": "The chosen OpenID URI is unknown. Associate it with a new account here.",
"auth.openid_register_title": "Create new account",
"auth.openid_register_desc": "The chosen OpenID URI is unknown. Associate it with a new account here.",
"auth.openid_signin_desc": "Enter your OpenID URI. For example: alice.openid.example.org or https://openid.example.org/alice.",
"auth.disable_forgot_password_mail": "Account recovery is disabled because no email address is set up. Please contact your site administrator.",
"auth.disable_forgot_password_mail_admin": "Account recovery is only available when an email address is set up.",
"auth.email_domain_blacklisted": "You cannot register with your email address.",
@@ -443,7 +436,6 @@
"auth.password_pwned_err": "Could not complete request to HaveIBeenPwned",
"auth.last_admin": "You cannot remove the last admin. There must be at least one admin.",
"auth.signin_passkey": "Sign in with a passkey",
"auth.back_to_sign_in": "Back to Sign In",
"mail.view_it_on": "View it on %s",
"mail.reply": "or reply to this email directly",
"mail.reply_directly": "Reply to this email directly",
@@ -563,7 +555,6 @@
"form.email_been_used": "The email address is already used.",
"form.email_invalid": "The email address is invalid.",
"form.email_domain_is_not_allowed": "The domain of user email address <b>%s</b> conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST. Please ensure your operation is expected.",
"form.openid_been_used": "The OpenID address \"%s\" is already used.",
"form.username_password_incorrect": "Username or password is incorrect.",
"form.password_complexity": "Password does not pass complexity requirements:",
"form.password_lowercase_one": "At least one lowercase character",
@@ -717,7 +708,6 @@
"settings.emails": "Email Addresses",
"settings.manage_emails": "Manage Email Addresses",
"settings.manage_themes": "Select default theme",
"settings.manage_openid": "Manage OpenID Addresses",
"settings.email_desc": "Your primary email address will be used for notifications, password recovery and, provided that it is not hidden, web-based Git operations.",
"settings.theme_desc": "This will be your default theme across the site.",
"settings.theme_colorblindness_help": "Color blindness Theme Support",
@@ -735,21 +725,14 @@
"settings.email_deletion_success": "The email address has been removed.",
"settings.theme_update_success": "Your theme was updated.",
"settings.theme_update_error": "The selected theme does not exist.",
"settings.openid_deletion": "Remove OpenID Address",
"settings.openid_deletion_desc": "Removing this OpenID address from your account will prevent you from signing in with it. Continue?",
"settings.openid_deletion_success": "The OpenID address has been removed.",
"settings.add_new_email": "Add New Email Address",
"settings.add_new_openid": "Add New OpenID URI",
"settings.add_email": "Add Email Address",
"settings.add_openid": "Add OpenID URI",
"settings.add_email_confirmation_sent": "A confirmation email has been sent to \"%s\". Please check your inbox within the next %s to confirm your email address.",
"settings.email_primary_not_found": "The selected email address could not be found.",
"settings.add_email_success": "The new email address has been added.",
"settings.email_preference_set_success": "Email preference has been set successfully.",
"settings.add_openid_success": "The new OpenID address has been added.",
"settings.keep_email_private": "Hide Email Address",
"settings.keep_email_private_popup": "This will hide your email address from your profile, as well as when you make a pull request or edit a file using the web interface. Pushed commits will not be modified. Use %s in commits to associate them with your account.",
"settings.openid_desc": "OpenID lets you delegate authentication to an external provider.",
"settings.manage_ssh_keys": "Manage SSH Keys",
"settings.manage_ssh_principals": "Manage SSH Certificate Principals",
"settings.manage_gpg_keys": "Manage GPG Keys",
@@ -818,8 +801,6 @@
"settings.key_state_desc": "This key has been used in the last 7 days",
"settings.token_state_desc": "This token has been used in the last 7 days",
"settings.principal_state_desc": "This principal has been used in the last 7 days",
"settings.show_openid": "Show on profile",
"settings.hide_openid": "Hide from profile",
"settings.ssh_disabled": "SSH Disabled",
"settings.ssh_signonly": "SSH is currently disabled so these keys are only used for commit signature verification.",
"settings.ssh_externally_managed": "This SSH key is externally managed for this user",
@@ -3358,8 +3339,6 @@
"admin.config.disable_register": "Disable Self-Registration",
"admin.config.allow_only_internal_registration": "Allow Registration Only Through Gitea itself",
"admin.config.allow_only_external_registration": "Allow Registration Only Through External Services",
"admin.config.enable_openid_signup": "Enable OpenID Self-Registration",
"admin.config.enable_openid_signin": "Enable OpenID Sign-In",
"admin.config.show_registration_button": "Show Register Button",
"admin.config.require_sign_in_view": "Require Sign-In to View Pages",
"admin.config.mail_notify": "Enable Email Notifications",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 448" class="svg fontawesome-openid" width="16" height="16" aria-hidden="true"><path d="M271.5 0v384l-68 32C88.5 405.75 0 344.5 0 270.25c0-71.5 82.5-131 191.75-144.25v43c-71.5 12.5-124 53-124 101.25 0 51 58.5 93.25 135.75 103v-340zm167.25 145.5L448 243l-131.25-28.5 36.75-20.75c-19.5-11.5-43.5-20-70-24.75v-43c46.25 5.5 87.75 19.5 120.25 39.25z"/></svg>

Before

Width:  |  Height:  |  Size: 409 B

View File

@@ -56,9 +56,6 @@ type CommonAuthOptions struct {
func prepareCommonAuthPageData(ctx *context.Context, opt CommonAuthOptions) {
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
// for OpenID Connect
ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
if opt.EnableCaptcha {
@@ -230,7 +227,6 @@ func performAutoLoginOAuth2(ctx *context.Context, data *preparedSignInData) bool
// If only 1 OAuth provider is present and other login methods are disabled, redirect to the OAuth provider.
onlySingleOAuth2 := len(data.oauth2Providers) == 1 &&
!setting.Service.EnablePasswordSignInForm &&
!setting.Service.EnableOpenIDSignIn &&
!setting.Service.EnablePasskeyAuth &&
!data.enableSSPI

View File

@@ -17,11 +17,9 @@ import (
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
auth_service "gitea.dev/services/auth"
"gitea.dev/services/auth/source/oauth2"
"gitea.dev/services/contexttest"
"gitea.dev/services/forms"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
@@ -103,10 +101,8 @@ func TestWebAuthOAuth2(t *testing.T) {
t.Run("RedirectSingleProvider", func(t *testing.T) {
enablePassword := &setting.Service.EnablePasswordSignInForm
enableOpenID := &setting.Service.EnableOpenIDSignIn
enablePasskey := &setting.Service.EnablePasskeyAuth
defer test.MockVariableValue(enablePassword, false)()
defer test.MockVariableValue(enableOpenID, false)()
defer test.MockVariableValue(enablePasskey, false)()
testSignIn := func(t *testing.T, link string, expectedCode int, expectedRedirect string) {
@@ -120,14 +116,12 @@ func TestWebAuthOAuth2(t *testing.T) {
testSignIn(t, "/user/login", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source")
testSignIn(t, "/user/login?redirect_to=/", http.StatusSeeOther, "/user/oauth2/dummy+auth%27s%20source?redirect_to=%2F")
*enablePassword, *enableOpenID, *enablePasskey = true, false, false
*enablePassword, *enablePasskey = true, false
testSignIn(t, "/user/login", http.StatusOK, "")
*enablePassword, *enableOpenID, *enablePasskey = false, true, false
testSignIn(t, "/user/login", http.StatusOK, "")
*enablePassword, *enableOpenID, *enablePasskey = false, false, true
*enablePassword, *enablePasskey = false, true
testSignIn(t, "/user/login", http.StatusOK, "")
*enablePassword, *enableOpenID, *enablePasskey = false, false, false
*enablePassword, *enablePasskey = false, false
addOAuth2Source(t, "dummy-auth-source-2", oauth2.Source{})
testSignIn(t, "/user/login", http.StatusOK, "")
})
@@ -187,37 +181,6 @@ func TestWebAuthOAuth2(t *testing.T) {
})
}
func TestOpenIDRequireTwoFactor(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
mockOpt := contexttest.MockContextOption{SessionStore: session.NewMockMemStore("dummy-sid-openid")}
user32 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32}) // has a webauthn credential
ctx, resp := contexttest.MockContext(t, "/user/openid/connect", mockOpt)
openIDRequireTwoFactor(ctx, user32, false, "https://example.com/id")
assert.Equal(t, "/user/webauthn", test.RedirectURL(resp))
unittest.AssertNotExistsBean(t, &user_model.UserOpenID{UID: user32.ID}) // not attached before the key answered
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
ctx, _ = contexttest.MockContext(t, "/user/openid/connect", mockOpt)
openIDRequireTwoFactor(ctx, user2, false, "https://example.com/id")
assert.False(t, ctx.Written())
}
func TestRegisterOpenIDPostRejectsWrongCaptcha(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
defer test.MockVariableValue(&setting.Service.EnableCaptcha, true)()
defer test.MockVariableValue(&setting.Service.CaptchaType, setting.ImageCaptcha)()
sess := session.NewMockMemStore("dummy-sid-openid-register")
require.NoError(t, sess.Set("openid_verified_uri", "https://example.com/openid"))
ctx, _ := contexttest.MockContext(t, "POST /user/openid/register", contexttest.MockContextOption{SessionStore: sess})
contexttest.MockRequestPostForm(ctx.Req, url.Values{"captcha_id": {"unknown"}, "captcha": {"000000"}})
web.SetForm(ctx, &forms.SignUpOpenIDForm{UserName: "openid-captcha-user", Email: "openid-captcha-user@example.com"})
RegisterOpenIDPost(ctx)
assert.Equal(t, true, ctx.Data["Err_Captcha"])
unittest.AssertNotExistsBean(t, &user_model.User{LowerName: "openid-captcha-user"})
}
func TestAutoSignInStaleCookie(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
at, token, err := auth_service.CreateAuthTokenForUserID(t.Context(), 2)

View File

@@ -263,15 +263,10 @@ func LinkAccountPostRegister(ctx *context.Context) {
}
func completePendingLinks(ctx *context.Context, user *user_model.User) error {
if ctx.Session.Get("linkAccount") != nil {
if err := linkAccountFromContext(ctx, user); err != nil {
return err
}
}
return openIDConnectFromContext(ctx, user)
if ctx.Session.Get("linkAccount") == nil {
return nil
}
func linkAccountFromContext(ctx *context.Context, user *user_model.User) error {
linkAccountData := oauth2GetLinkAccountData(ctx)
if linkAccountData == nil {
return errors.New("not in LinkAccount session")

View File

@@ -1,418 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package auth
import (
"errors"
"net/http"
"net/url"
auth_model "gitea.dev/models/auth"
user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/openid"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/auth"
"gitea.dev/services/context"
"gitea.dev/services/forms"
)
const (
tplSignInOpenID templates.TplName = "user/auth/signin_openid"
tplConnectOID templates.TplName = "user/auth/signup_openid_connect"
tplSignUpOID templates.TplName = "user/auth/signup_openid_register"
)
// the OpenID is attached only after the second factor passed, so a stolen password cannot leave one behind
func openIDRequireTwoFactor(ctx *context.Context, u *user_model.User, remember bool, pendingURI string) {
hasTwoFactor, err := auth_model.HasTwoFactorOrWebAuthn(ctx, u.ID)
if err != nil {
ctx.ServerError("HasTwoFactorOrWebAuthn", err)
return
}
if !hasTwoFactor {
return
}
handleTwoFactorRequired(ctx, u, remember, map[string]any{"openidPendingURI": pendingURI})
}
func openIDConnectFromContext(ctx *context.Context, u *user_model.User) error {
uri, _ := ctx.Session.Get("openidPendingURI").(string)
if uri == "" {
return nil
}
if err := ctx.Session.Delete("openidPendingURI"); err != nil {
return err
}
if err := user_model.AddUserOpenID(ctx, &user_model.UserOpenID{UID: u.ID, URI: uri}); err != nil {
if !user_model.IsErrOpenIDAlreadyUsed(err) {
return err
}
ctx.Flash.Error(ctx.Tr("form.openid_been_used", uri))
}
return nil
}
// SignInOpenID render sign in page
func SignInOpenID(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("sign_in")
if ctx.FormString("openid.return_to") != "" {
signInOpenIDVerify(ctx)
return
}
if performAutoLogin(ctx) {
return
}
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLoginOpenID"] = true
ctx.HTML(http.StatusOK, tplSignInOpenID)
}
// Check if the given OpenID URI is allowed by blacklist/whitelist
func allowedOpenIDURI(uri string) (err error) {
// In case a Whitelist is present, URI must be in it
// in order to be accepted
if len(setting.Service.OpenIDWhitelist) != 0 {
for _, pat := range setting.Service.OpenIDWhitelist {
if pat.MatchString(uri) {
return nil // pass
}
}
// must match one of this or be refused
return errors.New("URI not allowed by whitelist")
}
// A blacklist match expliclty forbids
for _, pat := range setting.Service.OpenIDBlacklist {
if pat.MatchString(uri) {
return errors.New("URI forbidden by blacklist")
}
}
return nil
}
// SignInOpenIDPost response for openid sign in request
func SignInOpenIDPost(ctx *context.Context) {
form := web.GetForm[*forms.SignInOpenIDForm](ctx)
ctx.Data["Title"] = ctx.Tr("sign_in")
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsLoginOpenID"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplSignInOpenID)
return
}
id, err := openid.Normalize(form.Openid)
if err != nil {
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form)
return
}
form.Openid = id
log.Trace("OpenID uri: " + id)
err = allowedOpenIDURI(id)
if err != nil {
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &form)
return
}
redirectTo := setting.AppURL + "user/login/openid"
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
if err != nil {
log.Error("Error in OpenID redirect URL: %s, %v", redirectTo, err.Error())
ctx.RenderWithErrDeprecated("Unable to find OpenID provider in "+redirectTo, tplSignInOpenID, &form)
return
}
// Request optional nickname and email info
// NOTE: change to `openid.sreg.required` to require it
url += "&openid.ns.sreg=http%3A%2F%2Fopenid.net%2Fextensions%2Fsreg%2F1.1"
url += "&openid.sreg.optional=nickname%2Cemail"
log.Trace("Form-passed openid-remember: %t", form.Remember)
if err := ctx.Session.Set("openid_signin_remember", form.Remember); err != nil {
log.Error("SignInOpenIDPost: Could not set openid_signin_remember in session: %v", err)
}
if err := ctx.Session.Release(); err != nil {
log.Error("SignInOpenIDPost: Unable to save changes to the session: %v", err)
}
ctx.Redirect(url)
}
// signInOpenIDVerify handles response from OpenID provider
func signInOpenIDVerify(ctx *context.Context) {
log.Trace("Incoming call to: %s", ctx.Req.URL.String())
fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
log.Trace("Full URL: %s", fullURL)
id, err := openid.Verify(fullURL)
if err != nil {
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
log.Trace("Verified ID: %s", id)
/* Now we should seek for the user and log him in, or prompt
* to register if not found */
u, err := user_model.GetUserByOpenID(ctx, id)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
log.Error("signInOpenIDVerify: %v", err)
}
if u != nil {
log.Trace("User exists, logging in")
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
log.Trace("Session stored openid-remember: %t", remember)
openIDRequireTwoFactor(ctx, u, remember, "")
if ctx.Written() {
return
}
handleSignIn(ctx, u, remember)
return
}
log.Trace("User with openid: %s does not exist, should connect or register", id)
parsedURL, err := url.Parse(fullURL)
if err != nil {
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
values, err := url.ParseQuery(parsedURL.RawQuery)
if err != nil {
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
email := values.Get("openid.sreg.email")
nickname := values.Get("openid.sreg.nickname")
log.Trace("User has email=%s and nickname=%s", email, nickname)
if email != "" {
u, err = user_model.GetUserByEmail(ctx, email)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
log.Error("signInOpenIDVerify: %v", err)
}
if u != nil {
log.Trace("Local user %s has OpenID provided email %s", u.LowerName, email)
}
}
if u == nil && nickname != "" {
u, _ = user_model.GetUserByName(ctx, nickname)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
ctx.RenderWithErrDeprecated(err.Error(), tplSignInOpenID, &forms.SignInOpenIDForm{
Openid: id,
})
return
}
}
if u != nil {
log.Trace("Local user %s has OpenID provided nickname %s", u.LowerName, nickname)
}
}
if u != nil {
nickname = u.LowerName
}
if err := regenerateSession(ctx, map[string]any{
"openid_verified_uri": id,
"openid_determined_email": email,
"openid_determined_username": nickname,
}); err != nil {
ctx.ServerError("updateSession", err)
return
}
if u != nil || !setting.Service.EnableOpenIDSignUp || setting.Service.AllowOnlyInternalRegistration {
ctx.Redirect(setting.AppSubURL + "/user/openid/connect")
} else {
ctx.Redirect(setting.AppSubURL + "/user/openid/register")
}
}
func prepareConnectOpenIDPageData(ctx *context.Context) (oid string) {
oid, _ = ctx.Session.Get("openid_verified_uri").(string)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return ""
}
ctx.Data["Title"] = "OpenID connect"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDConnect"] = true
ctx.Data["OpenID"] = oid
prepareCommonAuthPageData(ctx, CommonAuthOptions{EnableCaptcha: false})
return oid
}
// ConnectOpenID shows a form to connect an OpenID URI to an existing account
func ConnectOpenID(ctx *context.Context) {
oid := prepareConnectOpenIDPageData(ctx)
if oid == "" {
return
}
userName, _ := ctx.Session.Get("openid_determined_username").(string)
if userName != "" {
ctx.Data["user_name"] = userName
}
ctx.HTML(http.StatusOK, tplConnectOID)
}
// ConnectOpenIDPost handles submission of a form to connect an OpenID URI to an existing account
func ConnectOpenIDPost(ctx *context.Context) {
form := web.GetForm[*forms.ConnectOpenIDForm](ctx)
oid := prepareConnectOpenIDPageData(ctx)
if oid == "" {
return
}
u, _, err := auth.UserSignIn(ctx, form.UserName, form.Password)
if err != nil {
handleSignInError(ctx, form.UserName, &form, tplConnectOID, "ConnectOpenIDPost", err)
return
}
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
openIDRequireTwoFactor(ctx, u, remember, oid)
if ctx.Written() {
return
}
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
if err := user_model.AddUserOpenID(ctx, userOID); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplConnectOID, &form)
return
}
ctx.ServerError("AddUserOpenID", err)
return
}
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
handleSignIn(ctx, u, remember)
}
func prepareRegisterOpenIDPageData(ctx *context.Context) (oid string) {
oid, _ = ctx.Session.Get("openid_verified_uri").(string)
if oid == "" {
ctx.Redirect(setting.AppSubURL + "/user/login/openid")
return ""
}
ctx.Data["Title"] = "OpenID signup"
ctx.Data["PageIsSignIn"] = true
ctx.Data["PageIsOpenIDRegister"] = true
ctx.Data["OpenID"] = oid
prepareCommonAuthPageData(ctx, CommonAuthOptions{
EnableCaptcha: setting.Service.EnableCaptcha,
})
return oid
}
// RegisterOpenID shows a form to create a new user authenticated via an OpenID URI
func RegisterOpenID(ctx *context.Context) {
oid := prepareRegisterOpenIDPageData(ctx)
if oid == "" {
return
}
userName, _ := ctx.Session.Get("openid_determined_username").(string)
if userName != "" {
ctx.Data["user_name"] = userName
}
email, _ := ctx.Session.Get("openid_determined_email").(string)
if email != "" {
ctx.Data["email"] = email
}
ctx.HTML(http.StatusOK, tplSignUpOID)
}
// RegisterOpenIDPost handles submission of a form to create a new user authenticated via an OpenID URI
func RegisterOpenIDPost(ctx *context.Context) {
oid := prepareRegisterOpenIDPageData(ctx)
if oid == "" {
return
}
form := web.GetForm[*forms.SignUpOpenIDForm](ctx)
if setting.Service.AllowOnlyInternalRegistration {
ctx.HTTPError(http.StatusForbidden)
return
}
if setting.Service.EnableCaptcha {
if err := ctx.Req.ParseForm(); err != nil {
ctx.ServerError("", err)
return
}
if !context.VerifyCaptcha(ctx, tplSignUpOID, form) {
return
}
}
length := max(setting.MinPasswordLength, 256)
password := util.CryptoRandomString(int64(length))
u := &user_model.User{
Name: form.UserName,
Email: form.Email,
Passwd: password,
}
if !createUserInContext(ctx, tplSignUpOID, form, u, nil, nil) {
// error already handled
return
}
// add OpenID for the user
userOID := &user_model.UserOpenID{UID: u.ID, URI: oid}
if err := user_model.AddUserOpenID(ctx, userOID); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", oid), tplSignUpOID, &form)
return
}
ctx.ServerError("AddUserOpenID", err)
return
}
if !handleUserCreated(ctx, u, nil) {
// error already handled
return
}
remember, _ := ctx.Session.Get("openid_signin_remember").(bool)
log.Trace("Session stored openid-remember: %t", remember)
handleSignIn(ctx, u, remember)
}

View File

@@ -31,13 +31,6 @@ func prepareContextForProfileBigAvatar(ctx *context.Context) {
if setting.Service.UserLocationMapURL != "" {
ctx.Data["ContextUserLocationMapURL"] = setting.Service.UserLocationMapURL + url.QueryEscape(ctx.ContextUser.Location)
}
// Show OpenID URIs
openIDs, err := user_model.GetUserOpenIDs(ctx, ctx.ContextUser.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = openIDs
if len(ctx.ContextUser.Description) != 0 {
content, err := markdown.RenderString(markup.NewRenderContext(ctx).WithMetas(markup.ComposeSimpleDocumentMetas()), ctx.ContextUser.Description)
if err != nil {

View File

@@ -1,180 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"errors"
"fmt"
"net/http"
"time"
audit_model "gitea.dev/models/audit"
user_model "gitea.dev/models/user"
"gitea.dev/modules/auth/openid"
"gitea.dev/modules/base"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/modules/web"
"gitea.dev/services/audit"
"gitea.dev/services/context"
"gitea.dev/services/forms"
)
const openIDAssociationCodeMinutes = 10
// OpenIDPost response for change user's openid
func OpenIDPost(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
form := web.GetForm[*forms.AddOpenIDForm](ctx)
ctx.Data["Title"] = ctx.Tr("settings_title")
ctx.Data["PageIsSettingsSecurity"] = true
if ctx.HasError() {
loadSecurityData(ctx)
ctx.HTML(http.StatusOK, tplSettingsSecurity)
return
}
// WARNING: specifying a wrong OpenID here could lock
// a user out of her account, would be better to
// verify/confirm the new OpenID before storing it
// Also, consider allowing for multiple OpenID URIs
id, err := openid.Normalize(form.Openid)
if err != nil {
loadSecurityData(ctx)
ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &form)
return
}
form.Openid = id
log.Trace("Normalized id: " + id)
oids, err := user_model.GetUserOpenIDs(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = oids
// Check that the OpenID is not already used
for _, obj := range oids {
if obj.URI == id {
loadSecurityData(ctx)
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form)
return
}
}
state := base.CreateTimeLimitCode(fmt.Sprintf("openid_association:%d", ctx.Doer.ID), openIDAssociationCodeMinutes, time.Now(), nil)
redirectTo := setting.AppURL + "user/settings/security?state=" + state
url, err := openid.RedirectURL(id, redirectTo, setting.AppURL)
if err != nil {
loadSecurityData(ctx)
ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &form)
return
}
ctx.Redirect(url)
}
func settingsOpenIDVerify(ctx *context.Context) {
if !base.VerifyTimeLimitCode(time.Now(), fmt.Sprintf("openid_association:%d", ctx.Doer.ID), openIDAssociationCodeMinutes, ctx.FormString("state")) {
ctx.HTTPError(http.StatusForbidden)
return
}
log.Trace("Incoming call to: " + ctx.Req.URL.String())
fullURL := setting.AppURL + ctx.Req.URL.String()[1:]
log.Trace("Full URL: " + fullURL)
id, err := openid.Verify(fullURL)
if err != nil {
ctx.RenderWithErrDeprecated(err.Error(), tplSettingsSecurity, &forms.AddOpenIDForm{
Openid: id,
})
return
}
log.Trace("Verified ID: " + id)
oid := &user_model.UserOpenID{UID: ctx.Doer.ID, URI: id}
if err = user_model.AddUserOpenID(ctx, oid); err != nil {
if user_model.IsErrOpenIDAlreadyUsed(err) {
ctx.RenderWithErrDeprecated(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &forms.AddOpenIDForm{Openid: id})
return
}
ctx.ServerError("AddUserOpenID", err)
return
}
log.Trace("Associated OpenID %s to user %s", id, ctx.Doer.Name)
audit.Record(ctx, audit_model.UserOpenIDAdd, ctx.Doer, "openid", oid.URI)
ctx.Flash.Success(ctx.Tr("settings.add_openid_success"))
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}
// DeleteOpenID response for delete user's openid
func DeleteOpenID(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
oid, err := user_model.GetUserOpenIDByID(ctx, ctx.FormInt64("id"), ctx.Doer.ID)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("GetUserOpenIDByID", err)
}
return
}
if err := user_model.DeleteUserOpenID(ctx, oid); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("DeleteUserOpenID", err)
}
return
}
audit.Record(ctx, audit_model.UserOpenIDRemove, ctx.Doer, "openid", oid.URI)
log.Trace("OpenID address deleted: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success"))
ctx.JSONRedirect(setting.AppSubURL + "/user/settings/security")
}
// ToggleOpenIDVisibility response for toggle visibility of user's openid
func ToggleOpenIDVisibility(ctx *context.Context) {
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureManageCredentials) {
ctx.HTTPError(http.StatusNotFound)
return
}
if err := user_model.ToggleUserOpenIDVisibility(ctx, ctx.FormInt64("id"), ctx.Doer); err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.HTTPError(http.StatusNotFound)
} else {
ctx.ServerError("ToggleUserOpenIDVisibility", err)
}
return
}
ctx.Redirect(setting.AppSubURL + "/user/settings/security")
}

View File

@@ -1,113 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package security
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/base"
"gitea.dev/modules/setting"
"gitea.dev/modules/test"
"gitea.dev/modules/web"
"gitea.dev/services/context"
"gitea.dev/services/contexttest"
"gitea.dev/services/forms"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOpenIDAssociation(t *testing.T) {
unittest.PrepareTestEnv(t)
defer test.MockVariableValue(&setting.AppURL, "https://gitea.example/")()
defer test.MockVariableValue(&setting.AppSubURL, "")()
defer test.MockVariableValue(&setting.Security.AllowedHostList, "127.0.0.1:*")()
provider := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
assert.Equal(t, "check_authentication", r.FormValue("openid.mode"))
w.Header().Set("Content-Type", "text/plain")
fmt.Fprint(w, "ns:http://specs.openid.net/auth/2.0\nis_valid:true\n")
return
}
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `<html><head><link rel="openid2.provider" href="http://%s/provider"></head></html>`, r.Host)
}))
defer provider.Close()
identity := provider.URL + "/identity"
ctx, resp := contexttest.MockContext(t, "POST /user/settings/security/openid")
contexttest.LoadUser(t, ctx, 2)
web.SetForm(ctx, &forms.AddOpenIDForm{Openid: identity})
OpenIDPost(ctx)
require.Equal(t, http.StatusSeeOther, resp.Code)
redirect, err := url.Parse(resp.Header().Get("Location"))
require.NoError(t, err)
callback, err := url.Parse(redirect.Query().Get("openid.return_to"))
require.NoError(t, err)
state := callback.Query().Get("state")
require.NotEmpty(t, state)
params := url.Values{
"openid.ns": {"http://specs.openid.net/auth/2.0"},
"openid.mode": {"id_res"},
"openid.op_endpoint": {provider.URL + "/provider"},
"openid.claimed_id": {identity},
"openid.identity": {identity},
"openid.return_to": {callback.String()},
"openid.response_nonce": {time.Now().UTC().Format("2006-01-02T15:04:05Z") + "association"},
"openid.assoc_handle": {"test-association"},
"openid.signed": {"op_endpoint,claimed_id,identity,return_to,response_nonce,assoc_handle"},
"openid.sig": {"test-signature"},
}
for _, tc := range []struct {
name string
uid int64
state string
}{
{"unsolicited", 2, ""},
{"wrong state", 2, "wrong"},
{"other user", 1, state},
{"expired", 2, base.CreateTimeLimitCode("openid_association:2", openIDAssociationCodeMinutes, time.Now().Add(-time.Hour), nil)},
{"other purpose", 2, base.CreateTimeLimitCode("reset_password:2", openIDAssociationCodeMinutes, time.Now(), nil)},
} {
t.Run(tc.name, func(t *testing.T) {
params.Set("state", tc.state)
ctx, resp := contexttest.MockContext(t, "GET /user/settings/security?"+params.Encode())
contexttest.LoadUser(t, ctx, tc.uid)
Security(ctx)
assert.Equal(t, http.StatusForbidden, resp.Code)
unittest.AssertNotExistsBean(t, &user_model.UserOpenID{URI: identity})
})
}
params.Set("state", state)
ctx, resp = contexttest.MockContext(t, "GET /user/settings/security?"+params.Encode())
contexttest.LoadUser(t, ctx, 2)
Security(ctx)
require.Equal(t, http.StatusSeeOther, resp.Code)
unittest.AssertExistsAndLoadBean(t, &user_model.UserOpenID{UID: 2, URI: identity})
ctx, resp = contexttest.MockContext(t, "GET /user/settings/security?"+params.Encode())
contexttest.LoadUser(t, ctx, 2)
Security(ctx)
assert.Equal(t, http.StatusOK, resp.Code)
assert.Equal(t, "Nonce already used", ctx.Flash.ErrorMsg)
}
func TestOpenIDReturnsNotFoundForOtherUsersAddress(t *testing.T) {
unittest.PrepareTestEnv(t)
for _, handler := range []func(*context.Context){DeleteOpenID, ToggleOpenIDVisibility} {
ctx, _ := contexttest.MockContext(t, "POST /user/settings/security?id=1")
contexttest.LoadUser(t, ctx, 2)
handler(ctx)
assert.Equal(t, http.StatusNotFound, ctx.Resp.WrittenStatus())
}
}

View File

@@ -36,11 +36,6 @@ func Security(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.security")
ctx.Data["PageIsSettingsSecurity"] = true
if ctx.FormString("openid.return_to") != "" {
settingsOpenIDVerify(ctx)
return
}
loadSecurityData(ctx)
ctx.HTML(http.StatusOK, tplSettingsSecurity)
@@ -153,11 +148,4 @@ func loadSecurityData(ctx *context.Context) {
ctx.Data["OrderedOAuth2Names"] = orderedOAuth2Names
ctx.Data["OAuth2Providers"] = oauth2Providers
openid, err := user_model.GetUserOpenIDs(ctx, ctx.Doer.ID)
if err != nil {
ctx.ServerError("GetUserOpenIDs", err)
return
}
ctx.Data["OpenIDs"] = openid
}

View File

@@ -358,20 +358,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
// middleware: only apply CrossOriginProtection
crossOriginProtect := verifyAuthWithOptionsWeb(&common.VerifyOptions{DisableCrossOriginProtection: false})
openIDSignInEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignIn {
ctx.HTTPError(http.StatusForbidden)
return
}
}
openIDSignUpEnabled := func(ctx *context.Context) {
if !setting.Service.EnableOpenIDSignUp {
ctx.HTTPError(http.StatusForbidden)
return
}
}
oauth2Enabled := func(ctx *context.Context) {
if !setting.OAuth2.Enabled {
ctx.HTTPError(http.StatusForbidden)
@@ -579,21 +565,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/user/login", auth.SignIn)
m.Group("/user", func() {
m.Post("/login", web.Bind[*forms.SignInForm](), auth.SignInPost)
m.Group("", func() {
m.Combo("/login/openid").
Get(auth.SignInOpenID).
Post(web.Bind[*forms.SignInOpenIDForm](), auth.SignInOpenIDPost)
}, openIDSignInEnabled)
m.Group("/openid", func() {
m.Combo("/connect").
Get(auth.ConnectOpenID).
Post(web.Bind[*forms.ConnectOpenIDForm](), auth.ConnectOpenIDPost)
m.Group("/register", func() {
m.Combo("").
Get(auth.RegisterOpenID, openIDSignUpEnabled).
Post(web.Bind[*forms.SignUpOpenIDForm](), auth.RegisterOpenIDPost)
}, openIDSignUpEnabled)
}, openIDSignInEnabled)
m.Get("/sign_up", auth.SignUp)
m.Post("/sign_up", web.Bind[*forms.RegisterForm](), auth.SignUpPost)
m.Get("/link_account", auth.LinkAccount)
@@ -671,11 +642,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/rename", security.WebauthnRename)
m.Post("/delete", security.WebauthnDelete)
})
m.Group("/openid", func() {
m.Post("", web.Bind[*forms.AddOpenIDForm](), security.OpenIDPost)
m.Post("/delete", security.DeleteOpenID)
m.Post("/toggle_visibility", security.ToggleOpenIDVisibility)
}, openIDSignInEnabled)
m.Post("/account_link", security.DeleteAccountLink)
})

View File

@@ -61,14 +61,9 @@ func (s *Session) Verify(req *http.Request, w http.ResponseWriter, store DataSto
}
func ClearSessionKeysForSignIn(sess SessionStore) {
_ = sess.Delete("openid_verified_uri")
_ = sess.Delete("openid_signin_remember")
_ = sess.Delete("openid_determined_email")
_ = sess.Delete("openid_determined_username")
_ = sess.Delete("twofaUid")
_ = sess.Delete("twofaRemember")
_ = sess.Delete("webauthnAssertion")
_ = sess.Delete("linkAccount")
_ = sess.Delete("linkAccountData")
_ = sess.Delete("openidPendingURI")
}

View File

@@ -17,7 +17,6 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/log"
"gitea.dev/modules/optional"
"gitea.dev/modules/setting"
"gitea.dev/modules/templates"
"gitea.dev/services/audit"
"gitea.dev/services/auth/source/sspi"
@@ -87,7 +86,6 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore,
store.GetData()["Flash"] = map[string]string{
"ErrorMsg": err.Error(),
}
store.GetData()["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn
store.GetData()["EnableSSPI"] = true
// in this case, the Verify function is called in Gitea's web context
// FIXME: it doesn't look good to render the page here, why not redirect?

View File

@@ -206,12 +206,6 @@ type ChangePasswordForm struct {
Retype string `form:"retype"`
}
// AddOpenIDForm is for changing openid uri
type AddOpenIDForm struct {
middleware.FormDefaultValidator
Openid string `binding:"Required;MaxSize(256)"`
}
// AddKeyForm form for adding SSH/GPG key
type AddKeyForm struct {
middleware.FormDefaultValidator

View File

@@ -1,27 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forms
import "gitea.dev/modules/web/middleware"
// SignInOpenIDForm form for signing in with OpenID
type SignInOpenIDForm struct {
middleware.FormDefaultValidator
Openid string `binding:"Required;MaxSize(256)"`
Remember bool
}
// SignUpOpenIDForm form for signin up with OpenID
type SignUpOpenIDForm struct {
middleware.FormDefaultValidator
UserName string `binding:"Required;Username;MaxSize(40)"`
Email string `binding:"Required;Email;MaxSize(254)"`
}
// ConnectOpenIDForm form for connecting an existing account to an OpenID URI
type ConnectOpenIDForm struct {
middleware.FormDefaultValidator
UserName string `binding:"Required;MaxSize(254)"`
Password string `binding:"Required;MaxSize(255)"`
}

View File

@@ -82,7 +82,6 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
&activities_model.Action{UserID: u.ID},
&issues_model.IssueUser{UID: u.ID},
&user_model.EmailAddress{UID: u.ID},
&user_model.UserOpenID{UID: u.ID},
&issues_model.Reaction{UserID: u.ID},
&organization.TeamUser{UID: u.ID},
&issues_model.Stopwatch{UserID: u.ID},

View File

@@ -133,10 +133,6 @@
<dd>{{svg (Iif .Service.AllowOnlyExternalRegistration "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.show_registration_button"}}</dt>
<dd>{{svg (Iif .Service.ShowRegistrationButton "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signup"}}</dt>
<dd>{{svg (Iif .Service.EnableOpenIDSignUp "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}</dt>
<dd>{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}</dt>
<dd>{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}</dd>
<dt>{{ctx.Locale.Tr "admin.config.mail_notify"}}</dt>

View File

@@ -78,15 +78,6 @@
</div>
{{end}}
{{range .OpenIDs}}
{{if .Show}}
<div class="item">
{{svg "fontawesome-openid"}}
<a target="_blank" href="{{.URI}}">{{.URI}}</a>
</div>
{{end}}
{{end}}
<div class="item">{{svg "octicon-calendar"}} <span>{{ctx.Locale.Tr "user.joined_on" (DateUtils.AbsoluteShort .ContextUser.CreatedUnix)}}</span></div>
{{if and .Orgs .HasOrgsVisible}}

View File

@@ -4,11 +4,6 @@
{{$provider.IconHTML 24}} {{ctx.Locale.Tr "sign_in_with_provider" $provider.DisplayName}}
</a>
{{end}}
{{if .EnableOpenIDSignIn}}
<a class="ui button external-login-link tw-gap-3" data-require-appurl-check="true" rel="nofollow" href="{{AppSubUrl}}/user/login/openid">
{{svg "fontawesome-openid" 24}} {{ctx.Locale.Tr "sign_in_with_provider" "OpenID"}}
</a>
{{end}}
{{if .EnableSSPI}}
<a class="ui button external-login-link tw-gap-3" rel="nofollow" href="{{AppSubUrl}}/user/login?auth_with_sspi=1">
{{svg "fontawesome-windows" 24}} Windows SSPI

View File

@@ -49,7 +49,7 @@
{{end}}
</form>
{{end}}{{/*end if .EnablePasswordSignInForm*/}}
{{$showExternalAuthMethods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
{{$showExternalAuthMethods := or .OAuth2Providers .EnableSSPI}}
{{if and $showExternalAuthMethods .EnablePasswordSignInForm}}
<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
{{end}}

View File

@@ -1,50 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content user signin openid">
<div class="ui middle very relaxed page grid">
<div class="column tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
<a href="{{AppSubUrl}}/user/login" class="tw-mx-auto">
<img width="100" height="100" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}">
</a>
<div class="ui container fluid">
{{template "base/alert" .}}
<h4 class="ui top attached header center">
{{svg "fontawesome-openid"}}
OpenID
</h4>
<div class="ui attached segment">
<form class="ui form tw-m-auto" action="{{.Link}}" method="post">
<div class="inline field">
{{ctx.Locale.Tr "auth.openid_signin_desc"}}
</div>
<div class="required field {{if .Err_OpenID}}error{{end}}">
<label for="openid">
{{svg "fontawesome-openid"}}
OpenID URI
</label>
<input id="openid" name="openid" value="{{.openid}}" autofocus required>
</div>
<div class="inline field">
<div class="ui checkbox">
<label>{{ctx.Locale.Tr "auth.remember_me"}}</label>
<input name="remember" type="checkbox">
</div>
</div>
<div class="inline field">
<button type="submit" class="ui primary button tw-w-full">{{ctx.Locale.Tr "sign_in"}}</button>
</div>
</form>
</div>
</div>
<div class="ui container fluid">
{{template "user/auth/webauthn_error" .}}
<div class="ui attached segment header top tw-flex tw-flex-col tw-items-center">
<a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.back_to_sign_in"}}</a>
</div>
</div>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@@ -49,7 +49,7 @@
</button>
</div>
{{end}}
{{$showExternalAuthMethods := or .OAuth2Providers .EnableOpenIDSignIn .EnableSSPI}}
{{$showExternalAuthMethods := or .OAuth2Providers .EnableSSPI}}
{{if $showExternalAuthMethods}}
<div class="divider divider-text">{{ctx.Locale.Tr "sign_in_or"}}</div>
{{template "user/auth/external_auth_methods" .}}

View File

@@ -1,35 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content user signup">
{{template "user/auth/signup_openid_navbar" .}}
<div class="ui container medium-width">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "auth.openid_connect_title"}}
</h4>
<div class="ui attached segment">
<form class="ui form left-right-form" action="{{.Link}}" method="post">
<div class="inline field">
<span class="help">{{ctx.Locale.Tr "auth.openid_connect_desc"}}</span>
</div>
<div class="required inline field {{if .Err_UserName}}error{{end}}">
<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
</div>
<div class="required inline field {{if .Err_Password}}error{{end}}">
<label for="password">{{ctx.Locale.Tr "password"}}</label>
<input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required>
</div>
<div class="inline field">
<label for="openid">OpenID URI</label>
<input id="openid" value="{{.OpenID}}" readonly>
</div>
<div class="inline field">
<label></label>
<button type="submit" class="ui primary button">{{ctx.Locale.Tr "auth.openid_connect_submit"}}</button>
<a href="{{AppSubUrl}}/user/forgot_password">{{ctx.Locale.Tr "auth.forgot_password"}}</a>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@@ -1,12 +0,0 @@
<overflow-menu class="ui secondary pointing tabular top attached borderless menu secondary-nav">
<div class="overflow-menu-items tw-justify-center">
<a class="{{if .PageIsOpenIDConnect}}active {{end}}item" href="{{AppSubUrl}}/user/openid/connect">
{{ctx.Locale.Tr "auth.openid_connect_title"}}
</a>
{{if and .EnableOpenIDSignUp (not .AllowOnlyInternalRegistration)}}
<a class="{{if .PageIsOpenIDRegister}}active {{end}}item" href="{{AppSubUrl}}/user/openid/register">
{{ctx.Locale.Tr "auth.openid_register_title"}}
</a>
{{end}}
</div>
</overflow-menu>

View File

@@ -1,36 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content user signup">
{{template "user/auth/signup_openid_navbar" .}}
<div class="ui container">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "auth.openid_register_title"}}
</h4>
<div class="ui attached segment">
<p>
{{ctx.Locale.Tr "auth.openid_register_desc"}}
</p>
<form class="ui form" action="{{.Link}}" method="post">
<div class="required field {{if .Err_UserName}}error{{end}}">
<label for="user_name">{{ctx.Locale.Tr "username"}}</label>
<input id="user_name" type="text" name="user_name" value="{{.user_name}}" autofocus required>
</div>
<div class="required field {{if .Err_Email}}error{{end}}">
<label for="email">{{ctx.Locale.Tr "email"}}</label>
<input id="email" name="email" type="email" value="{{.email}}" required>
</div>
{{template "user/auth/captcha" .}}
<div class="field">
<label for="openid">OpenID URI</label>
<input id="openid" value="{{.OpenID}}" readonly>
</div>
<div class="inline field">
<button type="submit" class="ui primary button">{{ctx.Locale.Tr "auth.create_new_account"}}</button>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

View File

@@ -1,61 +0,0 @@
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.manage_openid"}}
</h4>
<div class="ui attached segment">
<div class="flex-divided-list items-with-main">
<div class="item">
{{ctx.Locale.Tr "settings.openid_desc"}}
</div>
{{range .OpenIDs}}
<div class="item tw-items-center">
<div class="item-leading">
{{svg "fontawesome-openid" 20}}
</div>
<div class="item-main">
<div class="item-title">{{.URI}}</div>
</div>
<div class="item-trailing">
<form action="{{AppSubUrl}}/user/settings/security/openid/toggle_visibility" method="post">
<input name="id" type="hidden" value="{{.ID}}">
{{if .Show}}
<button type="submit" class="ui tiny button">
{{svg "octicon-eye" 16 "icon"}}
{{ctx.Locale.Tr "settings.hide_openid"}}
</button>
{{else}}
<button type="submit" class="ui tiny button">
{{svg "octicon-eye-closed" 16 "icon"}}
{{ctx.Locale.Tr "settings.show_openid"}}
</button>
{{end}}
</form>
<button type="button" class="ui red tiny button link-action" data-modal-confirm="#delete-openid" data-url="{{AppSubUrl}}/user/settings/security/openid/delete?id={{.ID}}">
{{ctx.Locale.Tr "remove"}}
</button>
</div>
</div>
{{end}}
</div>
</div>
<div class="ui bottom attached segment">
<form class="ui form" action="{{AppSubUrl}}/user/settings/security/openid" method="post">
<div class="required field {{if .Err_OpenID}}error{{end}}">
<label for="openid">{{ctx.Locale.Tr "settings.add_new_openid"}}</label>
<input id="openid" name="openid" type="text" required>
</div>
<button type="submit" class="ui primary button">
{{ctx.Locale.Tr "settings.add_openid"}}
</button>
</form>
<dialog class="modal-dialog" id="delete-openid">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.openid_deletion"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "settings.openid_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</dialog>
</div>

View File

@@ -7,9 +7,6 @@
{{end}}
{{if not ($.UserDisabledFeatures.Contains "manage_credentials")}}
{{template "user/settings/security/accountlinks" .}}
{{if .EnableOpenIDSignIn}}
{{template "user/settings/security/openid" .}}
{{end}}
{{end}}
</div>
{{end}}

View File

@@ -1,3 +0,0 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="448" height="448" viewBox="0 0 448 448">
<path d="M271.5 0v384l-68 32c-115-10.25-203.5-71.5-203.5-145.75 0-71.5 82.5-131 191.75-144.25v43c-71.5 12.5-124 53-124 101.25 0 51 58.5 93.25 135.75 103v0-340zM438.75 145.5l9.25 97.5-131.25-28.5 36.75-20.75c-19.5-11.5-43.5-20-70-24.75v-43c46.25 5.5 87.75 19.5 120.25 39.25z"></path>
</svg>

Before

Width:  |  Height:  |  Size: 393 B